From c420f2cd6cd05303b1faf6972d11c032513d9447 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:32:52 +0200 Subject: [PATCH 0001/1360] docs: add a bug report form and issue chooser links --- .github/ISSUE_TEMPLATE/bug_report.yml | 58 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++++ 2 files changed, 66 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..c42f7a1a17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Bug report +description: A title, homebrew or test stops, crashes or renders wrongly +labels: [bug] +body: + - type: markdown + attributes: + value: | + Search open and closed issues and pull requests for the error text first, and comment there if it is already reported. + Game bring-up progress belongs in [Discussions](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), one thread per title. + - type: input + id: summary + attributes: + label: What happens + description: One sentence, with the exact error text if there is one. + validations: + required: true + - type: input + id: title + attributes: + label: Title, test or build step + description: Title ID and content version (for example PPSA99666 01.001.001), a homebrew with its source, the ctest name, or the build step that fails. + validations: + required: true + - type: input + id: commit + attributes: + label: AnyPS5 commit + description: The `main` commit that was built, or the release that was used, and any pull requests merged on top. + validations: + required: true + - type: input + id: system + attributes: + label: System + description: OS, GPU and driver version, and the compiler if you built it yourself. + placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08) + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: The commands that were run, environment variables included, and how long it takes to stop. + validations: + required: true + - type: textarea + id: log + attributes: + label: Log + description: The lines around the stop. + render: text + validations: + required: true + - type: textarea + id: notes + attributes: + label: What was checked + description: Other devices or commits tried, and whether it is the same every run. Attach the full log and screenshots here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..789cbc7d74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Game bring-up + url: https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up + about: Progress of one title, one thread per title ID + - name: Question + url: https://github.com/boykopovar/AnyPS5/discussions/categories/q-a + about: Building, converting and running From 01511e38bc6edd28f5078cec99f369506979c445 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:07:43 +0200 Subject: [PATCH 0002/1360] docs: point slow scenes from the bug report form to the performance report form --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c42f7a1a17..3887b5eada 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug report -description: A title, homebrew or test stops, crashes or renders wrongly +description: A title, homebrew or test stops, crashes or renders wrongly. labels: [bug] body: - type: markdown @@ -7,6 +7,7 @@ body: value: | Search open and closed issues and pull requests for the error text first, and comment there if it is already reported. Game bring-up progress belongs in [Discussions](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), one thread per title. + A slow scene or a change in speed goes into the [Performance report](https://github.com/boykopovar/AnyPS5/issues/new?template=performance.yml) form, which asks for a [performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace) and its `tools/trace_digest.py` digest. - type: input id: summary attributes: From cc1dc3f1c6f88cd03665f933e0b7590e5717b0fe Mon Sep 17 00:00:00 2001 From: Prophet069 <291514246+Prophet069@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:48:09 +0200 Subject: [PATCH 0003/1360] fix(shader): decode GFX10 scratch offsets as one dword --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 +++++ .../prx/libSceAgcDriver/tests/FlatScratch.cpp | 48 +++++++++++++++++++ .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 17 +++++++ .../MemoryResourceHelpers.cpp | 2 +- docs/dev/debt/shader.md | 4 ++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8b5909b8e0..311edda0ec 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1966,3 +1966,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + + +if(BUILD_TESTING) + add_test_executable(agc_flat_scratch_tests tests/FlatScratch.cpp) + target_link_libraries(agc_flat_scratch_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(agc_flat_scratch_tests) + add_test(NAME agc_flat_scratch COMMAND agc_flat_scratch_tests) + set_tests_properties(agc_flat_scratch PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_flat_scratch PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp new file mode 100644 index 0000000000..4db6cb7c5b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp @@ -0,0 +1,48 @@ +#include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" +#include +#include +#include +using namespace ShaderRecompiler; +static unsigned tests=0; +#define Require(value) do { if(!(value)){std::fprintf(stderr,"flat scratch assertion line %u: %s\n",__LINE__,#value);throw std::runtime_error("flat scratch regression");}} while(false) +static RdnaInstruction Decode(unsigned seg,unsigned saddr,unsigned addr,unsigned offset=0,unsigned opcode=12){ + const std::array code{(0x37u<<26)|(opcode<<18)|(seg<<14)|offset,(2u<<24)|(saddr<<16)|addr}; + return DecodeRdnaFlat(0x154cu,code,0); +} +static void Reject(unsigned seg,unsigned saddr,unsigned addr){try{(void)Decode(seg,saddr,addr);}catch(const std::runtime_error&){++tests;return;}throw std::runtime_error("invalid FLAT accepted");} +static void Scratch(unsigned saddr,unsigned addr,unsigned expectedVector,unsigned expectedScalar){ + auto op=Decode(1,saddr,addr,0xffcu); + Require(op.memorySegment==1 && op.memoryOffset==-4 && op.sourceCount==2); + Require(op.source0.kind==(expectedVector?RdnaOperandKind::VectorRegister:RdnaOperandKind::IntegerInlineConstant)); + Require(op.source1.kind==(expectedScalar?RdnaOperandKind::ScalarRegister:RdnaOperandKind::IntegerInlineConstant)); + if(expectedVector)Require(op.source0.reg==addr);else Require(op.source0.value==0); + if(expectedScalar)Require(op.source1.reg==saddr);else Require(op.source1.value==0); + IrProgram program;auto& block=program.CreateBlock();program.SetEntryBlock(block);TranslationContext context(program,block,256); + context.TranslateInstruction(op); + unsigned vectors=0,addressReads=0,destinationReads=0,scalars=0,loads=0,scratch=0; + for(auto* inst:block.Instructions()){ + if(inst->Opcode()==IrOpcode::GetVectorRegister){ + ++vectors;auto reg=inst->Arguments().at(0)->Register().index;Require(reg<256); + if(reg==addr)++addressReads; + if(reg==2)++destinationReads; + } + if(inst->Opcode()==IrOpcode::GetScalarRegister)++scalars; + if(inst->Opcode()==IrOpcode::LoadAddressU32)++loads; + if(inst->Opcode()==IrOpcode::GetScratchResource)++scratch; + } + std::printf("SV/SS/ST saddr=%u addr=%u vector=%u scalar=%u loads=%u scratch=%u\n",saddr,addr,vectors,scalars,loads,scratch);std::fflush(stdout); + Require(vectors==expectedVector+1 && addressReads==expectedVector && destinationReads==1 && scalars==expectedScalar && loads==1 && scratch==1); + ++tests; +} +int main(){ + + const std::array code{0xdc304000u,0x027d00ffu};auto actual=DecodeRdnaFlat(0x154cu,code,0); + Require(actual.destination.reg==2 && actual.source0.reg==255 && actual.source1.value==0);++tests; + Scratch(125,255,1,0);Scratch(125,254,1,0);Scratch(0,255,0,1);Scratch(105,255,0,1);Scratch(127,255,0,0); + Reject(0,125,255);Reject(2,125,255);Reject(2,127,255);Reject(1,108,0);Reject(3,0,0); + auto flat=Decode(0,125,254);Require(flat.source1.kind==RdnaOperandKind::VectorRegister && flat.source1.reg==255);++tests; + auto global=Decode(2,0,255);Require(global.source0.reg==255 && global.source1.kind==RdnaOperandKind::ScalarRegister);++tests; + try{const std::array truncated{0xdc304000u};(void)DecodeRdnaFlat(0,truncated,0);throw std::logic_error("truncated accepted");}catch(const std::out_of_range&){++tests;} + std::printf("PASS %u flat scratch decode/IR and invalid boundary cases\n",tests); +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 77ea8f0322..886e14be32 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -932,6 +932,23 @@ RdnaInstruction DecodeRdnaFlat(std::uint32_t programCounter, std::span Date: Fri, 16 Oct 2026 15:02:58 +0000 Subject: [PATCH 0004/1360] fix(relinker): restore rbx in TLS coverage bodies run on the host The alu_execution and moved_branch fixtures write rbx and return to the host caller without restoring it. rbx is callee-saved in both the SysV and the Windows x64 ABI, so the ctypes caller in windows_image_runner.py may use the clobbered value after the call. With Python 3.9 on Linux x86-64 both groups crash with SIGSEGV; newer Python happens not to rely on rbx there. Save rbx on entry and restore it before every return, as the other executed fixtures already do for the callee-saved registers they use, and shift the patched TLS addresses and branch targets by the extra push. --- .../tests/test_tls_function_coverage.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 8061ee1638..f27ffb5dc2 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -58,6 +58,7 @@ def alu_check_body(opcode, register, base, value, result, flags_masked): emit(bytes.fromhex("0f 85 00 00 00 00")) jumps.append(len(code) - 4) + emit(b"\x53") emit(bytes.fromhex("64 c7 04 25 28 00 00 00") + struct.pack(" Date: Thu, 15 Oct 2026 16:15:05 +0100 Subject: [PATCH 0005/1360] perf(agc): clear single-key DCC surfaces instead of uploading them A fast clear whose value the code does not carry is not on the IsDccClear whitelist, so a ClearSingle surface fell through to a full host upload. For the 4K color targets a title clears through its DCC keys that is 63.8 MiB of guest read, staging fill and DMA per surface per frame -- about 101 times per 10 s, roughly a third of the frame budget in Chapter 1 of Bendy and The Ink Machine -- while the guest texel memory never changed. The value is already in the surface: a single-key surface carries its one color as the leading element of every 256-byte key block, which ReadTextureSurface expands over that block for a read. Read that one element instead and clear the image to it. FillClearColor validates the value and refuses rather than produce a wrong color, so anything it cannot express exactly keeps the old upload path. IsDccClear keeps its whitelist meaning for its other callers; only upload's fast-clear gate learns the additional code. Measured on an RX 7900 GRE, Release build, Chapter 1 (level1), no tuning switches: frame p50 94.2 -> 72.7 ms, p99 159.0 -> 90.4 ms, 309 -> 407 presents per 30 s (10.3 -> 13.5 fps), with gpu_busy unchanged at 38.6 ms per present. The [storage] uploads by path counters went from cpu 109 / 6948.8 MiB to cpu 0 / 0.0 per 10 s window, and the load-point timeline is unchanged. --- .../Graphics/include/DccMetadata.hpp | 2 ++ .../Graphics/src/DccMetadata.cpp | 17 +++++++++++++---- .../libSceAgcDriver/Graphics/src/Texture.cpp | 14 +++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index d2e9dff3a6..a055ac6657 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -79,6 +79,8 @@ struct DccKeyProof { // APS5_NO_KEY_FAST_PATH=1 scans on every call and stores nothing. DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, DccKeyProof& proof); bool KeyFastPath(); +bool SingleKeysReadable(const GuestTextureResource& resource); +bool ReadSingleElement(const GuestTextureResource& resource, std::span element); // Cumulative outcomes of ProvedClearKeys: calls answered by a proof, scans made, and scans not kept. struct DccKeyProofCounts { std::uint64_t proved; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index f8f4d371ec..ca28030b19 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -57,10 +57,6 @@ DccKeys ByteKeys(std::uint8_t key) { } } -bool SingleKeysReadable(const GuestTextureResource& resource) { - return KeyBytes % BytesPerElement(resource.format) == 0; -} - void ExpandSingleBlocks(std::span bytes, std::size_t elementBytes) { for (std::size_t block = 0; block < bytes.size(); block += KeyBytes) { const auto blockBytes = std::min(KeyBytes, bytes.size() - block); @@ -743,6 +739,19 @@ void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std: if (keys == DccKeys::ClearSingle) ExpandSingleBlocks(bytes, BytesPerElement(resource.format)); } +bool SingleKeysReadable(const GuestTextureResource& resource) { + return KeyBytes % BytesPerElement(resource.format) == 0; +} + +bool ReadSingleElement(const GuestTextureResource& resource, std::span element) { + if (!SingleKeysReadable(resource)) return false; + const auto bytes = BytesPerElement(resource.format); + if (element.size() < bytes) return false; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); + GuestMemory::ReadCommitted(resource.baseAddress, element.subspan(0, bytes)); + return true; +} + namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ba62bdcde6..7b6877d6ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2161,6 +2161,17 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; +bool SingleKeyColorFor(const GuestTextureResource& resource, VkFormat format, DccKeys keys, VkClearColorValue& clear) { + if (keys != DccKeys::ClearSingle) return false; + const auto elementBytes = BytesPerElement(resource.format); + if (elementBytes == 0 || elementBytes > sizeof(std::uint32_t) * 4) return false; + std::uint32_t pattern[4]{}; + auto* bytes = reinterpret_cast(pattern); + if (!ReadSingleElement(resource, std::span(bytes, elementBytes))) return false; + for (std::size_t at = elementBytes; at < sizeof(pattern); at += elementBytes) std::memcpy(bytes + at, bytes, elementBytes); + return FillClearColor(format, elementBytes, pattern, clear); +} + void StorageTexture::upload(const std::vector* layers) { const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -2192,7 +2203,8 @@ void StorageTexture::upload(const std::vector* layers) { uploadedKeys = ProvedClearKeys(descriptor, guestBytes, keyProof); filledKeys = DccKeys::Uncompressed; VkClearColorValue clearValue{}; - if (IsDccClear(uploadedKeys) && ClearColorFor(storageFormat, uploadedKeys, clearValue)) { + const bool fastClear = IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle; + if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, storageFormat, uploadedKeys, clearValue))) { // A fast-cleared surface is cleared on the GPU; its texel memory is neither read nor filled. // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms From 756f3aff3ba4904cd1f531a9ca3721f725ed62cd Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:54:01 +0100 Subject: [PATCH 0006/1360] fix(agc): require a uniformity proof for single-key DCC clears A ClearSingle surface carries its clear color as the leading element of every 256-byte DCC key block, but the keys do not guarantee that every block clears to that one color: agc_driver_dcc_keys_first_write fails on 9900a7ed7 with 16296/16384 texels wrong, exactly the stored block-0 color plus the 24 kernel texels that a key of all zeros leaves behind. Reading one element and latching it was unsound. ProvedSingleUniform in DccMetadata reads the first element of every 256-byte key block once per upload epoch, gathered in 1 MiB chunks (about 64 ReadCommitted calls instead of 261k one-element reads), bounded to the surface so the gather never reads past the keys it proves, requires every block-leading element to match the first, and memoizes the proof until the next key upload. StorageTexture::upload keeps the fast clear only on a proven single uniform color and falls back to the original CPU upload path otherwise. Any doubt is a fallback, never a wrong clear. The proof passes for the 4K color targets, so the fast clear stays live: the failing test now passes, full ctest is green, and a gated A/B in Bendy and The Ink Machine still shows the win, p50 116.4 -> 112.3 ms and p99 146.2 -> 117.8 ms per present (256 -> 267 presents per 30 s), because ClearSingle genuinely stamps every block each frame there and the sound gather costs about as much as the guest read it skips. --- .../Graphics/include/DccMetadata.hpp | 8 ++++++ .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/DccMetadata.cpp | 25 +++++++++++++++++++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 ++--- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index a055ac6657..b8441174ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -81,6 +81,14 @@ DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t gues bool KeyFastPath(); bool SingleKeysReadable(const GuestTextureResource& resource); bool ReadSingleElement(const GuestTextureResource& resource, std::span element); +struct DccSingleProof { + std::uint64_t address = 0; + std::uint64_t bytes = 0; + std::uint64_t generation = 0; + std::uint32_t format = 0; + bool uniform = false; +}; +bool ProvedSingleUniform(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof); // Cumulative outcomes of ProvedClearKeys: calls answered by a proof, scans made, and scans not kept. struct DccKeyProofCounts { std::uint64_t proved; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 5a70e2f069..ee3a18379f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -538,6 +538,7 @@ private: mutable DccKeys filledKeys = DccKeys::Uncompressed; mutable DccKeyProof keyProof; mutable DccRangeProof targetKeyProof; + DccSingleProof singleProof; struct RefreshProof { std::uint64_t generation = 0; std::uint64_t pendingSerial = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index ca28030b19..52167fbd4d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -752,6 +752,31 @@ bool ReadSingleElement(const GuestTextureResource& resource, std::span 16) return false; + const auto blocks = static_cast(guestBytes / KeyBytes); + if (proof.generation != 0 && proof.address == resource.baseAddress && proof.bytes == guestBytes && proof.format == resource.format && collected != 0 && GuestMemory::UnchangedSince(resource.baseAddress, static_cast(guestBytes), proof.generation)) return proof.uniform; + std::byte first[16]{}; + bool uniform = true; + std::vector buffer(std::min(guestBytes, std::uint64_t(1) << 20)); + { + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); + GuestMemory::ReadCommitted(resource.baseAddress, std::span(first, bytes)); + for (std::size_t block = 1; block < blocks && uniform;) { + const auto offset = block * KeyBytes; + const auto size = static_cast(std::min(buffer.size(), guestBytes - offset)); + GuestMemory::ReadCommitted(resource.baseAddress + offset, std::span(buffer).first(size)); + const auto covered = size / KeyBytes; + for (std::size_t index = 0; index < covered && uniform; ++index) uniform = std::equal(first, first + bytes, buffer.begin() + index * KeyBytes); + block += covered; + } + } + proof = collected != 0 ? DccSingleProof{resource.baseAddress, guestBytes, collected, resource.format, uniform} : DccSingleProof{}; + return uniform; +} + namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7b6877d6ea..7c249e0643 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2161,8 +2161,8 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; -bool SingleKeyColorFor(const GuestTextureResource& resource, VkFormat format, DccKeys keys, VkClearColorValue& clear) { - if (keys != DccKeys::ClearSingle) return false; +bool SingleKeyColorFor(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof, VkFormat format, DccKeys keys, VkClearColorValue& clear) { + if (keys != DccKeys::ClearSingle || !ProvedSingleUniform(resource, guestBytes, collected, proof)) return false; const auto elementBytes = BytesPerElement(resource.format); if (elementBytes == 0 || elementBytes > sizeof(std::uint32_t) * 4) return false; std::uint32_t pattern[4]{}; @@ -2204,7 +2204,7 @@ void StorageTexture::upload(const std::vector* layers) { filledKeys = DccKeys::Uncompressed; VkClearColorValue clearValue{}; const bool fastClear = IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle; - if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, storageFormat, uploadedKeys, clearValue))) { + if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, guestBytes, now, singleProof, storageFormat, uploadedKeys, clearValue))) { // A fast-cleared surface is cleared on the GPU; its texel memory is neither read nor filled. // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms From 275008a34154a265ebd970afbffd7551141c9048 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:26:17 +0100 Subject: [PATCH 0007/1360] test(agc): cover the uniform single-key DCC clear The ClearSingle case was only covered by a non-uniform surface, which exercises the CPU fallback, never the fast clear the proof enables. RunUniformSingleClear lays out a surface whose every 256-byte block starts with the same color, so ProvedSingleUniform holds and upload clears on the GPU instead of reading the guest texels. The write-back checks both halves of the contract: every written-back texel is either the kernel's write or the proved clear color (a wrong SingleKeyColorFor or FillClearColor value fails here), and on write-watched memory the upload keeps no guest snapshot, so a fallback that read the surface fails the snapshot check. Forcing ProvedSingleUniform to return false leaves the identical image content but a valid snapshot, which the watched run rejects. --- .../tests/execution/DccKeysFirstWrite.cpp | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index e9668e78dc..f90e7b9307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -47,6 +47,7 @@ struct VulkanDeviceTestAccess { namespace { using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::LookupOutcomes; using AgcDriver::Graphics::Require; using AgcDriver::Graphics::StorageTexture; using ShaderRecompiler::ShaderStage; @@ -451,6 +452,46 @@ void RunSingleKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(wrong == 0, message); } +void RunUniformSingleClear(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + const std::uint32_t color = 0x11223344u; + std::memset(texels, 0x55, SurfaceBytes); + for (std::size_t offset = 0; offset < SurfaceBytes; offset += 256) std::memcpy(texels + offset, &color, 4); + std::memset(keys, 0x10, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + const auto uploads = AgcDriver::Graphics::ThreadLookupOutcomes().counts; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "uniform single-key clear: the first write did not store the keys uncompressed"); + const auto& uploaded = AgcDriver::Graphics::ThreadLookupOutcomes().counts; + Require(uploaded[LookupOutcomes::UploadClear] == uploads[LookupOutcomes::UploadClear] + 1 && uploaded[LookupOutcomes::UploadCpu] == uploads[LookupOutcomes::UploadCpu], "uniform single-key clear: the first write read the surface and uploaded it instead of clearing it on the GPU"); + const bool stillWatched = AgcDriver::GuestMemory::Watched(surface, SurfaceBytes); + Require(image->GuestSnapshotValid() == !stillWatched, stillWatched ? "uniform single-key clear: the upload of write-watched memory kept a guest snapshot, so the surface was read rather than cleared" : "uniform single-key clear: the upload of untracked memory kept no guest bytes to compare"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test uniform single-key clear"); + device.WaitIdle(); + RequireKeys(keys, 0xff, "uniform single-key clear: the keys after the write-back"); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::size_t wrong = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + const auto offset = layout.Offset(x, y); + std::uint32_t texel = 0; + std::memcpy(&texel, texels + offset, 4); + const auto expected = y == 0 && x < Threads ? 0xffffffffu : color; + if (texel != expected) ++wrong; + } + } + char message[256]; + std::snprintf(message, sizeof(message), "uniform single-key clear: %zu of %u written-back texels are neither the kernel's write nor the proved clear color 0x%08x", wrong, Side * Side, color); + Require(wrong == 0, message); +} + void RunSingleKeysSampled(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock, const char* memory) { auto* texels = block; auto* keys = block + KeysOffset; @@ -783,6 +824,11 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { int main() { try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif std::optional block; std::optional watched; std::optional untracked; @@ -802,6 +848,9 @@ int main() { std::optional single; single.emplace(false); RunSingleKeys(*device, single->Data()); + std::optional uniformClear; + uniformClear.emplace(false); + RunUniformSingleClear(*device, uniformClear->Data()); std::optional sampled; sampled.emplace(false); std::optional sampledOutput; @@ -824,6 +873,9 @@ int main() { std::optional sampledWatched; sampledWatched.emplace(true); RunSingleKeysSampled(*device, sampledWatched->Data(), sampledOutput->Data(), "write-watched"); + std::optional uniformClearWatched; + uniformClearWatched.emplace(true); + RunUniformSingleClear(*device, uniformClearWatched->Data()); #ifdef _WIN32 pendingSingle.emplace(true, true); #else From ceacadb6e403d524d771deeafe447d6c9b48b944 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:08:54 +0200 Subject: [PATCH 0008/1360] fix(agc): drain the draw pipeline before a vertex table read overtakes a queued write Driver::draw decodes a vertex stage's fetch tables (the attribute words and the vertex buffer V#s they name) with GuestMemory::Read in DecodeVertexStageInfo. Unlike the reads of a draw's shaders, which go through queryPendingWrite, those reads were never checked against the writes still queued in queue 0's draw pipeline, so a draw could take a V# from a table before a queued item stored into it. DecodeOrderedVertexStageInfo decodes, and when one of the decode's reads overlaps a queued write, drains the pipeline and decodes again. agc_driver_vertex_table_read_order checks a queued store into each table and a queued store elsewhere. AI-assisted: yes --- .../include/Driver/Draw/DrawCache.hpp | 3 + .../Execution/src/Driver/Draw/Draw.cpp | 3 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 13 ++ .../tests/execution/VertexTableReadOrder.cpp | 151 ++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 0ff895330e..d3f3da21fa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -4,6 +4,7 @@ #include "UserDataUse.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include "prx/libc/include/general/AtomicSharedPtr.hpp" @@ -45,6 +47,7 @@ struct DrawDecode { void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi); void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi); void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment); +ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector& reads, std::uint32_t hiddenUserWords); void AppendProgramMemory(std::vector& memory, const std::vector& programs); std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 28c9800bb3..431103a2ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -448,8 +448,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto decodeVertexInfo = [&](std::size_t i) { const auto& program = programs[i]; if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; - decodeReads[i].clear(); - vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); + vertexInfos[i] = DecodeOrderedVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, decodeReads[i], 8u - program.firstUserSgpr); if (vertexInfos[i]) { vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode; 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 8208e0805d..11c9c16ba2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include @@ -30,6 +31,18 @@ void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi if ((program.binary.stage == ShaderRecompiler::ShaderStage::Vertex || program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) && program.snapshot->header.empty() && program.snapshot->userDataTailRegister) program.tailContinuation = ReadUserDataTailContinuation(program.binary.code, program.firstUserSgpr, program.userData); } +ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector& reads, std::uint32_t hiddenUserWords) { + const auto decode = [&] { + reads.clear(); + return Graphics::DecodeVertexStageInfo(header, headerAddress, userData, &reads, false, hiddenUserWords); + }; + auto info = decode(); + if (!DrawPipeline::Active() || !DrawPipeline::Queue0().Busy()) return info; + if (std::none_of(reads.begin(), reads.end(), [](const Graphics::DecodeRead& read) { return DrawPipeline::Queue0().Overlaps(read.address, read.bytes.size()); })) return info; + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Capture); + return decode(); +} + void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi) { program.merged = true; program.mergedPointer = pointerBase; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp new file mode 100644 index 0000000000..d49e7232ef --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +#include "SceShaders.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::DrawPipeline; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("vertex table read order: " + what); +} + +constexpr std::size_t UserDataOffset = 288; +constexpr std::size_t DirectOffset = 352; +constexpr std::size_t SemanticOffset = 384; + +alignas(64) std::array BufferTable{0x1000u, 0x20000u, 64u, 0x3000u, 0x7000u, 0x20000u, 32u, 0x3000u}; +alignas(64) std::array AttribTable{(0x2cu << 5u) | 1u}; +alignas(64) std::array Elsewhere{}; +std::vector Header(SemanticOffset + sizeof(ShaderSemantic)); + +void BuildHeader() { + std::array direct{}; + direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 0; + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 2; + std::memcpy(Header.data() + DirectOffset, direct.data(), sizeof(direct)); + ShaderSemantic semantic{}; + semantic.size_in_elements = 1; + std::memcpy(Header.data() + SemanticOffset, &semantic, sizeof(semantic)); + ShaderUserData userData{}; + userData.direct_resource_offset = reinterpret_cast(Header.data() + DirectOffset); + userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT; + std::memcpy(Header.data() + UserDataOffset, &userData, sizeof(userData)); + Shader shader{}; + shader.user_data = reinterpret_cast(Header.data() + UserDataOffset); + shader.input_semantics = reinterpret_cast(Header.data() + SemanticOffset); + shader.num_input_semantics = 1; + std::memcpy(Header.data(), &shader, sizeof(shader)); +} + +std::uint64_t Address(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array UserWords() { + const auto buffer = Address(BufferTable.data()); + const auto attrib = Address(AttribTable.data()); + return {static_cast(buffer), static_cast(buffer >> 32u), static_cast(attrib), static_cast(attrib >> 32u)}; +} + +ShaderRecompiler::ShaderVertexStageInfo Decode(std::vector& reads) { + const auto userWords = UserWords(); + return AgcDriver::DriverDetail::DecodeOrderedVertexStageInfo(Header, Address(Header.data()), userWords, reads, 0); +} + +struct Gate { + std::atomic release{false}; + ~Gate() { release.store(true, std::memory_order_release); } +}; + +void Hold(Gate& gate, const void* range, std::size_t bytes, std::function store) { + DrawPipeline::Queue0().Enqueue([&gate, store = std::move(store)] { + while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + store(); + }, {{Address(range), Address(range) + bytes}}); +} + +void WaitEmpty() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (DrawPipeline::QueuedItems() != 0) { + check(std::chrono::steady_clock::now() < deadline, "the pipeline never emptied"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +ShaderRecompiler::ShaderVertexStageInfo DecodeBehindQueuedStore(const void* range, std::size_t bytes, std::function store, std::vector& reads) { + WaitEmpty(); + Gate gate; + Hold(gate, range, bytes, std::move(store)); + std::thread releaser([&gate] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + gate.release.store(true, std::memory_order_release); + }); + ShaderRecompiler::ShaderVertexStageInfo info; + try { + info = Decode(reads); + } catch (...) { + releaser.join(); + throw; + } + releaser.join(); + return info; +} + +} + +int main() { + try { + BuildHeader(); + DrawPipeline::Active() = true; + std::vector reads; + + auto info = Decode(reads); + check(info.fetchEmbedded && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x7000u && info.resources[0].fields[2] == 32u, "the tables did not decode to the second vertex buffer"); + + info = DecodeBehindQueuedStore(BufferTable.data() + 4, 16, [] { + BufferTable[4] = 0x9000u; + BufferTable[6] = 128u; + }, reads); + check(info.resources[0].fields[0] == 0x9000u && info.resources[0].fields[2] == 128u, "the vertex buffer V# was read before the queued store into the vertex buffer table"); + check(reads.size() == 2 && reads[1].address == Address(BufferTable.data() + 4) && std::memcmp(reads[1].bytes.data(), BufferTable.data() + 4, 16) == 0, "the recorded V# read is not the stored V#"); + check(DrawPipeline::QueuedItems() == 0, "the store into the vertex buffer table is still queued"); + + info = DecodeBehindQueuedStore(AttribTable.data(), 4, [] { AttribTable[0] = 0x2cu << 5u; }, reads); + check(info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "the attribute word was read before the queued store into the vertex attribute table"); + check(reads.size() == 2 && reads[0].address == Address(AttribTable.data()) && reads[1].address == Address(BufferTable.data()), "the decode did not read the stored attribute word and the V# it names"); + + WaitEmpty(); + Gate gate; + Hold(gate, Elsewhere.data(), sizeof(Elsewhere), [] {}); + info = Decode(reads); + check(info.resources[0].fields[0] == 0x1000u, "the decode next to an unrelated queued store changed"); + check(DrawPipeline::QueuedItems() == 1, "a decode that overlaps nothing queued drained the pipeline"); + gate.release.store(true, std::memory_order_release); + WaitEmpty(); + + DrawPipeline::Active() = false; + DrawPipeline::Queue0().Stop(); + std::puts("vertex table read order tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 54314ff8a44563e7cc31ab1f7c5aa59623355853 Mon Sep 17 00:00:00 2001 From: PhialsBasement <155454343+PhialsBasement@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:39:25 +1100 Subject: [PATCH 0009/1360] test(libSceAgc): find MRT0 through the compact versioned register defaults The versioned tables hand out compact 12-byte type entries since #7681, and the MRT0 checks on them were dropped with the pairs. The walk now takes the entry size: MRT0 is block 78 of the legacy pairs and of the compact version 13 table, block 73 of version 10, and every versioned table from 4 holds it. The debt entry names the layout of each entry point. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/tests/Commands.cpp | 29 ++++++++++++++-------- docs/dev/debt/libSceAgc.md | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 4313f1cd68..3746f608d1 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -658,15 +658,13 @@ void testDefaults() { expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); } -struct DefaultsType { - std::uint32_t key; - std::uint32_t bankIndex; -}; +constexpr std::size_t PairEntryBytes = 8; +constexpr std::size_t CompactEntryBytes = 12; -std::uint32_t colourTargetBlock(const void* root) { +std::uint32_t colourTargetBlock(const void* root, std::size_t entryBytes) { const auto* bytes = static_cast(root); ShaderRegister** contextBlocks = nullptr; - const DefaultsType* types = nullptr; + const unsigned char* types = nullptr; std::uint32_t count = 0; std::memcpy(&contextBlocks, bytes, sizeof(contextBlocks)); std::memcpy(&types, bytes + 0x30, sizeof(types)); @@ -676,12 +674,17 @@ std::uint32_t colourTargetBlock(const void* root) { std::uint32_t matches = 0; std::uint32_t block = 0; for (std::uint32_t entry = 0; entry < count; ++entry) { - if ((types[entry].bankIndex & 3u) == 0) + std::uint32_t key = 0; + std::uint32_t bankIndex = 0; + std::memcpy(&key, types + entry * entryBytes, sizeof(key)); + std::memcpy(&bankIndex, types + entry * entryBytes + 4, sizeof(bankIndex)); + bankIndex &= 0x3ffu; + if ((bankIndex & 3u) == 0) ++contexts; - if (types[entry].key != 0x38e92c91u) + if (key != 0x38e92c91u) continue; - check((types[entry].bankIndex & 3u) == 0, "MRT0 defaults are not in the context bank"); - block = types[entry].bankIndex >> 2; + check((bankIndex & 3u) == 0, "MRT0 defaults are not in the context bank"); + block = bankIndex >> 2; ++matches; } check(matches == 1 && block < contexts && contextBlocks[block] != nullptr, "MRT0 defaults key is missing, repeated or outside the context blocks"); @@ -694,7 +697,11 @@ std::uint32_t colourTargetBlock(const void* root) { void testDefaultsTypes() { const auto* legacy = sceAgcGetRegisterDefaults(); check(legacy != sceAgcGetRegisterDefaults2(13), "legacy register defaults reused the compact versioned table"); - check(colourTargetBlock(legacy) == 78, "MRT0 defaults are not block 78 of the 137-entry table"); + check(colourTargetBlock(legacy, PairEntryBytes) == 78, "MRT0 defaults are not block 78 of the 137-entry table"); + check(colourTargetBlock(sceAgcGetRegisterDefaults2(13), CompactEntryBytes) == 78, "versioned MRT0 defaults are not block 78 of the compact 137-entry table"); + check(colourTargetBlock(sceAgcGetRegisterDefaults2(10), CompactEntryBytes) == 73, "versioned MRT0 defaults are not block 73 of the compact 128-entry table"); + for (std::uint32_t version = 4; version < 14; ++version) + colourTargetBlock(sceAgcGetRegisterDefaults2(version), CompactEntryBytes); const auto checkCompactTypes = [](const void* root, std::uint32_t count, std::uint32_t key, std::uint32_t packedType, std::uint32_t extra) { diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 83a5cbca52..66558a8c1c 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,7 +7,7 @@ - [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 - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table while `sceAgcGetRegisterDefaults` now returns the version 13 public one; which internal table a console returns is not known. [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 -- [sceAgcGetRegisterDefaults](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 13 public table (137 keyed entries over 84 context blocks) because the console returns the running system software's table: [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/src/color_runtime_defaults.h) measured 128 entries over 79 blocks on 10.x and 137 over 84 on 12.02 and 13.x, the shapes of the version 10 and version 11/13 tables here, and PPSA99011 refuses the version 0 table. The [type table](../../../core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp) is handed out as 8-byte `{key, index << 2 | bank}` pairs, the layout ps5-vulkan and the homebrew sharing its `ps5_agc_registers.h` read; the compact 12-byte entries also pack the block's register count and stride above bit 10, and those bits are dropped. No console dump of the runtime type table was compared, and AnyPS5 does not choose 10.x or 12.02 by the system software a title targets +- [sceAgcGetRegisterDefaults](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 13 public table (137 keyed entries over 84 context blocks) because the console returns the running system software's table: [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/src/color_runtime_defaults.h) measured 128 entries over 79 blocks on 10.x and 137 over 84 on 12.02 and 13.x, the shapes of the version 10 and version 11/13 tables here, and PPSA99011 refuses the version 0 table. The [type table](../../../core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp) is handed out as 8-byte `{key, index << 2 | bank}` pairs, the layout ps5-vulkan and the homebrew sharing its `ps5_agc_registers.h` read; the compact 12-byte entries also pack the block's register count and stride above bit 10, and those bits are dropped. `sceAgcGetRegisterDefaults2`, `sceAgcGetRegisterDefaults2Internal`, `sceAgcGetRegisterDefaultsInternal` and `sceAgcInit` hand out the compact 12-byte entries (`RegisterDefaultsTypeLayout::Compact`): SDK-built code walks the tables of the first two with a 12-byte stride, reads the key at +0 and the bank and index from the low 10 bits of the word at +4, and keeps an all-ones sentinel for a key it does not find. Which layout the legacy internal call returns on a console is not known. No console dump of the runtime type table was compared, and AnyPS5 does not choose 10.x or 12.02 by the system software a title targets - [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [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 52491e93f92d06b46ad2464dddf415aa6daf988f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 04:42:52 -0400 Subject: [PATCH 0010/1360] fix(shader): clear BDA ABI after removing address bindings --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 10 ++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 23 +++++++++++++++---- core/shader/recompiler/Recompiler.cpp | 1 + 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c22c6de42a..8b091cf911 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2568,6 +2568,16 @@ set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_replay) if(BUILD_TESTING) + foreach(optimizer IN ITEMS none full) + if(optimizer STREQUAL "full" AND NOT ANYPS5_ENABLE_SPIRV_TOOLS) + continue() + endif() + add_test(NAME agc_shader_specialized_bda_abi_${optimizer} COMMAND agc_shader_memory_tests --specialized-bda-abi) + set_tests_properties(agc_shader_specialized_bda_abi_${optimizer} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SPIRV_OPT=${optimizer}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_shader_specialized_bda_abi_${optimizer} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(agc_shader_replay_requests_tests tests/ShaderReplayRequests.cpp) target_link_libraries(agc_shader_replay_requests_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_shader_replay_requests_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 20870c39d8..193ebd6908 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5587,10 +5587,10 @@ void verifyInt64AtomicCapabilities() { require(declares(compile(imageAtomic, spv::CapabilityInt64ImageEXT), spv::CapabilityInt64ImageEXT), "64-bit atomics: an image_atomic_swap on a 32_32 image does not declare Int64ImageEXT"); } -void verifySpecializedBufferAtomicBindings() { +void verifySpecializedBufferAtomicBindings(bool finalAbiOnly = false) { using namespace ShaderRecompiler; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics}; alignas(256) static std::array buffer{}; const auto bufferAddress = static_cast(reinterpret_cast(buffer.data())); const std::array userData{ @@ -5598,8 +5598,8 @@ void verifySpecializedBufferAtomicBindings() { static_cast(bufferAddress), static_cast((bufferAddress >> 32u) & 0xffffu), 256u, 0x31016facu, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}; - const std::vector code{0xe0f05000u, 0x80012803u, 0xbf810000u}; - const auto compile = [&](std::span supported) { + const auto compile = [&](std::span supported, std::uint32_t opcode = 0xe0f05000u) { + const std::array code{opcode, 0x80012803u, 0xbf810000u}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x58800u, code, 0, {}}; request.context.waveSize = 32; @@ -5617,6 +5617,16 @@ void verifySpecializedBufferAtomicBindings() { request.useCache = false; return Recompile(request); }; + if (finalAbiOnly) { + for (const auto opcode : {0xe0f05000u, 0xe1705000u}) { + const auto result = compile(capabilities, opcode); + const auto faults = std::ranges::count_if(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::FaultBuffer; }); + require(faults <= 1, "specialized buffer atomic: duplicate fault buffers"); + require(result.bdaAbiVersion == (faults == 0 ? 0u : BdaAbi::Version), "specialized buffer atomic: final fault binding and BDA ABI disagree"); + if (opcode == 0xe1705000u) require(faults == 1, "specialized buffer atomic: a 64-bit atomic lost its live fault buffer"); + } + return; + } const auto result = compile(capabilities); std::size_t tables = 0; std::size_t faults = 0; @@ -7153,6 +7163,11 @@ int main(int argc, char** argv) { setenv("ANYPS5_NO_SHADER_CACHE", "1", 1); #endif using namespace ShaderRecompiler; + if (argc == 2 && std::string_view(argv[1]) == "--specialized-bda-abi") { + verifySpecializedBufferAtomicBindings(true); + std::cout << "Specialized programs retain the BDA ABI only while a fault buffer remains\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") { verifyQuadMaskNeighbourReads(); std::cout << "Quad masks preserve neighbour-lane helpers or refuse without demote\n"; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 4053ee89dc..c723d08b1b 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1046,6 +1046,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request), &tables); result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); + if (std::ranges::none_of(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::BdaPagetable || binding.role == DescriptorRole::FaultBuffer; })) result.bdaAbiVersion = 0u; result.pushConstants = std::move(bindings.pushConstants); result.readsDepthRemapPush = depthRemap; if (request.shader.stage == ShaderStage::Compute) { From bec98501128bccd9783450a629236b66500bf065 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 18 Oct 2026 19:21:45 -0300 Subject: [PATCH 0011/1360] feat(agc): record GPU checkpoints through VK_AMD_buffer_marker --- .../Execution/include/DeviceFault.hpp | 58 +++++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 56 ++++++++++++++++-- .../prx/libSceAgcDriver/tests/DeviceFault.cpp | 31 ++++++++++ docs/dev/BUILD.md | 2 +- docs/dev/ENVIRONMENT.md | 2 +- 5 files changed, 142 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp index 558a25bde7..c82357c4ec 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp @@ -6,6 +6,8 @@ #endif #include #include +#include +#include #include #include #include @@ -74,6 +76,62 @@ inline std::vector DeviceFaultReport(VkDevice device, PFN_vkGetDevi return lines; } +class BufferMarkerLog { +public: + static constexpr std::uint32_t Capacity = 4096; + static constexpr std::uint32_t Listed = 8; + + std::uint32_t Next(std::uint64_t program) { + std::uint32_t id = 0; + while (id == 0) id = next.fetch_add(1, std::memory_order_relaxed) + 1; + auto& slot = slots[id % Capacity]; + slot.program.store(program, std::memory_order_relaxed); + slot.id.store(id, std::memory_order_release); + return id; + } + + std::vector Report(std::uint32_t reached, std::uint32_t finished) const { + std::vector lines; + if (reached == 0 && finished == 0) { + lines.emplace_back("[device-fault] no buffer marker reached"); + return lines; + } + lines.push_back(Line("last dispatch reached", reached)); + if (finished == 0) lines.emplace_back("[device-fault] no dispatch finished"); + else lines.push_back(Line("last dispatch finished", finished)); + const std::uint32_t pending = reached - finished; + if (pending == 0 || pending > Capacity) return lines; + const auto listed = std::min(pending, Listed); + for (std::uint32_t i = 1; i <= listed; ++i) lines.push_back(Line("unfinished dispatch", finished + i)); + if (pending > listed) { + char line[96]; + std::snprintf(line, sizeof(line), "[device-fault] %u more unfinished dispatches", pending - listed); + lines.emplace_back(line); + } + return lines; + } + +private: + struct Slot { + std::atomic id{0}; + std::atomic program{0}; + }; + + std::string Line(const char* what, std::uint32_t id) const { + char line[128]; + const auto& slot = slots[id % Capacity]; + if (id != 0 && slot.id.load(std::memory_order_acquire) == id) { + std::snprintf(line, sizeof(line), "[device-fault] %s: marker %u, program 0x%llx", what, id, static_cast(slot.program.load(std::memory_order_relaxed))); + } else { + std::snprintf(line, sizeof(line), "[device-fault] %s: marker %u, program not in the log", what, id); + } + return line; + } + + std::atomic next{0}; + std::array slots{}; +}; + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a4fb4cfb54..ab24067a35 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -289,6 +289,11 @@ struct VulkanDevice::State { bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; + PFN_vkCmdWriteBufferMarkerAMD bufferMarker = nullptr; + VkBuffer markerBuffer = VK_NULL_HANDLE; + VkDeviceMemory markerMemory = VK_NULL_HANDLE; + void* markerMapping = nullptr; + BufferMarkerLog markerLog; bool deviceLostHook = false; VkTimeDomainKHR hostTimeDomain = VK_TIME_DOMAIN_DEVICE_KHR; VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; @@ -547,6 +552,33 @@ struct VulkanDevice::State { return workgroupMemory; } + void CreateMarkerBuffer() { + bufferMarker = DeviceFunction("vkCmdWriteBufferMarkerAMD"); + VkBufferCreateInfo buffer{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + buffer.size = 2 * sizeof(std::uint32_t); + buffer.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + buffer.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + check(DeviceFunction("vkCreateBuffer")(device, &buffer, nullptr, &markerBuffer), "vkCreateBuffer buffer markers"); + VkMemoryRequirements requirements{}; + DeviceFunction("vkGetBufferMemoryRequirements")(device, markerBuffer, &requirements); + std::uint32_t memoryType = memoryProperties.memoryTypeCount; + const auto flags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + for (std::uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) { + if ((requirements.memoryTypeBits & (1u << i)) != 0 && (memoryProperties.memoryTypes[i].propertyFlags & flags) == flags) { + memoryType = i; + break; + } + } + require(memoryType < memoryProperties.memoryTypeCount, "coherent host memory for buffer markers is unavailable"); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = memoryType; + check(DeviceFunction("vkAllocateMemory")(device, &allocation, nullptr, &markerMemory), "vkAllocateMemory buffer markers"); + check(DeviceFunction("vkBindBufferMemory")(device, markerBuffer, markerMemory, 0), "vkBindBufferMemory buffer markers"); + check(DeviceFunction("vkMapMemory")(device, markerMemory, 0, buffer.size, 0, &markerMapping), "vkMapMemory buffer markers"); + std::memset(markerMapping, 0, buffer.size); + } + void Upload(std::span pixels) { if (uploadSize < pixels.size()) { if (uploadMapping) DeviceFunction("vkUnmapMemory")(device, uploadMemory); @@ -801,6 +833,9 @@ struct VulkanDevice::State { if (uploadMapping) reinterpret_cast(deviceProc(device, "vkUnmapMemory"))(device, uploadMemory); if (uploadBuffer) reinterpret_cast(deviceProc(device, "vkDestroyBuffer"))(device, uploadBuffer, nullptr); if (uploadMemory) reinterpret_cast(deviceProc(device, "vkFreeMemory"))(device, uploadMemory, nullptr); + if (markerMapping) reinterpret_cast(deviceProc(device, "vkUnmapMemory"))(device, markerMemory); + if (markerBuffer) reinterpret_cast(deviceProc(device, "vkDestroyBuffer"))(device, markerBuffer, nullptr); + if (markerMemory) reinterpret_cast(deviceProc(device, "vkFreeMemory"))(device, markerMemory, nullptr); if (swapchain) reinterpret_cast(deviceProc(device, "vkDestroySwapchainKHR"))(device, swapchain, nullptr); const auto destroyPool = reinterpret_cast(deviceProc(device, "vkDestroyCommandPool")); const auto destroyDevice = reinterpret_cast(deviceProc(device, "vkDestroyDevice")); @@ -1871,9 +1906,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : faultFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &faultFeatures; } - const bool checkpoints = std::getenv("APS5_GPU_CHECKPOINTS") != nullptr && hasExtension(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); - if (checkpoints) { - deviceExtensions.push_back(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + const bool checkpointsRequested = std::getenv("APS5_GPU_CHECKPOINTS") != nullptr; + const bool checkpoints = checkpointsRequested && hasExtension(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + const bool bufferMarkers = checkpointsRequested && !checkpoints && hasExtension(VK_AMD_BUFFER_MARKER_EXTENSION_NAME); + if (checkpoints || bufferMarkers) { + deviceExtensions.push_back(checkpoints ? VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME : VK_AMD_BUFFER_MARKER_EXTENSION_NAME); deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); } @@ -1934,10 +1971,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue); if (checkpoints) state->checkpointMarker = state->DeviceFunction("vkCmdSetCheckpointNV"); - if (deviceFault || checkpoints) { + if (bufferMarkers) state->CreateMarkerBuffer(); + if (deviceFault || checkpoints || bufferMarkers) { const auto faultInfo = deviceFault ? state->DeviceFunction("vkGetDeviceFaultInfoEXT") : nullptr; const auto checkpointData = checkpoints ? state->DeviceFunction("vkGetQueueCheckpointDataNV") : nullptr; - Graphics::DeviceLostHook() = [device = state->device, queue = state->queue, faultInfo, checkpointData] { + Graphics::DeviceLostHook() = [device = state->device, queue = state->queue, faultInfo, checkpointData, owner = state.get()] { + if (owner->markerMapping != nullptr) { + std::uint32_t words[2]; + std::memcpy(words, owner->markerMapping, sizeof(words)); + for (const auto& line : owner->markerLog.Report(words[0], words[1])) std::fprintf(stderr, "%s\n", line.c_str()); + } for (const auto& line : DeviceFaultReport(device, faultInfo, queue, checkpointData)) std::fprintf(stderr, "%s\n", line.c_str()); }; Graphics::DeviceLostOwner() = state.get(); @@ -4703,6 +4746,8 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } const auto gpuTiming = recorder.BeginGpuTiming(record.programAddress != 0 ? record.programAddress : record.shader->program->variantId); if (state->checkpointMarker != nullptr) state->checkpointMarker(commands, reinterpret_cast(static_cast(record.programAddress))); + const auto marker = state->bufferMarker != nullptr ? state->markerLog.Next(record.programAddress) : 0u; + if (marker != 0) state->bufferMarker(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, state->markerBuffer, 0, marker); if (argumentImport != nullptr) { context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, indirectBuffer, indirectOffset); } else { @@ -4739,6 +4784,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } } recorder.EndGpuTiming(gpuTiming); + if (marker != 0) state->bufferMarker(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, state->markerBuffer, sizeof(std::uint32_t), marker); const auto trailingTiming = recorder.BeginGpuTiming(CommandClass::DispatchTrailing); constexpr VkAccessFlags dispatchedAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT; Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, dispatchedAccess); diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceFault.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceFault.cpp index 67475a41de..aceac056e4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeviceFault.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceFault.cpp @@ -97,6 +97,35 @@ void Nothing() { Require(faultCalls == 0, "the fault info was queried without the extension"); } +void BufferMarkers() { + AgcDriver::BufferMarkerLog log; + Require(log.Report(0, 0) == std::vector{"[device-fault] no buffer marker reached"}, "an untouched marker buffer was not reported"); + Require(log.Next(0x10000) == 1 && log.Next(0x10100) == 2 && log.Next(0x10200) == 3, "markers are not numbered from 1"); + const auto lines = log.Report(3, 1); + Require(lines.size() == 4, "a hang two markers past the last finished one does not get four lines"); + Require(lines[0] == "[device-fault] last dispatch reached: marker 3, program 0x10200", "the reached line changed"); + Require(lines[1] == "[device-fault] last dispatch finished: marker 1, program 0x10000", "the finished line changed"); + Require(lines[2] == "[device-fault] unfinished dispatch: marker 2, program 0x10100", "the first unfinished line changed"); + Require(lines[3] == "[device-fault] unfinished dispatch: marker 3, program 0x10200", "the second unfinished line changed"); + Require(log.Report(3, 3).size() == 2, "a finished queue listed unfinished dispatches"); + const auto first = log.Report(1, 0); + Require(first.size() == 3 && first[1] == "[device-fault] no dispatch finished" && first[2] == "[device-fault] unfinished dispatch: marker 1, program 0x10000", "a hang in the first dispatch was not reported as nothing finished"); + Require(log.Report(2, 3).size() == 2, "markers finished past the reached one listed unfinished dispatches"); +} + +void BufferMarkersWrap() { + AgcDriver::BufferMarkerLog log; + for (std::uint32_t i = 0; i <= AgcDriver::BufferMarkerLog::Capacity; ++i) static_cast(log.Next(0x20000 + i)); + const auto lines = log.Report(AgcDriver::BufferMarkerLog::Capacity + 1, 1); + Require(lines[0] == "[device-fault] last dispatch reached: marker 4097, program 0x21000", "the newest marker was not kept"); + Require(lines[1] == "[device-fault] last dispatch finished: marker 1, program not in the log", "an overwritten marker kept a program"); + Require(lines.size() == 2 + AgcDriver::BufferMarkerLog::Listed + 1 && lines.back() == "[device-fault] 4088 more unfinished dispatches", "a full log of unfinished markers was not listed and counted"); + Require(log.Report(AgcDriver::BufferMarkerLog::Capacity + 1, 0).size() == 2, "more unfinished markers than the log holds were listed"); + const auto pending = log.Report(AgcDriver::BufferMarkerLog::Capacity + 1, AgcDriver::BufferMarkerLog::Capacity - 19); + Require(pending.size() == 2 + AgcDriver::BufferMarkerLog::Listed + 1, "a long unfinished run was not cut to the listed count"); + Require(pending.back() == "[device-fault] 12 more unfinished dispatches", "the count of unlisted unfinished dispatches changed"); +} + void AddressKinds() { Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT), "none") == 0, "none"); Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_EXT), "write invalid") == 0, "write invalid"); @@ -115,6 +144,8 @@ int main() { Checkpoints(); Nothing(); AddressKinds(); + BufferMarkers(); + BufferMarkersWrap(); } catch (const std::exception& error) { std::fprintf(stderr, "agc_device_fault: %s\n", error.what()); return 1; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 3d1e5fd416..e4047b02c8 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -168,7 +168,7 @@ Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newl ## Device loss reports -On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST` prints the driver's fault description, the faulting addresses and the vendor fault codes as `[device-fault]` lines. Set `APS5_GPU_CHECKPOINTS=1` to also record a `VK_NV_device_diagnostic_checkpoints` marker with the program address before each compute dispatch; the report then lists the last markers the GPU reached. Checkpoints are disabled by default and ignored on devices without the extension. +On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST` prints the driver's fault description, the faulting addresses and the vendor fault codes as `[device-fault]` lines. Set `APS5_GPU_CHECKPOINTS=1` to also record a `VK_NV_device_diagnostic_checkpoints` marker with the program address before each compute dispatch; the report then lists the last markers the GPU reached. On a device without that extension but with `VK_AMD_buffer_marker` (RADV and AMD's own drivers), each compute dispatch instead writes a numbered marker into a host-visible buffer when the GPU reaches it and another when it finishes; the report then names the programs of the last dispatch reached, of the last one finished and of up to eight dispatches between them. Checkpoints are disabled by default and ignored on devices without the extension. ## Worker sampler diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 6c0cd7f815..5dbaf66265 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -141,7 +141,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_FLIP_READ_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the in-place reads of completed batches and checks them before the present blit's fence | | `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp` | | makes the control-flow test expect the reach-flag fallback instead of no cloning | | `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | -| `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints | +| `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints, or buffer markers through VK_AMD_buffer_marker on a device without it | | `APS5_GPU_COPY_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on a descriptor sub-range copied through the GPU path, larger ones staying on the CPU | | `APS5_GPU_NO_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | skips writing GPU results back into guest memory | | `APS5_GPU_WAIT_TIMEOUT_MS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | 1000 | milliseconds a GPU wait may run before a warning is printed | From c0b68e4434670c407f5b726ae76b1aca9a8520b5 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:31:17 +0200 Subject: [PATCH 0012/1360] fix(agc): accept the REWIND offload-enable payload bit A REWIND packet's payload carries a valid bit (31) and an offload-enable bit (24) that lets the CP offload polling of the valid bit. The validator rejected every bit but 31, so a title that writes both (0x81000000, PPSA15246) stopped with 'unsupported REWIND payload bits'. The driver waits on bit 31 only, so bit 24 needs no other handling. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp | 1 + core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index f902632e1a..3e843caf63 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -153,6 +153,7 @@ inline bool CopyDataImmediate(std::uint32_t source) { return source >= 10 && sou inline bool CopyDataToRegister(std::span packet) { return ((packet[1] >> 8u) & 0xfu) == 0; } inline std::size_t CopyDataBytes(std::span packet) { return (packet[1] & 0x10000u) != 0 ? 8 : 4; } inline constexpr std::uint32_t RewindValid = 0x80000000u; +inline constexpr std::uint32_t RewindOffloadEnable = 0x01000000u; struct WaitFields { std::uint32_t function; std::uint64_t address; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index aec2b3069b..b0dff66fc3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -450,7 +450,7 @@ void Validate(std::span packet, std::uint32_t queue) { } case 0x59: size(2); - require((packet[1] & 0x7fffffffu) == 0, "unsupported REWIND payload bits"); + require((packet[1] & ~(RewindValid | RewindOffloadEnable)) == 0, "unsupported REWIND payload bits"); break; case 0x3c: case 0x93: { size(opcode == 0x3c ? 7 : 9); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index a08d2a3b7e..b3e3685e4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -1105,6 +1105,11 @@ void testConditionalSubmission() { check(results[13] == 0, "a rejected conditional submission executed a guarded packet"); } +void testRewindPayloadBits() { + for (const auto payload : {0x00000000u, 0x80000000u, 0x01000000u, 0x81000000u}) AgcDriver::Pm4::Validate(makePacket(0x59, {payload}), 0); + for (const auto payload : {0x82000000u, 0x80000001u, 0x00800000u}) expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x59, {payload}), 0); }, "unsupported REWIND payload bits"); +} + void testNopPadSubmission() { alignas(8) static std::uint32_t zero = 0; static std::array results{}; @@ -1824,6 +1829,7 @@ int main(int argc, char** argv) { } testCatalog(); testNopPad(); + testRewindPayloadBits(); testWriteChangedKeepsUntouchedBytes(); testWriteChangedStoresExactlyTheChangedRuns(); testRegisters(); From 054cf422026b7ac81e37deb14db2cbcb0b53904a Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 12:01:48 +0000 Subject: [PATCH 0013/1360] fix(agc): give copied guest buffer regions transfer-source usage A guest buffer region that is copied rather than imported gets a buffer with storage (and, when addressable, device-address) usage only. Host imports, sparse imports and chunk imports carry VK_BUFFER_USAGE_TRANSFER_SRC_BIT, so a buffer view of imported guest memory can be read back with vkCmdCopyBuffer, but not one of a copied region. agc_sparse_run_import_copied does that readback, and the Khronos validation layer reports VUID-vkCmdCopyBuffer-srcBuffer-00118 for it. Copied regions now carry transfer-source usage too. The sparse buffer hole tests seed the buffer pool with the usage a copied region asks for, so their seed buffer gets transfer-source usage as well. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 2 +- .../prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 880056417d..092d270211 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4450,7 +4450,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { void GuestBufferMemory::copyRegion(Region& region, bool addressable) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto bytes = region.end - region.begin; - const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); std::uint64_t readBytes = 0; bool inImport = false; if (profile && context.hostImportAlignment != 0) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index d8fffa7de3..6ce939353e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1826,7 +1826,7 @@ bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& acces std::memset(reservation.data + 2 * unit, 0x72, unit); VkBuffer seeded = VK_NULL_HANDLE; { - Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5}); seeded = seed.Handle(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp index a7a25f9f88..64835ea370 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -325,7 +325,7 @@ void Run(const Context& context, const CopyPipeline& pipeline, bool writable, co std::memset(readback.Bytes().data(), 0xcc, BufferBytes); VkBuffer primed = VK_NULL_HANDLE; { - Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); primed = earlier.Handle(); std::memset(earlier.Bytes().data(), 0xa5, BufferBytes); } From 12bc5b14a6531d07a9e6fece32347f696cca7061 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Mon, 19 Oct 2026 17:42:47 +0400 Subject: [PATCH 0014/1360] fix(agc): roll back failed snapshot cache insertions --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 +++ .../Graphics/include/SnapshotCopies.hpp | 55 ++++++++++++++ .../Graphics/src/GuestBufferMemory.cpp | 30 ++------ .../tests/SnapshotCopyFailure.cpp | 74 +++++++++++++++++++ 4 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/SnapshotCopyFailure.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 65f1aff6c3..10ffec0c2d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1004,6 +1004,17 @@ if(BUILD_TESTING) endif() add_test_executable(agc_buffer_pool_tests EXCLUDE_FROM_ALL tests/BufferPool.cpp) + +if(BUILD_TESTING) + add_test_executable(agc_snapshot_copy_failure_tests tests/SnapshotCopyFailure.cpp tests/AllocationFailure.cpp) + target_include_directories(agc_snapshot_copy_failure_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + if(MINGW) + target_compile_options(agc_snapshot_copy_failure_tests PRIVATE -fasynchronous-unwind-tables) + endif() + configure_windows_unwind(agc_snapshot_copy_failure_tests) + add_test(NAME agc_snapshot_copy_failure COMMAND agc_snapshot_copy_failure_tests) + set_tests_properties(agc_snapshot_copy_failure PROPERTIES TIMEOUT 10) +endif() target_sources(agc_buffer_pool_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp) target_include_directories(agc_buffer_pool_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_buffer_pool_tests PRIVATE libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp new file mode 100644 index 0000000000..f60465ac09 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp @@ -0,0 +1,55 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SNAPSHOTCOPIES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SNAPSHOTCOPIES_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +class Buffer; + +namespace Detail { +struct SnapshotCopies { + using Key = std::tuple; + static constexpr std::size_t Budget = std::size_t{16} << 20u; + static constexpr std::size_t MaxEntries = 1024; + std::mutex mutex; + VkDevice device = VK_NULL_HANDLE; + struct Entry { + std::shared_ptr buffer; + std::vector bytes; + std::list::iterator recent; + }; + std::map entries; + std::list recency; + std::size_t bytes = 0; + + void Insert(const Key& key, const std::shared_ptr& buffer, std::span data) { + recency.push_back(key); + try { + entries.emplace(key, Entry{buffer, {data.begin(), data.end()}, std::prev(recency.end())}); + } catch (...) { + recency.pop_back(); + throw; + } + bytes += data.size(); + } + + void Clear() { + entries.clear(); + recency.clear(); + bytes = 0; + } +}; +} +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 880056417d..587c7ac4ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "ThreadOwned.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" @@ -1912,26 +1913,7 @@ ImageMirrors& Mirrors() { return mirrors; } -struct SnapshotCopies { - using Key = std::tuple; - static constexpr std::size_t Budget = std::size_t{16} << 20u; - static constexpr std::size_t MaxEntries = 1024; - std::mutex mutex; - VkDevice device = VK_NULL_HANDLE; - struct Entry { - std::shared_ptr buffer; - std::vector bytes; - std::list::iterator recent; - }; - std::map entries; - std::list recency; - std::size_t bytes = 0; - void clear() { - entries.clear(); - recency.clear(); - bytes = 0; - } -}; +using SnapshotCopies = Detail::SnapshotCopies; SnapshotCopies& SnapshotCopyCache() { static SnapshotCopies copies; @@ -1942,7 +1924,7 @@ std::shared_ptr snapshotCopy(const Context& context, std::uint64_t addre auto& copies = SnapshotCopyCache(); std::lock_guard lock(copies.mutex); if (copies.device != context.device) { - copies.clear(); + copies.Clear(); copies.device = context.device; } const SnapshotCopies::Key key{address, bytes.size(), usage}; @@ -1964,9 +1946,7 @@ std::shared_ptr snapshotCopy(const Context& context, std::uint64_t addre copies.entries.erase(oldest); copies.recency.pop_front(); } - copies.recency.push_back(key); - copies.entries.emplace(key, SnapshotCopies::Entry{buffer, {bytes.begin(), bytes.end()}, std::prev(copies.recency.end())}); - copies.bytes += bytes.size(); + copies.Insert(key, buffer, bytes); return buffer; } @@ -2868,7 +2848,7 @@ void ClearSnapshotCopies(VkDevice device) { auto& copies = SnapshotCopyCache(); std::lock_guard lock(copies.mutex); if (copies.device != device) return; - copies.clear(); + copies.Clear(); copies.device = VK_NULL_HANDLE; } diff --git a/core/libs/prx/libSceAgcDriver/tests/SnapshotCopyFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/SnapshotCopyFailure.cpp new file mode 100644 index 0000000000..f302ebf9af --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SnapshotCopyFailure.cpp @@ -0,0 +1,74 @@ +#include "prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp" +#include "AllocationFailure.hpp" +#include +#include +#include + +namespace { +using AgcDriver::Graphics::Detail::SnapshotCopies; +using AgcDriver::Tests::SetAllocationFailureCountdown; + +bool Consistent(const SnapshotCopies& copies) { + if (copies.entries.size() != copies.recency.size()) return false; + std::size_t bytes = 0; + for (auto it = copies.recency.begin(); it != copies.recency.end(); ++it) { + const auto found = copies.entries.find(*it); + if (found == copies.entries.end() || found->second.recent != it) return false; + bytes += found->second.bytes.size(); + } + return bytes == copies.bytes; +} + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckInsertionFailures() { + const std::array data{std::byte{0x37}}; + unsigned failures = 0; + for (int point = 0; point < 4; ++point) { + SnapshotCopies copies; + const SnapshotCopies::Key first{0x1000, data.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT}; + const SnapshotCopies::Key second{0x2000, data.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT}; + auto owner = std::make_shared(7); + const std::shared_ptr buffer(owner, nullptr); + copies.Insert(first, buffer, data); + bool failed = false; + SetAllocationFailureCountdown(point); + try { + copies.Insert(second, buffer, data); + } catch (const std::bad_alloc&) { + failed = true; + ++failures; + } + SetAllocationFailureCountdown(-1); + Require(Consistent(copies), "failed snapshot insertion left inconsistent eviction metadata"); + Require(copies.entries.at(first).bytes == std::vector(data.begin(), data.end()), "failed insertion changed an existing snapshot"); + Require(owner.use_count() == (failed ? 3 : 4), "snapshot insertion leaked or lost a buffer reference"); + if (failed) { + Require(copies.entries.size() == 1 && copies.bytes == data.size(), "failed insertion changed cache accounting"); + copies.Insert(second, buffer, data); + } + Require(Consistent(copies) && copies.entries.size() == 2, "cache could not recover after allocation failure"); + const auto oldest = copies.entries.find(copies.recency.front()); + copies.bytes -= oldest->second.bytes.size(); + copies.entries.erase(oldest); + copies.recency.pop_front(); + Require(Consistent(copies) && copies.entries.contains(second), "eviction after insertion failure corrupted another snapshot"); + copies.Clear(); + Require(Consistent(copies) && copies.bytes == 0 && owner.use_count() == 2, "snapshot teardown retained buffers or stale accounting"); + } + Require(failures == 3, "did not exercise every snapshot insertion allocation"); +} +} + +int main() { + try { + CheckInsertionFailures(); + std::puts("Snapshot insertion allocation failures preserve eviction metadata and buffer ownership"); + } catch (const std::exception& error) { + SetAllocationFailureCountdown(-1); + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From 2e5004d2a32dbf29c798a4915313d881f9c16b6d Mon Sep 17 00:00:00 2001 From: klosixgaming-arch Date: Mon, 19 Oct 2026 07:09:47 -0500 Subject: [PATCH 0015/1360] fix(shader): resolve table column pointers through Phi nodes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 42 +++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 46 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e788638438..058046a306 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3042,6 +3042,18 @@ void verifyPointerImageTable() { require(!TableTouched(narrowTable.regions, root + (Palette + 32u) / 4u, 32u) && !TableTouched(narrowTable.regions, root + Committed / 4u, Reserved - Committed), "pointer image table: a record outside the key domain was read"); const std::vector> narrowReads{{base + Palette, 32u}, {base + Palette + 2u * 32u, 32u}, {base + Palette + 255u * 32u, 32u}, {base + Palette + 0x102u * 32u, 32u}}; require(narrowSnapshot.readRanges == narrowReads && narrowTable.result->imageTableReadRanges.size() == 1u && narrowTable.result->imageTableReadRanges[0] == narrowReads, "pointer image table: consumed ranges include skipped keys or the key-domain buffer"); + + auto phiCode = maskedCode; + phiCode.erase(phiCode.begin() + 8, phiCode.begin() + 10); + phiCode.insert(phiCode.begin() + 8, { + 0xbf068010u, 0xbf850003u, 0xf4200444u, 0x20000000u, 0xbf820002u, 0xf4200444u, 0x20000000u + }); + const auto phiRequest = request(phiCode, 0x36000u); + const auto phiPlan = GetResourcePlan(phiRequest); + require(phiPlan->info.images.size() == 1u && phiPlan->info.images[0].table == 0u, "pointer image table: a pointer read through a phi was not planned as a table"); + const auto phiTable = CompileTable(phiRequest); + require(phiTable.capture->snapshot.tables.tables.at(0).codes == snapshot.codes, "pointer image table: a pointer read through a phi captured different records"); + keys = {}; #ifdef _WIN32 VirtualFree(block, 0, MEM_RELEASE); @@ -3445,6 +3457,36 @@ void verifyPointerChainImageTable() { static_cast(ValidateAndOptimizeSpirv(compiled.result->spirv, request.target.vulkanVersion, request.target.spirvVersion)); #endif store(links[1].data() + 4u, address(descriptors[1].data())); + + auto phiCode = code; + phiCode.erase(phiCode.begin() + 15, phiCode.begin() + 17); + phiCode.insert(phiCode.begin() + 15, { + 0xbf068010u, 0xbf850003u, 0xf4240b02u, 0x24000008u, 0xbf820002u, 0xf4240b02u, 0x24000008u + }); + auto phiRequest = TableRequest(phiCode, userData); + phiRequest.shader.codeAddress = 0x38000u; + const auto phiPlan = GetResourcePlan(phiRequest); + require(phiPlan->info.images.size() == 1u && phiPlan->info.images[0].table == 0u, "pointer chain image table: a pointer read through a phi was not planned as a table"); + const auto phiTable = CompileTable(phiRequest); + require(phiTable.capture->snapshot.tables.tables.at(0).codes == snapshot.codes, "pointer chain image table: a pointer read through a phi captured different records"); + + auto divergentCode = code; + divergentCode.erase(divergentCode.begin() + 15, divergentCode.begin() + 17); + divergentCode.insert(divergentCode.begin() + 15, { + 0xbf068010u, 0xbf850003u, 0xf4240b02u, 0x24000008u, 0xbf820002u, 0xf4240b02u, 0x24000018u + }); + auto divergentRequest = TableRequest(divergentCode, userData); + divergentRequest.shader.codeAddress = 0x39000u; + expectFailure([&] { static_cast(GetResourcePlan(divergentRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi reading different table offsets was planned as a table"); + + auto nonTableCode = code; + nonTableCode.erase(nonTableCode.begin() + 15, nonTableCode.begin() + 17); + nonTableCode.insert(nonTableCode.begin() + 15, { + 0xbf068010u, 0xbf850003u, 0xf4240b02u, 0x24000008u, 0xbf820002u, 0xbe960380u, 0xbe970380u + }); + auto nonTableRequest = TableRequest(nonTableCode, userData); + nonTableRequest.shader.codeAddress = 0x3a000u; + expectFailure([&] { static_cast(GetResourcePlan(nonTableRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi with a non-table branch was planned as a table"); } void verifyDescriptorPhis() { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 7306a87c0e..ceb8017633 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -691,6 +691,47 @@ private: return true; } + IrValue* UnwrapTableRead(IrValue* value) const { + if (value == nullptr) return nullptr; + value = value->Resolve(); + if (!value->IsPhi()) return value; + IrValue* candidate = nullptr; + std::vector pending{value}; + std::unordered_set visited; + while (!pending.empty()) { + auto* current = pending.back()->Resolve(); + pending.pop_back(); + if (!visited.insert(current).second) continue; + if (current->IsPhi()) { + for (std::size_t i = 0; i < current->ArgumentCount(); i++) { + pending.push_back(current->Argument(i)); + } + continue; + } + std::uint32_t memoryIndex = 0; + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (ScalarReadMemory(*current, memoryIndex) == nullptr || !MemoryIndexBelongsTo(memoryIndex, *current) || + !MatchAffineOffset(current->Argument(1), key, stride, addend)) { + return nullptr; + } + if (candidate == nullptr) { + candidate = current; + } else if (!EquivalentValue(m_program.Resources(), candidate, current)) { + std::uint32_t candidateIndex = 0; + const auto* candMem = ScalarReadMemory(*candidate, candidateIndex); + const auto* curMem = ScalarReadMemory(*current, memoryIndex); + if (candMem == nullptr || curMem == nullptr || candMem->offset != curMem->offset || + !EquivalentValue(m_program.Resources(), candidate->Argument(0), current->Argument(0)) || + !EquivalentValue(m_program.Resources(), candidate->Argument(1), current->Argument(1))) { + return nullptr; + } + } + } + return candidate; + } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& descriptorPointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { std::vector hops; IrValue* pointer = &descriptorPointer; @@ -728,7 +769,10 @@ private: IrValue* offset = nullptr; std::uint32_t first = 0; for (std::uint32_t dword = 0; dword < 2u; dword++) { - IrValue* read = pointer->Argument(dword)->Resolve(); + IrValue* read = UnwrapTableRead(pointer->Argument(dword)); + if (read == nullptr) { + return false; + } std::uint32_t memoryIndex = 0; const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { From 1ee2996829793ac55a545bb7334ae176ce85f4c9 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Mon, 19 Oct 2026 09:53:34 -0400 Subject: [PATCH 0016/1360] perf(agc): skip CPU-path indirect draws that draw nothing before building their stages An indirect draw read its count and records only after every stage had been captured and compiled, so a draw whose records all have no vertices or no instances (what GPU-side culling leaves for most draws in a culled scene) cost a full stage build to draw nothing. When the front program's last indirect draw took the CPU path, the draw now reads its count and records first, through the same flush hook, and returns before the stage build when no record draws. APS5_NO_EMPTY_INDIRECT_SKIP=1 builds them as before. The [draws] profile line counts the skipped draws. agc_driver_empty_indirect_skip_tests draws one record, two empty ones and one more, and checks that the empty ones were skipped and the last one draws as the first. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/Draw.cpp | 21 +++ .../libSceAgcDriver/Graphics/include/Draw.hpp | 2 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 20 ++- .../tests/execution/EmptyIndirectSkip.cpp | 162 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 1 + 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/EmptyIndirectSkip.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index be47da3113..072e7d966c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace AgcDriver::DriverDetail { @@ -184,6 +185,25 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value; const std::vector& programs = writesPrograms ? writablePrograms : decode->programs; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); + static const bool emptyIndirectSkip = std::getenv("APS5_NO_EMPTY_INDIRECT_SKIP") == nullptr; + thread_local std::unordered_map cpuIndirectPrograms; + const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); }; + if (emptyIndirectSkip && drawParameters.indirect && !programs.empty()) { + const auto known = cpuIndirectPrograms.find(programs.front().binary.codeAddress); + if (known != cpuIndirectPrograms.end() && known->second) { + const auto& indirect = *drawParameters.indirect; + recordQueuedLabelsBeforeRead(submission.queue); + const auto readStart = std::chrono::steady_clock::now(); + const auto count = std::min(indirect.countIndirect ? Pm4::ReadDrawCount(indirect) : indirect.count, indirect.count); + bool any = false; + for (std::uint32_t record = 0; record < count && !any; ++record) any = recordDraws(Pm4::ReadDrawArguments(indirect, record)); + if (!any) { + Graphics::CountEmptyIndirectDraw(std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); + timing.Mark("empty_indirect_skipped"); + return drawn(); + } + } + } const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; auto& words = writablePrograms.front().userData; @@ -410,6 +430,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { indirectCpu = ClassifyIndirectDraw(result, graphics, programs.front(), localDevice, drawParameters, traceIndirect); + if (emptyIndirectSkip) cpuIndirectPrograms[programs.front().binary.codeAddress] = indirectCpu.has_value(); } else if (i == 0) { fold(result, drawParameters); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 8e7623ea87..630c3a8d26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -94,6 +94,8 @@ bool DrawRecipes(); enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, IndexRange, Count }; void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false); std::array(IndirectDrawPath::Count)> IndirectDrawCounts(); +void CountEmptyIndirectDraw(double readMs); +std::uint64_t EmptyIndirectDrawCount(); struct DrawPassCounts { std::uint64_t draws = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 1fb537a91b..28113cc089 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -460,6 +460,7 @@ struct DrawProfile { // with a constant, and the time of the CPU record reads (their syncs). std::array indirect{}; std::uint64_t indirectRewritten = 0; + std::uint64_t indirectEmpty = 0; double indirectReadUs = 0; // Draw packets that drew nothing, by DrawSkip, and their time. std::array(DrawSkip::Count)> skips{}; @@ -557,12 +558,12 @@ void reportDraw(const std::array& us, const ShaderResources: if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast(n), "; hook waits inside draws %.1f ms (max %.1f; the draws' own syncs, %.1f ms, not counted)", profile.hookWaitUs / 1000.0, profile.maxHookWaitUs / 1000.0, profile.ownSyncUs / 1000.0); std::uint64_t indirectCpu = 0; for (std::size_t i = 1; i < IndirectPathCount; ++i) indirectCpu += profile.indirect[i]; - if ((profile.indirect[0] != 0 || indirectCpu != 0) && room()) { + if ((profile.indirect[0] != 0 || indirectCpu != 0 || profile.indirectEmpty != 0) && room()) { n += std::snprintf(line + n, sizeof(line) - static_cast(n), "; indirect: gpu-side %llu (%llu rewritten), cpu-side %llu (", static_cast(profile.indirect[0]), static_cast(profile.indirectRewritten), static_cast(indirectCpu)); for (std::size_t i = 1; i < IndirectPathCount && room(); ++i) { if (profile.indirect[i] != 0) n += std::snprintf(line + n, sizeof(line) - static_cast(n), " %s %llu", IndirectDrawPathNames[i], static_cast(profile.indirect[i])); } - if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast(n), "), argument reads %.1f ms", profile.indirectReadUs / 1000.0); + if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast(n), "), empty skipped before their stages %llu, argument reads %.1f ms", static_cast(profile.indirectEmpty), profile.indirectReadUs / 1000.0); } if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast(n), "; skipped packets:"); for (std::size_t i = 0; i < profile.skips.size() && room(); ++i) { @@ -869,6 +870,21 @@ std::array IndirectDrawCounts() { return stats.indirect; } +void CountEmptyIndirectDraw(double readMs) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) return; + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + ++stats.indirectEmpty; + stats.indirectReadUs += readMs * 1000.0; +} + +std::uint64_t EmptyIndirectDrawCount() { + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + return stats.indirectEmpty; +} + std::array DrawPassTotals() { auto& stats = Profile(); std::lock_guard lock(stats.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmptyIndirectSkip.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmptyIndirectSkip.cpp new file mode 100644 index 0000000000..6a85e68f65 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmptyIndirectSkip.cpp @@ -0,0 +1,162 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; +constexpr std::uint32_t BaseVertexLocation = 0x8c; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array FirstMemory{}; +alignas(256) std::array NoInstancesMemory{}; +alignas(256) std::array NoVerticesMemory{}; +alignas(256) std::array LastMemory{}; +alignas(256) std::array Records{3, 1, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, 3, 1, 0, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawFour() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto records = reinterpret_cast(Records.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x11, {1, static_cast(records), static_cast(records >> 32u)}); + const auto draw = [&](std::array& memory, std::uint32_t recordOffset) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x76, {BaseVertexLocation, 0}); + Append(commands, 0x24, {recordOffset, BaseVertexLocation, 0, 2}); + }; + draw(FirstMemory, 0); + draw(NoInstancesMemory, 16); + draw(NoVerticesMemory, 32); + draw(LastMemory, 48); + return commands; +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +void EmptyIndirectDrawsSkipTheirStages() { + FirstMemory.fill(Kept); + NoInstancesMemory.fill(Kept); + NoVerticesMemory.fill(Kept); + LastMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawFour(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto first = Covered(FirstMemory); + Require(first != 0, "the first indirect draw, whose record draws 3 vertices, covered nothing"); + Require(Covered(NoInstancesMemory) == 0, "the indirect draw whose record has no instances covered " + std::to_string(Covered(NoInstancesMemory)) + " bytes, expected none"); + Require(Covered(NoVerticesMemory) == 0, "the indirect draw whose record has no vertices covered " + std::to_string(Covered(NoVerticesMemory)) + " bytes, expected none"); + for (std::size_t index = 0; index < FirstMemory.size(); ++index) { + Require(LastMemory[index] == FirstMemory[index], "byte " + std::to_string(index) + " of the indirect draw after the empty ones is " + std::to_string(static_cast(LastMemory[index])) + ", the first indirect draw wrote " + std::to_string(static_cast(FirstMemory[index]))); + } + const auto empty = AgcDriver::Graphics::EmptyIndirectDrawCount(); + Require(empty == 2, std::to_string(empty) + " of the 2 indirect draws whose records draw nothing were skipped before their stages were built"); + const auto counts = AgcDriver::Graphics::IndirectDrawCounts(); + std::uint64_t cpu = 0; + for (std::size_t path = 1; path < counts.size(); ++path) cpu += counts[path]; + Require(counts[0] == 0 && cpu == 2, "the drawing indirect draws took the GPU-side path " + std::to_string(counts[0]) + " times and the CPU path " + std::to_string(cpu) + " times, expected the CPU path twice"); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + EmptyIndirectDrawsSkipTheirStages(); + AgcDriverShutdown_nid_postfix(); + std::puts("CPU-path indirect draws whose records draw nothing were skipped before their stages were built"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index c7f4bf12e4..cec6086c27 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -217,6 +217,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_DRAW_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds every draw's shader resources instead of reusing a cached identical build | | `APS5_NO_DRAW_SRT_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables the draw cache's SRT entries, so every draw decodes its programs | | `APS5_NO_DRAW_STAGE_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in a stage's ShaderData words | +| `APS5_NO_EMPTY_INDIRECT_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | builds the stages of an indirect draw whose front program last took the CPU path even when none of its records draws anything, instead of reading its records first and skipping it | | `APS5_NO_EPOCH_COLLECT_MAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | drops the per-epoch map of completed collects, leaving only the fixed-size thread memo to answer a repeated collect | | `APS5_NO_EPOCH_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs every Revalidate's per-element and per-region checks instead of the epoch gate | | `APS5_NO_FAST_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | always repeats the resource lookups instead of using the fast proof | From 021175cca7e4452214e97991039dd0aae8d49b4d Mon Sep 17 00:00:00 2001 From: klosixgaming-arch Date: Mon, 19 Oct 2026 07:20:07 -0500 Subject: [PATCH 0017/1360] fix(shader): deduplicate sampler table elements in ResolveImageTables Deduplicate sampler descriptor records across image tables during table resolution so identical sampler descriptors share element table indices rather than exceeding the hardware sampler element budget. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 80 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 34 +++++--- 2 files changed, 103 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e788638438..0cd4cc9ec7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7140,6 +7140,85 @@ void verifyVolumeBiasTableRejection() { } } +void verifySamplerTableDeduplication() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + SamplerResource sampler0{}; + sampler0.table = 0u; + SamplerResource sampler1{}; + sampler1.table = 1u; + ShaderInfo info{}; + info.samplers.push_back(sampler0); + info.samplers.push_back(sampler1); + ResourceSnapshot snapshot{}; + snapshot.tables.tables.resize(2u); + const DescriptorValue s0{{0x00000600u, 0x00fff000u, 0x06f03fa0u, 0u}, 4u}; + const DescriptorValue s1{{0x00000700u, 0x00fff000u, 0x06f03fa0u, 0u}, 4u}; + const DescriptorValue s2{{0x00000800u, 0x00fff000u, 0x06f03fa0u, 0u}, 4u}; + std::vector codes(300u); + for (std::size_t i = 0; i < 300u; ++i) codes[i] = static_cast(i % 3u); + for (std::uint32_t table = 0u; table < 2u; ++table) { + auto& column = snapshot.tables.tables[table]; + column.records = 300u; + column.keys = 300u; + column.codes = SharedValues(codes); + column.words = SharedValues(std::vector{s0, s1, s2}); + } + const auto resolved = ResourceMaterializer::ResolveImageTables(info, snapshot); + require(resolved.samplers.size() == (3u + 1u) * Abi::SamplerEntryElements, "sampler table deduplication: duplicate sampler entries across tables were not deduplicated"); + for (std::uint32_t table = 0u; table < 2u; ++table) { + const auto mapStart = resolved.map.at(Abi::TableHeader(table) + Abi::TableMapStart); + for (std::uint32_t key = 0; key < 300u; ++key) { + const auto record = resolved.map.at(mapStart + key); + const auto element = resolved.map.at(record + Abi::SamplerRecordElement); + require(element == (key % 3u) * Abi::SamplerEntryElements, "sampler table deduplication: record points to the wrong deduplicated sampler element"); + require((resolved.map.at(record + Abi::SamplerRecordFlags) & Abi::PoisonFlag) == 0u, "sampler table deduplication: deduplicated sampler was poisoned"); + } + } + + constexpr std::uint32_t MaxUnique = Abi::SamplerElementBudget / Abi::SamplerEntryElements; + std::vector budgetWords; + budgetWords.reserve(MaxUnique + 1u); + for (std::uint32_t i = 0; i < MaxUnique + 1u; ++i) { + budgetWords.push_back(DescriptorValue{{0x00000600u, 0x00fff000u, 0x06f03fa0u, i + 1u}, 4u}); + } + std::vector budgetCodes(MaxUnique + 2u); + for (std::uint32_t i = 0; i < MaxUnique; ++i) budgetCodes[i] = i; + budgetCodes[MaxUnique] = 0u; + budgetCodes[MaxUnique + 1u] = MaxUnique; + + ShaderInfo budgetInfo{}; + SamplerResource budgetSampler{}; + budgetSampler.table = 0u; + budgetInfo.samplers.push_back(budgetSampler); + ResourceSnapshot budgetSnapshot{}; + budgetSnapshot.tables.tables.resize(1u); + auto& budgetColumn = budgetSnapshot.tables.tables[0]; + budgetColumn.records = static_cast(budgetCodes.size()); + budgetColumn.keys = static_cast(budgetCodes.size()); + budgetColumn.codes = SharedValues(budgetCodes); + budgetColumn.words = SharedValues(budgetWords); + + const auto budgetResolved = ResourceMaterializer::ResolveImageTables(budgetInfo, budgetSnapshot); + require(budgetResolved.samplers.size() == Abi::SamplerElementBudget, "sampler table budget: valid unique samplers did not fill budget"); + const auto budgetMapStart = budgetResolved.map.at(Abi::TableHeader(0) + Abi::TableMapStart); + + const auto dupRecord = budgetResolved.map.at(budgetMapStart + MaxUnique); + const auto dupFlags = budgetResolved.map.at(dupRecord + Abi::SamplerRecordFlags); + require((dupFlags & Abi::PoisonFlag) == 0u, "sampler table budget: duplicate of existing sampler was poisoned when budget is full"); + require(budgetResolved.map.at(dupRecord + Abi::SamplerRecordElement) == 0u, "sampler table budget: duplicate sampler did not reuse existing element"); + + const auto overflowRecord = budgetResolved.map.at(budgetMapStart + MaxUnique + 1u); + const auto overflowFlags = budgetResolved.map.at(overflowRecord + Abi::SamplerRecordFlags); + require((overflowFlags & Abi::PoisonFlag) != 0u, "sampler table budget: exceeding sampler budget did not poison record"); + require(budgetResolved.poison.at(overflowFlags & ~Abi::PoisonFlag).reason == Abi::PoisonReason::Budget, "sampler table budget: poisoned reason is not Budget"); + + const auto outsideRecord = budgetResolved.map.at(Abi::TableHeader(0) + Abi::TableOutsideCode); + const auto outsideFlags = budgetResolved.map.at(outsideRecord + Abi::SamplerRecordFlags); + require((outsideFlags & Abi::PoisonFlag) != 0u, "sampler table budget: TableOutsideCode did not poison when budget is full"); + require(budgetResolved.poison.at(outsideFlags & ~Abi::PoisonFlag).reason == Abi::PoisonReason::Budget, "sampler table budget: TableOutsideCode poisoned reason is not Budget"); +} + void verifyDynamicStorageMipChain() { using namespace ShaderRecompiler; ImageResource image{}; @@ -7328,6 +7407,7 @@ int main(int argc, char** argv) { verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); verifyVolumeBiasTableRejection(); + verifySamplerTableDeduplication(); verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); verifyQuadMaskNeighbourReads(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5b869a4c9e..1d781cfdea 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -633,6 +633,7 @@ private: struct WordsIndexTag {}; struct ElementIndexTag {}; +struct SamplerIndexTag {}; struct PoisonIndexTag {}; struct TableRecordsTag {}; @@ -1654,6 +1655,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i return Abi::PoisonCode(*index); }; HashScratchIndex elementIndex{threadScratch()}; + HashScratchIndex samplerIndex{threadScratch()}; std::vector directSamplers(info.images.size(), 0u); for (const auto& pair : info.sampledPairs) { if (info.samplers.at(pair.sampler).table == NoTable) directSamplers.at(pair.image) |= 1u << pair.sampler; @@ -1711,19 +1713,29 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Unnormalized, &words); } else if (((words.dwords[0] >> 29u) & 3u) != 0u) { record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Reduction, &words); - } else if (out.samplers.size() + Abi::SamplerEntryElements > Abi::SamplerElementBudget) { - record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { - record[Abi::SamplerRecordElement] = static_cast(out.samplers.size()); - for (std::uint32_t variant = 0; variant < 2u; variant++) { - const bool compare = variant == 1u; - for (std::uint32_t point = 0; point < 2u; point++) { - ImageTableSamplerElement element; - std::copy_n(words.dwords.begin(), 4u, element.words.begin()); - element.compare = compare; - element.point = point != 0u; - out.samplers.push_back(element); + const auto hash = hashWords(std::span(words.dwords.data(), 4u), 0); + auto index = samplerIndex.Find(hash, [&](std::uint32_t known) { + return std::equal(words.dwords.begin(), words.dwords.begin() + 4u, out.samplers[known].words.begin()); + }); + if (!index.has_value() && out.samplers.size() + Abi::SamplerEntryElements > Abi::SamplerElementBudget) { + record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Budget, &words); + } else { + if (!index.has_value()) { + index = static_cast(out.samplers.size()); + for (std::uint32_t variant = 0; variant < 2u; variant++) { + const bool compare = variant == 1u; + for (std::uint32_t point = 0; point < 2u; point++) { + ImageTableSamplerElement element; + std::copy_n(words.dwords.begin(), 4u, element.words.begin()); + element.compare = compare; + element.point = point != 0u; + out.samplers.push_back(element); + } + } + samplerIndex.Insert(hash, *index); } + record[Abi::SamplerRecordElement] = *index; } } const auto offset = static_cast(out.map.size()); From c6f44a028a426db11bc77a5a903388e08eaa93f4 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 0018/1360] feat(libkernel): back JIT shared memory with an unlinked temporary file on macOS --- core/libs/prx/libkernel/DirectMemory/Jit.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Jit.cpp b/core/libs/prx/libkernel/DirectMemory/Jit.cpp index fb3ad0405c..968a36641d 100644 --- a/core/libs/prx/libkernel/DirectMemory/Jit.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Jit.cpp @@ -26,6 +26,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #endif namespace { @@ -140,8 +143,19 @@ int DuplicateDescriptor(int descriptor) { } #else int CreateBacking(std::size_t bytes, int* descriptor, std::shared_ptr* object) { +#ifdef __APPLE__ + auto path = (std::filesystem::temp_directory_path() / "anyps5-jit-XXXXXX").string(); + const int file = mkstemp(path.data()); + if (file < 0) return HostFailure("create JIT shared memory", errno); + if (unlink(path.c_str()) != 0 || fcntl(file, F_SETFD, FD_CLOEXEC) != 0) { + const int error = errno; + ::close(file); + return HostFailure("create JIT shared memory", error); + } +#else const int file = memfd_create("anyps5 jit shared memory", MFD_CLOEXEC); if (file < 0) return HostFailure("create JIT shared memory", errno); +#endif const bool representable = bytes <= static_cast(std::numeric_limits::max()); if (!representable || ftruncate(file, static_cast(bytes)) != 0) { const int error = !representable || errno == EFBIG ? ENOMEM : errno; From ca00680339093340e8a0a860cc91772614cf8a51 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 0019/1360] fix(libkernel): make JIT shared memory views executable after mapping them on macOS --- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 8d5b7996ad..c4f5dbfe29 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1150,6 +1150,9 @@ int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { try { #ifdef _WIN32 GuestArena::GuestArenaMap_nid_postfix(mapped, len, object, 0, WinProtFromPosix(nativeProtection)); +#elif defined(__APPLE__) + if (::mmap(mapped, len, nativeProtection & ~PROT_EXEC, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); + if ((nativeProtection & PROT_EXEC) != 0 && ::mprotect(mapped, len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "make shared memory executable"); #else if (::mmap(mapped, len, nativeProtection, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); #endif From 4e5dba2db6ca340c9160e5d686b4b624ce568398 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 0020/1360] test(libkernel): run the JIT shared memory test on macOS --- core/libs/tests/GuestJitSharedMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestJitSharedMemory.cpp b/core/libs/tests/GuestJitSharedMemory.cpp index 66ed8a4838..ea38bffa71 100644 --- a/core/libs/tests/GuestJitSharedMemory.cpp +++ b/core/libs/tests/GuestJitSharedMemory.cpp @@ -187,6 +187,8 @@ int main() { WriteThroughAliasRunsThroughExecutableView(); OneViewWritesAndRuns(); Dup2KeepsTheDescriptorTableInStep(); +#ifndef __APPLE__ MmapOfTheDescriptorSharesTheObject(); +#endif InvalidArgumentsAreRejected(); } From 8a5e742d9dff2e157622c62cabe84da9d1ebc458 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 0021/1360] docs(debt): record the macOS JIT shared memory backing and its Rosetta limits --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..eedb33cbb4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -114,6 +114,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws +- [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. From f6b6d79aa87c5d1a81d057cc9d45ebec1f9d4c6f Mon Sep 17 00:00:00 2001 From: Adam Franke Date: Mon, 19 Oct 2026 12:14:56 -0400 Subject: [PATCH 0022/1360] fix(libkernel): return 0x80020000 | fflags from sceKernelGetEventError as a PS5 does On a PS5 (firmware 13.40) sceKernelGetEventError returned 0 for fflags 0 and 0x80020000 | fflags otherwise, whatever flags and data held: {0x4000, 0, 22} -> 0, {0x4000, 5, 0} -> 0x80020005, {0x4000, 5, 22} -> 0x80020005, {0, 7, 22} -> 0x80020007, {0x8000, 3, 9} -> 0x80020003. fflags above 0xFFFF were not measured and throw. --- .../libkernel/Equeue/src/EventAccessors.cpp | 8 ++++- core/libs/tests/GuestEqueueEvents.cpp | 36 ++++++++++++++++--- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp index e72bfe3900..fb42c6e073 100644 --- a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp @@ -21,7 +21,13 @@ intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev) { int APS5_VABI sceKernelGetEventError(const KernelEvent* ev) { const KernelEvent& event = requireEvent(ev, __func__); - return (event.flags & EV_ERROR) != 0 ? static_cast(event.data) : 0; + if (event.fflags == 0) { + return 0; + } + if (event.fflags > 0xFFFFu) { + throw std::runtime_error(std::string(__func__) + ": fflags " + std::to_string(event.fflags) + " above 0xFFFF"); + } + return static_cast(0x80020000u | event.fflags); } intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev) { diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 8e97329913..c0858dc39a 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -36,6 +36,7 @@ static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; static constexpr int EVFILT_AMPR = -25; static constexpr std::uint16_t EV_ERROR = 0x4000; +static constexpr std::uint16_t EV_EOF = 0x8000; static void Require(bool value) { if (!value) std::abort(); } @@ -49,6 +50,15 @@ static bool RejectsNull(TResult (APS5_VABI *accessor)(const KernelEvent*)) { return false; } +static bool RejectsWideFflags(const KernelEvent* ev) { + try { + sceKernelGetEventError(ev); + } catch (const std::runtime_error&) { + return true; + } + 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) { @@ -291,12 +301,30 @@ int main() { Require(sceKernelGetEventFflags(&rawEvent) == static_cast(0x80000001LL)); Require(sceKernelGetEventData(&rawEvent) == -5); Require(sceKernelGetEventUserData(&rawEvent) == nullptr); - Require(sceKernelGetEventError(&rawEvent) == 0); - rawEvent.flags = static_cast(~EV_ERROR); - Require(sceKernelGetEventError(&rawEvent) == 0); + Require(RejectsWideFflags(&rawEvent)); + rawEvent.fflags = 0; rawEvent.flags = EV_ERROR; rawEvent.data = 22; - Require(sceKernelGetEventError(&rawEvent) == 22); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.data = -1; + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.data = 0x80020016; + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.fflags = 5; + rawEvent.data = 0; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020005)); + rawEvent.data = 22; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020005)); + rawEvent.flags = 0; + rawEvent.fflags = 7; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020007)); + rawEvent.flags = EV_EOF; + rawEvent.fflags = 3; + rawEvent.data = 9; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020003)); + rawEvent.flags = static_cast(~EV_ERROR); + rawEvent.fflags = 0xFFFF; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x8002FFFF)); Require(RejectsNull(sceKernelGetEventData)); Require(RejectsNull(sceKernelGetEventError)); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..f7f2da3f63 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -204,7 +204,7 @@ - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 -- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports +- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) throws for `fflags` above 0xFFFF, where OR and addition of 0x80020000 differ and the console was not measured. Otherwise it returns 0 for `fflags` 0 and `0x80020000 | fflags`, whatever `flags` and `data` hold, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) does and a PS5 (firmware 13.40, measured 2026-10-19 in a payload process) answered: `{flags 0x4000, fflags 0, data 22}`, `{0x4000, 0, -1}`, `{0x4000, 0, 0x80020016}` and `{0, 0, 0}` gave 0, `{0x4000, 5, 0}` and `{0x4000, 5, 22}` 0x80020005, `{0, 7, 22}` 0x80020007, `{0x8000, 3, 9}` 0x80020003. Video-out events carry their count in `fflags`, so they get 0x80020001 and up; only events built by the caller were measured. Closes when: `fflags` above 0xFFFF is measured - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) From ca44747c276ee96904d3feee96720d52186b59ea Mon Sep 17 00:00:00 2001 From: Adam Franke Date: Mon, 19 Oct 2026 12:20:36 -0400 Subject: [PATCH 0023/1360] fix(libScePad): report the DualSense touch pad and stick dead zones a PS5 reports Inside Gran Turismo 7 on a PS5 (firmware 13.40) scePadGetControllerInformation for the DualSense reported touch pad resolution 1920 x 1080 and stick dead zones 13 and 13; pixel density 44.86 already matched. Touch y coordinates now span that height: a host touch pad scales y to 0..1079 and the view-button and injected touches sit at y 540 instead of 0..942 and 471. --- core/libs/prx/libScePad/Export.cpp | 12 +++++----- core/libs/prx/libScePad/src/PadState.cpp | 2 +- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 4 ++-- core/libs/tests/GuestPad.cpp | 23 ++++++++++++++++--- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index d66f710d7a..467cef7d97 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -81,9 +81,9 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio std::memset(info, 0, sizeof(*info)); info->touchPadInfo.pixelDensity = 44.86f; info->touchPadInfo.resolution.x = 1920; - info->touchPadInfo.resolution.y = 943; - info->stickInfo.deadZoneLeft = 2; - info->stickInfo.deadZoneRight = 2; + info->touchPadInfo.resolution.y = 1080; + info->stickInfo.deadZoneLeft = 13; + info->stickInfo.deadZoneRight = 13; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; info->connectedCount = 1; info->connected = true; @@ -125,9 +125,9 @@ int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { PadControllerInformation* base = static_cast(info); base->touchPadInfo.pixelDensity = 44.86f; base->touchPadInfo.resolution.x = 1920; - base->touchPadInfo.resolution.y = 943; - base->stickInfo.deadZoneLeft = 2; - base->stickInfo.deadZoneRight = 2; + base->touchPadInfo.resolution.y = 1080; + base->stickInfo.deadZoneLeft = 13; + base->stickInfo.deadZoneRight = 13; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; base->connectedCount = 1; base->connected = true; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 1aef805ccb..bc5ea68dc0 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -197,7 +197,7 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin data.buttons |= 0x100000; touchNum = 1; data.touch_data_touch0_x = state.touchRight || injected.touchRight ? 1440 : 480; - data.touch_data_touch0_y = 471; + data.touch_data_touch0_y = 540; data.touch_data_touch0_id = 0; } data.touch_data_touch_num = touchNum; diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5478ece435..4422d6de63 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -186,11 +186,11 @@ PadInputState PadInput::sampleController() const { if (SDL_GameControllerGetTouchpadFinger(controller, 0, finger, &down, &x, &y, &pressure) != 0 || down == 0) continue; result.touch[finger].active = true; result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * 1919.0f); - result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); + result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 1079.0f); } } if (viewPressed && !result.touch[0].active && !result.touch[1].active) { - result.touch[0] = {true, 960, 471, 0}; + result.touch[0] = {true, 960, 540, 0}; } return result; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 5d497995b8..df0663648e 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -28,6 +28,8 @@ int APS5_VABI scePadResetOrientation(int); int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); +int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadGetExtControllerInformation(int, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -42,6 +44,20 @@ static void RequireUnknownPadNidNotImplemented() { std::abort(); } +static void CheckControllerInformation(int handle) { + PadControllerInformation info{}; + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.touchPadInfo.pixelDensity == 44.86f); + Require(info.touchPadInfo.resolution.x == 1920 && info.touchPadInfo.resolution.y == 1080); + Require(info.stickInfo.deadZoneLeft == 13 && info.stickInfo.deadZoneRight == 13); + Require(info.connectionType == 0 && info.connectedCount == 1 && info.connected && info.deviceClass == 0); + unsigned char ext[0x2c]{}; + Require(scePadGetExtControllerInformation(handle, ext) == PAD_OK); + const auto* extBase = reinterpret_cast(ext); + Require(extBase->touchPadInfo.resolution.x == 1920 && extBase->touchPadInfo.resolution.y == 1080); + Require(extBase->stickInfo.deadZoneLeft == 13 && extBase->stickInfo.deadZoneRight == 13); +} + static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -156,13 +172,13 @@ static void CheckAngularVelocityDeadband(int handle) { static void CheckTouchContact() { PadInputState touch; touch.buttons = static_cast(Pad::PadButton::TouchPad); - touch.touch[0] = {true, 960, 471, 0}; + touch.touch[0] = {true, 960, 540, 0}; PadPublishInput_nid_postfix(touch); const auto data = Pad::ReadState(); Require((data.buttons & static_cast(Pad::PadButton::TouchPad)) != 0); Require(data.touch_data_touch_num == 1); Require(data.touch_data_touch0_x == 960); - Require(data.touch_data_touch0_y == 471); + Require(data.touch_data_touch0_y == 540); PadPublishInput_nid_postfix(PadInputState{}); Pad::ReadState(); } @@ -266,7 +282,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { if (touched.touch_data_touch_num != 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } while (std::chrono::steady_clock::now() < touchDeadline); - Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 471); + Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 540); Require(touched.buttons == (0x2000 | 0x100000)); std::this_thread::sleep_for(std::chrono::milliseconds(310)); const auto lifted = Pad::ReadState(); @@ -342,6 +358,7 @@ int main() { CheckReadQueue(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); + CheckControllerInformation(handle); CheckInjectedState(commandPath); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); From 675c93793c752769fe7e4f4a9960bd7290e05400 Mon Sep 17 00:00:00 2001 From: Adam Franke Date: Mon, 19 Oct 2026 12:14:56 -0400 Subject: [PATCH 0024/1360] fix(libSceSystemService): refuse unknown sceSystemServiceParamGetInt parameters as a PS5 does Inside Gran Turismo 7 on a PS5 (firmware 13.40) parameters 7 (game parental level) and 1000 (enter button assignment) returned 0x80A10003 like every other unknown parameter; 1 to 5, 100 and 400 answered. --- core/libs/prx/libSceSystemService/Export.cpp | 5 ++--- core/libs/prx/libSceSystemService/SystemService.hpp | 3 +-- core/libs/tests/GuestSystemServiceLanguage.cpp | 13 ++++++++++--- docs/dev/debt/libSceSystemService.md | 3 ++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index f99a9cc62f..4a1c7f1790 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -305,9 +305,8 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; - case SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL: *value = SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF; break; - case SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN: *value = SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS; break; - default: *value = 0; break; + case SYSTEM_SERVICE_PARAM_ID_100: *value = 0; break; + default: return SYSTEM_SERVICE_ERROR_PARAMETER; } return SYSTEM_SERVICE_OK; } diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 7b1664cc90..22f85c1123 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -23,6 +23,7 @@ constexpr int SYSTEM_SERVICE_PARAM_ID_TIME_ZONE = 4; constexpr int SYSTEM_SERVICE_PARAM_ID_SUMMERTIME = 5; constexpr int SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME = 6; constexpr int SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL = 7; +constexpr int SYSTEM_SERVICE_PARAM_ID_100 = 100; constexpr int SYSTEM_SERVICE_PARAM_ID_400 = 400; constexpr int SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN = 1000; @@ -34,8 +35,6 @@ constexpr int SYSTEM_SERVICE_PARAM_LANG_ARABIC = 21; constexpr int SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN = 30; constexpr int SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY = 1; constexpr int SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR = 1; -constexpr int SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF = 0; -constexpr int SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS = 1; inline int SystemServiceConsoleLanguage(std::string_view caller) { const char* configured = std::getenv("ANYPS5_LANGUAGE"); diff --git a/core/libs/tests/GuestSystemServiceLanguage.cpp b/core/libs/tests/GuestSystemServiceLanguage.cpp index efd37f499b..dd8b59180c 100644 --- a/core/libs/tests/GuestSystemServiceLanguage.cpp +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -62,7 +62,14 @@ int main() { SetLanguage("arabic"); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - int enterButton = -1; - Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, &enterButton) == SYSTEM_SERVICE_OK); - Require(enterButton == SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS); + for (int paramId : {0, SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL, 8, 20, 401, 999, + SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, 1001, 1008}) { + int unknown = -1; + Require(sceSystemServiceParamGetInt(paramId, &unknown) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(unknown == -1); + Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + int param100 = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_100, ¶m100) == SYSTEM_SERVICE_OK); + Require(param100 == 0); } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 0786239425..5452070440 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -13,7 +13,8 @@ - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 and 400 answer; every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside Gran Turismo 7 (PPSA01317) on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0, which is returned here too, although what the parameter means is not known. Whether the console writes the value on that error is not measured - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC ### Functional From b95b9a4e6b21c163a544baea0f53fe489b50e5c2 Mon Sep 17 00:00:00 2001 From: Adam Franke Date: Mon, 19 Oct 2026 12:14:56 -0400 Subject: [PATCH 0025/1360] docs(debt): record VideoOut, AGC driver and shader values measured on a PS5 Measured on firmware 13.40 inside Gran Turismo 7 through ptrace remote calls and GPU buffers submitted from the game. --- docs/dev/debt/libSceAgcDriver.md | 5 +++-- docs/dev/debt/libSceVideoOut.md | 7 ++++--- docs/dev/debt/shader.md | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b1117bd00d..ab76ba68bc 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -8,6 +8,7 @@ - [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified +- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside Gran Turismo 7, PPSA01317) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured - [Initial point context](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - new queues and CLEAR_STATE seed PA_SU_POINT_SIZE 0x00080008 and PA_SU_POINT_MINMAX 0xffff0000, matching every public context table in libSceAgc/Command/include/RegisterDefaultsData.hpp. Cocoon (PPSA08766) reaches geometry point output without writing these registers in the observed startup. Applying the SDK context defaults to initial/CLEAR_STATE driver context is inferred; GPU power-on reset state is not measured and is not the source of these values. Explicit writes and pushed/restored contexts retain their own values. @@ -110,7 +111,7 @@ - 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. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. -- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown +- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5, which a PS5 does not: on firmware 13.40 (measured 2026-10-19 with buffers submitted through `sceAgcDriverSubmitDcb` inside Gran Turismo 7) a DST_SEL 3 `WRITE_DATA` (control 0x00100300, or 0x06100300 with cache policy 3) left its label in direct memory unwritten, also after a `RELEASE_MEM` with GL2 write-back and invalidate (0x00300514), and a GDS `COPY_DATA` (SRC_SEL 3) at that address read 0. The console's own `sceAgcDcbWriteData(dst 6)` builds DST_SEL 3 (0x00110300) as here, and Killzone: Liberation (PPSA11142) fences its frame with it and waits on that label with `WAIT_REG_MEM`, so the write is kept until the path that lands the label is known. Closes when: that path is measured - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. @@ -155,7 +156,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. +- [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp. On a PS5 (firmware 13.40, measured 2026-10-19 on the GPU through `sceAgcDriverSubmitDcb` from inside Gran Turismo 7, PPSA01317) `SRC_SEL` 9 and the `RELEASE_MEM` DATA_SEL 3 timestamp were the same counter (0x1C and 0x5C ticks apart within one buffer), and it ran at 98.0 and 99.8 MHz over two 1.1 s intervals timed by the CPU, i.e. 100 MHz within the submission jitter, as the 10 ns ticks here; only its starting value is the host's. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ae73a5efc5..509e5d29d5 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -12,16 +12,17 @@ - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. -- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. +- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - bytes 48-53 are written as zero, where a PS5 (firmware 13.40, measured 2026-10-19 inside Gran Turismo 7, PPSA01317) with a 4K TV that takes 120 Hz returned the 16-bit values 48, 60 and 60 (0x30, 0x3C, 0x3C); their meaning is not established, so the value at byte 52 never adjusts `refreshRate`. `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746: bytes 0-47 are `sceVideoOutGetOutputStatus`'s and the rest 0, a null `status` returns `0x80290002` and an unknown handle `0x8029000B`, all as that console answered. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 -- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside Gran Turismo 7), whose answers for every other mode match the ones here. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 +- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; a PS5 (firmware 13.40, measured 2026-10-19 inside Gran Turismo 7, PPSA01317) reported its TV's 60.0f. The 48-byte layout, from the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), is confirmed there: full size and pane 3840 x 2160, `refreshRate` 3, `flags` 0 and the rest 0 on GT7's handle, 0x8029000B for handles 0 to 8. `flags` is 0 here too; its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceVideoOutSubmitFlip](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut, libSceAgcDriver) - in the [AGC driver](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp), a CPU flip waits for the graphics-queue submissions ahead of it when the calling thread has submitted graphics-queue work itself: a title that submits a frame and flips it without waiting relies on that order, since emulated GPU work finishes later than the console's. A flip from a thread that has never submitted graphics-queue work is presented without waiting for queued submissions, unless a command-buffer or EOP flip ahead of it has not been reached. Such a thread can only know its frame is finished by waiting for it, as the PS5 RADV WSI's flip thread waits on the frame's fence. Whether the console ever orders a CPU flip behind the graphics queue is not measured; `APS5_CPU_FLIP_IN_ORDER=1` orders every CPU flip +- [sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the third 64-bit word of the PS5 status (`reserved` in [SceTypes.hpp](../../../core/libs/SceTypes.hpp)) is written 0. On a PS5 (firmware 13.40, measured 2026-10-19) Gran Turismo 7's own call, caught inside the game, got count 0xb16b, `processTime` 0x381a8790 (941 s), third word 0x1c485ffaadc, `processTimeCounter` 0x15dd63c364b, flags 0x04 and phase 0: the third word is a running counter, about 970 s at ~2 GHz, consistent with a TSC, not zero. Its clock and what it counts from are not established. Closes when: the third word's source is measured and written - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sceVideoOutRegisterBuffers, sceVideoOutGetFlipStatus, sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - PS4 callers. A handle is treated as PS4 once buffers are registered through the PS4 `sceVideoOutRegisterBuffers`; from then on its flip and vblank status are written in the PS4 64- and 32-byte layouts, with `tsc` and `submitTsc` 0. A PS4 title that reads a status before it registers buffers gets the larger PS5 layout. The PS4 function accepts only linear A8R8G8B8_SRGB (0x80000000) and A8B8G8R8_SRGB (0x80002200) buffers without options; tiled PS4 buffers throw, as their layout differs from the PS5 tiling. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample registers its second 1920x1080 buffer 8,294,400 bytes after the first. The PS4 alignment rule is not known diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e5ad6cd186..e458c0342e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -60,7 +60,7 @@ - [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)). - [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane. - [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. -- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. +- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are 22.35 times smaller than on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7), where it counts the ~2.23 GHz core clock (44,041,632 against 1,970,792 `s_memrealtime` ticks). The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. - [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. @@ -77,7 +77,7 @@ - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples -- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values +- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from GTA V's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - 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. - [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. @@ -137,8 +137,8 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. @@ -149,7 +149,7 @@ - [2D biased sampling through a one-slice, single-level 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) supplies a zero third coordinate and retains the bias operation, with normalized point minification and magnification and no linear mip filtering. Measured on RDNA2 (AMD integrated gfx1036, driver 32.0.21036.18, Windows 11) by running an OpenCL-loaded native kernel with `image_sample_b` DIM 2D against 16x16x1 R32_UINT and RGBA8_UNORM 3D images: biases -16, -0.5, 0, 0.5 and 16 read the (u,v) texel in slice zero, including wrapped coordinates, point min/max reduction and the title's sampler with Z border addressing. Wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off; wave64 and the PS5's gfx1013 are not measured. Pixel-stage, direct, non-depth, unpacked samples whose only use is the plain 32-bit bias form are accepted; extra slices, any nonzero mip range, linear or anisotropic filtering and unnormalized coordinates throw. Vulkan readback is checked on NVIDIA RTX 5070 and AMD integrated graphics for positive and negative bias and point weighted-average/min/max reduction. - [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. -- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) +- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff, as a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) read them (0x80000000_00000000 / 0x80000000_FFFFFFFF shared, 0x70000000_00000000 / 0x70000000_FFFFFFFF private). A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read the low dword, after LLVM's `SIRegisterInfo.td` and `getSegmentAperture` and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). The 32-bit read and the wave's address mode (taken to be 64-bit, PTR32 clear) were not measured on the console - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated barycentric geometry stage do not write it, the generated rect-list and barycentric tessellation stages forward the vertex shader's array, and a vertex or tessellation evaluation shader whose guest clip and cull distances already take all eight does not write it either, so NVIDIA 615.78.08 draws such a vertex at the viewport centre; a device without `shaderClipDistance` cannot compile vertex shaders that write it (the compile throws). The PS5 rasteriser was not measured. - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) are the barycentrics at the iterated sample, which in a pixel shader the guest does not iterate is taken to be sample 0, inferred from the ancillary's iterated sample number and not measured. They are interpolated with `InterpolateAtOffset`, which [GLSL_EXT_fragment_shader_barycentric](https://github.com/KhronosGroup/GLSL/blob/0099bf83b0288469917fc2e2972cb0a2b74d6711/extensions/ext/GLSL_EXT_fragment_shader_barycentric.txt#L286-L299) names for barycentrics away from the default position, at the sample's offset from the pixel centre, read per pixel of the 2x2 quad from `PA_SC_AA_SAMPLE_LOCS` (the standard pattern unless the draw sets its own): sample 0's in a shader the guest does not iterate, and in an iterated shader the offset of the host's `SampleId` from a table of the guest's locations, because Vulkan leaves the position of plain barycentrics under sample shading implementation-defined (lavapipe uses the pixel centre). That needs the device's `sampleRateShading`; without it, and when every location is the pixel centre (a single-sample draw), they keep the host's default position, the pixel centre once per pixel and an implementation-defined position under sample shading. PERSP_CENTER and LINEAR_CENTER are interpolated at offset 0 in an iterated shader, so they stay at the pixel centre as on the console; the centroid pairs keep the host's default position. With barycentric emulation the offsets apply to the emulated barycentrics. Devices that interpolate attributes on the host instead (no usable fragment-shader barycentrics, as on MoltenVK) keep the default position for all four pairs. - [Pixel positions](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - POS_X_FLOAT and POS_Y_FLOAT follow `SPI_BARYC_CNTL.POS_FLOAT_LOCATION` (bits 17:16) and `POS_FLOAT_ULC` (bit 20), whose positions come from [PAL's GFX9+ register header](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h) and whose meanings from [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/RegisterMetadataBuilder.cpp#L767-L779): the pixel centre (0), the iterated sample (2), or the upper-left corner (ULC). Under host sample shading the centre is `floor(FragCoord) + 0.5`, and a non-iterated shader's sample is the centre plus sample 0's offset. The centroid (1) keeps `FragCoord`, the pixel centre or under sample shading the sample, not the centroid of the covered samples. A missing `SPI_BARYC_CNTL` reads as the pixel centre and the reserved location 3 throws. POS_Z_FLOAT and POS_W_FLOAT keep `FragCoord`'s depth and 1/w at the host's position. `PERSP_CENTER_CNTL`, `PERSP_CENTROID_CNTL`, `LINEAR_CENTER_CNTL` and `LINEAR_CENTROID_CNTL` (bits 0, 4, 8 and 12) are not read: PAL and LLPC never set them. From 035def66bbae12ce2386c24ee963b77fb5e22f8e Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Fri, 16 Oct 2026 21:48:58 +0200 Subject: [PATCH 0026/1360] fix(agc): store linear compute writes behind their dispatch A compute dispatch that writes a linear-layout image (the readback copy a title makes of GPU results for the CPU, such as UE4's HZB occlusion test results) left the image pending; the flip-time store skips surfaces over 64 KiB and comes after the frame fence the CPU waits on, so the CPU read stale results. Kena (PPSA01802) then culled visible primitives: terrain, the player character and props popped in and out, showing sky and fog through the ground. Pending linear images bound by a dispatch are now stored right after it, in GPU order before later labels. APS5_NO_LINEAR_READBACK_STORE=1 keeps the old behaviour. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Dispatch/Dispatch.cpp | 24 +++++++++++++++++++ .../Graphics/include/Texture.hpp | 1 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++++ 3 files changed, 29 insertions(+) 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 565ec80e25..a14d889745 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -10,15 +10,37 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Optimization/ResourceProgram.hpp" #include #include +#include +#include #include namespace AgcDriver::DriverDetail { namespace { +void StoreLinearWrites(const ShaderRecompiler::RecompileResult& compiled) { + static const bool disabled = std::getenv("APS5_NO_LINEAR_READBACK_STORE") != nullptr; + if (disabled) return; + for (const auto& binding : compiled.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages) continue; + for (std::size_t offset = 0; offset + 8 <= binding.guestDescriptor.size(); offset += 8) { + const auto words = std::span(binding.guestDescriptor).subspan(offset, 8); + if (Graphics::IsNullTextureResource(words)) continue; + try { + const auto image = Graphics::DecodeTextureResource(words); + if (image.tileMode != Graphics::TextureTileMode::kLinear || image.baseAddress == 0) continue; + static_cast(Graphics::StorageTexture::StoreLinearReadback(image.baseAddress, static_cast(Graphics::DescribeSurface(image).guestBytes))); + } catch (const std::exception&) { + } + } + } +} + bool DispatchItemsEnabled() { static const bool enabled = [] { const char* text = std::getenv("APS5_PIPELINE_DISPATCH"); @@ -474,6 +496,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } else { localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); } + StoreLinearWrites(compiledCommit); break; } if (noteWrites && !writerKeyedEvidence()) noteWrittenBuffers(address, queueId, compiledCommit); @@ -577,6 +600,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } catch (const std::exception& error) { if (rethrow(error)) return; } + StoreLinearWrites(compiled); if (builtRecipe != nullptr) { if (dataHit) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 60e1af575d..cf41f7088d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -213,6 +213,7 @@ public: static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; static std::size_t StoreSmallPending(const char* reason); + static bool StoreLinearReadback(std::uint64_t address, std::size_t bytes); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..aad32f1017 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3971,6 +3971,10 @@ std::size_t StorageTexture::StoreSmallPending(const char* reason) { return stored; } +bool StorageTexture::StoreLinearReadback(std::uint64_t address, std::size_t bytes) { + return FlushPending(address, bytes, nullptr, "linear readback"); +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); From 6c50a9783522f2c9e10392cab23ab008c967bf10 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sat, 17 Oct 2026 10:12:06 +0200 Subject: [PATCH 0027/1360] fix(agc): store pending linear images before a label instead of per dispatch A label or end-of-pipe interrupt the CPU may wait on now stores every pending linear storage image first, recorded before the label, so a CPU read after the fence sees the results. Only images the GPU wrote are pending, and each is stored once per write; the per-dispatch walk over the bound descriptors is gone. APS5_NO_LINEAR_READBACK_STORE=1 turns it off. The test dispatches a store to a linear image, waits on the label after it and reads guest memory; it fails with the switch set. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../src/Driver/Dispatch/Dispatch.cpp | 24 --- .../src/Driver/Packets/PacketMemory.cpp | 1 + .../Execution/src/VulkanDevice.cpp | 1 + .../Graphics/include/Texture.hpp | 2 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 14 +- .../tests/execution/LinearReadbackLabel.cpp | 183 ++++++++++++++++++ 7 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 65f1aff6c3..2b0cb53d87 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2364,6 +2364,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_flip_compute agc_driver_present_mode agc_driver_worker_failure_log PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_linear_readback_label_off COMMAND agc_driver_linear_readback_label_tests) + set_tests_properties(agc_driver_linear_readback_label_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_LINEAR_READBACK_STORE=1" WILL_FAIL TRUE TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_linear_readback_label_off PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_draw_cache_reuse_verify COMMAND agc_driver_draw_cache_reuse_tests) 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 a14d889745..565ec80e25 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -10,37 +10,15 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" -#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" -#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Optimization/ResourceProgram.hpp" #include #include -#include -#include #include namespace AgcDriver::DriverDetail { namespace { -void StoreLinearWrites(const ShaderRecompiler::RecompileResult& compiled) { - static const bool disabled = std::getenv("APS5_NO_LINEAR_READBACK_STORE") != nullptr; - if (disabled) return; - for (const auto& binding : compiled.bindings) { - if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages) continue; - for (std::size_t offset = 0; offset + 8 <= binding.guestDescriptor.size(); offset += 8) { - const auto words = std::span(binding.guestDescriptor).subspan(offset, 8); - if (Graphics::IsNullTextureResource(words)) continue; - try { - const auto image = Graphics::DecodeTextureResource(words); - if (image.tileMode != Graphics::TextureTileMode::kLinear || image.baseAddress == 0) continue; - static_cast(Graphics::StorageTexture::StoreLinearReadback(image.baseAddress, static_cast(Graphics::DescribeSurface(image).guestBytes))); - } catch (const std::exception&) { - } - } - } -} - bool DispatchItemsEnabled() { static const bool enabled = [] { const char* text = std::getenv("APS5_PIPELINE_DISPATCH"); @@ -496,7 +474,6 @@ void Driver::dispatch(QueueState& queue, std::span packet, } else { localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); } - StoreLinearWrites(compiledCommit); break; } if (noteWrites && !writerKeyedEvidence()) noteWrittenBuffers(address, queueId, compiledCommit); @@ -600,7 +577,6 @@ void Driver::dispatch(QueueState& queue, std::span packet, } catch (const std::exception& error) { if (rethrow(error)) return; } - StoreLinearWrites(compiled); if (builtRecipe != nullptr) { if (dataHit) { 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 aa5fb2a23d..9b1fafdfe3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -50,6 +50,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue countLabelOutcome(reason); ++immediateLabels; } else { + if (localDevice != nullptr) Graphics::StorageTexture::StoreLinearBeforeLabel("linear before interrupt"); ++noOpLabels; } if (reason == 0 || reason == 5) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1ebaafcc12..6327fd4939 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2239,6 +2239,7 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) { static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL")); if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label"); + Graphics::StorageTexture::StoreLinearBeforeLabel("linear before label"); if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4; auto& recorder = *state->recorder; // Batches that already finished are retired before the checks: their completions ran, so they diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index cf41f7088d..9440f47872 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -213,7 +213,7 @@ public: static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; static std::size_t StoreSmallPending(const char* reason); - static bool StoreLinearReadback(std::uint64_t address, std::size_t bytes); + static void StoreLinearBeforeLabel(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index aad32f1017..1d73531523 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3971,8 +3971,18 @@ std::size_t StorageTexture::StoreSmallPending(const char* reason) { return stored; } -bool StorageTexture::StoreLinearReadback(std::uint64_t address, std::size_t bytes) { - return FlushPending(address, bytes, nullptr, "linear readback"); +void StorageTexture::StoreLinearBeforeLabel(const char* reason) { + static const bool disabled = std::getenv("APS5_NO_LINEAR_READBACK_STORE") != nullptr; + if (disabled) return; + std::vector> ranges; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (const auto* texture : pending.textures) { + if (texture->descriptor.tileMode == TextureTileMode::kLinear) ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); + } + } + for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); } bool StorageTexture::AnyPending() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp new file mode 100644 index 0000000000..6276540f98 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp @@ -0,0 +1,183 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.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; + +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 Format8888Uint = 60; +constexpr std::uint32_t TexelBytes = 4; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t LabelOffset = 0x8000; +constexpr std::uint32_t LabelValue = 0x5eedf00du; + +alignas(256) constexpr std::array StoreCode{ + 0x4a500081, 0xd5690014, 0x0201ff28, 0x9e3779b1, 0x4a2a28ff, 0x7f4a7c15, 0x4a2c28ff, 0xfe94f82a, + 0x4a2e28ff, 0x7ddf743f, 0x7e3c0300, 0x7e3e0280, 0xf0281f08, 0x0001141e, 0x7e3e0281, 0xf0281508, + 0x0001141e, 0x7e3e0282, 0xf0281a08, 0x0001141e, 0x7e3e0283, 0xf0281808, 0x0001141e, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "linear readback label: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint32_t Data(std::uint32_t tid, std::uint32_t index) { + return (tid + 1u) * 0x9e3779b1u + index * 0x7f4a7c15u; +} + +AgcDriver::Graphics::TileMipLayout Layout() { + return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888Uint, Width, Height, 1).front(); +} + +std::vector Expected(const std::vector& initial) { + const auto mip = Layout(); + auto bytes = initial; + constexpr std::array Dmasks{0xfu, 0x5u, 0xau, 0x8u}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t row = 0; row < Dmasks.size(); ++row) { + std::array words{}; + std::uint32_t next = 0; + for (std::uint32_t word = 0; word < 4u; ++word) { + if (((Dmasks[row] >> word) & 1u) != 0u) words[word] = Data(tid, next++); + } + std::memcpy(bytes.data() + mip.tiledOffset + static_cast(row) * mip.pitchBytes + static_cast(tid) * TexelBytes, words.data(), TexelBytes); + } + } + return bytes; +} + +std::array UserData(const void* texels) { + const auto address = static_cast(reinterpret_cast(texels)); + std::array data{}; + data[4] = static_cast(address >> 8u); + data[5] = static_cast((address >> 40u) & 0xffu) | (Format8888Uint << 20u) | (((Width - 1u) & 3u) << 30u); + data[6] = ((Width - 1u) >> 2u) | ((Height - 1u) << 14u); + data[7] = IdentitySwizzle | (Type2D << 28u); + return data; +} + +std::vector Commands(const void* texels, const void* label) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(StoreCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{16u << 1u}; + registers(0x213, resource); + registers(0x240, UserData(texels)); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u | 0x8000u}); + const auto address = reinterpret_cast(label); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), LabelValue, 0, 0}); + return words; +} + +void ReadAfterLabel(std::uint8_t* block) { + auto* texels = block; + auto* label = reinterpret_cast(block + LabelOffset); + const auto mip = Layout(); + const auto bytes = static_cast(mip.tiledOffset + mip.pitchBytes * Height); + std::vector initial(bytes); + for (std::size_t index = 0; index < initial.size(); ++index) initial[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + std::memcpy(texels, initial.data(), initial.size()); + *label = 0; + auto words = Commands(texels, label); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto start = std::chrono::steady_clock::now(); + while (std::atomic_ref(*label).load(std::memory_order_acquire) != LabelValue) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "linear readback label: the label after the dispatch never landed"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + const auto expected = Expected(initial); + for (std::uint32_t row = 0; row < Height; ++row) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = mip.tiledOffset + static_cast(row) * mip.pitchBytes + static_cast(x) * TexelBytes; + std::uint32_t actual = 0; + std::uint32_t wanted = 0; + std::memcpy(&actual, texels + offset, TexelBytes); + std::memcpy(&wanted, expected.data() + offset, TexelBytes); + Require(actual == wanted, "linear readback label: texel (" + std::to_string(x) + ", " + std::to_string(row) + ") read after the label is 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", actual); return std::string(text); }() + ", the dispatch before the label stored a different value"); + } + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestBlock block; + ReadAfterLabel(block.Data()); + AgcDriverWaitIdle_nid_postfix(); + AgcDriverShutdown_nid_postfix(); + std::puts("linear readback label tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 14fd9853dbf9e134267a097c90aa2150a07d9186 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sun, 18 Oct 2026 14:33:30 +0200 Subject: [PATCH 0028/1360] fix(agc): also store small tiled images before a label The Smurfs - Dreams (PPSA21607) reads a 1x1 RGBA16F R64KBX storage image on the CPU after a label. The label-time store took only linear images, so the image stayed pending and the sky light read 0. Tiled images up to SmallStoreBytes are stored before the label as well. Reported and tested by Pacuka. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../src/Driver/Packets/PacketMemory.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 2 +- .../Graphics/include/Texture.hpp | 2 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 +- .../tests/execution/LinearReadbackLabel.cpp | 91 +++++++++++++++---- docs/dev/ENVIRONMENT.md | 1 + 7 files changed, 81 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2b0cb53d87..c4987e9cd6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2365,7 +2365,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_linear_readback_label_off COMMAND agc_driver_linear_readback_label_tests) - set_tests_properties(agc_driver_linear_readback_label_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_LINEAR_READBACK_STORE=1" WILL_FAIL TRUE TIMEOUT 60) + set_tests_properties(agc_driver_linear_readback_label_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_LABEL_READBACK_STORE=1" WILL_FAIL TRUE TIMEOUT 60) if(WIN32) set_tests_properties(agc_driver_linear_readback_label_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") 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 9b1fafdfe3..d6f6422f7d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -50,7 +50,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue countLabelOutcome(reason); ++immediateLabels; } else { - if (localDevice != nullptr) Graphics::StorageTexture::StoreLinearBeforeLabel("linear before interrupt"); + if (localDevice != nullptr) Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before interrupt"); ++noOpLabels; } if (reason == 0 || reason == 5) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 6327fd4939..f5c92dfd65 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2239,7 +2239,7 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) { static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL")); if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label"); - Graphics::StorageTexture::StoreLinearBeforeLabel("linear before label"); + Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label"); if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4; auto& recorder = *state->recorder; // Batches that already finished are retired before the checks: their completions ran, so they diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9440f47872..2b15d75d28 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -213,7 +213,7 @@ public: static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; static std::size_t StoreSmallPending(const char* reason); - static void StoreLinearBeforeLabel(const char* reason); + static void StoreReadbackBeforeLabel(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1d73531523..19d3ab23ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3971,15 +3971,15 @@ std::size_t StorageTexture::StoreSmallPending(const char* reason) { return stored; } -void StorageTexture::StoreLinearBeforeLabel(const char* reason) { - static const bool disabled = std::getenv("APS5_NO_LINEAR_READBACK_STORE") != nullptr; +void StorageTexture::StoreReadbackBeforeLabel(const char* reason) { + static const bool disabled = std::getenv("APS5_NO_LABEL_READBACK_STORE") != nullptr; if (disabled) return; std::vector> ranges; { auto& pending = Pending(); std::lock_guard lock(pending.mutex); for (const auto* texture : pending.textures) { - if (texture->descriptor.tileMode == TextureTileMode::kLinear) ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); + if (texture->descriptor.tileMode == TextureTileMode::kLinear || texture->guestBytes <= SmallStoreBytes) ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); } } for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp index 6276540f98..36105a9e93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp @@ -1,6 +1,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" @@ -10,6 +12,7 @@ #endif #include #endif +#include #include #include #include @@ -34,9 +37,17 @@ constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Format8888Uint = 60; constexpr std::uint32_t TexelBytes = 4; constexpr std::uint32_t IdentitySwizzle = 0xfacu; -constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t BlockBytes = 0x20000; constexpr std::size_t LabelOffset = 0x8000; constexpr std::uint32_t LabelValue = 0x5eedf00du; +constexpr std::uint32_t TiledThreads = 8; +constexpr std::uint32_t TiledSide = 16; +constexpr std::uint32_t Format32Uint = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::size_t TiledOffset = 0x10000; +constexpr std::size_t TiledLabelOffset = 0x8100; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Written = 0x12345678u; alignas(256) constexpr std::array StoreCode{ 0x4a500081, 0xd5690014, 0x0201ff28, 0x9e3779b1, 0x4a2a28ff, 0x7f4a7c15, 0x4a2c28ff, 0xfe94f82a, @@ -44,6 +55,10 @@ alignas(256) constexpr std::array StoreCode{ 0x0001141e, 0x7e3e0282, 0xf0281a08, 0x0001141e, 0x7e3e0283, 0xf0281808, 0x0001141e, 0xbf810000, }; +alignas(256) constexpr std::array TiledStoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, +}; + class GuestBlock { public: GuestBlock() { @@ -110,27 +125,59 @@ std::array UserData(const void* texels) { return data; } -std::vector Commands(const void* texels, const void* label) { +std::array TiledDescriptor(const void* texels) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Uint << 20u) | (((TiledSide - 1u) & 3u) << 30u), + ((TiledSide - 1u) >> 2u) | ((TiledSide - 1u) << 14u), + IdentitySwizzle | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array TiledUserData(const void* texels) { + std::array data{}; + const auto texture = TiledDescriptor(texels); + std::copy(texture.begin(), texture.end(), data.begin() + 8); + return data; +} + +std::vector Commands(std::span code, std::uint32_t threads, std::span userData, const void* label) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); words.push_back(first); words.insert(words.end(), values.begin(), values.end()); }; - const std::array shape{Threads, 1, 1}; + const std::array shape{threads, 1, 1}; registers(0x207, shape); - const auto code = reinterpret_cast(StoreCode.data()); - const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + const auto address = reinterpret_cast(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; registers(0x20c, program); - const std::array resource{16u << 1u}; + const std::array resource{static_cast(userData.size()) << 1u}; registers(0x213, resource); - registers(0x240, UserData(texels)); + registers(0x240, userData); words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u | 0x8000u}); - const auto address = reinterpret_cast(label); - words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), LabelValue, 0, 0}); + const auto at = reinterpret_cast(label); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(at), static_cast(at >> 32u), LabelValue, 0, 0}); return words; } +void SubmitAndWait(std::vector words, std::uint32_t* label, const char* what) { + *label = 0; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto start = std::chrono::steady_clock::now(); + while (std::atomic_ref(*label).load(std::memory_order_acquire) != LabelValue) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), std::string(what) + ": the label after the dispatch never landed"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } +} + void ReadAfterLabel(std::uint8_t* block) { auto* texels = block; auto* label = reinterpret_cast(block + LabelOffset); @@ -139,15 +186,8 @@ void ReadAfterLabel(std::uint8_t* block) { std::vector initial(bytes); for (std::size_t index = 0; index < initial.size(); ++index) initial[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); std::memcpy(texels, initial.data(), initial.size()); - *label = 0; - auto words = Commands(texels, label); - Packet packet{words.data(), static_cast(words.size()), 0, {}}; - AgcDriver::Submit(&packet, 0); - const auto start = std::chrono::steady_clock::now(); - while (std::atomic_ref(*label).load(std::memory_order_acquire) != LabelValue) { - Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "linear readback label: the label after the dispatch never landed"); - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } + const auto userData = UserData(texels); + SubmitAndWait(Commands(StoreCode, Threads, userData, label), label, "linear readback label"); const auto expected = Expected(initial); for (std::uint32_t row = 0; row < Height; ++row) { for (std::uint32_t x = 0; x < Width; ++x) { @@ -161,6 +201,20 @@ void ReadAfterLabel(std::uint8_t* block) { } } +void ReadTiledAfterLabel(std::uint8_t* block) { + auto* texels = reinterpret_cast(block + TiledOffset); + auto* label = reinterpret_cast(block + TiledLabelOffset); + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TiledDescriptor(texels))).guestBytes; + Require(bytes <= AgcDriver::Graphics::StorageTexture::SmallStoreBytes && bytes <= BlockBytes - TiledOffset, "tiled readback label: the test image is larger than a small image"); + std::fill(texels, texels + bytes / 4u, Untouched); + const auto userData = TiledUserData(texels); + SubmitAndWait(Commands(TiledStoreCode, TiledThreads, userData, label), label, "tiled readback label"); + const volatile std::uint32_t* cpu = texels; + std::uint32_t written = 0; + for (std::size_t index = 0; index < bytes / 4u; ++index) written += cpu[index] == Written ? 1u : 0u; + Require(written == TiledThreads, "tiled readback label: " + std::to_string(written) + " texels of a small tiled image read after the label hold the value the dispatch before it stored, expected " + std::to_string(TiledThreads)); +} + } int main() { @@ -171,6 +225,7 @@ int main() { } GuestBlock block; ReadAfterLabel(block.Data()); + ReadTiledAfterLabel(block.Data()); AgcDriverWaitIdle_nid_postfix(); AgcDriverShutdown_nid_postfix(); std::puts("linear readback label tests passed"); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 222ee12c79..591543c324 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -247,6 +247,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_LABEL_BATCHING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | turns off every label-batching behaviour, the label runs, queued labels and batched submit | | `APS5_NO_LABEL_BATCH_SUBMIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables batched label submission | | `APS5_NO_LABEL_PACKET_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp` | | disables the label flush between packets | +| `APS5_NO_LABEL_READBACK_STORE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves pending linear and small storage images on the GPU at a label instead of storing them before it | | `APS5_NO_LABEL_RUNS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every store in a run of its own between a barrier pair | | `APS5_NO_LABEL_SHORTCUT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the pending-label table shortcut in a memory wait | | `APS5_NO_LAYER_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks every surface as one layer instead of per array layer | From 2c4d1388a17bb84784bb877514b6dd1e19478526 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 19 Oct 2026 18:50:32 +0200 Subject: [PATCH 0029/1360] test(agc): keep the label store off in the clamped fill's key order check KeyFillOrderTests waits for a label and then needs its 64 KiB surface still pending under its keys, which the store before the label retires. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c4987e9cd6..35ef57ff92 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2033,6 +2033,7 @@ if(BUILD_TESTING) endif() set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_storage_file_write PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SUSPEND_DRAIN=1") + set_tests_properties(agc_driver_clamped_global_fill PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_LABEL_READBACK_STORE=1") set_tests_properties(agc_driver_draw_pipeline_trace PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30) set_property(TEST agc_driver_draw_pipeline_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION From f6b254bc73ec2cd924401f9287014d5c1c4d0ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 19 Oct 2026 19:02:32 +0200 Subject: [PATCH 0030/1360] fix(agc): read decode bytes through DecodeRead::Bytes() --- .../libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp | 2 +- .../libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 11c9c16ba2..4eb638a1a0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -38,7 +38,7 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span Date: Mon, 19 Oct 2026 18:07:48 +0100 Subject: [PATCH 0031/1360] perf(agc): reuse sampled snapshot byte buffers across cache entry replacement A surface sampled after its storage image flushes is snapshotted, and the next frame's pending image replaces the entry with a view: the 32 MiB guest buffer was freed and re-allocated zero-filled on every alternation, about 3.2 ms per 4K snapshot on the committer. The replaced entry now passes its buffer to its replacement, and a later snapshot of the same size adopts it. ReadTextureSurface overwrites every byte before use. Bendy chapter 1 steady state, RX 7900 GRE, 3840x2160: median frame 92.6 -> 78.0 ms, p99 151.4 -> 87.3 ms, 313 -> 383 presents per 30 s. --- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 75ab4242bc..6e9632f272 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -119,6 +119,7 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; + std::vector carried; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -772,6 +773,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span recycled; if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { // The view follows the storage image, whatever the GPU wrote to it since; guest memory @@ -817,10 +819,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spantexture; } } + if (it->bytes.size() == bytes) recycled = std::move(it->bytes); + else if (it->carried.size() == bytes) recycled = std::move(it->carried); eraseTexture(cache, it); ++cache.counts.stale; } - CachedTexture entry{key, address, std::vector(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; + CachedTexture entry{key, address, source != nullptr ? std::vector{} : (recycled.empty() ? std::vector(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration}; + if (source != nullptr) entry.carried = std::move(recycled); entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; From 3223b7a4b06e706590ba14befc478dfa4e71479b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 05:51:39 +0000 Subject: [PATCH 0032/1360] refactor(shader): reuse descriptor array lengths within a block --- .../tests/FormattedBufferOnce.cpp | 31 +++++++++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 7 +++++ .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 18 +++++++++-- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index 0eba15d628..fc46cdd1a5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -31,6 +31,7 @@ SpirvTarget Target() { struct Counts { std::size_t words = 0; + std::size_t arrayLengths = 0; std::size_t switches = 0; std::size_t calls = 0; std::size_t functions = 0; @@ -46,6 +47,7 @@ Counts Count(std::span words) { Check(count != 0 && count <= words.size() - offset, "formatted buffer once: malformed SPIR-V instruction"); const auto opcode = words[offset] & spv::OpCodeMask; if (opcode == spv::OpSwitch) ++counts.switches; + if (opcode == spv::OpArrayLength) ++counts.arrayLengths; if (opcode == spv::OpFunctionCall) ++counts.calls; if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; if (opcode == spv::OpFunction) { @@ -83,6 +85,32 @@ Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false return Count(Recompile(request).spirv.Words()); } +Counts CompileScalarReads(std::size_t loads) { + std::array userData{}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0u, {}, 0, {}}; + request.target = Target(); + request.layout = {0, 0, 0, 128}; + 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.useCache = false; + std::vector code; + for (std::uint32_t load = 0; load < loads; ++load) { + code.insert(code.end(), {0xf4000200u | (load * 4u), 0xfa000004u, 0x7e040208u | (load << 8u)}); + } + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0701000u | (load * 4u), 0x80010000u | ((10u + load) << 8u)}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + request.shader.codeAddress = reinterpret_cast(words.data()); + request.shader.code = words; + request.context.memory = memory; + return Count(Recompile(request).spirv.Words()); +} + } int main() { @@ -90,6 +118,8 @@ int main() { const auto none = Compile(0); const auto one = Compile(1); const auto three = Compile(3); + const auto scalarOne = CompileScalarReads(1); + const auto scalarThree = CompileScalarReads(3); const auto gpuOne = Compile(1, false, true); const auto vertexSpecialized = Compile(1, false, false, true, false); const auto vertexRegistration = Compile(1, false, false, true, true); @@ -105,6 +135,7 @@ int main() { Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); Check(three.words - one.words < (one.words - none.words) / 2u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared function (" + std::to_string(one.words - none.words) + ")"); + Check(scalarOne.arrayLengths == 1u && scalarThree.arrayLengths == scalarOne.arrayLengths, "scalar buffer repeated reads emitted " + std::to_string(scalarThree.arrayLengths) + " OpArrayLength instructions for 3 reads (expected one)"); const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 19cf5628f0..4735fbee9d 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -126,6 +126,13 @@ struct SpirvEmitterState { std::vector memoryByteOffsets; std::map, std::uint32_t> formattedBufferFunctions; std::map, std::uint32_t> formattedGpuBufferFunctions; + struct BufferLengthCacheEntry { + std::array descriptor{}; + std::uint32_t variable = 0; + std::uint32_t element = 0; + std::uint32_t length = 0; + }; + std::vector bufferLengths; std::uint32_t bufferFormatTable = 0; std::uint32_t bdaPagetableVariable = 0; std::uint32_t faultBufferVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index a19022f3c7..78008402ef 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -862,6 +862,7 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu if (block == nullptr) { context.Fail("structured control flow contains a null block"); } + state.bufferLengths.clear(); const BlockInfo* info = BlockInfoFor(program, block); if (info == nullptr) { context.Fail("structured control flow block has no terminator metadata"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index fa019cafce..bf84d35589 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -142,8 +142,22 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& state.module.AddFunction(spv::OpAccessChain, wide ? TypeStorageBufferU64Pointer(state) : TypeStorageBufferPointer(state), access.objectPointer, variable, ConstantU32(state, element)); access.byteOffset = state.memoryByteOffsets.at(element); access.memoryAccess = memory.coherent ? spv::MemoryAccessVolatileMask : 0u; - access.length = state.module.AllocateId(); - state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); + { + const auto* descriptor = instruction.Argument(0u)->Resolve(); + std::array key{}; + for (std::size_t index = 0; index < key.size(); ++index) key[index] = descriptor->Argument(index)->Resolve(); + const auto found = std::find_if(state.bufferLengths.begin(), state.bufferLengths.end(), [&](const auto& entry) { return entry.descriptor == key && entry.variable == variable && entry.element == element; }); + if (found != state.bufferLengths.end()) access.length = found->length; + else { + access.length = state.module.AllocateId(); + state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); + state.bufferLengths.push_back({key, variable, element, access.length}); + } + } + if (access.length == 0u) { + access.length = state.module.AllocateId(); + state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); + } return access; } From 268dfdda102f1f821698f6045629077faa539b11 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 14:10:16 -0400 Subject: [PATCH 0033/1360] fix(agc): separate pipeline drain and device lock timings --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 077fef550a..c1938a3b5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -240,8 +240,10 @@ void Driver::execute(const Submission& submission) { continue; } } + timing.Mark("pipeline_prepare"); if (pipelined && DrawPipeline::Queue0().Busy() && !keepsDrawPipeline(packet, header, opcode)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, header == FlipPacketHeader ? 0xffu : opcode); if (pipelined && predicated && DrawPipeline::Queue0().Busy() && DrawPipeline::Queue0().Overlaps(queue.predication.address, queue.predication.operation == 3 ? 8 : 4)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); + timing.Mark("pipeline_drain"); std::shared_lock deviceUse(deviceReplacement, std::defer_lock); if (opcode != 0x3c && opcode != 0x93 && header != RenderingWaitPacketHeader && header != FlipPacketHeader) deviceUse.lock(); From 43ca2784bc1bac223c612666d2db93ea13baacfc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:09:58 +0000 Subject: [PATCH 0034/1360] test(shader): validate buffer length dominance across atomic merge --- .../libSceAgcDriver/tests/FormattedBufferOnce.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index fc46cdd1a5..f30140ed91 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -19,8 +19,8 @@ void Check(bool condition, const std::string& message) { } SpirvTarget Target() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, spv::CapabilityInt64Atomics, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", @@ -59,9 +59,10 @@ Counts Count(std::span words) { return counts; } -Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false) { +Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false, bool atomicBeforeLoads = false) { std::vector code; if (gpu) code.push_back(0x7e160500u); + if (atomicBeforeLoads) code.insert(code.end(), {0xe1705000u, 0x80012803u, 0xbf8c3f70u}); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0000000u | (load * 4u), 0x80020000u | ((10u + load) << 8u)}); code.push_back(0xbf8c3f70u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {(formattedStores ? 0xe0100000u : 0xe0700000u) | (load * 4u), 0x80010000u | ((10u + load) << 8u)}); @@ -120,6 +121,7 @@ int main() { const auto three = Compile(3); const auto scalarOne = CompileScalarReads(1); const auto scalarThree = CompileScalarReads(3); + const auto atomicThenLoad = Compile(1, false, false, false, false, true); const auto gpuOne = Compile(1, false, true); const auto vertexSpecialized = Compile(1, false, false, true, false); const auto vertexRegistration = Compile(1, false, false, true, true); @@ -135,7 +137,12 @@ int main() { Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); Check(three.words - one.words < (one.words - none.words) / 2u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared function (" + std::to_string(one.words - none.words) + ")"); - Check(scalarOne.arrayLengths == 1u && scalarThree.arrayLengths == scalarOne.arrayLengths, "scalar buffer repeated reads emitted " + std::to_string(scalarThree.arrayLengths) + " OpArrayLength instructions for 3 reads (expected one)"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + Check(atomicThenLoad.words > 5u, "a 64-bit buffer atomic followed by a buffer read did not produce a SPIR-V module"); +#else + Check(scalarOne.arrayLengths == 1u && scalarThree.arrayLengths == 3u, "scalar buffer reads across generated labels emitted " + std::to_string(scalarThree.arrayLengths) + " OpArrayLength instructions for 3 reads (expected one per label)"); + Check(atomicThenLoad.arrayLengths == 2u, "a buffer query inside the 64-bit atomic path was reused after its merge (expected 2 OpArrayLength instructions, got " + std::to_string(atomicThenLoad.arrayLengths) + ")"); +#endif const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); From 87300c7cb259bfa69d36ad768ee5394b55e776da Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:14:45 +0000 Subject: [PATCH 0035/1360] fix(shader): scope buffer length reuse to defining label --- .../prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp | 2 +- .../include/SpirvBackend/SpirvEmitterState.hpp | 1 + .../recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp | 10 ++++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index f30140ed91..86b372cf72 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -141,7 +141,7 @@ int main() { Check(atomicThenLoad.words > 5u, "a 64-bit buffer atomic followed by a buffer read did not produce a SPIR-V module"); #else Check(scalarOne.arrayLengths == 1u && scalarThree.arrayLengths == 3u, "scalar buffer reads across generated labels emitted " + std::to_string(scalarThree.arrayLengths) + " OpArrayLength instructions for 3 reads (expected one per label)"); - Check(atomicThenLoad.arrayLengths == 2u, "a buffer query inside the 64-bit atomic path was reused after its merge (expected 2 OpArrayLength instructions, got " + std::to_string(atomicThenLoad.arrayLengths) + ")"); + Check(atomicThenLoad.arrayLengths > 1u, "a buffer query inside the 64-bit atomic path was reused after its merge (expected at least 2 OpArrayLength instructions, got " + std::to_string(atomicThenLoad.arrayLengths) + ")"); #endif const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 4735fbee9d..1a0794b91a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -130,6 +130,7 @@ struct SpirvEmitterState { std::array descriptor{}; std::uint32_t variable = 0; std::uint32_t element = 0; + std::uint32_t label = 0; std::uint32_t length = 0; }; std::vector bufferLengths; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index bf84d35589..9958d9649a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -146,18 +146,16 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& const auto* descriptor = instruction.Argument(0u)->Resolve(); std::array key{}; for (std::size_t index = 0; index < key.size(); ++index) key[index] = descriptor->Argument(index)->Resolve(); - const auto found = std::find_if(state.bufferLengths.begin(), state.bufferLengths.end(), [&](const auto& entry) { return entry.descriptor == key && entry.variable == variable && entry.element == element; }); + const auto found = std::find_if(state.bufferLengths.begin(), state.bufferLengths.end(), [&](const auto& entry) { + return entry.descriptor == key && entry.variable == variable && entry.element == element && entry.label == state.currentLabel; + }); if (found != state.bufferLengths.end()) access.length = found->length; else { access.length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); - state.bufferLengths.push_back({key, variable, element, access.length}); + state.bufferLengths.push_back({key, variable, element, state.currentLabel, access.length}); } } - if (access.length == 0u) { - access.length = state.module.AllocateId(); - state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); - } return access; } From 3c905ee2d6500f1eaeb9ea6e27f2a4cb780556ac Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 14:49:27 +0700 Subject: [PATCH 0036/1360] fix(agc): give a block-copied BC texture an sRGB view for FORCE_DEGAMMA samplers Co-Authored-By: Claude --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 ++ .../tests/execution/BcBlockCopy.cpp | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..4778470b76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -673,6 +673,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& imageInfo.arrayLayers = copyLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.flags = SrgbTwinFormat(context, blockFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -702,6 +703,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp index 23eeb6dcf7..216bfbc31d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp @@ -35,6 +35,7 @@ constexpr std::uint32_t Format32x2Uint = 62; constexpr std::uint32_t Format32x4Uint = 75; constexpr std::uint32_t FormatBc1Unorm = 169; constexpr std::uint32_t FormatBc7Unorm = 181; +constexpr std::uint32_t ForceDegamma = 1u << 20u; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t ClampEdge = 2; constexpr std::size_t BlockBytes = 1u << 20u; @@ -130,8 +131,8 @@ std::array TextureDescriptor(const void* data, const Shape& sh }; } -std::array SamplerDescriptor() { - return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +std::array SamplerDescriptor(std::uint32_t forced) { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u) | forced, 0u, 0u, 0u}; } ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { @@ -167,12 +168,12 @@ void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); } -std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape) { +std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t forced = 0u) { std::vector userData(24, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); const auto texture = TextureDescriptor(texels, shape); - const auto sampler = SamplerDescriptor(); + const auto sampler = SamplerDescriptor(forced); 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); @@ -189,12 +190,12 @@ std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* return {Output.begin(), Output.end()}; } -std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value) { +std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value, std::uint32_t forced = 0u) { GuestBlock block; auto* texels = block.Data(); Store(device, texels, test, value); Settle(device, texels, SurfaceBytes(texels, test.blocks)); - return Sample(device, texels, test.compressed); + return Sample(device, texels, test.compressed, forced); } void RequireSame(const std::vector& sampled, const std::vector& expected, const std::string& what) { @@ -208,7 +209,11 @@ void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { constexpr std::uint32_t Second = 0x9bdf6840u; const auto first = Reference(device, test, First); const auto second = Reference(device, test, Second); + const auto forcedFirst = Reference(device, test, First, ForceDegamma); + const auto forcedSecond = Reference(device, test, Second, ForceDegamma); Require(first != second, test.name + ": the two block patterns sample alike"); + Require(forcedFirst != first && forcedSecond != second, test.name + ": a sampler that forces sRGB decoding samples the stored blocks like a plain one"); + Require(forcedFirst != forcedSecond, test.name + ": the two block patterns sample alike through a sampler that forces sRGB decoding"); GuestBlock block; auto* texels = block.Data(); const auto bytes = SurfaceBytes(texels, test.compressed); @@ -222,6 +227,9 @@ void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { RequireSame(pending, second, test.name + ": sample of pending blocks"); Store(device, texels, test, First); RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of blocks stored again"); + Store(device, texels, test, Second); + RequireSame(Sample(device, texels, test.compressed, ForceDegamma), forcedSecond, test.name + ": sRGB-forced sample of pending blocks"); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes) != nullptr, test.name + ": the sRGB-forced sample stored the pending blocks to guest memory"); } } From 7ca5faf4a4672853c394cb291fac64ac5472169f Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 00:39:13 +0400 Subject: [PATCH 0037/1360] refactor(agc): write expanded quad-list indices through one index writer --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 94 ++++++++----------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index bce39d0a09..c98b7915e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1040,6 +1040,7 @@ struct DrawInputs { VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; std::vector restartTable; + std::uint32_t expandedIndexCount = 0; const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; } }; @@ -1128,17 +1129,43 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (state.quadList) { Require(!draw.indirect, "indirect quad-list draw is unsupported"); - const std::uint32_t quadCount = draw.indexCount / 4u; - if (quadCount == 0u || draw.instanceCount == 0u) { + const std::uint32_t primitives = draw.indexCount / 4u; + if (primitives == 0u || draw.instanceCount == 0u) { inputs.nothing = true; return inputs; } - const std::uint32_t outputIndexCount = quadCount * 6u; - const std::size_t quadIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); - auto indexCopy = std::make_shared(context, quadIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + const std::uint32_t corners = 6u; + const std::uint32_t outputIndexCount = primitives * corners; + const std::size_t expandedIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); + auto indexCopy = std::make_shared(context, expandedIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); auto* outIndices = reinterpret_cast(indexCopy->Bytes().data()); std::uint32_t maxIdx = 0; const bool lastProvoking = state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + const auto expand = [&](const auto& source) { + for (std::uint32_t primitive = 0; primitive < primitives; ++primitive) { + auto* out = outIndices + static_cast(primitive) * corners; + const auto i0 = source(primitive * 4u + 0u); + const auto i1 = source(primitive * 4u + 1u); + const auto i2 = source(primitive * 4u + 2u); + const auto i3 = source(primitive * 4u + 3u); + if (lastProvoking) { + out[0] = i0; + out[1] = i1; + out[2] = i3; + out[3] = i1; + out[4] = i2; + out[5] = i3; + } else { + out[0] = i0; + out[1] = i1; + out[2] = i2; + out[3] = i0; + out[4] = i2; + out[5] = i3; + } + for (std::uint32_t corner = 0; corner < corners; ++corner) maxIdx = std::max(maxIdx, out[corner]); + } + }; if (draw.indexed) { GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); @@ -1153,59 +1180,18 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P return v; } }; - for (std::uint32_t q = 0; q < quadCount; ++q) { - const auto i0 = readIndex(q * 4u + 0u); - const auto i1 = readIndex(q * 4u + 1u); - const auto i2 = readIndex(q * 4u + 2u); - const auto i3 = readIndex(q * 4u + 3u); - maxIdx = std::max({maxIdx, i0, i1, i2, i3}); - if (lastProvoking) { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i3; - outIndices[q * 6u + 3u] = i1; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } else { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i2; - outIndices[q * 6u + 3u] = i0; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } - } + expand(readIndex); Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "indexed draw vertex range overflow"); - inputs.maxIndex = maxIdx + draw.firstVertex; } else { - maxIdx = quadCount * 4u - 1u; - Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "auto draw vertex range overflow"); - for (std::uint32_t q = 0; q < quadCount; ++q) { - const auto i0 = q * 4u + 0u; - const auto i1 = q * 4u + 1u; - const auto i2 = q * 4u + 2u; - const auto i3 = q * 4u + 3u; - if (lastProvoking) { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i3; - outIndices[q * 6u + 3u] = i1; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } else { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i2; - outIndices[q * 6u + 3u] = i0; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } - } - inputs.maxIndex = maxIdx + draw.firstVertex; + const std::uint32_t lastVertex = primitives * 4u - 1u; + Require(draw.firstVertex <= std::numeric_limits::max() - lastVertex, "auto draw vertex range overflow"); + expand([](std::uint32_t idx) { return idx; }); } + inputs.maxIndex = maxIdx + draw.firstVertex; Require(inputs.maxIndex <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); inputs.indexHandle = indexCopy->Handle(); inputs.indexOffset = 0; + inputs.expandedIndexCount = outputIndexCount; inputs.indices = std::move(indexCopy); } else if (draw.indexed) { const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; @@ -1488,9 +1474,9 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const return; } if (!inputs.vertexHandles.empty()) context.Resolved(&DeviceFunctions::cmdBindVertexBuffers, "vkCmdBindVertexBuffers")(commands, 0, static_cast(inputs.vertexHandles.size()), inputs.vertexHandles.data(), inputs.vertexOffsets.data()); - if (state.quadList) { + if (inputs.expandedIndexCount != 0u) { context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, VK_INDEX_TYPE_UINT32); - context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, (draw.indexCount / 4u) * 6u, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); + context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, inputs.expandedIndexCount, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); return; } if (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); From 310ac4a26d815a3fc854d61c236b16c4219607d3 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 00:39:13 +0400 Subject: [PATCH 0038/1360] feat(agc): draw triangle strips through the barycentric tessellation stages --- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 65 ++++++++++++------- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 +- .../Graphics/src/ShaderValidation.cpp | 2 +- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 7672835ca9..558cadeb1b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -478,7 +478,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); } else { - require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart"); + require((graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list or strip without primitive restart"); auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents()); const auto first = results.size(); results.push_back(std::move(patches.control)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index c98b7915e7..0ae3e57693 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1044,6 +1044,10 @@ struct DrawInputs { const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; } }; +bool StripPatches(const State& state, std::span shaders) { + return state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && BarycentricPatches(state, shaders); +} + std::shared_ptr zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) { auto zero = std::make_shared(context, DecodeVertexFormat(attribute).bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); std::fill(zero->Bytes().begin(), zero->Bytes().end(), std::byte{0}); @@ -1127,14 +1131,14 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P ValidateSampleMask(context, state, shaders); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; - if (state.quadList) { - Require(!draw.indirect, "indirect quad-list draw is unsupported"); - const std::uint32_t primitives = draw.indexCount / 4u; + if (state.quadList || StripPatches(state, shaders)) { + Require(!draw.indirect, state.quadList ? "indirect quad-list draw is unsupported" : "an indirect triangle strip through emulated barycentric patches is unsupported"); + const std::uint32_t primitives = state.quadList ? draw.indexCount / 4u : draw.indexCount >= 3u ? draw.indexCount - 2u : 0u; if (primitives == 0u || draw.instanceCount == 0u) { inputs.nothing = true; return inputs; } - const std::uint32_t corners = 6u; + const std::uint32_t corners = state.quadList ? 6u : 3u; const std::uint32_t outputIndexCount = primitives * corners; const std::size_t expandedIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); auto indexCopy = std::make_shared(context, expandedIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); @@ -1144,24 +1148,37 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto expand = [&](const auto& source) { for (std::uint32_t primitive = 0; primitive < primitives; ++primitive) { auto* out = outIndices + static_cast(primitive) * corners; - const auto i0 = source(primitive * 4u + 0u); - const auto i1 = source(primitive * 4u + 1u); - const auto i2 = source(primitive * 4u + 2u); - const auto i3 = source(primitive * 4u + 3u); - if (lastProvoking) { - out[0] = i0; - out[1] = i1; - out[2] = i3; - out[3] = i1; - out[4] = i2; - out[5] = i3; + if (state.quadList) { + const auto i0 = source(primitive * 4u + 0u); + const auto i1 = source(primitive * 4u + 1u); + const auto i2 = source(primitive * 4u + 2u); + const auto i3 = source(primitive * 4u + 3u); + if (lastProvoking) { + out[0] = i0; + out[1] = i1; + out[2] = i3; + out[3] = i1; + out[4] = i2; + out[5] = i3; + } else { + out[0] = i0; + out[1] = i1; + out[2] = i2; + out[3] = i0; + out[4] = i2; + out[5] = i3; + } } else { - out[0] = i0; - out[1] = i1; - out[2] = i2; - out[3] = i0; - out[4] = i2; - out[5] = i3; + const std::uint32_t odd = primitive & 1u; + if (lastProvoking) { + out[0] = source(primitive + odd); + out[1] = source(primitive + 1u - odd); + out[2] = source(primitive + 2u); + } else { + out[0] = source(primitive); + out[1] = source(primitive + 1u + odd); + out[2] = source(primitive + 2u - odd); + } } for (std::uint32_t corner = 0; corner < corners; ++corner) maxIdx = std::max(maxIdx, out[corner]); } @@ -1183,7 +1200,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P expand(readIndex); Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "indexed draw vertex range overflow"); } else { - const std::uint32_t lastVertex = primitives * 4u - 1u; + const std::uint32_t lastVertex = state.quadList ? primitives * 4u - 1u : primitives + 1u; Require(draw.firstVertex <= std::numeric_limits::max() - lastVertex, "auto draw vertex range overflow"); expand([](std::uint32_t idx) { return idx; }); } @@ -2097,7 +2114,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(state.samples == 1u && (!state.depth || HasStencil(state.depth->format)), "stencil replay requires single-sample rendering and an eight-bit stencil format"); recipeOut = nullptr; } - if (state.quadList) recipeOut = nullptr; + if (state.quadList || StripPatches(state, shaders)) recipeOut = nullptr; if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); PerformanceTimer timing("Graphics.Draw"); // APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals. @@ -2765,7 +2782,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con }; Require(!draw.indirect, "a draw recipe covers direct draws only"); auto* recorder = Recorder::Active(); - if (state.quadList || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); + if (state.quadList || StripPatches(state, shaders) || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone); const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u); Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 465fa91e9e..8964fa27a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -329,7 +329,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); const bool barycentricPatches = BarycentricPatches(state, shaders); - if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart"); + if (barycentricPatches) Require(context.tessellationShader && (state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list or strip without primitive restart"); if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders"); if (state.stages.tessellation) { Require(context.tessellationShader, "device does not support tessellation shaders"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 61438856a5..8f6de8e80a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -650,7 +650,7 @@ std::set ValidateShaders(std::span shaders, const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u; const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active; Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count"); - if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list"); + if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, "emulated barycentrics through tessellation require a triangle list or strip"); if (geometry) { Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives"); From 54fa48cb2a7927ad41f437d46196048069861bb7 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 00:39:14 +0400 Subject: [PATCH 0039/1360] test(agc): check emulated barycentrics of an even and an odd strip triangle --- .../execution/PixelBarycentricEmulation.cpp | 78 ++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 9978674554..aebfe16d45 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -53,6 +53,14 @@ std::array, 3> ClipPositions() { return positions; } +constexpr std::array StripCorners{{{-0.8f, -0.8f, 1.0f}, {0.8f, -0.8f, 1.0f}, {-0.8f, 0.8f, 1.0f}, {0.8f, 0.8f, 1.0f}}}; + +std::array, 4> StripPositions() { + std::array, 4> positions{}; + for (std::size_t i = 0; i < StripCorners.size(); ++i) positions[i] = {StripCorners[i].x, StripCorners[i].y, 0.5f, 1.0f}; + return positions; +} + std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; @@ -63,11 +71,13 @@ enum class Emulation { Tessellation, }; -ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool strip = false) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; - static const auto positions = ClipPositions(); + static const auto trianglePositions = ClipPositions(); + static const auto stripPositions = StripPositions(); + const auto positions = strip ? std::span>(stripPositions) : std::span>(trianglePositions); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast(positions.size())); @@ -120,7 +130,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = strip ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; @@ -256,6 +266,67 @@ void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const st } } +struct StripPixel { + bool inside; + std::array order; + std::array weights; +}; + +StripPixel StripBarycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { + std::array, 4> window{}; + for (std::size_t i = 0; i < StripCorners.size(); ++i) { + window[i] = {(StripCorners[i].x + 1.0) * Width / 2.0, (1.0 - StripCorners[i].y) * Height / 2.0}; + if (snapped) { + for (auto& coordinate : window[i]) coordinate = std::round(coordinate * 256.0) / 256.0; + } + } + const double px = x + 0.5; + const double py = y + 0.5; + const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; + for (const std::array order : {std::array{0, 1, 2}, std::array{1, 3, 2}}) { + const auto& a = window[order[0]]; + const auto& b = window[order[1]]; + const auto& c = window[order[2]]; + const double area = edge(a, b, c[0], c[1]); + const std::array weights{edge(b, c, px, py) / area, edge(c, a, px, py) / area, edge(a, b, px, py) / area}; + if (std::min({weights[0], weights[1], weights[2]}) > 0.02) return {true, order, weights}; + } + return {false, {}, {}}; +} + +void CheckStrip(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + Draw(device, emulation, BarycentricPixelCode, PixelInputBit(PixelInput::LinearCenter), VK_CULL_MODE_NONE, true); + std::array checked{}; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto snapped = StripBarycentrics(x, y, true); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto reference = static_cast(expected.weights[1u + (channel & 1u)]); + const auto snappedReference = snapped.inside && snapped.order == expected.order ? static_cast(snapped.weights[1u + (channel & 1u)]) : reference; + Require(MatchesEither(Pixel(x, y, channel), reference, snappedReference), name + " triangle strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(reference)); + } + ++checked[expected.order[0]]; + } + } + Require(checked[0] > 200u && checked[1] > 200u, name + " triangle strip: too few pixels of each triangle were checked"); + static const auto positions = StripPositions(); + Draw(device, emulation, VertexValuePixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, true); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto first = positions[expected.order[0]][0]; + const std::array values{positions[expected.order[1]][0] - first, positions[expected.order[2]][0] - first, first}; + for (std::uint32_t channel = 0; channel < 3; ++channel) { + Require(Pixel(x, y, channel) == values[channel], name + " triangle strip v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(values[channel])); + } + } + } + std::printf("barycentric emulation through the %s passed for a triangle strip: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); +} + void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); @@ -273,6 +344,7 @@ void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); CheckInterpolated(); CheckWinding(device, emulation, name); + CheckStrip(device, emulation, name); std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); } From ba9991c6481870279e591a49e3876aad9691aee6 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 00:39:24 +0400 Subject: [PATCH 0040/1360] docs(debt): record triangle strips through the barycentric tessellation stages --- docs/dev/debt/libSceAgcDriver.md | 1 + docs/dev/debt/shader.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2cde0d07ac..9abd3a2cac 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -121,6 +121,7 @@ - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. A geometry program's primitives keep the vertex indices of its own primitive export on the [mesh path](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp), so last mode takes their flat inputs (from [Vulkan](https://docs.vulkan.org/spec/latest/chapters/vertexpostproc.html#vertexpostproc-flatshading)) and their [layer](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) from the last index of each primitive, while their per-vertex inputs keep the order of first mode ([Vulkan](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-barycentric)); that the console does the same for an exported primitive is not measured. Last mode throws without `VK_EXT_provoking_vertex`, for tessellation primitives, for triangle strips or fans assembled for a geometry program (whose odd-triangle order on the console is not measured), explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 8c179efa90..9648199d80 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -93,7 +93,7 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle fans and primitive restart without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). From 20ddc95c566ef660e07de2161dc68596c355a5fa Mon Sep 17 00:00:00 2001 From: Izamaru Date: Mon, 19 Oct 2026 18:01:31 -0300 Subject: [PATCH 0041/1360] fix(agc): allocate Windows page permissions on demand --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 ++ .../Execution/src/GuestMemory.cpp | 57 +++++-- .../prx/libSceAgcDriver/tests/PageStates.cpp | 145 ++++++++++++++++++ 3 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/PageStates.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d4b9a576e1..139b4c5fa1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -629,6 +629,17 @@ target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel l set_target_properties(agc_shared_write_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shared_write_tests) +if(BUILD_TESTING AND WIN32) + add_test_executable(agc_page_state_tests tests/PageStates.cpp) + target_include_directories(agc_page_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_page_state_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(agc_page_state_tests) + add_test(NAME agc_page_states COMMAND agc_page_state_tests) + add_test(NAME agc_page_state_metadata COMMAND agc_page_state_tests --metadata) + set_tests_properties(agc_page_states agc_page_state_metadata PROPERTIES + TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") +endif() + add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 90afd6a816..23f2e4ce41 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -367,18 +368,38 @@ struct PageSpan { std::size_t size = 0; // Per page: access flags, zero while unknown. std::uint8_t* pages = nullptr; + static constexpr std::size_t ChunkEntries = 65536; + struct alignas(8) Chunk { std::array entries{}; }; + std::atomic* chunks = nullptr; + + std::uint8_t* chunkData(std::size_t index, bool create) const { + auto* chunk = chunks[index].load(std::memory_order_acquire); + if (chunk == nullptr && create) { + auto fresh = std::unique_ptr(new (std::nothrow) Chunk{}); + if (fresh != nullptr && chunks[index].compare_exchange_strong(chunk, fresh.get(), std::memory_order_acq_rel, std::memory_order_acquire)) chunk = fresh.release(); + } + return chunk == nullptr ? nullptr : chunk->entries.data(); + } + + std::uint8_t* entry(std::size_t index, bool create) const { + if (chunks == nullptr) return pages + index; + auto* data = chunkData(index / ChunkEntries, create); + return data == nullptr ? nullptr : data + index % ChunkEntries; + } - // Reserves and commits the table; untouched table pages cost nothing until a guest page is recorded. bool allocate() { if (size == 0) return false; const auto count = (size / PageBytes + 8) & ~static_cast(7); #ifdef _WIN32 - pages = static_cast(VirtualAlloc(nullptr, count, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + const auto chunkCount = (count + ChunkEntries - 1) / ChunkEntries; + chunks = new (std::nothrow) std::atomic[chunkCount]{}; + if (chunks == nullptr) size = 0; + return chunks != nullptr; #else pages = static_cast(std::calloc(count, 1)); -#endif if (pages == nullptr) size = 0; return pages != nullptr; +#endif } bool covers(std::uintptr_t address) const { @@ -386,11 +407,23 @@ struct PageSpan { } std::uint8_t load(std::uintptr_t address) const { - return std::atomic_ref(const_cast(pages[(address - base) / PageBytes])).load(std::memory_order_relaxed); + auto* value = entry((address - base) / PageBytes, false); + return value == nullptr ? 0 : std::atomic_ref(*value).load(std::memory_order_relaxed); } void store(std::uintptr_t address, std::uint8_t value) { - std::atomic_ref(pages[(address - base) / PageBytes]).store(value, std::memory_order_relaxed); + if (auto* at = entry((address - base) / PageBytes, value != 0 && value != PageInaccessible)) std::atomic_ref(*at).store(value, std::memory_order_relaxed); + } + + void store(std::uintptr_t first, std::uintptr_t last, std::uint8_t value) { + auto index = (first - base) / PageBytes; + const auto stop = (last - 1 - base) / PageBytes + 1; + while (index < stop) { + const auto next = std::min(stop, (index / ChunkEntries + 1) * ChunkEntries); + if (auto* at = entry(index, value != 0 && value != PageInaccessible)) { + for (; index < next; ++index, ++at) std::atomic_ref(*at).store(value, std::memory_order_relaxed); + } else index = next; + } } // End of the run of pages equal to `value` that starts at `address`, at most `limit`. @@ -399,11 +432,17 @@ struct PageSpan { const auto stop = std::min((std::min(limit, base + size) - base + PageBytes - 1) / PageBytes, size / PageBytes); const std::uint64_t wide = 0x0101010101010101ull * value; while (index < stop) { - if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(const_cast(pages + index))).load(std::memory_order_relaxed) == wide) { + auto* at = entry(index, false); + if (at == nullptr) { + if (value != 0) break; + index = std::min(stop, (index / ChunkEntries + 1) * ChunkEntries); + continue; + } + if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(at)).load(std::memory_order_relaxed) == wide) { index += 8; continue; } - if (std::atomic_ref(const_cast(pages[index])).load(std::memory_order_relaxed) != value) break; + if (std::atomic_ref(*at).load(std::memory_order_relaxed) != value) break; ++index; } return std::min(limit, base + index * PageBytes); @@ -413,7 +452,7 @@ struct PageSpan { if (size == 0 || bytes == 0 || address >= base + size || address + bytes <= base) return; const auto first = (std::max(address, base) - base) / PageBytes; const auto last = (std::min(address + bytes, base + size) - 1 - base) / PageBytes; - for (auto page = first; page <= last; ++page) std::atomic_ref(pages[page]).store(0, std::memory_order_relaxed); + store(base + first * PageBytes, base + (last + 1) * PageBytes, 0); } }; @@ -444,7 +483,7 @@ struct PageStates { const std::uint8_t value = readable ? static_cast(PageReadable | (writable ? PageWritable : 0u)) : PageInaccessible; const auto first = std::max(base, span->base); const auto last = std::min(regionEnd, span->base + span->size); - for (auto at = first; at < last; at += PageBytes) span->store(at, value); + span->store(first, last, value); if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation || accessSerial.load(std::memory_order_acquire) != access) span->forget(first, static_cast(last - first)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/PageStates.cpp b/core/libs/prx/libSceAgcDriver/tests/PageStates.cpp new file mode 100644 index 0000000000..fb92db485c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PageStates.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +namespace { +using namespace AgcDriver::GuestMemory; +constexpr std::size_t Unit = 65536; +constexpr std::size_t ChunkGuestBytes = 256ull << 20u; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Range { +public: + explicit Range(std::size_t bytes) : bytes(bytes) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, ChunkGuestBytes)); + Require(data != nullptr, "cannot reserve page-state fixture"); + } + + ~Range() { + { + GuestAllocations::Mutation mutation; + for (const auto offset : committed) mutation.Remove(data + offset); + } + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } + + std::byte* Commit(std::size_t offset) { + auto* at = data + offset; + GuestArena::GuestArenaCommit_nid_postfix(at, Unit, PAGE_READWRITE, Unit); + std::memset(at, 0x5b, Unit); + GuestAllocations::Mutation mutation; + mutation.Add(at, Unit, true, true, true); + committed.push_back(offset); + return at; + } + + std::byte* Data() const { return data; } + +private: + std::byte* data = nullptr; + std::size_t bytes; + std::vector committed; +}; + +std::size_t PrivateBytes() { + PROCESS_MEMORY_COUNTERS_EX counters{}; + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)), "cannot query page-state fixture private commit"); + return counters.PrivateUsage; +} + +void CheckMetadata() { + Range range(3 * ChunkGuestBytes); + auto* data = range.Commit(0); + const auto before = PrivateBytes(); + Require(Accessible(data, Unit, true), "committed fixture is inaccessible"); + const auto after = PrivateBytes(); + const auto added = after > before ? after - before : 0; + std::printf("page-permission initialization: %zu bytes of private commit\n", added); + Require(added < (16ull << 20u), "page permissions committed a dense arena table"); + const auto queried = PrivateBytes(); + Require(!Accessible(range.Data() + 2 * ChunkGuestBytes, Unit), "reserved fixture tail is accessible"); + Require(PrivateBytes() < queried + (4ull << 20u), "querying reserved pages allocated untouched metadata chunks"); +} + +void CheckBoundaries() { + constexpr auto span = 3 * ChunkGuestBytes; + Range range(span); + const std::array offsets{0, ChunkGuestBytes - Unit, ChunkGuestBytes, 2 * ChunkGuestBytes + Unit}; + for (const auto offset : offsets) range.Commit(offset); + auto* block = range.Data(); + std::atomic begin{false}; + std::atomic errors{0}; + std::vector readers; + for (unsigned reader = 0; reader < 8; ++reader) { + readers.emplace_back([&] { + while (!begin.load(std::memory_order_acquire)) std::this_thread::yield(); + for (unsigned round = 0; round < 500; ++round) { + if (!Accessible(block + offsets.back(), Unit, true)) errors.fetch_add(1, std::memory_order_relaxed); + } + }); + } + begin.store(true, std::memory_order_release); + for (auto& reader : readers) reader.join(); + Require(errors.load() == 0, "concurrent page-state publication changed access"); + for (const auto offset : offsets) { + std::array copy{}; + Read(reinterpret_cast(block + offset), copy); + Require(std::all_of(copy.begin(), copy.end(), [](std::byte value) { return value == std::byte{0x5b}; }), "page cache changed guest bytes"); + } + Require(Accessible(block + ChunkGuestBytes - Unit, 2 * Unit, true), "adjacent chunks lost their boundary pages"); + const auto described = DescribeCommitted(reinterpret_cast(block), span, true); + std::uint64_t total = 0; + for (const auto& [first, last] : described.ranges) total += last - first; + Require(!described.whole && total == offsets.size() * Unit, "reserved holes were reported committed"); + auto* changed = block + offsets[1]; + const auto protect = [&](bool writable) { + GuestAllocations::Mutation mutation; + mutation.Protect(changed, Unit, true, writable, true, [&] { + const DWORD protection = writable ? PAGE_READWRITE : PAGE_READONLY; + DWORD previous = 0; + Require(VirtualProtect(changed, Unit, protection, &previous) != 0, "cannot protect page-state fixture"); + GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(changed), Unit, protection); + }); + }; + protect(false); + Require(Accessible(changed, Unit) && !Accessible(changed, Unit, true), "protection invalidation retained writable state"); + protect(true); + Require(Accessible(changed, Unit, true), "restored writable state was not discovered"); + GuestArena::GuestArenaReset_nid_postfix(block, Unit); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block), Unit); + Require(!Accessible(block, Unit), "decommitted pages remained readable"); + GuestArena::GuestArenaCommit_nid_postfix(block, Unit, PAGE_READWRITE, Unit); + Require(Accessible(block, Unit, true), "access observer did not invalidate inaccessible state after commit"); + std::puts("page-state boundaries, publication, holes and invalidation passed"); +} +} + +int main(int argc, char** argv) { + try { + Require(GuestArena::GuestArenaAvailable_nid_postfix(), "page-state fixture needs the guest arena"); + if (argc == 2 && std::string_view(argv[1]) == "--metadata") CheckMetadata(); + else CheckBoundaries(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From efd895b5118d793d5a6e7c9f39481ffeebf73e87 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 01:07:33 +0400 Subject: [PATCH 0042/1360] test(agc): accept triangle strips in the barycentric patch validation --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4d309b99dc..4fc605b475 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7179,9 +7179,11 @@ void barycentricEmulationTests() { Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches"); state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; ValidateShaders(patchShaders, state, subgroup, false); - for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + ValidateShaders(patchShaders, state, subgroup, false); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { state.topology = topology; - expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list"); + expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list or strip"); } state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; const std::array plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}}; From 11cc05929f6f552a900dd7d7a1934e0f34741af2 Mon Sep 17 00:00:00 2001 From: ebosido444-ai Date: Tue, 20 Oct 2026 00:59:30 +0300 Subject: [PATCH 0043/1360] fix(agc): place a layered depth storage view at the guest's layer pitch A 2D-array storage view over a depth surface is served as one storage image with a copy per layer, but each layer's depth surface was looked up at layer * DepthSliceBytes, and any other guest layer pitch was refused outright even though the sampling path already reads each layer at geometry.GuestLayerOffset(layer). The lookup now uses the descriptor geometry's layer pitch, kept with the layered storage, and the refusal is gone: a tight linear depth array whose pitch is under the 64 KiB-aligned slice size is the ordinary case, not an error. --- .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 10 +++------- .../libSceAgcDriver/tests/execution/DepthStorage.cpp | 12 ++++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b28491b129..a69d2a538d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1151,6 +1151,7 @@ class LayeredDepthStorage { public: LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t firstLayer, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), firstLayer(firstLayer), layers(layers) { const auto bytes = DescribeSurface(resource).guestBytes; + layerPitchBytes = DescribeSurface(resource).GuestLayerOffset(1); StorageTexture::FlushPending(resource.baseAddress, static_cast(bytes), nullptr, "storage image creation", PublishScope::None); storage = StorageTexture::Create(context, *context.detiler, resource, 0); Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers"); @@ -1204,6 +1205,7 @@ public: const std::uint32_t bytesPerTexel; const std::uint32_t firstLayer; const std::uint32_t layers; + std::uint64_t layerPitchBytes = 0; std::shared_ptr storage; private: @@ -1947,7 +1949,7 @@ std::vector layeredSlices(const Context& context, const LayeredDep std::vector slices; const auto& list = surfaces(); for (std::uint32_t layer = storage.firstLayer; layer < storage.layers; ++layer) { - const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel); + const auto address = base.address + static_cast(layer) * storage.layerPitchBytes; const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && !surface->retired && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); if (slice != list.rend()) slices.push_back({slice->get(), layer}); } @@ -1960,12 +1962,6 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytesPerTexel = d16 ? 2u : 4u; surface.RequireStorable(resource); - const auto geometry = DescribeSurface(resource); - if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { - char text[256]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast(target.address), static_cast(geometry.GuestLayerOffset(1)), static_cast(DepthSliceBytes(target.extent, bytesPerTexel))); - throw std::runtime_error(text); - } auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseArray}]; if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, resource.baseArray, layers); else static_cast(entry->storage->Refresh()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 3ab804d307..d5a4fac983 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -250,9 +250,17 @@ void Run(Context context, std::uint8_t* block) { layeredStencil.depthOrLastArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted"); layeredDepth.depthOrLastArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); + const auto layerPitch = static_cast(DescribeSurface(layeredDepth).GuestLayerOffset(1)); + const auto sliceBytes = DepthSliceBytes({Side, Side}, 4u); + Require(layerPitch != sliceBytes, "the test's layered depth view has the depth slice size as its layer pitch"); + const auto layered = DepthSurfaceStorage(context, {}, layeredDepth, 0); + Require(layered != nullptr && layered->ImageLayers() == 2, "a two-layer 2D array view over the depth plane's guest layer pitch was not taken as a storage image"); + submit(recorder); + readBack(*layered); + Require(layerPitch + sliceBytes > BlockBytes || CountFloats(block + layerPitch, 0.5f) == Side * Side, "the two-layer 2D array view did not place its layers at the guest layer pitch"); + Require(CountFloats(block, 0.5f) == Side * Side, "the two-layer 2D array view did not receive the layer the depth surface holds"); layeredDepth.baseArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a 2D array view of layer 1 of the depth plane with a layer pitch other than the depth slice size was accepted"); + Require(DepthSurfaceStorage(context, {}, layeredDepth, 0) != nullptr, "a 2D array view from layer 1 of the depth plane was not taken as a storage image"); const auto own = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); const std::array eighth{std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f)}; Require(own->FillClear(eighth, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); From b236ea557b1b7681141c5e81b1436a87b5a17074 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 01:05:09 +0300 Subject: [PATCH 0044/1360] feat(libSceCamera): report that no camera is attached Add libSceCamera with sceCameraIsAttached, which reports camera 0 as not attached and rejects other indexes, and declare the ten camera functions PS4 titles import without an implementation. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCamera/CMakeLists.txt | 21 ++++++++ core/libs/prx/libSceCamera/Export.cpp | 65 +++++++++++++++++++++++ core/libs/tests/GuestCamera.cpp | 61 +++++++++++++++++++++ docs/dev/debt/libSceCamera.md | 9 ++++ 4 files changed, 156 insertions(+) create mode 100644 core/libs/prx/libSceCamera/CMakeLists.txt create mode 100644 core/libs/prx/libSceCamera/Export.cpp create mode 100644 core/libs/tests/GuestCamera.cpp create mode 100644 docs/dev/debt/libSceCamera.md diff --git a/core/libs/prx/libSceCamera/CMakeLists.txt b/core/libs/prx/libSceCamera/CMakeLists.txt new file mode 100644 index 0000000000..27f982657d --- /dev/null +++ b/core/libs/prx/libSceCamera/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceCamera SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceCamera PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCamera) + +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_camera_tests ${LIBS_INCLUDE_DIR}/tests/GuestCamera.cpp) + target_include_directories(guest_camera_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_camera_tests PRIVATE libSceCamera libc) + configure_windows_unwind(guest_camera_tests) + add_test(NAME guest_camera COMMAND guest_camera_tests) + if(WIN32) + set_tests_properties(guest_camera PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCamera/Export.cpp b/core/libs/prx/libSceCamera/Export.cpp new file mode 100644 index 0000000000..26319c1e6c --- /dev/null +++ b/core/libs/prx/libSceCamera/Export.cpp @@ -0,0 +1,65 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +static constexpr int SCE_CAMERA_ERROR_PARAM = static_cast(0x802E0000); + +extern "C" { + +int APS5_VABI sceCameraIsAttached(int32_t index) { + if (index != 0) return SCE_CAMERA_ERROR_PARAM; + return 0; +} + +int APS5_VABI sceCameraOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraIsValidFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/tests/GuestCamera.cpp b/core/libs/tests/GuestCamera.cpp new file mode 100644 index 0000000000..95647e9a97 --- /dev/null +++ b/core/libs/tests/GuestCamera.cpp @@ -0,0 +1,61 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCameraIsAttached(std::int32_t index); +int APS5_VABI sceCameraOpen(void); +int APS5_VABI sceCameraClose(void); +int APS5_VABI sceCameraStart(void); +int APS5_VABI sceCameraStop(void); +int APS5_VABI sceCameraGetConfig(void); +int APS5_VABI sceCameraSetConfig(void); +int APS5_VABI sceCameraSetAttribute(void); +int APS5_VABI sceCameraGetDeviceInfo(void); +int APS5_VABI sceCameraGetFrameData(void); +int APS5_VABI sceCameraIsValidFrameData(void); +} + +namespace { + +constexpr int ErrorParam = static_cast(0x802E0000u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Camera: %s\n", message); + std::abort(); + } +} + +template +void RequireNotImplemented(TCall call, const std::string& name) { + try { + call(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == name + " not implemented", "a declared camera function threw another error"); + return; + } + Require(false, "a declared camera function returned"); +} + +} + +int main() { + Require(sceCameraIsAttached(0) == 0, "camera 0 must be reported as not attached"); + Require(sceCameraIsAttached(1) == ErrorParam, "camera index 1 must be rejected"); + Require(sceCameraIsAttached(-1) == ErrorParam, "a negative camera index must be rejected"); + RequireNotImplemented([] { sceCameraOpen(); }, "sceCameraOpen"); + RequireNotImplemented([] { sceCameraClose(); }, "sceCameraClose"); + RequireNotImplemented([] { sceCameraStart(); }, "sceCameraStart"); + RequireNotImplemented([] { sceCameraStop(); }, "sceCameraStop"); + RequireNotImplemented([] { sceCameraGetConfig(); }, "sceCameraGetConfig"); + RequireNotImplemented([] { sceCameraSetConfig(); }, "sceCameraSetConfig"); + RequireNotImplemented([] { sceCameraSetAttribute(); }, "sceCameraSetAttribute"); + RequireNotImplemented([] { sceCameraGetDeviceInfo(); }, "sceCameraGetDeviceInfo"); + RequireNotImplemented([] { sceCameraGetFrameData(); }, "sceCameraGetFrameData"); + RequireNotImplemented([] { sceCameraIsValidFrameData(); }, "sceCameraIsValidFrameData"); + return 0; +} diff --git a/docs/dev/debt/libSceCamera.md b/docs/dev/debt/libSceCamera.md new file mode 100644 index 0000000000..d0346584a7 --- /dev/null +++ b/docs/dev/debt/libSceCamera.md @@ -0,0 +1,9 @@ +# libSceCamera technical debt + +### Unknown function info + +- [sceCameraOpen, sceCameraClose, sceCameraStart, sceCameraStop, sceCameraGetConfig, sceCameraSetConfig, sceCameraSetAttribute, sceCameraGetDeviceInfo, sceCameraGetFrameData, sceCameraIsValidFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - imported by PS4 titles and added without an implementation, declared without parameters. What the console returns from them while no camera is attached is not known, so every call throws. + +### Functional + +- [sceCameraIsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - no camera is ever attached: index 0 returns 0 (not attached) and any other index returns `SCE_CAMERA_ERROR_PARAM` (0x802E0000), as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera.cpp#L187-L200) does without a configured camera, with the error value from its [camera_error.h](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera_error.h). Not measured on a console. Closes when: a host camera is exposed to guests. From 2b991e8751935c5f8d666b16676d9d7872ae0a0b Mon Sep 17 00:00:00 2001 From: simonbronk Date: Sat, 17 Oct 2026 19:07:22 +0200 Subject: [PATCH 0045/1360] fix(json2): keep numeric conversion independent of host locale Prevent decimal-comma LC_NUMERIC from emitting invalid JSON and silently truncating decimal-point input. Keep 17-digit output, signed underflow and overflow rejection without changing the process locale. AI-assisted: yes (OpenAI Codex). Windows WinLibs GCC15.2 Release: original shared Json2 fails the final locale regression; fixed guest_json and guest_json_numeric_locale pass (2/2, no skips). Final conventions pass. Only relevant runtime/test targets built; full build/full suite and game/console/Linux/macOS behavior unverified. No performance or coverage increase claimed. --- core/libs/prx/libSceJson2/CMakeLists.txt | 4 +- core/libs/prx/libSceJson2/Export.cpp | 20 +++++-- core/libs/tests/GuestJson.cpp | 75 +++++++++++++++++++++++- docs/dev/debt/libSceJson2.md | 2 + 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index f3e7cb682f..edfcf10e10 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -8,8 +8,10 @@ if(BUILD_TESTING) target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc) configure_windows_unwind(guest_json_tests) add_test(NAME guest_json COMMAND guest_json_tests) + add_test(NAME guest_json_numeric_locale COMMAND guest_json_tests --numeric-locale) + set_tests_properties(guest_json_numeric_locale PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_json guest_json_numeric_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 745771a3b4..d75d355ee7 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,12 +1,15 @@ #include #include +#include #include #include #include #include #include #include +#include #include +#include #include #include #include "SceTypes.hpp" @@ -286,8 +289,9 @@ void AppendEscaped(std::string& out, const std::string& text) { std::string RealText(double value) { if (!std::isfinite(value)) throw std::runtime_error("sce::Json: cannot serialize a non-finite number"); char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "%.17g", value); - return buffer; + const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 17); + if (result.ec != std::errc{}) throw std::runtime_error("sce::Json: real number formatting failed"); + return std::string(buffer, result.ptr); } void Serialize(std::string& out, const Node& n) { @@ -445,8 +449,16 @@ private: } } } - const double real = std::strtod(text.c_str(), nullptr); - if (!std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); + double real = 0; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), real); + bool converted = parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size(); + if (parsed.ec == std::errc::result_out_of_range) { + std::istringstream stream(text); + stream.imbue(std::locale::classic()); + stream >> real; + converted = !stream.fail() && stream.eof(); + } + if (!converted || !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; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 7c438e3867..eeac38d083 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include @@ -7,6 +9,7 @@ #include #include #include +#include #include struct Value { void* node; }; @@ -931,7 +934,77 @@ static void ValueSpecialFloatHandler() { _ZN3sce4Json5ValueD1Ev(&real); } -int main() { +static int NumericLocale() { + const std::string previous = std::setlocale(LC_NUMERIC, nullptr); + const char* selected = nullptr; + for (const char* name : {"de_DE.UTF-8", "de_DE.utf8", "de-DE", "German_Germany.1252", "fr_FR.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) && std::strcmp(std::localeconv()->decimal_point, ",") == 0) { + selected = name; + break; + } + } + if (!selected) { + std::setlocale(LC_NUMERIC, previous.c_str()); + std::fprintf(stderr, "No decimal-comma numeric locale is installed\n"); + return 77; + } + std::printf("JSON numeric locale: %s\n", selected); + int failures = 0; + const auto check = [&](bool success, const char* operation, const char* actual) { + if (!success) { + std::fprintf(stderr, "%s failed: %s\n", operation, actual); + ++failures; + } + }; + for (const auto number : {1.5, -0.125, 1.25e30, std::numeric_limits::max(), + std::numeric_limits::min(), std::numeric_limits::denorm_min()}) { + Value value{}; + _ZN3sce4Json5ValueC1Ed(&value, number); + const auto text = Serialize(value); + check(text.find(',') == std::string::npos, "Decimal-point serialization", text.c_str()); + Value parsed{}; + _ZN3sce4Json5ValueC1Ev(&parsed); + const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&parsed, text.c_str(), text.size()); + check(result == 0 && *_ZNK3sce4Json5Value7getRealEv(&parsed) == number, "Numeric round trip", text.c_str()); + _ZN3sce4Json5ValueD1Ev(&parsed); + _ZN3sce4Json5ValueD1Ev(&value); + } + for (const auto [text, number] : {std::pair{"1.5", 1.5}, {"-1.25e2", -125.0}, + {"4.9406564584124654e-324", std::numeric_limits::denorm_min()}, + {"2.4703282292062327e-324", 0.0}, + {"2.4703282292062328e-324", std::numeric_limits::denorm_min()}, + {"18446744073709551616", 18446744073709551616.0}, + {"1e-999", 0.0}, {"-1e-999", -0.0}}) { + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)); + const auto parsed = *_ZNK3sce4Json5Value7getRealEv(&value); + check(result == 0 && parsed == number && std::signbit(parsed) == std::signbit(number), "Decimal-point parsing", text); + _ZN3sce4Json5ValueD1Ev(&value); + } + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + constexpr char document[] = "{\"value\":1.5,\"list\":[2.5,-0.125]}"; + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, document, sizeof(document) - 1) == 0, + "Nested numeric parsing", document); + const auto serialized = Serialize(root); + check(serialized == document, "Nested numeric serialization", serialized.c_str()); + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1,5", 3) != 0, "Reject decimal comma", "1,5"); + bool overflow = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1e309", 5); + } catch (const std::runtime_error&) { + overflow = true; + } + check(overflow, "Retain overflow rejection", "1e309"); + _ZN3sce4Json5ValueD1Ev(&root); + check(std::strcmp(std::localeconv()->decimal_point, ",") == 0, "Preserve host locale", selected); + std::setlocale(LC_NUMERIC, previous.c_str()); + return failures ? 1 : 0; +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--numeric-locale") == 0) return NumericLocale(); ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index c5ee284c4c..f3587ae29d 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -2,6 +2,8 @@ ### Unknown function info +- [JSON real-number conversion](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) uses locale-independent `to_chars` with 17 significant digits and `from_chars`, following JSON's decimal-point grammar in RFC 8259 section 6. Out-of-range input falls back to a classic-locale stream so underflow retains the prior signed-zero behavior and overflow throws. Console rounding and formatting are unverified. + - [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown From 7372189a7162e83fa57c86332bff32a65b36c071 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Tue, 20 Oct 2026 02:40:00 +0530 Subject: [PATCH 0046/1360] fix(agc): write-watch the AGC driver memory on Linux On Linux the 2 MiB AGC driver memory at 0xfe0000000 is a plain mapping in the arena's system-reserved hole, outside the write watch, while Windows commits it inside the write-watched arena. Every address-based build maps the registered ranges, so when the host import budget refuses the range it gets no heap mirror and is copied on the CPU for every build. The range is now registered with the write watch, so a refused import is served from a mirror refreshed by the blocks the CPU writes. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 4 ++++ .../libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index b70750163e..16d0d9ed78 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -14,6 +14,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "ShaderDiskCache.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include #include #include @@ -87,6 +88,9 @@ void MapDriverMemory() { auto* slot = reinterpret_cast(FsTableAddress); slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u; slot[1] = FsTableDword3 << 32u | FsRoutineRecords; +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(mapped, DriverMemoryBytes); +#endif GuestAllocations::Mutation mutation; mutation.Add(mapped, DriverMemoryBytes, true, true, true); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp index 923b3b7212..15e527edc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" #include @@ -20,6 +21,8 @@ constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Untouched = 0xcafebabeu; constexpr std::uint32_t Marker = 0x55u; constexpr std::uint64_t FsTable = 0xfe0040000ull; +constexpr std::uint64_t DriverMemory = 0xfe0000000ull; +constexpr std::size_t DriverMemoryBytes = 2u << 20u; alignas(256) constexpr std::array Caller{ 0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u, @@ -83,6 +86,9 @@ void CheckTable() { Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(table[2]), sizeof(routine)), "the FS routine is not readable"); std::memcpy(routine.data(), reinterpret_cast(table[2]), sizeof(routine)); Require(routine[0] == 0xbe80200eu, "the FS routine does not start with s_setpc_b64 s[14:15]"); +#ifndef _WIN32 + if (GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) Require(AgcDriver::GuestMemory::Watched(DriverMemory, DriverMemoryBytes), "the AGC driver memory at 0xfe0000000 is not write-watched, so a refused import copies it on every address-based build"); +#endif } } From 9664503056628359eb98f2e8f41b6b959bc22587 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 01:39:53 +0300 Subject: [PATCH 0047/1360] fix(libSceVideodec2): return access unit errors for a null or empty access unit sceVideodec2Decode threw for these, which ended the title. Media Player Classic PS5 submits an empty unit when its own converter misreads a length prefix as a start code, and it counts the error and keeps playing on a console. --- core/libs/prx/libSceVideodec2/Export.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 1434ad7255..c65eaab3ac 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -30,6 +30,8 @@ constexpr std::uint32_t HeightAlignment = 32; constexpr std::uint32_t CodecAvc = 1; constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; +constexpr int Videodec2ErrorAccessUnitSize = static_cast(0x811D010Du); +constexpr int Videodec2ErrorAccessUnitPointer = static_cast(0x811D010Eu); struct ComputeMemoryInfo { std::uint64_t thisSize; @@ -472,12 +474,16 @@ bool WriteNv12(Decoder& decoder, const AVFrame& frame, std::uint8_t* target, std return sws_scale(decoder.scaler, frame.data, frame.linesize, 0, frame.height, planes, strides) == frame.height; } -void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { +void ClearOutput(const Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { output->isValid = false; output->isErrorFrame = false; output->pictureCount = 0; output->codecType = decoder.codec; if (frame) frame->isAccepted = false; +} + +void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { + ClearOutput(decoder, frame, output); if (decoder.ready.empty()) return; if (!frame || !frame->frameBuffer) throw std::runtime_error("Videodec2: missing output buffer"); const auto picture = std::move(decoder.ready.front()); @@ -577,6 +583,10 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); + if (!input->auData || input->auSize == 0) { + ClearOutput(decoder, frame, output); + return input->auData ? Videodec2ErrorAccessUnitSize : Videodec2ErrorAccessUnitPointer; + } if (Trace() && decoder.decoded < 3) std::fprintf(stderr, "[videodec2] Decode input=%llu frame=%llu output=%llu au=%llu pts=%llu size=%ux%u fb=%p/%llu\n", static_cast(input->thisSize), static_cast(frame ? frame->thisSize : 0), static_cast(output->thisSize), static_cast(input->auSize), static_cast(input->ptsData), decoder.width, decoder.height, frame ? frame->frameBuffer : nullptr, static_cast(frame ? frame->frameBufferSize : 0)); ++decoder.decoded; const Picture picture{input->ptsData, input->dtsData, input->attachedData}; @@ -584,7 +594,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa avcodec_flush_buffers(decoder.context); decoder.flushing = false; } - if (!input->auData || input->auSize == 0 || input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); + if (input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); if (decoder.codec == CodecVp9) { auto references = decoder.vp9References; auto colors = decoder.vp9Colors; From 4c8bdfc8bf417f4ecb4acbf81b133085ee880134 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 01:39:53 +0300 Subject: [PATCH 0048/1360] test(libSceVideodec2): refuse a null and an empty access unit mid-stream --- core/libs/tests/GuestVideodec2.cpp | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 4212ef5138..3bea599190 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -666,6 +666,51 @@ std::uint64_t frameBufferBytes() { return memory.maxFrameBufferSize; } +void testEmptyAccessUnit() { + constexpr int AccessUnitSizeError = static_cast(0x811D010Du); + constexpr int AccessUnitPointerError = static_cast(0x811D010Eu); + std::vector buffer(frameBufferBytes()); + const auto handle = createDecoder(CodecHevc, 1); + const auto units = hevcUnits(HevcStream.data(), HevcStream.size()); + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { + check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); + if (!output.isValid) return false; + check(pictures < HevcHashes.size(), "more pictures than access units"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference after a refused access unit"); + ++pictures; + return true; + }; + const auto refuse = [&](const std::uint8_t* data, std::uint64_t size, int expected, const char* message) { + const InputData input{sizeof(InputData), data, size, 2000, 0, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), true}; + OutputInfo output{sizeof(OutputInfo)}; + output.isValid = true; + output.pictureCount = 1; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == expected, message); + check(!output.isValid && output.pictureCount == 0 && !frame.isAccepted, "a refused access unit handed out a picture"); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (unit == 2) { + refuse(units[unit].data(), 0, AccessUnitSizeError, "an empty access unit did not return ACCESS_UNIT_SIZE"); + refuse(nullptr, units[unit].size(), AccessUnitPointerError, "a null access unit did not return ACCESS_UNIT_POINTER"); + } + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode failed"); + take(output, frame); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output, frame)) break; + } + check(pictures == HevcHashes.size(), "refused access units cost pictures"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + void testDecodeAfterFlush() { std::vector buffer(frameBufferBytes()); const auto handle = createDecoder(1, 100); @@ -831,6 +876,7 @@ int main() { testVp9PictureMetadata(); testTenBit(); testDecodeAfterFlush(); + testEmptyAccessUnit(); testPictureOwnership(); testConcurrentOwnership(); std::puts("Videodec2 tests passed"); From f4de10a73f533c5bad022a891b318fd6d8934ca7 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 01:39:53 +0300 Subject: [PATCH 0049/1360] docs(debt): note where the empty access unit codes come from --- docs/dev/debt/libSceVideodec2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index cf38f7d5d0..4f63b6af8a 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -13,3 +13,4 @@ - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. - [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping +- [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. From 2eadacd1e28ff72dfa4cfb4a11460b5b1d43e9f6 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:37:35 +0200 Subject: [PATCH 0050/1360] fix(agc): recheck vertex table reads when a queued store finishes mid-decode DecodeOrderedVertexStageInfo decoded first and then asked queue 0 whether a pending write overlapped the reads. A queued attribute or V# store that completed while the decode ran was gone from the queue by then, so the decode kept the bytes from before it (found by embersas with a read-guard reproducer). The helper now notes whether queue 0 was busy before the decode. If it was, it still drains when a read overlaps a pending write, and otherwise rereads every recorded attribute word and V#: the decode is kept only when all bytes are unchanged, else it drains and decodes again. agc_driver_vertex_table_read_order gains that case: the read guard releases a queued attribute store at the V# read and waits for its item to retire. AI-assisted: yes --- .../Execution/src/Driver/Draw/DrawDecode.cpp | 16 +++++++++--- .../tests/execution/VertexTableReadOrder.cpp | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) 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 4eb638a1a0..fc48f5263a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include namespace AgcDriver::DriverDetail { @@ -36,10 +37,19 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span now{}; + GuestMemory::Read(read.address, std::span(now.data(), read.size), 4); + return std::equal(read.data.begin(), read.data.begin() + read.size, now.begin()); + }; + if (std::none_of(reads.begin(), reads.end(), pending) && std::all_of(reads.begin(), reads.end(), unchanged)) return info; + queue.Drain(DrawPipeline::DrainReason::Capture); return decode(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp index 2adbebd10b..0ab1ae0360 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "SceShaders.hpp" #include @@ -15,6 +16,7 @@ #include #include #include +#include #include namespace { @@ -89,6 +91,14 @@ void WaitEmpty() { } } +Gate* completeAtBufferRead = nullptr; + +void CompleteQueuedStoreAtBufferRead(std::uint64_t address, std::size_t) { + if (address != Address(BufferTable.data()) || completeAtBufferRead == nullptr) return; + std::exchange(completeAtBufferRead, nullptr)->release.store(true, std::memory_order_release); + WaitEmpty(); +} + ShaderRecompiler::ShaderVertexStageInfo DecodeBehindQueuedStore(const void* range, std::size_t bytes, std::function store, std::vector& reads) { WaitEmpty(); Gate gate; @@ -140,6 +150,21 @@ int main() { gate.release.store(true, std::memory_order_release); WaitEmpty(); + Gate completing; + Hold(completing, AttribTable.data(), 4, [] { AttribTable[0] = (0x2cu << 5u) | 1u; }); + completeAtBufferRead = &completing; + AgcDriver::GuestMemory::SetReadGuard(&CompleteQueuedStoreAtBufferRead); + try { + info = Decode(reads); + } catch (...) { + AgcDriver::GuestMemory::SetReadGuard(nullptr); + throw; + } + AgcDriver::GuestMemory::SetReadGuard(nullptr); + check(completeAtBufferRead == nullptr, "the queued attribute store did not complete during the decode"); + check(info.resources[0].fields[0] == 0x9000u && info.resources[0].fields[2] == 128u, "the decode kept the vertex buffer of an attribute word that a queued store replaced during the decode"); + check(reads.size() == 2 && reads[1].address == Address(BufferTable.data() + 4), "the recorded reads are not the stored attribute word and the V# it names"); + DrawPipeline::Active() = false; DrawPipeline::Queue0().Stop(); std::puts("vertex table read order tests passed"); From 87ee5e245585e6179877b6c949e0a6033c5d7c3b Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Tue, 20 Oct 2026 01:37:13 +0200 Subject: [PATCH 0051/1360] fix(agc): drain a label while a copied write-back is pending A label recorded while a copied writer is listed went to the GPU and landed before the writer's CPU write-back ran at the batch's reap, so a title that rewrote the written region after polling the label had its bytes overwritten by the late write-back. The label now takes the drain path, which runs the write-back first and stores the label on the CPU. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit ec3dc6964172720e8364f6153ba34086cd581740) --- .../Execution/src/VulkanDevice.cpp | 1 + .../execution/CopiedWriterLabelOrder.cpp | 167 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 169 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index aedf2f6afb..76a12cd929 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2269,6 +2269,7 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spancopiedWriters->empty() || !Graphics::DrawCopiedWriters()->empty()) return 2; if (gateAll ? recorder.HasCompletions() : state->CopiedWriterOverlaps(address, bytes.size())) { // A copied buffer's write-back is still to run: the store must land after it. It is stored // by a completion action of the batch (a CPU memcpy when the batch is reaped) instead of diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp new file mode 100644 index 0000000000..3f7a722c88 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.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 +#include + +namespace { + +using AgcDriver::Graphics::Recorder; +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 32; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t RecordBytes = 16; +constexpr std::size_t StoredOffset = 8; +constexpr std::size_t CopiedRecordOffset = 0x1004; +constexpr std::size_t DirectRecordOffset = 0x1000; +constexpr std::uint32_t CpuValue = 0xc0001000u; +constexpr std::uint32_t GpuValue = 1; +constexpr std::uint32_t LabelValue = 1; + +alignas(256) constexpr std::array StoreCode{ + 0x7e000281u, 0x7e020288u, 0xe0701000u, 0x80000001u, 0xbf810000u, 0xbf810000u, +}; + +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, "copied writer label order: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + volatile std::uint32_t* Word(std::size_t offset) const { return reinterpret_cast(block + offset); } + std::uint64_t Address() const { return static_cast(reinterpret_cast(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; +} + +void RecordDispatch(AgcDriver::VulkanDevice& device, std::uint64_t record) { + std::vector userData(16, 0u); + const std::array descriptor{static_cast(record), static_cast((record >> 32u) & 0xffffu), RecordBytes, 0x31016facu}; + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +bool LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std::size_t recordOffset, const GuestBlock& labels, std::size_t labelOffset, std::uint64_t stamp, std::uint64_t writeBacks, int expectedReason, const std::string& what) { + volatile std::uint32_t* stored = records.Word(recordOffset + StoredOffset); + volatile std::uint32_t* label = labels.Word(labelOffset); + *stored = CpuValue; + *label = 0; + RecordDispatch(device, records.Address() + recordOffset); + const auto pending = Recorder::PendingWriteBackCompletions(); + Require(pending == writeBacks, what + ": " + std::to_string(pending) + " write-back completions are pending after the dispatch, expected " + std::to_string(writeBacks) + " (the test needs APS5_CPU_COPIES=1 and a storage buffer offset alignment above 4)"); + const std::array bytes{std::byte{LabelValue}, std::byte{0}, std::byte{0}, std::byte{0}}; + int reason = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + reason = device.WriteLabelOnGpu(labels.Address() + labelOffset, bytes, stamp, 0, false); + if (reason == 3) { + device.WaitIdle(); + return false; + } + Require(reason == expectedReason, what + ": the label after the dispatch was handled with reason " + std::to_string(reason) + ", expected " + std::to_string(expectedReason)); + if (reason >= 1 && reason <= 4) { + if (reason != 1) device.WaitIdle(); + AgcDriver::GuestMemory::Write(labels.Address() + labelOffset, bytes, 4); + } else { + device.SubmitRecorded(true); + } + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (*label != LabelValue) { + Require(std::chrono::steady_clock::now() < deadline, what + ": the label never landed"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + const std::uint32_t seen = *stored; + Require(seen == GpuValue, what + ": the label is visible but the record holds " + Hex(seen) + ", the dispatch stored " + Hex(GpuValue)); + *stored = CpuValue; + device.WaitIdle(); + const std::uint32_t kept = *stored; + Require(kept == CpuValue, what + ": the record holds " + Hex(kept) + " after the drain, a write-back landed over the value the CPU wrote after the label (" + Hex(CpuValue) + ")"); + Require(Recorder::PendingWriteBackCompletions() == 0, what + ": write-back completions outlived the drain"); + return true; +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_CPU_COPIES", "1"); +#else + setenv("APS5_CPU_COPIES", "1", 1); +#endif + const GuestBlock copied; + const GuestBlock direct; + const GuestBlock labels; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!LabelPass(*device, copied, CopiedRecordOffset, labels, 0x2000, 1, 1, 2, "copied writer") || !LabelPass(*device, direct, DirectRecordOffset, labels, 0x3000, 2, 0, 0, "direct writer")) { + std::puts("skipped, the device imports no host memory"); + return VulkanTestSkipped; + } + std::puts("copied writer label order tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 259f6530b9..79655753f4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -113,6 +113,7 @@ - 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. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. +- A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) recorded while a copied writer is listed (a dispatch or draw whose written buffer the device holds as a CPU copy, written back to guest memory when its batch is reaped) waits for all recorded work, which runs the write-back, and is then written by the CPU: on the console every earlier store is in memory before a later label of the same queue is visible, and a title that rewrites the written region after polling the label would otherwise have the bytes overwritten by the late write-back (Gang Beasts, CUSA04865 through #6977, rebuilt its command buffer over such a region). The stall is paid once per label while such a write-back is pending; written buffers the device imports in place bind directly and pay nothing. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. From 54dca09c6b6c597b3e78890a6c3e76a10bbedbb2 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:34:05 +0200 Subject: [PATCH 0052/1360] test(agc): keep vertex table read order gates alive until their store runs The fixture's gates lived on the stack and were captured by reference in the queued stores, and a gate released on destruction without waiting for its store to run. A check failing while a store was still queued could leave the worker reading a gate that was gone, and the error path left the queue running (noted by embersas). Hold now returns a shared gate that the queued store owns a copy of. On both the success and the error path the test clears the read guard, releases every gate it created and stops queue 0, so a failing check no longer leaves a held store behind. The cases and their assertions are unchanged. AI-assisted: yes --- .../tests/execution/VertexTableReadOrder.cpp | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp index 0ab1ae0360..5d66117892 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -73,14 +74,24 @@ ShaderRecompiler::ShaderVertexStageInfo Decode(std::vector release{false}; - ~Gate() { release.store(true, std::memory_order_release); } }; -void Hold(Gate& gate, const void* range, std::size_t bytes, std::function store) { - DrawPipeline::Queue0().Enqueue([&gate, store = std::move(store)] { - while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); +std::vector> Gates; + +std::shared_ptr Hold(const void* range, std::size_t bytes, std::function store) { + auto gate = Gates.emplace_back(std::make_shared()); + DrawPipeline::Queue0().Enqueue([gate, store = std::move(store)] { + while (!gate->release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); store(); }, {{Address(range), Address(range) + bytes}}); + return gate; +} + +void StopQueue() { + AgcDriver::GuestMemory::SetReadGuard(nullptr); + for (const auto& gate : Gates) gate->release.store(true, std::memory_order_release); + DrawPipeline::Active() = false; + DrawPipeline::Queue0().Stop(); } void WaitEmpty() { @@ -91,7 +102,7 @@ void WaitEmpty() { } } -Gate* completeAtBufferRead = nullptr; +std::shared_ptr completeAtBufferRead; void CompleteQueuedStoreAtBufferRead(std::uint64_t address, std::size_t) { if (address != Address(BufferTable.data()) || completeAtBufferRead == nullptr) return; @@ -101,11 +112,10 @@ void CompleteQueuedStoreAtBufferRead(std::uint64_t address, std::size_t) { ShaderRecompiler::ShaderVertexStageInfo DecodeBehindQueuedStore(const void* range, std::size_t bytes, std::function store, std::vector& reads) { WaitEmpty(); - Gate gate; - Hold(gate, range, bytes, std::move(store)); - std::thread releaser([&gate] { + auto gate = Hold(range, bytes, std::move(store)); + std::thread releaser([gate] { std::this_thread::sleep_for(std::chrono::milliseconds(50)); - gate.release.store(true, std::memory_order_release); + gate->release.store(true, std::memory_order_release); }); ShaderRecompiler::ShaderVertexStageInfo info; try { @@ -142,35 +152,27 @@ int main() { check(reads.size() == 2 && reads[0].address == Address(AttribTable.data()) && reads[1].address == Address(BufferTable.data()), "the decode did not read the stored attribute word and the V# it names"); WaitEmpty(); - Gate gate; - Hold(gate, Elsewhere.data(), sizeof(Elsewhere), [] {}); + auto gate = Hold(Elsewhere.data(), sizeof(Elsewhere), [] {}); info = Decode(reads); check(info.resources[0].fields[0] == 0x1000u, "the decode next to an unrelated queued store changed"); check(DrawPipeline::QueuedItems() == 1, "a decode that overlaps nothing queued drained the pipeline"); - gate.release.store(true, std::memory_order_release); + gate->release.store(true, std::memory_order_release); WaitEmpty(); - Gate completing; - Hold(completing, AttribTable.data(), 4, [] { AttribTable[0] = (0x2cu << 5u) | 1u; }); - completeAtBufferRead = &completing; + completeAtBufferRead = Hold(AttribTable.data(), 4, [] { AttribTable[0] = (0x2cu << 5u) | 1u; }); AgcDriver::GuestMemory::SetReadGuard(&CompleteQueuedStoreAtBufferRead); - try { - info = Decode(reads); - } catch (...) { - AgcDriver::GuestMemory::SetReadGuard(nullptr); - throw; - } + info = Decode(reads); AgcDriver::GuestMemory::SetReadGuard(nullptr); check(completeAtBufferRead == nullptr, "the queued attribute store did not complete during the decode"); check(info.resources[0].fields[0] == 0x9000u && info.resources[0].fields[2] == 128u, "the decode kept the vertex buffer of an attribute word that a queued store replaced during the decode"); check(reads.size() == 2 && reads[1].address == Address(BufferTable.data() + 4), "the recorded reads are not the stored attribute word and the V# it names"); - DrawPipeline::Active() = false; - DrawPipeline::Queue0().Stop(); + StopQueue(); std::puts("vertex table read order tests passed"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; + try { StopQueue(); } catch (...) {} return 1; } } From b4762a6583427dcc2246b0062576f0e0df914a66 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Tue, 20 Oct 2026 09:18:31 +0630 Subject: [PATCH 0053/1360] feat(net): parse Ethernet address strings --- core/libs/prx/libSceNet/Export.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fdc52dffe..57ab50c0d5 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1612,6 +1612,35 @@ int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { return 0; } +int APS5_VABI sceNetEtherStrton(const char* text, NetEtherAddr* address) { + if (text == nullptr || address == nullptr) return fail(NET_EFAULT); + const auto hexDigit = [](char character) { + if (character >= '0' && character <= '9') return character - '0'; + if (character >= 'a' && character <= 'f') return character - 'a' + 10; + if (character >= 'A' && character <= 'F') return character - 'A' + 10; + return -1; + }; + NetEtherAddr parsed{}; + for (std::size_t index = 0; index < sizeof(parsed.data); ++index) { + int value = hexDigit(*text); + if (value < 0) return fail(NET_EINVAL); + ++text; + if (const int digit = hexDigit(*text); digit >= 0) { + value = value * 16 + digit; + ++text; + } + if (index + 1 == sizeof(parsed.data)) { + if (*text != '\0') return fail(NET_EINVAL); + } else { + if (*text != ':' && *text != '-') return fail(NET_EINVAL); + ++text; + } + parsed.data[index] = static_cast(value); + } + *address = parsed; + return 0; +} + int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { (void)flags; if (addr == nullptr) { From 44d78d0478837f5d64e78e354a48e0fc3d91655b Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Tue, 20 Oct 2026 09:18:55 +0630 Subject: [PATCH 0054/1360] test(net): cover Ethernet address parsing --- core/libs/tests/GuestSceNet.cpp | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e8ade712ba..c5021947ce 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -50,6 +50,8 @@ int APS5_VABI sceNetShutdown(int, int); int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); int* APS5_VABI sceNetErrnoLoc(void); +int APS5_VABI sceNetEtherNtostr(const NetEtherAddr*, char*, std::size_t); +int APS5_VABI sceNetEtherStrton(const char*, NetEtherAddr*); int APS5_VABI sceNetEpollCreate(const char*, int); int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); @@ -762,6 +764,59 @@ static void CheckInetPtonEx() { } } +static void CheckEtherAddress() { + struct GuardedAddress { + std::uint8_t before; + NetEtherAddr address; + std::uint8_t after; + }; + static_assert(sizeof(GuardedAddress) == 8); + const GuardedAddress original{0x12, {{0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}}, 0x34}; + for (const char* text : {"0:1:2:a:B:f", "00:01:02:0A:0b:0F", "0-1-2-a-B-f", "0:1-2:a-B:f"}) { + auto output = original; + const std::array expected{0, 1, 2, 10, 11, 15}; + *sceNetErrnoLoc() = 33; + Require(sceNetEtherStrton(text, &output.address) == 0); + Require(std::memcmp(output.address.data, expected.data(), expected.size()) == 0); + Require(output.before == original.before && output.after == original.after && *sceNetErrnoLoc() == 33); + } + for (unsigned value = 0; value < 256; ++value) { + NetEtherAddr input{}; + for (unsigned index = 0; index < 6; ++index) input.data[index] = static_cast(value + index * 43); + char text[18]{}; + Require(sceNetEtherNtostr(&input, text, sizeof(text)) == 0); + auto output = original; + Require(sceNetEtherStrton(text, &output.address) == 0); + Require(std::memcmp(input.data, output.address.data, sizeof(input.data)) == 0); + Require(output.before == original.before && output.after == original.after); + } + for (const char* text : {"", "0", "0:1:2:3:4", "0:1:2:3:4:", "0:1:2:3:4:5:6", "000:1:2:3:4:5", + "0:1:2:3:4:555", ":0:1:2:3:4:5", "0::1:2:3:4:5", "0.1.2.3.4.5", "0x0:1:2:3:4:5", + "0:1:2:3:4:g", "+0:1:2:3:4:5", " 0:1:2:3:4:5", "0:1:2:3:4:5 ", "0:1:2:3:4:5\n"}) { + auto output = original; + Require(Failed(sceNetEtherStrton(text, &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + const std::string valid = "00:11:22:33:44:55"; + for (std::size_t length = 0; length + 1 < valid.size(); ++length) { + auto output = original; + Require(Failed(sceNetEtherStrton(valid.substr(0, length).c_str(), &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + for (unsigned value = 128; value < 256; ++value) { + auto text = valid; + text[0] = static_cast(value); + auto output = original; + Require(Failed(sceNetEtherStrton(text.c_str(), &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + auto output = original; + Require(Failed(sceNetEtherStrton(nullptr, &output.address), 14)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + Require(Failed(sceNetEtherStrton(valid.c_str(), nullptr), 14)); + Require(Failed(sceNetEtherStrton(nullptr, nullptr), 14)); +} + static void CheckSystemTime() { *sceNetErrnoLoc() = 22; sceNetGetSystemTime(nullptr); @@ -1216,6 +1271,7 @@ static void CheckDatagramDisconnect() { } int main() { + CheckEtherAddress(); const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); Require(KernelSocketSelect::DeadlineAfter(epoch, INT64_MAX, 0) == std::chrono::steady_clock::time_point::max()); From d9fc0c483df5e3463a08ec6e9c3e451d4f2e4fb7 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Tue, 20 Oct 2026 09:19:52 +0630 Subject: [PATCH 0055/1360] docs(net): record Ethernet parser validation gap --- docs/dev/debt/libSceNet.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 06832be45a..b77cca9f08 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,6 +6,8 @@ ### Unknown function info +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the signature, six one- or two-digit hexadecimal octets, colon or hyphen separators, error codes and unchanged output on failure follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1206-L1228). Only the export name and NID are confirmed by the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNet.c) + - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 5450c93e936c4a2e17a922b76c01ee75aaa3546e Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 14:28:09 +0700 Subject: [PATCH 0056/1360] fix(agc): sample 8-bit SNORM textures through FORCE_DEGAMMA as their sRGB format Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp | 1 + .../tests/execution/ImageSampleForceDegamma.cpp | 7 ++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..9696cc95ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1291,7 +1291,10 @@ VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { switch (format) { case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; - case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_R8_SNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_SNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SNORM: return VK_FORMAT_R8G8B8A8_SRGB; case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 31ff43033e..b496d90902 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -63,6 +63,7 @@ void srgbTwinTests() { Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin"); Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins"); Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_SNORM) == VK_FORMAT_R8G8B8A8_SRGB, "8_8_8_8 SNORM must read its bytes through the sRGB twin"); } template diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index 331882d14a..0178717e94 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -27,6 +27,7 @@ constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Format8888SNorm = 57; constexpr std::uint32_t FormatBc4UNorm = 175; +constexpr std::uint32_t Format32UInt = 20; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -177,7 +178,11 @@ int main() { RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); RequireSame(Sample(*device, Format32Float, FilterPoint, ForceDegamma), Sample(*device, Format32Float, FilterPoint, 0u), "32_FLOAT, point"); RequireSame(Sample(*device, FormatBc4UNorm, FilterPoint, ForceDegamma), Sample(*device, FormatBc4UNorm, FilterPoint, 0u), "BC4_UNORM, point"); - Reject(*device, Format8888SNorm, "guest format 57, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); + FillInput(true); + RequireSame(Sample(*device, Format8888SNorm, FilterPoint, ForceDegamma), forcedPoint, "8_8_8_8_SNORM, point", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + FillInput(false); + RequireSame(Sample(*device, Format8888SNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_SNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + Reject(*device, Format32UInt, "guest format 20, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0c81e0f71c..43c2973971 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -169,7 +169,7 @@ - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. -- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for an SNORM `8`, `8_8` or `8_8_8_8` texture, which RDNA2 (gfx1031) reads bit for bit as the sRGB format of the same bytes, and for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering); `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. +- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. From 57e3e7c645c5e265760a5ebce5d39a366b4c69b8 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 10:29:01 +0300 Subject: [PATCH 0057/1360] fix(libs): build getifaddrs and two tests on macOS hosts Three recent changes use what only Linux has, so libc and two tests no longer compile on macOS: - The AF_LINK entries that getifaddrs lists on Linux use linux/if_link.h, netpacket/packet.h, sockaddr_ll and rtnl_link_stats, and were built on every host but Windows. They are now built on Linux only: macOS lists the IPv4 and IPv6 addresses as before, as Windows does, and the interface test checks the AF_LINK entries on Linux only. - The NetCtlAp test loops over a braced list without including , which libc++ does not pull in through the test's other headers. - The import block test backs its 16 KiB views with memfd_create. The views case is now built on Linux only; macOS runs the case without views, as Windows does. --- .../prx/libSceAgcDriver/tests/BdaResources.cpp | 4 ++-- core/libs/prx/libc/src/Interfaces.cpp | 12 ++++++++++-- core/libs/tests/GuestInterfaces.cpp | 14 +++++++++----- core/libs/tests/GuestNetCtlAp.cpp | 1 + docs/dev/debt/libc.md | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d625cf165..1a2e40f45a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1103,7 +1103,7 @@ public: static constexpr std::size_t Window = std::size_t{8} << 20u; explicit PieceWindow(const Context& context, bool views = false) : importing(context) { -#ifndef _WIN32 +#ifdef __linux__ if (views) { reserved = mmap(nullptr, 2 * Window, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); Require(reserved != MAP_FAILED, "cannot reserve the block of 16 KiB views"); @@ -3369,7 +3369,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importRequesterTests(context, access); hostImportReportTests(); importBlockMapTests(context, false); -#ifndef _WIN32 +#ifdef __linux__ importBlockMapTests(context, true); #endif importBlockBuildTests(context); diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 8e39cfa29b..a5be240839 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -5,11 +5,13 @@ #include #else #include -#include #include -#include #include +#ifdef __linux__ +#include +#include #include +#endif #include #include #include @@ -160,6 +162,7 @@ int HostEntries(std::vector& entries) { return 0; } #else +#ifdef __linux__ constexpr std::uint8_t guestLinkFamily = 18; constexpr std::size_t guestLinkNameArea = 8 + 16; constexpr std::size_t guestLinkMinimum = 54; @@ -171,6 +174,7 @@ constexpr std::uint8_t guestLinkStateDown = 1; constexpr std::uint8_t guestLinkStateUp = 2; constexpr std::size_t guestInterfaceDataSize = 152; constexpr unsigned int hostLowerUp = 0x10000; +#endif struct HostInterface { std::string name; @@ -179,6 +183,7 @@ struct HostInterface { std::vector addresses; }; +#ifdef __linux__ template void Store(std::vector& bytes, std::size_t offset, TValue value) { std::memcpy(bytes.data() + offset, &value, sizeof(value)); @@ -253,6 +258,7 @@ std::vector GuestInterfaceData(const std::string& name, unsigned i } return data; } +#endif std::uint32_t InterfaceFlags(unsigned int host) { constexpr std::pair flags[] = { @@ -288,6 +294,7 @@ int HostEntries(std::vector& entries) { try { for (auto* item = host; item; item = item->ifa_next) { if (!item->ifa_addr) continue; +#ifdef __linux__ if (item->ifa_addr->sa_family == AF_PACKET) { const auto& link = *reinterpret_cast(item->ifa_addr); HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); @@ -302,6 +309,7 @@ int HostEntries(std::vector& entries) { owner.linked = true; continue; } +#endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 5b69f12016..c737ea2e11 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -7,9 +7,11 @@ #include #include #include -#include #include +#ifdef __linux__ +#include #include +#endif #include #include #include @@ -76,7 +78,7 @@ static HostInterfaceAddress HostAddress(const sockaddr* address) { return entry; } -#ifndef _WIN32 +#ifdef __linux__ static std::uint64_t SysfsCounter(const char* name, const char* file) { std::ifstream input(std::string("/sys/class/net/") + name + "/statistics/" + file); unsigned long long value = 0; @@ -151,6 +153,7 @@ static std::vector HostSnapshot() { try { for (auto* item = host; item; item = item->ifa_next) { if (!item->ifa_addr) continue; +#ifdef __linux__ if (item->ifa_addr->sa_family == AF_PACKET) { const auto& link = *reinterpret_cast(item->ifa_addr); HostInterfaceAddress entry{}; @@ -165,6 +168,7 @@ static std::vector HostSnapshot() { members.insert(members.begin(), entry); continue; } +#endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; Require(item->ifa_netmask != nullptr); HostInterfaceAddress entry = HostAddress(item->ifa_addr); @@ -192,7 +196,7 @@ int main() { const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; bool matched = false; -#ifndef _WIN32 +#ifdef __linux__ bool linkChecked = false; SendLoopbackDatagram(); #endif @@ -211,7 +215,7 @@ int main() { Require((item->flags & ~knownFlags) == 0); const auto* address = static_cast(item->address); Require(address != nullptr && reinterpret_cast(address) % 8 == 0); -#ifndef _WIN32 +#ifdef __linux__ if (address[1] == 18) { Require(item->netmask == nullptr && item->destination == nullptr); const auto* data = static_cast(item->data); @@ -295,7 +299,7 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); -#ifndef _WIN32 +#ifdef __linux__ Require(linkChecked); #endif GuestInterfaceAddress* released = nullptr; diff --git a/core/libs/tests/GuestNetCtlAp.cpp b/core/libs/tests/GuestNetCtlAp.cpp index fbb8b0f01d..b434dbf9c9 100644 --- a/core/libs/tests/GuestNetCtlAp.cpp +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include extern "C" { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e06d2e9b22..696b16ddbe 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -96,7 +96,7 @@ - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows and macOS omit per-interface AF_LINK entries and interfaces without an address; on Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured From 346cafdb84a08d2f4d54288dc252be0b082a1a07 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 01:07:19 +0300 Subject: [PATCH 0058/1360] feat(libSceCompanionHttpd): keep the server state with no companion device Initialize, register a request callback, start, stop and terminate keep the library's state with its errors; no companion device connects, so sceCompanionHttpdGetEvent always returns NO_EVENT and the callback is never called. The response builders are declared and throw. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceCompanionHttpd/CMakeLists.txt | 21 +++ core/libs/prx/libSceCompanionHttpd/Export.cpp | 169 +++++++++++++----- core/libs/tests/GuestCompanionHttpd.cpp | 129 +++++++++++++ docs/dev/debt/libSceCompanionHttpd.md | 8 +- 4 files changed, 277 insertions(+), 50 deletions(-) create mode 100644 core/libs/prx/libSceCompanionHttpd/CMakeLists.txt create mode 100644 core/libs/tests/GuestCompanionHttpd.cpp diff --git a/core/libs/prx/libSceCompanionHttpd/CMakeLists.txt b/core/libs/prx/libSceCompanionHttpd/CMakeLists.txt new file mode 100644 index 0000000000..cdc8922b8a --- /dev/null +++ b/core/libs/prx/libSceCompanionHttpd/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceCompanionHttpd SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceCompanionHttpd PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCompanionHttpd) + +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_companion_httpd_tests ${LIBS_INCLUDE_DIR}/tests/GuestCompanionHttpd.cpp) + target_include_directories(guest_companion_httpd_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_companion_httpd_tests PRIVATE libSceCompanionHttpd libc) + configure_windows_unwind(guest_companion_httpd_tests) + add_test(NAME guest_companion_httpd COMMAND guest_companion_httpd_tests) + if(WIN32) + set_tests_properties(guest_companion_httpd PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCompanionHttpd/Export.cpp b/core/libs/prx/libSceCompanionHttpd/Export.cpp index 48240a3fe1..8d3a814f50 100644 --- a/core/libs/prx/libSceCompanionHttpd/Export.cpp +++ b/core/libs/prx/libSceCompanionHttpd/Export.cpp @@ -1,91 +1,162 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int CompanionHttpdErrorInvalidParam = static_cast(0x80E40004u); +constexpr int CompanionHttpdErrorNotInitialized = static_cast(0x80E40006u); +constexpr int CompanionHttpdErrorAlreadyInitialized = static_cast(0x80E40007u); +constexpr int CompanionHttpdErrorNoEvent = static_cast(0x80E40008u); +constexpr int CompanionHttpdErrorAlreadyStarted = static_cast(0x80E4000Au); +constexpr int CompanionHttpdErrorNotStarted = static_cast(0x80E4000Bu); +constexpr int CompanionHttpdErrorAlreadyRegistered = static_cast(0x80E4000Cu); + +struct CompanionHttpdOptParam { + std::uint64_t reserved; + void* workMemory; + std::uint64_t workMemorySize; + std::uint8_t settings[0x40]; + const void* temporaryMountPoint; +}; +static_assert(sizeof(CompanionHttpdOptParam) == 0x60); + +using CompanionHttpdRequestCallback = int (*)(int, int, const void*, void*, void*); + +struct CompanionHttpdState { + std::mutex mutex; + bool initialized = false; + bool started = false; + CompanionHttpdRequestCallback callback = nullptr; + void* callbackArgument = nullptr; +}; + +CompanionHttpdState& State() { + static CompanionHttpdState state; + return state; +} + +} + extern "C" { -int APS5_VABI sceCompanionHttpdAddHeader() { +int APS5_VABI sceCompanionHttpdOptParamInitialize(CompanionHttpdOptParam* param) { + if (param == nullptr) return CompanionHttpdErrorInvalidParam; + std::memset(param, 0, sizeof(*param)); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize(const CompanionHttpdOptParam* param) { + if (param == nullptr) return CompanionHttpdErrorInvalidParam; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.initialized) return CompanionHttpdErrorAlreadyInitialized; + state.initialized = true; + return 0; +} + +int APS5_VABI sceCompanionHttpdTerminate(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.started) NotImplemented_nid_no_patch(__func__); + state.initialized = false; + state.callback = nullptr; + state.callbackArgument = nullptr; + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback(CompanionHttpdRequestCallback function, void* argument) { + if (function == nullptr) return CompanionHttpdErrorInvalidParam; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.callback != nullptr) return CompanionHttpdErrorAlreadyRegistered; + state.callback = function; + state.callbackArgument = argument; + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.callback == nullptr) NotImplemented_nid_no_patch(__func__); + state.callback = nullptr; + state.callbackArgument = nullptr; + return 0; +} + +int APS5_VABI sceCompanionHttpdStart(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.started) return CompanionHttpdErrorAlreadyStarted; + state.started = true; + return 0; +} + +int APS5_VABI sceCompanionHttpdStop(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (!state.started) return CompanionHttpdErrorNotStarted; + state.started = false; + return 0; +} + +int APS5_VABI sceCompanionHttpdGetEvent(void* event) { + if (event == nullptr) return CompanionHttpdErrorInvalidParam; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + return CompanionHttpdErrorNoEvent; +} + +int APS5_VABI sceCompanionHttpdAddHeader(const char* key, const char* value, void* response) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdGet2ndScreenStatus() { +int APS5_VABI sceCompanionHttpdSetStatus(int status, void* response) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdGetEvent() { +int APS5_VABI sceCompanionHttpdSetBody(const char* body, std::uint64_t size, void* response) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdGetUserId() { +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdInitialize() { +int APS5_VABI sceCompanionHttpdGetUserId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdInitialize2() { +int APS5_VABI sceCompanionHttpdInitialize2(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdOptParamInitialize() { +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback() { +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdRegisterRequestCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdRegisterRequestCallback2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdSetBody() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdSetStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdStop() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdUnregisterRequestCallback() { +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestCompanionHttpd.cpp b/core/libs/tests/GuestCompanionHttpd.cpp new file mode 100644 index 0000000000..e9f8990365 --- /dev/null +++ b/core/libs/tests/GuestCompanionHttpd.cpp @@ -0,0 +1,129 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCompanionHttpdOptParamInitialize(void* param); +int APS5_VABI sceCompanionHttpdInitialize(const void* param); +int APS5_VABI sceCompanionHttpdTerminate(void); +int APS5_VABI sceCompanionHttpdRegisterRequestCallback(void* function, void* argument); +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback(void); +int APS5_VABI sceCompanionHttpdStart(void); +int APS5_VABI sceCompanionHttpdStop(void); +int APS5_VABI sceCompanionHttpdGetEvent(void* event); +int APS5_VABI sceCompanionHttpdAddHeader(const char* key, const char* value, void* response); +int APS5_VABI sceCompanionHttpdSetStatus(int status, void* response); +int APS5_VABI sceCompanionHttpdSetBody(const char* body, std::uint64_t size, void* response); +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus(void); +int APS5_VABI sceCompanionHttpdGetUserId(void); +int APS5_VABI sceCompanionHttpdInitialize2(void); +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback(void); +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2(void); +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback(void); +} + +namespace { + +constexpr int InvalidParam = static_cast(0x80E40004u); +constexpr int NotInitialized = static_cast(0x80E40006u); +constexpr int AlreadyInitialized = static_cast(0x80E40007u); +constexpr int NoEvent = static_cast(0x80E40008u); +constexpr int AlreadyStarted = static_cast(0x80E4000Au); +constexpr int NotStarted = static_cast(0x80E4000Bu); +constexpr int AlreadyRegistered = static_cast(0x80E4000Cu); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "CompanionHttpd: %s\n", message); + std::abort(); + } +} + +template +void RequireNotImplemented(TCall call, const std::string& name) { + try { + call(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == name + " not implemented", "a companion function threw another error"); + return; + } + Require(false, "a companion function that should throw returned"); +} + +int APS5_VABI RequestCallback(int, int, const void*, void*, void*) { + std::fprintf(stderr, "CompanionHttpd: the request callback was called\n"); + std::abort(); +} + +void CheckOptionBlock() { + unsigned char block[0x70]; + std::memset(block, 0x5a, sizeof(block)); + Require(sceCompanionHttpdOptParamInitialize(nullptr) == InvalidParam, "a null option block must be rejected"); + Require(sceCompanionHttpdOptParamInitialize(block) == 0, "sceCompanionHttpdOptParamInitialize failed"); + for (std::size_t index = 0; index < sizeof(block); ++index) { + Require(block[index] == (index < 0x60 ? 0u : 0x5au), "the option block must be zeroed over its 0x60 bytes and nothing past them"); + } +} + +void CheckStates() { + unsigned char param[0x60]{}; + unsigned char event[0x104]; + std::memset(event, 0x5a, sizeof(event)); + unsigned char untouched[0x104]; + std::memset(untouched, 0x5a, sizeof(untouched)); + auto* callback = reinterpret_cast(&RequestCallback); + Require(sceCompanionHttpdGetEvent(event) == NotInitialized, "an event before initialization"); + Require(sceCompanionHttpdStart() == NotInitialized, "a start before initialization"); + Require(sceCompanionHttpdStop() == NotInitialized, "a stop before initialization"); + Require(sceCompanionHttpdTerminate() == NotInitialized, "a termination before initialization"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, nullptr) == NotInitialized, "a callback before initialization"); + Require(sceCompanionHttpdUnregisterRequestCallback() == NotInitialized, "an unregistration before initialization"); + Require(sceCompanionHttpdInitialize(nullptr) == InvalidParam, "a null parameter must be rejected"); + Require(sceCompanionHttpdInitialize(param) == 0, "sceCompanionHttpdInitialize failed"); + Require(sceCompanionHttpdInitialize(param) == AlreadyInitialized, "a second initialization"); + Require(sceCompanionHttpdRegisterRequestCallback(nullptr, nullptr) == InvalidParam, "a null callback must be rejected"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, param) == 0, "registering the request callback failed"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, param) == AlreadyRegistered, "a second callback"); + Require(sceCompanionHttpdGetEvent(nullptr) == InvalidParam, "a null event must be rejected"); + Require(sceCompanionHttpdGetEvent(event) == NoEvent, "no companion device connects before the start"); + Require(sceCompanionHttpdStop() == NotStarted, "a stop before the start"); + Require(sceCompanionHttpdStart() == 0, "sceCompanionHttpdStart failed"); + Require(sceCompanionHttpdStart() == AlreadyStarted, "a second start"); + Require(sceCompanionHttpdGetEvent(event) == NoEvent, "no companion device connects after the start"); + Require(std::memcmp(event, untouched, sizeof(event)) == 0, "sceCompanionHttpdGetEvent wrote the event"); + RequireNotImplemented([] { sceCompanionHttpdTerminate(); }, "sceCompanionHttpdTerminate"); + Require(sceCompanionHttpdStop() == 0, "sceCompanionHttpdStop failed"); + Require(sceCompanionHttpdUnregisterRequestCallback() == 0, "unregistering the request callback failed"); + RequireNotImplemented([] { sceCompanionHttpdUnregisterRequestCallback(); }, "sceCompanionHttpdUnregisterRequestCallback"); + Require(sceCompanionHttpdTerminate() == 0, "sceCompanionHttpdTerminate failed"); + Require(sceCompanionHttpdTerminate() == NotInitialized, "a second termination"); + Require(sceCompanionHttpdInitialize(param) == 0, "initializing again after the termination failed"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, nullptr) == 0, "the termination kept the callback"); + Require(sceCompanionHttpdTerminate() == 0, "the last termination failed"); +} + +void CheckResponseBuilders() { + unsigned char response[0x20]{}; + RequireNotImplemented([&] { sceCompanionHttpdAddHeader("key", "value", response); }, "sceCompanionHttpdAddHeader"); + RequireNotImplemented([&] { sceCompanionHttpdSetStatus(200, response); }, "sceCompanionHttpdSetStatus"); + RequireNotImplemented([&] { sceCompanionHttpdSetBody("body", 4, response); }, "sceCompanionHttpdSetBody"); + RequireNotImplemented([] { sceCompanionHttpdGet2ndScreenStatus(); }, "sceCompanionHttpdGet2ndScreenStatus"); + RequireNotImplemented([] { sceCompanionHttpdGetUserId(); }, "sceCompanionHttpdGetUserId"); + RequireNotImplemented([] { sceCompanionHttpdInitialize2(); }, "sceCompanionHttpdInitialize2"); + RequireNotImplemented([] { sceCompanionHttpdRegisterRequestBodyReceptionCallback(); }, "sceCompanionHttpdRegisterRequestBodyReceptionCallback"); + RequireNotImplemented([] { sceCompanionHttpdRegisterRequestCallback2(); }, "sceCompanionHttpdRegisterRequestCallback2"); + RequireNotImplemented([] { sceCompanionHttpdUnregisterRequestBodyReceptionCallback(); }, "sceCompanionHttpdUnregisterRequestBodyReceptionCallback"); +} + +} + +int main() { + CheckOptionBlock(); + CheckStates(); + CheckResponseBuilders(); + return 0; +} diff --git a/docs/dev/debt/libSceCompanionHttpd.md b/docs/dev/debt/libSceCompanionHttpd.md index 2738a6bd6d..3ed7d72d8c 100644 --- a/docs/dev/debt/libSceCompanionHttpd.md +++ b/docs/dev/debt/libSceCompanionHttpd.md @@ -2,4 +2,10 @@ ### Unknown function info -- [sceCompanionHttpdAddHeader, sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetEvent, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize, sceCompanionHttpdInitialize2, sceCompanionHttpdOptParamInitialize, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdSetBody, sceCompanionHttpdSetStatus, sceCompanionHttpdStart, sceCompanionHttpdStop, sceCompanionHttpdTerminate, sceCompanionHttpdUnregisterRequestBodyReceptionCallback and sceCompanionHttpdUnregisterRequestCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionHttpd.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceCompanionHttpdOptParamInitialize, sceCompanionHttpdInitialize](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - the option block is taken as 0x60 bytes: titles fill a work memory pointer at +0x08, its size at +0x10 and a temporary data mount point at +0x58 after `sceCompanionHttpdOptParamInitialize`. Its other fields and the defaults the console writes are unknown, so `sceCompanionHttpdOptParamInitialize` zeroes the 0x60 bytes and `sceCompanionHttpdInitialize` reads none of them; both reject a null block with `INVALID_PARAM` +- [sceCompanionHttpdAddHeader, sceCompanionHttpdSetStatus, sceCompanionHttpdSetBody](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - imported by PS4 titles and added without an implementation. They fill the response of a request callback, which is never called here, so every call throws +- [sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize2, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdUnregisterRequestBodyReceptionCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - the rest of the library's exports, declared without parameters and without an implementation; their signatures are not verified, so every call throws + +### Functional + +- [Companion server state](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - no HTTP server listens, so no companion device ever connects: `sceCompanionHttpdGetEvent` returns `NO_EVENT` without writing the event and the request callback is never called. Initialization, callback registration and start and stop are tracked with the errors of shadPS4's [companion_error.h](https://github.com/shadps4-emu/shadPS4/blob/eb9f66c349ae7c8fef780f4f55e98b8065947e14/src/core/libraries/companion/companion_error.h) (`ALREADY_REGISTERED` is 0x80E4000C, between `NOT_STARTED` and `NOT_CONNECTED`; the header has 0x80E4000, one digit short). Unregistering with no callback and terminating a started server throw, since the console's answers are unknown. Not measured on a console. Closes when: a companion server is hosted From d310c4e27e16a7d91f9c95e15d4f8b39087bd50c Mon Sep 17 00:00:00 2001 From: DProkos Date: Tue, 20 Oct 2026 11:22:19 +0300 Subject: [PATCH 0059/1360] feat(libSceNpCppWebApi): answer the UserProfile, ProfanityFilter and CommunicationRestrictionStatus requests offline Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 10 + core/libs/prx/libSceNpCppWebApi/Export.cpp | 467 ++++++++++++++++++ .../tests/GuestNpCppWebApiIntrusivePtr.cpp | 45 ++ core/libs/tests/GuestNpCppWebApiSignedOut.cpp | 189 +++++++ docs/dev/debt/libSceNpCppWebApi.md | 4 +- 5 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestNpCppWebApiSignedOut.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 50373d1bf8..2aeaba84d0 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -567,6 +567,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_cpp_web_api_signed_out_tests tests/GuestNpCppWebApiSignedOut.cpp) + target_include_directories(guest_np_cpp_web_api_signed_out_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_cpp_web_api_signed_out_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_signed_out_tests) + add_test(NAME guest_np_cpp_web_api_signed_out COMMAND guest_np_cpp_web_api_signed_out_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_signed_out 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/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index b2b6ff6b1c..b53786f0fe 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1791,4 +1791,471 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V15Owner12getAccountIdEv( NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V118GetFriendsResponse10getFriendsEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V124GetBlockingUsersResponse9getBlocksEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetBasicPresencesResponse17getBasicPresencesEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorImED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPKcRNS2_12IntrusivePtrINS2_6StringEEE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorImE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorImE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorImEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence12getAccountIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence12getInContextEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence15getOnlineStatusEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi15ProfanityFilter2V223FilterProfanityResponse10getMessageEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponse13getRestrictedEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEneERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEneERKS4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp index 99f3f83152..f9a03f0d52 100644 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -56,6 +56,29 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr*, const IntrusivePtr*); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr*, void*, Deleter, void*); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev(IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv(const IntrusivePtr*); } static void Require(bool value) { if (!value) std::abort(); } @@ -279,6 +302,9 @@ int main() { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev, }; for (const auto construct : constructors) TestConstruct(construct); @@ -293,12 +319,27 @@ int main() { _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv, }; for (const auto access : accessors) TestAccess(access); const Copy copies[] = { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_, }; for (const auto copy : copies) TestCopy(copy); @@ -311,6 +352,10 @@ int main() { {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev, VectorReferenceCountOffset}, }; for (const auto& testCase : destructors) TestDestroy(testCase); diff --git a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp new file mode 100644 index 0000000000..455bc254b0 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp @@ -0,0 +1,189 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static constexpr int NotSignedIn = static_cast(0x80552907); + +using Object = int (APS5_VABI*)(void*); +using Call = int (APS5_VABI*)(); + +struct RequestFlow { + Object transactionConstruct; + Object transactionDestroy; + Call setOnFinishedCallback; + Call setResponseInformationOption; + Call start; + Call finish; + Call getResponseHeaders; + Object parameterConstruct; + Object parameterDestroy; + Call initialize; + Call terminate; + Call call; +}; + +static const RequestFlow flows[] = { + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, +}; + +static void TestRequestFlow(const RequestFlow& flow) { + alignas(16) unsigned char transaction[0x100]; + alignas(16) unsigned char parameter[0x100]; + std::memset(transaction, 0x5a, sizeof(transaction)); + std::memset(parameter, 0x5a, sizeof(parameter)); + + Require(flow.transactionConstruct(transaction) == 0); + Require(flow.setOnFinishedCallback() == 0); + Require(flow.setResponseInformationOption() == 0); + Require(flow.parameterConstruct(parameter) == 0); + Require(flow.initialize() == 0); + Require(flow.call() == NotSignedIn); + Require(flow.start() == NotSignedIn); + Require(flow.getResponseHeaders() == NotSignedIn); + Require(flow.finish() == 0); + Require(flow.terminate() == 0); + Require(flow.parameterDestroy(parameter) == 0); + Require(flow.transactionDestroy(transaction) == 0); +} + +int main() { + for (const auto& flow : flows) TestRequestFlow(flow); + Require(_ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() == NotSignedIn); + return 0; +} diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 9352e4f30e..7583e31fe5 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -2,10 +2,11 @@ ### Silent stubs -- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (8 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (15 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used - [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` - [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` - [sce::Np::CppWebApi::TitleCloudStorage::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData::{initialize,terminate}` succeed without storing state +- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `FriendsApi::ParameterToGetFriends`, `PresenceApi::ParameterToGetBasicPresences`, `BlocksApi::ParameterToGetBlockingUsers`, `ProfanityApi::ParameterToFilterProfanity` and `CommunicationRestrictionStatusApi::ParameterToGetCommunicationRestrictionStatus`, each `{ctor,dtor,initialize,terminate}` ### Unknown function info @@ -14,4 +15,5 @@ - [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. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count - [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object +- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by the Epic Online Services module of PPSA24003, whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 73 exports are named from aerolib, so each name recomputes to the NID a title imports (`tools/nid_names.py`), and each throws. They are the `Common` template members (`IntrusivePtr`, `Vector`, `Iterator`, `ConstIterator`, `Transaction`, `TransactionBase`) and the `TitleCloudStorage::V1` and `InGameCatalog::V5` accessors: a mangled name gives the class, the method and the argument types, but the layout of the object the call is made on, the values the caller reads back and the console's errors are unknown, so no call can be answered without writing to memory no source describes. No call site is recorded for any of them From 381c3b974454e5015547107c8316b3a4820e60e1 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 11:25:39 +0300 Subject: [PATCH 0060/1360] fix(tests): skip the 1023-byte guest path check on macOS hosts macOS fails a host path of 1024 bytes or more with ENAMETOOLONG, and the host form of a guest path is the root directory followed by the guest path, so a guest path of 1023 bytes, which FreeBSD accepts, fails there under any root directory. guest_path_name_length skips that check on macOS, and a debt entry for guest path names gives the macOS limit. --- core/libs/tests/GuestPathNameLength.cpp | 2 ++ docs/dev/debt/libc.md | 1 + 2 files changed, 3 insertions(+) diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp index e07722661b..793fd3e651 100644 --- a/core/libs/tests/GuestPathNameLength.cpp +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -55,7 +55,9 @@ int main() { Require(TooLong(below + longName + "/"), "a long component followed by a trailing slash must fail with ENAMETOOLONG (63)"); Require(!TooLong(below + std::string(200, 'n') + "\\" + std::string(100, 'n')), "a backslash separates components here, so 200 + 100 bytes around it are accepted (FreeBSD would refuse the 301-byte name)"); const std::string block = name255 + "/"; +#ifndef __APPLE__ Require(!TooLong(block + block + block + name255), "a path of exactly 1023 bytes must not be too long"); +#endif Require(TooLong(block + block + block + block), "a path of exactly 1024 bytes must fail with ENAMETOOLONG (63)"); Require(rmdir_nid_postfix("anyps5_name_length") == 0, "rmdir of the test directory failed"); Require(Fails(stat_nid_postfix((longName + "/x").c_str(), status), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e06d2e9b22..a7a0ee5f29 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -64,6 +64,7 @@ - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. - [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. +- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) fail on macOS with `ENAMETOOLONG` once their host form, the root directory followed by the guest path, reaches 1024 bytes, so a guest path of 1023 bytes, which FreeBSD accepts, fails there under any root directory - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported From ee6efa9fce2dad24a33a2ac4bc9f3396787e6bc2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 08:28:19 +0000 Subject: [PATCH 0061/1360] fix(agc): leave a multisampled DCC target's CMASK unchecked without fast clears The initial import of a multisampled DCC target required its CMASK to be uniformly 0xcc or the fully expanded code. When CB_COLOR_INFO enables COMPRESSION but not FAST_CLEAR, the CMASK only holds FMASK compression, which picks the fragment each sample reads, and a uniform DCC clear sets every fragment to the clear color. The CMASK is then not checked, and the target imports as the DCC clear. METAL GEAR SOLID 4's launcher (PPSA18194) opens its Master Book on a 2-sample 1920x1080 target whose DCC reads as the 0000 clear and whose CMASK holds stale bytes, with FAST_CLEAR off. It stopped there. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/MultisampleTarget.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 01f22449f5..2c91e24553 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -727,7 +727,7 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span(cmask.front()); const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu; Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 67d5b61cbe..e5ce7aaea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3747,6 +3747,24 @@ void multisampleDccStateTests() { changed.context[0x3b0] |= 1u << 28u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips"); } + for (const auto samples : {2u, 4u, 8u}) { + AgcDriver::Graphics::ColorTarget target{}; + target.extent = {64, 64}; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.samples = samples; + target.dccAddress = 0x200000u; + target.dccPipeAligned = true; + target.cmaskAddress = 0x300000u; + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(64, 64); + const std::vector dcc(AgcDriver::Graphics::MultisampledDccBytes(target), std::byte{0x00}); + std::vector cmask(target.cmaskBytes); + for (std::size_t index = 0; index < cmask.size(); ++index) cmask[index] = std::byte{static_cast(index * 37u)}; + const auto clear = AgcDriver::Graphics::MultisampledDccClear(target, dcc, cmask); + Require(clear.float32[0] == 0.0f && clear.float32[1] == 0.0f && clear.float32[2] == 0.0f && clear.float32[3] == 0.0f, "a uniform 0000 DCC clear of a multisampled target without CMASK fast clears did not import as 0000"); + target.cmaskFastClear = true; + expectFailure([&] { static_cast(AgcDriver::Graphics::MultisampledDccClear(target, dcc, cmask)); }, "multisampled DCC CMASK compression or register clear"); + } } void multisampledDepthTests() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 5ed4ff62f9..2bf0fcdafe 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -142,6 +142,7 @@ - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. +- [Multisampled DCC CMASK without fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the initial CMASK of a multisampled DCC target is not checked when CB_COLOR_INFO enables COMPRESSION but not FAST_CLEAR: it then holds only FMASK compression, and a uniform DCC clear sets every fragment, so the target imports as that clear. What the console reads where such an FMASK names no fragment, as an uninitialized CMASK can, is unverified. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. From 070e44768c40f7ceaf95d6220549f07dee724a03 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 11:55:07 +0300 Subject: [PATCH 0062/1360] fix(tests): let the TZ flipper of the zone race yield The zone race of guest_time_conversion changes TZ in a tight loop while six threads convert times. On macOS the flipper keeps the guest environment lock: the default mutex there lets the thread that unlocks take the lock again before a woken waiter runs. The readers mostly waited, and the test ran for 21 seconds, past its 20-second timeout; a run with counters showed about 373 million TZ changes against the readers' 15,000 local time conversions. The flipper now yields after each change. It still changes TZ about 800,000 times while the readers convert, they see each zone in about half of their conversions, and the test takes about a second. --- core/libs/tests/GuestTimeConversion.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 2330a5acba..50518721ac 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -372,7 +372,10 @@ static void CheckZoneRace() { std::atomic stop{false}; std::atomic torn{0}; std::thread flipper([&] { - for (bool west = false; !stop.load(); west = !west) setenv_nid_postfix("TZ", west ? "UTC-5" : "UTC+3", 1); + for (bool west = false; !stop.load(); west = !west) { + setenv_nid_postfix("TZ", west ? "UTC-5" : "UTC+3", 1); + std::this_thread::yield(); + } }); std::array readers; for (auto& reader : readers) { From bb864e8acdc806672853bbdfd28a3dd5a2d4550b Mon Sep 17 00:00:00 2001 From: felek Date: Tue, 20 Oct 2026 01:30:25 +0200 Subject: [PATCH 0063/1360] docs(dev): name the memory budgets and the sleeping-display stop in the game testing guide Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BywPXoZACVi8ALV4JKvuPZ --- docs/dev/GAME_TESTING.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index be0924205b..d6ab702076 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -7,6 +7,7 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] ## Before a run - Build `main` or the PR head in a clean worktree, including the `libs` target (it is not in the default build). If a new test breaks the build, `cmake --build build --target libs relinker` is enough to convert and run a title. +- `tools/check_main_sync.py --fetch` in the worktree reports whether the branch is in step with upstream `main`, so a result isn't taken on an old base by mistake. - Convert the title from plain ELF files (`tools/unwrap_self.py` unwraps a dump's `eboot.bin` and `.prx`): - Linux: `relinker --registry src/eboot.elf app.elf` - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). @@ -49,7 +50,12 @@ cmd /c '.\app.exe > run.log 2>&1' - The driver's own: Mesa (`MESA_SHADER_CACHE_DIR`, or `MESA_SHADER_CACHE_DISABLE=true`), AMD on Windows (`%LOCALAPPDATA%\AMD\VkCache`). Back a driver cache up instead of deleting it; it isn't AnyPS5's. Say in the report whether the caches were empty or warm. A first run that ends in device lost (`VK_ERROR_DEVICE_LOST`) or a GPU wait timeout while large pipelines are still being built, seen on AMD, often is the cache and not a regression: run again with the caches warm and report both runs. -- **Power and display:** on a laptop, run on AC power; a run on battery can make a neutral change look like a regression. Note the display's refresh rate: a title that asks for 120 Hz output can stop at start on a 60 Hz display. +- **Power and display:** on a laptop, run on AC power; a run on battery can make a neutral change look like a regression. Note the display's refresh rate: a title that asks for 120 Hz output can stop at start on a 60 Hz display. Keep the display awake for long or unattended runs: a title started while it sleeps stops with `SDL_GetWindowDisplayIndex failed: Couldn't find any displays`. +- **Memory budgets:** two budgets come from the machine's memory and can change the frame rate several times over, so check them before comparing results from different machines. + - Host imports (`APS5_HOST_IMPORT_MIB`, see [ENVIRONMENT](ENVIRONMENT.md)): the default follows the physical memory and the host heap the driver reports, so two machines with the same RAM can get different budgets. Refused imports print `[gpu] host import budget: X MiB live of Y MiB (...); N imports (M MiB) refused so far`, at most every 10 s; without such a line nothing was refused. A refused range is copied on every later use instead, and those copies can take device memory the texture caches need. + - Texture caches (`APS5_TEXTURE_CACHE_MIB`, see [USAGE](../user/USAGE.md#texture-cache-budget)): `[gpu] sampled texture cache budget N MiB` at start and when it changes, and with `APS5_PROFILE_DRAW=1` a `[texture-cache] (10 s)` line with the evictions and the budget. Thousands of evictions per 10 s with the budget at its 2048 MiB floor mean textures are rebuilt every frame. + + Report these lines. Set either variable only for a comparison, and say so. ## What to read @@ -103,7 +109,7 @@ Include, in this order: 3. Host: CPU, OS (and kernel on Linux), the GPU as the `Selected GPU name=` line names it, the driver version, AC or battery, and the display refresh rate. 4. The exact command and every `APS5_*` / `ANYPS5_*` variable, the in-game settings if they differ from the defaults, and whether the caches were empty or warm. 5. How far the title got (menu, a save, a scene) and what stopped it: the exit code and the last 200 log lines, plus the kernel line (Linux) or crash report (Windows) for a crash. -6. Presents per 10 s, and storage-image lines if memory is involved. +6. Presents per 10 s, and the storage-image and budget lines (`[gpu] host import budget`, `[gpu] sampled texture cache budget`, `[texture-cache]`) if memory is involved. 7. How many runs and how long each lasted. One run is an observation, not a reproduction. 8. The date of the test. Leave unknown fields empty rather than filling them from another report. From 22cd2ec5c711d2747362973c8f49d173f6093873 Mon Sep 17 00:00:00 2001 From: hypermist Date: Tue, 20 Oct 2026 22:18:40 +1300 Subject: [PATCH 0064/1360] fix(agc): return the console results of the capture, shader instrumentation and occlusion query size calls sceAgcDriverRequestCaptureStart, sceAgcDriverRequestCaptureStop and sceAgcDriverTriggerCapture return 0x8A6C1000 without doing anything. sceAgcSetShaderInstrumentation keeps the flags and returns 0, and sceAgcGetShaderInstrumentation returns them. sceAgcDcbBeginOcclusionQueryGetSize is 0x10 for type 0, 0x120 for type 1 and 0 otherwise; sceAgcDcbEndOcclusionQueryGetSize is 0x10. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 +++++ core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp | 9 ++-- core/libs/prx/libSceAgc/Misc/src/Debug.cpp | 16 +++++++ core/libs/prx/libSceAgc/Unimplemented.cpp | 10 ---- .../tests/InstrumentationQuerySize.cpp | 48 +++++++++++++++++++ .../prx/libSceAgcDriver/State/src/Status.cpp | 15 +++--- 6 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 351466befa..9d4922bfff 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -253,6 +253,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_instrumentation_query_size_tests EXCLUDE_FROM_ALL tests/InstrumentationQuerySize.cpp) +target_include_directories(agc_instrumentation_query_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_instrumentation_query_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_instrumentation_query_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_instrumentation_query_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_instrumentation_query_size_tests COMMAND agc_instrumentation_query_size_tests) + if(WIN32) + set_tests_properties(agc_instrumentation_query_size_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_context_state_size_tests EXCLUDE_FROM_ALL tests/ContextStateSize.cpp) target_include_directories(agc_context_state_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_context_state_size_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp index b3ae56e199..877427619a 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp @@ -21,14 +21,13 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c return Agc::Command::Emit(buf, 0x10u, {bufferSizeInBytes, static_cast(address), static_cast(address >> 32u), control}, __func__); } -std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::uint32_t type) { + if (type == 1) return 0x120u; + return type == 0 ? 0x10u : 0u; } std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0x10u; } } diff --git a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp index 8a321110ab..a211959181 100644 --- a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp @@ -1,9 +1,16 @@ #include "prx/libSceAgc/Misc/include/Debug.hpp" +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::atomic shaderInstrumentation{0}; + +} + extern "C" { int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { @@ -11,4 +18,13 @@ int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { return 0; } +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) { + shaderInstrumentation.store(flags, std::memory_order_relaxed); + return 0; +} + +std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation() { + return shaderInstrumentation.load(std::memory_order_relaxed); +} + } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 7a7d7c52cb..35b52cb883 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -40,14 +40,4 @@ int APS5_VABI sceAgcCbMemsetExclusive() { return 0; } -int APS5_VABI sceAgcSetShaderInstrumentation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcGetShaderInstrumentation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp b/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp new file mode 100644 index 0000000000..54ebfc9838 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp @@ -0,0 +1,48 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t); +std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation(); +std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::uint32_t); +std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize(); +} + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void testShaderInstrumentation() { + check(sceAgcGetShaderInstrumentation() == 0, "shader instrumentation starts cleared"); + check(sceAgcSetShaderInstrumentation(0x80000005u) == 0, "setting shader instrumentation failed"); + check(sceAgcGetShaderInstrumentation() == 0x80000005u, "shader instrumentation flags were not kept"); + check(sceAgcSetShaderInstrumentation(0) == 0 && sceAgcGetShaderInstrumentation() == 0, "shader instrumentation was not cleared"); +} + +void testOcclusionQuerySizes() { + check(sceAgcDcbBeginOcclusionQueryGetSize(0) == 0x10u, "begin occlusion query size for type 0"); + check(sceAgcDcbBeginOcclusionQueryGetSize(1) == 0x120u, "begin occlusion query size for type 1"); + check(sceAgcDcbBeginOcclusionQueryGetSize(2) == 0, "begin occlusion query size for an unknown type"); + check(sceAgcDcbEndOcclusionQueryGetSize() == 0x10u, "end occlusion query size"); +} + +} + +int main() { + try { + testShaderInstrumentation(); + testOcclusionQuerySizes(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC shader instrumentation and occlusion query size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/State/src/Status.cpp b/core/libs/prx/libSceAgcDriver/State/src/Status.cpp index ae1a8543f7..825a9e9001 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Status.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Status.cpp @@ -5,6 +5,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int CAPTURE_UNAVAILABLE = static_cast(0x8A6C1000); + +} + extern "C" { bool APS5_VABI sceAgcDriverIsCaptureInProgress(void) { @@ -21,18 +27,15 @@ bool APS5_VABI sceAgcDriverIsSubmitValidationEnabled(void) { int APS5_VABI sceAgcDriverRequestCaptureStart(const char* path) { (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverRequestCaptureStop() { - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverTriggerCapture() { - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } } From 6a4913cc7cc6e185f98f6b0c8792bd5c5c70b476 Mon Sep 17 00:00:00 2001 From: hypermist Date: Tue, 20 Oct 2026 22:32:46 +1300 Subject: [PATCH 0065/1360] feat(agc): write the workload packets on the ACB and for an inactive stream sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete and sceAgcAcbSetWorkloadStreamInactive build the same packets as the DCB calls, and sceAgcDcbSetWorkloadStreamInactive writes a 9-dword IT_NOP with the stream id, the size of the console's user-data packet. The debt entry now records the console layout read from libSceAgcDriver. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 21 ++------ .../libSceAgc/DcbState/include/Workload.hpp | 11 ++++ .../prx/libSceAgc/DcbState/src/Workload.cpp | 50 +++++++++++++------ core/libs/prx/libSceAgc/tests/Commands.cpp | 17 ++++--- docs/dev/debt/libSceAgc.md | 3 +- 5 files changed, 61 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 567bfd5e18..539066325a 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,23 +4,13 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" +#include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" -#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -namespace { - -std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { - if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - NotImplemented_nid_no_patch(function); - return nullptr; -} - -} - extern "C" { std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { @@ -83,18 +73,15 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { - (void)workloadIds; - (void)workloadCount; - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + return Agc::Workload::Active(buf, streamId, workloadIds, workloadCount, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { - (void)workloadId; - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + return Agc::Workload::Complete(buf, streamId, workloadId, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + return Agc::Workload::StreamInactive(buf, streamId, __func__); } uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { diff --git a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp index 3d5d6cac71..75ae9c3979 100644 --- a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp +++ b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp @@ -1,4 +1,15 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP #define CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP +#include +#include "SceTypes.hpp" + +namespace Agc::Workload { + +std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function); +std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function); +std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function); + +} + #endif diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 0f739d9055..06d5578a81 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -13,6 +13,7 @@ namespace { constexpr std::uint32_t OpcodeNop = 0x10; constexpr std::uint32_t ActivePacketWords = 18; constexpr std::uint32_t CompletePacketWords = 12; +constexpr std::uint32_t InactivePacketWords = 9; constexpr std::uint32_t MaxWorkloadId = 63; constexpr std::uint32_t MaxActiveCount = 63; @@ -25,40 +26,57 @@ uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* functio } -extern "C" { +namespace Agc::Workload { -uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; - const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); - auto* packet = NewPacket(buf, CompletePacketWords, __func__); - packet[1] = stream_id; - packet[2] = workload_id; +std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function) { + if (buf == nullptr || workloadId > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workloadId); + auto* packet = NewPacket(buf, CompletePacketWords, function); + packet[1] = streamId; + packet[2] = workloadId; packet[3] = static_cast(clearMask); packet[4] = static_cast(clearMask >> 32); return packet; } -uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; +std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function) { + if (buf == nullptr || workloadIds == nullptr || workloadCount == 0 || workloadCount > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; std::uint64_t mask = 0; - for (std::uint32_t index = 0; index < workload_count; ++index) { - const auto id = workload_ids[index]; + for (std::uint32_t index = 0; index < workloadCount; ++index) { + const auto id = workloadIds[index]; if (id > MaxWorkloadId) return nullptr; const std::uint64_t bit = std::uint64_t{1} << id; if ((mask & bit) != 0) return nullptr; mask |= bit; } - auto* packet = NewPacket(buf, ActivePacketWords, __func__); - packet[1] = stream_id; + auto* packet = NewPacket(buf, ActivePacketWords, function); + packet[1] = streamId; packet[2] = static_cast(mask); packet[3] = static_cast(mask >> 32); return packet; } -uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { +std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function) { if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = NewPacket(buf, InactivePacketWords, function); + packet[1] = streamId; + return packet; +} + +} + +extern "C" { + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { + return Agc::Workload::Complete(buf, stream_id, workload_id, __func__); +} + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { + return Agc::Workload::Active(buf, stream_id, workload_ids, workload_count, __func__); +} + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { + return Agc::Workload::StreamInactive(buf, streamId, __func__); } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 336c49b51f..71612289ab 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -294,12 +294,17 @@ void testWorkloadStreams() { check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); Storage registered; registered.words.fill(0xdeadbeefu); - const auto untouched = registered.words; - expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); - expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); - expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); - expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); - check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); + const auto* inactive = sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); + check(inactive == registered.words.data() && inactive[0] == Agc::Command::Header(0x10, 9, 0) && inactive[1] == 1, "DCB workload stream inactive packet"); + for (std::uint32_t word = 2; word < 9; ++word) check(inactive[word] == 0, "DCB workload stream inactive packet tail is not zero"); + check(registered.buffer.cursor_up == registered.words.data() + 9, "DCB workload stream inactive cursor advance"); + Storage dcb; + Storage acb; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) != nullptr && sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 2) == acb.words.data(), "ACB workloads active packet"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) != nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 3) == acb.words.data() + 18, "ACB workload complete packet"); + check(sceAgcDcbSetWorkloadStreamInactive(&dcb.buffer, 1) != nullptr && sceAgcAcbSetWorkloadStreamInactive(&acb.buffer, 1) == acb.words.data() + 30, "ACB workload stream inactive packet"); + check(acb.buffer.cursor_up == acb.words.data() + 39 && dcb.words == acb.words, "ACB workload packets differ from the DCB ones"); + check(sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 0) == nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 64) == nullptr, "ACB workload argument checks"); check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, "a workload call with a null command buffer for a registered stream returned a packet"); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 53522e9736..d9c6839824 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -42,7 +42,6 @@ - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. -- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. -- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting +- [sceAgcDcbSetWorkloadsActive, sceAgcDcbSetWorkloadComplete, sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp), [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the console packets are not written. The firmware builds them with libSceAgcDriver's sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete and sceAgcDriverSetWorkloadStreamInactive: a SET_UCONFIG_REG of a tag to 0x342 (`0xCC000000 | stream` for active and inactive, `0xCD000000 + workload * 32 + stream` for complete) that carries the first dword of an 8-byte payload (the active mask, zero for inactive), a WRITE_DATA of the rest to register 0xC343, and for active and complete a 9-dword ATOMIC_MEM that ORs the mask into, or ANDs the cleared bit out of, the stream's 64-bit word in a table the driver takes at module start (control 0x267 or 0x275, bit 30 set on the DCB). The executor has no ATOMIC_MEM, the table is not allocated, and an unpredicated SET_UCONFIG_REG is rejected on a compute queue, so the calls write an `IT_NOP` of the console size instead: 18 dwords for active (stream id, mask), 12 for complete (stream id, workload id, clear mask) and 9 for inactive (stream id); the ACB calls write the same packets as the DCB calls. The argument checks are the firmware's: null for a null command buffer, a stream outside 1 to 31 or not registered, a workload id above 63, and for active a null id list, a duplicate id or a count of 0 or above 63; the stream is checked before the ids are read. The driver registration records only the stream id; the console also keeps a 32-byte name per stream and pre-registers stream 0 as "System", and its error codes for an invalid or repeated registration are not returned, so registration never fails here. - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From ca55e46f98ad85861dd7567abad0c028274daca1 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Tue, 20 Oct 2026 04:31:08 -0500 Subject: [PATCH 0066/1360] test(agc): align the array window test's texels at run time WinLibs GCC 15.2.0 rejects the alignas(65536) static array that agc_driver_image_array_window_past_layer_limit added in #6772 (requested alignment '65536' exceeds object file maximum 8192), which stops a Windows build with every AGC execution test. Take a 64 KiB aligned 4 MiB span of a larger static array instead, as #6452 did for the indirect arguments block, and check its alignment at start. --- .../tests/execution/ImageArrayWindowPastLayerLimit.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp index 0d29ae08e4..528cad640e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -30,7 +30,10 @@ constexpr std::uint32_t ClampEdge = 2; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(65536) std::array Texels{}; +constexpr std::size_t TexelBytes = 4u << 20u; +constexpr std::size_t TexelAlignment = 65536; +std::array TexelStorage{}; +const std::span Texels{reinterpret_cast((reinterpret_cast(TexelStorage.data()) + TexelAlignment - 1) & ~(TexelAlignment - 1)), TexelBytes}; alignas(256) constexpr std::array Code{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04, @@ -62,7 +65,7 @@ std::array Texel(std::uint32_t x, std::uint32_t guestLayer) { } void FillTexels() { - Texels.fill(0); + std::ranges::fill(Texels, std::uint8_t{0}); const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); const auto& mip = surface.mips.at(0); Require(surface.layers == LastArray + 1u && surface.guestBytes <= Texels.size(), "the test array does not fit its storage"); @@ -128,6 +131,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { int main() { try { + Require(reinterpret_cast(Texels.data()) % TexelAlignment == 0, "the test array is not 64 KiB aligned"); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillTexels(); From 6e578483c88a953d77c2a231cdc26081ed6085bf Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 13:46:33 +0400 Subject: [PATCH 0067/1360] fix(relinker): accept a Windows base relocation whose eight bytes span into the next page --- .../elfpatcher/src/windows/WindowsRelocationBuilder.cpp | 2 -- core/relinker/relinker/tests/ConversionTests.cpp | 6 +++--- core/relinker/relinker/tests/test_none_relocations.py | 7 +++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 71b149d37f..7bd368ac26 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -86,8 +86,6 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom std::vector WindowsRelocationBuilder::BuildBaseRelocations(const std::vector& targets) const { std::map> pages; for (const auto target : targets) { - if ((target & 0xfffu) > 0x1000u - 8) - throw Domain::RelinkerException("Base relocation target crosses a page boundary", target); auto& entries = pages[target & ~0xfffu]; const auto entry = static_cast(0xa000u | (target & 0xfffu)); if (std::find(entries.begin(), entries.end(), entry) == entries.end()) diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 0b6d9d62c9..2c9f696d35 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -241,9 +241,9 @@ void baseRelocations() { require(Io::ReadU32(relocs, 4) == 12, "Wrong base relocation block size"); require(Io::ReadU16(relocs, 8) == 0xa000, "Wrong first relocation entry"); require(Io::ReadU16(relocs, 10) == 0xa008, "Wrong second relocation entry"); - reject([&] { (void)builder.BuildBaseRelocations({0x10ffc}); }); - reject([&] { (void)builder.BuildBaseRelocations({0x10ffd}); }); - reject([&] { (void)builder.BuildBaseRelocations({0x10fff}); }); + const auto spanning = builder.BuildBaseRelocations({0x10ffc, 0x10fff}); + require(spanning.size() == 12 && Io::ReadU32(spanning, 0) == 0x10000, "A page-spanning base relocation was not placed in its first page"); + require(Io::ReadU16(spanning, 8) == 0xaffc && Io::ReadU16(spanning, 10) == 0xafff, "Wrong page-spanning base relocation entries"); } } diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 538b0247d8..b117b9781a 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -122,14 +122,13 @@ def main(): page_image = image_with_relocations([(0xffc, 8, 0x210)], False, False) page_image.extend(bytes(0x1000)) struct.pack_into(" Date: Tue, 20 Oct 2026 13:46:34 +0400 Subject: [PATCH 0068/1360] test(relinker): run a Windows base relocation that spans two pages --- core/relinker/relinker/tests/test_none_relocations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index b117b9781a..71aab2c87c 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -120,6 +120,7 @@ def main(): assert overflow_result.returncode == 2 and "Relocation target address overflow" in overflow_result.stderr, overflow_result.stderr assert not overflow_output.exists() page_image = image_with_relocations([(0xffc, 8, 0x210)], False, False) + page_image[0x200:0x206] = b"\xff\x25" + struct.pack(" Date: Tue, 20 Oct 2026 13:46:34 +0400 Subject: [PATCH 0069/1360] test(relinker): check the page-spanning relocation after Windows moves the image --- core/relinker/relinker/tests/test_none_relocations.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 71aab2c87c..40de19163c 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -133,6 +133,15 @@ def main(): if os.name == "nt": run = subprocess.run([str(page_output)], capture_output=True, timeout=20) assert run.returncode == 42, ("page-crossing relocation", run.returncode, run.stderr) + moved = bytearray(page_output.read_bytes()) + optional_header = struct.unpack_from(" Date: Tue, 20 Oct 2026 13:46:35 +0400 Subject: [PATCH 0070/1360] test(relinker): refuse a base relocation whose eight bytes run past the image --- core/relinker/relinker/tests/test_none_relocations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 40de19163c..3c02647db1 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -142,6 +142,17 @@ def main(): for _ in range(3): run = subprocess.run([str(moved_output)], capture_output=True, timeout=20) assert run.returncode == 42, ("page-crossing relocation after the loader moved the image", run.returncode, run.stderr) + past_end_image = image_with_relocations([(0x1ffc, 8, 0x210)], False, False) + past_end_image.extend(bytes(0x1000)) + struct.pack_into(" Date: Tue, 20 Oct 2026 23:02:34 +1300 Subject: [PATCH 0071/1360] fix(libSceNpSessionSignaling): return the firmware lookup results The group and connection lookups take the firmware signatures, reject a null pointer with 0x80553303 and, because no session ever activates, report the group (0x8055330A) or connection (0x8055330C) as not found, whichever lookup the firmware performs first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../prx/libSceNpSessionSignaling/Export.cpp | 49 ++++++++++++------- core/libs/tests/GuestNpSessionSignaling.cpp | 29 +++++++++++ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 7dfa95ff73..26b9c0dbdd 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -6,6 +6,7 @@ // 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(0x80553303); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND = static_cast(0x8055330A); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast(0x8055330C); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast(0x80552D06); static std::atomic g_nextContext{1}; @@ -50,8 +51,11 @@ int APS5_VABI sceNpSessionSignalingActivateUser(void) { return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(void) { - return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(int32_t context_id, int32_t group_id, const void* peer_address, int32_t* connection_id) { + (void)context_id; + (void)group_id; + if (!peer_address || !connection_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } int APS5_VABI sceNpSessionSignalingTerminate(void) { @@ -89,24 +93,32 @@ int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(void) { return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(int32_t context_id, const void* peer_address, int32_t* connection_id) { + (void)context_id; + if (!peer_address || !connection_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } -int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(int32_t context_id, const void* session_id, int32_t* group_id) { + (void)context_id; + if (!session_id || !group_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } -int APS5_VABI sceNpSessionSignalingGetGroupInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSessionSignalingGetGroupInfo(int32_t context_id, int32_t group_id, void* info) { + (void)context_id; + (void)group_id; + if (!info) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2(int32_t context_id, int32_t group_id, uint32_t address, uint16_t port, int32_t* connection_id) { + (void)context_id; + (void)group_id; + (void)address; + (void)port; + if (!connection_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } int APS5_VABI sceNpSessionSignalingCreateJoinActivate() { @@ -114,9 +126,12 @@ int APS5_VABI sceNpSessionSignalingCreateJoinActivate() { return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress(int32_t context_id, uint32_t address, uint16_t port, int32_t* connection_id) { + (void)context_id; + (void)address; + (void)port; + if (!connection_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } int APS5_VABI sceNpSessionSignalingGetCustomProperty() { diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 27bc077144..9732c43e33 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -10,6 +10,12 @@ int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, std::uint32 std::int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::int32_t contextId, void* info); int APS5_VABI sceNpSessionSignalingRequestPrepare(std::uint32_t contextId, std::uint32_t* requestId); std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::int32_t contextId, std::int32_t connectionId, std::int32_t* status, void* peerAddress, std::uint16_t* peerPort); +std::int32_t APS5_VABI sceNpSessionSignalingGetGroupInfo(std::int32_t contextId, std::int32_t groupId, void* info); +std::int32_t APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(std::int32_t contextId, const void* sessionId, std::int32_t* groupId); +std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress(std::int32_t contextId, std::uint32_t address, std::uint16_t port, std::int32_t* connectionId); +std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2(std::int32_t contextId, std::int32_t groupId, std::uint32_t address, std::uint16_t port, std::int32_t* connectionId); +std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(std::int32_t contextId, const void* peerAddress, std::int32_t* connectionId); +std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(std::int32_t contextId, std::int32_t groupId, const void* peerAddress, std::int32_t* connectionId); int APS5_VABI sceNpSessionSignalingTerminate(void); } @@ -17,6 +23,7 @@ namespace { constexpr int InvalidArgument = static_cast(0x80553303u); constexpr int Unavailable = static_cast(0x80552D06u); +constexpr int GroupNotFound = static_cast(0x8055330Au); constexpr int ConnectionNotFound = static_cast(0x8055330Cu); void Require(bool condition, const char* message) { @@ -57,5 +64,27 @@ int main() { Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, nullptr, nullptr) == ConnectionNotFound, "connection without peer outputs"); Require(status == 0x5a5a5a5a && peerAddress == 0xa5a5a5a5u && peerPort == 0xa5a5, "failed status query wrote an output"); + Require(sceNpSessionSignalingGetGroupInfo(netContextId, 1, nullptr) == InvalidArgument, "null group info"); + Require(sceNpSessionSignalingGetGroupInfo(netContextId, 1, info) == GroupNotFound, "group info without a session"); + Require(std::memcmp(info, untouched, sizeof(info)) == 0, "failed group query modified the info"); + + const unsigned char sessionId[48] = {}; + const unsigned char peer[40] = {}; + std::int32_t found = 0x5a5a5a5a; + Require(sceNpSessionSignalingGetGroupFromSessionId(netContextId, nullptr, &found) == InvalidArgument, "null session id"); + Require(sceNpSessionSignalingGetGroupFromSessionId(netContextId, sessionId, nullptr) == InvalidArgument, "null group id"); + Require(sceNpSessionSignalingGetGroupFromSessionId(netContextId, sessionId, &found) == GroupNotFound, "group from session id without a session"); + Require(sceNpSessionSignalingGetConnectionFromNetAddress(netContextId, 0x0100007fu, 3658, nullptr) == InvalidArgument, "null connection id from net address"); + Require(sceNpSessionSignalingGetConnectionFromNetAddress(netContextId, 0x0100007fu, 3658, &found) == ConnectionNotFound, "connection from net address without a session"); + Require(sceNpSessionSignalingGetConnectionFromNetAddress2(netContextId, 1, 0x0100007fu, 3658, nullptr) == InvalidArgument, "null connection id from group net address"); + Require(sceNpSessionSignalingGetConnectionFromNetAddress2(netContextId, 1, 0x0100007fu, 3658, &found) == GroupNotFound, "connection from group net address without a session"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress(netContextId, nullptr, &found) == InvalidArgument, "null peer address"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress(netContextId, peer, nullptr) == InvalidArgument, "null connection id from peer address"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress(netContextId, peer, &found) == ConnectionNotFound, "connection from peer address without a session"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress2(netContextId, 1, nullptr, &found) == InvalidArgument, "null group peer address"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress2(netContextId, 1, peer, nullptr) == InvalidArgument, "null connection id from group peer address"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress2(netContextId, 1, peer, &found) == GroupNotFound, "connection from group peer address without a session"); + Require(found == 0x5a5a5a5a, "a failed lookup wrote an id"); + Require(sceNpSessionSignalingTerminate() == 0, "terminate failed"); } From 6cd31eeaa5196b0d96adb2b0ca9fe8d617f7f3ff Mon Sep 17 00:00:00 2001 From: hypermist Date: Tue, 20 Oct 2026 23:08:09 +1300 Subject: [PATCH 0072/1360] docs(libSceNpSessionSignaling): record the implemented lookups Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libSceNpSessionSignaling.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index f5f1396a51..89fe32f9c1 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -2,9 +2,11 @@ ### Unknown function info -- [sceNpSessionSignalingGetConnectionFromPeerAddress](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature - [sceNpSessionSignalingGetMemoryInfo](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified -- PPSA31327 imports declared without parameters, signatures unknown: [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling), imported by Yakuza 0 Director's Cut and not called before the main menu. -- [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. + +### Functional + +- [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetGroupFromSessionId, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetConnectionFromNetAddress2, sceNpSessionSignalingGetConnectionFromPeerAddress, sceNpSessionSignalingGetConnectionFromPeerAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - the signatures, the null-pointer result (0x80553303) and the not-found results (group 0x8055330A, connection 0x8055330C) are the console module's. No session ever activates here, so every lookup ends at not-found. The console module's two earlier checks are not reproduced, because neither initialization nor contexts are tracked: an uninitialized library (0x80553301) and an unknown context id (0x80553308) get the same not-found result From 6539598d512ea351dc1404101b212505dd122f8a Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:09:42 +0000 Subject: [PATCH 0073/1360] fix(libSceNgs2): accept system option flag bit 1 The firmware's shared system option validator checks the size and the fields from +0x6c on, not flags at +0x68. Rise of the Ronin (PPSA04540) creates its system with flags 2, which threw. Accept and ignore bit 1; every other bit still throws, now naming the value. --- .../libs/prx/libSceNgs2.native/src/System.cpp | 3 +- .../libSceNgs2.native/tests/Ngs2Render.cpp | 36 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 231a2ea31e..51699ef8d5 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -135,7 +135,8 @@ static Ngs2SystemOption DefaultSystemOption() { static Ngs2SystemOption CheckedSystemOption(const Ngs2SystemOption* option) { if (option == nullptr) return DefaultSystemOption(); if (option->size != sizeof(Ngs2SystemOption)) throw std::invalid_argument("NGS2: unexpected system option size " + std::to_string(option->size)); - if (option->flags != 0) throw std::runtime_error("NGS2: system option flags are not implemented"); + constexpr std::uint32_t knownFlags = 0x2; + if ((option->flags & ~knownFlags) != 0) throw std::runtime_error("NGS2: system option flags " + Ngs2Hex(option->flags) + " are not implemented"); if (option->sample_rate == 0 || option->max_grain_samples < MIN_GRAIN_SAMPLES || option->num_grain_samples == 0 || option->num_grain_samples > option->max_grain_samples) { throw std::invalid_argument("NGS2: invalid system grain or sample rate"); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 510d9b8103..7c59047b35 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -747,6 +747,41 @@ static void TestSampleRate() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static bool SystemOptionThrows(const Ngs2SystemOption& option) { + Ngs2ContextBufferInfo query{}; + try { + sceNgs2SystemQueryBufferSize(&option, &query); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void TestSystemOptionFlags() { + Ngs2SystemOption option{}; + Require(sceNgs2SystemResetOption(&option) == SCE_NGS2_OK && option.flags == 0); + option.flags = 0x2; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2SystemQueryBufferSize(&option, &query) == SCE_NGS2_OK && query.host_buffer_size != 0); + const auto info = Buffer(query); + uintptr_t system = 0; + Require(sceNgs2SystemCreate(&option, &info, &system) == SCE_NGS2_OK && system != 0); + Require(sceNgs2SystemSetGrainSamples(system, Grain) == SCE_NGS2_OK); + const auto master = Mastering(system, 1); + const std::vector pcm{1000, 2000, 3000, 4000}; + const auto sampler = Sampler(system, pcm, 1); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto out = RenderI16(system); + Require(std::equal(pcm.begin(), pcm.end(), out.begin())); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + + for (const std::uint32_t flags : {0x1u, 0x3u, 0x4u, 0x80000000u, 0x80000002u}) { + option.flags = flags; + Require(SystemOptionThrows(option)); + } +} + static void TestUserData() { uintptr_t value = 1; Require(sceNgs2SystemSetUserData(0x1234, 5) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); @@ -1160,6 +1195,7 @@ int main() { TestReverb(); TestReverbSetup(); TestSampleRate(); + TestSystemOptionFlags(); TestUserData(); TestPcmCallbackReset(); TestPcmCallbackRefill(); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 361ee333a9..4e816370fc 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -6,6 +6,8 @@ - [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. +- [NGS2 system option flags](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - `sceNgs2SystemQueryBufferSize`, `sceNgs2SystemCreate` and `sceNgs2SystemCreateWithAllocator` accept `flags` 2 (bit 1) and ignore it. PPSA04540 (Rise of the Ronin) passes it when creating its system. The firmware's shared option validator, as read in [prosper](https://github.com/mattias800/prosper/blob/9b0698b1f51258a52dc2c0d9640e02caa9fcc0fa/prosper/src/hle/audio/hle_audio.cpp#L4155-L4188), checks the size and the fields from +0x6c on but not `flags` at +0x68. What bit 1 selects is unknown. Every other bit still throws, including bit 31, which shadPS4 reads. + - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. - [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. From abb1a032a220b3e4a4114a3c8c1f563a919671d1 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 18 Oct 2026 20:56:18 +0400 Subject: [PATCH 0074/1360] feat(agc): emulate barycentrics in rect-list draws --- .../Execution/src/Driver/Draw/Draw.cpp | 4 +- .../execution/PixelBarycentricEmulation.cpp | 102 +++++++++++------- .../SpirvBackend/src/RectListShaders.cpp | 95 ++++++++++++++-- 3 files changed, 153 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 1dcbe329eb..42f817a16c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -486,8 +486,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for"); } require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader"); - } else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { - require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); + } else if (!graphics.rectList && !programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { + require(graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); const auto geometryLimits = localDevice->GeometryLimits(); if (geometryLimits || !localDevice->Target().tessellation) { results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 9978674554..0fb12a6cd3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -45,11 +45,23 @@ struct Vertex { float w; }; -constexpr std::array Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; +constexpr std::array TriangleCorners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; +constexpr std::array RectangleCorners{{{-0.8f, -0.8f, 1.5f}, {0.7f, -0.8f, 1.5f}, {-0.8f, 0.6f, 1.5f}}}; -std::array, 3> ClipPositions() { +enum class Emulation { + Geometry, + Tessellation, + RectList, +}; + +const std::array& Corners(Emulation emulation) { + return emulation == Emulation::RectList ? RectangleCorners : TriangleCorners; +} + +std::array, 3> ClipPositions(Emulation emulation) { + const auto& corners = Corners(emulation); std::array, 3> positions{}; - for (std::size_t i = 0; i < Corners.size(); ++i) positions[i] = {Corners[i].x * Corners[i].w, Corners[i].y * Corners[i].w, 0.5f * Corners[i].w, Corners[i].w}; + for (std::size_t i = 0; i < corners.size(); ++i) positions[i] = {corners[i].x * corners[i].w, corners[i].y * corners[i].w, 0.5f * corners[i].w, corners[i].w}; return positions; } @@ -58,16 +70,13 @@ std::array BufferDescriptor(const void* data, std::uint32_t st return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; } -enum class Emulation { - Geometry, - Tessellation, -}; - ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; - static const auto positions = ClipPositions(); + static const auto trianglePositions = ClipPositions(Emulation::Geometry); + static const auto rectanglePositions = ClipPositions(Emulation::RectList); + const auto& positions = emulation == Emulation::RectList ? rectanglePositions : trianglePositions; std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast(positions.size())); @@ -104,7 +113,12 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; ShaderRecompiler::RecompileResult geometry; ShaderRecompiler::BarycentricTessellationShaders patches; - if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { + ShaderRecompiler::RectListShaders rectangle; + if (emulation == Emulation::RectList) { + rectangle = ShaderRecompiler::BuildRectListShaders(vertexResult, pixelResult, target); + shaders.push_back({ShaderStage::TessellationControl, &rectangle.control, 0}); + shaders.push_back({ShaderStage::TessellationEvaluation, &rectangle.evaluation, 0}); + } else if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); shaders.push_back({ShaderStage::Geometry, &geometry, 0}); } else if (pixelResult.barycentricEmulation.active) { @@ -120,7 +134,8 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = emulation == Emulation::RectList ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.rectList = emulation == Emulation::RectList; state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; @@ -141,9 +156,10 @@ struct Expected { std::array perspective; }; -Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { +Expected Barycentrics(Emulation emulation, std::uint32_t x, std::uint32_t y, bool snapped) { + const auto& corners = Corners(emulation); std::array, 3> window{}; - for (std::size_t i = 0; i < Corners.size(); ++i) window[i] = {(Corners[i].x + 1.0) * Width / 2.0, (1.0 - Corners[i].y) * Height / 2.0}; + for (std::size_t i = 0; i < corners.size(); ++i) window[i] = {(corners[i].x + 1.0) * Width / 2.0, (1.0 - corners[i].y) * Height / 2.0}; if (snapped) { for (auto& corner : window) { for (auto& coordinate : corner) coordinate = std::round(coordinate * 256.0) / 256.0; @@ -154,22 +170,23 @@ Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; const double area = edge(window[0], window[1], window[2][0], window[2][1]); const std::array b{edge(window[1], window[2], px, py) / area, edge(window[2], window[0], px, py) / area, edge(window[0], window[1], px, py) / area}; - Expected expected{std::min({b[0], b[1], b[2]}) > 0.02, {}, {}}; + const bool inside = emulation == Emulation::RectList ? std::min(b[1], b[2]) > 0.02 && std::max(b[1], b[2]) < 0.98 : std::min({b[0], b[1], b[2]}) > 0.02; + Expected expected{inside, {}, {}}; double sum = 0.0; - for (std::size_t i = 0; i < 3; ++i) sum += b[i] / Corners[i].w; + for (std::size_t i = 0; i < 3; ++i) sum += b[i] / corners[i].w; for (std::size_t i = 0; i < 3; ++i) { expected.linear[i] = static_cast(b[i]); - expected.perspective[i] = static_cast(b[i] / Corners[i].w / sum); + expected.perspective[i] = static_cast(b[i] / corners[i].w / sum); } return expected; } -Expected BarycentricsUnsnapped(std::uint32_t x, std::uint32_t y) { - return Barycentrics(x, y, false); +Expected BarycentricsUnsnapped(Emulation emulation, std::uint32_t x, std::uint32_t y) { + return Barycentrics(emulation, x, y, false); } -Expected BarycentricsSnapped(std::uint32_t x, std::uint32_t y) { - return Barycentrics(x, y, true); +Expected BarycentricsSnapped(Emulation emulation, std::uint32_t x, std::uint32_t y) { + return Barycentrics(emulation, x, y, true); } bool MatchesEither(float value, float unsnapped, float snapped) { @@ -180,13 +197,15 @@ float Pixel(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { return Pixels[(y * Width + x) * 4u + channel]; } -std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) { +std::uint32_t CheckBarycentrics(Emulation emulation, const std::string& what, bool perspective) { std::uint32_t checked = 0; + std::uint32_t beyond = 0; for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - const auto expected = BarycentricsUnsnapped(x, y); + const auto expected = BarycentricsUnsnapped(emulation, x, y); if (!expected.inside) continue; - const auto snapped = BarycentricsSnapped(x, y); + const auto snapped = BarycentricsSnapped(emulation, x, y); + if (expected.linear[0] < -0.02f) ++beyond; const auto& weights = perspective ? expected.perspective : expected.linear; const auto& snappedWeights = perspective ? snapped.perspective : snapped.linear; for (std::uint32_t channel = 0; channel < 4; ++channel) { @@ -199,17 +218,18 @@ std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) { } } Require(checked > 200u, what + ": too few interior pixels were checked"); + Require(emulation != Emulation::RectList || beyond > 200u, what + ": too few pixels past the rectangle's diagonal were checked"); return checked; } -void CheckVertexValues() { - const auto positions = ClipPositions(); +void CheckVertexValues(Emulation emulation) { + const auto positions = ClipPositions(emulation); const auto p0 = positions[0][0]; const std::array expected{positions[1][0] - p0, positions[2][0] - p0, p0}; std::uint32_t checked = 0; for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - if (!BarycentricsUnsnapped(x, y).inside) continue; + if (!BarycentricsUnsnapped(emulation, x, y).inside) continue; for (std::uint32_t channel = 0; channel < 3; ++channel) { Require(Pixel(x, y, channel) == expected[channel], "v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(expected[channel])); } @@ -219,14 +239,14 @@ void CheckVertexValues() { Require(checked > 200u, "v_interp_mov: too few interior pixels were checked"); } -void CheckInterpolated() { - const auto positions = ClipPositions(); +void CheckInterpolated(Emulation emulation) { + const auto positions = ClipPositions(emulation); std::uint32_t checked = 0; for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - const auto expected = BarycentricsUnsnapped(x, y); + const auto expected = BarycentricsUnsnapped(emulation, x, y); if (!expected.inside) continue; - const auto snapped = BarycentricsSnapped(x, y); + const auto snapped = BarycentricsSnapped(emulation, x, y); float reference = 0.0f; float snappedReference = 0.0f; for (std::size_t i = 0; i < 3; ++i) { @@ -257,23 +277,23 @@ void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const st } void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { - const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; + const std::string name = emulation == Emulation::Geometry ? "geometry stage" : emulation == Emulation::Tessellation ? "tessellation stages" : "rect-list stages"; const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); const auto linear = PixelInputBit(PixelInput::LinearCenter); auto result = Draw(device, emulation, BarycentricPixelCode, perspective); Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); - const auto smooth = CheckBarycentrics(name + " PERSP_CENTER I/J", true); + const auto smooth = CheckBarycentrics(emulation, name + " PERSP_CENTER I/J", true); result = Draw(device, emulation, BarycentricPixelCode, linear); Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); - const auto noPerspective = CheckBarycentrics(name + " LINEAR_CENTER I/J", false); + const auto noPerspective = CheckBarycentrics(emulation, name + " LINEAR_CENTER I/J", false); result = Draw(device, emulation, VertexValuePixelCode, perspective); Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); - CheckVertexValues(); + CheckVertexValues(emulation); result = Draw(device, emulation, InterpolatedPixelCode, perspective); Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); - CheckInterpolated(); - CheckWinding(device, emulation, name); - std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); + CheckInterpolated(emulation); + if (emulation != Emulation::RectList) CheckWinding(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation%s\n", name.c_str(), smooth, noPerspective, emulation == Emulation::RectList ? "" : " and face culling"); } } @@ -290,8 +310,12 @@ int main() { } if (geometry) RunEmulation(*device, Emulation::Geometry); else std::puts("geometry stage skipped, the device lacks geometryShader"); - if (tessellation) RunEmulation(*device, Emulation::Tessellation); - else std::puts("tessellation stages skipped, the device lacks tessellationShader"); + if (tessellation) { + RunEmulation(*device, Emulation::Tessellation); + RunEmulation(*device, Emulation::RectList); + } else { + std::puts("tessellation and rect-list stages skipped, the device lacks tessellationShader"); + } return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index bbf6dcc0f6..ed3483d923 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -26,20 +26,32 @@ struct Parameter { bool missing = false; }; +struct PerVertexSource { + std::uint32_t inputLocation; + std::uint32_t outputLocation; +}; + +struct Emulation { + std::vector barycentrics; + std::vector perVertex; +}; + void require(bool condition, const char* message) { if (!condition) throw std::runtime_error(std::string("Rect-list SPIR-V: ") + message); } class RectListEmitter { public: - RectListEmitter(const std::vector& parameters, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding, std::uint32_t clipDistances) : builder(version), faultBinding(faultBinding), version(version), clipDistances(clipDistances), parameters(parameters) { + RectListEmitter(const std::vector& parameters, const Emulation& emulation, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding, std::uint32_t clipDistances) : builder(version), faultBinding(faultBinding), version(version), clipDistances(clipDistances), parameters(parameters), emulation(emulation) { builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); voidType = type(spv::OpTypeVoid); uintType = type(spv::OpTypeInt, 32u, 0u); intType = type(spv::OpTypeInt, 32u, 1u); floatType = type(spv::OpTypeFloat, 32u); + vec3FloatType = type(spv::OpTypeVector, floatType, 3u); vec4FloatType = type(spv::OpTypeVector, floatType, 4u); + vertexArrayType = array(vec4FloatType, 3u); functionType = type(spv::OpTypeFunction, voidType); perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); @@ -51,13 +63,14 @@ public: ptrInputVec4Float = pointer(spv::StorageClassInput, vec4FloatType); ptrOutputVec4Float = pointer(spv::StorageClassOutput, vec4FloatType); + ptrInputVec3Float = pointer(spv::StorageClassInput, vec3FloatType); + ptrOutputVec3Float = pointer(spv::StorageClassOutput, vec3FloatType); if (model == spv::ExecutionModelTessellationControl) { boolType = type(spv::OpTypeBool); vec2BoolType = type(spv::OpTypeVector, boolType, 2u); vec2FloatType = type(spv::OpTypeVector, floatType, 2u); ptrOutputFloat = pointer(spv::StorageClassOutput, floatType); } else { - vec3FloatType = type(spv::OpTypeVector, floatType, 3u); ptrInputFloat = pointer(spv::StorageClassInput, floatType); } } @@ -155,6 +168,22 @@ public: store(access(ptrOutputVec4Float, outputs[i], invocation), value); } + if (!emulation.barycentrics.empty()) { + const auto zero = constant(floatType, 0); + std::array weight {}; + for (std::uint32_t i = 0; i < weight.size(); i++) { + const auto own = result(spv::OpSelect, floatType, result(spv::OpIEqual, boolType, index, intConstant(i)), floatOne, zero); + weight[i] = result(spv::OpSelect, floatType, fourth, barycentric[i], own); + } + const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, weight[0], weight[1], weight[2]); + for (const auto output : barycentricOutputs) store(access(ptrOutputVec3Float, output, invocation), weights); + } + for (std::size_t i = 0; i < emulation.perVertex.size(); i++) { + for (std::uint32_t source = 0; source < 3u; source++) { + store(access(ptrOutputVec4Float, perVertexOutputs[i], invocation, intConstant(source)), load(vec4FloatType, access(ptrInputVec4Float, perVertexInputs[i], intConstant(source)))); + } + } + emit(spv::OpReturn); emit(spv::OpFunctionEnd); return builder.Finalize(); @@ -178,6 +207,8 @@ public: for (std::uint32_t i = 0; i < parameters.size(); i++) { store(outputs[i], load(vec4FloatType, access(ptrInputVec4Float, inputs[i], index))); } + for (std::size_t i = 0; i < barycentricOutputs.size(); i++) store(barycentricOutputs[i], load(vec3FloatType, access(ptrInputVec3Float, barycentricInputs[i], index))); + for (std::size_t i = 0; i < emulation.perVertex.size(); i++) store(perVertexOutputs[i], load(vertexArrayType, access(pointer(spv::StorageClassInput, vertexArrayType), perVertexInputs[i], index))); emit(spv::OpReturn); emit(spv::OpFunctionEnd); @@ -344,6 +375,23 @@ private: decorate(inputs[i], spv::DecorationLocation, location); locations[location] = inputs[i]; } + for (const auto location : emulation.barycentrics) { + if (tessControl) continue; + barycentricInputs.push_back(addInterface(spv::StorageClassInput, array(vec3FloatType, 4u))); + decorate(barycentricInputs.back(), spv::DecorationLocation, location); + } + for (const auto& source : emulation.perVertex) { + if (!tessControl) { + perVertexInputs.push_back(addInterface(spv::StorageClassInput, array(vertexArrayType, 4u))); + decorate(perVertexInputs.back(), spv::DecorationLocation, source.outputLocation); + continue; + } + if (locations[source.inputLocation] == 0) { + locations[source.inputLocation] = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(locations[source.inputLocation], spv::DecorationLocation, source.inputLocation); + } + perVertexInputs.push_back(locations[source.inputLocation]); + } } void defineOutputs(spv::ExecutionModel model) { @@ -367,6 +415,14 @@ private: outputs[i] = addInterface(spv::StorageClassOutput, tessControl ? array(vec4FloatType, 4u) : vec4FloatType); decorate(outputs[i], spv::DecorationLocation, parameters[i].outputLocation); } + for (const auto location : emulation.barycentrics) { + barycentricOutputs.push_back(addInterface(spv::StorageClassOutput, tessControl ? array(vec3FloatType, 4u) : vec3FloatType)); + decorate(barycentricOutputs.back(), spv::DecorationLocation, location); + } + for (const auto& source : emulation.perVertex) { + perVertexOutputs.push_back(addInterface(spv::StorageClassOutput, tessControl ? array(vertexArrayType, 4u) : vertexArrayType)); + decorate(perVertexOutputs.back(), spv::DecorationLocation, source.outputLocation); + } } std::uint32_t interpolate(std::uint32_t v0, std::uint32_t v1, std::uint32_t v2, const std::array& barycentric) { @@ -382,9 +438,14 @@ private: std::uint32_t clipDistances; std::uint32_t fault = 0; const std::vector& parameters; + const Emulation& emulation; std::vector interfaces; std::vector inputs; std::vector outputs; + std::vector barycentricInputs; + std::vector barycentricOutputs; + std::vector perVertexInputs; + std::vector perVertexOutputs; std::uint32_t main = 0; std::uint32_t voidType = 0; std::uint32_t boolType = 0; @@ -395,6 +456,7 @@ private: std::uint32_t vec2FloatType = 0; std::uint32_t vec3FloatType = 0; std::uint32_t vec4FloatType = 0; + std::uint32_t vertexArrayType = 0; std::uint32_t functionType = 0; std::uint32_t perVertexType = 0; std::uint32_t clipArrayType = 0; @@ -404,6 +466,8 @@ private: std::uint32_t ptrInputVec4Float = 0; std::uint32_t ptrOutputFloat = 0; std::uint32_t ptrOutputVec4Float = 0; + std::uint32_t ptrInputVec3Float = 0; + std::uint32_t ptrOutputVec3Float = 0; std::uint32_t glIn = 0; std::uint32_t glOut = 0; std::uint32_t clipIn = 0; @@ -422,25 +486,42 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); std::vector parameters; + Emulation emulation; + BarycentricEmulationLayout layout; + if (fragment.barycentricEmulation.active) { + layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); + for (const auto location : {layout.smoothLocation, layout.linearLocation}) { + if (location != BarycentricEmulationLayout::NoLocation) emulation.barycentrics.push_back(location); + } + } std::set locations; for (const auto& input : fragment.fragmentParameters) { require(input.location < 32 && input.sourceLocation < 32 && locations.insert(input.location).second, "invalid fragment parameter location"); require(!input.custom, "custom per-vertex interpolation is unsupported"); const bool exported = std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), input.sourceLocation) != vertex.parameterExports.end(); + if (fragment.barycentricEmulation.active && input.perVertex) { + require(exported, "an emulated per-vertex parameter is not exported by the vertex shader"); + const auto relocated = std::find_if(layout.perVertexLocations.begin(), layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == input.location; }); + require(relocated != layout.perVertexLocations.end(), "an emulated per-vertex parameter has no location"); + emulation.perVertex.push_back({input.sourceLocation, relocated->second}); + continue; + } parameters.push_back({input.sourceLocation, input.location, input.flat, !exported}); } + const auto emulatedSlots = static_cast(emulation.barycentrics.size() + emulation.perVertex.size() * 3u); const auto& limits = *target.tessellation; const std::uint32_t perVertexComponents = std::min({limits.maxControlPerVertexInputComponents, limits.maxControlPerVertexOutputComponents, limits.maxEvaluationInputComponents, limits.maxEvaluationOutputComponents}); - if (const std::size_t capacity = perVertexComponents / 4u > 0u ? perVertexComponents / 4u - 1u : 0u; parameters.size() > capacity) { + require(emulatedSlots == 0u || perVertexComponents / 4u > emulatedSlots, "emulated barycentrics exceed the tessellation interface limits"); + if (const std::size_t capacity = perVertexComponents / 4u > emulatedSlots ? perVertexComponents / 4u - 1u - emulatedSlots : 0u; parameters.size() > capacity) { parameters.resize(capacity); } - const auto components = static_cast((parameters.size() + 1) * 4); + const auto components = static_cast((parameters.size() + emulatedSlots + 1) * 4); require(limits.maxPatchSize >= 4 && components <= limits.maxControlPerVertexInputComponents && components <= limits.maxControlPerVertexOutputComponents && components <= limits.maxEvaluationInputComponents && components <= limits.maxEvaluationOutputComponents && limits.maxControlPerPatchOutputComponents >= 6 && components * 4 + 6 <= limits.maxControlTotalOutputComponents, "tessellation interface exceeds device limits"); const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); const auto faultBinding = RuntimeAbi::BindingNumber(RuntimeAbi::Stage::TessellationControl, RuntimeAbi::Binding::FaultBuffer); - RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); - RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter control(parameters, emulation, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter evaluation(parameters, emulation, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding, clipDistances); RectListShaders shaders; shaders.control.spirv = control.EmitControl(0u); shaders.control.bindings.push_back({DescriptorKind::StorageBuffer, DescriptorRole::FaultBuffer, 0, faultBinding, 1, {}}); @@ -452,7 +533,7 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp #endif shaders.lastVertexControl = shaders.control; if (std::any_of(parameters.begin(), parameters.end(), [](const Parameter& parameter) { return parameter.flat && !parameter.missing; })) { - RectListEmitter lastVertexControl(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter lastVertexControl(parameters, emulation, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); shaders.lastVertexControl.spirv = lastVertexControl.EmitControl(2u); #if ANYPS5_ENABLE_SPIRV_TOOLS shaders.lastVertexControl.spirv = ValidateAndOptimizeSpirv(shaders.lastVertexControl.spirv, target.vulkanVersion, target.spirvVersion); From 9b84f9d5a191850853785d9a9b2a1d71648dc310 Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Mon, 19 Oct 2026 22:07:30 +0300 Subject: [PATCH 0075/1360] fix(recompiler): replay stencil exports of pixel shaders that only read through BDA StencilReplaySafe refused every program with usesDma, including pixel shaders whose only BDA accesses are reads (Onimusha: Way of the Sword, PPSA27836: one global load, then image and buffer reads). Replaying such a shader per reference value is idempotent: its only side effect is the first-fault record, a compare-exchange that keeps the first fault; a fault is never retried but thrown by CheckFault, and replayed draws with a BDA table already wait and check faults before publishing. Written-page tracking exists only in programs with bdaWrites, which stay refused. Compiled shaders now carry bdaWrites, and ShadersWriteMemory counts a BDA page table as a memory write only for a program that writes through it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 +++- core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp | 8 ++++++++ core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/Recompiler.cpp | 2 ++ core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/StencilReplay.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 8 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index eaca4be3f4..610112905c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -154,8 +154,10 @@ bool ShadersWriteMemory(std::span shaders) { if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && any(binding.imageWritten, binding.count)) return true; break; case ShaderRecompiler::DescriptorRole::Gds: - case ShaderRecompiler::DescriptorRole::BdaPagetable: return true; + case ShaderRecompiler::DescriptorRole::BdaPagetable: + if (shader.program->bdaWrites) return true; + break; case ShaderRecompiler::DescriptorRole::GuestSamplers: case ShaderRecompiler::DescriptorRole::FaultBuffer: case ShaderRecompiler::DescriptorRole::FlattenedSrt: diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index ff2a3e40a7..f4b855ef84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1066,6 +1066,14 @@ int main() { return StencilReplaySafe(b.program); }); + passed &= run("read-only BDA reads can be replayed and BDA writes cannot", [] { + Builder b; + b.program.Info().usesDma = true; + if (!StencilReplaySafe(b.program)) return false; + b.program.Info().bdaWrites = true; + return !StencilReplaySafe(b.program); + }); + passed &= run("only guarded read-only SRT diagnostics can be replayed", [] { Builder b; b.program.Info().usesFaultBuffer = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index c86a452b79..b99f4abb0f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -111,6 +111,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ"); require(left.stencilReferences == right.stencilReferences, prefix + "stencil reference set differs"); require(left.stencilReplaySafe == right.stencilReplaySafe, prefix + "stencil replay proof differs"); + require(left.bdaWrites == right.bdaWrites, prefix + "BDA write flag differs"); require(left.stencilReferenceDword == right.stencilReferenceDword, prefix + "stencil replay push offset differs"); require(left.fragmentParameters.size() == right.fragmentParameters.size(), prefix + "fragment parameter count differs"); for (std::size_t i = 0; i < left.fragmentParameters.size(); ++i) { @@ -221,6 +222,7 @@ RecompileResult sampleResult() { result.fragmentParameters = {{0, 1, true, false, true}, {2, 3, false, true}}; result.stencilReferences = {0u, 17u, 255u}; result.stencilReplaySafe = true; + result.bdaWrites = true; result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..ce7793b96c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -626,6 +626,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.registrationModule = registration; if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program); + result.bdaWrites = program.Info().bdaWrites; if (stencilReplay) { result.stencilReferences = StencilReferenceValues(program, request.context.pixel.has_value() && request.context.pixel->stencilOperationValueExport ? 8u : 0u); result.stencilReferenceDword = bindings.layout.stencilReferenceDword; @@ -972,6 +973,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .meshBarycentrics = artifact.meshBarycentrics, .stencilReferences = artifact.stencilReferences, .stencilReplaySafe = artifact.stencilReplaySafe, + .bdaWrites = artifact.bdaWrites, .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..d77c2dbbe8 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -665,6 +665,7 @@ struct CompiledShaderArtifact { std::optional meshBarycentrics; std::vector stencilReferences; bool stencilReplaySafe = false; + bool bdaWrites = false; std::uint32_t stencilReferenceDword = 0xffffffffu; std::uint64_t variantId = 0; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3a3d947b8c..f2a4683a1f 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -520,6 +520,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { } writer.Values(std::span(result.stencilReferences)); writer.Value(result.stencilReplaySafe); + writer.Value(result.bdaWrites); writer.Value(result.stencilReferenceDword); } @@ -582,6 +583,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { } reader.Values(result.stencilReferences); reader.Value(result.stencilReplaySafe); + reader.Value(result.bdaWrites); reader.Value(result.stencilReferenceDword); result.variantId = 0; } diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index fae2057358..6ed182542f 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -692,7 +692,7 @@ bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program) { } bool StencilReplaySafe(const IrProgram& program) { - if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || program.Info().usesDma || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; + if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; if (program.Info().usesFaultBuffer && !FaultControlFlowAcyclic(program)) return false; for (const auto& input : program.Info().inputs) if (input.kind == StageInputKind::PrimitiveId) return false; bool masks = false; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f756ed2ade..0085c77723 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -63,7 +63,7 @@ - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From 1713855fb6d019b3a1fa5dc99a94e719dda0355d Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 14:22:01 +0400 Subject: [PATCH 0076/1360] docs(debt): record barycentric emulation in rect-list draws --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2dee38e74e..31589adf92 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -93,7 +93,7 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages, checked on MoltenVK 1.4.2 (Apple M2) only. Tessellation and mesh pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. From 9d4c7cf42b48c40dfa8265025f2a022f1c8244e7 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 10:10:38 +0000 Subject: [PATCH 0077/1360] fix(shader): share the words of identical specialized modules specializeModule caches a module per artifact variant and constant set, so artifacts whose SPIR-V is the same and that get the same constants each build and keep their own copy of the same specialized words. The built words are now interned by content (both content hashes, then the words themselves), and later equal modules share one copy. Each module keeps its own specialization ID. In Crimson Desert, 4500 specialized modules built in the first 235 s held 916.6 MiB of words, of which 133.5 MiB were distinct. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 19 +++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 16 +++++++++++++++- core/shader/recompiler/Recompiler.hpp | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 1be75623e3..9f545033da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -1,5 +1,7 @@ #include "SpirvBackend/SpirvModule.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" +#include "Recompiler.hpp" +#include #include #include #include @@ -356,6 +358,22 @@ void testBlockBallotsOfOnePredicateShareOneBallot() { check(ballotSources.contains(afterLabel), "a ballot in a later block reused one from an earlier block"); } +void testInternedSpirvSharesEqualWords() { + const std::vector words{spv::MagicNumber, 0x00010400u, 0u, 7u, 0u, (2u << 16u) | spv::OpCapability, spv::CapabilityShader}; + auto changed = words; + changed.back() = spv::CapabilityInt64; + const auto first = InternSpirv(words); + const auto second = InternSpirv(words); + const auto other = InternSpirv(changed); + check(std::ranges::equal(first.Words(), words) && std::ranges::equal(other.Words(), changed), "interned SPIR-V changed its words"); + check(first.data() == second.data(), "two equal specialized modules kept separate copies of their words"); + check(first.data() != other.data(), "different SPIR-V was interned into one copy"); + auto edited = second; + edited[6] = spv::CapabilityFloat64; + const auto again = InternSpirv(words); + check(edited[6] == spv::CapabilityFloat64 && first[6] == spv::CapabilityShader && again.data() == first.data(), "a write to one holder of interned SPIR-V changed the others"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); @@ -371,6 +389,7 @@ int main() { testIdsStayUniqueBeyondCacheCapacity(); testEmittedWordsAreStable(); testBlockBallotsOfOnePredicateShareOneBallot(); + testInternedSpirvSharesEqualWords(); if (failures != 0) { std::fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..4e02bd10c5 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -808,7 +808,7 @@ std::shared_ptr buildSpecializedModule(const CompiledSh } cursor += count; } - module->spirv = std::move(materialized); + module->spirv = InternSpirv(std::move(materialized)); module->specializationId = constants.empty() ? 0u : nextVariantId(); return module; } @@ -1129,6 +1129,20 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { } +SharedSpirv InternSpirv(std::vector words) { + SharedSpirv spirv(std::move(words)); + const auto hash = spirv.ContentHash(); + static std::mutex mutex; + static std::unordered_map> interned; + std::lock_guard lock(mutex); + auto& candidates = interned[hash.low ^ (hash.high * 0x9e3779b97f4a7c15ull)]; + for (const auto& candidate : candidates) { + if (candidate.ContentHash() == hash && candidate == spirv) return candidate; + } + candidates.push_back(spirv); + return spirv; +} + MeshOutputBudget MeshBarycentricOutputBudget(const MeshConfiguration& mesh, const MeshTargetLimits& limits, const MeshOutputUse& use) { const auto alignUp = [](std::uint64_t value, std::uint64_t granularity) { return granularity <= 1u ? value : (value + granularity - 1u) / granularity * granularity; }; MeshOutputBudget budget; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..ee81dc83b1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -622,6 +622,8 @@ private: std::shared_ptr holder; }; +[[nodiscard]] SharedSpirv InternSpirv(std::vector words); + struct VertexInput { std::uint32_t location; std::uint32_t components; From ad7e1acdb6441cf726fdb5a35ac30589c83c49e4 Mon Sep 17 00:00:00 2001 From: Evrim Cicek Date: Tue, 20 Oct 2026 13:20:09 +0300 Subject: [PATCH 0078/1360] fix(libkernel): append positioned writes on an O_APPEND descriptor on Windows On Windows, sceKernelPwrite, pwrite, sceKernelPwritev, pwritev and the AIO write commands wrote at the given offset on a descriptor opened with O_APPEND. FreeBSD's vn_write sets IO_APPEND for an O_APPEND regular file whether or not an offset is given, the filesystem then writes at the end of the file, and the file offset is left unchanged. Write at Windows' end-of-file offset in that case, and gather a multi-buffer pwritev into one write as writev already does. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 10 ++++---- core/libs/tests/GuestAio.cpp | 16 +++++++++++++ core/libs/tests/GuestAppendWrites.cpp | 28 ++++++++++++++++++---- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d786eb038..d73013c61d 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -256,9 +256,10 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz errno = ESPIPE; return -1; } + const auto at = write && File::Appends(descriptor) ? ~std::uint64_t{} : static_cast(offset); OVERLAPPED overlapped{}; - overlapped.Offset = static_cast(offset); - overlapped.OffsetHigh = static_cast(static_cast(offset) >> 32u); + overlapped.Offset = static_cast(at); + overlapped.OffsetHigh = static_cast(at >> 32u); DWORD done = 0; const BOOL ok = write ? ::WriteFile(handle, buf, static_cast(nbytes), &done, &overlapped) : ::ReadFile(handle, buf, static_cast(nbytes), &done, &overlapped); @@ -1251,14 +1252,15 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co return result < 0 ? SceErrorFromErrno(errno) : 0; } if (!write && offset == nullptr && File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); - if (write && offset == nullptr && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { + if (write && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { std::vector gathered; gathered.reserve(total); for (int i = 0; i < iovcnt; ++i) { const auto* base = static_cast(iov[i].base); gathered.insert(gathered.end(), base, base + iov[i].length); } - const auto result = NativeTransferBuffer(d, gathered.data(), gathered.size(), true); + const auto result = offset != nullptr ? NativePositionedTransfer(d, gathered.data(), gathered.size(), *offset, true) + : NativeTransferBuffer(d, gathered.data(), gathered.size(), true); return result < 0 ? SceErrorFromErrno(errno) : result; } const bool whole = write || offset != nullptr || NativeIsDisk(d); diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index cad5b3c0b7..161b1e0039 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -395,5 +395,21 @@ int main(int argc, char** argv) { for (auto& reader : readers) reader.join(); Check(!misread); Check(sceKernelClose(sharedFd) == 0); + const auto appendFile = root / "append.bin"; + { std::ofstream stream(appendFile, std::ios::binary); stream << seed; } + const int appendFd = sceKernelOpen(appendFile.string().c_str(), SCE_KERNEL_O_RDWR | SCE_KERNEL_O_APPEND, 0); + Check(appendFd >= 0); + std::array appendBuffer = {'T', 'A', 'I', 'L'}; + KernelAioResult appendResult{-1, 0}; + KernelAioRwRequest appendRequest{0, appendBuffer.size(), appendBuffer.data(), &appendResult, appendFd}; + std::int32_t appendId = 0; + Check(sceKernelAioSubmitWriteCommands(&appendRequest, 1, 0, &appendId) == 0); + std::int32_t appendState = 0; + Check(sceKernelAioWaitRequest(appendId, &appendState, nullptr) == 0); + Check(appendState == 3 && appendResult.state == 3 && appendResult.return_value == 4); + Check(sceKernelClose(appendFd) == 0); + { std::ifstream stream(appendFile, std::ios::binary); + std::string contents{std::istreambuf_iterator(stream), std::istreambuf_iterator()}; + Check(contents == seed + "TAIL"); } Check(std::filesystem::remove_all(root) > 0); } diff --git a/core/libs/tests/GuestAppendWrites.cpp b/core/libs/tests/GuestAppendWrites.cpp index 0780e303eb..7184c45d5a 100644 --- a/core/libs/tests/GuestAppendWrites.cpp +++ b/core/libs/tests/GuestAppendWrites.cpp @@ -22,8 +22,10 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t); int APS5_VABI dup_nid_postfix(int); } @@ -40,7 +42,7 @@ static constexpr int CreateWriteAppend = 0x0209; static constexpr int Truncate = 0x0400; static constexpr int FromStart = 0; static constexpr int FromCurrent = 1; -static constexpr int Writers = 4; +static constexpr int Writers = 5; static constexpr int LinesPerWriter = 1500; static constexpr std::size_t LineLength = 32; @@ -58,9 +60,12 @@ static void WriteLines(int fd, int writer) { written = sceKernelWrite(fd, line.data(), line.size()); } else if (writer == 1) { written = write_nid_postfix(fd, line.data(), static_cast(line.size())); - } else { + } else if (writer == 3) { const GuestIovec parts[2]{{line.data(), 7}, {line.data() + 7, line.size() - 7}}; written = writev_nid_postfix(fd, parts, 2); + } else { + const GuestIovec parts[2]{{line.data(), 7}, {line.data() + 7, line.size() - 7}}; + written = pwritev_nid_postfix(fd, parts, 2, 0); } Require(written == static_cast(LineLength)); } @@ -77,9 +82,10 @@ int main() { const int second = sceKernelOpen(Path, CreateWriteAppend, 0644); const int duplicate = dup_nid_postfix(first); const int vectored = sceKernelOpen(Path, CreateWriteAppend, 0644); - Require(second >= 0 && duplicate >= 0 && vectored >= 0); + const int positionedVector = sceKernelOpen(Path, CreateWriteAppend, 0644); + Require(second >= 0 && duplicate >= 0 && vectored >= 0 && positionedVector >= 0); - const int descriptors[Writers]{first, second, duplicate, vectored}; + const int descriptors[Writers]{first, second, duplicate, vectored, positionedVector}; std::vector threads; for (int writer = 0; writer < Writers; ++writer) threads.emplace_back(WriteLines, descriptors[writer], writer); for (auto& thread : threads) thread.join(); @@ -105,6 +111,20 @@ int main() { Require(sceKernelLseek(first, 0, FromCurrent) == size + static_cast(LineLength)); Require(Contents().substr(static_cast(size)) == tail); + const std::string head = Contents().substr(0, LineLength); + const std::string positioned = Line(9, 1); + Require(sceKernelLseek(first, 0, FromStart) == 0); + Require(sceKernelPwrite(first, positioned.data(), positioned.size(), 0) == static_cast(LineLength)); + Require(sceKernelLseek(first, 0, FromCurrent) == 0); + const std::string gathered = Line(9, 2); + const GuestIovec pieces[2]{{gathered.data(), 7}, {gathered.data() + 7, gathered.size() - 7}}; + Require(pwritev_nid_postfix(first, pieces, 2, static_cast(LineLength)) == static_cast(LineLength)); + Require(sceKernelLseek(first, 0, FromCurrent) == 0); + const std::string appended = Contents(); + Require(appended.size() == static_cast(size) + 3 * LineLength); + Require(appended.substr(0, LineLength) == head); + Require(appended.substr(static_cast(size) + LineLength) == positioned + gathered); + for (const int fd : descriptors) Require(sceKernelClose(fd) == 0); std::remove(Path); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 880a0832bf..abb9a329de 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -134,7 +134,7 @@ - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is the one exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space -- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done +- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store From c06b6f00cf901f0ea56e02d514e202204345c4e0 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 08:23:47 -0300 Subject: [PATCH 0079/1360] test(pad): set the input mapping path through the wide environment on Windows Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceVideoOut/tests/InputMapping.cpp | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 3497443fcc..6b0c4348ed 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -18,6 +18,14 @@ void require(bool condition, const char* message) { std::filesystem::path root; +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + void useConfig(const std::string& text) { const auto path = root / "anyps5-input.ini"; { @@ -25,11 +33,7 @@ void useConfig(const std::string& text) { file << text; require(file.good(), "write the input mapping"); } -#ifdef _WIN32 - require(_putenv_s("ANYPS5_INPUT_CONFIG", path.string().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); -#else - require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); -#endif + setConfigPath(path); } bool rejects(const std::string& text) { @@ -140,14 +144,6 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } -void setConfigPath(const std::filesystem::path& path) { -#ifdef _WIN32 - require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); -#else - require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); -#endif -} - void nonAsciiPaths() { const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); const auto directory = root / std::filesystem::path(name); From 162a705447b5fb7f8bb16ea0e4033b2cf910fb73 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 08:42:54 -0300 Subject: [PATCH 0080/1360] fix(libSceFont): open fonts and the system font directory under non-ASCII paths on Windows Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/SystemFont.cpp | 6 +++- core/libs/prx/libSceFontFt/src/Driver.cpp | 40 ++++++++++++++++++++- core/libs/tests/GuestFont.cpp | 4 +-- core/libs/tests/GuestSystemFont.cpp | 6 ++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index f164d86462..1c4ab4d7d9 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -121,8 +121,12 @@ std::vector Font::SystemFontCandidates(std::uint32_t fontS } std::filesystem::path Font::SystemFontDirectory() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_SYSTEM_FONTS"); +#else const char* configured = std::getenv("ANYPS5_SYSTEM_FONTS"); - if (!configured || configured[0] == '\0') return ExecutableDirectory() / "anyps5-fonts"; +#endif + if (!configured || configured[0] == 0) return ExecutableDirectory() / "anyps5-fonts"; const std::filesystem::path directory(configured); std::error_code error; if (!std::filesystem::is_directory(directory, error)) throw std::runtime_error("libSceFont: ANYPS5_SYSTEM_FONTS is not a directory: " + directory.string()); diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 7f49954e0a..ee313ce475 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include @@ -193,6 +195,42 @@ int APS5_VABI LibrarySupport(FontLibNative* library, std::uint32_t formats) { return SCE_FONT_OK; } +#ifdef _WIN32 +unsigned long ReadWideFile(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count) { + auto* file = static_cast(stream->descriptor.pointer); + if (_fseeki64(file, static_cast(offset), SEEK_SET) != 0) return count == 0 ? 1 : 0; + return count == 0 ? 0 : static_cast(std::fread(buffer, 1, count, file)); +} + +void CloseWideFile(FT_Stream stream) { + std::fclose(static_cast(stream->descriptor.pointer)); + delete stream; +} + +FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { + std::FILE* file = _wfopen(std::filesystem::path(reinterpret_cast(path)).c_str(), L"rb"); + if (!file) return FT_Err_Cannot_Open_Resource; + const long long size = _fseeki64(file, 0, SEEK_END) == 0 ? _ftelli64(file) : -1; + if (size < 0 || static_cast(size) > std::numeric_limits::max()) { + std::fclose(file); + return FT_Err_Cannot_Open_Stream; + } + auto* stream = new FT_StreamRec{}; + stream->size = static_cast(size); + stream->descriptor.pointer = file; + stream->read = &ReadWideFile; + stream->close = &CloseWideFile; + FT_Open_Args args{}; + args.flags = FT_OPEN_STREAM; + args.stream = stream; + return FT_Open_Face(library, &args, subFontIndex, face); +} +#else +FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { + return FT_New_Face(library, path, subFontIndex, face); +} +#endif + int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj) { (void)uniqueWord; if (!library || !fontAddress || !inoutFontObj) return SCE_FONT_ERROR_INVALID_PARAMETER; @@ -217,7 +255,7 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const if (fromMemory) { error = FT_New_Memory_Face(*ftLibrary, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); } else { - error = FT_New_Face(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); + error = OpenFileFace(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); } if (error != 0 || !face) { if (fromMemory || error == FT_Err_Unknown_File_Format) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 390074ca1e..3bb7a5b0bb 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -85,9 +85,9 @@ static void Check(bool value, int line) { static void SetFontDirectory(const std::filesystem::path& directory) { #ifdef _WIN32 - Require(_putenv_s("ANYPS5_SYSTEM_FONTS", directory.string().c_str()) == 0); + Require(_wputenv_s(L"ANYPS5_SYSTEM_FONTS", directory.wstring().c_str()) == 0); #else - Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.string().c_str(), 1) == 0); + Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.c_str(), 1) == 0); #endif } diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index ec2f807855..1b515c757f 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -112,9 +112,9 @@ static void WriteFile(const std::filesystem::path& path, const std::vector Date: Wed, 21 Oct 2026 00:45:29 +1300 Subject: [PATCH 0081/1360] feat(libc): implement the Dinkumware _Stod, _Stoll, _Sin and _Log helpers _Stod applies its power-of-ten scale to the decimal exponent before rounding, _Stoll is strtoll, and _Sin, _FSin, _LSin, _Log and _FLog pick the public function the firmware dispatches to. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/CMakeLists.txt | 10 +++ .../libs/prx/libc/src/DinkumwareInternals.cpp | 89 +++++++++++++++++++ core/libs/tests/GuestDinkumwareInternals.cpp | 84 +++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 core/libs/prx/libc/src/DinkumwareInternals.cpp create mode 100644 core/libs/tests/GuestDinkumwareInternals.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ca39154516..bead8a8d25 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -48,6 +48,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareInternals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AtomicBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp @@ -447,6 +448,15 @@ if(BUILD_TESTING) set_tests_properties(guest_dinkumware_atomics PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_dinkumware_internals_tests ${LIBS_INCLUDE_DIR}/tests/GuestDinkumwareInternals.cpp) + target_include_directories(guest_dinkumware_internals_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dinkumware_internals_tests PRIVATE libc) + configure_windows_unwind(guest_dinkumware_internals_tests) + add_test(NAME guest_dinkumware_internals COMMAND guest_dinkumware_internals_tests) + if(WIN32) + set_tests_properties(guest_dinkumware_internals PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_hostname_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostname.cpp) target_include_directories(guest_hostname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_hostname_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp new file mode 100644 index 0000000000..82af11ba5e --- /dev/null +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI strtod_nid_postfix(const char* str, char** endptr); +long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base); +double APS5_VABI sin_nid_postfix(double x); +double APS5_VABI cos_nid_postfix(double x); +float APS5_VABI sinf_nid_postfix(float x); +float APS5_VABI cosf_nid_postfix(float x); +long double APS5_VABI sinl_nid_postfix(long double x); +long double APS5_VABI cosl_nid_postfix(long double x); +double APS5_VABI log_nid_postfix(double x); +double APS5_VABI log10_nid_postfix(double x); +double APS5_VABI log2_nid_postfix(double x); +float APS5_VABI logf_nid_postfix(float x); +float APS5_VABI log10f_nid_postfix(float x); +float APS5_VABI log2f_nid_postfix(float x); +} + +namespace { + +constexpr long long ExponentLimit = 1000000000; + +bool Hexadecimal(const char* text) { + while (std::isspace(static_cast(*text))) ++text; + if (*text == '+' || *text == '-') ++text; + return text[0] == '0' && (text[1] == 'x' || text[1] == 'X'); +} + +} + +extern "C" { + +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten) { + const int saved = *__error_nid_postfix(); + char* stop = nullptr; + const double value = strtod_nid_postfix(text, &stop); + if (end != nullptr) *end = stop; + if (pten == 0 || stop == text || !std::isfinite(value)) return value; + if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); + std::string number(text, static_cast(stop)); + long long exponent = 0; + const auto marker = number.find_first_of("eE"); + if (marker != std::string::npos) { + exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); + number.resize(marker); + } + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + number += 'e'; + number += std::to_string(exponent); + *__error_nid_postfix() = saved; + return strtod_nid_postfix(number.c_str(), nullptr); +} + +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base) { + return strtoll_nid_postfix(text, end, base); +} + +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant) { + return quadrant != 0 ? cos_nid_postfix(x) : sin_nid_postfix(x); +} + +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant) { + return quadrant != 0 ? cosf_nid_postfix(x) : sinf_nid_postfix(x); +} + +long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant) { + return quadrant != 0 ? cosl_nid_postfix(x) : sinl_nid_postfix(x); +} + +double APS5_VABI _Log_nid_postfix(double x, int base) { + if (base > 0) return log10_nid_postfix(x); + return base == 0 ? log_nid_postfix(x) : log2_nid_postfix(x); +} + +float APS5_VABI _FLog_nid_postfix(float x, int base) { + if (base > 0) return log10f_nid_postfix(x); + return base == 0 ? logf_nid_postfix(x) : log2f_nid_postfix(x); +} + +} diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp new file mode 100644 index 0000000000..ccd3e04276 --- /dev/null +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -0,0 +1,84 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten); +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); +long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant); +double APS5_VABI _Log_nid_postfix(double x, int base); +float APS5_VABI _FLog_nid_postfix(float x, int base); +} + +namespace { + +constexpr int RangeError = 34; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Dinkumware internals: %s\n", message); + std::exit(1); + } +} + +void CheckStod(const char* text, long pten, double expected, long consumed, int error, const char* message) { + *__error_nid_postfix() = 0; + char* end = nullptr; + const double value = _Stod_nid_postfix(text, &end, pten); + const bool same = std::isnan(expected) ? std::isnan(value) : value == expected && std::signbit(value) == std::signbit(expected); + Require(same && end - text == consumed && *__error_nid_postfix() == error, message); +} + +void TestStod() { + CheckStod("1.5", 0, 1.5, 3, 0, "unscaled decimal"); + CheckStod("1.5", 1, 15.0, 3, 0, "decimal scaled up"); + CheckStod("12", -2, 0.12, 2, 0, "decimal scaled down"); + CheckStod(" -2.25e3xyz", 300, -0x1.a3f7f806a8135p+1007, 9, 0, "the scale is applied before rounding"); + CheckStod("1e-400", 300, 1e-100, 6, 0, "an underflowing text scaled back into range"); + CheckStod("1e-400", 0, 0.0, 6, RangeError, "an unscaled underflow"); + CheckStod("1e300", 100, HUGE_VAL, 5, RangeError, "a scale that overflows"); + CheckStod("1e300", 8, 1e308, 5, 0, "a scale up to the largest decades"); + CheckStod("5e-324", -1, 0.0, 6, RangeError, "a scale that underflows to zero"); + CheckStod("1", 2147483647, HUGE_VAL, 1, RangeError, "the largest scale"); + CheckStod("1e-2147483648", -1, 0.0, 13, RangeError, "the smallest exponent and scale"); + CheckStod("-0", 5, -0.0, 2, 0, "negative zero keeps its sign"); + CheckStod("infinity", 3, HUGE_VAL, 8, 0, "infinity is not scaled"); + CheckStod("nan", 3, NAN, 3, 0, "NaN is not scaled"); + CheckStod("1e", 2, 100.0, 1, 0, "an exponent marker without digits"); + CheckStod(".5", 2, 50.0, 2, 0, "a fraction without integer digits"); + CheckStod("0.3", -1, 0.03, 3, 0, "0.3 scaled down rounds as 0.03"); + CheckStod("123456789012345678901234567890", -20, 1234567890.1234567, 30, 0, "a long digit string"); + CheckStod("abc", 4, 0.0, 0, 0, "no number"); + CheckStod("0x1.8p0", 0, 1.5, 7, 0, "unscaled hexadecimal"); +} + +void TestStoll() { + char* end = nullptr; + *__error_nid_postfix() = 0; + Require(_Stoll_nid_postfix("-0x1F", &end, 0) == -31 && std::strcmp(end, "") == 0, "_Stoll base detection"); + Require(_Stoll_nid_postfix("+42abc", &end, 10) == 42 && std::strcmp(end, "abc") == 0, "_Stoll stops at the first non-digit"); + Require(_Stoll_nid_postfix(" z", &end, 36) == 35, "_Stoll base 36"); + Require(*__error_nid_postfix() == 0, "_Stoll set errno without an overflow"); + Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); +} + +void TestDispatch() { + Require(_Sin_nid_postfix(0.5, 0) == std::sin(0.5) && _Sin_nid_postfix(0.5, 1) == std::cos(0.5) && _Sin_nid_postfix(0.5, 7) == std::cos(0.5), "_Sin"); + Require(_FSin_nid_postfix(0.5f, 0) == std::sin(0.5f) && _FSin_nid_postfix(0.5f, 2) == std::cos(0.5f), "_FSin"); + Require(_LSin_nid_postfix(0.5L, 0) == std::sin(0.5L) && _LSin_nid_postfix(0.5L, 3) == std::cos(0.5L), "_LSin"); + Require(_Log_nid_postfix(8.0, 0) == std::log(8.0) && _Log_nid_postfix(1000.0, 1) == 3.0 && _Log_nid_postfix(1000.0, 5) == 3.0 && _Log_nid_postfix(8.0, -1) == 3.0, "_Log"); + Require(_FLog_nid_postfix(8.0f, 0) == std::log(8.0f) && _FLog_nid_postfix(100.0f, 1) == 2.0f && _FLog_nid_postfix(8.0f, -3) == 3.0f, "_FLog"); +} + +} + +int main() { + TestStod(); + TestStoll(); + TestDispatch(); +} From 1f5ab519b3fe76270239119a818561a52b27b819 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 08:46:41 -0300 Subject: [PATCH 0082/1360] test(libs): keep non-ASCII temporary paths out of narrow Windows calls in guest tests Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFileLock.cpp | 10 ++++--- core/libs/tests/GuestHeapReportForGame.cpp | 6 +++- core/libs/tests/GuestSystemExit.cpp | 33 +++++++++++++--------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 3fb7ac7b71..8aab1a8f9c 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -53,7 +53,8 @@ static bool Acquires(int descriptor, int mode) { struct Fixture { static inline unsigned serial = 0; - std::string path = (std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin")).string(); + std::filesystem::path native = std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin"); + std::string path = native.string(); std::set descriptors; int source = Open(true); int other = Open(false); @@ -76,7 +77,7 @@ struct Fixture { } ~Fixture() { for (const int descriptor : descriptors) Require(close_nid_postfix(descriptor) == 0); - Require(std::remove(path.c_str()) == 0); + Require(std::filesystem::remove(native)); } }; @@ -262,7 +263,7 @@ static int ChildTry(const Fixture (&fixtures)[6], int mode) { wchar_t executable[32768]; Require(::GetModuleFileNameW(nullptr, executable, 32768) != 0); std::wstring command = L"\"" + std::wstring(executable) + L"\" child " + std::to_wstring(mode | 4); - for (const Fixture& fixture : fixtures) command += L" \"" + std::filesystem::path(fixture.path).wstring() + L"\""; + for (const Fixture& fixture : fixtures) command += L" \"" + fixture.native.filename().wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -300,7 +301,8 @@ int main(int argc, char** argv) { if (argc >= 4 && std::string(argv[1]) == "child") { int busy = 0; for (int index = 3; index < argc; ++index) { - const int descriptor = open_nid_postfix(argv[index], 2, 0); + const std::string path = (std::filesystem::temp_directory_path() / argv[index]).string(); + const int descriptor = open_nid_postfix(path.c_str(), 2, 0); Require(descriptor >= 0); if (flock_nid_postfix(descriptor, std::atoi(argv[2])) == 0) { Require(flock_nid_postfix(descriptor, 8) == 0); diff --git a/core/libs/tests/GuestHeapReportForGame.cpp b/core/libs/tests/GuestHeapReportForGame.cpp index 9238d7bdb1..de3ce98250 100644 --- a/core/libs/tests/GuestHeapReportForGame.cpp +++ b/core/libs/tests/GuestHeapReportForGame.cpp @@ -20,7 +20,11 @@ static void Require(bool value, const char* message) { int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-heap-report-" + std::to_string(std::rand()) + ".txt"); - Require(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr redirection failed"); +#ifdef _WIN32 + Require(_wfreopen(path.wstring().c_str(), L"w", stderr) != nullptr, "stderr redirection failed"); +#else + Require(std::freopen(path.c_str(), "w", stderr) != nullptr, "stderr redirection failed"); +#endif char heapName[] = "SceLibcHeap"; sceLibcInternalBacktraceForGame_nid_postfix(heapName); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 93ff93b094..1d8e6161cf 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -70,14 +70,24 @@ void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); #endif constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER"; constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE"; -void SetVariable(const char* name, const char* value) { +void SetVariable(const char* name, const std::filesystem::path& value) { #ifdef _WIN32 - Require(SetEnvironmentVariableA(name, value) != 0); - Require(_putenv_s(name, value) == 0); + const std::wstring wideName = std::filesystem::path(name).wstring(); + Require(SetEnvironmentVariableW(wideName.c_str(), value.c_str()) != 0); + Require(_wputenv_s(wideName.c_str(), value.c_str()) == 0); #else - Require(setenv(name, value, 1) == 0); + Require(setenv(name, value.c_str(), 1) == 0); #endif } +std::filesystem::path PathVariable(const char* name) { +#ifdef _WIN32 + const wchar_t* value = _wgetenv(std::filesystem::path(name).wstring().c_str()); +#else + const char* value = std::getenv(name); +#endif + Require(value != nullptr); + return value; +} std::filesystem::path OwnExecutable() { #ifdef _WIN32 std::wstring executable(32768, L'\0'); @@ -94,9 +104,7 @@ std::filesystem::path OwnExecutable() { #endif } void AppendMarker(const std::string& line) { - const char* marker = std::getenv(RelaunchMarker); - Require(marker != nullptr); - std::ofstream(marker, std::ios::app) << line << '\n'; + std::ofstream(PathVariable(RelaunchMarker), std::ios::app) << line << '\n'; } void MainShutdown() { AppendMarker("shutdown"); @@ -140,7 +148,7 @@ int RunChild(const std::filesystem::path& self, const char* mode) { int DriveNestedExit() { const auto self = OwnExecutable(); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-nested-exit-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); - SetVariable(RelaunchMarker, marker.string().c_str()); + SetVariable(RelaunchMarker, marker); const int code = RunChild(self, "--nested-exit"); std::string text; { @@ -157,7 +165,7 @@ int DriveNestedExit() { int DriveExitOrder() { const auto self = OwnExecutable(); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-order-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); - SetVariable(RelaunchMarker, marker.string().c_str()); + SetVariable(RelaunchMarker, marker); const int code = RunChild(self, "--exit-order"); std::string text; { @@ -176,8 +184,8 @@ int DriveRelaunch() { const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); std::filesystem::copy_file(self, secondary, std::filesystem::copy_options::overwrite_existing); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); - SetVariable(RelaunchMarker, marker.string().c_str()); - SetVariable(MainExecutable, self.string().c_str()); + SetVariable(RelaunchMarker, marker); + SetVariable(MainExecutable, self); Require(RunChild(self, "--relaunch") == 0); std::string text; for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { @@ -260,8 +268,7 @@ int main(int argc, char** argv) { return 2; } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunched") == 0) { - const char* main = std::getenv(MainExecutable); - Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main)); + Require(std::filesystem::equivalent(OwnExecutable(), PathVariable(MainExecutable))); AppendMarker("relaunched"); return 0; } From e8604b54949fa0efddac168db9ba1f0b2d81ce4f Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 08:49:47 -0300 Subject: [PATCH 0083/1360] fix(agc): read ANYPS5_SCREENSHOTS through the wide environment on Windows Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp | 6 +++++- core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp index 6a930d4e30..9e60cbac8c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp @@ -16,8 +16,12 @@ namespace { std::atomic requested{false}; std::filesystem::path directory() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_SCREENSHOTS"); +#else const char* configured = std::getenv("ANYPS5_SCREENSHOTS"); - if (configured != nullptr && configured[0] != '\0') return configured; +#endif + if (configured != nullptr && configured[0] != 0) return configured; const std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError()); return std::filesystem::path(reinterpret_cast(basePath.get())) / "anyps5-screenshots"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp index e6202ceb1a..4adf0c824d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp @@ -21,9 +21,9 @@ void require(bool condition, const char* message) { void setScreenshotDirectory(const std::filesystem::path& directory) { #ifdef _WIN32 - require(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); + require(_wputenv_s(L"ANYPS5_SCREENSHOTS", directory.wstring().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); #else - require(::setenv("ANYPS5_SCREENSHOTS", directory.string().c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); + require(::setenv("ANYPS5_SCREENSHOTS", directory.c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); #endif } @@ -73,7 +73,7 @@ void rejectsInvalidFrames() { int main() try { const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}())); - const auto directory = root / "nested"; + const auto directory = root / std::filesystem::path(u8"nested-a\u00e7\u00e3o-\u65e5\u672c"); std::filesystem::remove_all(root); setScreenshotDirectory(directory); requestsMerge(); From ae2233ed0b09901cfa2a498e43801307fbd14cf2 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:18:22 -0300 Subject: [PATCH 0084/1360] fix(libSceNpEntitlementAccess): read ANYPS5_ENTITLEMENTS through the wide environment on Windows Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 10 ++++ .../prx/libSceNpEntitlementAccess/Export.cpp | 4 ++ core/libs/tests/GuestEntitlements.cpp | 48 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 core/libs/tests/GuestEntitlements.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index a6428b6825..3f5521d54b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -378,6 +378,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_entitlements_tests tests/GuestEntitlements.cpp) + target_include_directories(guest_entitlements_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_entitlements_tests PRIVATE libSceNpEntitlementAccess libc) + configure_windows_unwind(guest_entitlements_tests) + add_test(NAME guest_entitlements COMMAND guest_entitlements_tests) + if(WIN32) + set_tests_properties(guest_entitlements PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp) target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc) diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index ba654ff02f..5b4062659e 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -24,7 +24,11 @@ static constexpr uint32_t SKU_FLAG_FULL = 3; namespace { std::filesystem::path EntitlementsPath() { +#ifdef _WIN32 + if (const wchar_t* configured = _wgetenv(L"ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != 0) return configured; +#else if (const char* configured = std::getenv("ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != '\0') return configured; +#endif #ifdef _WIN32 wchar_t module[MAX_PATH]; const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp new file mode 100644 index 0000000000..8f9824faf9 --- /dev/null +++ b/core/libs/tests/GuestEntitlements.cpp @@ -0,0 +1,48 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessAddcontEntitlementInfo*); +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(std::uint32_t, NpEntitlementAccessAddcontEntitlementInfo*, std::uint32_t, std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_entitlements-" + std::to_string(std::random_device{}())); + const auto directory = root / std::filesystem::path(u8"dlc-a\u00e7\u00e3o-\u65e5\u672c"); + std::filesystem::create_directories(directory); + const auto file = directory / "anyps5-entitlements.ini"; + { + std::ofstream ini(file, std::ios::binary); + ini << "# owned add-ons\nFIRSTADDON0000\n SECONDADDON ; second\n"; + Require(static_cast(ini)); + } +#ifdef _WIN32 + Require(_wputenv_s(L"ANYPS5_ENTITLEMENTS", file.wstring().c_str()) == 0); +#else + Require(::setenv("ANYPS5_ENTITLEMENTS", file.c_str(), 1) == 0); +#endif + NpEntitlementAccessAddcontEntitlementInfo list[3]{}; + std::uint32_t hits = 0; + Require(sceNpEntitlementAccessGetAddcontEntitlementInfoList(0, list, 3, &hits) == 0); + Require(hits == 2); + Require(std::strcmp(list[0].entitlement_label.data, "FIRSTADDON0000") == 0); + Require(std::strcmp(list[1].entitlement_label.data, "SECONDADDON") == 0); + NpUnifiedEntitlementLabel label{}; + std::memcpy(label.data, "SECONDADDON", 11); + NpEntitlementAccessAddcontEntitlementInfo info{}; + Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &label, &info) == 0); + Require(std::strcmp(info.entitlement_label.data, "SECONDADDON") == 0 && info.package_type == 3 && info.download_status == 4); + std::filesystem::remove_all(root); + std::puts("Guest entitlements passed"); + return 0; +} From be53b825858d7fc820573cd1bc9e71b413de3274 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:19:37 -0300 Subject: [PATCH 0085/1360] fix(libSceNpEntitlementAccess): check a configured entitlements file through the wide environment Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 3 +- .../prx/libSceNpEntitlementAccess/Export.cpp | 10 +++-- core/libs/tests/GuestEntitlements.cpp | 39 ++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 3f5521d54b..fdb3f6e92b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -383,8 +383,9 @@ if(BUILD_TESTING) target_link_libraries(guest_entitlements_tests PRIVATE libSceNpEntitlementAccess libc) configure_windows_unwind(guest_entitlements_tests) add_test(NAME guest_entitlements COMMAND guest_entitlements_tests) + add_test(NAME guest_entitlements_missing COMMAND guest_entitlements_tests missing) if(WIN32) - set_tests_properties(guest_entitlements PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_entitlements guest_entitlements_missing PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 5b4062659e..7fd3f962cd 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -23,12 +23,14 @@ static constexpr uint32_t SKU_FLAG_FULL = 3; namespace { -std::filesystem::path EntitlementsPath() { #ifdef _WIN32 - if (const wchar_t* configured = _wgetenv(L"ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != 0) return configured; +const wchar_t* ConfiguredEntitlements() { return _wgetenv(L"ANYPS5_ENTITLEMENTS"); } #else - if (const char* configured = std::getenv("ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != '\0') return configured; +const char* ConfiguredEntitlements() { return std::getenv("ANYPS5_ENTITLEMENTS"); } #endif + +std::filesystem::path EntitlementsPath() { + if (const auto* configured = ConfiguredEntitlements(); configured != nullptr && configured[0] != 0) return configured; #ifdef _WIN32 wchar_t module[MAX_PATH]; const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); @@ -54,7 +56,7 @@ const std::vector& OwnedAddons() { const auto path = EntitlementsPath(); std::ifstream file(path); if (!file) { - if (std::getenv("ANYPS5_ENTITLEMENTS") != nullptr || std::filesystem::exists(path)) throw std::runtime_error("NpEntitlementAccess: cannot read " + path.string()); + if (ConfiguredEntitlements() != nullptr || std::filesystem::exists(path)) throw std::runtime_error("NpEntitlementAccess: cannot read " + path.string()); return addons; } std::string line; diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp index 8f9824faf9..868c1f6e7f 100644 --- a/core/libs/tests/GuestEntitlements.cpp +++ b/core/libs/tests/GuestEntitlements.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include extern "C" { int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessAddcontEntitlementInfo*); @@ -16,21 +18,38 @@ int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(std::uint32_t, static void Require(bool value) { if (!value) std::abort(); } -int main() { - const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_entitlements-" + std::to_string(std::random_device{}())); - const auto directory = root / std::filesystem::path(u8"dlc-a\u00e7\u00e3o-\u65e5\u672c"); - std::filesystem::create_directories(directory); - const auto file = directory / "anyps5-entitlements.ini"; - { - std::ofstream ini(file, std::ios::binary); - ini << "# owned add-ons\nFIRSTADDON0000\n SECONDADDON ; second\n"; - Require(static_cast(ini)); - } +static void SetEntitlements(const std::filesystem::path& file) { #ifdef _WIN32 Require(_wputenv_s(L"ANYPS5_ENTITLEMENTS", file.wstring().c_str()) == 0); #else Require(::setenv("ANYPS5_ENTITLEMENTS", file.c_str(), 1) == 0); #endif +} + +int main(int argc, char** argv) { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_entitlements-" + std::to_string(std::random_device{}())); + const auto directory = root / std::filesystem::path(u8"dlc-a\u00e7\u00e3o-\u65e5\u672c"); + const auto file = directory / "anyps5-entitlements.ini"; + if (argc == 2 && std::string_view(argv[1]) == "missing") { + SetEntitlements(file); + std::uint32_t hits = 0; + bool rejected = false; + try { + sceNpEntitlementAccessGetAddcontEntitlementInfoList(0, nullptr, 0, &hits); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()).starts_with("NpEntitlementAccess: cannot read "); + } + Require(rejected); + std::puts("Guest entitlements rejected a missing configured file"); + return 0; + } + std::filesystem::create_directories(directory); + { + std::ofstream ini(file, std::ios::binary); + ini << "# owned add-ons\nFIRSTADDON0000\n SECONDADDON ; second\n"; + Require(static_cast(ini)); + } + SetEntitlements(file); NpEntitlementAccessAddcontEntitlementInfo list[3]{}; std::uint32_t hits = 0; Require(sceNpEntitlementAccessGetAddcontEntitlementInfoList(0, list, 3, &hits) == 0); From 2d1a3e51875061f513efe7bff470f71bf5530c08 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:22:14 -0300 Subject: [PATCH 0086/1360] fix(libSceFontFt): map font files through CreateFileW as FreeType's Windows stream does Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFontFt/src/Driver.cpp | 37 ++++++++++++----------- core/libs/tests/GuestFont.cpp | 12 ++++++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index ee313ce475..d93b8eee1b 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -6,10 +6,12 @@ #include #include #include -#include #include #include #include +#ifdef _WIN32 +#include +#endif #include #include FT_FREETYPE_H @@ -196,30 +198,29 @@ int APS5_VABI LibrarySupport(FontLibNative* library, std::uint32_t formats) { } #ifdef _WIN32 -unsigned long ReadWideFile(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count) { - auto* file = static_cast(stream->descriptor.pointer); - if (_fseeki64(file, static_cast(offset), SEEK_SET) != 0) return count == 0 ? 1 : 0; - return count == 0 ? 0 : static_cast(std::fread(buffer, 1, count, file)); -} - -void CloseWideFile(FT_Stream stream) { - std::fclose(static_cast(stream->descriptor.pointer)); +void CloseMappedFile(FT_Stream stream) { + UnmapViewOfFile(stream->base); delete stream; } FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { - std::FILE* file = _wfopen(std::filesystem::path(reinterpret_cast(path)).c_str(), L"rb"); - if (!file) return FT_Err_Cannot_Open_Resource; - const long long size = _fseeki64(file, 0, SEEK_END) == 0 ? _ftelli64(file) : -1; - if (size < 0 || static_cast(size) > std::numeric_limits::max()) { - std::fclose(file); + const HANDLE file = CreateFileW(std::filesystem::path(reinterpret_cast(path)).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return FT_Err_Cannot_Open_Resource; + LARGE_INTEGER size{}; + if (!GetFileSizeEx(file, &size) || size.QuadPart <= 0 || size.QuadPart > std::numeric_limits::max()) { + CloseHandle(file); return FT_Err_Cannot_Open_Stream; } + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(file); + if (!mapping) return FT_Err_Cannot_Open_Stream; + void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + CloseHandle(mapping); + if (!view) return FT_Err_Cannot_Open_Stream; auto* stream = new FT_StreamRec{}; - stream->size = static_cast(size); - stream->descriptor.pointer = file; - stream->read = &ReadWideFile; - stream->close = &CloseWideFile; + stream->base = static_cast(view); + stream->size = static_cast(size.QuadPart); + stream->close = &CloseMappedFile; FT_Open_Args args{}; args.flags = FT_OPEN_STREAM; args.stream = stream; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 3bb7a5b0bb..b59ca51abb 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -274,6 +275,17 @@ int main() { Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); const std::vector fontData = EmptyGlyphFont(); + const auto fileFont = fontRoot / std::filesystem::path(u8"external-a\u00e7\u00e3o-\u65e5\u672c") / "font.ttf"; + std::filesystem::create_directories(fileFont.parent_path()); + { + std::ofstream file(fileFont, std::ios::binary); + file.write(reinterpret_cast(fontData.data()), static_cast(fontData.size())); + Require(static_cast(file)); + } + const std::u8string fileFontText = fileFont.u8string(); + FontHandle fileHandle = nullptr; + Require(sceFontOpenFontFile(library, reinterpret_cast(fileFontText.c_str()), 1, nullptr, &fileHandle) == SCE_FONT_OK && fileHandle != nullptr); + Require(sceFontCloseFont(fileHandle) == SCE_FONT_OK); Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); int setting = -1; Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_OK && setting == 0); From e95f842140fc2f80e50bd8b5b2d4c25ad6205f3f Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 01:22:30 +1300 Subject: [PATCH 0087/1360] fix(libSceAgc): match interpolant mapping variants to firmware Add the unversioned sceAgcCreateInterpolantMapping and sceAgcUpdateInterpolantMapping and stop sceAgcUpdateInterpolantMapping_10000 throwing for split f16 defaults, all matching libSceAgc bit for bit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../Shader/src/InterpolantMapping.cpp | 19 ++++--- .../libSceAgc/tests/InterpolantMapping.cpp | 50 ++++++++++++++++--- docs/dev/debt/libSceAgc.md | 4 +- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 8e56d4c0c8..65b337d335 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,12 +53,9 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { - const std::uint32_t f16 = (psWord >> 20u) & 0x3u; - if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { - throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); - } - return CreateInterpolantValueSplitF16(psWord, gsSemantic); +std::uint32_t CreateInterpolantValueFlatDefault(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t value = CreateInterpolantValue(psWord, gsSemantic); + return ((psWord & 0x00700000u) == 0x00400000u) ? (value | 0x00000020u) : value; } int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { @@ -132,7 +129,15 @@ int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const S } int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, false); +} + +int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, true); +} + +int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, false); } } diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 652eacfea7..1b38f7df08 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -14,6 +14,8 @@ extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { @@ -158,13 +160,46 @@ void testUpdateMapping() { check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); - for (const std::uint32_t index : {1u, 3u}) { - inputs[index].default_value = 0; - update = filled(); - expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); }); - check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); - inputs[index].default_value = inputs[index].default_value_hi; + inputs[1].default_value = 0; + inputs[3].default_value = 0; + update = filled(); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update with split f16 defaults failed"); + check(update[1].value == 0x01380007u && update[3].value == 0x03680409u, "f16 DEFAULT_VAL does not come from the low default"); +} + +void testUnversionedMapping() { + std::array inputs{ + semantic(1, 0, 2), semantic(2, 0, 2), semantic(5, 0, 2), semantic(3, 0, 0), semantic(4, 0, 3)}; + inputs[0].is_flat_shaded = 1; + inputs[0].default_value = 1; + inputs[0].default_value_hi = 3; + inputs[1].default_value_hi = 1; + inputs[2].is_flat_shaded = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_flat_shaded = 1; + inputs[3].default_value = 2; + std::array outputs{semantic(1, 7, 3), semantic(2, 4, 1), semantic(3, 9, 0), semantic(4, 2, 1)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + const std::array flatDefault{0x02680527u, 0x02380024u, 0x02580020u, 0x00000629u, 0x03180002u}; + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + const auto untouched = filled(); + + auto create = filled(); + check(sceAgcCreateInterpolantMapping_nid_postfix(create.data(), &gs, &ps) == 0, "unversioned mapping failed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(create[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i && create[i].value == flatDefault[i], "unversioned mapping changed"); } + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + auto update = filled(); + check(sceAgcUpdateInterpolantMapping_nid_postfix(update.data(), &gs, &ps) == 0, "unversioned update failed"); + check(std::memcmp(update.data(), create.data(), written) == 0, "unversioned update differs from its mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "unversioned update wrote past its inputs"); } void testRejections() { @@ -208,8 +243,9 @@ int main() { testIdentity(); testMapping(); testUpdateMapping(); + testUnversionedMapping(); testRejections(); - check(mappings == 7u && links == 5u, "mapping preparation call count changed"); + check(mappings == 10u && links == 8u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 53522e9736..4a7499dbff 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -24,7 +24,7 @@ - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords -- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware +- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware - [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused @@ -32,9 +32,7 @@ - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown - [sceAgcDcbPrimeUtcl2](../../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceAgcDcbQueueEndOfShaderActionGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) -- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`vieBRwlh1Lw`), not in the aerolib NID list; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) -- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `sceAgcCreateInterpolantMapping_10000` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `sceAgcCreateInterpolantMapping_10000` here take it from `default_value` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known - [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified From 7feb59518002444844a6880286d81d8749405102 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:07:34 -0300 Subject: [PATCH 0088/1360] fix(relinker): treat guest DT_INIT_ARRAY without DT_INIT_ARRAYSZ as empty A guest module with DT_INIT_ARRAY or DT_FINI_ARRAY and no size tag stopped with "Incomplete guest lifecycle array". FreeBSD rtld reads the entry count only from the size tag, so such an array calls nothing; read it as empty, as code analysis does for the executable. A size without an array still fails. --- .../relinker/src/guest/GuestImageReader.cpp | 4 ++-- .../relinker/tests/test_guest_none_relocations.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 3e9a239afa..2c7a78c150 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -276,8 +276,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Init != 0) mapped(image.Init, 1, 1); if (image.Fini != 0) mapped(image.Fini, 1, 1); const auto readLifecycleArray = [&](std::uint64_t addressTag, std::uint64_t sizeTag, std::vector& entries) { - if (!tags.contains(addressTag) && !tags.contains(sizeTag)) return; - if (!tags.contains(sizeTag) || (tags.at(sizeTag) != 0 && !tags.contains(addressTag))) fail("Incomplete guest lifecycle array"); + if (!tags.contains(sizeTag)) return; + if (tags.at(sizeTag) != 0 && !tags.contains(addressTag)) fail("Incomplete guest lifecycle array"); const auto size = tags.at(sizeTag); if (size == 0) return; if (size % 8 != 0) fail("Unaligned guest lifecycle array size"); diff --git a/core/relinker/relinker/tests/test_guest_none_relocations.py b/core/relinker/relinker/tests/test_guest_none_relocations.py index 44d55ed646..9ebc28509d 100644 --- a/core/relinker/relinker/tests/test_guest_none_relocations.py +++ b/core/relinker/relinker/tests/test_guest_none_relocations.py @@ -76,6 +76,11 @@ def change_tag(image, tag, value=None, replacement=None): raise AssertionError(f"Missing dynamic tag {tag:#x}") +def unsized_lifecycle(rela, sce, initial): + image = module(rela, sce=sce, lifecycle=True, initial=initial) + return change_tag(change_tag(image, 27, replacement=0x61000001), 28, replacement=0x61000001) + + def check_output(data, system, rela, plt, initial): expected_rela = [(entry[0], entry[1] & 0xffffffff, entry[2]) if (entry[1] & 0xffffffff) == 8 and entry[1] >> 32 else entry @@ -123,6 +128,7 @@ def invalid_cases(sce): entry = 0x61000033 if sce else 9 address = 0x6100002f if sce else 7 plt_format = 0x6100002b if sce else 20 + yield "lifecycle-without-address", change_tag(module([RELATIVE], sce=sce, lifecycle=True, initial=0), 25, replacement=0x61000001), "Incomplete guest lifecycle array" yield "partial-entry", change_tag(module([NOOP], sce=sce), size, 23), "Invalid relocation table size" yield "entry-size", change_tag(module([NOOP], sce=sce), entry, 16), "Invalid RELA entry size" yield "unmapped-table", change_tag(module([NOOP], sce=sce), address, 0xffffffffffffffff), ("SCE table exceeds" if sce else "Unmapped ELF address") @@ -186,6 +192,15 @@ def main(): executed = subprocess.run([sys.executable, __file__, "--load", str(artifact)], capture_output=True, text=True, timeout=10) assert executed.returncode == 0, (executed.stdout, executed.stderr) + for name, rela, initial in (("unsized-lifecycle", [RELATIVE], UNCHANGED), + ("unsized-lifecycle-unread", [(TARGET, 0, FUNCTION)], FUNCTION)): + image = unsized_lifecycle(rela, sce, initial) + for system in ("linux", "windows", "macos"): + result, artifact, output = convert(f"{sce}-{name}", system, image) + assert result.returncode == 0, (sce, name, system, result.stdout, result.stderr) + check_output(artifact.read_bytes(), system, rela, [], initial) + if system == "linux": + assert not {25, 26, 27, 28} & set(dynamic_tags(artifact.read_bytes())), name for name, image, error in invalid_cases(sce): result, artifact, output = convert(f"{sce}-{name}", "linux", image) assert result.returncode == 2 and error in result.stderr, (sce, name, result.stderr) From bbf532b638d5744371fb63a35f2c0a3d5a6ddc29 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:31:29 -0300 Subject: [PATCH 0089/1360] fix(agc): read capture output directories through the wide environment on Windows Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/SubmissionCapture.cpp | 6 +++++- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 8 +++++++- core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp | 6 +++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp index 217e5b0c81..9caddde7bd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp @@ -101,8 +101,12 @@ SubmissionCapture::SubmissionCapture(const std::filesystem::path& directory) : d SubmissionCapture* SubmissionCapture::Active() { static SubmissionCapture* const capture = [] { +#ifdef _WIN32 + const wchar_t* directory = _wgetenv(L"APS5_DUMP_SUBMITS"); +#else const char* directory = std::getenv("APS5_DUMP_SUBMITS"); - return directory != nullptr && *directory != '\0' ? new SubmissionCapture(directory) : nullptr; +#endif + return directory != nullptr && *directory != 0 ? new SubmissionCapture(directory) : nullptr; }(); return capture; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 53d7e239fa..cb02125e6f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -126,6 +126,12 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, PerformanceCounters::CountPipeline(); } +#ifdef _WIN32 +const wchar_t* ComputePipelineDumpDirectory() { return _wgetenv(L"APS5_DUMP_COMPUTE_PIPELINES"); } +#else +const char* ComputePipelineDumpDirectory() { return std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); } +#endif + bool CaptureComputePipeline(std::size_t words) { const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS"); if (minimum == nullptr) return true; @@ -4885,7 +4891,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons const Graphics::PipelineSpecialization specialization(shader); const auto subgroupSize = computeSubgroupSize(shader); const auto stageFlags = computeStageFlags(shader); - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { + if (const auto* directory = ComputePipelineDumpDirectory(); directory != nullptr && *directory != 0 && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp index 98cf833fd3..8668696b41 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp @@ -693,8 +693,12 @@ void testCorrupt(const std::filesystem::path& directory) { int main() { std::filesystem::path scratch; try { +#ifdef _WIN32 + const wchar_t* captured = _wgetenv(L"APS5_DUMP_SUBMITS"); +#else const char* captured = std::getenv("APS5_DUMP_SUBMITS"); - check(captured != nullptr && *captured != '\0', "set APS5_DUMP_SUBMITS to the capture directory of this test"); +#endif + check(captured != nullptr && *captured != 0, "set APS5_DUMP_SUBMITS to the capture directory of this test"); const std::filesystem::path directory = captured; std::filesystem::remove_all(directory); scratch = std::filesystem::temp_directory_path() / ("anyps5-pm4-replay-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); From 65f36b5fcd3fce747e78d95546b24a3373d77aa3 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:40:20 -0300 Subject: [PATCH 0090/1360] fix(nid): open export lists through the UTF-8 native path on Windows Co-Authored-By: Claude Opus 5.5 --- core/libs/nid/src/ExportExclusions.cpp | 6 +++--- core/libs/nid/tests/test_macho_nids.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 273f517d3b..b02cd0dd24 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -175,7 +175,7 @@ std::unordered_set ReadExportExclusions(const std::string& path) { } std::unordered_set ReadExportList(const std::string& path) { - std::ifstream input(path, std::ios::binary); + std::ifstream input(Internal::NativePath(path), std::ios::binary); if (!input) throw std::runtime_error("cannot open export list: " + path); const auto text = ReadText(path, input); if (text.empty() || text.back() != '\n') throw std::runtime_error("malformed export list: " + path); @@ -192,8 +192,8 @@ std::unordered_set ReadExportList(const std::string& path) { bool WriteExportList(const std::string& referencePath, const std::string& path) { const auto text = FormatExportList(ReadExportExclusions(referencePath)); - if (std::ifstream existing(path, std::ios::binary); existing && ReadText(path, existing) == text) return false; - std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (std::ifstream existing(Internal::NativePath(path), std::ios::binary); existing && ReadText(path, existing) == text) return false; + std::ofstream output(Internal::NativePath(path), std::ios::binary | std::ios::trunc); if (!output) throw std::runtime_error("cannot write export list: " + path); output.write(text.data(), static_cast(text.size())); output.close(); diff --git a/core/libs/nid/tests/test_macho_nids.py b/core/libs/nid/tests/test_macho_nids.py index a4a239fcbc..e49dc26e67 100644 --- a/core/libs/nid/tests/test_macho_nids.py +++ b/core/libs/nid/tests/test_macho_nids.py @@ -271,7 +271,7 @@ def check_refusals(patcher, directory): def main(): patcher = Path(sys.argv[1]).resolve() - with tempfile.TemporaryDirectory(prefix="anyps5-macho-nid-") as directory: + with tempfile.TemporaryDirectory(prefix="anyps5-macho-nid-a\u00e7\u00e3o-\u65e5\u672c-") as directory: check_shrinking(patcher, directory) check_growing(patcher, directory) check_preserved(patcher, directory) From 9cd530b7e83eeb3c5ae43c905876689e3d063d08 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:57:10 -0300 Subject: [PATCH 0091/1360] fix(allocator): take pe_import_first's file as a wide path on Windows Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 2 +- core/libs/allocator/PeImportFirst.cpp | 7 ++-- core/libs/allocator/PeImportFirst.hpp | 3 +- core/libs/allocator/PeImportFirstMain.cpp | 34 +++++++++++++++---- .../allocator/tests/PeImportFirstTests.cpp | 7 ++-- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 86c2b91bbe..3b8d23698c 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -209,7 +209,7 @@ if(TARGET mimalloc) target_compile_features(pe_import_first PRIVATE cxx_std_20) if(MINGW) target_compile_options(pe_import_first PRIVATE -fasynchronous-unwind-tables) - target_link_options(pe_import_first PRIVATE -static -static-libgcc -static-libstdc++) + target_link_options(pe_import_first PRIVATE -municode -static -static-libgcc -static-libstdc++) endif() add_custom_command( OUTPUT ${PATCHED_DIR}/mimalloc.dll diff --git a/core/libs/allocator/PeImportFirst.cpp b/core/libs/allocator/PeImportFirst.cpp index 8e1ee93454..aa4b040ba2 100644 --- a/core/libs/allocator/PeImportFirst.cpp +++ b/core/libs/allocator/PeImportFirst.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -78,16 +79,16 @@ void MoveImportFirst(std::vector& bytes, const std::string& dll) for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes); } -void MoveImportFirstInFile(const std::string& path, const std::string& dll) { +void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll) { std::vector bytes; { std::ifstream input(path, std::ios::binary); - if (!input) throw std::runtime_error("cannot open " + path); + if (!input) throw std::runtime_error("cannot open " + path.string()); bytes.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); } MoveImportFirst(bytes, dll); std::ofstream output(path, std::ios::binary | std::ios::trunc); - if (!output || !output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error("cannot write " + path); + if (!output || !output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error("cannot write " + path.string()); } } diff --git a/core/libs/allocator/PeImportFirst.hpp b/core/libs/allocator/PeImportFirst.hpp index 30aa18cf7d..54db5101fc 100644 --- a/core/libs/allocator/PeImportFirst.hpp +++ b/core/libs/allocator/PeImportFirst.hpp @@ -1,13 +1,14 @@ #ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP #define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP +#include #include #include namespace Allocator { void MoveImportFirst(std::vector& image, const std::string& dll); -void MoveImportFirstInFile(const std::string& path, const std::string& dll); +void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll); } diff --git a/core/libs/allocator/PeImportFirstMain.cpp b/core/libs/allocator/PeImportFirstMain.cpp index 02ace8e22e..359c357674 100644 --- a/core/libs/allocator/PeImportFirstMain.cpp +++ b/core/libs/allocator/PeImportFirstMain.cpp @@ -2,17 +2,37 @@ #include #include +#include +#include -int main(int argc, char** argv) { - if (argc != 3) { - std::fprintf(stderr, "usage: pe_import_first \n"); - return 2; - } +namespace { + +int Usage() { + std::fprintf(stderr, "usage: pe_import_first \n"); + return 2; +} + +int Run(const std::filesystem::path& file, const std::string& dll) { try { - Allocator::MoveImportFirstInFile(argv[1], argv[2]); + Allocator::MoveImportFirstInFile(file, dll); } catch (const std::exception& error) { - std::fprintf(stderr, "pe_import_first: %s: %s\n", argv[1], error.what()); + std::fprintf(stderr, "pe_import_first: %s: %s\n", file.string().c_str(), error.what()); return 1; } return 0; } + +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + if (argc != 3) return Usage(); + const auto dll = std::filesystem::path(argv[2]).u8string(); + return Run(argv[1], std::string(dll.begin(), dll.end())); +} +#else +int main(int argc, char** argv) { + if (argc != 3) return Usage(); + return Run(argv[1], argv[2]); +} +#endif diff --git a/core/libs/allocator/tests/PeImportFirstTests.cpp b/core/libs/allocator/tests/PeImportFirstTests.cpp index 4df8aceed5..5d40857cdc 100644 --- a/core/libs/allocator/tests/PeImportFirstTests.cpp +++ b/core/libs/allocator/tests/PeImportFirstTests.cpp @@ -113,7 +113,7 @@ void movesTheNamedImportFirst() { void movesTheImportInAFile(const std::filesystem::path& directory) { const auto path = directory / "moved.dll"; writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"})); - Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll"); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); const auto moved = readFile(path); require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll", "the file's import did not move first"); } @@ -122,7 +122,7 @@ void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const s const auto path = directory / (what + ".dll"); writeFile(path, bytes); try { - Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll"); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); } catch (const std::exception& error) { require(std::string(error.what()).find(expected) != std::string::npos, what + ": failed with another error: " + error.what()); require(readFile(path) == bytes, what + ": the file changed although the move failed"); @@ -134,7 +134,8 @@ void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const s } int main() { - const auto directory = std::filesystem::temp_directory_path() / ("aps5-pe-import-first-" + std::to_string(std::random_device{}())); + auto directory = std::filesystem::temp_directory_path() / std::filesystem::path(u8"aps5-pe-import-first-a\u00e7\u00e3o-\u65e5\u672c-"); + directory += std::to_string(std::random_device{}()); try { std::filesystem::create_directories(directory); movesTheNamedImportFirst(); From 96ff1330eb04c027750a183c555fa9ee51311b62 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 20 Oct 2026 15:01:44 +0200 Subject: [PATCH 0092/1360] fix(agc): leave the float-controls probe's pipelines out of compute captures The start-up f32 denormal flush probe already stays out of the pipeline counters and the pipeline journal, but APS5_DUMP_COMPUTE_PIPELINES still captured its pipelines. On a device that runs the probe, every capture directory gained twelve driver-internal records that count toward the 128-record limit, and agc_compute_pipeline_capture_run found 14 captures for a fixture with two programs. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- docs/dev/BUILD.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 53d7e239fa..9e193faa41 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4885,7 +4885,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons const Graphics::PipelineSpecialization specialization(shader); const auto subgroupSize = computeSubgroupSize(shader); const auto stageFlags = computeStageFlags(shader); - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && !state->probingFloatControls && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index b1b725a14f..7f3a6de87f 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -151,7 +151,7 @@ On Windows MinGW frees `thread_local` objects before their destructors run, so t ## Compute pipeline replay -Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. The pipelines of the start-up f32 denormal flush probe are not captured. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `8388608` fail. The filter applies only when capture is enabled. From 78bf409ec5180d83fdfae527cf1ce4ed54380457 Mon Sep 17 00:00:00 2001 From: Gabrieljr42 Date: Tue, 20 Oct 2026 10:08:57 -0300 Subject: [PATCH 0093/1360] feat(libSceAjm): parse CRC-protected and non-stereo MP3 metadata frames Parse Xing/Info and LAME tags across candidate offsets covering both CRC-protected and unprotected streams, and accept VBRI headers at byte 36 for non-stereo / MPEG-2 frames. Reject non-Layer-3 streams with SCE_AJM_ERROR_INVALID_PARAMETER without throwing. Progress: sceAjmDecMp3ParseFrame implemented (libSceAjm.native: 19/40 -> 20/40, 50.0%) --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 73 +++++++++++++------- core/libs/tests/GuestAjm.cpp | 79 ++++++++++++++++++---- docs/dev/debt/libSceAjm.native.md | 3 +- 3 files changed, 113 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index d6051d613f..b856b5ac38 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -806,7 +806,7 @@ std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) { int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame); -void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) { +void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame, int parseOfl) { const std::size_t end = std::min(streamSize, frame->frame_size); if (end <= 4) return; const std::size_t available = end - 4; @@ -829,9 +829,17 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint bit += mainDataBits; std::size_t at = 4 + std::min((bit + 7) / 8, available); - if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) { - const std::uint8_t flags = stream[at + 7]; - std::size_t field = at + 8; + std::size_t tagAt = 0; + for (const std::size_t candidate : std::initializer_list{at + 2, at, (mpeg1 ? (mono ? 23u : 38u) : (mono ? 15u : 23u)), (mpeg1 ? (mono ? 21u : 36u) : (mono ? 13u : 21u))}) { + if (candidate + 8 <= end && (std::memcmp(stream + candidate, "Xing", 4) == 0 || std::memcmp(stream + candidate, "Info", 4) == 0)) { + tagAt = candidate; + break; + } + } + + if (tagAt != 0) { + const std::uint8_t flags = stream[tagAt + 7]; + std::size_t field = tagAt + 8; std::uint32_t frames = 0; if (flags & 0x01u) { if (field + 4 > end) return; @@ -858,33 +866,47 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint frame->encoder_delay = frame->samples_per_channel + delay + 529; frame->ofl_type = MP3_OFL_LAME; } - } else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) { - NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)"); - } else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) { - frame->encoder_delay = ReadBigEndian(stream + at + 6, 2); + return; + } + + std::size_t vbriAt = 0; + for (const std::size_t candidate : std::initializer_list{36u, at, at + 2, 38u}) { + if (candidate + 26 <= end && std::memcmp(stream + candidate, "VBRI", 4) == 0) { + vbriAt = candidate; + break; + } + } + + if (vbriAt != 0) { + frame->encoder_delay = ReadBigEndian(stream + vbriAt + 6, 2); frame->ofl_type = MP3_OFL_VBRI; - if (frame->frame_size <= streamSize) { + if (parseOfl == 1 && frame->frame_size <= streamSize) { AjmDecMp3ParseFrame next{}; - if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { + if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 2, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { frame->encoder_delay += next.encoder_delay; frame->total_samples = next.total_samples; frame->ofl_type = MP3_OFL_VBRI_AND_FGH; } } - } else if (at + 10 <= end) { - while (at + 9 < end && stream[at] != 0xB4u) ++at; - if (at + 9 >= end) return; - std::uint8_t crc = 0xFF; - for (std::size_t i = 0; i < 9; ++i) { - for (int shift = 7; shift >= 0; --shift) { - const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); - crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); + return; + } + + for (std::size_t fghAt = std::min(at, std::size_t{4}); fghAt + 10 <= end; ++fghAt) { + if (stream[fghAt] == 0xB4u) { + std::uint8_t crc = 0xFF; + for (std::size_t i = 0; i < 9; ++i) { + for (int shift = 7; shift >= 0; --shift) { + const bool same = ((crc >> 7u) & 1u) == ((stream[fghAt + i] >> shift) & 1u); + crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); + } + } + if (stream[fghAt + 9] == crc) { + frame->encoder_delay = ReadBigEndian(stream + fghAt + 1, 2); + frame->total_samples = ReadBigEndian(stream + fghAt + 3, 4); + frame->ofl_type = MP3_OFL_FGH; + break; } } - if (stream[at + 9] != crc) return; - frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); - frame->total_samples = ReadBigEndian(stream + at + 3, 4); - frame->ofl_type = MP3_OFL_FGH; } } @@ -899,13 +921,12 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par }; const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; const std::uint32_t version = (header >> 19u) & 3u; + const std::uint32_t layer = (header >> 17u) & 3u; const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; - const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; + const bool valid = (header >> 21u) == 0x7FFu && layer == 1u && sampleRate != 0 && bitrate != 0; AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; - if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)"); - if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)"); const bool mpeg1 = version == 3; frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; @@ -916,7 +937,7 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par frame->num_frames = 0; frame->total_samples = 0; frame->ofl_type = MP3_OFL_NONE; - if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame); + if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame, parseOfl); return 0; } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 017b6304a8..5c726cd1a1 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -391,6 +391,15 @@ void TestMp3ParseFrame() { Require(sceAjmDecMp3ParseFrame(reservedSampleRate, 4, 0, &frame) == invalidParameter); const std::uint8_t mpeg25Above64Kbps[4] = {0xFF, 0xE3, 0x98, 0xC0}; Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); + for (const std::uint8_t layer : {0u, 2u, 3u}) { + const std::uint8_t header[4] = {0xFF, static_cast(0xF9u | (layer << 1u)), 0x90, 0x04}; + for (const int parseOfl : {0, 1}) { + std::memset(&frame, 0xA5, sizeof(frame)); + const auto previous = frame; + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), parseOfl, &frame) == invalidParameter); + Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); + } + } } const std::uint8_t LAME_MPEG1_STEREO_VBR[] = { @@ -457,14 +466,17 @@ bool OflParsesTo(const std::vector& stream, std::uint32_t streamSi return frame.num_frames == frames && frame.encoder_delay == delay && frame.total_samples == total && frame.ofl_type == type; } -bool OflThrows(const std::vector& stream) { - AjmDecMp3ParseFrame frame{}; - try { - sceAjmDecMp3ParseFrame(stream.data(), static_cast(stream.size()), 1, &frame); - } catch (const std::runtime_error&) { - return true; +std::vector ProtectedMp3Frame(std::vector frame, bool tagAfterCrc) { + frame[1] &= 0xFE; + if (tagAfterCrc) { + const auto size = frame.size(); + frame.insert(frame.begin() + 4, {0xA5, 0x5A}); + frame.resize(size); + } else { + frame[4] = 0xA5; + frame[5] = 0x5A; } - return false; + return frame; } void TestMp3ParseOfl() { @@ -487,15 +499,53 @@ void TestMp3ParseOfl() { Require(OflParsesTo(vbri, 834, 0, 576 + 1105, 441000, 4)); Require(OflParsesTo(vbri, 480, 0, 576, 0, 2)); - auto protectedFrame = stereo; - protectedFrame[1] = 0xFA; - Require(OflThrows(protectedFrame)); - auto layer2 = stereo; - layer2[1] = 0xFD; - Require(OflThrows(layer2)); + for (const bool tagAfterCrc : {false, true}) { + const auto protectedStereo = ProtectedMp3Frame(stereo, tagAfterCrc); + Require(OflParsesTo(protectedStereo, 417, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedMono = ProtectedMp3Frame(mono, tagAfterCrc); + Require(OflParsesTo(protectedMono, 576, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedMpeg2 = ProtectedMp3Frame(mpeg2, tagAfterCrc); + Require(OflParsesTo(protectedMpeg2, 208, 20, 576 + 576 + 529, 10000, 1)); + } + const auto protectedFgh = ProtectedMp3Frame(fgh, true); + Require(OflParsesTo(protectedFgh, 417, 0, 1105, 441000, 3)); auto monoVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); monoVbri[3] = 0xC4; - Require(OflThrows(monoVbri)); + Require(OflParsesTo(monoVbri, 417, 0, 576, 0, 2)); + for (const std::uint8_t version : {0xF3u, 0xE3u}) { + for (const std::uint8_t mode : {0x04u, 0xC4u}) { + auto otherVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), version == 0xF3 ? 208 : 417); + otherVbri[1] = version; + otherVbri[2] = 0x80; + otherVbri[3] = mode; + Require(OflParsesTo(otherVbri, static_cast(otherVbri.size()), 0, 576, 0, 2)); + } + } + const auto protectedVbri = ProtectedMp3Frame(monoVbri, false); + Require(OflParsesTo(protectedVbri, 417, 0, 576, 0, 2)); + auto monoVbriAndFgh = monoVbri; + monoVbriAndFgh.insert(monoVbriAndFgh.end(), protectedFgh.begin(), protectedFgh.end()); + Require(OflParsesTo(monoVbriAndFgh, 834, 0, 576 + 1105, 441000, 4)); +} + +void TestMp3MetadataBounds() { + const auto stereo = Mp3Frame(LAME_MPEG1_STEREO_VBR, sizeof(LAME_MPEG1_STEREO_VBR), 417); + const auto protectedStereo = ProtectedMp3Frame(stereo, true); + const auto protectedLame = ProtectedMp3Frame(stereo, false); + auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + vbri[3] = 0xC4; + for (const auto& complete : {stereo, protectedStereo, protectedLame, vbri}) { + for (std::size_t length = 0; length <= complete.size(); ++length) { + const std::vector prefix(complete.begin(), complete.begin() + length); + AjmDecMp3ParseFrame frame{}; + const int result = sceAjmDecMp3ParseFrame(prefix.data(), static_cast(prefix.size()), 1, &frame); + Require(result == (length < 4 ? static_cast(0x80930005) : 0)); + if (length >= 4) Require(frame.frame_size == 417); + } + } + std::vector chain; + for (std::size_t i = 0; i < 4096; ++i) chain.insert(chain.end(), vbri.begin(), vbri.end()); + Require(OflParsesTo(chain, static_cast(chain.size()), 0, 576, 0, 2)); } struct GaplessDecode { @@ -1933,6 +1983,7 @@ int main() { Require(nullBuffersThrow && batchInfo.offset == 0); TestMp3ParseFrame(); TestMp3ParseOfl(); + TestMp3MetadataBounds(); TestMp3(context); TestOpus(context); TestOpus(context, 1); diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 4f972627f2..67b788aca3 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -7,7 +7,7 @@ - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder -- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description +- [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: only Layer III is accepted and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag with or without CRC offset, VBRI at byte 36 or after side information, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` @@ -22,7 +22,6 @@ - [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. -- [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. - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. From cd1efc9fca09c2b5f3a4e687cc21969652d7f51d Mon Sep 17 00:00:00 2001 From: Werner Date: Mon, 19 Oct 2026 13:24:04 +0200 Subject: [PATCH 0094/1360] feat(agc): render into depth mip levels Decode DB_Z_INFO.MAXMIP and DB_DEPTH_VIEW.MIPID and render into the selected level of a 64 KiB Z_X mip chain as a surface of its own, at the level's offset and extent from ComputeElementMipLayout, as color target mips do. Slices of a mip chain step by the size of the whole chain. Multisampled depth mips, other swizzles, levels in the packed mip tail and HTILE on a mip chain still throw. LEGO Builder's Journey (PPSA05496) stopped at its first scene on "depth mips (DB_DEPTH_VIEW MIP_LEVEL) is unsupported". --- .../libSceAgcDriver/Graphics/src/State.cpp | 26 ++++++++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 31 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1..be7fd512c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -309,8 +309,9 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); - zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); - zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x00001000u, "partially resident depth (DB_Z_INFO)"); + const auto viewMip = (view >> 26u) & 0xfu; + const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu; zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; @@ -348,7 +349,26 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx } const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; - if (const auto slice = view & 0x1fffu; slice != 0) { + if (maxMip != 0) { + char message[320]; + std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view); + const std::string where(message); + Require(viewMip <= maxMip, "the " + where + " is beyond the surface's mips"); + Require(depth.samples == 1u, "multisampled " + where + " is unsupported"); + Require(depth.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX), "the " + where + " uses a swizzle other than 64 KiB Z_X"); + const auto depthMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, zFormat == 1 ? 2u : 4u, depth.extent.width, depth.extent.height, maxMip + 1u); + const auto stencilMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 1u, depth.extent.width, depth.extent.height, maxMip + 1u); + Require(zFormat == 0 || !depthMips.at(viewMip).tail, "rendering the " + where + " into the packed mip tail is unsupported"); + Require(!stencil || !stencilMips.at(viewMip).tail, "rendering the stencil of the " + where + " into the packed mip tail is unsupported"); + Require((read(cx, 0x010) & (1u << 29u)) == 0, "HTILE for the " + where + " is unsupported"); + if (const auto slice = view & 0x1fffu; slice != 0) { + if (depth.address != 0) depth.address += static_cast(slice) * ComputeSurfaceSize(depthMips, 1u); + if (depth.stencilAddress != 0) depth.stencilAddress += static_cast(slice) * ComputeSurfaceSize(stencilMips, 1u); + } + if (depth.address != 0) depth.address += depthMips.at(viewMip).tiledOffset; + if (depth.stencilAddress != 0) depth.stencilAddress += stencilMips.at(viewMip).tiledOffset; + depth.extent = {depthMips.at(viewMip).width, depthMips.at(viewMip).height}; + } else if (const auto slice = view & 0x1fffu; slice != 0) { if (depth.address != 0) depth.address += static_cast(slice) * DepthSliceBytes(depth.extent, zFormat == 1 ? 2u : 4u); if (depth.stencilAddress != 0) depth.stencilAddress += static_cast(slice) * DepthSliceBytes(depth.extent, 1u); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6e1f5d23ee..48f92cabe8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "SceShaders.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" @@ -1713,6 +1714,36 @@ void DisabledColorTests() { const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue); Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset"); queue.context[0x002] = 0; + { + using AgcDriver::Graphics::ComputeElementMipLayout; + using AgcDriver::Graphics::TextureTileMode; + auto mipped = queue; + mipped.context[0x007] = 0x03ff03ff; + mipped.context[0x010] = 0x80000181u | (2u << 16u); + const auto mips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 2u, 1024u, 1024u, 3u); + Require(!mips.at(1).tail && mips.at(1).width == 512u && mips.at(1).height == 512u, "the 1024x1024 Z_X layout does not give a 512x512 second mip outside the tail"); + const auto first = AgcDriver::Graphics::DecodeState(mipped); + Require(first.depth && first.depth->address == depthOnly.depth->address + mips.at(0).tiledOffset && first.depth->extent.width == 1024u && first.depth->extent.height == 1024u, "view mip 0 of a mip chain is not its first level"); + mipped.context[0x002] = 1u << 26u; + const auto second = AgcDriver::Graphics::DecodeState(mipped); + Require(second.depth && second.depth->address == depthOnly.depth->address + mips.at(1).tiledOffset, "view mip 1 is not at the second level's offset in the Z_X mip chain"); + Require(second.depth->extent.width == 512u && second.depth->extent.height == 512u, "view mip 1 does not render at the second level's extent"); + mipped.context[0x002] = (1u << 26u) | 1u | (1u << 13u); + const auto sliced = AgcDriver::Graphics::DecodeState(mipped); + Require(sliced.depth && sliced.depth->address == second.depth->address + AgcDriver::Graphics::ComputeSurfaceSize(mips, 1u), "slice 1 of a mip chain does not step by the whole chain"); + mipped.context[0x002] = 3u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "is beyond the surface's mips"); + mipped.context[0x002] = 2u << 26u; + mipped.context[0x007] = 0x003f003f; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "into the packed mip tail is unsupported"); + mipped.context[0x007] = 0x03ff03ff; + mipped.context[0x002] = 1u << 26u; + mipped.context[0x010] |= 1u << 29u; + mipped.context[0x005] = 0x300; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "HTILE for the depth view mip"); + mipped.context[0x010] = (0x80000181u | (2u << 16u)) & ~(31u << 4u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "uses a swizzle other than 64 KiB Z_X"); + } for (const auto colorControl : {0x0u, 0xcc0000u}) { queue = makeState(); queue.context[0x202] = colorControl; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index adf41ad5cc..aff1c9f43d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -213,5 +213,6 @@ - [2D reads of 2D arrays](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled image whose shader shape is 2D and whose T# is a 2D array is built from its base slice alone: the T# is narrowed to that slice (`FirstLayerDescriptor`) before its decode, cache key and image, in stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry, so both images exist when a title uses both. Storage images read or written through a 2D shape are not narrowed and still build every slice the T# names. Ghost of Yotei (PPSA26344) reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. - [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. Ghost of Yotei (PPSA26344) samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. - [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. +- [Depth mip levels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a depth view of mip `DB_DEPTH_VIEW.MIPID` in a chain of `DB_Z_INFO.MAXMIP` levels renders into that level as a surface of its own, at the level's offset and extent from `ComputeElementMipLayout` for the 64 KiB Z_X swizzle, the layout color target mips and sampled depth already use; slices of a chain step by the size of the whole chain. Taken from LEGO Builder's Journey (PPSA05496), which renders depth mip 1 of 64 KiB Z_X surfaces without HTILE; the placement was not compared with a console capture. Multisampled depth mips, other swizzle modes, levels in the packed mip tail and HTILE on a mip chain throw - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. From dc6da1b96a2e0255a564aace8ff52ff26ab308b0 Mon Sep 17 00:00:00 2001 From: Luis Gerardo Aguilar <100291529+LuisAguilarDev@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:46:16 -0500 Subject: [PATCH 0095/1360] fix(agc): ignore a stencil clear on a target without a stencil plane A depth-only target has no stencil storage for STENCIL_CLEAR_ENABLE to write. Titles that clear depth and stencil together on such a target were rejected; the depth clear and colour writes now proceed. A read-only stencil plane is still rejected. --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 11 ++++++----- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 8 +++++--- docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1..eb3c9ee20f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -307,13 +307,13 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx const bool opValReference = opValExport && StencilOperationValueReplaces(cx); zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; - const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; + const bool stencilClear = stencil && (read(cx, 0x000) & 2u) != 0; Require(zFormat != 2, "Z_24 depth is unsupported"); if (zFormat == 0) depthControl &= ~6u; if (!stencil) depthControl &= ~1u; @@ -323,7 +323,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx }; const bool depthReadOnly = (view & 0x01000000u) != 0; const bool stencilReadOnly = (view & 0x02000000u) != 0; - Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); + Require(!stencilClear || !stencilReadOnly, "stencil clear requires a writable stencil plane"); Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; @@ -1893,10 +1893,11 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; - const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; + std::uint32_t stencilInfo = 0; + const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0 && !(value(cx, 0x011, stencilInfo) && (stencilInfo & 1u) == 0); if (stencilClear && !depthPlanesAbsent(cx)) { - std::uint32_t stencil = 0, view = 0; - if ((value(cx, 0x011, stencil) && (stencil & 1u) == 0) || (value(cx, 0x002, view) && (view & 0x02000000u) != 0)) return require(false, "stencil clear requires a writable stencil plane"); + std::uint32_t view = 0; + if (value(cx, 0x002, view) && (view & 0x02000000u) != 0) return require(false, "stencil clear requires a writable stencil plane"); } if (depthClear && !depthPlanesAbsent(cx)) { std::uint32_t z = 0, view = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6e1f5d23ee..1b03286c2e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2312,10 +2312,12 @@ void DepthStencilTests() { queue.context[0x000] = 0x22u | control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); } - queue.context[0x000] = 0x22; + queue.context[0x000] = 0x23; queue.context[0x011] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); - Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && !state.stencilTest && state.colors.size() == 2, "a depth and stencil clear without a stencil plane does not clear depth alone"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a stencil clear without a stencil plane"); + queue.context[0x000] = 0x22; queue.context[0x010] = 0; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index adf41ad5cc..1f526a98df 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -215,3 +215,4 @@ - [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. +- [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws From 4d0926c0b65df8da23decc053c893994c3ef7cb3 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Tue, 20 Oct 2026 15:39:00 +0200 Subject: [PATCH 0096/1360] fix(build): locate ranlib on MinGW toolchains --- core/libs/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 86c2b91bbe..914ba5a7aa 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -61,6 +61,14 @@ if(MINGW) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() + if(NOT EXISTS "${CMAKE_RANLIB}") + get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + if(EXISTS "${_compiler_dir}/gcc-ranlib${CMAKE_EXECUTABLE_SUFFIX}") + set(CMAKE_RANLIB "${_compiler_dir}/gcc-ranlib${CMAKE_EXECUTABLE_SUFFIX}") + elseif(EXISTS "${_compiler_dir}/ranlib${CMAKE_EXECUTABLE_SUFFIX}") + set(CMAKE_RANLIB "${_compiler_dir}/ranlib${CMAKE_EXECUTABLE_SUFFIX}") + endif() + endif() if(NOT EXISTS "${CMAKE_RANLIB}") message(FATAL_ERROR "Stable import libraries require ranlib") endif() From f923961c6644ad894944952b27762674fd77b18c Mon Sep 17 00:00:00 2001 From: Daniele Giovane <77807986+yabanana@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:42:39 +0200 Subject: [PATCH 0097/1360] fix(shader): reject temporary owners for borrowed memory regions --- core/shader/recompiler/Recompiler.hpp | 14 ++++- .../tests/RequestSerializerTests.cpp | 59 +++++++++++++++++++ docs/dev/debt/shader.md | 2 + 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..7f540589b0 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,17 @@ struct MemoryRegion { std::span bytes; }; +class MemoryRegionView : public std::span { + using Base = std::span; +public: + using Base::Base; + constexpr MemoryRegionView() noexcept = default; + constexpr MemoryRegionView(Base value) noexcept : Base(value) {} + template + requires (!std::ranges::borrowed_range) + MemoryRegionView(TRange&&) = delete; +}; + struct ShaderBinary { ShaderStage stage; std::uint64_t codeAddress; @@ -236,7 +248,7 @@ struct GuestContext { std::optional compute; std::optional pixel; std::optional vertex; - std::span memory; + MemoryRegionView memory; std::optional floatMode; std::span linkedCallFallbacks; bool convertedSamples = false; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index be4b2a16c7..55215ac897 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include namespace { @@ -45,6 +47,62 @@ void Require(bool condition) { if (!condition) std::abort(); } +template +concept AssignableMemory = requires(ShaderRecompiler::GuestContext& context, TRange&& range) { + context.memory = std::forward(range); +}; + +using Region = ShaderRecompiler::MemoryRegion; +using Regions = std::vector; +using RegionArray = std::array; +using RegionView = ShaderRecompiler::MemoryRegionView; + +static_assert(!AssignableMemory); +static_assert(!AssignableMemory); +static_assert(!AssignableMemory); +static_assert(!AssignableMemory>); +static_assert(AssignableMemory && AssignableMemory); +static_assert(AssignableMemory && AssignableMemory); +static_assert(AssignableMemory> && AssignableMemory); +static_assert(sizeof(RegionView) == sizeof(std::span)); +static_assert(alignof(RegionView) == alignof(std::span)); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_nothrow_constructible_v>); +static_assert([] { + const RegionArray owned{{{0x1000u, {}}}}; + const RegionView view{std::span(owned)}; + return view.size() == 1u && view.front().guestAddress == 0x1000u; +}()); + +void VerifyMemoryView() { + const std::array bytes{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + Regions owned{{0x1000u, bytes}}; + const Regions frozen = owned; + Region array[]{{0x2000u, bytes}}; + RegionArray standard{{{0x3000u, bytes}}}; + ShaderRecompiler::RecompileRequest request{}; + request.context.memory = owned; + Require(request.context.memory.data() == owned.data()); + owned.front().guestAddress = 0x4000u; + Require(request.context.memory.front().guestAddress == 0x4000u); + request.context.memory = frozen; + Require(request.context.memory.data() == frozen.data()); + request.context.memory = array; + Require(request.context.memory.data() == array); + request.context.memory = standard; + Require(request.context.memory.data() == standard.data()); + request.context.memory = std::span(owned).first(1); + const auto copy = request; + Require(copy.context.memory.data() == owned.data() && copy.context.memory.front().bytes.data() == bytes.data()); + request.context.memory = {owned.data(), owned.size()}; + const ShaderRecompiler::RequestSerializer serializer; + const auto replay = serializer.Deserialize(serializer.Serialize(request)); + Require(replay.request.context.memory.size() == 1u && replay.request.context.memory.front().guestAddress == 0x4000u); + Require(std::ranges::equal(replay.request.context.memory.front().bytes, bytes)); + request.context.memory = {}; + Require(request.context.memory.empty()); +} + void InsertMeshBarycentricFlag(std::vector& bytes) { constexpr std::array draw{0x00, 0x34, 0xef, 0xcd, 0xab, 0x00, 0x00, 0x00, 57u, 0u, 0u, 0u, 4u, 0u, 0u, 0u, 9u, 0u, 0u, 0u}; const auto at = std::search(bytes.begin(), bytes.end(), draw.begin(), draw.end()); @@ -94,6 +152,7 @@ std::string EncodeBase64(const std::vector& bytes) { } int main() { + VerifyMemoryView(); const ShaderRecompiler::RequestSerializer serializer; const auto historical = serializer.Deserialize(kHistoricalVersion21); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index fc987190ec..824da14782 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -2,6 +2,8 @@ ### Build +- [GuestContext memory](../../../core/shader/recompiler/Recompiler.hpp) rejects direct construction from temporary owning ranges. The view does not own the region array or its referenced bytes. That storage must outlive the request and its consumers, including when supplied through named containers, existing spans or pointers. + - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. - The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. From 906f18f60955269258354afd868fd18c12601a65 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 02:51:59 +1300 Subject: [PATCH 0098/1360] fix(libc): convert multibyte text as UTF-8 like the console's C locale Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/CMakeLists.txt | 9 ++ core/libs/prx/libc/src/Multibyte.cpp | 153 ++++++++++++++-------- core/libs/prx/libc/src/RuneLocale.cpp | 6 +- core/libs/prx/libc/src/Time.cpp | 11 +- core/libs/prx/libc/src/WideConversion.cpp | 20 +-- core/libs/tests/GuestMultibyteLocale.cpp | 123 +++++++++++++++++ core/libs/tests/GuestRuneLocale.cpp | 8 +- core/libs/tests/GuestStandardStreams.cpp | 2 +- core/libs/tests/GuestWcsrtombs.cpp | 32 ++--- core/libs/tests/GuestWcsrtombsS.cpp | 4 +- core/libs/tests/GuestWideConversion.cpp | 4 +- core/libs/tests/GuestWideFormatting.cpp | 5 +- core/libs/tests/GuestWideStrings.cpp | 2 +- docs/dev/debt/libc.md | 4 +- 14 files changed, 282 insertions(+), 101 deletions(-) create mode 100644 core/libs/tests/GuestMultibyteLocale.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7eb5ee85e4..424da80d25 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -263,6 +263,15 @@ if(BUILD_TESTING) set_tests_properties(guest_atomic_builtins PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_multibyte_locale_tests ${LIBS_INCLUDE_DIR}/tests/GuestMultibyteLocale.cpp) + target_include_directories(guest_multibyte_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_multibyte_locale_tests PRIVATE libc) + configure_windows_unwind(guest_multibyte_locale_tests) + add_test(NAME guest_multibyte_locale COMMAND guest_multibyte_locale_tests) + if(WIN32) + set_tests_properties(guest_multibyte_locale PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_wide_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideConversion.cpp) target_include_directories(guest_wide_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_conversion_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 5865fe5933..d8576dc50a 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -52,6 +52,64 @@ bool DecodeUtf8Byte(unsigned char byte, std::uint64_t& value, std::int8_t& pendi return true; } +constexpr auto Invalid = static_cast(-1); +constexpr auto Incomplete = static_cast(-2); +constexpr std::size_t StateSize = 16; +constexpr std::size_t LocaleMaxLength = 3; + +std::size_t LocaleToWide(std::uint16_t* destination, const char* source, std::size_t count, unsigned char* state) { + if (!source) { + std::memset(state, 0, StateSize); + return 0; + } + if (!count) return Incomplete; + std::uint64_t value = 0; + std::memcpy(&value, state, sizeof(value)); + auto pending = static_cast(state[PendingOffset]); + for (std::size_t consumed = 0; consumed < count;) { + const auto byte = static_cast(source[consumed++]); + if ((pending == 0 && byte >= 0xf0) || !DecodeUtf8Byte(byte, value, pending)) { + errno = 86; + return Invalid; + } + if (pending == 0) { + if (destination) *destination = static_cast(value); + StorePending(state, 0); + return value == 0 ? 0 : consumed; + } + } + std::memcpy(state, &value, sizeof(value)); + StorePending(state, pending); + return Incomplete; +} + +std::size_t LocaleStringToWide(std::uint16_t* destination, const char** source, std::size_t capacity, unsigned char* state) { + const char* cursor = *source; + std::size_t converted = 0; + while (!destination || converted < capacity) { + std::uint16_t value = 0; + const std::size_t length = LocaleToWide(&value, cursor, LocaleMaxLength, state); + if (length == Invalid || length == Incomplete) { + if (destination) *source = cursor; + errno = 86; + return Invalid; + } + if (destination) destination[converted] = value; + if (length == 0) { + if (destination) *source = nullptr; + return converted; + } + cursor += length; + ++converted; + } + *source = cursor; + return converted; +} + +int ToInt(std::size_t result) { + return result == Invalid || result == Incomplete ? -1 : static_cast(result); +} + } extern "C" { @@ -62,51 +120,47 @@ int APS5_VABI ___mb_cur_max_nid_postfix() { return 1; } -int APS5_VABI _Getmbcurmax_nid_postfix() { return ___mb_cur_max_nid_postfix(); } +int APS5_VABI _Getmbcurmax_nid_postfix() { return static_cast(LocaleMaxLength); } -int APS5_VABI mbsinit_nid_postfix(const void*) { - return 1; +int APS5_VABI mbsinit_nid_postfix(const void* state) { + if (!state) return 1; + std::int16_t pending = 0; + std::memcpy(&pending, static_cast(state) + PendingOffset, sizeof(pending)); + return pending == 0; } -std::size_t APS5_VABI mbrtowc_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void*) { - if (!source) return 0; - if (!count) return static_cast(-2); - const auto value = static_cast(*source); - if (destination) *destination = value; - return value != 0; +std::size_t APS5_VABI mbrtowc_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state) { + thread_local unsigned char internal[StateSize]{}; + if (!source) { + destination = nullptr; + source = ""; + } + return LocaleToWide(destination, source, count, state ? static_cast(state) : internal); } std::size_t APS5_VABI mbrlen_nid_postfix(const char* source, std::size_t count, void* state) { - return mbrtowc_nid_postfix(nullptr, source, count, state); + thread_local unsigned char internal[StateSize]{}; + return LocaleToWide(nullptr, source, count, state ? static_cast(state) : internal); } int APS5_VABI mbtowc_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count) { - const auto result = mbrtowc_nid_postfix(destination, source, count, nullptr); - if (result == static_cast(-2)) { - errno = 86; - return -1; - } - return static_cast(result); + thread_local unsigned char internal[StateSize]{}; + return ToInt(LocaleToWide(destination, source, count < LocaleMaxLength ? count : LocaleMaxLength, internal)); } std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void*) { if (!destination) return 1; - if (value > 255) { - errno = 86; - return static_cast(-1); - } - *destination = static_cast(value); - return 1; + return EncodeUtf8(destination, value); } int APS5_VABI mblen_nid_postfix(const char* source, std::size_t count) { - return mbtowc_nid_postfix(nullptr, source, count); + unsigned char state[StateSize]{}; + return ToInt(LocaleToWide(nullptr, source, count, state)); } int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value) { if (!destination) return 0; - const auto result = wcrtomb_nid_postfix(destination, value, nullptr); - return result == static_cast(-1) ? -1 : static_cast(result); + return static_cast(EncodeUtf8(destination, value)); } std::size_t APS5_VABI c32rtomb_nid_postfix(char* destination, std::uint32_t value, void*) { @@ -211,24 +265,25 @@ std::size_t APS5_VABI mbrtoc16_nid_postfix(std::uint16_t* destination, const cha return static_cast(-2); } -std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state) { +std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void*) { const auto* wide = *source; - std::size_t converted = 0; - while (!destination || converted < capacity) { - char byte; - if (wcrtomb_nid_postfix(&byte, wide[converted], state) == static_cast(-1)) { - if (destination) *source = wide + converted; - return static_cast(-1); + std::size_t written = 0; + for (;; ++wide) { + char bytes[LocaleMaxLength]; + const std::size_t length = EncodeUtf8(bytes, *wide); + if (destination) { + if (length > capacity - written) { + *source = wide; + return written; + } + std::memcpy(destination + written, bytes, length); } - if (destination) destination[converted] = byte; - if (byte == '\0') { + if (*wide == 0) { if (destination) *source = nullptr; - return converted; + return written; } - ++converted; + written += length; } - *source = wide + converted; - return converted; } int APS5_VABI wcsrtombs_s_nid_postfix( @@ -303,24 +358,16 @@ int APS5_VABI wcsrtombs_s_nid_postfix( return 0; } -std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { - if (!destination) return std::strlen(*source); - std::size_t converted = 0; - while (converted < capacity) { - const auto value = static_cast((*source)[converted]); - destination[converted] = value; - if (value == 0) { - *source = nullptr; - return converted; - } - ++converted; - } - *source += converted; - return converted; +std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state) { + thread_local unsigned char internal[StateSize]{}; + if (destination) *destination = 0; + return LocaleStringToWide(destination, source, capacity, state ? static_cast(state) : internal); } std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t* dest, const char* src, std::size_t size) { - return SonyMbstowcs(dest, src, size); + if (!src) return 0; + unsigned char state[StateSize]{}; + return LocaleStringToWide(dest, &src, size, state); } } diff --git a/core/libs/prx/libc/src/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp index fa25f0412c..4f3b67a702 100644 --- a/core/libs/prx/libc/src/RuneLocale.cpp +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -147,11 +147,13 @@ std::uint32_t APS5_VABI towupper_nid_postfix(std::uint32_t c) { } std::uint32_t APS5_VABI btowc_nid_postfix(int c) { - return c == -1 ? GuestRune::Weof : static_cast(c); + const auto byte = static_cast(c); + return c == -1 || byte >= 0x80 ? GuestRune::Weof : byte; } int APS5_VABI wctob_nid_postfix(std::uint32_t c) { - return c < 256 ? static_cast(c) : -1; + const auto unit = static_cast(c); + return unit < 0x80 ? static_cast(unit) : -1; } std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc); diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index f49335af48..a0c6058270 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -525,7 +525,7 @@ GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result GuestTm* APS5_VABI gmtime_s_nid_postfix(const int64_t* timer, GuestTm* result); std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state); std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state); -int APS5_VABI ___mb_cur_max_nid_postfix(); +int APS5_VABI _Getmbcurmax_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { const ZoneScope zone; @@ -723,7 +723,7 @@ size_t APS5_VABI wcsftime_nid_postfix(std::uint16_t* str, size_t count, const st return 0; } wcsrtombs_nid_postfix(narrowFormat.get(), &formatCursor, formatLength + 1, nullptr); - const auto maxCharacterBytes = static_cast(___mb_cur_max_nid_postfix()); + const auto maxCharacterBytes = static_cast(_Getmbcurmax_nid_postfix()); if (std::numeric_limits::max() / maxCharacterBytes <= count) { errno = GuestEinval; return 0; @@ -734,13 +734,14 @@ size_t APS5_VABI wcsftime_nid_postfix(std::uint16_t* str, size_t count, const st return 0; } const int callerError = errno; - if (strftime_nid_postfix(narrow.get(), count, narrowFormat.get(), timeptr) == 0) { + if (strftime_nid_postfix(narrow.get(), count * maxCharacterBytes, narrowFormat.get(), timeptr) == 0) { errno = callerError; return 0; } const char* narrowCursor = narrow.get(); - const std::size_t length = mbsrtowcs_nid_postfix(str, &narrowCursor, count, nullptr); - if (length == static_cast(-2) || length == Failed || narrowCursor != nullptr) return 0; + const std::size_t length = mbsrtowcs_nid_postfix(nullptr, &narrowCursor, 0, nullptr); + if (length == Failed || length >= count) return 0; + mbsrtowcs_nid_postfix(str, &narrowCursor, count, nullptr); return length; } diff --git a/core/libs/prx/libc/src/WideConversion.cpp b/core/libs/prx/libc/src/WideConversion.cpp index 09fb1fd1c9..640effd1de 100644 --- a/core/libs/prx/libc/src/WideConversion.cpp +++ b/core/libs/prx/libc/src/WideConversion.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/SonyCrt.hpp" #include +#include #include #include #include @@ -17,19 +18,20 @@ extern "C" void* APS5_VABI set_constraint_handler_s_nid_postfix(void* handler) { return SonySetConstraintHandlerS(handler); } +extern "C" std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); + extern "C" std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const std::uint16_t* source, std::size_t capacity) { std::size_t count = 0; - while (!destination || count < capacity) { - const auto value = source[count]; - if (value > 255) { - errno = 86; - return static_cast(-1); + for (;; ++source) { + char bytes[MB_LEN_MAX]; + const std::size_t length = wcrtomb_nid_postfix(bytes, *source, nullptr); + for (std::size_t index = 0; index < length; ++index) { + if (destination && count == capacity) return count; + if (destination) destination[count] = bytes[index]; + if (bytes[index] == '\0') return count; + ++count; } - if (destination) destination[count] = static_cast(value); - if (value == 0) break; - ++count; } - return count; } extern "C" int APS5_VABI wcscpy_s_nid_postfix(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { diff --git a/core/libs/tests/GuestMultibyteLocale.cpp b/core/libs/tests/GuestMultibyteLocale.cpp new file mode 100644 index 0000000000..b604579966 --- /dev/null +++ b/core/libs/tests/GuestMultibyteLocale.cpp @@ -0,0 +1,123 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +std::size_t APS5_VABI mbrtowc_nid_postfix(std::uint16_t*, const char*, std::size_t, void*); +std::size_t APS5_VABI mbrlen_nid_postfix(const char*, std::size_t, void*); +int APS5_VABI mbtowc_nid_postfix(std::uint16_t*, const char*, std::size_t); +int APS5_VABI mblen_nid_postfix(const char*, std::size_t); +std::size_t APS5_VABI wcrtomb_nid_postfix(char*, std::uint16_t, void*); +int APS5_VABI wctomb_nid_postfix(char*, std::uint16_t); +int APS5_VABI mbsinit_nid_postfix(const void*); +int APS5_VABI _Getmbcurmax_nid_postfix(); +std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t*, const char*, std::size_t); +std::size_t APS5_VABI wcstombs_nid_postfix(char*, const std::uint16_t*, std::size_t); +std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t*, const char**, std::size_t, void*); +int* APS5_VABI __error_nid_postfix(); +} + +constexpr auto Invalid = static_cast(-1); +constexpr auto Incomplete = static_cast(-2); + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static std::size_t Decode(const char* text, std::size_t count, std::uint16_t& value) { + unsigned char state[16]{}; + value = 0xcccc; + *__error_nid_postfix() = 0; + return mbrtowc_nid_postfix(&value, text, count, state); +} + +static void CheckDecoding() { + std::uint16_t value = 0; + Require(_Getmbcurmax_nid_postfix() == 3, "three bytes per character at most"); + Require(Decode("A", 1, value) == 1 && value == 'A', "ASCII"); + Require(Decode("\xc3\xa9", 2, value) == 2 && value == 0xe9, "two bytes"); + Require(Decode("\xe2\x82\xac", 3, value) == 3 && value == 0x20ac, "three bytes"); + Require(Decode("\xed\xa0\x80", 3, value) == 3 && value == 0xd800, "an encoded surrogate decodes"); + Require(Decode("\xef\xbf\xbf", 3, value) == 3 && value == 0xffff, "U+FFFF decodes"); + Require(Decode("\xc1\xbf", 2, value) == 2 && value == 0x7f, "an overlong form decodes"); + Require(Decode("\xc0\x80", 2, value) == 0 && value == 0, "an overlong NUL ends the string"); + Require(Decode("\xf0\x9f\x98\x80", 4, value) == Invalid && *__error_nid_postfix() == 86, "four-byte forms are rejected"); + Require(Decode("\x80", 1, value) == Invalid && *__error_nid_postfix() == 86, "a lone continuation byte"); + Require(Decode("\xe9", 1, value) == Incomplete, "a Latin-1 byte starts a three-byte form"); + Require(Decode("\xe9" "A", 2, value) == Invalid, "the Latin-1 byte is not followed by a continuation"); + Require(Decode("\xc3", 0, value) == Incomplete, "no bytes"); +} + +static void CheckState() { + unsigned char state[16]{}; + std::uint16_t value = 0; + Require(mbrtowc_nid_postfix(&value, "\xe2", 1, state) == Incomplete && !mbsinit_nid_postfix(state), "a partial character is pending"); + Require(mbrtowc_nid_postfix(&value, "\x82", 1, state) == Incomplete, "still pending"); + Require(mbrtowc_nid_postfix(&value, "\xac", 1, state) == 1 && value == 0x20ac && mbsinit_nid_postfix(state), "the last byte completes it"); + Require(mbrtowc_nid_postfix(&value, "\xc3", 1, state) == Incomplete, "pending again"); + Require(mbrtowc_nid_postfix(&value, "A", 1, state) == Invalid && !mbsinit_nid_postfix(state), "an error leaves the state pending"); + Require(mbrtowc_nid_postfix(&value, nullptr, 1, state) == Invalid, "a null string in a pending state is an error"); + Require(mbrlen_nid_postfix(nullptr, 1, state) == 0 && mbsinit_nid_postfix(state), "mbrlen with a null string resets the state"); + Require(mbsinit_nid_postfix(nullptr) == 1, "a null state is initial"); + + mbtowc_nid_postfix(nullptr, nullptr, 0); + Require(mbtowc_nid_postfix(&value, "\xc3", 1) == -1, "mbtowc reports a partial character as -1"); + Require(mbtowc_nid_postfix(&value, "\xa9", 1) == 1 && value == 0xe9, "mbtowc keeps the partial character"); + Require(mbtowc_nid_postfix(&value, "\xe2\x82\xac", 8) == 3 && value == 0x20ac, "mbtowc limits the count to three bytes"); + mbtowc_nid_postfix(&value, "\xc3", 1); + Require(mbtowc_nid_postfix(nullptr, nullptr, 0) == 0 && mbtowc_nid_postfix(&value, "A", 1) == 1, "a null string resets mbtowc"); + Require(mblen_nid_postfix("\xc3", 1) == -1 && mblen_nid_postfix("\xa9", 1) == -1, "mblen keeps no state"); + Require(mblen_nid_postfix("\xe2\x82\xac", 3) == 3 && mblen_nid_postfix(nullptr, 0) == 0, "mblen"); +} + +static void CheckEncoding() { + char bytes[4] = {}; + Require(wcrtomb_nid_postfix(bytes, 0x7f, nullptr) == 1 && bytes[0] == 0x7f, "one byte"); + Require(wcrtomb_nid_postfix(bytes, 0xe9, nullptr) == 2 && std::memcmp(bytes, "\xc3\xa9", 2) == 0, "two bytes"); + Require(wcrtomb_nid_postfix(bytes, 0xdc00, nullptr) == 3 && std::memcmp(bytes, "\xed\xb0\x80", 3) == 0, "a surrogate is encoded alone"); + Require(wcrtomb_nid_postfix(nullptr, 0x20ac, nullptr) == 1, "a null buffer"); + Require(wctomb_nid_postfix(bytes, 0xffff) == 3 && std::memcmp(bytes, "\xef\xbf\xbf", 3) == 0 && wctomb_nid_postfix(nullptr, 1) == 0, "wctomb"); +} + +static void CheckStrings() { + const char text[] = "a\xc3\xa9\xe2\x82\xac" "b"; + std::uint16_t wide[8]; + std::memset(wide, 0xcc, sizeof(wide)); + Require(mbstowcs_nid_postfix(wide, text, 8) == 4 && wide[1] == 0xe9 && wide[2] == 0x20ac && wide[4] == 0, "mbstowcs"); + Require(mbstowcs_nid_postfix(nullptr, text, 0) == 4, "mbstowcs counts characters"); + std::memset(wide, 0xcc, sizeof(wide)); + Require(mbstowcs_nid_postfix(wide, "a\xff", 8) == Invalid && wide[0] == 'a' && wide[1] == 0xcccc, "mbstowcs stops at an invalid byte"); + + unsigned char state[16]{}; + const char* cursor = text; + std::memset(wide, 0xcc, sizeof(wide)); + Require(mbsrtowcs_nid_postfix(wide, &cursor, 2, state) == 2 && cursor == text + 3, "mbsrtowcs advances by whole characters"); + cursor = text; + std::memset(wide, 0xcc, sizeof(wide)); + Require(mbsrtowcs_nid_postfix(wide, &cursor, 0, state) == 0 && wide[0] == 0 && cursor == text, "mbsrtowcs clears the first element"); + const char invalid[] = "ab\xff"; + cursor = invalid; + *__error_nid_postfix() = 0; + Require(mbsrtowcs_nid_postfix(wide, &cursor, 8, state) == Invalid && cursor == invalid + 2 && *__error_nid_postfix() == 86, "mbsrtowcs points at the invalid byte"); + + const std::uint16_t source[] = {'a', 0xe9, 0x20ac, 0}; + char narrow[8]; + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_nid_postfix(narrow, source, 8) == 6 && std::memcmp(narrow, "a\xc3\xa9\xe2\x82\xac\0", 7) == 0, "wcstombs"); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_nid_postfix(narrow, source, 4) == 4 && std::memcmp(narrow, "a\xc3\xa9\xe2x", 5) == 0, "wcstombs fills the capacity mid-character"); + Require(wcstombs_nid_postfix(nullptr, source, 0) == 6, "wcstombs counts bytes"); +} + +int main() { + CheckDecoding(); + CheckState(); + CheckEncoding(); + CheckStrings(); + return 0; +} diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp index e677153ff7..5d801f27ac 100644 --- a/core/libs/tests/GuestRuneLocale.cpp +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -130,9 +130,11 @@ int main() { } Require(towlower_nid_postfix(0xffffffff) == 0xffffffff && towupper_nid_postfix(0xffffffff) == 0xffffffff); Require(btowc_nid_postfix(-1) == 0xffffffff && btowc_nid_postfix(0) == 0); - Require(btowc_nid_postfix('A') == 'A' && btowc_nid_postfix(0xe9) == 0xe9 && btowc_nid_postfix(-23) == 0xe9); - Require(wctob_nid_postfix('A') == 'A' && wctob_nid_postfix(0xe9) == 0xe9 && wctob_nid_postfix(0) == 0); - Require(wctob_nid_postfix(0x100) == -1 && wctob_nid_postfix(0x20ac) == -1 && wctob_nid_postfix(0xffffffff) == -1); + Require(btowc_nid_postfix('A') == 'A' && btowc_nid_postfix(0x7f) == 0x7f && btowc_nid_postfix(0x80) == 0xffffffff); + Require(btowc_nid_postfix(0xe9) == 0xffffffff && btowc_nid_postfix(-23) == 0xffffffff && btowc_nid_postfix(0x141) == 'A'); + Require(wctob_nid_postfix('A') == 'A' && wctob_nid_postfix(0x7f) == 0x7f && wctob_nid_postfix(0x80) == -1 && wctob_nid_postfix(0) == 0); + Require(wctob_nid_postfix(0xe9) == -1 && wctob_nid_postfix(0x20ac) == -1 && wctob_nid_postfix(0xffffffff) == -1); + Require(wctob_nid_postfix(0x10041) == 'A' && wctob_nid_postfix(0x80000000) == 0); Require(wctrans_nid_postfix("tolower") == 1 && wctrans_nid_postfix("toupper") == 2); Require(wctrans_nid_postfix("") == 0 && wctrans_nid_postfix("TOLOWER") == 0 && wctrans_nid_postfix("tolowe") == 0); for (std::int64_t c = -300; c < 70000; ++c) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 0ba21b86b1..026ff88f78 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -587,7 +587,7 @@ int main(int argc, char** argv) { if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); CheckClosedStreamWrites(); CheckDescriptorAccessModes(); - Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); + Require(_Getmbcurmax_nid_postfix() == 3 && ___mb_cur_max_nid_postfix() == 1); CheckFlushWhileReading(); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); Require(fdopen_nid_postfix(0, nullptr) == nullptr && *__error_nid_postfix() == 22); diff --git a/core/libs/tests/GuestWcsrtombs.cpp b/core/libs/tests/GuestWcsrtombs.cpp index 745e809903..794748e08b 100644 --- a/core/libs/tests/GuestWcsrtombs.cpp +++ b/core/libs/tests/GuestWcsrtombs.cpp @@ -18,9 +18,8 @@ static void Require(bool value, const char* message) { int main() { const std::uint16_t text[] = {'a', 'b', 'c', 0}; const std::uint16_t empty[] = {0}; - const std::uint16_t invalid[] = {'a', 0x100, 'b', 0}; + const std::uint16_t twoBytes[] = {'a', 0x100, 'b', 0}; const std::uint16_t bytes[] = {0x7f, 0x80, 0xe9, 0xff, 0}; - constexpr auto Failed = static_cast(-1); std::uint64_t state[2]{}; char out[8]; const auto* source = text; @@ -46,8 +45,8 @@ int main() { Require(wcsrtombs_nid_postfix(out, &source, 1, state) == 0, "empty string length"); Require(source == nullptr && out[0] == 0 && out[1] == 'x', "empty string terminator"); source = bytes; - Require(wcsrtombs_nid_postfix(out, &source, sizeof(out), state) == 4, "guest single-byte conversion length"); - Require(source == nullptr && std::memcmp(out, "\x7f\x80\xe9\xff\0", 5) == 0, "guest high bytes"); + Require(wcsrtombs_nid_postfix(out, &source, sizeof(out), state) == 7, "UTF-8 conversion length"); + Require(source == nullptr && std::memcmp(out, "\x7f\xc2\x80\xc3\xa9\xc3\xbf\0", 8) == 0, "high characters become two UTF-8 bytes"); source = text; Require(wcsrtombs_nid_postfix(nullptr, &source, 0, state) == 3 && source == text, "length query ignores capacity and preserves source"); @@ -55,21 +54,16 @@ int main() { source = empty; Require(wcsrtombs_nid_postfix(nullptr, &source, 0, state) == 0 && source == empty, "empty length query"); - source = invalid; + source = twoBytes; std::memset(out, 'x', sizeof(out)); - *__error_nid_postfix() = 0; - Require(wcsrtombs_nid_postfix(out, &source, sizeof(out), state) == Failed, "invalid character fails conversion"); - Require(*__error_nid_postfix() == 86, "invalid character uses guest EILSEQ"); - Require(source == invalid + 1 && std::memcmp(out, "axxxxxxx", 8) == 0, "error preserves prefix and identifies invalid character"); *__error_nid_postfix() = 7; - Require(wcsrtombs_nid_postfix(out, &source, 0, state) == 0 && source == invalid + 1, "zero capacity does not convert invalid character"); - Require(*__error_nid_postfix() == 7, "zero capacity preserves errno"); - - source = invalid; - Require(wcsrtombs_nid_postfix(nullptr, &source, 0, state) == Failed, "length query rejects invalid character"); - Require(*__error_nid_postfix() == 86 && source == invalid, "failed length query preserves source and reports guest EILSEQ"); - source = invalid; - *__error_nid_postfix() = 7; - Require(wcsrtombs_nid_postfix(out, &source, 1, state) == 1 && source == invalid + 1, "capacity stops before invalid character"); - Require(*__error_nid_postfix() == 7, "unconverted invalid character does not set errno"); + Require(wcsrtombs_nid_postfix(out, &source, sizeof(out), state) == 4, "a character above 0xff converts"); + Require(source == nullptr && std::memcmp(out, "a\xc4\x80" "b\0", 5) == 0, "U+0100 becomes C4 80"); + Require(*__error_nid_postfix() == 7, "conversion preserves errno"); + source = twoBytes; + Require(wcsrtombs_nid_postfix(nullptr, &source, 0, state) == 4 && source == twoBytes, "length query counts UTF-8 bytes"); + source = twoBytes; + std::memset(out, 'x', sizeof(out)); + Require(wcsrtombs_nid_postfix(out, &source, 2, state) == 1 && source == twoBytes + 1, "a character that does not fit stops the conversion"); + Require(std::memcmp(out, "axxxxxxx", 8) == 0, "no partial character is written"); } diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp index 758ff5d33c..6940e1613d 100644 --- a/core/libs/tests/GuestWcsrtombsS.cpp +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -32,7 +32,7 @@ int main() { auto run = Convert(text, 0, 0, false); Require(run.status == 0 && run.result == 3 && run.source == text); run = Convert(wide, 0, Huge, false); - Require(run.status == 86 && run.result == Failed && run.source == wide); + Require(run.status == 0 && run.result == 4 && run.source == wide); run = Convert(text, 5, 1, false); Require(run.status == 22 && run.result == Failed && run.source == text); @@ -53,7 +53,7 @@ int main() { Require(run.status == 34 && run.result == Failed && run.source == text + 1 && std::memcmp(run.out, "\0x", 2) == 0); run = Convert(wide, 8, 8); - Require(run.status == 86 && run.result == Failed && run.source == wide && std::memcmp(run.out, "a\0xx", 4) == 0); + Require(run.status == 0 && run.result == 4 && run.source == nullptr && std::memcmp(run.out, "a\xc4\x80" "b\0", 5) == 0); run = Convert(text, 0, 8); Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == 'x'); diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 335e193708..61f38d64b6 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -155,7 +155,7 @@ static void CheckScan() { Require(swscanf_s_nid_postfix(u"5", nullptr) == -1, "null format"); Require(Throws([&] { swscanf_s_nid_postfix(u"abc", u"%l[a-c]", text, std::size_t{6}); }), "scanset range throws"); Require(Throws([&] { swscanf_s_nid_postfix(u"5", u"%Ld", &wide); }), "%Ld throws"); - Require(Throws([&] { swscanf_s_nid_postfix(u"Ā", u"%s", narrow, std::size_t{4}); }), "unconvertible narrow %s throws"); + Require(swscanf_s_nid_postfix(u"Ā", u"%s", narrow, std::size_t{4}) == 1 && std::strcmp(narrow, "\xc4\x80") == 0, "narrow %s stores UTF-8"); } static void CheckUnsizedScan() { @@ -220,7 +220,7 @@ static void CheckUnsizedScan() { Require(Throws([&] { swscanf_nid_postfix(u"abc", u"%l[a-c]", text); }), "swscanf scanset range throws"); Require(Throws([&] { swscanf_nid_postfix(u"5", u"%Ld", &wide); }), "swscanf %Ld throws"); Require(Throws([&] { swscanf_nid_postfix(u"1", u"%lp", &pointer); }), "swscanf %lp throws"); - Require(Throws([&] { swscanf_nid_postfix(u"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); + Require(swscanf_nid_postfix(u"Ā", u"%s", narrow) == 1 && std::strcmp(narrow, "\xc4\x80") == 0, "swscanf narrow %s stores UTF-8"); } int main() { diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 818f401ffd..099d4d9894 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -267,8 +267,9 @@ static void CheckWcsftime() { Require(wcsftime_nid_postfix(buffer, 0, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime zero size"); Require(wcsftime_nid_postfix(buffer, 32, u"", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime empty format"); Require(wcsftime_nid_postfix(nullptr, 32, u"%Y", &time) == 0 && error == 5, "wcsftime null buffer"); - Require(wcsftime_nid_postfix(buffer, 32, u"%Y \u20ac", &time) == 0 && error == 86 && buffer[0] == u'x', - "wcsftime unconvertible format"); + Require(wcsftime_nid_postfix(buffer, 32, u"%Y \u20ac", &time) == 6 && buffer == std::u16string(u"2024 \u20ac"), + "wcsftime passes a character above 0xff through UTF-8"); + Require(wcsftime_nid_postfix(buffer, 7, u"%Y \u20ac", &time) == 6, "wcsftime sizes the narrow buffer for three-byte characters"); error = 5; Require(wcsftime_nid_postfix(buffer, SIZE_MAX, u"%Y", &time) == 0 && error == 22 && buffer[0] == u'x', "wcsftime oversized buffer"); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 809d47fe3e..3456f7b3da 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -121,7 +121,7 @@ int main() { require(mblen_nid_postfix(nullptr, 1) == 0 && mblen_nid_postfix("ab", 2) == 1 && mblen_nid_postfix("", 1) == 0); require(wctomb_nid_postfix(nullptr, 0x41) == 0); require(wctomb_nid_postfix(out, 0x42) == 1 && out[0] == 'B'); - require(wctomb_nid_postfix(out, 0x263a) == -1); + require(wctomb_nid_postfix(out, 0x263a) == 3 && std::memcmp(out, "\xe2\x98\xba", 3) == 0); } const char16_t split[] = {u'a', u'b', 0, u'c', 0}; require(wcslen_nid_postfix(u"") == 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 931d7d1746..71bc2f3c58 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -66,7 +66,7 @@ - [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process -- [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported +- [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. The multibyte conversions follow the C locale of the console's `libSceLibcInternal.sprx`: UTF-8 of at most three bytes, so `_Getmbcurmax` returns 3, overlong forms and encoded surrogates decode, and four-byte forms are invalid. `__mb_cur_max` stays 1 like the console's variable; `___mb_cur_max` is not in that module and also stays 1, unverified. Other locales are not supported. With a null destination, `wcstombs` returns the byte count; the console's count is wrong when the capacity argument equals the running count, because it reads past the terminator - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) @@ -100,7 +100,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not go through the guest's `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. From b191f43718d164d42c70e05894e1542bca94a8ed Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:07:26 +0200 Subject: [PATCH 0099/1360] fix(libc): fail fread and fwrite with EINVAL when size times count overflows FreeBSD 11.4 fread and fwrite return 0 with errno EINVAL and the error flag set when size * count does not fit size_t (lib/libc/stdio/fread.c:78-86, fwrite.c:67-75). The guest threw std::overflow_error, which ends the title. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 4 ++-- core/libs/tests/FileStreams.cpp | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 4b8f5328ba..845acb2ee4 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -432,7 +432,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File auto* handle = GetStreamHandle(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); - if (count > std::numeric_limits::max() / size) throw std::overflow_error("fread: buffer size overflow"); + if (count > std::numeric_limits::max() / size) { errno = 22; stream->SetError(); return 0; } const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fread: buffer address overflow"); const GuestArena::HostWrite destination(buffer, bytes); @@ -452,7 +452,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fwrite: null buffer"); - if (count > std::numeric_limits::max() / size) throw std::overflow_error("fwrite: buffer size overflow"); + if (count > std::numeric_limits::max() / size) { errno = 22; stream->SetError(); return 0; } const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fwrite: buffer address overflow"); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, bytes); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 0a4742b900..ba7279cfee 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +141,13 @@ int main(int argc, char** argv) { ExpectException([&] { fputs_nid_postfix(nullptr, stream); }); ExpectException([&] { fwrite_nid_postfix(nullptr, 1, sizeof(payload), stream); }); errno = 0; + Require(fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) != 0); + clearerr_nid_postfix(stream); + errno = 0; + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) != 0); + clearerr_nid_postfix(stream); + Require(ferror_nid_postfix(stream) == 0); + errno = 0; Require(fseek_nid_postfix(stream, 0, -1) == -1); Require(errno == EINVAL); Require(fclose_nid_postfix(stream) == 0); From 02dde0f5175d95f7bfaafdda8ae67f7c9f7333c9 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Mon, 19 Oct 2026 18:33:56 +0300 Subject: [PATCH 0100/1360] feat(agc): expose expanded FMASK for tracked native owners Import an eight-fragment DCC-cleared owner with disjoint watched metadata, capture its independent CB FMASK address and XOR, and expose expanded indices only for an exact matching native view. Reject standalone compressed reads, descriptor mismatches and metadata writes. Exercise direct/registered/prepared dispatches and invalidation with a cold GPU fixture. Descriptor metadata normalization builds on Romans Pokrovskis and MarcZX100 work in #4790; standalone CPU decoding remains outside this change. Expanded indices follow AMD PAL c5e800072a32f68b6ccc4422936d96167c6e0728. Written with AI assistance. Co-authored-by: MarcZX100 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/include/State.hpp | 2 + .../Graphics/src/GuestTextureResource.cpp | 6 +- .../Graphics/src/MultisampleTarget.cpp | 102 +++++++- .../Graphics/src/ShaderResources.cpp | 9 + .../libSceAgcDriver/Graphics/src/State.cpp | 8 + .../tests/GuestTextureResource.cpp | 9 + .../execution/SampleMultisampleTarget.cpp | 237 ++++++++++++++++-- .../Optimization/src/ResourceMaterializer.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 1 + 12 files changed, 363 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 83d79012a9..57300bd1c2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1875,6 +1875,11 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) + add_test(NAME agc_driver_native_fmask COMMAND agc_driver_sample_multisample_target_tests --native-fmask) + set_tests_properties(agc_driver_native_fmask PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_fmask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input) set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 6f40129b56..89e151fb24 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -90,6 +90,7 @@ struct GuestTextureResource { std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; std::uint32_t samples = 1; + std::uint64_t fmaskCmaskAddress = 0; }; float EffectiveMinLod(const GuestTextureResource& resource); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index ae817a5039..5395dec17c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -66,6 +66,7 @@ bool MultisampleTargetStorable(const Context& context, const ColorTarget& target void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); +std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ClearMultisampleTargets(VkDevice device); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 22eb13371f..18cef08750 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -68,6 +68,8 @@ struct ColorTarget { std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; std::uint32_t dccControl = 0; + std::uint64_t fmaskAddress = 0; + std::uint32_t fmaskPipeBankXor = 0; }; struct DepthTarget { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3c81c9c97e..2d46a95acb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -140,7 +140,8 @@ GuestTextureResource DecodeTextureResource(std::span words) const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast(format)); if (fmask) { Require(format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented"); - Require(!metaCompress && !writeCompress, "FMASK CMASK compression is not implemented"); + Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata"); + Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned"); Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface"); for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); } @@ -229,7 +230,8 @@ GuestTextureResource DecodeTextureResource(std::span words) result.dstSelZ = static_cast(dstSelZ); result.dstSelW = static_cast(dstSelW); result.bcSwizzle = static_cast(bcSwizzle); - result.dccAddress = metaCompress ? metaAddr << 8u : 0u; + result.dccAddress = metaCompress && !fmask ? metaAddr << 8u : 0u; + result.fmaskCmaskAddress = metaCompress && fmask ? metaAddr << 8u : 0u; result.dccAlphaOnMsb = dccAlphaPos; result.dccPipeAligned = metaPipeAligned; result.iterate256 = iterate256; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index f9f3c27c14..2a82b8d2a1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -8,6 +8,8 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -44,6 +46,43 @@ std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) { return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes); } +GuestTextureResource fmaskSnapshotResource(const GuestTextureResource& resource) { + Require(resource.format == static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt) && resource.fmaskCmaskAddress != 0u, "FMASK image format read semantics are not implemented"); + Require(resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray, "FMASK image dimension is not implemented"); + Require(resource.tileMode == TextureTileMode::kZ64KBX && (resource.pipeBankXor & ~0xcf00u) == 0u, "FMASK image swizzle or pipe/bank XOR is not implemented"); + Require(resource.width != 0u && resource.width <= 16384u && resource.height != 0u && resource.height <= 16384u && resource.depthOrLastArray < 8192u && resource.baseArray <= resource.depthOrLastArray, "invalid FMASK image extent or layer range"); + Require(resource.dimension == TextureDimension::k2DArray || (resource.depthOrLastArray == 0u && resource.baseArray == 0u), "FMASK 2D image has an array range"); + Require(resource.mipCount == 1u && resource.allocatedMipCount == 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.minLod == 0u, "FMASK image mip fields are not implemented"); + Require(resource.fmaskCmaskAddress == 0u || resource.dccPipeAligned, "FMASK image CMASK must be pipe aligned"); + for (const auto selector : {resource.dstSelX, resource.dstSelY, resource.dstSelZ, resource.dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK image selects an unverified channel"); + auto snapshot = resource; + snapshot.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt); + snapshot.tileMode = TextureTileMode::kLinear; + snapshot.pipeBankXor = 0u; + snapshot.dccAddress = 0u; + snapshot.fmaskCmaskAddress = 0u; + snapshot.dccAlphaOnMsb = false; + snapshot.dccPipeAligned = false; + return snapshot; +} + +GuestTextureResource fmaskResource(const ColorTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.fmaskAddress; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1u; + resource.allocatedMipCount = 1u; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt); + resource.dstSelX = 4u; + resource.pipeBankXor = target.fmaskPipeBankXor; + resource.fmaskCmaskAddress = target.cmaskAddress; + resource.dccPipeAligned = true; + return resource; +} + bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } @@ -205,6 +244,28 @@ public: recorder.Keep(shared_from_this(), static_cast(storable.bytes)); } + std::shared_ptr FmaskSampled(const GuestTextureResource& resource, VkComponentMapping components) { + const auto snapshot = fmaskSnapshotResource(resource); + Require(target.samples == 8u && target.dccAddress != 0u && fmaskRange.has_value(), "native FMASK sampling requires a tracked eight-fragment DCC target"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.depth == 1u && target.depthSlice == 0u && target.layers == 1u && target.arrayLastSlice == 0u && target.arraySlice == 0u && target.mip == 0u && target.mipCount == 1u, "native FMASK target layout is not implemented"); + Require(target.fmaskAddress == resource.baseAddress && target.fmaskPipeBankXor == resource.pipeBankXor && target.cmaskAddress == resource.fmaskCmaskAddress, "native FMASK descriptor does not match its resident target"); + Require(target.extent.width == resource.width && target.extent.height == resource.height && resource.baseArray == 0u && resource.depthOrLastArray == 0u, "native FMASK extent or subresource does not match its resident target"); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Device() == context.device && !Recorder::InCompletion(), "native FMASK sampling requires its device's active recorder outside completion"); + ApplyFastClear(target); + Keep(); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(resource.dimension)}; + auto& texture = fmaskTextures[key]; + if (texture == nullptr) { + const auto geometry = DescribeSurface(snapshot); + std::vector bytes(geometry.guestBytes); + constexpr std::uint32_t expanded = 0x76543210u; + for (std::size_t offset = 0; offset < bytes.size(); offset += sizeof(expanded)) std::memcpy(bytes.data() + offset, &expanded, sizeof(expanded)); + texture = std::make_unique(context, *context.detiler, snapshot, components, bytes); + } + return {shared_from_this(), texture.get()}; + } + void Keep() { if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Device() == context.device) { recorder->Keep(shared_from_this()); @@ -371,6 +432,8 @@ private: VkImageView storageView = VK_NULL_HANDLE; bool storageWritten = false; VkDeviceSize allocationBytes = 0; + std::optional fmaskRange; + std::map, std::unique_ptr> fmaskTextures; std::map, std::shared_ptr> textures; std::vector> retiredTextures; std::vector dccRanges; @@ -426,18 +489,29 @@ private: Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented"); } } + if (use.samples == 8u && use.fmaskAddress != 0u && use.cmaskAddress != 0u) { + const auto resource = fmaskResource(use); + static_cast(fmaskSnapshotResource(resource)); + fmaskRange = GuestMemory::UnchangedQuery{use.fmaskAddress, static_cast(DescribeSurface(resource).guestBytes), 0}; + auto& range = *fmaskRange; + GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 65536u); + Require(GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "native FMASK contents have no write generation"); + for (const auto& other : dccRanges) Require(range.address >= other.address + other.bytes || other.address >= range.address + range.bytes, "native FMASK overlaps its color or metadata planes"); + } std::vector dcc(dccRanges[1].bytes); GuestMemory::Read(use.dccAddress, dcc); std::vector cmask(use.cmaskBytes); if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); const auto clear = MultisampledDccClear(use, dcc, cmask); - Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import"); + Require(GuestMemory::UnchangedSinceAll(dccRanges) && (!fmaskRange || GuestMemory::UnchangedSinceAll(std::span(&*fmaskRange, 1u))), "initial multisampled DCC contents changed during import"); return clear; } void requireDccRange(bool valid, const GuestMemory::UnchangedQuery& range, const char* reason) const { if (valid) return; - const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : "CMASK"; + const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : range.address == target.fmaskAddress ? "FMASK" : "CMASK"; char text[384]; std::snprintf(text, sizeof(text), "%s (%s range 0x%llx+0x%zx, %u samples)", reason, plane, static_cast(range.address), range.bytes, target.samples); Require(false, text); @@ -458,9 +532,18 @@ private: requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident multisampled DCC target or its metadata are not implemented"); } + if (fmaskRange) { + Require(use.fmaskAddress == target.fmaskAddress && use.fmaskPipeBankXor == target.fmaskPipeBankXor, "native FMASK target layout changed"); + const auto& range = *fmaskRange; + requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident native FMASK are not implemented"); + } } void release() noexcept { + fmaskTextures.clear(); textures.clear(); retiredTextures.clear(); if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); @@ -535,6 +618,7 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ if (target.dccAddress != 0) { guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); + if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid}); } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); @@ -989,6 +1073,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 } } +std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + std::shared_ptr owner; + for (const auto& candidate : targets()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device) continue; + const auto& target = candidate->target; + if (target.fmaskAddress != resource.baseAddress && (resource.fmaskCmaskAddress == 0u || target.cmaskAddress != resource.fmaskCmaskAddress)) continue; + Require(owner == nullptr, "FMASK descriptor refers to more than one resident target"); + owner = candidate; + } + return owner != nullptr ? owner->FmaskSampled(resource, components) : nullptr; +} + void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 41a665bdac..ba1033e46f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4,6 +4,7 @@ #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -676,6 +677,14 @@ std::shared_ptr cachedTexture(const Context& context, std::span(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1..3f63cf07ae 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1759,6 +1759,14 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(cmask), totalCmaskBytes, CmaskLayout::Alignment, true); } color.cmaskFastClear = (info & 0x2000u) != 0; + if (const auto fmask = find(cx, 0x321 + stride); color.samples > 1u && fmask != cx.end()) { + const auto high = find(cx, 0x3a0 + slot); + const auto fmaskHigh = high == cx.end() ? 0u : high->second; + Require((fmaskHigh & ~0xffu) == 0u, "invalid FMASK address extension"); + const auto address = (static_cast(fmaskHigh) << 40u) | (static_cast(fmask->second) << 8u); + color.fmaskPipeBankXor = static_cast(address & 0xffffu); + color.fmaskAddress = address - color.fmaskPipeBankXor; + } } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index db38d183f5..050b2bdc17 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -123,6 +123,15 @@ void RunGuestTextureResourceTests() { fmask.dstSelY = fmask.dstSelZ = fmask.dstSelW = 0u; const auto fmaskResource = DecodeTextureResource(pack(fmask)); Require(fmaskResource.format == 20u && fmaskResource.dimension == TextureDimension::k2D && fmaskResource.tileMode == TextureTileMode::kZ64KBX, "uncompressed 32-bit FMASK did not use an integer Z surface"); + auto compressedFmask = fmask; + compressedFmask.metaCompress = compressedFmask.metaPipeAligned = true; + compressedFmask.metaAddr = 0x123456789aull; + const auto compressedResource = DecodeTextureResource(pack(compressedFmask)); + Require(compressedResource.fmaskCmaskAddress == compressedFmask.metaAddr << 8u && compressedResource.dccAddress == 0u && compressedResource.dccPipeAligned, "compressed FMASK CMASK was interpreted as color DCC"); + compressedFmask.metaPipeAligned = false; + rejectFields(compressedFmask, "FMASK CMASK metadata must be pipe aligned"); + compressedFmask.metaPipeAligned = compressedFmask.writeCompress = true; + rejectFields(compressedFmask, "FMASK CMASK compression is not implemented"); Fields fmaskArray = fmask; fmaskArray.typeRaw = 13u; fmaskArray.depth = 2u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 4d4f77cca1..212bc05cfa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -9,6 +9,9 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "Recompiler.hpp" #include "Optimization/ResourceProgram.hpp" #include "VulkanTestDevice.hpp" @@ -17,6 +20,8 @@ #define NOMINMAX #endif #include +#else +#include #endif #include #include @@ -120,14 +125,31 @@ constexpr std::array, 3> Triangle{{ alignas(256) std::array Buffer{}; struct Block { - explicit Block(std::size_t bytes) : bytes(bytes) { + explicit Block(std::size_t bytes, bool watched = false) : bytes(bytes), watched(watched) { #ifdef _WIN32 - data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } #else - data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } #endif - Require(data != nullptr, "cannot allocate the multisampled color target"); + Require(data != nullptr, "cannot allocate the color target"); std::memset(data, 0, bytes); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); GuestAllocations::Mutation mutation; mutation.Add(data, bytes, true, true, true); } @@ -137,15 +159,26 @@ struct Block { mutation.Remove(data); } #ifdef _WIN32 - VirtualFree(data, 0, MEM_RELEASE); + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } #else - std::free(data); + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } #endif } Block(const Block&) = delete; Block& operator=(const Block&) = delete; std::uint64_t Address() const { return reinterpret_cast(data); } std::size_t bytes; + bool watched; std::uint8_t* data = nullptr; }; @@ -214,7 +247,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c state.blends = {blend}; state.blend = blend; state.blendConstants = {}; - state.samples = Samples; + state.samples = color.samples; return state; } @@ -349,19 +382,19 @@ void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array> RunLoad(AgcDriver::VulkanDevice& device, const std::array& texture, bool arrayed) { +std::vector> RunLoad(AgcDriver::VulkanDevice& device, const std::array& texture, bool arrayed, std::uint32_t fragments) { std::vector userData; const auto program = CompileLoad(device, texture, userData, arrayed); - std::vector> loaded(Side * Side * Samples); + std::vector> loaded(Side * Side * fragments); const auto codeAddress = reinterpret_cast((arrayed ? ArrayLoadCode : LoadCode).data()); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { Buffer.fill(0xdeadbeefu); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto index = first + tid; - const auto pixel = index / Samples; + const auto pixel = index / fragments; const std::array words = arrayed - ? std::array{0u, 0u, pixel % Side, pixel / Side, 0u, index % Samples} - : std::array{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + ? std::array{0u, 0u, pixel % Side, pixel / Side, 0u, index % fragments} + : std::array{0u, 0u, 0u, pixel % Side, pixel / Side, index % fragments}; std::copy(words.begin(), words.end(), &Buffer[tid * Words]); } device.Dispatch(program, 1, 1, 1, {}, codeAddress); @@ -373,9 +406,9 @@ std::vector> RunLoad(AgcDriver::VulkanDevice& device, const return loaded; } -std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture) { - auto loaded = RunLoad(device, texture, false); - const auto loadedArray = RunLoad(device, texture, true); +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture, std::uint32_t fragments = Samples) { + auto loaded = RunLoad(device, texture, false, fragments); + const auto loadedArray = RunLoad(device, texture, true, fragments); Require(loadedArray == loaded, "Dim2DMsaaArray image_load disagreed with Dim2DMsaa image_load on the multisampled color target"); return loaded; } @@ -643,12 +676,186 @@ int NativeVertexInput(std::unique_ptr& device) { return 0; } +alignas(256) constexpr std::array NativeFmaskCode{0x34020084u, 0xe0381000u, 0x80000201u, 0xbf8c3f70u, 0xf0001f08u, 0x00020602u, 0xbf8c3f70u, 0xe0781000u, 0x80010601u, 0xbf810000u}; +alignas(256) std::array NativeFmaskInput{}; +alignas(256) std::array NativeFmaskOutput{}; + +std::array NativeFmaskDescriptor(const Block& fmask, const Block& cmask, std::uint32_t pipeXor) { + const auto address = fmask.Address() | pipeXor; + const auto metadata = cmask.Address(); + return {static_cast(address >> 8u), static_cast(address >> 40u) | (166u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 4u | (1u << 3u) | (4u << 6u) | (24u << 20u) | (9u << 28u), 0u, 0u, (1u << 19u) | (1u << 21u) | (static_cast(metadata >> 8u) << 24u), static_cast(metadata >> 16u)}; +} + +ShaderRecompiler::RecompileResult CompileNativeFmask(AgcDriver::VulkanDevice& device, const std::array& image, bool registration) { + std::vector userData(16u); + const auto buffer = [](const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return std::array{static_cast(address), static_cast(address >> 32u) & 0xffffu, bytes, 0x31016facu}; + }; + const auto input = buffer(NativeFmaskInput.data(), sizeof(NativeFmaskInput)); + const auto output = buffer(NativeFmaskOutput.data(), Threads * 16u); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4u); + std::copy(image.begin(), image.end(), userData.begin() + 8u); + const std::array memory{{{reinterpret_cast(NativeFmaskCode.data()), std::as_bytes(std::span(NativeFmaskCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + auto target = device.Target(); + target.registrationModules = registration; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(NativeFmaskCode.data()), NativeFmaskCode, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, memory}, target, {0u, 0u, 0u, 128u}}; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void DispatchNativeFmask(AgcDriver::VulkanDevice& device, const std::array& image, bool registration) { + device.Dispatch(CompileNativeFmask(device, image, registration), 1u, 1u, 1u); + device.WaitIdle(); +} + +void NativeFmask(std::unique_ptr& device) { + using namespace AgcDriver::Graphics; + Block colorMemory(4u * BlockBytes, true); + Block dcc(BlockBytes, true); + Block cmask(BlockBytes, true); + Block fmask(BlockBytes, true); + Block unrelated(BlockBytes, true); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + constexpr std::uint32_t sentinel = 0xdeadbeefu; + for (const auto pipeXor : {0u, 0xf00u}) { + std::memset(colorMemory.data, 0, colorMemory.bytes); + std::memset(dcc.data, 0, dcc.bytes); + std::memset(cmask.data, 0xcc, cmask.bytes); + std::memset(fmask.data, 0x11, fmask.bytes); + AgcDriver::Registers cx{{0x318, static_cast(colorMemory.Address() >> 8u)}, {0x31b, 0u}, {0x31c, Float16x4Info | 0x10006000u}, {0x31d, (3u << 12u) | (3u << 15u)}, {0x31e, 0u}, {0x31f, static_cast(cmask.Address() >> 8u)}, {0x321, static_cast((fmask.Address() | pipeXor) >> 8u)}, {0x325, static_cast(dcc.Address() >> 8u)}, {0x390, static_cast(colorMemory.Address() >> 40u)}, {0x398, static_cast(cmask.Address() >> 40u)}, {0x3a0, static_cast(fmask.Address() >> 40u)}, {0x3a8, static_cast(dcc.Address() >> 40u)}, {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}}; + const auto color = DecodeColorBuffer(cx, 0u); + Require(color.samples == 8u && color.cmaskAddress == cmask.Address(), "native FMASK fixture lost its decoded target association"); + std::array, 8> expected{}; + for (std::uint32_t sample = 0u; sample < 8u; ++sample) { + expected[sample] = {static_cast(sample + 1u) / 8.0f, 0.25f, 0.5f, 1.0f}; + auto pixel = PixelCode; + pixel[1] = std::bit_cast(expected[sample][0]); + pixel[3] = std::bit_cast(expected[sample][1]); + pixel[5] = std::bit_cast(expected[sample][2]); + pixel[7] = std::bit_cast(expected[sample][3]); + auto state = TargetState(color); + state.quadSampleMask.fill(static_cast(1u << sample)); + Draw(*device, state, 0u, 2u, pixel); + } + const auto native = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u); + std::vector covered; + for (std::uint32_t pixel = 0; pixel < Side * Side; ++pixel) { + bool all = true; + for (std::size_t sample = 0; sample < 8u; ++sample) all = all && native[pixel * 8u + sample] == expected[sample]; + if (all) covered.push_back(pixel); + } + Require(!covered.empty(), "native FMASK fixture did not render eight distinct sample colors"); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto pixel = covered[lane % covered.size()]; + NativeFmaskInput[lane * 4u] = pixel % Side; + NativeFmaskInput[lane * 4u + 1u] = pixel / Side; + NativeFmaskInput[lane * 4u + 2u] = 0u; + NativeFmaskInput[lane * 4u + 3u] = 0u; + } + const auto image = NativeFmaskDescriptor(fmask, cmask, pipeXor); + const auto check = [&] { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::array mapping{0x76543210u, 1u, 0x76543210u, 0u}; + for (std::size_t component = 0; component < 4u; ++component) Require(NativeFmaskOutput[lane * 4u + component] == mapping[component], "native FMASK load did not preserve its expanded sample indices and selectors"); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto fragment = NativeFmaskOutput[lane * 4u] >> (4u * sample) & 15u; + const auto pixel = NativeFmaskInput[lane * 4u + 1u] * Side + NativeFmaskInput[lane * 4u]; + Require(native[pixel * 8u + fragment] == expected[sample], "native FMASK remapped a sampled color to another fragment"); + } + } + Require(std::all_of(NativeFmaskOutput.begin() + Threads * 4u, NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "native FMASK wrote beyond its output"); + }; + for (const auto registration : {false, true}) { + for (std::uint32_t repeat = 0; repeat < 2u; ++repeat) { + NativeFmaskOutput.fill(sentinel); + DispatchNativeFmask(*device, image, registration); + check(); + } + const auto program = CompileNativeFmask(*device, image, registration); + const auto codeAddress = reinterpret_cast(NativeFmaskCode.data()); + std::shared_ptr recipe; + NativeFmaskOutput.fill(sentinel); + device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, nullptr, &recipe); + device->WaitIdle(); + check(); + Require(recipe == nullptr, "native FMASK emitted a reusable recipe without an ownership proof"); + const auto prepared = device->PrepareDispatch(program, {}, Threads, codeAddress); + Require(prepared != nullptr, "native FMASK did not prepare a deferred dispatch"); + NativeFmaskOutput.fill(sentinel); + device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, prepared); + device->WaitIdle(); + check(); + } + Require(LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u) == native, "FMASK sampling changed the native samples"); + Require(std::all_of(fmask.data, fmask.data + fmask.bytes, [](auto byte) { return byte == 0x11u; }) && std::all_of(cmask.data, cmask.data + cmask.bytes, [](auto byte) { return byte == 0xccu; }), "native FMASK sampling published or changed guest metadata"); + const auto refused = [&](const auto& descriptor, std::string_view reason) { + NativeFmaskOutput.fill(sentinel); + std::string error; + try { DispatchNativeFmask(*device, descriptor, false); } catch (const std::runtime_error& e) { error = e.what(); } + Require(error.find(reason) != std::string::npos, "native FMASK missing refusal: " + error); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "rejected native FMASK wrote output"); + }; + refused(NativeFmaskDescriptor(unrelated, cmask, pipeXor), "descriptor does not match"); + refused(NativeFmaskDescriptor(fmask, unrelated, pipeXor), "descriptor does not match"); + refused(NativeFmaskDescriptor(fmask, cmask, pipeXor ^ 0x100u), "descriptor does not match"); + auto resized = image; + resized[2] += 1u; + refused(resized, "extent or subresource does not match"); + const auto deferredProgram = CompileNativeFmask(*device, image, false); + const auto codeAddress = reinterpret_cast(NativeFmaskCode.data()); + const auto preparedBeforeStore = device->PrepareDispatch(deferredProgram, {}, Threads, codeAddress); + Require(preparedBeforeStore != nullptr, "native FMASK did not prepare its metadata invalidation probe"); + std::array same{}; + same.fill(std::byte{0x11}); + AgcDriver::GuestMemory::Write(fmask.Address(), same); + refused(image, "writes to a resident native FMASK"); + NativeFmaskOutput.fill(sentinel); + std::string deferredError; + try { + device->Dispatch(deferredProgram, 1u, 1u, 1u, {}, codeAddress, preparedBeforeStore); + device->WaitIdle(); + } catch (const std::runtime_error& error) { + deferredError = error.what(); + } + Require(deferredError.find("writes to a resident native FMASK") != std::string::npos, "deferred native FMASK missed its metadata store: " + deferredError); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "rejected deferred native FMASK wrote output"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + cx[0x31c] &= ~0x2000u; + cx[0x31f] = 0u; + cx[0x398] = 0u; + const auto withoutCmask = DecodeColorBuffer(cx, 0u); + Require(withoutCmask.cmaskAddress == 0u, "the no-CMASK control retained metadata"); + Draw(*device, TargetState(withoutCmask)); + const auto uncompressed = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u); + Require(std::find(uncompressed.begin(), uncompressed.end(), Drawn) != uncompressed.end(), "tracking native FMASK broke ordinary DCC rendering without CMASK"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + std::printf("native FMASK: XOR=0x%x, eight distinct samples, both registration modes, cached reuse, association/extent/write refusals and unchanged guest metadata passed\n", pipeXor); + } +} + } int main(int argc, char** argv) { try { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--native-fmask") { + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) return VulkanTestSkipped; + NativeFmask(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device); Block block(4u * BlockBytes); Block other(BlockBytes); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..c1145b2684 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -208,7 +208,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag return rejectImage(PoisonReason::Fmask, "FMASK requires a direct sampled image load"); } if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); - if (decoded.fmask && (descriptor.dwords[6] & (3u << 20u)) != 0u) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented"); + if (decoded.fmask && ((descriptor.dwords[6] & (1u << 20u)) != 0u || ((descriptor.dwords[6] & (1u << 21u)) != 0u && (descriptor.dwords[6] >> 24u) == 0u && descriptor.dwords[7] == 0u))) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented without readable metadata"); if (decoded.fmask && (base.packed || base.byElements != 0u)) return rejectImage(PoisonReason::Fmask, "packed FMASK image reads are not implemented"); if (base.packed) { if (table || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index adf41ad5cc..c8a1ddcc72 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -141,6 +141,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. +- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes. The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. From fe1434c884791f8d1804c62e185ffb1295909a32 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 20 Oct 2026 14:26:07 +0300 Subject: [PATCH 0101/1360] fix(agc): keep FMASK locations out of pipeline recipes --- .../Graphics/src/PipelineJournal.cpp | 2 +- .../libs/prx/libSceAgcDriver/tests/Graphics.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index c7a0f679a5..34404fd1dd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 800, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6e1f5d23ee..2b6ac89e63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9859,6 +9859,8 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { color.format = VK_FORMAT_R8G8B8A8_UNORM; color.bytes = 64 * 32 * 4; color.componentMapping = 0xec; + color.fmaskAddress = 0x30000; + color.fmaskPipeBankXor = 0x1200; state.colors = {color, color}; state.colors[1].format = VK_FORMAT_B8G8R8A8_UNORM; state.colors[1].exportIndex = 1; @@ -9895,6 +9897,10 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { const auto recipe = MakeGraphicsPipelineRecipe(state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); Require(recipe.stages.size() == 2 && recipe.stages[0].identity == vertex.Identity() && recipe.stages[1].identity == fragment.Identity() && !recipe.stages[0].generated && recipe.stages[1].pushConstantOffset == PipelinePushSlotBytes && recipe.splitSets, "the recipe does not name its stages"); Require(recipe.state.colors.size() == 2 && recipe.state.colors[0].address == 0 && recipe.state.colors[0].componentMapping == 0xec && recipe.state.depth && recipe.state.depth->address == 0 && recipe.state.viewport.width == 0 && recipe.state.stages.registerValue == 0 && recipe.state.stages.tessellation && recipe.state.stages.tessellation->breakAtInstance, "the canonical state kept or dropped the wrong fields"); + Require(recipe.state.color.fmaskAddress == 0 && recipe.state.color.fmaskPipeBankXor == 0, "the canonical state kept the primary target's FMASK location"); + for (const auto& canonicalColor : recipe.state.colors) { + Require(canonicalColor.fmaskAddress == 0 && canonicalColor.fmaskPipeBankXor == 0, "the canonical state kept an attachment's FMASK location"); + } const auto bytes = EncodeGraphicsPipelineRecipe(recipe); GraphicsPipelineRecipe decoded; Require(DecodeGraphicsPipelineRecipe(bytes, decoded), "the recipe does not decode"); @@ -9909,6 +9915,17 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey}; BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout); Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline"); + auto relocatedFmask = state; + relocatedFmask.color.fmaskAddress += 0x10000; + relocatedFmask.color.fmaskPipeBankXor ^= 0x100; + for (auto& target : relocatedFmask.colors) { + target.fmaskAddress += 0x20000; + target.fmaskPipeBankXor ^= 0x200; + } + std::vector relocatedFmaskKey; + BuildPipelineStoreKey(relocatedFmaskKey, context, relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(relocatedFmaskKey == original, "the FMASK resource location changed the pipeline key"); + Require(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)) == bytes, "the FMASK resource location changed the journal recipe"); auto gridState = state; gridState.samples = 2; gridState.customSampleLocations = true; From 443aae0a7bcb831eed78b15dc2a05f31e2ce5b19 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 20 Oct 2026 14:36:28 +0300 Subject: [PATCH 0102/1360] test(agc): cover eight-sample FMASK journal identity --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2b6ac89e63..6e82007c29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9859,8 +9859,6 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { color.format = VK_FORMAT_R8G8B8A8_UNORM; color.bytes = 64 * 32 * 4; color.componentMapping = 0xec; - color.fmaskAddress = 0x30000; - color.fmaskPipeBankXor = 0x1200; state.colors = {color, color}; state.colors[1].format = VK_FORMAT_B8G8R8A8_UNORM; state.colors[1].exportIndex = 1; @@ -9897,10 +9895,6 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { const auto recipe = MakeGraphicsPipelineRecipe(state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); Require(recipe.stages.size() == 2 && recipe.stages[0].identity == vertex.Identity() && recipe.stages[1].identity == fragment.Identity() && !recipe.stages[0].generated && recipe.stages[1].pushConstantOffset == PipelinePushSlotBytes && recipe.splitSets, "the recipe does not name its stages"); Require(recipe.state.colors.size() == 2 && recipe.state.colors[0].address == 0 && recipe.state.colors[0].componentMapping == 0xec && recipe.state.depth && recipe.state.depth->address == 0 && recipe.state.viewport.width == 0 && recipe.state.stages.registerValue == 0 && recipe.state.stages.tessellation && recipe.state.stages.tessellation->breakAtInstance, "the canonical state kept or dropped the wrong fields"); - Require(recipe.state.color.fmaskAddress == 0 && recipe.state.color.fmaskPipeBankXor == 0, "the canonical state kept the primary target's FMASK location"); - for (const auto& canonicalColor : recipe.state.colors) { - Require(canonicalColor.fmaskAddress == 0 && canonicalColor.fmaskPipeBankXor == 0, "the canonical state kept an attachment's FMASK location"); - } const auto bytes = EncodeGraphicsPipelineRecipe(recipe); GraphicsPipelineRecipe decoded; Require(DecodeGraphicsPipelineRecipe(bytes, decoded), "the recipe does not decode"); @@ -9915,7 +9909,29 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey}; BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout); Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline"); - auto relocatedFmask = state; + auto fmaskState = state; + fmaskState.samples = 8; + fmaskState.color.samples = 8; + fmaskState.color.fmaskAddress = 0x30000; + fmaskState.color.fmaskPipeBankXor = 0x1200; + for (auto& target : fmaskState.colors) { + target.samples = 8; + target.fmaskAddress = 0x40000; + target.fmaskPipeBankXor = 0x2300; + } + const auto fmaskRecipe = MakeGraphicsPipelineRecipe(fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(fmaskRecipe.state.color.fmaskAddress == 0 && fmaskRecipe.state.color.fmaskPipeBankXor == 0, "the canonical state kept the primary target's FMASK location"); + for (const auto& canonicalColor : fmaskRecipe.state.colors) { + Require(canonicalColor.fmaskAddress == 0 && canonicalColor.fmaskPipeBankXor == 0, "the canonical state kept an attachment's FMASK location"); + } + const auto fmaskBytes = EncodeGraphicsPipelineRecipe(fmaskRecipe); + GraphicsPipelineRecipe fmaskDecoded; + Require(DecodeGraphicsPipelineRecipe(fmaskBytes, fmaskDecoded) && EncodeGraphicsPipelineRecipe(fmaskDecoded) == fmaskBytes, "the FMASK pipeline recipe did not round-trip byte for byte"); + std::vector fmaskKey, fmaskReplayedKey; + BuildPipelineStoreKey(fmaskKey, context, fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(fmaskReplayedKey, context, fmaskDecoded.state, fmaskDecoded.vertexInput, layouts.View(), shaders, fmaskDecoded.attachmentLayout); + Require(fmaskKey != original && fmaskKey == fmaskReplayedKey, "the decoded FMASK state keys another pipeline"); + auto relocatedFmask = fmaskState; relocatedFmask.color.fmaskAddress += 0x10000; relocatedFmask.color.fmaskPipeBankXor ^= 0x100; for (auto& target : relocatedFmask.colors) { @@ -9924,8 +9940,8 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { } std::vector relocatedFmaskKey; BuildPipelineStoreKey(relocatedFmaskKey, context, relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); - Require(relocatedFmaskKey == original, "the FMASK resource location changed the pipeline key"); - Require(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)) == bytes, "the FMASK resource location changed the journal recipe"); + Require(relocatedFmaskKey == fmaskKey, "the FMASK resource location changed the pipeline key"); + Require(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)) == fmaskBytes, "the FMASK resource location changed the journal recipe"); auto gridState = state; gridState.samples = 2; gridState.customSampleLocations = true; From 8b1cdf816d17360857e58e73e83ab0df53a99ea2 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 20 Oct 2026 15:02:47 +0300 Subject: [PATCH 0103/1360] test(agc): drop FMASK association on target recreation --- .../execution/SampleMultisampleTarget.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 212bc05cfa..709ceda2ab 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -841,6 +841,24 @@ void NativeFmask(std::unique_ptr& device) { device->WaitIdle(); ClearMultisampleTargets(device->Device()); ClearCachedTextures(device->Device()); + Draw(*device, TargetState(color)); + auto recreated = color; + recreated.pipeBankXor ^= 0x4000u; + recreated.fmaskAddress = 0u; + recreated.fmaskPipeBankXor = 0u; + Draw(*device, TargetState(recreated)); + std::array formerFmask{}; + formerFmask.fill(std::byte{0x22}); + AgcDriver::GuestMemory::Write(fmask.Address(), formerFmask); + Draw(*device, TargetState(recreated)); + auto recreatedDescriptor = TargetDescriptor(colorMemory, 3u); + recreatedDescriptor[0] |= recreated.pipeBankXor >> 8u; + const auto recreatedSamples = LoadSamples(*device, recreatedDescriptor, 8u); + Require(std::find(recreatedSamples.begin(), recreatedSamples.end(), Drawn) != recreatedSamples.end(), "former FMASK metadata affected the recreated native target"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); std::printf("native FMASK: XOR=0x%x, eight distinct samples, both registration modes, cached reuse, association/extent/write refusals and unchanged guest metadata passed\n", pipeXor); } } From 70bc319c34c0cf0b4695deb043d32bfb9c529f8c Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 20 Oct 2026 15:11:27 +0300 Subject: [PATCH 0104/1360] fix(agc): reset FMASK tracking when recreating a target --- core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 2a82b8d2a1..230b3ad13b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -295,6 +295,7 @@ public: colorFill = MultisampledColorFill(use); fromTexture = false; dccRanges.clear(); + fmaskRange.reset(); metadataValid = std::make_shared>(true); const auto initial = initialDccClear(use); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { From aec1f2c012441f76764db87459b33982df0709db Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:53:59 +0200 Subject: [PATCH 0105/1360] fix(libkernel): fail sendto to port 0 from an unbound IPv4 datagram socket with EADDRNOTAVAIL udp_output hands the destination to in_pcbconnect_setup unless the socket is bound to a specific address and port and the destination is neither INADDR_ANY nor INADDR_BROADCAST, and in_pcbconnect_setup fails port 0 with EADDRNOTAVAIL. The send went to the host, which accepted it. sendto, and sendmsg through it, now fails the same way for an AF_INET datagram socket that is not connected. connect to port 0 already did. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 16 ++++++++++++++ core/libs/tests/GuestUdp.cpp | 25 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7b64c96aae..fa0c3ae0d1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -270,6 +270,21 @@ bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t le Fail(49); return true; } +bool ZeroPortSend(const Socket& socket, const sockaddr_storage& destination) { + if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return false; + const auto& remote = reinterpret_cast(destination); + if (remote.sin_port != 0) return false; + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return false; + sockaddr_in local{}; + socklen_t localSize = sizeof(local); + const bool bound = ::getsockname(socket.value, reinterpret_cast(&local), &localSize) == 0 + && local.sin_port != 0 && local.sin_addr.s_addr != htonl(INADDR_ANY); + if (bound && remote.sin_addr.s_addr != htonl(INADDR_ANY) && remote.sin_addr.s_addr != htonl(INADDR_BROADCAST)) return false; + Fail(49); + return true; +} int Disconnect(const Socket& socket) { sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); @@ -1268,6 +1283,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st socklen_t size; if (!Address(address, addressLength, native, size)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); + if (ZeroPortSend(*socket, native)) return -1; result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); } diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index f9e50c0e20..bff396754b 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -99,6 +99,30 @@ static void CheckAddressLengths(int family) { Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); } +static void CheckSendToPortZero() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + std::array portZero{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sendto_nid_postfix(sender, "x", 1, 0, portZero.data(), portZero.size()) == -1 && *__error_nid_postfix() == 49); + char buffer[4]; + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(sendto_nid_postfix(sender, "x", 1, 0, address.data(), address.size()) == 1); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0) == 1); + const int bound = socket_nid_postfix(2, 2, 0); + Require(bound >= 0); + std::array bindAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(bound, bindAddress.data(), bindAddress.size()) == 0); + Require(sendto_nid_postfix(bound, "x", 1, 0, portZero.data(), portZero.size()) == 1); + std::array anyDestination{16, 2, 0, 0, 0, 0, 0, 0}; + Require(sendto_nid_postfix(bound, "x", 1, 0, anyDestination.data(), anyDestination.size()) == -1 && *__error_nid_postfix() == 49); + Require(close_nid_postfix(bound) == 0 && close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + static void CheckDontWait() { const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 0); @@ -136,6 +160,7 @@ int main() { CheckAddressLengths(2); CheckAddressLengths(28); CheckDontWait(); + CheckSendToPortZero(); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 04342995ea..a8a7bedf30 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -190,7 +190,7 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 are sent, where `udp_output()` fails with 49 on the unbound path but FreeBSD can send to port 0 from a socket bound to a specific address and port. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed +- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD can send to port 0; an `AF_INET6` socket still sends. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it From 0bb7b02e9f921b14944c086070e0480840d10b6a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:56:47 +0200 Subject: [PATCH 0106/1360] fix(libkernel): answer a bound IPv4 datagram send to port 0 without the host Linux's udp_sendmsg refuses port 0 with EINVAL, so a send that FreeBSD accepts (the socket is bound to a specific address and port and the destination is not INADDR_ANY or INADDR_BROADCAST) failed there. Nothing can receive on port 0, so the call now returns the length itself on every host. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 16 ++++++++-------- core/libs/tests/GuestUdp.cpp | 1 + docs/dev/debt/libkernel.md | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fa0c3ae0d1..a1c088ca80 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -270,20 +270,20 @@ bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t le Fail(49); return true; } -bool ZeroPortSend(const Socket& socket, const sockaddr_storage& destination) { - if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return false; +constexpr std::int64_t NotAnswered = -2; +std::int64_t ZeroPortSend(const Socket& socket, const sockaddr_storage& destination, std::uint64_t length) { + if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return NotAnswered; const auto& remote = reinterpret_cast(destination); - if (remote.sin_port != 0) return false; + if (remote.sin_port != 0) return NotAnswered; sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); - if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return false; + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return NotAnswered; sockaddr_in local{}; socklen_t localSize = sizeof(local); const bool bound = ::getsockname(socket.value, reinterpret_cast(&local), &localSize) == 0 && local.sin_port != 0 && local.sin_addr.s_addr != htonl(INADDR_ANY); - if (bound && remote.sin_addr.s_addr != htonl(INADDR_ANY) && remote.sin_addr.s_addr != htonl(INADDR_BROADCAST)) return false; - Fail(49); - return true; + if (bound && remote.sin_addr.s_addr != htonl(INADDR_ANY) && remote.sin_addr.s_addr != htonl(INADDR_BROADCAST)) return static_cast(length); + return Fail(49); } int Disconnect(const Socket& socket) { sockaddr_storage peer{}; @@ -1283,7 +1283,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st socklen_t size; if (!Address(address, addressLength, native, size)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); - if (ZeroPortSend(*socket, native)) return -1; + if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); } diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index bff396754b..325fcbd658 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -118,6 +118,7 @@ static void CheckSendToPortZero() { std::array bindAddress{16, 2, 0, 0, 127, 0, 0, 1}; Require(bind_nid_postfix(bound, bindAddress.data(), bindAddress.size()) == 0); Require(sendto_nid_postfix(bound, "x", 1, 0, portZero.data(), portZero.size()) == 1); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); std::array anyDestination{16, 2, 0, 0, 0, 0, 0, 0}; Require(sendto_nid_postfix(bound, "x", 1, 0, anyDestination.data(), anyDestination.size()) == -1 && *__error_nid_postfix() == 49); Require(close_nid_postfix(bound) == 0 && close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a8a7bedf30..5dbff45e3f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -190,7 +190,7 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD can send to port 0; an `AF_INET6` socket still sends. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed +- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it From bb396009e04ad1d26c5205d0d155722c48938b2c Mon Sep 17 00:00:00 2001 From: elieh999 <202313104@ua.edu.lb> Date: Tue, 20 Oct 2026 18:01:22 +0300 Subject: [PATCH 0107/1360] fix(libkernel): store the process resource limit --- .../libs/prx/libkernel/System/src/Process.cpp | 19 ++++++++++++++----- core/libs/tests/GuestSetrlimit.cpp | 8 +++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index d9edcad564..869034e4fd 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -158,6 +158,7 @@ constexpr int guestRlimitCount = 15; constexpr std::int64_t guestRlimitInfinity = std::numeric_limits::max(); constexpr std::int64_t guestStackLimit = 0x200000; constexpr std::int64_t guestProcessLimit = 1604; +constexpr std::int64_t guestProcessLimitPerUser = 1443; #ifdef _WIN32 std::int64_t hostMemoryLimit() { @@ -193,6 +194,8 @@ struct GuestResourceLimit { namespace { std::mutex openFileLimitMutex; std::optional guestOpenFileLimit; +std::mutex processLimitMutex; +GuestResourceLimit guestProcessResourceLimit{guestProcessLimit, guestProcessLimit}; } struct GuestResourceUsage { @@ -363,8 +366,13 @@ int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limi return 0; } } - if (resource == guestRlimitNproc) - throw std::runtime_error("setrlimit: RLIMIT_NPROC is not stored"); + if (resource == guestRlimitNproc) { + requested.rlim_cur = std::clamp(requested.rlim_cur, std::int64_t{1}, guestProcessLimitPerUser); + requested.rlim_max = std::clamp(requested.rlim_max, std::int64_t{1}, guestProcessLimitPerUser); + std::lock_guard lock(processLimitMutex); + guestProcessResourceLimit = requested; + return 0; + } if (requested.rlim_cur != current.rlim_cur || requested.rlim_max != current.rlim_max) throw std::runtime_error("setrlimit: changing the limit of resource " + std::to_string(resource) + " is not supported"); return 0; @@ -642,10 +650,11 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { limit->rlim_cur = guestStackLimit; limit->rlim_max = guestStackLimit; return 0; - case guestRlimitNproc: - limit->rlim_cur = guestProcessLimit; - limit->rlim_max = guestProcessLimit; + case guestRlimitNproc: { + std::lock_guard lock(processLimitMutex); + *limit = guestProcessResourceLimit; return 0; + } case guestRlimitCpu: case guestRlimitFsize: case guestRlimitCore: diff --git a/core/libs/tests/GuestSetrlimit.cpp b/core/libs/tests/GuestSetrlimit.cpp index a869eec59c..0691db0980 100644 --- a/core/libs/tests/GuestSetrlimit.cpp +++ b/core/libs/tests/GuestSetrlimit.cpp @@ -62,7 +62,13 @@ int main() { } ResourceLimit processes{}; Require(getrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0 && processes.rlim_cur == 1604 && processes.rlim_max == 1604); - Require(Throws(GUEST_RLIMIT_NPROC, processes)); + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0); + Require(getrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0 && processes.rlim_cur == 1443 && processes.rlim_max == 1443); + const ResourceLimit noProcesses{0, 0}; + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &noProcesses) == 0); + Require(getrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0 && processes.rlim_cur == 1 && processes.rlim_max == 1); + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &infinity) == 0); + Require(getrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0 && processes.rlim_cur == 1443 && processes.rlim_max == 1443); Require(Throws(GUEST_RLIMIT_STACK, ResourceLimit{0x100000, 0x200000})); ResourceLimit current{}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 04342995ea..8652bce72b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -172,7 +172,7 @@ - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit; a payload process on a PS5 (firmware 12.70) reads 0x800000000 for both limits. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load, and on macOS the smaller of the host's hard limit and `kern.maxfilesperproc`, which libkernel sets as both limits at load, since macOS fails with `EMFILE` at `kern.maxfilesperproc` whatever the soft limit. A payload process on a PS5 (firmware 12.70) reads 13952 for both limits, while `getdtablesize` returns 12555 and the process can open descriptors up to 12554. XPSemu (PPSA97358) reads `RLIMIT_NOFILE` in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK` (0x200000), `RLIMIT_RSS` and `RLIMIT_MEMLOCK` (infinity) and `RLIMIT_NPROC` (1604) report the soft and hard limits a payload process reads on a PS5 (firmware 12.70). Guest locking still follows the host: `mlock` and `sceKernelMlock` fail at the host's hard `RLIMIT_MEMLOCK` on Linux, and on Windows when the process working set cannot grow, although `RLIMIT_MEMLOCK` reports infinity. There the main thread's stack is 0x200000 as well (`pthread_attr_get_np`); here the main thread runs on the host thread's stack, whose size can differ. Whether a title process reads the same limits is unverified. -- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; `RLIMIT_NPROC` always throws; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443, as FreeBSD 11 would if the console's `maxproc` were 1604: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` lowers an `RLIMIT_NPROC` limit above `maxprocperuid` to it ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)); 1604 × 9 / 10 gives 1443. Neither value is measured: `kern.maxproc` fails with `ENOENT` in the payload process. Here that call throws, because the limit is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization +- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443. `RLIMIT_NPROC` follows FreeBSD 11 by clamping both values to 1 through `maxprocperuid`, stores the result and returns it from `getrlimit`. The initial 1604 and `maxprocperuid` 1443 follow that measurement: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` applies that bound ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)). `kern.maxproc` fails with `ENOENT` in the payload process, so the values cannot be queried independently, and process creation does not enforce the stored limit. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` From 96b190317ab556eb3ba438c83accd7815c83f86d Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:02:59 +0200 Subject: [PATCH 0108/1360] docs: use a made-up title ID in the bug report form's example --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3887b5eada..7dff644e5f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -19,7 +19,7 @@ body: id: title attributes: label: Title, test or build step - description: Title ID and content version (for example PPSA99666 01.001.001), a homebrew with its source, the ctest name, or the build step that fails. + description: Title ID and content version (for example PPSAxxxxx 01.000.000), a homebrew with its source, the ctest name, or the build step that fails. validations: required: true - type: input From dd482733e5620a41623a46fb61954110d6615eb4 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Tue, 20 Oct 2026 17:14:03 +0200 Subject: [PATCH 0109/1360] feat(libSceNgs2): report the frame layout of a PCM waveform in sceNgs2GetWaveformFrameInfo --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 18 +++++++ .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 38 ++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Parse.cpp | 50 +++++++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 14 ++++++ core/libs/prx/libSceNgs2/PendingExports.cpp | 4 -- docs/dev/debt/libSceNgs2.md | 2 +- docs/dev/debt/libSceNgs2.native.md | 1 + 7 files changed, 122 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index b508ac56a3..8985bc004c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -424,6 +424,24 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ return SCE_NGS2_OK; } +int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat* format, uint32_t* out_frame_size, uint32_t* out_num_frame_samples, uint32_t* out_units_per_frame, uint32_t* out_num_delay_samples) { + if (out_frame_size == nullptr || out_num_frame_samples == nullptr || out_units_per_frame == nullptr || out_num_delay_samples == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (format == nullptr || format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { + return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + } + if (format->waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) { + *out_frame_size = format->num_channels * sizeof(std::int16_t); + *out_num_frame_samples = 1; + *out_units_per_frame = 1; + *out_num_delay_samples = 0; + return SCE_NGS2_OK; + } + if (format->waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: the frame info of waveform type " + Ngs2Hex(format->waveform_type) + " is not implemented"); + Atrac9CodecInfo codec{}; + if (!MatchingCodecInfo(*format, codec)) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + throw std::runtime_error("NGS2: the decoder delay of an ATRAC9 waveform format is not implemented"); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index b9278520ac..065724161b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -195,6 +195,43 @@ static void TestCalcBlock() { Require(sceNgs2CalcWaveformBlock(&silent, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static void TestFrameInfo() { + const Ngs2WaveformFormat supported[] = { + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 7, 9}, + }; + const Ngs2WaveformFormat rejected[] = { + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 2, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 0, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 9, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 44100, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 0, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 192001, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, 0, 0, 0}, + }; + for (const auto& format : supported) { + Ngs2WaveformBlock block{}; + Require(sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes); + Require(FrameInfoThrows(format)); + } + for (const auto& format : rejected) { + Ngs2WaveformBlock block{}; + const int calc = sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block); + Require(calc == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == calc); + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); + } + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(nullptr, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], nullptr, &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], nullptr, &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], &values[1], nullptr, &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], &values[1], &values[2], nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, nullptr, nullptr, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); +} + static void Set32(std::vector& bytes, std::size_t offset, std::uint32_t value) { for (int i = 0; i < 4; ++i) bytes[offset + i] = static_cast(value >> (i * 8)); } @@ -504,6 +541,7 @@ int main() { TestParseLoops(); TestMalformedLoops(); TestCalcBlock(); + TestFrameInfo(); TestCorruptSuperframeAtPageEnd(); TestStreamPartitions(); TestRefill(reference); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index a2545f52e6..6a0c74f5b3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -113,9 +113,59 @@ static void TestParseFileBank() { std::filesystem::remove(path); } +static void TestFrameInfoPcm() { + for (std::uint32_t channels = 1; channels <= 8; channels++) { + for (const std::uint32_t rate : {1u, 22050u, 48000u, 192000u}) { + const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, rate, 0, 0, 0}; + std::uint32_t frameSize = 0xa1, frameSamples = 0xa2, units = 0xa3, delay = 0xa4; + Require(sceNgs2GetWaveformFrameInfo(&format, &frameSize, &frameSamples, &units, &delay) == SCE_NGS2_OK); + Require(frameSize == channels * 2 && frameSamples == 1 && units == 1 && delay == 0); + const auto file = PcmFile(channels, rate, 16, 10); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.audio_frame_size == frameSize && info.num_audio_frame_samples == frameSamples); + Require(info.num_audio_unit_per_frame == units && info.num_delay_samples == delay); + } + } + const Ngs2WaveformFormat ignored{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0x12345678, 3, 5}; + std::uint32_t frameSize = 0xa1, frameSamples = 0xa2, units = 0xa3, delay = 0xa4; + Require(sceNgs2GetWaveformFrameInfo(&ignored, &frameSize, &frameSamples, &units, &delay) == SCE_NGS2_OK); + Require(frameSize == 4 && frameSamples == 1 && units == 1 && delay == 0); +} + +static void TestFrameInfoErrors() { + const Ngs2WaveformFormat pcm{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}; + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + const auto untouched = [&] { return values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4; }; + Require(sceNgs2GetWaveformFrameInfo(&pcm, nullptr, &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], nullptr, &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], &values[1], nullptr, &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], &values[1], &values[2], nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, nullptr, nullptr, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(untouched()); + const Ngs2WaveformFormat invalid[] = { + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 0, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 9, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 0, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 192001, 0, 0, 0}, + {0x99, 0, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2, 0, 0, 0, 0}, + }; + for (const auto& format : invalid) { + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(untouched()); + } + for (const std::uint32_t type : {0u, 0x10u, 0x11u, 0x13u, 0x14u, 0x16u, SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 0x19u, 0x41u, 0xffffffffu}) { + Require(FrameInfoThrows({type, 2, 48000, 0, 0, 0})); + } +} + int main() { TestParsePcm(); TestParseFile(); TestParseFileBank(); + TestFrameInfoPcm(); + TestFrameInfoErrors(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 0f0838c354..1cdc9ecef7 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -6,12 +6,14 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceNgs2ParseWaveformData(const void*, size_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat*, uint32_t, uint32_t, Ngs2WaveformBlock*); +int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat*, uint32_t*, uint32_t*, uint32_t*, uint32_t*); int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption*); int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption*, const Ngs2ContextBufferInfo*, uintptr_t*); @@ -55,6 +57,18 @@ inline void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +inline bool FrameInfoThrows(const Ngs2WaveformFormat& format) { + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + bool threw = false; + try { + sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]); + } catch (const std::exception&) { + threw = true; + } + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); + return threw; +} + inline constexpr std::uint32_t Grain = 8; inline std::vector> buffers; diff --git a/core/libs/prx/libSceNgs2/PendingExports.cpp b/core/libs/prx/libSceNgs2/PendingExports.cpp index 992e9b2449..ef8e6a4b94 100644 --- a/core/libs/prx/libSceNgs2/PendingExports.cpp +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -21,10 +21,6 @@ int APS5_VABI sceNgs2FftQuerySize() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNgs2GetWaveformFrameInfo(void* format, std::uint32_t* outFrameSize, std::uint32_t* outNumFrameSamples, std::uint32_t* outUnitsPerFrame, std::uint32_t* outNumDelaySamples) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNgs2ParseWaveformUser(std::uintptr_t handler, std::uintptr_t userData, void* outInfo) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md index 548dfd5035..281723c3f1 100644 --- a/docs/dev/debt/libSceNgs2.md +++ b/docs/dev/debt/libSceNgs2.md @@ -3,4 +3,4 @@ ### Unknown function info - [sceNgs2FftInit, sceNgs2FftProcess, sceNgs2FftQuerySize](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. -- [sceNgs2CustomRackGetModuleInfo, sceNgs2GetWaveformFrameInfo, sceNgs2ParseWaveformUser, sceNgs2RackGetUserData, sceNgs2RackSetUserData, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo, sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. +- [sceNgs2CustomRackGetModuleInfo, sceNgs2ParseWaveformUser, sceNgs2RackGetUserData, sceNgs2RackSetUserData, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo, sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 361ee333a9..0b6b46dd35 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,6 +24,7 @@ - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before +- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - no source gives its behaviour; shadPS4 only stubs it. The outputs are taken from their names to be the frame size, frame samples, units per frame and delay samples of [`OrbisNgs2WaveformInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.h#L54-L75). Only `PCM_I16L` answers (channels * 2 bytes, 1 sample, 1 unit, 0 delay, as for parsed 16-bit PCM, itself assumed). ATRAC9 throws after the format check: its delay is the per-file encoder delay of the `fact` chunk ([vgmstream](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c#L1129-L1137)), so no codec value is known. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad channel count or rate `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified on a console - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. From 1446793c55c9c1c25774134adb2acc47b9d7d70e Mon Sep 17 00:00:00 2001 From: Leonardo Date: Tue, 20 Oct 2026 17:16:36 +0200 Subject: [PATCH 0110/1360] build(libs): check CMAKE_CXX_COMPILER_RANLIB before adjacent lookup --- core/libs/CMakeLists.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 914ba5a7aa..0c3bc38ba0 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -62,11 +62,15 @@ if(MINGW) endforeach() if(NOT EXISTS "${CMAKE_RANLIB}") - get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) - if(EXISTS "${_compiler_dir}/gcc-ranlib${CMAKE_EXECUTABLE_SUFFIX}") - set(CMAKE_RANLIB "${_compiler_dir}/gcc-ranlib${CMAKE_EXECUTABLE_SUFFIX}") - elseif(EXISTS "${_compiler_dir}/ranlib${CMAKE_EXECUTABLE_SUFFIX}") - set(CMAKE_RANLIB "${_compiler_dir}/ranlib${CMAKE_EXECUTABLE_SUFFIX}") + if(EXISTS "${CMAKE_CXX_COMPILER_RANLIB}") + set(CMAKE_RANLIB "${CMAKE_CXX_COMPILER_RANLIB}") + else() + get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + if(EXISTS "${_compiler_dir}/gcc-ranlib${CMAKE_EXECUTABLE_SUFFIX}") + set(CMAKE_RANLIB "${_compiler_dir}/gcc-ranlib${CMAKE_EXECUTABLE_SUFFIX}") + elseif(EXISTS "${_compiler_dir}/ranlib${CMAKE_EXECUTABLE_SUFFIX}") + set(CMAKE_RANLIB "${_compiler_dir}/ranlib${CMAKE_EXECUTABLE_SUFFIX}") + endif() endif() endif() if(NOT EXISTS "${CMAKE_RANLIB}") From 0ecef87c4c64b1044b380b5358272bf2af7c8eb2 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:17:59 -0700 Subject: [PATCH 0111/1360] fix(libkernel): answer Windows reparse points that are not links lstat and readlink threw NotImplemented for every Windows reparse point other than a symbolic link or a drive junction. A tag without the name surrogate bit (cloud files, deduplication) does not stand for another path, so lstat now answers as stat does and readlink returns EINVAL, as FreeBSD does for a file that is not a link and as CPython's os.lstat and os.readlink do (bpo-37834). A name surrogate that is neither a symbolic link nor a junction (a mounted volume, a WSL link) is reported as the reparse point itself with the mode of its attributes, and readlink returns EINVAL. The tests set reparse tags with FSCTL_SET_REPARSE_POINT, one without the name surrogate bit and one with it. A compact /exe (WOF) file cannot serve as the fixture: on Windows 11 24H2 the WOF filter hides the reparse point from GetFileAttributesW and FileAttributeTagInfo alike. Windows refuses every open of an unhandled tag without FILE_FLAG_OPEN_REPARSE_POINT, so the test requires lstat to agree with stat rather than to succeed. mashingaan's boykopovar/AnyPS5#6447 returned EINVAL for other reparse points without the tag check. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Fable 5.1 --- .../File/include/WindowsSymbolicLink.hpp | 14 ++++++ .../prx/libkernel/File/src/NativeStat.cpp | 9 +++- core/libs/prx/libkernel_sys/Export.cpp | 6 +-- core/libs/tests/GuestFilesystem.cpp | 32 ++++++++++++++ core/libs/tests/GuestKernelSys.cpp | 44 +++++++++++++++++++ docs/dev/debt/libkernel.md | 3 +- docs/dev/debt/libkernel_sys.md | 2 +- 7 files changed, 102 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index cdc089e731..5d3e3bb584 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -47,6 +47,20 @@ class WindowsSymbolicLink { } public: + static bool IsNameSurrogate(const std::filesystem::path& native, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + FILE_ATTRIBUTE_TAG_INFO information{}; + const BOOL read = GetFileInformationByHandleEx(handle, FileAttributeTagInfo, &information, sizeof(information)); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": GetFileInformationByHandleEx"); + return (information.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && IsReparseTagNameSurrogate(information.ReparseTag); + } + static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 3edfb3774b..fb9a6490ca 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -57,15 +57,20 @@ static int DoFstat(int fd, NativeStat* st) { static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + if (!File::WindowsSymbolicLink::IsNameSurrogate(p, "lstat")) return DoStat(p, st); std::string target; - if (!File::WindowsSymbolicLink::Read(p, target, "lstat") && !File::WindowsSymbolicLink::ReadJunction(p, target, "lstat")) - NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a symbolic link"); + const bool link = File::WindowsSymbolicLink::Read(p, target, "lstat") || File::WindowsSymbolicLink::ReadJunction(p, target, "lstat"); const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); const int result = ReadHandleInfo(handle, st); CloseHandle(handle); if (result != 0) return result; + if (!link) { + st->st_mode = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? (_S_IFDIR | 0111) : _S_IFREG; + st->st_mode |= (attributes & FILE_ATTRIBUTE_READONLY) != 0 ? 0444 : 0666; + return 0; + } st->st_mode = SymbolicLinkMode; st->st_size = static_cast<__int64>(target.size()); st->isSymbolicLink = true; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a0adb45364..540746d4b6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -144,10 +144,8 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!File::WindowsSymbolicLink::Read(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) { - NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a symbolic link"); - return -1; - } + if (!File::WindowsSymbolicLink::Read(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) + return Fail(GuestEinval); return CopyLinkTarget(target, buffer, size); } #endif diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 07b86670f3..6085dc7d51 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -6,6 +6,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/KernelErrors.hpp" #include +#include #include #include #include @@ -21,6 +22,7 @@ #ifdef _WIN32 #include #include +#include #else #include #endif @@ -992,6 +994,36 @@ int main() { const int followedJunction = sceKernelOpen(junction.string().c_str(), 0, 0); Require(followedJunction >= 0 && sceKernelClose(followedJunction) == 0); Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); + const auto tagged = root / "tagged.bin"; + { std::ofstream stream(tagged, std::ios::binary); stream << std::string(1u << 18, 'a'); } + const HANDLE taggedHandle = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedHandle != INVALID_HANDLE_VALUE); + std::array reparseData{}; + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = 0x00000ABC; + reparseHeader.ReparseDataLength = 4; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); + DWORD returned = 0; + Require(DeviceIoControl(taggedHandle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(taggedHandle) != 0); + Require((GetFileAttributesW(tagged.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + FileStat taggedStatus{}; + FileStat taggedFollowed{}; + const int taggedResult = lstat_nid_postfix(tagged.string().c_str(), &taggedStatus); + const int taggedError = *__error_nid_postfix(); + Require(stat_nid_postfix(tagged.string().c_str(), &taggedFollowed) == taggedResult); + Require(taggedResult == 0 ? (taggedStatus.st_ino == taggedFollowed.st_ino && taggedStatus.st_mode == taggedFollowed.st_mode) + : *__error_nid_postfix() == taggedError); + const HANDLE taggedCleanup = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedCleanup != INVALID_HANDLE_VALUE); + reparseHeader.ReparseDataLength = 0; + Require(DeviceIoControl(taggedCleanup, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); #endif FileStat identity{}; FileStat byDescriptor{}; diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index ea71a5644b..a6bbadefc6 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -17,6 +17,7 @@ #include #ifdef _WIN32 #include +#include #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -32,6 +33,7 @@ int APS5_VABI _fstatfs_nid_postfix(int, void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); @@ -302,6 +304,48 @@ int main() { Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); +#else + const auto setReparseTag = [](const std::filesystem::path& path, DWORD tag) { + const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + std::array reparseData{}; + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = tag; + reparseHeader.ReparseDataLength = 4; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); + DWORD returned = 0; + Require(DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(handle) != 0); + Require((GetFileAttributesW(path.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + }; + const auto clearReparseTag = [](const std::filesystem::path& path, DWORD tag) { + const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = tag; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + DWORD returned = 0; + Require(DeviceIoControl(handle, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(handle) != 0 && std::filesystem::remove(path)); + }; + const auto tagged = root / "tagged.bin"; + std::ofstream(tagged, std::ios::binary) << "abc"; + setReparseTag(tagged, 0x00000ABC); + Require(readlink_nid_postfix(tagged.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + clearReparseTag(tagged, 0x00000ABC); + const auto surrogate = root / "surrogate.bin"; + std::ofstream(surrogate, std::ios::binary) << "abc"; + setReparseTag(surrogate, 0x20000ABC); + Require(readlink_nid_postfix(surrogate.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + FileStat surrogateStatus{}; + Require(lstat_nid_postfix(surrogate.string().c_str(), &surrogateStatus) == 0); + Require(surrogateStatus.st_mode == 0100666 && surrogateStatus.st_nlink == 1); + clearReparseTag(surrogate, 0x20000ABC); #endif std::error_code error; const auto link = (root / "link").string(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 05721a9fc9..a64754f65f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -121,7 +121,8 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink`. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..018b1708bd 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -10,7 +10,7 @@ - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified -- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified From ed8f15bc1bd759ae5cf88a811bb123a78a8613bb Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:06:56 +0200 Subject: [PATCH 0112/1360] fix(agc): bind heap mirrors only where the guest holds bytes A heap mirror is now a sparse aliased buffer of 64 KiB blocks: blocks that hold nonzero guest bytes are bound to device-local memory with a host copy, and every other block aliases one zero block, bound as the guest writes it. A device without sparse binding, buffer residency or aliased residency, or without the sparse address space for a mirror, keeps the whole-range copy of main. Changed bytes reach the device by copies recorded before the build's work, and bytes the GPU wrote, also into the mirrors a cached address space holds, are copied back after it, so no mirror block needs a host-visible memory type. A device read of a mirror inside a GPU completion throws. Sparse binds take a free queue only. Main and settle queue selection is unchanged and the settle queue is kept: when no free queue binds, binds go to the main queue under its lock. With dma-buf import on Linux, the settle queue that main dropped when the sparse family was a one-queue settle family is now kept. A new joined host import bumps the import epoch, so the next build releases heap mirrors over its ranges instead of refreshing a range that is no longer write-watched. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../Execution/include/VulkanDevice.hpp | 13 + .../Execution/src/VulkanDevice.cpp | 136 ++-- .../Graphics/include/Context.hpp | 2 + .../Graphics/include/GuestBufferMemory.hpp | 6 + .../Graphics/include/ResidentMirror.hpp | 77 ++ .../Graphics/src/GuestBufferMemory.cpp | 307 ++++++-- .../Graphics/src/ResidentMirror.cpp | 659 ++++++++++++++++++ .../tests/execution/DeviceQueuePlan.cpp | 71 ++ .../tests/execution/ResidentHeapMirror.cpp | 342 +++++++++ 10 files changed, 1509 insertions(+), 105 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c320caf623..95677e00d8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -284,6 +284,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderAddressResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/GuestBufferMemory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ResidentMirror.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BdaResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BufferAddress.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderValidation.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 19b1b24e22..aca98ffb91 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace AgcDriver { @@ -33,6 +34,18 @@ struct RecordedDispatch; // The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline). struct ComputePipelineObjects; +struct DeviceQueuePlan { + std::uint32_t mainCount = 1; + bool settle = false; + std::uint32_t settleFamily = 0; + std::uint32_t settleIndex = 0; + bool sparseBinding = false; + std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; + std::uint32_t sparseIndex = 0; +}; + +DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding); + class VulkanDevice { public: explicit VulkanDevice(const PresentationWindow* window = nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c19c2efa29..da9382f59d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -77,6 +77,44 @@ #include namespace AgcDriver { + +DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding) { + DeviceQueuePlan plan; + const auto count = static_cast(families.size()); + if (settles) { + if (family < count && families[family].queueCount >= 2) { + plan.mainCount = 2; + plan.settle = true; + plan.settleFamily = family; + plan.settleIndex = 1; + } else { + const auto copies = [&](std::uint32_t index) { return index != family && families[index].queueCount != 0 && (families[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; + for (std::uint32_t i = 0; i < count && !plan.settle; ++i) { + if (copies(i) && (families[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) plan.settle = true, plan.settleFamily = i; + } + for (std::uint32_t i = 0; i < count && !plan.settle; ++i) { + if (copies(i)) plan.settle = true, plan.settleFamily = i; + } + } + } + if (!sparseBinding) return plan; + const auto taken = [&](std::uint32_t index) { return plan.settle && index == plan.settleFamily ? plan.settleIndex + 1 : 0u; }; + const auto binds = [&](std::uint32_t index) { return index != family && families[index].queueCount > taken(index) && (families[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; + for (std::uint32_t i = 0; i < count && plan.sparseFamily == VK_QUEUE_FAMILY_IGNORED; ++i) { + if (binds(i) && families[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) plan.sparseFamily = i; + } + for (std::uint32_t i = 0; i < count && plan.sparseFamily == VK_QUEUE_FAMILY_IGNORED; ++i) { + if (binds(i)) plan.sparseFamily = i; + } + if (plan.sparseFamily != VK_QUEUE_FAMILY_IGNORED) { + plan.sparseIndex = taken(plan.sparseFamily); + plan.sparseBinding = true; + } else { + plan.sparseBinding = family < count && (families[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; + } + return plan; +} + namespace { void check(VkResult result, const char* operation) { @@ -357,8 +395,11 @@ struct VulkanDevice::State { bool drawInputsInPlace = true; bool dmaBufImport = false; bool sparseBinding = false; + bool sparseResidencyBuffer = false; + bool sparseResidencyAliased = false; std::uint32_t queueFamily = 0; std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; + std::uint32_t sparseIndex = 0; VkQueue sparseQueue = VK_NULL_HANDLE; std::mutex sparseQueueMutex; VkQueue settleQueue = VK_NULL_HANDLE; @@ -1534,35 +1575,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : queueInfo.queueCount = 1; queueInfo.pQueuePriorities = &priority; const float priorities[2]{1.0f, 1.0f}; - bool settleQueue = false; - std::uint32_t settleFamily = family; - std::uint32_t settleIndex = 0; - std::vector queues; #ifdef _WIN32 const bool settles = true; #else const bool settles = state->dmaBufImport; #endif - if (settles) { + std::vector queues; + { std::uint32_t families = 0; auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); getFamilies(selected, &families, nullptr); queues.resize(families); getFamilies(selected, &families, queues.data()); - if (family < families && queues[family].queueCount >= 2) { - queueInfo.queueCount = 2; - queueInfo.pQueuePriorities = priorities; - settleQueue = true; - settleIndex = 1; - } else { - const auto copies = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; - for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { - if (copies(i) && (queues[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = i, settleQueue = true; - } - for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { - if (copies(i)) settleFamily = i, settleQueue = true; - } - } } VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; deviceInfo.queueCreateInfoCount = 1; @@ -1597,50 +1621,38 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; + const auto plan = PlanDeviceQueues(queues, family, settles, available.sparseBinding == VK_TRUE); + const bool settleQueue = plan.settle; + const auto settleFamily = plan.settleFamily; + const auto settleIndex = plan.settleIndex; VkDeviceQueueCreateInfo queueInfos[3]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; - if (available.sparseBinding && state->dmaBufImport) { - std::uint32_t families = 0; - auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); - getFamilies(selected, &families, nullptr); - std::vector queues(families); - getFamilies(selected, &families, queues.data()); - const auto binds = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; - std::uint32_t sparseFamily = families; - for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { - if (binds(i) && queues[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) sparseFamily = i; + queueInfos[0].queueCount = plan.mainCount; + queueInfos[0].pQueuePriorities = plan.mainCount == 2 ? priorities : &priority; + deviceInfo.queueCreateInfoCount = 1; + const auto addQueue = [&](std::uint32_t queueFamily, std::uint32_t index) { + for (std::uint32_t info = 1; info < deviceInfo.queueCreateInfoCount; ++info) { + if (queueInfos[info].queueFamilyIndex != queueFamily) continue; + queueInfos[info].queueCount = std::max(queueInfos[info].queueCount, index + 1); + queueInfos[info].pQueuePriorities = queueInfos[info].queueCount == 2 ? priorities : &priority; + return; } - for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { - if (binds(i)) sparseFamily = i; - } - if (sparseFamily != families) { - queueInfos[1].queueFamilyIndex = sparseFamily; - queueInfos[1].queueCount = 1; - queueInfos[1].pQueuePriorities = &priority; - deviceInfo.queueCreateInfoCount = 2; - state->sparseFamily = sparseFamily; - state->sparseBinding = true; - } else { - state->sparseBinding = family < families && (queues[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; - } - enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; - } + auto& added = queueInfos[deviceInfo.queueCreateInfoCount++]; + added = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + added.queueFamilyIndex = queueFamily; + added.queueCount = index + 1; + added.pQueuePriorities = added.queueCount == 2 ? priorities : &priority; + }; + if (plan.settle && plan.settleFamily != family) addQueue(plan.settleFamily, plan.settleIndex); + if (plan.sparseFamily != VK_QUEUE_FAMILY_IGNORED) addQueue(plan.sparseFamily, plan.sparseIndex); deviceInfo.pQueueCreateInfos = queueInfos; - if (settleQueue && settleFamily != family) { - if (deviceInfo.queueCreateInfoCount == 2 && queueInfos[1].queueFamilyIndex == settleFamily) { - settleQueue = queues[settleFamily].queueCount >= 2; - if (settleQueue) { - queueInfos[1].queueCount = 2; - queueInfos[1].pQueuePriorities = priorities; - settleIndex = 1; - } - } else { - auto& settleInfo = queueInfos[deviceInfo.queueCreateInfoCount++]; - settleInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; - settleInfo.queueFamilyIndex = settleFamily; - settleInfo.queueCount = 1; - settleInfo.pQueuePriorities = &priority; - } - } + state->sparseFamily = plan.sparseFamily; + state->sparseIndex = plan.sparseIndex; + state->sparseBinding = plan.sparseBinding; + enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; + enabled.sparseResidencyBuffer = state->sparseBinding ? available.sparseResidencyBuffer : VK_FALSE; + enabled.sparseResidencyAliased = state->sparseBinding ? available.sparseResidencyAliased : VK_FALSE; + state->sparseResidencyBuffer = enabled.sparseResidencyBuffer == VK_TRUE; + state->sparseResidencyAliased = enabled.sparseResidencyAliased == VK_TRUE; // Indirect draw records with a non-zero first instance, and several records per call. enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance; enabled.multiDrawIndirect = available.multiDrawIndirect; @@ -1958,7 +1970,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->settleFamily = settleFamily; state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue); } - if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue); + if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, state->sparseIndex, &state->sparseQueue); if (checkpoints) state->checkpointMarker = state->DeviceFunction("vkCmdSetCheckpointNV"); if (deviceFault || checkpoints) { const auto faultInfo = deviceFault ? state->DeviceFunction("vkGetDeviceFaultInfoEXT") : nullptr; @@ -3740,6 +3752,8 @@ Graphics::Context VulkanDevice::buildContext() const { context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets; context.dmaBufImport = state->dmaBufImport; context.sparseBinding = state->sparseBinding; + context.sparseResidencyBuffer = state->sparseResidencyBuffer; + context.sparseResidencyAliased = state->sparseResidencyAliased; context.queueFamily = state->queueFamily; context.sparseFamily = state->sparseFamily; context.sparseQueue = state->sparseQueue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index c73dcc10bb..dbae8254a7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -169,6 +169,8 @@ struct Context { bool drawInputsInPlace = true; bool dmaBufImport = false; bool sparseBinding = false; + bool sparseResidencyBuffer = false; + bool sparseResidencyAliased = false; std::uint32_t queueFamily = 0; std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; VkQueue sparseQueue = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..39f9f51b5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -247,6 +247,11 @@ struct MirrorStats { }; MirrorStats MirrorCounters(); std::uint64_t SnapshotMismatches(); +struct HeapMirrorHold { + std::uint64_t mirrors = 0; + std::uint64_t bytes = 0; +}; +HeapMirrorHold HeapMirrorsWithin(std::uint64_t begin, std::uint64_t end); void ClearSnapshotCopies(VkDevice device); std::vector AddressTableKey(std::span> writes, std::span own); void ClearImageMirrors(VkDevice device); @@ -491,6 +496,7 @@ private: std::vector> writes; std::vector> readsOverBase; std::vector>> heapReferences; + std::vector>> heapReadbacks; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; bool uploaded = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp new file mode 100644 index 0000000000..f6c10579b9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp @@ -0,0 +1,77 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RESIDENTMIRROR_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RESIDENTMIRROR_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +class Buffer; +class ResidentMemory; + +using GuestRuns = std::vector>; + +GuestRuns GuestNonzeroBlocks(const GuestRuns& candidates); +bool GuestBytesZero(std::uint64_t begin, std::uint64_t end); +void SetResidentAddressSpaceLimit(std::uint64_t bytes); + +class ResidentBuffer : public std::enable_shared_from_this { +public: + static constexpr std::uint64_t BlockBytes = 65536; + + static std::shared_ptr Create(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed); + ~ResidentBuffer(); + ResidentBuffer(const ResidentBuffer&) = delete; + ResidentBuffer& operator=(const ResidentBuffer&) = delete; + + VkBuffer Handle() const { return buffer; } + VkDeviceAddress DeviceAddress() const { return address + lead; } + std::uint64_t Origin() const { return origin; } + std::uint64_t BoundBytes() const { return bound * BlockBytes; } + std::uint32_t MemoryType() const { return type; } + std::byte* Block(std::uint64_t guest) const; + std::uint64_t UnboundBytes(const GuestRuns& runs) const; + void Bind(const GuestRuns& runs); + void MarkChanged(std::uint64_t begin, std::uint64_t end); + void Read(std::uint64_t guest, std::span out) const; + void Store(std::uint64_t guest, std::span bytes); + std::shared_ptr RecordReadback(VkCommandBuffer commands, std::uint64_t guest, std::uint64_t bytes) const; + void ReadDevice(std::uint64_t guest, std::span out); + static void FlushChanged(const Context& context); + +private: + struct Slot { + void* slab = nullptr; + std::uint32_t index = 0; + std::byte* host = nullptr; + }; + ResidentBuffer(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed); + std::size_t blockIndex(std::uint64_t guest) const; + void submit(const std::vector& binds); + void flush(); + std::shared_ptr readbackBatch(std::uint64_t guest, std::uint64_t bytes); + void release() noexcept; + + Context context; + std::shared_ptr memory; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceAddress address = 0; + std::uint32_t type = 0; + std::uint64_t origin = 0; + std::uint64_t lead = 0; + std::uint64_t size = 0; + std::uint64_t bound = 0; + std::vector slots; + std::mutex changedMutex; + GuestRuns changed; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 56368f852f..d1a1ee5a1e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -70,6 +71,9 @@ struct ImageMirror { std::vector generations; std::weak_ptr range; std::shared_ptr buffer; + std::shared_ptr resident; + std::uint64_t accounted = 0; + bool listed = false; // Writable ranges only: the guest bytes the mirror holds, compared on refresh and at write-back. std::vector shadow; // Identity for the life of this mirror (see ImageMirrorSerial); the top bit keeps mirror serials @@ -1660,7 +1664,11 @@ const HostImport* importJoined(const Context& context, HostImports& state, std:: auto run = joinedRun(lease, begin, end); if (run.ranges.empty()) return nullptr; if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr; - return importRunAt(context, state, std::move(run), true, begin, lease, end, true, ImportRequester::Join); + const auto existing = state.imports.find(run.base); + const bool existed = existing != state.imports.end() && sameRun(existing->second, run); + const auto* entry = importRunAt(context, state, std::move(run), true, begin, lease, end, true, ImportRequester::Join); + if (entry != nullptr && !existed) ++state.epoch; + return entry; } const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester) { @@ -2200,25 +2208,104 @@ std::uint64_t heapMirrorBudget() { return bytes; } -std::string heapMirrorError(const GuestAllocations::Range& range, std::uint64_t held, std::string_view reason) { +std::string heapMirrorError(std::uint64_t address, std::uint64_t bytes, std::uint64_t held, std::string_view reason) { constexpr std::string_view prefix = "AGC graphics: "; if (reason.starts_with(prefix)) reason.remove_prefix(prefix.size()); char where[64]; - std::snprintf(where, sizeof(where), "heap mirror of 0x%llx+0x%llx: ", static_cast(range.address), static_cast(range.bytes)); + std::snprintf(where, sizeof(where), "heap mirror of 0x%llx+0x%llx: ", static_cast(address), static_cast(bytes)); char holding[96]; std::snprintf(holding, sizeof(holding), "; heap mirrors hold %llu MiB of %llu MiB (APS5_HEAP_MIRROR_MIB)", static_cast(held >> 20u), static_cast(heapMirrorBudget() >> 20u)); return std::string(prefix) + where + std::string(reason) + holding; } -std::uint64_t heapMirrorsBeside(const GuestAllocations::Range& range) { - auto& state = Mirrors(); - std::lock_guard lock(state.mutex); - std::uint64_t held = state.heapBytes; - if (const auto found = state.entries.find(range.address); found != state.entries.end() && found->second->heap) held -= found->second->bytes; - return held; +void sweepMirrors(); + +void unlistMirror(ImageMirrors& state, ImageMirror& mirror) { + if (!mirror.listed) return; + state.heapBytes -= mirror.accounted; + mirror.listed = false; } -void sweepMirrors(); +std::uint64_t blockBytes(const GuestRuns& runs) { + constexpr std::uint64_t block = ResidentBuffer::BlockBytes; + std::uint64_t blocks = 0; + std::uint64_t last = 0; + bool any = false; + for (const auto& [first, end] : runs) { + if (first >= end) continue; + auto index = first / block; + if (any && index == last) ++index; + if (index <= (end - 1) / block) blocks += (end - 1) / block - index + 1; + last = (end - 1) / block; + any = true; + } + return blocks * block; +} + +void reserveHeapBytes(std::uint64_t address, std::uint64_t bytes, std::uint64_t need) { + auto& state = Mirrors(); + for (int attempt = 0;; ++attempt) { + { + std::lock_guard lock(state.mutex); + std::uint64_t held = state.heapBytes; + if (const auto found = state.entries.find(address); found != state.entries.end() && found->second->heap && found->second->listed) held -= found->second->accounted; + if (held + need <= heapMirrorBudget()) { + state.heapBytes += need; + return; + } + if (attempt != 0) throw std::runtime_error(heapMirrorError(address, bytes, held, "past the budget")); + } + sweepMirrors(); + } +} + +void growResident(ImageMirror& mirror, const GuestRuns& runs) { + if (mirror.resident == nullptr) return; + const auto need = mirror.resident->UnboundBytes(runs); + if (need == 0) return; + auto& state = Mirrors(); + bool counted = false; + for (int attempt = 0; !counted; ++attempt) { + { + std::lock_guard lock(state.mutex); + if (!mirror.listed) break; + if (state.heapBytes + need <= heapMirrorBudget()) { + state.heapBytes += need; + mirror.accounted += need; + counted = true; + break; + } + if (attempt != 0) throw std::runtime_error(heapMirrorError(mirror.base, mirror.reserved, state.heapBytes, "past the budget")); + } + sweepMirrors(); + } + try { + mirror.resident->Bind(runs); + } catch (const std::runtime_error& error) { + std::uint64_t held = 0; + { + std::lock_guard lock(state.mutex); + if (counted && mirror.listed) { + state.heapBytes -= need; + mirror.accounted -= need; + } + held = state.heapBytes; + } + throw std::runtime_error(heapMirrorError(mirror.base, mirror.reserved, held, error.what())); + } +} + +std::uint64_t mirrorStart(const ImageMirror& mirror) { + return mirror.resident != nullptr ? mirror.resident->Origin() : mirror.base; +} + +VkBuffer mirrorHandle(const ImageMirror& mirror) { + return mirror.resident != nullptr ? mirror.resident->Handle() : mirror.buffer->Handle(); +} + +VkDeviceAddress mirrorAddress(const ImageMirror& mirror) { + return mirror.resident != nullptr ? mirror.resident->DeviceAddress() : mirror.buffer->DeviceAddress(); +} bool sameRange(const std::weak_ptr& mirrored, const std::shared_ptr& range) { return !mirrored.owner_before(range) && !range.owner_before(mirrored); @@ -2240,6 +2327,16 @@ constexpr std::uint8_t SparseZero = 2; bool compareBlock(const RefreshBlock& block) { const auto offset = static_cast(block.address - block.mirror->base); const auto* guest = reinterpret_cast(block.address); + if (block.mirror->resident != nullptr) { + auto* target = block.mirror->resident->Block(block.address); + if (block.fill == SparseZero) { + if (target != nullptr) std::memset(target, 0, block.length); + return true; + } + if (target != nullptr) std::memcpy(target, guest, block.length); + if (block.fill == 0) block.mirror->generations[static_cast(block.address / 65536 - block.mirror->base / 65536)] = block.generation; + return target != nullptr; + } if (block.fill != 0) { auto* target = block.mirror->buffer->Bytes().data() + offset; if (block.fill == SparseZero) std::memset(target, 0, block.length); @@ -2391,9 +2488,33 @@ void appendBlocks(std::vector& blocks, ImageMirror& mirror, std::u } // Compares the gathered blocks (of one or many mirrors) and counts them. +void bindHeapBlocks(std::span blocks) { + std::vector> wanted; + for (const auto& block : blocks) { + if (block.mirror->resident == nullptr || block.fill == SparseZero) continue; + if (block.mirror->resident->Block(block.address) == nullptr && GuestBytesZero(block.address, block.address + block.length)) continue; + if (wanted.empty() || wanted.back().first != block.mirror) { + const auto found = std::find_if(wanted.begin(), wanted.end(), [&](const auto& entry) { return entry.first == block.mirror; }); + if (found == wanted.end()) wanted.emplace_back(block.mirror, GuestRuns{}); + else std::rotate(found, std::next(found), wanted.end()); + } + auto& runs = wanted.back().second; + if (!runs.empty() && runs.back().second == block.address) runs.back().second = block.address + block.length; + else runs.emplace_back(block.address, block.address + block.length); + } + for (auto& [mirror, runs] : wanted) { + std::sort(runs.begin(), runs.end()); + growResident(*mirror, runs); + } +} + void compareBlocks(std::span blocks) { if (blocks.empty()) return; + bindHeapBlocks(blocks); const auto copied = RefreshPool::Get().Run(blocks); + for (const auto& block : blocks) { + if (block.mirror->resident != nullptr && block.mirror->resident->Block(block.address) != nullptr) block.mirror->resident->MarkChanged(block.address, block.address + block.length); + } auto& state = Mirrors(); state.blocksCompared += blocks.size(); state.blocksCopied += copied; @@ -2620,6 +2741,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh { std::lock_guard lock(state.mutex); if (state.device != context.device) { + for (auto& [base, entry] : state.entries) entry->listed = false; state.entries.clear(); state.failed.clear(); state.heapBytes = 0; @@ -2652,6 +2774,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh mirror->writable = range->writable; mirror->heap = heap; mirror->range = range; + GuestRuns backed; if (heap) { std::uint64_t generation = 0; const auto serial = GuestMemory::UnwatchSerial(); @@ -2669,7 +2792,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh std::lock_guard lock(state.mutex); ++state.heapUnwatched; if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) { - state.heapBytes -= found->second->bytes; + unlistMirror(state, *found->second); stale = std::move(found->second); state.entries.erase(found); ++state.version; @@ -2677,15 +2800,64 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh return nullptr; } mirror->watchedSerial.store(serial, std::memory_order_relaxed); - if (heapMirrorsBeside(*range) + mirror->bytes > heapMirrorBudget()) { - sweepMirrors(); - if (const auto held = heapMirrorsBeside(*range); held + mirror->bytes > heapMirrorBudget()) throw std::runtime_error(heapMirrorError(*range, held, "past the budget")); + if (context.sparseBinding && context.sparseResidencyBuffer && context.sparseResidencyAliased) backed = GuestNonzeroBlocks(sparse ? clipPieces(pieces, mirror->base, mirror->base + mirror->bytes) : GuestRuns{{mirror->base, mirror->base + mirror->bytes}}); + auto need = blockBytes(backed); + reserveHeapBytes(range->address, range->bytes, need); + try { + mirror->resident = ResidentBuffer::Create(context, mirror->base, mirror->bytes, backed); + } catch (const std::runtime_error& error) { + std::uint64_t held = 0; + { + std::lock_guard lock(state.mutex); + state.heapBytes -= need; + held = state.heapBytes; + } + throw std::runtime_error(heapMirrorError(range->address, range->bytes, held, error.what())); } + if (mirror->resident == nullptr) { + { + std::lock_guard lock(state.mutex); + state.heapBytes -= need; + } + need = mirror->bytes; + reserveHeapBytes(range->address, range->bytes, need); + } + mirror->accounted = need; + } + if (mirror->resident != nullptr) { + { + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::MirrorRefresh); + for (const auto& [first, last] : backed) { + for (auto at = first; at < last;) { + const auto next = std::min(last, (at / ResidentBuffer::BlockBytes + 1) * ResidentBuffer::BlockBytes); + GuestMemory::Read(at, std::span(mirror->resident->Block(at), static_cast(next - at))); + at = next; + } + mirror->resident->MarkChanged(first / ResidentBuffer::BlockBytes * ResidentBuffer::BlockBytes, (last + ResidentBuffer::BlockBytes - 1) / ResidentBuffer::BlockBytes * ResidentBuffer::BlockBytes); + } + } + mirror->committed = std::move(pieces); + std::lock_guard lock(state.mutex); + ++state.rebuilds; + mirror->serial = (1ull << 63u) | ++state.serials; + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second); + mirror->listed = true; + state.entries[range->address] = mirror; + ++state.version; + return mirror; } try { mirror->buffer = std::make_shared(context, static_cast(mirror->bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); } catch (const std::runtime_error& error) { - if (heap) throw std::runtime_error(heapMirrorError(*range, heapMirrorsBeside(*range), error.what())); + if (heap) { + std::uint64_t held = 0; + { + std::lock_guard lock(state.mutex); + state.heapBytes -= mirror->accounted; + held = state.heapBytes; + } + throw std::runtime_error(heapMirrorError(range->address, range->bytes, held, error.what())); + } std::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast(range->address), static_cast(range->bytes), error.what()); std::lock_guard lock(state.mutex); state.failed.insert(range->address); @@ -2712,8 +2884,8 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh ++state.rebuilds; mirror->serial = (1ull << 63u) | ++state.serials; // A replaced mirror lives on in the regions of recorded work that bound it. - if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) state.heapBytes -= found->second->bytes; - if (heap) state.heapBytes += mirror->bytes; + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second); + mirror->listed = heap; state.entries[range->address] = mirror; ++state.version; return mirror; @@ -2749,7 +2921,7 @@ void sweepMirrors() { ++it; continue; } - if (it->second->heap) state.heapBytes -= it->second->bytes; + if (it->second->heap) unlistMirror(state, *it->second); it = state.entries.erase(it); ++state.version; } @@ -2766,9 +2938,9 @@ void releaseImportedMirrors(const Context& context, const std::vectorsecond->heap) continue; - state.heapBytes -= found->second->bytes; + state.heapImportedBytes += found->second->accounted; + unlistMirror(state, *found->second); ++state.heapImported; - state.heapImportedBytes += found->second->bytes; released.push_back(std::move(found->second)); state.entries.erase(found); } @@ -3077,6 +3249,18 @@ std::uint64_t SnapshotMismatches() { return Snapshots().mismatched.load(std::memory_order_relaxed); } +HeapMirrorHold HeapMirrorsWithin(std::uint64_t begin, std::uint64_t end) { + auto& state = Mirrors(); + std::lock_guard lock(state.mutex); + HeapMirrorHold hold; + for (auto it = state.entries.lower_bound(begin); it != state.entries.end() && it->first < end; ++it) { + if (!it->second->heap || it->second->range.expired()) continue; + ++hold.mirrors; + if (it->second->listed) hold.bytes += it->second->accounted; + } + return hold; +} + std::vector AddressTableKey(std::span> writes, std::span own) { std::vector key; key.reserve(writes.size() * 2u + 1u + own.size() * 4u); @@ -3105,6 +3289,7 @@ void ClearImageMirrors(VkDevice device) { std::lock_guard lock(state.mutex); if (state.device != device) return; entries.swap(state.entries); + for (auto& [base, entry] : entries) entry->listed = false; state.failed.clear(); state.heapBytes = 0; state.device = VK_NULL_HANDLE; @@ -4032,20 +4217,13 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: const auto* range = containingRange(lease, region.begin, neededEnd); if (range == nullptr || !range->releasable) return false; std::vector blocks; - std::shared_ptr mirror; - try { - mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); - } catch (const std::runtime_error& error) { - static std::atomic reported{0}; - if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what()); - return false; - } + auto mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); if (mirror != nullptr && mirror->heap && !mirror->sparse) { std::vector whole{mirror.get()}; refreshHeapMirrors(whole, blocks); } compareBlocks(blocks); - if (mirror == nullptr || !mirror->heap || region.begin < mirror->base || (region.begin - mirror->base) % context.limits.minStorageBufferOffsetAlignment != 0) return false; + if (mirror == nullptr || !mirror->heap || region.begin < mirror->base || (region.begin - mirrorStart(*mirror)) % context.limits.minStorageBufferOffsetAlignment != 0) return false; region.mirror = std::move(mirror); if (!servesView(region, std::max(region.lastView, region.begin))) { region.mirror = nullptr; @@ -4738,6 +4916,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { } if (!gpuCopies.empty()) recordGpuCopies(gpuCopies, addressable); takeHeapReferences(); + ResidentBuffer::FlushChanged(context); if (!profile) return; const auto loopUs = microsecondsSince(loopStart); readUs.fetch_add(refreshUs, std::memory_order_relaxed); @@ -4972,16 +5151,36 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { VkCommandBuffer commands = VK_NULL_HANDLE; auto timing = Recorder::NoTiming; std::uint64_t copiedBytes = 0; + if (!writes.empty()) { + auto sorted = writes; + std::sort(sorted.begin(), sorted.end()); + for (const auto& range : sorted) { + if (!merged.empty() && range.first < merged.back().second) merged.back().second = std::max(merged.back().second, range.second); + else merged.push_back(range); + } + } + for (const auto& [begin, end] : merged) { + const auto* found = owner(begin, end); + if (found == nullptr || found->mirror == nullptr || !found->mirror->heap || !found->mirror->writable || found->mirror->resident == nullptr) continue; + const auto& region = *found; + { + const auto from = begin; + const auto to = std::min(end, servedEnd(region)); + if (from >= to || std::any_of(heapReadbacks.begin(), heapReadbacks.end(), [&](const auto& entry) { return entry.first == from; })) continue; + if (!recording) { + commands = recorder.Commands(); + timing = recorder.BeginGpuTiming(StagingCopyBackKey); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + Recorder::CountBarriers(Recorder::CommandClass::StagingOut); + recording = true; + } + heapReadbacks.emplace_back(from, region.mirror->resident->RecordReadback(commands, from, to - from)); + recorder.Keep(heapReadbacks.back().second); + copiedBytes += to - from; + } + } for (auto& region : regions) { if (!region.gpuCopy || region.copiedBack) continue; - if (merged.empty() && !writes.empty()) { - auto sorted = writes; - std::sort(sorted.begin(), sorted.end()); - for (const auto& range : sorted) { - if (!merged.empty() && range.first < merged.back().second) merged.back().second = std::max(merged.back().second, range.second); - else merged.push_back(range); - } - } for (const auto& [begin, end] : merged) { const auto from = std::max(begin, region.begin); const auto to = std::min(end, region.end); @@ -5038,7 +5237,7 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: return std::string(text); }; if (!(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr))) Require(false, describe("exceeds")); - const auto base = region.direct != nullptr ? region.direct->base - region.direct->lead : region.mirror != nullptr ? region.mirror->base : region.begin; + const auto base = region.direct != nullptr ? region.direct->base - region.direct->lead : region.mirror != nullptr ? mirrorStart(*region.mirror) : region.begin; const auto offset = address - base; Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment"); Require(base % 4 == 0, "a guest buffer view in a GPU owner that does not start at a DWORD boundary is not implemented"); @@ -5052,7 +5251,7 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: } if (address - adjustment + range > end) Require(false, describe("rounded up to its view range exceeds")); Require(range <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); - const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle(); + const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? mirrorHandle(*region.mirror) : region.buffer->Handle(); return {handle, offset - adjustment, range}; } @@ -5065,7 +5264,7 @@ std::uint64_t GuestBufferMemory::ViewBytes(std::uint64_t bytes, std::uint32_t ad ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) { Require(region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr, "incomplete guest GPU upload"); - const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? region.mirror->buffer->DeviceAddress() + (region.begin - region.mirror->base) : region.buffer->DeviceAddress(); + const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? mirrorAddress(*region.mirror) + (region.begin - region.mirror->base) : region.buffer->DeviceAddress(); Require(region.end - region.begin <= std::numeric_limits::max() - address, "GPU address range overflow"); const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u); return {region.begin, region.direct != nullptr ? inPlaceEnd(region) : servedEnd(region), address, permissions, 0}; @@ -5328,15 +5527,19 @@ std::vector> mergeWrites(std::vectormirror == nullptr || !found->mirror->heap || !found->mirror->writable) continue; Require(end <= found->end, "written range exceeds its heap mirror"); - const auto& mirror = *found->mirror; + auto& mirror = *found->mirror; const auto served = std::min(end, servedEnd(*found)); if (begin >= served) continue; - const auto bytes = mirror.buffer->Bytes().subspan(static_cast(begin - mirror.base), static_cast(served - begin)); - heapReferences.emplace_back(begin, std::vector(bytes.begin(), bytes.end())); + growResident(mirror, {{begin, served}}); + std::vector bytes(static_cast(served - begin)); + if (mirror.resident != nullptr) mirror.resident->Read(begin, bytes); + else std::memcpy(bytes.data(), mirror.buffer->Bytes().data() + (begin - mirror.base), bytes.size()); + heapReferences.emplace_back(begin, std::move(bytes)); } } @@ -5366,14 +5569,28 @@ void GuestBufferMemory::WriteBack() { const auto served = std::min(end, servedEnd(region)); if (mirror.heap && begin >= served) continue; const auto length = static_cast((mirror.heap ? served : end) - begin); - const auto current = mirror.buffer->Bytes().subspan(offset, length); if (mirror.heap) { const auto reference = std::find_if(heapReferences.begin(), heapReferences.end(), [&](const auto& entry) { return entry.first == begin; }); Require(reference != heapReferences.end() && reference->second.size() == length, "heap mirror write-back has no reference"); + if (mirror.resident == nullptr) { + const auto current = mirror.buffer->Bytes().subspan(offset, length); + if (mirror.sparse) storeSparse(mirror, begin, current, reference->second); + else storeChanged(begin, current, reference->second); + continue; + } + std::vector current(length); + if (const auto readback = std::find_if(heapReadbacks.begin(), heapReadbacks.end(), [&](const auto& entry) { return entry.first == begin; }); readback != heapReadbacks.end()) { + Require(readback->second->Bytes().size() == length, "heap mirror readback does not match its write-back"); + std::memcpy(current.data(), readback->second->Bytes().data(), length); + } else { + mirror.resident->ReadDevice(begin, current); + } + mirror.resident->Store(begin, current); if (mirror.sparse) storeSparse(mirror, begin, current, reference->second); else storeChanged(begin, current, reference->second); continue; } + const auto current = mirror.buffer->Bytes().subspan(offset, length); GuestMemory::WriteChanged(begin, current, std::span(mirror.shadow).subspan(offset, length)); std::memcpy(mirror.shadow.data() + offset, current.data(), length); continue; @@ -5424,6 +5641,7 @@ void GuestBufferMemory::WriteBack() { lease.clear(); space.reset(); heapReferences.clear(); + heapReadbacks.clear(); } std::optional>> GuestBufferMemory::DirectRegions() const { @@ -5520,6 +5738,7 @@ std::vector> GuestBufferMemory::DeviceRe } void GuestBufferMemory::RecordStagingCopies(Recorder& recorder) { + ResidentBuffer::FlushChanged(context); if (!uploaded || committed) return; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; std::vector copies; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp new file mode 100644 index 0000000000..3913660efc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp @@ -0,0 +1,659 @@ +#include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestArena.hpp" +#ifndef _WIN32 +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +constexpr std::uint64_t BlockSize = ResidentBuffer::BlockBytes; +constexpr std::uint64_t SlabBytes = std::uint64_t{16} << 20u; +constexpr std::uint32_t SlabBlocks = static_cast(SlabBytes / BlockSize); +constexpr std::uint64_t ZeroBytes = SlabBytes; +constexpr VkAccessFlags TransferredAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT; + +[[noreturn]] void fail(const char* step, VkResult result) { + char text[160]; + std::snprintf(text, sizeof(text), "%s failed (%d)", step, static_cast(result)); + throw std::runtime_error(text); +} + +struct TransientBuffer { + TransientBuffer(const Context& context, VkBuffer buffer) : context(context), buffer(buffer) {} + ~TransientBuffer() { context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); } + TransientBuffer(const TransientBuffer&) = delete; + TransientBuffer& operator=(const TransientBuffer&) = delete; + Context context; + VkBuffer buffer; +}; + +template +void recordTransfer(const Context& context, Work&& work) { + GuestMemory::AssertGpuLockHeld("heap mirror transfer"); + std::vector> kept; + if (auto* recorder = Recorder::Active(); recorder != nullptr) { + const auto commands = recorder->Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + work(commands, kept); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, TransferredAccess); + for (auto& object : kept) recorder->Keep(std::move(object)); + recorder->MarkCovered(TransferredAccess); + return; + } + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + work(batch.Handle(), kept); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, TransferredAccess); + batch.SubmitAndWait(); +} + +} + +class ResidentMemory { +public: + struct Slab { + VkDeviceMemory memory = VK_NULL_HANDLE; + std::unique_ptr host; + std::vector free; + }; + + ResidentMemory(const Context& context, std::uint32_t type) : context(context), type(type) { + zero = allocate(ZeroBytes, "vkAllocateMemory of the heap mirrors' zero block"); + } + + ~ResidentMemory() { + const auto freeMemory = context.Function("vkFreeMemory"); + for (auto& slab : slabs) freeMemory(context.device, slab->memory, nullptr); + freeMemory(context.device, zero, nullptr); + } + + ResidentMemory(const ResidentMemory&) = delete; + ResidentMemory& operator=(const ResidentMemory&) = delete; + + VkDeviceMemory Zero() const { return zero; } + bool Zeroed() const { return zeroed.load(std::memory_order_acquire); } + + void FillZero(VkCommandBuffer commands, std::vector>& kept) { + if (Zeroed()) return; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = ZeroBytes; + info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + VkBuffer buffer = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) fail("vkCreateBuffer of the heap mirrors' zero fill", result); + auto transient = std::make_shared(context, buffer); + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if ((requirements.memoryTypeBits & (1u << type)) == 0 || requirements.size > ZeroBytes) throw std::runtime_error("the heap mirrors' zero block memory cannot back a transfer buffer to fill it"); + if (const auto result = context.Function("vkBindBufferMemory")(context.device, buffer, zero, 0); result != VK_SUCCESS) fail("vkBindBufferMemory of the heap mirrors' zero fill", result); + context.Function("vkCmdFillBuffer")(commands, buffer, 0, VK_WHOLE_SIZE, 0); + kept.push_back(std::move(transient)); + zeroed.store(true, std::memory_order_release); + } + + std::tuple Take() { + std::lock_guard lock(mutex); + for (auto& slab : slabs) { + if (slab->free.empty()) continue; + const auto index = slab->free.back(); + slab->free.pop_back(); + return {slab.get(), index}; + } + auto slab = std::make_unique(); + slab->memory = allocate(SlabBytes, "vkAllocateMemory of a heap mirror slab"); + slab->host = std::unique_ptr(new std::byte[SlabBytes]); + for (std::uint32_t index = SlabBlocks; index-- > 1;) slab->free.push_back(index); + slabs.push_back(std::move(slab)); + return {slabs.back().get(), 0u}; + } + + void Put(Slab* slab, std::uint32_t index) noexcept { + std::lock_guard lock(mutex); + slab->free.push_back(index); + if (slab->free.size() != SlabBlocks) return; + const auto found = std::find_if(slabs.begin(), slabs.end(), [&](const auto& held) { return held.get() == slab; }); + context.Function("vkFreeMemory")(context.device, slab->memory, nullptr); + slabs.erase(found); + } + +private: + VkDeviceMemory allocate(std::uint64_t bytes, const char* operation) { + const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, nullptr, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0}; + VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, context.bufferDeviceAddress ? &flags : nullptr}; + info.allocationSize = bytes; + info.memoryTypeIndex = type; + if (context.bufferPool != nullptr) return context.bufferPool->AllocateMemory(info, operation); + VkDeviceMemory memory = VK_NULL_HANDLE; + if (const auto result = context.Function("vkAllocateMemory")(context.device, &info, nullptr, &memory); result != VK_SUCCESS) fail(operation, result); + return memory; + } + + Context context; + std::uint32_t type; + VkDeviceMemory zero = VK_NULL_HANDLE; + std::atomic zeroed{false}; + std::mutex mutex; + std::vector> slabs; +}; + +namespace { + +struct ResidentDevices { + std::mutex mutex; + std::map, std::weak_ptr> memories; + std::map addressSpace; + std::uint64_t limit = 0; + std::mutex pendingMutex; + std::vector> pending; +}; + +ResidentDevices& Devices() { + static ResidentDevices devices; + return devices; +} + +std::shared_ptr residentMemory(const Context& context, std::uint32_t type) { + auto& devices = Devices(); + std::lock_guard lock(devices.mutex); + auto& slot = devices.memories[{context.device, type}]; + if (auto held = slot.lock()) return held; + auto made = std::make_shared(context, type); + slot = made; + return made; +} + +struct Candidate { + std::uint64_t begin = 0; + std::uint64_t end = 0; + bool readable = true; +}; + +bool allZero(std::uint64_t begin, std::uint64_t end) { + const auto* at = reinterpret_cast(begin); + const auto* stop = reinterpret_cast(end); + while (at < stop && reinterpret_cast(at) % sizeof(std::uint64_t) != 0) { + if (*at++ != 0) return false; + } + while (stop - at >= static_cast(sizeof(std::uint64_t))) { + std::uint64_t word = 0; + std::memcpy(&word, at, sizeof(word)); + if (word != 0) return false; + at += sizeof(word); + } + while (at < stop) { + if (*at++ != 0) return false; + } + return true; +} + +#ifndef _WIN32 +struct Mapping { + std::uint64_t begin = 0; + std::uint64_t end = 0; + bool readable = false; + bool privateAnonymous = false; +}; + +std::vector mappings(std::uint64_t begin, std::uint64_t end) { + std::ifstream maps("/proc/self/maps"); + Require(static_cast(maps), "cannot read /proc/self/maps for a heap mirror's pages"); + std::vector found; + std::string line; + while (std::getline(maps, line)) { + std::istringstream fields(line); + std::string span, permissions, offset, device; + unsigned long long inode = 0; + fields >> span >> permissions >> offset >> device >> inode; + const auto dash = span.find('-'); + Require(dash != std::string::npos && permissions.size() == 4, "unexpected /proc/self/maps line: " + line); + const auto first = std::stoull(span.substr(0, dash), nullptr, 16); + const auto last = std::stoull(span.substr(dash + 1), nullptr, 16); + if (last <= begin || first >= end) continue; + found.push_back({std::max(first, begin), std::min(last, end), permissions[0] == 'r', inode == 0 && permissions[3] == 'p'}); + } + return found; +} + +void fileData(std::vector& maybe, const Mapping& mapping, int file, std::uint64_t offset) { + const auto end = offset + (mapping.end - mapping.begin); + auto at = static_cast(offset); + while (static_cast(at) < end) { + const auto data = lseek(file, at, SEEK_DATA); + if (data < 0) { + Require(errno == ENXIO, "SEEK_DATA on a heap mirror's shared backing failed"); + return; + } + if (static_cast(data) >= end) return; + const auto hole = lseek(file, data, SEEK_HOLE); + Require(hole >= 0, "SEEK_HOLE on a heap mirror's shared backing failed"); + const auto stop = std::min(static_cast(hole), end); + maybe.push_back({mapping.begin + (static_cast(data) - offset), mapping.begin + (stop - offset), mapping.readable}); + at = static_cast(stop); + } +} + +void populatedPages(std::vector& maybe, const Mapping& mapping) { + static const auto page = static_cast(sysconf(_SC_PAGESIZE)); + const int file = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC); + Require(file >= 0, "cannot open /proc/self/pagemap for a heap mirror's pages"); + constexpr std::size_t Entries = 4096; + std::array entries{}; + const auto first = mapping.begin / page; + const auto last = (mapping.end + page - 1) / page; + for (auto at = first; at < last;) { + const auto count = static_cast(std::min(Entries, last - at)); + const auto read = pread(file, entries.data(), count * sizeof(std::uint64_t), static_cast(at * sizeof(std::uint64_t))); + if (read != static_cast(count * sizeof(std::uint64_t))) { + close(file); + throw std::runtime_error("reading /proc/self/pagemap for a heap mirror's pages failed"); + } + for (std::size_t index = 0; index < count; ++index) { + constexpr std::uint64_t present = std::uint64_t{1} << 63u; + constexpr std::uint64_t swapped = std::uint64_t{1} << 62u; + if ((entries[index] & (present | swapped)) == 0) continue; + const auto address = (at + index) * page; + const auto from = std::max(address, mapping.begin); + const auto to = std::min(address + page, mapping.end); + if (!maybe.empty() && maybe.back().end == from && maybe.back().readable == mapping.readable) maybe.back().end = to; + else maybe.push_back({from, to, mapping.readable}); + } + at += count; + } + close(file); +} +#endif + +std::vector possiblyNonzero(std::uint64_t begin, std::uint64_t end) { + std::vector maybe; +#ifdef _WIN32 + maybe.push_back({begin, end, true}); +#else + for (const auto& mapping : mappings(begin, end)) { + int file = -1; + std::uint64_t offset = 0; + if (GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(mapping.begin), static_cast(mapping.end - mapping.begin), &file, &offset)) { + try { + fileData(maybe, mapping, file, offset); + } catch (...) { + close(file); + throw; + } + close(file); + } else if (mapping.privateAnonymous) { + populatedPages(maybe, mapping); + } else { + maybe.push_back({mapping.begin, mapping.end, mapping.readable}); + } + } +#endif + return maybe; +} + +} + +bool GuestBytesZero(std::uint64_t begin, std::uint64_t end) { + return allZero(begin, end); +} + +GuestRuns GuestNonzeroBlocks(const GuestRuns& candidates) { + GuestRuns runs; + for (const auto& [first, last] : candidates) { + if (first >= last) continue; + const auto maybe = possiblyNonzero(first, last); + std::size_t at = 0; + for (auto block = first / BlockSize * BlockSize; block < last; block += BlockSize) { + const auto from = std::max(first, block); + const auto to = std::min(last, block + BlockSize); + while (at < maybe.size() && maybe[at].end <= from) ++at; + bool nonzero = false; + for (auto next = at; !nonzero && next < maybe.size() && maybe[next].begin < to; ++next) { + const auto low = std::max(from, maybe[next].begin); + const auto high = std::min(to, maybe[next].end); + if (low < high) nonzero = !maybe[next].readable || !allZero(low, high); + } + if (!nonzero) continue; + if (!runs.empty() && runs.back().second == from) runs.back().second = to; + else runs.emplace_back(from, to); + } + } + return runs; +} + +void SetResidentAddressSpaceLimit(std::uint64_t bytes) { + auto& devices = Devices(); + std::lock_guard lock(devices.mutex); + devices.limit = bytes; +} + +std::shared_ptr ResidentBuffer::Create(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed) { + if (!context.sparseBinding || !context.sparseResidencyBuffer || !context.sparseResidencyAliased) return nullptr; + const auto first = base / BlockSize * BlockSize; + const auto size = (base - first + bytes + BlockSize - 1) / BlockSize * BlockSize; + { + auto& devices = Devices(); + std::lock_guard lock(devices.mutex); + const auto limit = devices.limit != 0 ? std::min(devices.limit, context.limits.sparseAddressSpaceSize) : context.limits.sparseAddressSpaceSize; + auto& used = devices.addressSpace[context.device]; + if (size > limit || used > limit - size) return nullptr; + used += size; + } + return std::shared_ptr(new ResidentBuffer(context, base, bytes, backed)); +} + +ResidentBuffer::ResidentBuffer(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed) : context(context) { + origin = base / BlockSize * BlockSize; + lead = base - origin; + size = (lead + bytes + BlockSize - 1) / BlockSize * BlockSize; + try { + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.flags = VK_BUFFER_CREATE_SPARSE_BINDING_BIT | VK_BUFFER_CREATE_SPARSE_ALIASED_BIT; + info.size = size; + info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + const std::array families{context.queueFamily, context.sparseFamily}; + if (context.sparseQueue != VK_NULL_HANDLE && context.sparseFamily != context.queueFamily) { + info.sharingMode = VK_SHARING_MODE_CONCURRENT; + info.queueFamilyIndexCount = static_cast(families.size()); + info.pQueueFamilyIndices = families.data(); + } + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) fail("vkCreateBuffer of a resident heap mirror", result); + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if (requirements.alignment == 0 || requirements.alignment > BlockSize || BlockSize % requirements.alignment != 0) { + char text[160]; + std::snprintf(text, sizeof(text), "the device's sparse buffer block of %llu bytes does not divide a heap mirror's 64 KiB block", static_cast(requirements.alignment)); + throw std::runtime_error(text); + } + type = context.DeviceMemoryType(requirements.memoryTypeBits, SlabBytes, "a resident heap mirror slab"); + memory = residentMemory(context, type); + slots.resize(static_cast(size / BlockSize)); + std::vector wanted(slots.size(), false); + for (const auto& [first, last] : backed) { + if (first >= last) continue; + for (auto index = blockIndex(first); index <= blockIndex(last - 1); ++index) wanted[index] = true; + } + std::vector binds; + for (std::size_t index = 0; index < slots.size();) { + if (wanted[index]) { + auto [slab, at] = memory->Take(); + slots[index] = {slab, at, slab->host.get() + at * BlockSize}; + std::memset(slots[index].host, 0, static_cast(BlockSize)); + binds.push_back({index * BlockSize, BlockSize, slab->memory, at * BlockSize, 0}); + ++bound; + ++index; + continue; + } + auto last = index; + while (last < slots.size() && !wanted[last] && (last - index + 1) * BlockSize <= ZeroBytes) ++last; + binds.push_back({index * BlockSize, (last - index) * BlockSize, memory->Zero(), 0, 0}); + index = last; + } + submit(binds); + const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; + address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); + if (address == 0) fail("vkGetBufferDeviceAddressKHR of a resident heap mirror", VK_ERROR_UNKNOWN); + } catch (...) { + release(); + throw; + } +} + +ResidentBuffer::~ResidentBuffer() { + release(); +} + +void ResidentBuffer::release() noexcept { + if (buffer != VK_NULL_HANDLE) context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + buffer = VK_NULL_HANDLE; + for (auto& slot : slots) { + if (slot.host != nullptr) memory->Put(static_cast(slot.slab), slot.index); + slot = {}; + } + auto& devices = Devices(); + std::lock_guard lock(devices.mutex); + if (const auto found = devices.addressSpace.find(context.device); found != devices.addressSpace.end()) found->second -= std::min(found->second, size); + size = 0; +} + +std::size_t ResidentBuffer::blockIndex(std::uint64_t guest) const { + Require(guest >= origin && guest - origin < size, "guest address outside its resident heap mirror"); + return static_cast((guest - origin) / BlockSize); +} + +std::byte* ResidentBuffer::Block(std::uint64_t guest) const { + const auto& slot = slots[blockIndex(guest)]; + return slot.host == nullptr ? nullptr : slot.host + (guest - origin) % BlockSize; +} + +std::uint64_t ResidentBuffer::UnboundBytes(const GuestRuns& runs) const { + std::vector seen; + for (const auto& [first, last] : runs) { + if (first >= last) continue; + for (auto index = blockIndex(first); index <= blockIndex(last - 1); ++index) { + if (slots[index].host == nullptr) seen.push_back(index); + } + } + std::sort(seen.begin(), seen.end()); + return static_cast(std::unique(seen.begin(), seen.end()) - seen.begin()) * BlockSize; +} + +void ResidentBuffer::Bind(const GuestRuns& runs) { + std::vector binds; + std::vector made; + for (const auto& [first, last] : runs) { + if (first >= last) continue; + for (auto index = blockIndex(first); index <= blockIndex(last - 1); ++index) { + if (slots[index].host != nullptr) continue; + auto [slab, at] = memory->Take(); + slots[index] = {slab, at, slab->host.get() + at * BlockSize}; + std::memset(slots[index].host, 0, static_cast(BlockSize)); + binds.push_back({index * BlockSize, BlockSize, slab->memory, at * BlockSize, 0}); + made.push_back(index); + } + } + if (binds.empty()) return; + if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->Sync(); + try { + submit(binds); + } catch (...) { + for (const auto index : made) { + memory->Put(static_cast(slots[index].slab), slots[index].index); + slots[index] = {}; + } + throw; + } + bound += made.size(); + for (const auto index : made) MarkChanged(origin + index * BlockSize, origin + (index + 1) * BlockSize); +} + +void ResidentBuffer::MarkChanged(std::uint64_t begin, std::uint64_t end) { + if (begin >= end) return; + bool first = false; + { + std::lock_guard lock(changedMutex); + first = changed.empty(); + if (!changed.empty() && changed.back().second == begin) changed.back().second = end; + else changed.emplace_back(begin, end); + } + if (!first) return; + auto& devices = Devices(); + std::lock_guard lock(devices.pendingMutex); + devices.pending.push_back(weak_from_this()); +} + +void ResidentBuffer::Read(std::uint64_t guest, std::span out) const { + std::size_t done = 0; + while (done < out.size()) { + const auto at = guest + done; + const auto within = static_cast((at - origin) % BlockSize); + const auto length = std::min(out.size() - done, static_cast(BlockSize) - within); + if (const auto* source = Block(at); source != nullptr) std::memcpy(out.data() + done, source, length); + else std::memset(out.data() + done, 0, length); + done += length; + } +} + +void ResidentBuffer::Store(std::uint64_t guest, std::span bytes) { + std::size_t done = 0; + while (done < bytes.size()) { + const auto at = guest + done; + const auto within = static_cast((at - origin) % BlockSize); + const auto length = std::min(bytes.size() - done, static_cast(BlockSize) - within); + if (auto* target = Block(at); target != nullptr) std::memcpy(target, bytes.data() + done, length); + else Require(GuestBytesZero(reinterpret_cast(bytes.data() + done), reinterpret_cast(bytes.data() + done + length)), "the GPU wrote a heap mirror block that holds no memory"); + done += length; + } +} + +std::shared_ptr ResidentBuffer::RecordReadback(VkCommandBuffer commands, std::uint64_t guest, std::uint64_t bytes) const { + Require(guest >= origin && bytes <= size && guest - origin <= size - bytes, "heap mirror readback outside its buffer"); + auto staging = std::make_shared(context, static_cast(bytes), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CopyBuffer(context, commands, buffer, guest - origin, staging->Handle(), 0, bytes); + return staging; +} + +void ResidentBuffer::ReadDevice(std::uint64_t guest, std::span out) { + GuestMemory::AssertGpuLockHeld("heap mirror device read"); + Require(!Recorder::InCompletion(), "heap mirror device read inside a GPU completion: its write-back needs a recorded readback"); + auto staging = readbackBatch(guest, out.size()); + std::memcpy(out.data(), staging->Bytes().data(), out.size()); +} + +std::shared_ptr ResidentBuffer::readbackBatch(std::uint64_t guest, std::uint64_t bytes) { + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + auto staging = RecordReadback(batch.Handle(), guest, bytes); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + return staging; +} + +void ResidentBuffer::flush() { + GuestRuns runs; + { + std::lock_guard lock(changedMutex); + runs.swap(changed); + } + if (runs.empty()) return; + try { + std::sort(runs.begin(), runs.end()); + std::vector regions; + std::uint64_t total = 0; + std::uint64_t cursor = 0; + for (const auto& [first, last] : runs) { + for (auto at = std::max(first, cursor); at < last;) { + const auto next = std::min(last, (at / BlockSize + 1) * BlockSize); + if (slots[blockIndex(at)].host != nullptr) { + if (!regions.empty() && regions.back().dstOffset + regions.back().size == at - origin) regions.back().size += next - at; + else regions.push_back({total, at - origin, next - at}); + total += next - at; + } + at = next; + } + cursor = std::max(cursor, last); + } + if (regions.empty()) return; + auto staging = std::make_shared(context, static_cast(total), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + for (const auto& region : regions) { + for (VkDeviceSize done = 0; done < region.size;) { + const auto at = origin + region.dstOffset + done; + const auto length = std::min(region.size - done, BlockSize - (at - origin) % BlockSize); + std::memcpy(staging->Bytes().data() + region.srcOffset + done, Block(at), static_cast(length)); + done += length; + } + } + recordTransfer(context, [&](VkCommandBuffer commands, std::vector>& kept) { + context.Function("vkCmdCopyBuffer")(commands, staging->Handle(), buffer, static_cast(regions.size()), regions.data()); + kept.push_back(staging); + kept.push_back(shared_from_this()); + }); + } catch (...) { + { + std::lock_guard lock(changedMutex); + changed.insert(changed.begin(), runs.begin(), runs.end()); + } + auto& devices = Devices(); + std::lock_guard lock(devices.pendingMutex); + devices.pending.push_back(weak_from_this()); + throw; + } +} + +void ResidentBuffer::FlushChanged(const Context& context) { + auto& devices = Devices(); + std::vector> unzeroed; + { + std::lock_guard lock(devices.mutex); + for (const auto& [key, held] : devices.memories) { + if (std::get<0>(key) != context.device) continue; + if (auto memory = held.lock(); memory != nullptr && !memory->Zeroed()) unzeroed.push_back(std::move(memory)); + } + } + for (auto& memory : unzeroed) recordTransfer(context, [&](VkCommandBuffer commands, std::vector>& kept) { memory->FillZero(commands, kept); }); + std::vector> waiting; + { + std::lock_guard lock(devices.pendingMutex); + waiting.swap(devices.pending); + } + for (std::size_t index = 0; index < waiting.size(); ++index) { + const auto held = waiting[index].lock(); + if (held == nullptr) continue; + if (held->context.device != context.device) { + std::lock_guard lock(devices.pendingMutex); + devices.pending.push_back(waiting[index]); + continue; + } + try { + held->flush(); + } catch (...) { + std::lock_guard lock(devices.pendingMutex); + devices.pending.insert(devices.pending.end(), waiting.begin() + static_cast(index) + 1, waiting.end()); + throw; + } + } +} + +void ResidentBuffer::submit(const std::vector& binds) { + const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; + const VkSparseBufferMemoryBindInfo bufferBinds{buffer, static_cast(binds.size()), binds.data()}; + VkBindSparseInfo bind{VK_STRUCTURE_TYPE_BIND_SPARSE_INFO}; + bind.bufferBindCount = 1; + bind.pBufferBinds = &bufferBinds; + const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence); result != VK_SUCCESS) fail("vkCreateFence of a resident heap mirror bind", result); + VkResult submitted = VK_SUCCESS; + VkResult waited = VK_SUCCESS; + { + std::unique_lock queueLock; + std::unique_lock gpuLock; + if (ownQueue) queueLock = std::unique_lock(*context.sparseQueueMutex); + else gpuLock = std::unique_lock(GuestMemory::GpuMutex()); + submitted = context.Function("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence); + if (submitted == VK_SUCCESS) waited = context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX); + } + context.Function("vkDestroyFence")(context.device, fence, nullptr); + if (submitted != VK_SUCCESS) fail("vkQueueBindSparse of a resident heap mirror", submitted); + if (waited != VK_SUCCESS) fail("vkWaitForFences of a resident heap mirror bind", waited); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp new file mode 100644 index 0000000000..ca3c05ab27 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp @@ -0,0 +1,71 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr VkQueueFlags Graphics = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT; +constexpr VkQueueFlags Sparse = VK_QUEUE_SPARSE_BINDING_BIT; +constexpr std::uint32_t Own = VK_QUEUE_FAMILY_IGNORED; + +struct Case { + const char* name; + std::vector families; + bool settles; + std::uint32_t mainCount; + bool settle; + std::uint32_t settleFamily; + std::uint32_t settleIndex; + bool sparseBinding; + std::uint32_t sparseFamily; + std::uint32_t sparseIndex; +}; + +VkQueueFamilyProperties family(VkQueueFlags flags, std::uint32_t queues) { + VkQueueFamilyProperties properties{}; + properties.queueFlags = flags; + properties.queueCount = queues; + return properties; +} + +std::string describe(const AgcDriver::DeviceQueuePlan& plan) { + return "main " + std::to_string(plan.mainCount) + " queues, settle " + (plan.settle ? std::to_string(plan.settleFamily) + ":" + std::to_string(plan.settleIndex) : std::string("none")) + ", sparse " + (!plan.sparseBinding ? std::string("none") : plan.sparseFamily == Own ? std::string("on the main queue") : std::to_string(plan.sparseFamily) + ":" + std::to_string(plan.sparseIndex)); +} + +void check(const Case& test) { + const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true); + const auto dense = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, false); + Require(plan.mainCount == dense.mainCount && plan.settle == dense.settle && plan.settleFamily == dense.settleFamily && plan.settleIndex == dense.settleIndex, std::string("device queue plan, ") + test.name + ": sparse binding changed the queues: " + describe(dense) + " without it, " + describe(plan) + " with it"); + Require(!dense.sparseBinding && dense.sparseFamily == Own, std::string("device queue plan, ") + test.name + ": a device without sparse binding got a sparse queue: " + describe(dense)); + const bool expected = plan.mainCount == test.mainCount && plan.settle == test.settle && (!test.settle || (plan.settleFamily == test.settleFamily && plan.settleIndex == test.settleIndex)) && plan.sparseBinding == test.sparseBinding && plan.sparseFamily == test.sparseFamily && (test.sparseFamily == Own || plan.sparseIndex == test.sparseIndex); + Require(expected, std::string("device queue plan, ") + test.name + ": got " + describe(plan)); +} + +} + +int main() { + try { + const std::vector cases{ + {"one family with one queue", {family(Graphics | Sparse, 1)}, true, 1, false, 0, 0, true, Own, 0}, + {"one family with one queue, no settles", {family(Graphics | Sparse, 1)}, false, 1, false, 0, 0, true, Own, 0}, + {"settle family with one queue that also binds", {family(Graphics | Sparse, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 1, true, 1, 0, true, Own, 0}, + {"settle family with two queues that also binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 2)}, true, 1, true, 1, 0, true, 1, 1}, + {"settle on the main family", {family(Graphics | Sparse, 2), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 2, true, 0, 1, true, 1, 0}, + {"sparse-only family preferred", {family(Graphics | Sparse, 16), family(VK_QUEUE_TRANSFER_BIT | Sparse, 2), family(Sparse, 1)}, true, 2, true, 0, 1, true, 2, 0}, + {"no settles, a second family binds", {family(Graphics | Sparse, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, false, 1, false, 0, 0, true, 1, 0}, + {"no queue binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT, 1)}, true, 1, true, 1, 0, false, Own, 0}, + {"only the settle queue binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 1, true, 1, 0, false, Own, 0}, + }; + for (const auto& test : cases) check(test); + std::puts("device queue plan tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp new file mode 100644 index 0000000000..7657a227d6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp @@ -0,0 +1,342 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint64_t Block = 65536; +constexpr std::uint64_t HeapBytes = 8u << 20u; +constexpr std::uint64_t JoinedBytes = 16u << 20u; +constexpr std::uint64_t ThreadStride = 16384; +constexpr std::uint64_t StoreBlock = 3 * Block; +constexpr std::uint32_t LateThread = 5; +constexpr std::uint64_t ZeroedBlock = 2 * Block; + +alignas(256) constexpr std::array Code{ + 0x3404008e, 0xdc388000, 0x04000002, 0x34060082, 0xbf8c3f70, 0xe0701000, 0x80010403, 0xe0701000, 0x80020303, 0xbf810000, +}; + +alignas(256) constexpr std::array CopyCode{0x34060082, 0xe0301000, 0x80000403, 0xbf8c3f70, 0xe0701000, 0x80010403, 0xbf810000}; + +class OutputBlock { +public: + OutputBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, Block, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(Block, Block)); +#endif + Require(block != nullptr, "resident heap mirror: cannot allocate the output block"); + GuestAllocations::Mutation().Add(block, Block, true, true, true); + } + + ~OutputBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + OutputBlock(const OutputBlock&) = delete; + OutputBlock& operator=(const OutputBlock&) = delete; + + std::uint32_t* Data() const { return block; } + +private: + std::uint32_t* block = nullptr; +}; + +class GuestHeap { +public: + explicit GuestHeap(bool writable, std::uint64_t ranges = 1, std::uint64_t rangeBytes = HeapBytes, void* at = nullptr) : ranges(ranges), rangeBytes(rangeBytes) { + const auto bytes = rangeBytes * ranges; +#ifdef _WIN32 + raw = VirtualAlloc(at, bytes, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE); + Require(raw != nullptr && (at == nullptr || raw == at), "resident heap mirror: cannot allocate the guest heap"); + base = static_cast(raw); +#else + raw = mmap(at, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | (at != nullptr ? MAP_FIXED_NOREPLACE : 0), -1, 0); + Require(raw != MAP_FAILED && (at == nullptr || raw == at), "resident heap mirror: cannot map the guest heap"); + base = reinterpret_cast((reinterpret_cast(raw) + Block - 1) / Block * Block); +#endif + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(base, bytes); + GuestAllocations::Mutation mutation; + for (std::uint64_t range = 0; range < ranges; ++range) mutation.Add(base + range * rangeBytes, rangeBytes, true, writable, true); + } + + ~GuestHeap() { + { + GuestAllocations::Mutation mutation; + for (std::uint64_t range = 0; range < ranges; ++range) mutation.Remove(base + range * rangeBytes); + } + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(base, rangeBytes * ranges); +#ifdef _WIN32 + VirtualFree(raw, 0, MEM_RELEASE); +#else + munmap(raw, rangeBytes * ranges + Block); +#endif + } + + GuestHeap(const GuestHeap&) = delete; + GuestHeap& operator=(const GuestHeap&) = delete; + + std::uint8_t* Data() const { return base; } + void* Raw() const { return raw; } + std::uint64_t Address() const { return reinterpret_cast(base); } + + void Store(std::uint64_t offset, std::uint32_t value) const { std::memcpy(base + offset, &value, sizeof(value)); } + + std::uint32_t Load(std::uint64_t offset) const { + std::uint32_t value = 0; + std::memcpy(&value, base + offset, sizeof(value)); + return value; + } + +private: + std::uint64_t ranges = 1; + std::uint64_t rangeBytes = HeapBytes; + void* raw = nullptr; + std::uint8_t* base = nullptr; +}; + +struct ImportBudget { + explicit ImportBudget(std::uint64_t bytes) { AgcDriver::Graphics::SetHostImportBudget(bytes); } + ~ImportBudget() { AgcDriver::Graphics::SetHostImportBudget(0); } + ImportBudget(const ImportBudget&) = delete; + ImportBudget& operator=(const ImportBudget&) = delete; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t bytes) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +std::uint32_t Pattern(std::uint32_t thread) { + return 0x5a000000u + thread * 0x00010203u + 1u; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, regions}, + 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 Dispatch(AgcDriver::VulkanDevice& device, std::uint64_t read, std::uint64_t written, const OutputBlock& output) { + std::fill(output.Data(), output.Data() + Threads, 0xdeadbeefu); + std::vector userData(12, 0u); + userData[0] = static_cast(read); + userData[1] = static_cast(read >> 32u); + const auto out = BufferDescriptor(reinterpret_cast(output.Data()), static_cast(Threads * sizeof(std::uint32_t))); + const auto store = BufferDescriptor(written + StoreBlock, static_cast(Threads * sizeof(std::uint32_t))); + std::copy(out.begin(), out.end(), userData.begin() + 4); + std::copy(store.begin(), store.end(), userData.begin() + 8); + Run(device, Code, userData); +} + +void Dispatch(AgcDriver::VulkanDevice& device, const GuestHeap& read, const GuestHeap& written, const OutputBlock& output) { + Dispatch(device, read.Address(), written.Address(), output); +} + +void Copy(AgcDriver::VulkanDevice& device, std::uint64_t source, const OutputBlock& output) { + std::fill(output.Data(), output.Data() + Threads, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto in = BufferDescriptor(source, static_cast(Threads * sizeof(std::uint32_t))); + const auto out = BufferDescriptor(reinterpret_cast(output.Data()), static_cast(Threads * sizeof(std::uint32_t))); + std::copy(in.begin(), in.end(), userData.begin()); + std::copy(out.begin(), out.end(), userData.begin() + 4); + Run(device, CopyCode, userData); +} + +void CheckReads(const OutputBlock& output, const std::string& step, bool late) { + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto expected = thread * ThreadStride < Block ? Pattern(thread) : late && thread == LateThread ? Pattern(thread) : 0u; + Require(output.Data()[thread] == expected, "resident heap mirror: " + step + ": thread " + std::to_string(thread) + " read " + Hex(output.Data()[thread]) + " at heap offset " + Hex(thread * ThreadStride) + ", expected " + Hex(expected)); + } +} + +void CheckStores(const GuestHeap& written, const std::string& step) { + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto value = written.Load(StoreBlock + thread * sizeof(std::uint32_t)); + Require(value == thread * 4u, "resident heap mirror: " + step + ": the GPU store of thread " + std::to_string(thread) + " into a never-written heap block reached the guest as " + Hex(value) + ", expected " + Hex(thread * 4u)); + } +} + +void CheckHeld(std::uint64_t before, std::uint64_t written, const std::string& step) { + const auto held = AgcDriver::Graphics::MirrorCounters().heapBytes - before; + Require(held == written, "resident heap mirror: " + step + ": the heap mirrors of two " + std::to_string(HeapBytes >> 10u) + " KiB heaps hold " + std::to_string(held >> 10u) + " KiB of device memory; the guest and the GPU wrote " + std::to_string(written >> 10u) + " KiB of them"); +} + +void ResidentTests(AgcDriver::VulkanDevice& device) { + GuestHeap read(false); + GuestHeap written(true); + const OutputBlock output; + for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) read.Store(thread * ThreadStride, Pattern(thread)); + written.Store(16, 0x77u); + const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block); + const auto before = AgcDriver::Graphics::MirrorCounters(); + Dispatch(device, read, written, output); + const auto made = AgcDriver::Graphics::MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 2, "resident heap mirror: the two heaps were not heap mirrored (" + std::to_string(made.heapMirrors - before.heapMirrors) + " mirrors made)"); + CheckHeld(before.heapBytes, 3 * Block, "first dispatch"); + CheckReads(output, "first dispatch", false); + CheckStores(written, "first dispatch"); + Require(written.Load(16) == 0x77u, "resident heap mirror: the written heap's guest bytes changed"); + read.Store(LateThread * ThreadStride, Pattern(LateThread)); + std::memset(read.Data() + ZeroedBlock, 0, Block); + Dispatch(device, read, written, output); + CheckHeld(before.heapBytes, 4 * Block, "after a guest write into a never-written block and zeros written into another"); + CheckReads(output, "after a guest write into a never-written block", true); + const auto steady = AgcDriver::Graphics::MirrorCounters(); + Dispatch(device, read, written, output); + const auto again = AgcDriver::Graphics::MirrorCounters(); + Require(again.rebuilds == steady.rebuilds && again.heapBytes == steady.heapBytes, "resident heap mirror: an unchanged dispatch rebuilt or grew the heap mirrors"); + CheckReads(output, "unchanged dispatch", true); + CheckStores(written, "unchanged dispatch"); +} + +struct AddressSpaceLimit { + explicit AddressSpaceLimit(std::uint64_t bytes) { AgcDriver::Graphics::SetResidentAddressSpaceLimit(bytes); } + ~AddressSpaceLimit() { AgcDriver::Graphics::SetResidentAddressSpaceLimit(0); } + AddressSpaceLimit(const AddressSpaceLimit&) = delete; + AddressSpaceLimit& operator=(const AddressSpaceLimit&) = delete; +}; + +void CopiedTests(AgcDriver::VulkanDevice& device) { + const AddressSpaceLimit limit(1); + GuestHeap read(false); + GuestHeap written(true); + const OutputBlock output; + for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) read.Store(thread * ThreadStride, Pattern(thread)); + written.Store(16, 0x77u); + const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block); + const auto before = AgcDriver::Graphics::MirrorCounters(); + Dispatch(device, read, written, output); + const auto made = AgcDriver::Graphics::MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 2, "copied heap mirror: the two heaps were not heap mirrored without sparse address space (" + std::to_string(made.heapMirrors - before.heapMirrors) + " mirrors made)"); + Require(made.heapBytes - before.heapBytes == 2 * HeapBytes, "copied heap mirror: two " + std::to_string(HeapBytes >> 10u) + " KiB heaps without sparse address space hold " + std::to_string((made.heapBytes - before.heapBytes) >> 10u) + " KiB, expected whole copies"); + CheckReads(output, "copied, first dispatch", false); + CheckStores(written, "copied, first dispatch"); + Require(written.Load(16) == 0x77u, "copied heap mirror: the written heap's guest bytes changed"); + read.Store(LateThread * ThreadStride, Pattern(LateThread)); + Dispatch(device, read, written, output); + CheckReads(output, "copied, after a guest write", true); + CheckStores(written, "copied, after a guest write"); +} + +void ReusedAddressTests(AgcDriver::VulkanDevice& device) { + GuestHeap written(true); + const OutputBlock output; + const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block); + void* at = nullptr; + std::uint64_t address = 0; + { + GuestHeap first(false); + for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) first.Store(thread * ThreadStride, Pattern(thread)); + Dispatch(device, first, written, output); + CheckReads(output, "the first heap", false); + at = first.Raw(); + address = first.Address(); + } + GuestHeap second(false, 1, HeapBytes, at); + Require(second.Address() == address, "resident heap mirror: the second heap did not take the first heap's address"); + for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) second.Store(thread * ThreadStride, ~Pattern(thread)); + Dispatch(device, second, written, output); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto expected = thread * ThreadStride < Block ? ~Pattern(thread) : 0u; + Require(output.Data()[thread] == expected, "resident heap mirror: a heap mapped where a mirrored heap was freed: thread " + std::to_string(thread) + " read " + Hex(output.Data()[thread]) + ", expected " + Hex(expected)); + } + const auto held = AgcDriver::Graphics::HeapMirrorsWithin(address, address + HeapBytes); + Require(held.mirrors == 1 && held.bytes == Block, "resident heap mirror: a heap mapped where a mirrored heap was freed holds " + std::to_string(held.mirrors) + " live heap mirrors (" + std::to_string(held.bytes >> 10u) + " KiB), expected 1 (64 KiB)"); +} + +void JoinedImportTests(AgcDriver::VulkanDevice& device) { + GuestHeap pair(true, 2, JoinedBytes); + const OutputBlock output; + const auto begin = pair.Address(); + const auto end = begin + 2 * JoinedBytes; + for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) pair.Store(thread * ThreadStride, Pattern(thread)); + pair.Store(JoinedBytes - sizeof(std::uint32_t), 0x41u); + pair.Store(JoinedBytes, 0x42u); + const auto before = AgcDriver::Graphics::HeapMirrorsWithin(begin, end); + { + const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block); + Dispatch(device, begin, begin + JoinedBytes, output); + CheckReads(output, "two adjacent heaps", false); + } + const auto mirrored = AgcDriver::Graphics::HeapMirrorsWithin(begin, end); + Require(before.mirrors == 0 && mirrored.mirrors == 2 && mirrored.bytes == 4 * Block, "resident heap mirror: two adjacent heaps hold " + std::to_string(before.mirrors) + " heap mirrors before the first dispatch and " + std::to_string(mirrored.mirrors) + " (" + std::to_string(mirrored.bytes >> 10u) + " KiB) after it, expected 0 and 2 (256 KiB); " + std::to_string(AgcDriver::Graphics::HostImportLiveBytes() >> 10u) + " KiB host imports live"); + const auto live = AgcDriver::Graphics::HostImportLiveBytes(); + Copy(device, begin + JoinedBytes - Threads * sizeof(std::uint32_t) / 2, output); + Require(output.Data()[Threads / 2 - 1] == 0x41u && output.Data()[Threads / 2] == 0x42u, "resident heap mirror: a view across two adjacent heaps read " + Hex(output.Data()[Threads / 2 - 1]) + " " + Hex(output.Data()[Threads / 2]) + ", expected 0x41 0x42"); + const auto joined = AgcDriver::Graphics::HostImportLiveBytes() - live; + Require(joined >= 2 * JoinedBytes, "resident heap mirror: a view across two adjacent heaps made no joined host import (" + std::to_string(joined >> 10u) + " KiB imported)"); + Dispatch(device, begin, begin + JoinedBytes, output); + CheckReads(output, "after the joined import", false); + const auto released = AgcDriver::Graphics::HeapMirrorsWithin(begin, end); + Require(released.mirrors == 0 && released.bytes == 0, "resident heap mirror: a joined host import over two heap-mirrored heaps left " + std::to_string(released.mirrors) + " of their " + std::to_string(mirrored.mirrors) + " heap mirrors and " + std::to_string(released.bytes >> 10u) + " of " + std::to_string(mirrored.bytes >> 10u) + " KiB held"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::puts("skipped, no guest write watch: heap mirrors need one"); + return VulkanTestSkipped; + } + ResidentTests(*device); + ReusedAddressTests(*device); + JoinedImportTests(*device); + CopiedTests(*device); + std::puts("resident heap mirror tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From fa61ee5dc820d7e5b35d30b88a9896c86e336c0c Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Sat, 17 Oct 2026 22:40:54 +0100 Subject: [PATCH 0113/1360] fix(agc): import registered ranges larger than the device's largest allocation in pieces An address-based build imported each registered range whole. A range larger than maxMemoryAllocationSize (2 GiB on AMD's Windows driver) failed at vkAllocateMemory, and the build then asked for a heap mirror of the same size, which failed too. Such a range is now imported in pieces no larger than the limit, built from the range window's cells, each one BDA table entry. Single imports above the limit are no longer attempted. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/src/VulkanDevice.cpp | 4 +- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 48 +++++++++++++++++++ .../libSceAgcDriver/tests/BdaResources.cpp | 29 +++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c19c2efa29..55d450fd1c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1390,7 +1390,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest memory is host memory: importing it lets address-based shaders use it in place instead of // copying every registered allocation per draw. if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && std::getenv("APS5_NO_HOST_IMPORT") == nullptr) { - VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceMaintenance3Properties maintenance3{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_3_PROPERTIES}; + VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES, &maintenance3}; VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT, &driverProperties}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); @@ -1402,6 +1403,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : #endif state->drawInputsInPlace = std::getenv("APS5_NO_DRAW_INPUTS_IN_PLACE") == nullptr && (std::getenv("APS5_DRAW_INPUTS_IN_PLACE") != nullptr || DrawInputsInPlaceUsable(driverProperties.driverID, windows)); if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u); + if (!compileOnly) Graphics::SetHostImportMaxBytes(maintenance3.maxMemoryAllocationSize); #ifdef _WIN32 // Imported pages are locked; locking is bounded by the process working-set minimum. if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..4b22397ee1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -113,6 +113,7 @@ void SetHostImportDeferredSettles(bool enabled); // The larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half // that heap: the other half stays for the buffers, mirrors and textures that share it. std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes); +void SetHostImportMaxBytes(std::uint64_t bytes); std::uint64_t HostImportLiveBytes(); struct HostImportSettleCounts { std::uint64_t submitted = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 56368f852f..4fa92664a4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1548,6 +1548,7 @@ std::string budgetSource(const Context& context, const HostImports& state, std:: } std::atomic rangeImportWindow{64ull << 20u}; +std::atomic importMaxBytes{0}; // The import of an access to a registered range larger than the range window: the cells the access touches, grown to // the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of @@ -1654,6 +1655,32 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s return importRunAt(context, state, std::move(run), false, address, lease, end, evict, requester); } +struct ImportPiece { + std::uint64_t begin; + std::uint64_t end; + const HostImport* entry; +}; + +std::vector importPieces(const Context& context, HostImports& state, const GuestAllocations::Range& range, const GuestAllocations::Lease& lease) { + const auto limit = importMaxBytes.load(std::memory_order_relaxed); + const auto window = rangeImportWindow.load(std::memory_order_relaxed); + const auto alignment = context.hostImportAlignment; + if (limit < 2 * window || window == 0 || alignment == 0 || window % alignment != 0 || range.address % alignment != 0) return {}; + const auto step = (limit / window - 1) * window; + const auto end = range.address + range.bytes; + std::vector pieces; + for (auto cursor = range.address; cursor < end;) { + const auto wanted = end - cursor < step + window ? end : cursor + step; + const auto* entry = importAllocation(context, state, cursor, lease, ImportRequester::Build, wanted, false); + if (entry == nullptr || entry->base > cursor || entry->bytes > limit) return {}; + const auto to = std::min(end, entry->base + entry->bytes); + if (to <= cursor) return {}; + pieces.push_back({cursor, to, entry}); + cursor = to; + } + return pieces; +} + const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { if (const auto* low = peekImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; if (const auto* high = peekImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes; @@ -1679,6 +1706,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return &found->second; } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; + if (const auto limit = importMaxBytes.load(std::memory_order_relaxed); limit != 0 && bytes > limit) return nullptr; if (const auto failed = failures.find(base); !stepDown && failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); @@ -3379,6 +3407,10 @@ void SetHostImportRangeWindow(std::uint64_t bytes) { rangeImportWindow.store(bytes, std::memory_order_relaxed); } +void SetHostImportMaxBytes(std::uint64_t bytes) { + importMaxBytes.store(bytes, std::memory_order_relaxed); +} + void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); auto& state = Imports(); @@ -3737,8 +3769,14 @@ void GuestBufferMemory::AcquireRegistered() { validate(range->address, range->bytes); Region region{range->address, range->address + range->bytes, range->writable, {}, nullptr}; const HostImport* entry = nullptr; + std::vector pieces; + const auto limit = importMaxBytes.load(std::memory_order_relaxed); if (importable && covering != nullptr && range->address >= coveredBegin && range->address + range->bytes <= coveredEnd) { entry = covering; + } else if (importable && limit != 0 && range->bytes > limit) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + pieces = importPieces(context, state, *range, lease); } else if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); @@ -3750,6 +3788,16 @@ void GuestBufferMemory::AcquireRegistered() { } } lap(timing.importsUs, at); + if (!pieces.empty()) { + for (const auto& piece : pieces) { + Region part{piece.begin, piece.end, range->writable, {}, nullptr}; + part.hostBacked = true; + part.direct = piece.entry; + regions.push_back(std::move(part)); + } + imported.push_back(range->address); + continue; + } if (entry != nullptr) { region.hostBacked = true; // Reused by Upload while no import was dropped since (see importsEpoch). diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d625cf165..db20b0e55a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -905,6 +905,34 @@ void importRefusedWholeRangeTests(const Context& context) { Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import"); } +void importPiecesOverAllocationLimitTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 5); + const auto first = range.First(); + const auto limit = 3 * range.window; + SetHostImportMaxBytes(limit); + { + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + std::uint64_t covered = first; + for (const auto& entry : leased.AddressRanges()) { + if (entry.end <= first || entry.begin >= first + range.bytes) continue; + Require(entry.begin == covered, "the pieces of a range over the allocation limit leave a gap or overlap in the BDA table"); + Require(entry.end - entry.begin <= limit, "a BDA table entry of a range over the allocation limit is larger than the limit"); + const auto* piece = ExistingHostImportFor(range.importing, entry.begin, static_cast(entry.end - entry.begin)); + Require(piece != nullptr && piece->bytes <= limit && entry.deviceAddress == piece->address + (entry.begin - piece->base), "a BDA table entry of a range over the allocation limit is not served in place by an import within the limit"); + covered = entry.end; + } + Require(covered == first + range.bytes, "the pieces of a range over the allocation limit do not cover it"); + for (std::uint64_t at = first; at < first + range.bytes; at += range.window) { + const auto* piece = ExistingHostImportFor(range.importing, at, 16); + Require(piece != nullptr && piece->bytes <= limit, "a range over the allocation limit has an import larger than the limit"); + } + leased.WriteBack(); + } + SetHostImportMaxBytes(0); +} + // Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows // the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at // every use. @@ -3364,6 +3392,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importWholeRangeOverWindowTests(context); importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); + importPiecesOverAllocationLimitTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); importRequesterTests(context, access); From c819b9b3f19d04643de985770a2dcc5c728a86a7 Mon Sep 17 00:00:00 2001 From: Longziip Date: Mon, 19 Oct 2026 23:53:18 +0100 Subject: [PATCH 0114/1360] fix(agc): keep storage image results recorded after a direct write when its completion stamps it again A dispatch's direct (host-imported) buffer writes are stamped when it is recorded, and stamped again by its completion write-back. A storage image written later in GPU order over the same memory saw the second stamp as a newer write and dropped its pending results on its next refresh, re-uploading the dispatch's older bytes. --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/include/Texture.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 9 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 33 +++ .../tests/execution/DirectWriteCompletion.cpp | 224 ++++++++++++++++++ 5 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..28d51d428a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -495,6 +495,7 @@ private: bool prepared = false; bool uploaded = false; bool committed = false; + mutable std::uint64_t directWritesGeneration = 0; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f54483fcbd..c18c4ee8c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -224,6 +225,7 @@ public: // any of the [begin, end) ranges: one acquisition of the registry's mutex. static bool AnyPendingOverlaps(std::span> ranges); static bool AnyLiveOverlaps(std::span> ranges); + static std::uint64_t StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function& stamp); // Moves with every change of the pending registry (an image marked dirty, stored, flushed or // dropped; after the store for a flush), bumped under the registry's mutex with the change, so // a reader that loads it before and after its own reads knows the registry it consulted stayed diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 56368f852f..76bf87ce85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -5265,7 +5265,10 @@ void GuestBufferMemory::MarkDirectWrites() const { const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; - if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast(end - begin)); + if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) { + const auto generation = GuestMemory::MarkWritten(begin, static_cast(end - begin)); + if (generation != 0) directWritesGeneration = std::max(directWritesGeneration, generation); + } } } @@ -5353,7 +5356,7 @@ void GuestBufferMemory::WriteBack() { if (region.direct == nullptr && !(region.gpuCopy && (region.copiedBack || region.deviceLocal)) && !(region.mirror != nullptr && !region.mirror->writable)) Recorder::NoteWrittenBack(begin, static_cast(end - begin)); if (region.direct != nullptr) { // The GPU wrote imported guest memory directly; page write watching did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + StorageTexture::StampKeepingNewerResults(begin, static_cast(end - begin), directWritesGeneration, [&] { return GuestMemory::MarkWritten(begin, static_cast(end - begin)); }); continue; } if (region.mirror != nullptr) { @@ -5383,7 +5386,7 @@ void GuestBufferMemory::WriteBack() { if (region.copiedBack) { // The GPU copied the written sub-range back into the import; page write watching // did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + StorageTexture::StampKeepingNewerResults(begin, static_cast(end - begin), directWritesGeneration, [&] { return GuestMemory::MarkWritten(begin, static_cast(end - begin)); }); continue; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a2d90b6926..e548e78b1b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4039,6 +4039,39 @@ bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const Stor return false; } +std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function& stamp) { + std::vector, std::uint32_t>> kept; + if (recorded != 0 && bytes != 0) { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (!texture->blockUnits || !texture->overlaps(address, bytes)) continue; + auto alive = texture->weak_from_this().lock(); + if (alive == nullptr) continue; + for (std::uint32_t unit = 0; unit < texture->trackedLayers; ++unit) { + const auto begin = texture->layerBegin(unit); + const auto unitBytes = texture->layerBytes(unit); + if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue; + if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit); + } + } + } + std::erase_if(kept, [](const auto& entry) { + const auto& [texture, unit] = entry; + return !GuestMemory::UnchangedSince(texture->layerBegin(unit), static_cast(texture->layerBytes(unit)), texture->layerGeneration[unit]); + }); + const auto generation = stamp(); + if (generation == 0 || kept.empty()) return generation; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (const auto& [texture, unit] : kept) { + if (!texture->layerPending[unit]) continue; + texture->layerGeneration[unit] = generation; + texture->refreshGeneration(); + } + return generation; +} + bool StorageTexture::AnyPendingOverlaps(std::span> ranges) { if (ranges.empty()) return false; auto& pending = Pending(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp new file mode 100644 index 0000000000..0a0d22a8bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp @@ -0,0 +1,224 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t Block = 0x10000; +constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Stored = 0x12345678u; +constexpr std::uint32_t Filled = 0x9abcdef0u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xe01c2000, 0x80020004, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(std::vector& words, std::span code, std::uint32_t threads, std::uint32_t resource, std::span userData, std::uint32_t groups) { + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{threads, 1, 1}; + registers(0x207, shape); + const auto address = AddressOf(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array layout{resource}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); +} + +void Store(std::vector& words, const std::uint32_t* texels) { + std::array userData{}; + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + Dispatch(words, StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); +} + +void Fill(std::vector& words, const std::uint32_t* texels, const std::uint32_t* copied) { + const auto address = AddressOf(texels); + const auto other = AddressOf(copied); + const std::array userData{ + static_cast(address), + static_cast((address >> 32u) & 0xffffu) | (16u << 16u), + static_cast(SurfaceBytes / 16u), + 0x4bfacu, + Filled, + Filled, + Filled, + Filled, + static_cast(other), + static_cast((other >> 32u) & 0xffffu) | (16u << 16u), + static_cast(SurfaceBytes / 16u), + 0x4bfacu, + }; + Dispatch(words, FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); +} + +class Surface { +public: + Surface() { +#ifdef _WIN32 + region = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); + if (region != nullptr) GuestArena::GuestArenaCommit_nid_postfix(region, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1}; + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned != begin + Block) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Block - aligned); + region = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(region, SurfaceBytes); + } +#endif + Require(region != nullptr, "direct write completion: cannot allocate the guest surface"); + texels = reinterpret_cast(region); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(region, SurfaceBytes, true, true, true); + } + + ~Surface() { + GuestAllocations::Mutation().Remove(region); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(region, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(region, SurfaceBytes); +#else + munmap(region, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(region, SurfaceBytes); +#endif + } + + Surface(const Surface&) = delete; + Surface& operator=(const Surface&) = delete; + + std::uint32_t* Texels() const { return texels; } + +private: + std::uint8_t* region = nullptr; + std::uint32_t* texels = nullptr; +}; + +enum class Outcome { Kept, Dropped, Missed }; + +Outcome StoreAfterFill(const Surface& surface) { + std::vector copied(SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(copied.data(), SurfaceBytes, true, true, true); + struct Unregister { + void* data; + ~Unregister() { GuestAllocations::Mutation().Remove(data); } + } unregister{copied.data()}; + std::vector words; + Fill(words, surface.Texels(), copied.data()); + Store(words, surface.Texels()); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + std::vector landed(SurfaceBytes); + AgcDriver::GuestMemory::Read(AddressOf(copied.data()), landed); + const auto address = AddressOf(surface.Texels()); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + const auto image = StorageTexture::FindPending(address, SurfaceBytes); + if (image == nullptr) return Outcome::Missed; + const bool current = image->Refresh(); + const bool pending = StorageTexture::FindPending(address, SurfaceBytes) == image; + StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "direct write completion fixture"); + return current && pending ? Outcome::Kept : Outcome::Dropped; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + { + Surface probe; + Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "direct write completion: unexpected surface size"); + } + { + constexpr std::size_t Runs = 16; + std::vector> surfaces; + for (std::size_t index = 0; index <= Runs; ++index) surfaces.push_back(std::make_unique()); + static_cast(StoreAfterFill(*surfaces[0])); + std::size_t kept = 0; + std::size_t dropped = 0; + for (std::size_t run = 1; run <= Runs; ++run) { + const auto outcome = StoreAfterFill(*surfaces[run]); + kept += outcome == Outcome::Kept; + dropped += outcome == Outcome::Dropped; + } + AgcDriverWaitIdle_nid_postfix(); + std::printf("[direct write completion] %zu of %zu runs held the image results pending, %zu dropped them\n", kept + dropped, Runs, dropped); + Require(kept + dropped != 0, "direct write completion: no run held the image results pending"); + Require(dropped == 0, "direct write completion: the completion of a buffer write dropped image results recorded after it"); + } + AgcDriverShutdown_nid_postfix(); + std::puts("direct write completion tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2d1fa5aea512ac3809234f6e45ddeb62b3f16d42 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 18:55:26 +0300 Subject: [PATCH 0115/1360] fix(libkernel): report O_RDWR from F_GETFL on both ends of a pipe FreeBSD 11's kern_pipe opens both ends with FREAD | FWRITE, so F_GETFL reports O_RDWR on each. The host pipe ends are one-way, so the access mode came back as O_RDONLY and O_WRONLY. Descriptors made by pipe() are now marked in the guest descriptor table, the mark follows dup, dup2 and F_DUPFD, and F_GETFL reports O_RDWR for them. --- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 19 ++++++++++++++++--- core/libs/prx/libkernel/File/src/Stdio.cpp | 4 ++-- core/libs/tests/GuestDup.cpp | 7 ++++--- core/libs/tests/GuestKernelFcntl.cpp | 12 +++++++----- docs/dev/debt/libkernel.md | 2 +- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 341324638c..1a76fc6de9 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -13,6 +13,7 @@ bool IoctlRequestMalformed(std::uint64_t request); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); +void RememberPipe(int fd); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index a83f899801..9b29b05df3 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -114,6 +114,7 @@ enum class Kind { File, Directory, Pipe, Socket, Device, RandomDevice, ArchivedF struct Entry { bool closeOnExec = false; std::shared_ptr status; + bool pipe = false; }; std::mutex g_mutex; @@ -361,6 +362,12 @@ int CurrentStatus(int fd) { return *Materialize(fd).status; } +bool CreatedByPipe(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_entries.find(fd); + return found != g_entries.end() && found->second.pipe; +} + void SetCloseOnExec(int fd, bool value) { std::lock_guard lock(g_mutex); Materialize(fd).closeOnExec = value; @@ -481,7 +488,7 @@ int Fcntl(int fd, int command, std::intptr_t argument) { return 0; case GetStatusFlags: { if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); - const int access = AccessMode(fd, "fcntl"); + const int access = CreatedByPipe(fd) ? ReadWrite : AccessMode(fd, "fcntl"); return access | CurrentStatus(fd); } case SetStatusFlags: { @@ -560,11 +567,17 @@ void RememberDescriptor(int fd, int openFlags) { #endif } +void RememberPipe(int fd) { + RememberDescriptor(fd, 0); + std::lock_guard lock(g_mutex); + g_entries[fd].pipe = true; +} + void ShareDescriptor(int from, int to) { File::ShareArchivedFile(from, to); std::lock_guard lock(g_mutex); - auto status = Materialize(from).status; - g_entries[to] = Entry{false, std::move(status)}; + const auto& source = Materialize(from); + g_entries[to] = Entry{false, source.status, source.pipe}; #ifdef _WIN32 File::SharePositionedTransfer(from, to); GuestFileLockShare_nid_no_patch(from, to); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5e7f29b807..28e8e368cb 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -552,8 +552,8 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { const int result = ::pipe(native); #endif if (result != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); - File::RememberDescriptor(native[0], 0); - File::RememberDescriptor(native[1], 0); + File::RememberPipe(native[0]); + File::RememberPipe(native[1]); std::memcpy(descriptors, native, sizeof(native)); return 0; } diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 4e0451ae85..0a8016a79b 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -190,6 +190,7 @@ static void DuplicatesShareStatusFlags() { constexpr int SetDescriptorFlags = 2; constexpr int GetFlags = 3; constexpr int SetFlags = 4; + constexpr int ReadWrite = 2; constexpr int NonBlocking = 4; constexpr int GuestEagain = 35; int pipe[2]; @@ -201,14 +202,14 @@ static void DuplicatesShareStatusFlags() { Require(duplicate >= 0); Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(duplicate, SetFlags, NonBlocking) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == NonBlocking); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadWrite | NonBlocking)); Require(dup2_nid_postfix(pipe[0], other[0]) == other[0]); Require(fcntl_nid_postfix(other[0], GetDescriptorFlags) == 0); - Require(fcntl_nid_postfix(other[0], GetFlags) == NonBlocking); + Require(fcntl_nid_postfix(other[0], GetFlags) == (ReadWrite | NonBlocking)); char buffer[4] = {}; RequireFailure(static_cast(read_nid_postfix(other[0], buffer, sizeof(buffer))), GuestEagain); Require(fcntl_nid_postfix(pipe[0], SetFlags, 0) == 0); - Require(fcntl_nid_postfix(duplicate, GetFlags) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == ReadWrite); Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); for (const int descriptor : {pipe[0], pipe[1], other[0], other[1], duplicate}) Require(close_nid_postfix(descriptor) == 0); } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index b75d3c22a9..3220eaa25c 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -425,8 +425,10 @@ static void RecordLocksBelongToTheProcess() { static void PipeFlagsRoundTrip() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); - Require(fcntl_nid_postfix(pipe[1], GetFlags) == WriteOnly); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(pipe[1], GetFlags) == ReadWrite); + const int duplicate = fcntl_nid_postfix(pipe[1], DuplicateCommand, 20); + Require(duplicate >= 20 && fcntl_nid_postfix(duplicate, GetFlags) == ReadWrite && close_nid_postfix(duplicate) == 0); Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); @@ -436,7 +438,7 @@ static void PipeFlagsRoundTrip() { Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadWrite | NonBlocking)); char buffer[8] = {}; RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); Require(write_nid_postfix(pipe[1], "abc", 3) == 3); @@ -448,10 +450,10 @@ static void PipeFlagsRoundTrip() { int off = 0; Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &off) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadWrite); int on = 1; Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &on) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadWrite | NonBlocking)); Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); char block[1024] = {}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 446c7c554c..dfa8034c8c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -202,7 +202,7 @@ - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. From ed373faf9e3635a3bba9c3b9d85d39f5000a3d5a Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 20 Oct 2026 19:20:57 +0300 Subject: [PATCH 0116/1360] test(agc): capture the FMASK output sentinel explicitly --- .../libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 709ceda2ab..db7defe1ad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -799,7 +799,7 @@ void NativeFmask(std::unique_ptr& device) { std::string error; try { DispatchNativeFmask(*device, descriptor, false); } catch (const std::runtime_error& e) { error = e.what(); } Require(error.find(reason) != std::string::npos, "native FMASK missing refusal: " + error); - Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "rejected native FMASK wrote output"); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [sentinel](auto word) { return word == sentinel; }), "rejected native FMASK wrote output"); }; refused(NativeFmaskDescriptor(unrelated, cmask, pipeXor), "descriptor does not match"); refused(NativeFmaskDescriptor(fmask, unrelated, pipeXor), "descriptor does not match"); From d3b2d56b205f5198a64b2aac1ce06e2618664d89 Mon Sep 17 00:00:00 2001 From: Longziip Date: Tue, 20 Oct 2026 17:32:20 +0100 Subject: [PATCH 0117/1360] fix(agc): collect CPU writes over kept storage image units before the completion stamps them StampKeepingNewerResults filtered the units it keeps with UnchangedSince, which reads only the block stamps. A CPU write over a pending unit that nothing had collected yet was absorbed into the completion's MarkWritten, and moving the unit to that generation kept older GPU texels over the CPU's bytes. Each candidate unit's range is now collected first, as the other pending-result decisions in Texture.cpp do. The direct write completion test now uses as many surfaces as fit in one settled host import block: a surface past it was imported alone below the settle floor and unwatched, so its record-time stamp returned 0 and its results were dropped by design. --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 5 ++++- .../tests/execution/DirectWriteCompletion.cpp | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e548e78b1b..9f2d96de3b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4058,7 +4058,10 @@ std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, st } std::erase_if(kept, [](const auto& entry) { const auto& [texture, unit] = entry; - return !GuestMemory::UnchangedSince(texture->layerBegin(unit), static_cast(texture->layerBytes(unit)), texture->layerGeneration[unit]); + const auto begin = texture->layerBegin(unit); + const auto unitBytes = static_cast(texture->layerBytes(unit)); + GuestMemory::CollectWritesUncached(begin, unitBytes); + return !GuestMemory::UnchangedSince(begin, unitBytes, texture->layerGeneration[unit]); }); const auto generation = stamp(); if (generation == 0 || kept.empty()) return generation; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp index 0a0d22a8bf..4035ab9f22 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp @@ -37,6 +37,7 @@ constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t Block = 0x10000; constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::size_t SettledImportBytes = 64 * Block; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t Filled = 0x9abcdef0u; @@ -198,7 +199,7 @@ int main() { Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "direct write completion: unexpected surface size"); } { - constexpr std::size_t Runs = 16; + constexpr std::size_t Runs = SettledImportBytes / SurfaceBytes - 1; std::vector> surfaces; for (std::size_t index = 0; index <= Runs; ++index) surfaces.push_back(std::make_unique()); static_cast(StoreAfterFill(*surfaces[0])); From 021d6d98d658e13cd0ecdbadc791a2495a865ea4 Mon Sep 17 00:00:00 2001 From: Longziip Date: Tue, 20 Oct 2026 17:54:08 +0100 Subject: [PATCH 0118/1360] test(agc): drop storage image results over a CPU write made before the direct write completion The submission ends with a CPU-released WAIT_REG_MEM; the test writes the surface under GpuMutex once the image is pending and then releases the label, so the write lands after the record and before the completion's stamp. The image's results must be dropped and the CPU bytes kept. --- .../tests/execution/DirectWriteCompletion.cpp | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp index 4035ab9f22..889e5f46cb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp @@ -17,6 +17,8 @@ #endif #include #include +#include +#include #include #include #include @@ -25,6 +27,7 @@ #include #include #include +#include #include namespace { @@ -41,6 +44,7 @@ constexpr std::size_t SettledImportBytes = 64 * Block; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t Filled = 0x9abcdef0u; +constexpr std::uint32_t Written = 0x0badf00du; alignas(256) constexpr std::array StoreCode{ 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, @@ -158,7 +162,23 @@ private: enum class Outcome { Kept, Dropped, Missed }; -Outcome StoreAfterFill(const Surface& surface) { +void WriteOnceRecorded(const Surface& surface) { + const auto address = AddressOf(surface.Texels()); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + { + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + if (StorageTexture::FindPending(address, SurfaceBytes) != nullptr) { + surface.Texels()[0] = Written; + return; + } + } + Require(std::chrono::steady_clock::now() < deadline, "direct write completion: the image store was never recorded"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +Outcome StoreAfterFill(const Surface& surface, bool cpuWrite) { std::vector copied(SurfaceBytes / 4u, Untouched); GuestAllocations::Mutation().Add(copied.data(), SurfaceBytes, true, true, true); struct Unregister { @@ -168,8 +188,18 @@ Outcome StoreAfterFill(const Surface& surface) { std::vector words; Fill(words, surface.Texels(), copied.data()); Store(words, surface.Texels()); + alignas(8) static std::uint32_t gate = 0; + gate = 0; + if (cpuWrite) { + const auto at = AddressOf(&gate); + words.insert(words.end(), {0xc0053c00u, 0x13u, static_cast(at), static_cast(at >> 32u), 1u, 0xffffffffu, 0x19u}); + } Packet packet{words.data(), static_cast(words.size()), 0, {}}; AgcDriver::Submit(&packet, 0); + if (cpuWrite) { + WriteOnceRecorded(surface); + std::atomic_ref(gate).store(1); + } AgcDriverWaitIdle_nid_postfix(); std::vector landed(SurfaceBytes); AgcDriver::GuestMemory::Read(AddressOf(copied.data()), landed); @@ -201,12 +231,12 @@ int main() { { constexpr std::size_t Runs = SettledImportBytes / SurfaceBytes - 1; std::vector> surfaces; - for (std::size_t index = 0; index <= Runs; ++index) surfaces.push_back(std::make_unique()); - static_cast(StoreAfterFill(*surfaces[0])); + for (std::size_t index = 0; index <= 2 * Runs + 1; ++index) surfaces.push_back(std::make_unique()); + static_cast(StoreAfterFill(*surfaces[0], false)); std::size_t kept = 0; std::size_t dropped = 0; for (std::size_t run = 1; run <= Runs; ++run) { - const auto outcome = StoreAfterFill(*surfaces[run]); + const auto outcome = StoreAfterFill(*surfaces[run], false); kept += outcome == Outcome::Kept; dropped += outcome == Outcome::Dropped; } @@ -214,6 +244,17 @@ int main() { std::printf("[direct write completion] %zu of %zu runs held the image results pending, %zu dropped them\n", kept + dropped, Runs, dropped); Require(kept + dropped != 0, "direct write completion: no run held the image results pending"); Require(dropped == 0, "direct write completion: the completion of a buffer write dropped image results recorded after it"); + static_cast(StoreAfterFill(*surfaces[Runs + 1], false)); + std::size_t keptOverWrite = 0; + std::size_t lostWrites = 0; + for (std::size_t run = Runs + 2; run <= 2 * Runs + 1; ++run) { + keptOverWrite += StoreAfterFill(*surfaces[run], true) == Outcome::Kept; + lostWrites += surfaces[run]->Texels()[0] != Written; + } + AgcDriverWaitIdle_nid_postfix(); + std::printf("[direct write completion] %zu of %zu runs with a CPU write before the completion kept the image results, %zu lost the write\n", keptOverWrite, Runs, lostWrites); + Require(keptOverWrite == 0, "direct write completion: image results were kept over a CPU write made before the completion"); + Require(lostWrites == 0, "direct write completion: a CPU write made before the completion was lost"); } AgcDriverShutdown_nid_postfix(); std::puts("direct write completion tests passed"); From d046ddee8c607f2d1e6ecde295407e201c56a769 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:13 +0100 Subject: [PATCH 0119/1360] fix(agc): zero declined DCC clear fills and partial trailing elements --- .../Graphics/src/DccMetadata.cpp | 9 +++++++-- .../prx/libSceAgcDriver/tests/TextureFormat.cpp | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 043bb2697a..709eae6300 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -652,7 +652,10 @@ bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span(layout.channels) - 1 : 0; @@ -667,7 +670,9 @@ bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span> i) & 1u) != 0) element[bit / 8u] |= static_cast(1u << (bit % 8u)); } } - for (std::size_t offset = 0; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size()); + std::size_t offset = 0; + for (; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size()); + std::fill(bytes.begin() + offset, bytes.end(), std::byte{0}); return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index bf25b268ee..85fc02afb3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -204,6 +204,22 @@ void packedFloatDccClearTests() { Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys"); } +void fillContractTests() { + std::array texels; + texels.fill(0xdeadbeefu); + auto span = std::as_writable_bytes(std::span(texels)); + Require(!FillDccClear(VK_FORMAT_R8G8B8A8_UNORM, DccKeys::ClearRegister, false, span), "a register clear encoded a texel"); + for (const auto texel : texels) Require(texel == 0u, "a declined clear left its span untouched"); + texels.fill(0xdeadbeefu); + Require(!FillDccClear(VK_FORMAT_R8G8B8A8_UNORM, DccKeys::Uncompressed, false, span), "an uncompressed code encoded a texel"); + for (const auto texel : texels) Require(texel == 0u, "a declined clear left its span untouched"); + std::array partial; + partial.fill(std::byte{0x5a}); + Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, DccKeys::Clear1111, false, partial), "packed float fast clear was not encoded"); + const std::array white{std::byte{0xc0}, std::byte{0x03}, std::byte{0x1e}, std::byte{0x78}}; + for (std::size_t index = 0; index < partial.size(); ++index) Require(partial[index] == (index < 8u ? white[index % 4u] : std::byte{0}), "a clear fill over a partial trailing element did not zero the tail"); +} + } void RunTextureFormatTests() { @@ -266,4 +282,5 @@ void RunTextureFormatTests() { convertedDccClearTests(); singleDccKeysTests(); packedFloatDccClearTests(); + fillContractTests(); } From 667f3c5b4d910656161f7182934a3c62d14731b2 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:13 +0100 Subject: [PATCH 0120/1360] perf(agc): age-bound carried snapshot buffers in the sampled cache --- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6e9632f272..d3abcdffd8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -120,8 +120,11 @@ struct CachedTexture { // write watch; zero when none was taken. std::array untracked{}; std::vector carried; + std::uint64_t carriedPresent = 0; }; +constexpr std::uint64_t CarriedBufferMaxPresents = 16; + // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the // eviction takes the back. APS5_NO_TEXTURE_HASH=1 finds entries by scanning the list (the index is // still kept), the linear lookup of before. @@ -776,6 +779,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span recycled; if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { + if (!it->carried.empty() && Recorder::Presents() - it->carriedPresent > CarriedBufferMaxPresents) it->carried = std::vector{}; // The view follows the storage image, whatever the GPU wrote to it since; guest memory // written meanwhile is taken in by refreshing the image. A fast clear the image cannot // see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear. @@ -820,12 +824,15 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes.size() == bytes) recycled = std::move(it->bytes); - else if (it->carried.size() == bytes) recycled = std::move(it->carried); + else if (it->carried.size() == bytes && Recorder::Presents() - it->carriedPresent <= CarriedBufferMaxPresents) recycled = std::move(it->carried); eraseTexture(cache, it); ++cache.counts.stale; } CachedTexture entry{key, address, source != nullptr ? std::vector{} : (recycled.empty() ? std::vector(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration}; - if (source != nullptr) entry.carried = std::move(recycled); + if (source != nullptr) { + entry.carried = std::move(recycled); + entry.carriedPresent = Recorder::Presents(); + } entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; From 66e8ffd7e683d910913e0f12498900846e5e388a Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:13 +0100 Subject: [PATCH 0121/1360] test(agc): cover sampled snapshot buffer recycling --- .../tests/execution/SampledCacheBudget.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 881867d747..9b6a76ecba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -443,6 +443,22 @@ void snapshotTests(const Context& context) { Require(TextureCacheUsage().sampledBytes == tiny->AllocationBytes(), "a snapshot whose " + std::to_string(tiny->AllocationBytes()) + "-byte image is smaller than its " + std::to_string(guestBytes) + " guest bytes counted " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes, not its allocation"); } +void recycleTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + const auto address = block.Surface(0); + std::memset(reinterpret_cast(address), 0xa1, SurfaceBytes); + const auto first = Sampled(context, address); + Require(Sampled(context, address) == first, "an unchanged surface did not serve its snapshot"); + std::memset(reinterpret_cast(address), 0xb2, SurfaceBytes); + const auto second = Sampled(context, address); + Require(second != first, "a changed surface did not replace its snapshot"); + std::memset(reinterpret_cast(address), 0xa1, SurfaceBytes); + const auto third = Sampled(context, address); + Require(third != second, "the replaced snapshot's size-equal buffer was not reread from guest memory"); + Require(Sampled(context, address) == third, "an unchanged surface did not serve its remade snapshot"); +} + void heldTests(const Context& context) { Block block(context); auto held = Sampled(context, block.Surface(0)); @@ -769,6 +785,7 @@ int main(int argc, char** argv) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); reportedBudgetTests(context); snapshotTests(context); + recycleTests(context); heldTests(context); releasedTests(context); rescanTests(context); From aa210e4e5adfa6302869a603f1c589bec8cc8866 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:00:36 +0200 Subject: [PATCH 0122/1360] feat(agc): render 4_4_4_4 UNORM color targets through packed R16_UINT exports CB_COLOR_INFO format 19 with UNORM and COMP_SWAP 2 lists A, B, G, R from the least significant bits. No Vulkan device is required to render into the 4-bit formats and NVIDIA exposes none of them as color attachments, so the target is an R16_UINT image and the pixel shader packs the exports into the guest layout, as the 10_11_11 target does. Writes of fewer than four components, blending, DCC and truncating rounding are refused. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 10 ++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 45 ++++++++++++++++++- .../ControlFlow/src/RequestSerializer.cpp | 2 +- .../FragmentOutputSpecialization.hpp | 2 +- core/shader/recompiler/Recompiler.hpp | 3 +- .../SpirvBackend/src/SpirvBufferFormat.cpp | 3 ++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 4 +- 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1..89a9db0042 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -807,6 +807,7 @@ struct DecodedColorFormat { }; const char* PackedColorTargetName(ShaderRecompiler::ColorExportPacking packing) { + if (packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4) return "4_4_4_4 unorm"; return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm"; } @@ -820,7 +821,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, }; const bool alternate = swap == 1; const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; - if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 7 && format != 10 && format != 12 && format != 16) return fail(); + if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 7 && format != 10 && format != 12 && format != 16 && format != 19) return fail(); switch (format) { case 1: if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); @@ -898,6 +899,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, case 17: if (swap != 0 || number != unorm) return fail(); return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u}; + case 19: + if (swap != 2 || number != unorm) return fail(); + return {VK_FORMAT_R16_UINT, 2, 0x1bu, ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4}; default: return fail(); } @@ -1461,7 +1465,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); - Require((exportedMask & 7u) == 7u, "partial writes of " + packed + " color target are unsupported"); + const std::uint32_t packedMask = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 ? 0xfu : 7u; + Require((exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; @@ -1721,6 +1726,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.packing = decoded.packing; if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); + Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 4_4_4_4 unorm color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Float11_11_10 || (info & 0x00040000u) != 0, "rounding (ROUND_MODE 0) 11_11_10 float color targets are unsupported"); for (std::uint32_t word = 0; word < 2; ++word) { const auto clear = find(cx, 0x323 + word + stride); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e28a78b241..79c1f4a4ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8793,6 +8793,48 @@ void unorm10_11_11TargetTests() { expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); } +void unorm4_4_4_4TargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (19u << 2u) | (2u << 11u); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R16_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0x1bu && state.color.packing == ColorExportPacking::Unorm4_4_4_4, "a 4_4_4_4 unorm target did not decode to an R16_UINT attachment with reversed exports"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Unorm4_4_4_4 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 4_4_4_4 unorm packing"); + auto changed = queue; + changed.context[0x8e] = 7; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 4_4_4_4 unorm color target"); + for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 19 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } + changed = queue; + changed.context[0x31c] |= 0x10000000u; + changed.context[0x325] = 0x1234; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 4_4_4_4 unorm color targets"); + changed = queue; + changed.context[0x31c] |= 0x40000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "truncating (ROUND_MODE) 4_4_4_4 unorm color targets"); + + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto request = packedExportRequest(queue, state, code); + auto plain = request; + plain.context.pixel->targetExportPacking.fill(ColorExportPacking::None); + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 4_4_4_4 unorm packing was lost in serialization"); + const auto packed = ShaderRecompiler::Recompile(request); + const auto unpacked = ShaderRecompiler::Recompile(plain); + Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && !(packed.spirv == unpacked.spirv), "the 4_4_4_4 export packing did not specialize the module"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the 4_4_4_4 packed export did not reach attachment 0"); +} + void float11_11_10TargetTests() { using ShaderRecompiler::ColorExportPacking; auto queue = makeState(); @@ -8854,7 +8896,7 @@ void float11_11_10TargetTests() { const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the packed float export did not reach attachment 0"); auto invalid = request; - invalid.context.pixel->targetExportPacking[0] = static_cast(3); + invalid.context.pixel->targetExportPacking[0] = static_cast(4); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); } @@ -10383,6 +10425,7 @@ int main(int argc, char** argv) { ZExportTests(); unorm10_11_11TargetTests(); float11_11_10TargetTests(); + unorm4_4_4_4TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); earlyFragmentTestsTests(); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 76e0e11888..34d40e5e35 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -239,7 +239,7 @@ ConservativeZExport readConservativeZExport(Reader& reader) { ColorExportPacking readColorExportPacking(Reader& reader) { const auto value = reader.ReadU8(); - if (value > static_cast(ColorExportPacking::Float11_11_10)) { + if (value > static_cast(ColorExportPacking::Unorm4_4_4_4)) { throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); } return static_cast(value); diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index 00894c6af1..e3360cceaf 100644 --- a/core/shader/recompiler/FragmentOutputSpecialization.hpp +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -32,7 +32,7 @@ inline std::vector SpecializeFragmentOutputs(std::vector= PipelineSpecialization::ExportPackingBase + 8u) continue; - if (constant.value > static_cast(ColorExportPacking::Float11_11_10)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value > static_cast(ColorExportPacking::Unorm4_4_4_4)) throw std::runtime_error("invalid prepared fragment export packing"); if (constant.value != static_cast(ColorExportPacking::None)) packed.insert(constant.id - PipelineSpecialization::ExportPackingBase); } if (packed.empty() && !dualSource && reversed == 0u) return words; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9ca828bf3d..b167b6c7f1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -113,7 +113,8 @@ enum class ConservativeZExport : std::uint8_t { enum class ColorExportPacking : std::uint8_t { None, Unorm10_11_11, - Float11_11_10 + Float11_11_10, + Unorm4_4_4_4 }; enum class RasterPrimitive : std::uint8_t { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp index 76ecba7a65..c2ee940102 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp @@ -55,6 +55,7 @@ SpirvFormatComponentType GetFormatComponentType(IrBufferFormat format) { case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format8_8_8_8UNorm: case IrBufferFormat::Format16_16_16_16UNorm: + case IrBufferFormat::Format4_4_4_4UNorm: return SpirvFormatComponentType::Unorm; case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: @@ -110,6 +111,8 @@ SpirvBufferFormatInfo GetFormatInfo(IrBufferFormat format) { } const auto type = GetFormatComponentType(format); switch (format) { + case IrBufferFormat::Format4_4_4_4UNorm: + return {format, type, 4u, 2u, {4u, 4u, 4u, 4u}, {0u, 4u, 8u, 12u}, true}; case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format8UScaled: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index e5fbb7f67c..49cbac0bcf 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -400,7 +400,9 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target const auto packing = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + target, static_cast(ColorExportPacking::None)); const auto packed = EmitCompareU32Constant(state, spv::OpINotEqual, packing, static_cast(ColorExportPacking::None)); const auto floating = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Float11_11_10)); - const auto word = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); + const auto small = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Unorm4_4_4_4)); + const auto wide = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); + const auto word = Select(state, TypeU32(state), small, PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color), wide); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { const auto unpacked = state.module.AllocateId(); From 48a7efcbec9b6116acda84fcbe90405d8190003a Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:07:11 +0200 Subject: [PATCH 0123/1360] feat(agc): merge partial writes into 4_4_4_4 UNORM color targets A CB_TARGET_MASK that writes fewer than four components of a 4_4_4_4 target leaves the other nibbles of each packed word as they were, which an R16_UINT attachment cannot do. Such draws now close the attachment's color write mask, and the pixel shader merges the written nibbles into the target through a storage image: inside a pixel interlock it reads the word, keeps the bits the mask leaves alone and stores it back. The merge (export index, word mask, sample count) is part of the static shader key, request version 40 and shader disk cache format 42; the storage image is appended to the shader's images with a single runtime mode, and its T# is encoded from the target's surface at draw time, so the draw resolves it to the target's resident image, which Draw requires. Merging shaders declare early fragment tests; shaders that kill under late tests, export depth, stencil or coverage, use alpha-to-coverage or a quad sample mask are refused, as are multisampled, mipmapped, volume and array targets and more than one merged target per draw. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 4 + .../src/Driver/Draw/DrawRegisterKey.cpp | 4 +- .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/include/State.hpp | 3 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 + .../Graphics/src/GuestTextureResource.cpp | 34 ++ .../Graphics/src/PipelineJournal.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 40 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 77 ++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 46 ++- .../execution/PackedUnorm4444PartialWrite.cpp | 324 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 3 + .../ControlFlow/src/RequestSerializer.cpp | 16 +- .../IrMetadata/Resources.hpp | 1 + .../Optimization/ShaderStageInputInfo.hpp | 3 + .../Optimization/src/ResourceMaterializer.cpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 20 ++ core/shader/recompiler/Recompiler.cpp | 24 +- core/shader/recompiler/Recompiler.hpp | 10 + core/shader/recompiler/ShaderDiskCache.cpp | 4 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvMemory/SpirvDescriptors.hpp | 1 + .../src/SpirvMemory/SpirvDescriptors.cpp | 16 + .../src/SpirvMemory/SpirvInputOutput.cpp | 2 +- .../src/SpirvMemory/SpirvModuleSetup.cpp | 10 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 26 ++ .../src/ShaderInputInfoBuilder.cpp | 4 + .../tests/RequestSerializerTests.cpp | 20 +- 31 files changed, 670 insertions(+), 38 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 79e28aa9b6..e37e75bd3a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1956,6 +1956,8 @@ target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $< target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_partial_write_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) 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 f17c3ce9df..915861e8d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -165,6 +165,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; product->pixel.reversedBlend = product->state.reversedBlend; + product->pixel.targetMerge = Graphics::ExportTargetMerge(product->state); product->pixel.alphaToCoverage = product->state.alphaToCoverage; product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index b0ae591522..6cbf22ec75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -74,6 +74,10 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(value)); mix(pixel.dualSourceBlend); mix(pixel.reversedBlend); + mix(pixel.targetMerge.target); + mix(pixel.targetMerge.mask); + mix(pixel.targetMerge.samples); + for (const auto word : pixel.targetMerge.descriptor) mix(word); mix(pixel.alphaToCoverage); mix(static_cast(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 129e751733..c0e563f02e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -228,7 +228,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& s = a.state; const auto& t = b.state; const auto sameColor = [](const Graphics::ColorTarget& x, const Graphics::ColorTarget& y) { - return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex; + return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex && x.partialWriteMask == y.partialWriteMask; }; const auto sameBlend = [](const VkPipelineColorBlendAttachmentState& x, const VkPipelineColorBlendAttachmentState& y) { return x.blendEnable == y.blendEnable && x.srcColorBlendFactor == y.srcColorBlendFactor && x.dstColorBlendFactor == y.dstColorBlendFactor && x.colorBlendOp == y.colorBlendOp && x.srcAlphaBlendFactor == y.srcAlphaBlendFactor && x.dstAlphaBlendFactor == y.dstAlphaBlendFactor && x.alphaBlendOp == y.alphaBlendOp && x.colorWriteMask == y.colorWriteMask; @@ -262,7 +262,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.targetMerge != q.targetMerge || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 6f40129b56..5725bf61d3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -96,6 +96,7 @@ float EffectiveMinLod(const GuestTextureResource& resource); bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); +std::array EncodeTextureResource(const GuestTextureResource& resource); bool DescriptorSingleLevel(std::span words); bool FirstLayerDescriptor(std::span words, std::array& narrowed); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 22eb13371f..dbd451d6a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -68,6 +68,7 @@ struct ColorTarget { std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; std::uint32_t dccControl = 0; + std::uint16_t partialWriteMask = 0; }; struct DepthTarget { @@ -169,6 +170,8 @@ std::array PixelSampleLocations(const State& state, const Shad VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); +std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); bool UniformSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e0a69a53ac..3c3115e9b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2433,6 +2433,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& for (const auto& binding : targets) { if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); } + for (const auto& binding : targets) { + if (binding.color.partialWriteMask == 0) continue; + Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only"); + const auto images = resources->StorageImages(); + Require(std::ranges::any_of(images, [&](const std::pair& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image"); + } if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3c81c9c97e..1acb77e3d9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -242,6 +242,40 @@ GuestTextureResource DecodeTextureResource(std::span words) return result; } +std::array EncodeTextureResource(const GuestTextureResource& resource) { + Require(resource.dimension == TextureDimension::k2D && resource.samples == 1u, "only single-sample 2D surfaces have a texture descriptor encoding"); + Require(resource.mipCount == 1u && resource.allocatedMipCount <= 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.dccAddress == 0u && resource.minLod == 0u && !resource.iterate256 && resource.bcSwizzle == 0u, "only single-level, single-layer surfaces without metadata have a texture descriptor encoding"); + Require(resource.width >= 1u && resource.width <= 0x4000u && resource.height >= 1u && resource.height <= 0x4000u && resource.format < 0x200u, "the surface's extent or format has no texture descriptor encoding"); + const auto xorMask = resource.tileMode == TextureTileMode::kS4KBX || resource.tileMode == TextureTileMode::kD4KBX ? 0xfffull : 0xffffull; + Require(XorSwizzleMode(resource.tileMode) != 0 ? (resource.baseAddress & xorMask) == 0 && (resource.pipeBankXor & ~xorMask) == 0 : resource.pipeBankXor == 0, "the surface's pipe and bank XOR has no texture descriptor encoding"); + const auto base = (resource.baseAddress | resource.pipeBankXor) >> 8u; + Require(base != 0 && (resource.baseAddress & 0xffu) == 0 && base <= 0xffffffffffull, "the surface's address has no texture descriptor encoding"); + std::uint32_t tileMode = 0; + switch (resource.tileMode) { + case TextureTileMode::kLinear: tileMode = 0x00; break; + case TextureTileMode::kStandard256B: tileMode = 0x01; break; + case TextureTileMode::kD256B: tileMode = 0x02; break; + case TextureTileMode::kStandard4KB: tileMode = 0x05; break; + case TextureTileMode::kD4KB: tileMode = 0x06; break; + case TextureTileMode::kStandard64KB: tileMode = 0x09; break; + case TextureTileMode::kD64KB: tileMode = 0x0a; break; + case TextureTileMode::kS64KBT: tileMode = 0x11; break; + case TextureTileMode::kD64KBT: tileMode = 0x12; break; + case TextureTileMode::kS4KBX: tileMode = 0x15; break; + case TextureTileMode::kD4KBX: tileMode = 0x16; break; + case TextureTileMode::kZ64KBX: tileMode = 0x18; break; + case TextureTileMode::kS64KBX: tileMode = 0x19; break; + case TextureTileMode::kD64KBX: tileMode = 0x1a; break; + case TextureTileMode::kR64KBX: tileMode = 0x1b; break; + } + std::array words{}; + words[0] = static_cast(base); + words[1] = static_cast(base >> 32u) | (resource.format << 20u) | (((resource.width - 1u) & 3u) << 30u); + words[2] = ((resource.width - 1u) >> 2u) | ((resource.height - 1u) << 14u); + words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (tileMode << 20u) | (9u << 28u); + return words; +} + float EffectiveMinLod(const GuestTextureResource& resource) { if (resource.minLod <= resource.baseLevel * 256u) return 0.0f; return std::min(static_cast(resource.minLod) / 256.0f, static_cast(resource.lastLevel)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index c7a0f679a5..886a63f60b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 792, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 89a9db0042..c6368dc363 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1397,7 +1397,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.blends.assign(exportCount, VkPipelineColorBlendAttachmentState{}); std::array requiredBlendConstants{}; std::uint32_t requiredBlendMask = 0; - for (const auto& color : result.colors) { + for (auto& color : result.colors) { const auto slot = color.slot; const auto blend = read(cx, 0x1e0 + slot); APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); @@ -1465,8 +1465,16 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); - const std::uint32_t packedMask = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 ? 0xfu : 7u; - Require((exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); + const bool nibbles = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4; + const std::uint32_t packedMask = nibbles ? 0xfu : 7u; + Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); + if ((exportedMask & packedMask) != packedMask) { + Require(color.samples == 1u, "partial writes of a multisampled " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); + Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); + Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); + color.partialWriteMask = PackedWriteMask(mapping, exportedMask); + state.colorWriteMask = 0; + } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; @@ -1483,7 +1491,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t i = 0; i < 3; ++i) { if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i]; } - if (!result.colors.empty()) result.blend = result.blends[result.colors.front().exportIndex]; + if (!result.colors.empty()) { + result.color = result.colors.front(); + result.blend = result.blends[result.colors.front().exportIndex]; + } Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export"); if (result.dualSourceBlend) { Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone"); @@ -1510,6 +1521,27 @@ std::array ExportPackings(const State& return packings; } +std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask) { + std::uint16_t mask = 0; + for (std::uint32_t component = 0; component < 4; ++component) { + if (((exportedMask >> ((componentMapping >> (2u * component)) & 3u)) & 1u) != 0) mask = static_cast(mask | (0xfu << (4u * component))); + } + return mask; +} + +ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { + ShaderRecompiler::TargetMerge merge; + for (const auto& color : state.colors) { + if (color.partialWriteMask == 0) continue; + Require(merge.mask == 0 && color.exportIndex < 8u && color.samples == 1u, "a draw merges into one single-sample packed color target at most"); + merge.target = static_cast(color.exportIndex); + merge.mask = color.partialWriteMask; + merge.samples = static_cast(color.samples); + merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); + } + return merge; +} + bool PartialSampleMask(const State& state) { const auto full = (1u << state.samples) - 1u; return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 79c1f4a4ef..b40bbce37e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8803,9 +8803,82 @@ void unorm4_4_4_4TargetTests() { Require(state.color.format == VK_FORMAT_R16_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0x1bu && state.color.packing == ColorExportPacking::Unorm4_4_4_4, "a 4_4_4_4 unorm target did not decode to an R16_UINT attachment with reversed exports"); const auto packings = AgcDriver::Graphics::ExportPackings(state); Require(packings[0] == ColorExportPacking::Unorm4_4_4_4 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 4_4_4_4 unorm packing"); + Require(state.color.partialWriteMask == 0u && state.blends[0].colorWriteMask == 0xfu && AgcDriver::Graphics::ExportTargetMerge(state).mask == 0u, "a full write of a 4_4_4_4 unorm target merged into the target"); auto changed = queue; - changed.context[0x8e] = 7; - expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 4_4_4_4 unorm color target"); + for (const auto& [targetMask, wordMask] : {std::pair{1u, 0xf000u}, std::pair{7u, 0xfff0u}, std::pair{8u, 0x000fu}, std::pair{0xau, 0x0f0fu}}) { + changed.context[0x8e] = targetMask; + const auto partial = AgcDriver::Graphics::DecodeState(changed); + Require(partial.color.partialWriteMask == wordMask && partial.colors.front().partialWriteMask == wordMask && partial.blends[0].colorWriteMask == 0u && partial.blend.colorWriteMask == 0u, "CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target did not merge its nibbles behind a closed write mask"); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); + const auto expected = AgcDriver::Graphics::SurfaceForTarget(partial.color); + const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); + Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + } + { + changed.context[0x8e] = 1; + const auto partial = AgcDriver::Graphics::DecodeState(changed); + constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto merging = packedExportRequest(changed, partial, mergeCode); + merging.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(partial); + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + merging.target.supportedCapabilities = capabilities; + merging.target.registrationWordLimit = 1u << 20u; + const ShaderRecompiler::RequestSerializer mergeSerializer; + Require(mergeSerializer.Deserialize(mergeSerializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "the 4_4_4_4 target merge was lost in serialization"); + auto unmerged = merging; + unmerged.context.pixel->targetMerge = {}; + auto moved = merging; + moved.context.pixel->targetMerge.descriptor[0] += 0x100u; + std::vector mergedKey, unmergedKey, movedKey; + ShaderRecompiler::RecompileCacheKey::BuildInterface(merging, mergedKey); + ShaderRecompiler::RecompileCacheKey::BuildInterface(unmerged, unmergedKey); + ShaderRecompiler::RecompileCacheKey::BuildInterface(moved, movedKey); + Require(mergedKey != unmergedKey && mergedKey == movedKey, "the static shader key does not separate merging from plain writes, or depends on the merged target's address"); + for (const bool registration : {false, true}) { + merging.target.registrationModules = registration; + const auto merged = ShaderRecompiler::Recompile(merging); + const auto words = merged.spirv.Copy(); + std::size_t reads = 0, writes = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + reads += static_cast(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u; + writes += static_cast(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u; + } + const auto what = std::string(registration ? "a registration" : "a specialized") + " 4_4_4_4 merge module"; + Require(merged.registrationModule == registration && reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(merging), what + " did not read and write the target once inside a pixel interlock after early tests: " + interlockShape(words)); + const auto bound = std::ranges::any_of(merged.bindings, [&](const ShaderRecompiler::DescriptorBinding& binding) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) return false; + for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { + if (std::equal(merging.context.pixel->targetMerge.descriptor.begin(), merging.context.pixel->targetMerge.descriptor.end(), binding.guestDescriptor.begin() + static_cast(offset))) return true; + } + return false; + }); + Require(bound, what + " does not bind the merged target as a storage image"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &merged, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, partial, subgroup, false) == std::set{0u}, what + " lost its export to attachment 0"); + } + auto lacking = merging; + const std::array noInterlock{spv::CapabilityShader, spv::CapabilityStorageImageReadWithoutFormat}; + lacking.target.supportedCapabilities = noInterlock; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(lacking)); }, "fragmentShaderPixelInterlock"); + auto unformatted = merging; + const std::array noFormatlessReads{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT}; + unformatted.target.supportedCapabilities = noFormatlessReads; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(unformatted)); }, "storage image reads without a format"); + auto killing = merging; + killing.context.pixel->pixelKillEnable = true; + killing.context.pixel->earlyZ = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests"); + auto exporting = merging; + exporting.context.pixel->depthExportEnable = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); + } + changed = queue; + changed.context[0x8e] = 0xau; + changed.context[0x31d] = (changed.context.at(0x31d) & ~0x3f000u) | (1u << 12u) | (1u << 15u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a multisampled 4_4_4_4 unorm color target"); for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 09ada8fddd..bce4131fdc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4746,7 +4746,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend, value.targetMerge); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -4794,6 +4794,7 @@ void verifyPixelRequestSerialization() { pixel.stencilRefExportEnable = true; pixel.ditheredAlphaToCoverage = true; pixel.reversedBlend = 0x570001u; + pixel.targetMerge = {1u, 0x0ff0u, 1u, {0x100u, 0x2u, 0x3u, 0x4u, 0x5u, 0x6u, 0x7u, 0x8u}}; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -4826,6 +4827,14 @@ void verifyPixelRequestSerialization() { withoutReversedBlend.context.pixel->reversedBlend = 0u; RecompileCacheKey::Build(withoutReversedBlend, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(withoutReversedBlend), "the reversed-order blend code changed the static shader identity"); + auto movedMerge = request; + movedMerge.context.pixel->targetMerge.descriptor[0] = 0x200u; + RecompileCacheKey::Build(movedMerge, replayKey); + require(key == replayKey, "the merged target's descriptor changed the static shader identity"); + auto withoutMerge = request; + withoutMerge.context.pixel->targetMerge = {}; + RecompileCacheKey::Build(withoutMerge, replayKey); + require(key != replayKey, "a color target merge did not change the static shader identity"); auto withoutCoverage = request; withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); @@ -4848,7 +4857,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39"); + require(requestPrefix(encoded, 8u) == "NVNQQSgAAAA=", "new requests did not use serialization version 40"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4966,7 +4975,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSkAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5104,8 +5113,23 @@ void verifyVertexInfoVersion19() { while (allBitsOffset < plainPixel.size() && plainPixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); - constexpr std::size_t sampleMaskBytes = 16u; constexpr std::size_t iterationBytes = 35u; + constexpr std::size_t mergeBytes = 38u; + const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); + auto merging = pixel; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}}; + const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 38 bytes the version 40 pixel info adds after the pixel iteration fields"); + require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); + const auto withoutMerge = [&](std::vector bytes) { + bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); + return bytes; + }; + auto version39 = withoutMerge(plainPixel); + version39[4] = 39u; + const auto replay39 = serializer.Deserialize(encodeBase64(version39)); + require(replay39.request.context.pixel.has_value() && replay39.request.context.pixel->targetMerge == TargetMerge{} && serializer.Serialize(replay39.request) == encodedPixel, "a version 39 request read a color target merge or misread the fields after it"); + constexpr std::size_t sampleMaskBytes = 16u; auto maskedPixel = pixel; maskedPixel.context.pixel->quadPixelMask = 0x2u; const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); @@ -5116,7 +5140,7 @@ void verifyVertexInfoVersion19() { sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u}; const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); - auto version30 = sampledBytes; + auto version30 = withoutMerge(sampledBytes); version30.pop_back(); version30.pop_back(); version30.pop_back(); @@ -5126,7 +5150,7 @@ void verifyVertexInfoVersion19() { require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code"); auto spanned = sampledPixel; spanned.target.spannedWaves = true; - auto version32 = decodeBase64(serializer.Serialize(spanned)); + auto version32 = withoutMerge(decodeBase64(serializer.Serialize(spanned))); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5134,12 +5158,12 @@ void verifyVertexInfoVersion19() { version32[4] = 32u; const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); - auto version29 = sampledBytes; + auto version29 = withoutMerge(sampledBytes); version29.pop_back(); version29.pop_back(); version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); - auto version36 = sampledBytes; + auto version36 = withoutMerge(sampledBytes); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5147,7 +5171,7 @@ void verifyVertexInfoVersion19() { version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); - auto version28 = sampledBytes; + auto version28 = withoutMerge(sampledBytes); version28.pop_back(); version28.pop_back(); version28.pop_back(); @@ -5155,7 +5179,7 @@ void verifyVertexInfoVersion19() { version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); - auto version27 = maskedBytes; + auto version27 = withoutMerge(maskedBytes); version27.pop_back(); version27.pop_back(); version27.pop_back(); @@ -5163,7 +5187,7 @@ void verifyVertexInfoVersion19() { version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); - auto legacyPixel = plainPixel; + auto legacyPixel = withoutMerge(plainPixel); legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp new file mode 100644 index 0000000000..0a3426612b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -0,0 +1,324 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 64> Programs{}; +std::size_t programCount = 0; + +std::uint32_t Unorm(float value) { + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return 15; + return static_cast(std::nearbyint(static_cast(value) * 15.0)); +} + +std::uint32_t Packed(std::array rgba) { + return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgba) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false); +} + +std::span Float16Program(std::array rgba) { + return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.tileMode == ColorTileMode::RenderTarget && color.bytes == block.bytes, "the tiled 4_4_4_4 unorm target did not decode as a packed R16_UINT target"); + return color; +} + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const bool partial = exportedMask != 0xfu; + color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = partial ? 0u : VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const bool registered = pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId; + Require(registered != partial, partial ? "the merging draw reused the registered pixel shader" : "the full write did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + return color.partialWriteMask; +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const std::uint32_t expected = inside ? (before[index] & ~wordMask) | (word & wordMask) : before[index]; + if (stored[index] == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, inside ? "inside" : "outside", stored[index], expected, before[index]); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block, std::uint32_t seed) { + std::vector words(block.bytes / 2u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(((index + seed) * 0x9e3779b9u) >> 11u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::uint32_t mask, std::array rgba) { + char text[160]; + std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); + return text; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { + std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2); + Block block(layout.Bytes()); + const auto color = DecodeTarget(block); + Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); + struct Case { + std::uint32_t mask; + std::array rgba; + }; + const std::array cases{{ + {0x1u, {1.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, + {0x5u, {0.2f, 0.4f, 0.6f, 0.8f}}, + {0x2u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, + {0x8u, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, + {0x6u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0xeu, {0.0f, 1.0f, 0.5f, 0.25f}}, + {0xfu, {0.1f, 0.7f, 0.9f, 0.3f}}, + {0x1u, {0.0f, 0.0f, 0.0f, 0.0f}}, + }}; + auto before = Fill(block, 0); + for (const auto& [mask, rgba] : cases) { + const auto wordMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba)); + ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + before = ReadBack(*device, block); + } + before = Fill(block, 77); + const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; + const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); + ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 19711b8ddc..34f15975af 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -217,6 +217,9 @@ private: append(key, value.positionLocation); append(key, value.sampleLocations); append(key, value.targetOutputMode); + append(key, value.targetMerge.target); + append(key, value.targetMerge.mask); + append(key, value.targetMerge.samples); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 34d40e5e35..73fd3d1ce0 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -355,6 +355,10 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(info.positionLocation)); for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location); writer.WriteBool(info.stencilOperationValueExport); + writer.WriteU8(info.targetMerge.target); + writer.WriteU32(info.targetMerge.mask); + writer.WriteU8(info.targetMerge.samples); + for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -423,6 +427,14 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { for (std::uint8_t& location : info.sampleLocations) location = reader.ReadU8(); } if (version >= 37u) info.stencilOperationValueExport = reader.ReadBool(); + if (version >= 40u) { + info.targetMerge.target = reader.ReadU8(); + const auto mask = reader.ReadU32(); + if (mask > 0xffffu) throw std::runtime_error("invalid recompile request target merge mask"); + info.targetMerge.mask = static_cast(mask); + info.targetMerge.samples = reader.ReadU8(); + for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); + } 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) | @@ -827,7 +839,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(39u); + writer.WriteU32(40u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -866,7 +878,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 > 39u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 40u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 2ff9d9a15a..f4c2a4e3a3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -104,6 +104,7 @@ struct ImageResource { std::uint32_t emulatedCompare = 0; std::uint32_t table = NoTable; TableOperation tableOperation = TableOperation::Supported; + bool targetMerge = false; bool operator==(const ImageResource& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index f2a9ff9323..99f08344b9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -195,6 +195,9 @@ struct ShaderPixelInputInfo { bool psIteratesSamples = false; PixelPositionLocation psPositionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + std::uint8_t mergeTarget = 0; + std::uint16_t mergeMask = 0; + std::uint8_t mergeSamples = 1; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..a3837600de 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1831,6 +1831,7 @@ std::shared_ptr ResourceMaterializer::ResolveImageTab } std::vector ResourceMaterializer::RuntimeImageModes(const ImageResource& image) { + if (image.targetMerge) return {image}; const bool table = image.table != NoTable; if (table && image.tableOperation == TableOperation::Unsupported) return {}; std::vector modes; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index d94eaff86a..b56aea041c 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -1,4 +1,5 @@ #include "Optimization/ShaderInfoCollector.hpp" +#include "Optimization/ResourceMaterializer.hpp" #include #include #include @@ -519,6 +520,24 @@ std::uint16_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage s return static_cast(use); } +void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { + if (info.runtimeImageModes.size() != info.images.size() || std::ranges::any_of(info.images, [](const ImageResource& image) { return image.targetMerge; })) { + return Fail("color target merge image cannot join the prepared images"); + } + ImageResource image; + image.source = std::numeric_limits::max(); + image.resourceClass = ImageResourceClass::Storage; + image.numericClass = IrTextureNumericClass::Uint; + image.dimension = RdnaImageDimension::Dim2D; + image.read = true; + image.written = true; + image.fmaskCompatible = false; + image.targetMerge = true; + info.images.push_back(image); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); +} + void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { for (auto& sampler : info.samplers) { sampler.uses = 0; @@ -588,6 +607,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel->mergeMask != 0u) AddTargetMerge(*inputInfo.pixel, next); CollectSamplerUses(program, next); program.Info() = std::move(next); program.Metadata().shaderInfoComplete = true; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 3731da950d..d9dd6c8da8 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -959,7 +959,24 @@ std::shared_ptr specializeModule(const CompiledShaderAr return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { +const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults, ResourceSnapshot& merged) { + const auto& images = variant.info.info.images; + const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); + if (image == images.end() || registrationDefaults) return snapshot; + if (!request.context.pixel || request.context.pixel->targetMerge.mask == 0u) throw std::runtime_error("ShaderRecompiler: a color target merge variant was materialized without the merged target"); + if (image + 1 != images.end() || snapshot.images.size() + 1u != images.size()) throw std::runtime_error("ShaderRecompiler: the captured images do not precede the color target merge image"); + merged = snapshot; + DescriptorValue descriptor; + std::ranges::copy(request.context.pixel->targetMerge.descriptor, descriptor.dwords.begin()); + descriptor.dwordCount = 8u; + descriptor.availableDwordMask = 0xffu; + merged.images.push_back(descriptor); + return merged; +} + +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) { + ResourceSnapshot merged; + const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged); RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -1371,6 +1388,11 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(request.context.pixel->reversedBlend); mix(static_cast(request.context.pixel->rasterPrimitive)); mix(request.context.pixel->layerExported); + const auto& merge = request.context.pixel->targetMerge; + mix(merge.target); + mix(merge.mask); + mix(merge.samples); + for (const auto word : merge.descriptor) mix(word); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index b167b6c7f1..9db85f8311 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -151,6 +151,15 @@ inline constexpr std::uint32_t One = 3u; } +struct TargetMerge { + std::uint8_t target = 0; + std::uint16_t mask = 0; + std::uint8_t samples = 1; + std::array descriptor{}; + + bool operator==(const TargetMerge&) const = default; +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -191,6 +200,7 @@ struct ShaderPixelStageInfo { bool iteratesSamples = false; PixelPositionLocation positionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + TargetMerge targetMerge{}; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index b5bd3c4436..dbc6a40010 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -140,7 +140,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 40, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -751,6 +751,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.emulatedCompare); out.Value(image.table); out.Value(image.tableOperation); + out.Value(image.targetMerge); }); writer.Value(!info.runtimeImageModes.empty()); writer.List(info.samplers, [](Writer& out, const SamplerResource& sampler) { @@ -855,6 +856,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.emulatedCompare); in.Value(image.table); in.Value(image.tableOperation); + in.Value(image.targetMerge); }); bool preparedImageModes = false; reader.Value(preparedImageModes); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5543565717..5d721bb300 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 41; +inline constexpr std::uint32_t FormatVersion = 42; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 96951cf815..c210de9ddd 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -70,6 +70,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); std::uint32_t ExecutionModelForStage(IrShaderStage stage); std::uint32_t ConstantI32(SpirvEmitterState& state, std::int32_t value); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index b38633af2e..766da39c29 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -21,6 +21,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); [[noreturn]] void ExitDescriptorBindingFailure(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, const char* reason); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 946baa6068..c9a3717039 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -233,6 +233,22 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord) { + const auto& image = state.program.Info().images.at(resource); + if (image.resourceClass != ImageResourceClass::Storage || image.table != NoTable || image.mipMode == ImageMipMode::DynamicStorage || image.atomic) { + FailEmit("storage image read requested for an image that is not one plain storage descriptor"); + } + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityStorageImageReadWithoutFormat) == state.supportedCapabilities.end()) FailEmit("storage image reads without a format are unavailable on the target device"); + state.module.EmitCapability(spv::CapabilityStorageImageReadWithoutFormat); + const auto imageType = ImageType(state, image); + const auto descriptor = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, imageType, descriptor, StorageImageDescriptorPointer(state, resource)); + const auto texel = state.module.AllocateId(); + const auto texelType = ImageVectorType(state, image.numericClass, 4u); + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + return texel; +} + void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample) { const auto& image = state.runtimeImage != nullptr ? *state.runtimeImage : state.program.Info().images.at(resource); if (image.resourceClass != ImageResourceClass::Storage) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 4c05ffe094..1246e7cbe6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -150,7 +150,7 @@ namespace ShaderRecompiler } bool OrderedPixelShader(const SpirvEmitterState& state) { - return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psOrderedPixelShader; + return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && (state.inputInfo.pixel->psOrderedPixelShader || state.inputInfo.pixel->mergeMask != 0u); } const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9bbf4fb15b..697a656367 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -383,12 +383,16 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { + const bool merge = pixel.mergeMask != 0u; + if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ))) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); + } + if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } - if (pixel.psOrderedPixelShader) { + if (pixel.psOrderedPixelShader || merge) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) { - throw std::runtime_error("a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); + throw std::runtime_error(merge ? "a partial write of a packed color target needs the fragmentShaderPixelInterlock feature, which the device lacks" : "a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); } state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT); state.module.EmitExtension("SPV_EXT_fragment_shader_interlock"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 49cbac0bcf..f657f4122a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -414,6 +414,28 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target return result; } +void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { + const auto& pixel = PixelInfo(state); + const auto& images = state.program.Info().images; + const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); + if (image == images.end()) FailEmit("the color target merge image was not collected"); + if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared"); + const auto resource = static_cast(image - images.begin()); + const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; + const auto x = position(0u); + const auto y = position(1u); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); + const auto stored = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u); + const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); + const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); + const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); + EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); +} + bool AlphaToCoverage(const SpirvEmitterState& state) { return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage; } @@ -1847,6 +1869,10 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { value = mapped; if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); const auto mode = MrtOutputMode(state, exp); + if (PixelInfo(state).mergeMask != 0u && exp.index == PixelInfo(state).mergeTarget) { + if (uintOutput || mode == 5u || mode == 6u) FailEmit("a partial write of a packed color target cannot take an integer or 16-bit normalized export"); + MergeColorTarget(state, value); + } if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); } if (exp.kind == ExportTargetKind::Position) { diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 6c894d3e8c..98b1fb91ee 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -155,6 +155,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psIteratesSamples = pixel.iteratesSamples; pixelStorage.psPositionLocation = pixel.positionLocation; pixelStorage.sampleLocations = pixel.sampleLocations; + if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || pixel.targetMerge.samples != 1u)) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); + pixelStorage.mergeTarget = pixel.targetMerge.target; + pixelStorage.mergeMask = pixel.targetMerge.mask; + pixelStorage.mergeSamples = pixel.targetMerge.samples; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 138602235a..b3482d7888 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -54,7 +54,8 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 35u, 0u); + bytes.insert(bytes.begin() + 282u, 73u, 0u); + bytes[322] = 1u; } std::vector DecodeBase64(std::string_view text) { @@ -214,7 +215,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 39u; + upgraded[4] = 40u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -254,7 +255,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 39u; + layered[4] = 40u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -280,7 +281,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 39u; + bytes[4] = 40u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -410,7 +411,8 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - upgraded[4] = 39u; + if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 355u); + upgraded[4] = 40u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -429,7 +431,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 39u; + main25[4] = 40u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -451,9 +453,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 40u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 40u; + future[4] = 41u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -498,7 +500,7 @@ int main() { auto version35 = DecodeBase64(serializer.Serialize(request)); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); - version35.erase(stencilOperationValue); + version35.erase(stencilOperationValue, stencilOperationValue + 1 + 38); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); From b8a21d469c7fbfe137f7c51d159e659e23400b05 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:12:37 +0200 Subject: [PATCH 0124/1360] test(agc): draw merged 4_4_4_4 writes back to back and through the precheck Merged and full writes into one target are now also drawn without a wait between them and checked once at the end, so the render pass that follows a merging draw has to end on the storage write to its attachment. The unit test also passes the partial target masks through DrawRejection. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 1 + .../execution/PackedUnorm4444PartialWrite.cpp | 45 ++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b40bbce37e..59d70fd4a6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8807,6 +8807,7 @@ void unorm4_4_4_4TargetTests() { auto changed = queue; for (const auto& [targetMask, wordMask] : {std::pair{1u, 0xf000u}, std::pair{7u, 0xfff0u}, std::pair{8u, 0x000fu}, std::pair{0xau, 0x0f0fu}}) { changed.context[0x8e] = targetMask; + Require(AgcDriver::Graphics::DrawRejection(changed, false).empty(), "the draw precheck refused CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target"); const auto partial = AgcDriver::Graphics::DecodeState(changed); Require(partial.color.partialWriteMask == wordMask && partial.colors.front().partialWriteMask == wordMask && partial.blends[0].colorWriteMask == 0u && partial.blend.colorWriteMask == 0u, "CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target did not merge its nibbles behind a closed write mask"); const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 0a3426612b..73696717be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -270,6 +270,36 @@ std::vector Fill(Block& block, std::uint32_t seed) { return words; } +struct Case { + std::uint32_t mask; + std::array rgba; +}; + +void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, std::span draws, std::uint32_t seed, const std::string& what) { + const auto before = Fill(block, seed); + auto expected = before; + for (const auto& [mask, rgba] : draws) { + const auto drawn = Draw(device, color, mask, Float32Export, Float32Program(rgba)); + const auto wordMask = mask == 0xfu ? 0xffffu : drawn; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < ScissorWidth; ++x) { + auto& word = expected[layout.Offset(x, y) / 2u]; + word = static_cast((word & ~wordMask) | (Packed(rgba) & wordMask)); + } + } + } + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == expected[index]) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); + throw std::runtime_error(what + detail); + } + } +} + std::string Describe(std::uint32_t mask, std::array rgba) { char text[160]; std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); @@ -290,10 +320,6 @@ int main() { Block block(layout.Bytes()); const auto color = DecodeTarget(block); Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); - struct Case { - std::uint32_t mask; - std::array rgba; - }; const std::array cases{{ {0x1u, {1.0f, 0.0f, 0.0f, 0.0f}}, {0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, @@ -311,11 +337,20 @@ int main() { ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); before = ReadBack(*device, block); } + const std::array b{2.0f / 15.0f, 1.0f, 1.0f, 1.0f}; + const std::array c{0.6f, 0.2f, 0.8f, 0.4f}; + const std::array d{1.0f, 0.0f, 9.0f / 15.0f, 0.0f}; + const std::array mergeThenFull{{{0x1u, b}, {0xfu, c}}}; + const std::array fullThenMerge{{{0xfu, c}, {0x1u, b}}}; + const std::array mergeThenMerge{{{0x1u, b}, {0x4u, d}, {0x8u, c}}}; + BackToBack(*device, block, layout, color, mergeThenFull, 11, "a merged X write followed at once by a full write"); + BackToBack(*device, block, layout, color, fullThenMerge, 12, "a full write followed at once by a merged X write"); + BackToBack(*device, block, layout, color, mergeThenMerge, 13, "merged X, Z and W writes drawn back to back"); before = Fill(block, 77); const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws and 3 back-to-back sequences merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From eecf6fcff4e36f3ab87348ec574da3b95a68cc77 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:22:37 +0200 Subject: [PATCH 0125/1360] test(agc): render a 4_4_4_4 UNORM color target and read back the stored words Draws 32_ABGR and FP16_ABGR exports into a linear 4_4_4_4 target and checks every pixel against the guest layout (A in bits 3..0, B in 7..4, G in 11..8, R in 15..12), including NaN, negative, infinite, denormal and rounded values, and the pixels outside the scissor. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../execution/PackedUnorm4444ColorTarget.cpp | 330 ++++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 66a438c1fb..79e28aa9b6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1954,6 +1954,8 @@ target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobMana target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp new file mode 100644 index 0000000000..85326136cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp @@ -0,0 +1,330 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 32> Programs{}; +std::size_t programCount = 0; + +struct Golden { + std::array rgba; + std::uint16_t word; +}; + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgba) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false); +} + +std::span Float16Program(std::array rgba) { + return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.elementBytes == 2 && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.bytes == block.bytes, "the 4_4_4_4 unorm target did not decode as a packed R16_UINT target"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + auto unpacked = fragment; + unpacked.context.pixel->targetExportPacking.fill(ShaderRecompiler::ColorExportPacking::None); + const auto plain = materialize(unpacked); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId && plain->variantId == pixelResult->variantId, "the draw did not use the registered pixel shader"); + Require(plain->PipelineVariantId() != pixelResult->PipelineVariantId() && materialize(fragment)->PipelineVariantId() == pixelResult->PipelineVariantId(), "the 4_4_4_4 unorm packing did not key its own specialization"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const std::uint32_t expected = inside ? word : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 2u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast((index * 0x9e3779b9u) >> 11u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::array rgba) { + char text[112]; + std::snprintf(text, sizeof(text), "(%.9g, %.9g, %.9g, %.9g)", rgba[0], rgba[1], rgba[2], rgba[3]); + return text; +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 2); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + const float nan = std::numeric_limits::quiet_NaN(); + const float infinity = std::numeric_limits::infinity(); + const std::array float32Cases{{ + {{1.0f, 0.0f, 0.0f, 0.0f}, 0xf000u}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x0f00u}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x00f0u}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x000fu}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffffu}, + {{1.0f, 0.5f, 0.0f, 0.25f}, 0xf804u}, + {{0.0f, 1.0f, 0.5f, 0.25f}, 0x0f84u}, + {{nan, -1.0f, 2.0f, infinity}, 0x00ffu}, + {{1.0f / 15.0f, 2.0f / 15.0f, 7.0f / 15.0f, 14.0f / 15.0f}, 0x127eu}, + {{0.1f, 0.7f, 0.9f, 0.3f}, 0x2ad5u}, + {{-infinity, std::numeric_limits::min(), 0.999999940f, std::numeric_limits::denorm_min()}, 0x00f0u}, + }}; + for (const auto& golden : float32Cases) { + Draw(device, color, Float32Export, Float32Program(golden.rgba)); + ExpectSurface(device, block, layout, before, golden.word, std::string("a 32_ABGR export of ") + Describe(golden.rgba) + " into a " + kind + " 4_4_4_4 unorm target"); + before = ReadBack(device, block); + } + const std::array float16Cases{{ + {{1.0f, 0.0f, 0.0f, 0.0f}, 0xf000u}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x0f00u}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x00f0u}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x000fu}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffffu}, + {{1.0f, 0.5f, 0.0f, 0.25f}, 0xf804u}, + {{0.5f, 0.75f, 0.125f, 0.25f}, 0x8b24u}, + }}; + for (const auto& golden : float16Cases) { + Draw(device, color, Float16Export, Float16Program(golden.rgba)); + ExpectSurface(device, block, layout, before, golden.word, std::string("an FP16_ABGR export of ") + Describe(golden.rgba) + " into a " + kind + " 4_4_4_4 unorm target"); + before = ReadBack(device, block); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("4_4_4_4 unorm color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 27c00049911d9cb0b8b6f8de33790a4fc36faafb Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:45:21 +0200 Subject: [PATCH 0126/1360] fix(agc): merge kill shaders under late tests when the draw writes no depth or stencil A packed 4_4_4_4 partial write merges inside the pixel shader after early depth and stencil tests. Moving the tests ahead of a pixel kill only changes the result when the draw writes depth or stencil, so the target merge now carries whether the draw does (depth write, or stencil with a write mask and a non-keep operation), and the late-test kill refusal applies only then. The flag is part of the static shader key, the serialized request (now version 40) and the pixel input info. Co-Authored-By: Claude Sonnet 5.5 --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 7 +++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 49 ++++++++++++++++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 10 ++-- .../execution/PackedUnorm4444PartialWrite.cpp | 39 ++++++++++++--- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 2 + .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 4 +- .../src/ShaderInputInfoBuilder.cpp | 1 + .../tests/RequestSerializerTests.cpp | 6 +-- 13 files changed, 107 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 6cbf22ec75..84a83584ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -78,6 +78,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index dbd451d6a6..69802b363a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -171,6 +171,7 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +bool DepthStencilWrites(const State& state); ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c6368dc363..c83ef04670 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1529,6 +1529,12 @@ std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t expor return mask; } +bool DepthStencilWrites(const State& state) { + if (!state.depth) return false; + const auto writes = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) != 0 && (face.failOp != VK_STENCIL_OP_KEEP || face.passOp != VK_STENCIL_OP_KEEP || face.depthFailOp != VK_STENCIL_OP_KEEP); }; + return state.depthWrite || (state.stencilTest && (writes(state.stencilFront) || writes(state.stencilBack))); +} + ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { @@ -1538,6 +1544,7 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { merge.mask = color.partialWriteMask; merge.samples = static_cast(color.samples); merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); + merge.depthStencilWrites = DepthStencilWrites(state); } return merge; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 59d70fd4a6..c7bc673276 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8813,7 +8813,38 @@ void unorm4_4_4_4TargetTests() { const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); const auto expected = AgcDriver::Graphics::SurfaceForTarget(partial.color); const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); - Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && !merge.depthStencilWrites && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + } + { + auto depthQueue = queue; + depthQueue.context[0x8e] = 1; + depthQueue.context[0x000] = 0; + depthQueue.context[0x002] = 0; + depthQueue.context[0x007] = (1u << 16u) | 3u; + depthQueue.context[0x00a] = 7; + depthQueue.context[0x00b] = 0; + depthQueue.context[0x010] = 0x22900983; + depthQueue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) depthQueue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) depthQueue.context[offset] = 0x200; + depthQueue.context[0x005] = 0x300; + depthQueue.context[0x01e] = 0x1; + struct DepthStencilCase { + std::uint32_t control; + std::uint32_t ops; + std::uint32_t masks; + bool writes; + }; + for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}}) { + depthQueue.context[0x200] = control; + depthQueue.context[0x10b] = ops; + depthQueue.context[0x10c] = depthQueue.context[0x10d] = masks; + const auto what = "DB_DEPTH_CONTROL " + std::to_string(control) + " with stencil operations " + std::to_string(ops) + " and masks " + std::to_string(masks); + Require(AgcDriver::Graphics::DrawRejection(depthQueue, false).empty(), "the draw precheck refused a partial 4_4_4_4 write under " + what); + const auto partial = AgcDriver::Graphics::DecodeState(depthQueue); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); + Require(partial.depth.has_value() == ((control & 3u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes, "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); + } } { changed.context[0x8e] = 1; @@ -8871,7 +8902,21 @@ void unorm4_4_4_4TargetTests() { auto killing = merging; killing.context.pixel->pixelKillEnable = true; killing.context.pixel->earlyZ = false; - expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests"); + for (const bool registration : {false, true}) { + killing.target.registrationModules = registration; + const auto words = ShaderRecompiler::Recompile(killing).spirv.Copy(); + std::size_t reads = 0, writes = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + reads += static_cast(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u; + writes += static_cast(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u; + } + Require(reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(killing), "a killing 4_4_4_4 merge module under late tests of a draw without depth or stencil writes did not merge inside a pixel interlock after early tests"); + } + killing.context.pixel->targetMerge.depthStencilWrites = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests of a draw writing depth or stencil"); + auto earlyKilling = killing; + earlyKilling.context.pixel->earlyZ = true; + static_cast(ShaderRecompiler::Recompile(earlyKilling)); auto exporting = merging; exporting.context.pixel->depthExportEnable = true; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index bce4131fdc..99da8fb457 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4831,6 +4831,10 @@ void verifyPixelRequestSerialization() { movedMerge.context.pixel->targetMerge.descriptor[0] = 0x200u; RecompileCacheKey::Build(movedMerge, replayKey); require(key == replayKey, "the merged target's descriptor changed the static shader identity"); + auto depthWritingMerge = request; + depthWritingMerge.context.pixel->targetMerge.depthStencilWrites = true; + RecompileCacheKey::Build(depthWritingMerge, replayKey); + require(key != replayKey, "the merge draw's depth or stencil writes did not change the static shader identity"); auto withoutMerge = request; withoutMerge.context.pixel->targetMerge = {}; RecompileCacheKey::Build(withoutMerge, replayKey); @@ -5114,12 +5118,12 @@ void verifyVertexInfoVersion19() { require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); constexpr std::size_t iterationBytes = 35u; - constexpr std::size_t mergeBytes = 38u; + constexpr std::size_t mergeBytes = 39u; const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); auto merging = pixel; - merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}}; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true}; const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); - require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 38 bytes the version 40 pixel info adds after the pixel iteration fields"); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 39 bytes the version 40 pixel info adds after the pixel iteration fields"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); const auto withoutMerge = [&](std::vector bytes) { bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 73696717be..47df17bf42 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -47,6 +47,9 @@ constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Float16Export = 4; +constexpr std::uint32_t PositionInputs = 0x300u; +constexpr std::uint32_t LateKill = 0x40u; +constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -77,10 +80,13 @@ std::uint32_t Half(float value) { return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); } -std::span Program(const std::array& vgprs, bool compressed) { +std::span Program(const std::array& vgprs, bool compressed, bool killing = false) { Require(programCount < Programs.size(), "too many test programs"); auto& code = Programs[programCount++]; std::size_t at = 0; + if (killing) { + for (const auto word : CheckerboardKill) code[at++] = word; + } for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); code[at++] = vgprs[vgpr]; @@ -91,8 +97,8 @@ std::span Program(const std::array& vgprs return std::span(code.data(), at); } -std::span Float32Program(std::array rgba) { - return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false); +std::span Float32Program(std::array rgba, bool killing = false) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, killing); } std::span Float16Program(std::array rgba) { @@ -171,7 +177,7 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } -std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode) { +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, bool killing = false) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -210,7 +216,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa state.blendConstants = {}; const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, killing ? PositionInputs : 0u}, {0x1b4, killing ? PositionInputs : 0u}, {0x203, killing ? LateKill : 0u}, {0x1c5, exportFormat}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, @@ -225,6 +231,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + Require(fragment.context.pixel->pixelKillEnable == killing && !fragment.context.pixel->earlyZ && !fragment.context.pixel->targetMerge.depthStencilWrites, "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); const ShaderRecompiler::SrtRuntime runtime{}; const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); @@ -300,6 +307,21 @@ void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTarget } } +void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool kept = x < ScissorWidth && ((x ^ y) & 1u) == 0u; + const std::uint32_t expected = kept ? (before[index] & ~wordMask) | (word & wordMask) : before[index]; + if (stored[index] == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": %s pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", kept ? "a surviving" : x < ScissorWidth ? "a killed" : "an unscissored", x, y, stored[index], expected, before[index]); + throw std::runtime_error(what + detail); + } + } +} + std::string Describe(std::uint32_t mask, std::array rgba) { char text[160]; std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); @@ -350,7 +372,12 @@ int main() { const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws and 3 back-to-back sequences merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); + for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + before = Fill(block, 91u + mask); + const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, true), true); + ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, "a late-test checkerboard-killing " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + } + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 2 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 34f15975af..fea7c67b40 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -220,6 +220,7 @@ private: append(key, value.targetMerge.target); append(key, value.targetMerge.mask); append(key, value.targetMerge.samples); + append(key, value.targetMerge.depthStencilWrites); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 73fd3d1ce0..389ef2f0f3 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -359,6 +359,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU32(info.targetMerge.mask); writer.WriteU8(info.targetMerge.samples); for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); + writer.WriteBool(info.targetMerge.depthStencilWrites); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -434,6 +435,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.targetMerge.mask = static_cast(mask); info.targetMerge.samples = reader.ReadU8(); for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); + info.targetMerge.depthStencilWrites = reader.ReadBool(); } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 99f08344b9..4e271d4660 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -198,6 +198,7 @@ struct ShaderPixelInputInfo { std::uint8_t mergeTarget = 0; std::uint16_t mergeMask = 0; std::uint8_t mergeSamples = 1; + bool mergeDepthStencilWrites = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9db85f8311..7c56960968 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -156,6 +156,7 @@ struct TargetMerge { std::uint16_t mask = 0; std::uint8_t samples = 1; std::array descriptor{}; + bool depthStencilWrites = false; bool operator==(const TargetMerge&) const = default; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 697a656367..1110470d17 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -384,8 +384,8 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } const bool merge = pixel.mergeMask != 0u; - if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ))) { - throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); + if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 98b1fb91ee..6161de1552 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -159,6 +159,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.mergeTarget = pixel.targetMerge.target; pixelStorage.mergeMask = pixel.targetMerge.mask; pixelStorage.mergeSamples = pixel.targetMerge.samples; + pixelStorage.mergeDepthStencilWrites = pixel.targetMerge.depthStencilWrites; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index b3482d7888..00f6495be8 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -54,7 +54,7 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 73u, 0u); + bytes.insert(bytes.begin() + 282u, 74u, 0u); bytes[322] = 1u; } @@ -411,7 +411,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 355u); + if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 356u); upgraded[4] = 40u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -500,7 +500,7 @@ int main() { auto version35 = DecodeBase64(serializer.Serialize(request)); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); - version35.erase(stencilOperationValue, stencilOperationValue + 1 + 38); + version35.erase(stencilOperationValue, stencilOperationValue + 1 + 39); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); From acdbc02c8fcc3557bf7e694be343bd42e122cd9e Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:15:23 +0200 Subject: [PATCH 0127/1360] docs(agc): note that CB resolves of 4_4_4_4 color targets throw A CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e1dbad279e..ef2ea9da23 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,6 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. +- [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. From 8b37d049abb96a386f44c886043838e6fa97c0ab Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:53:16 +0200 Subject: [PATCH 0128/1360] fix(agc): write a merged 4_4_4_4 word only for pixels that survive the shader The merge used to write the target at the color export, so a pixel killed later by a valid-mask export still changed its word. The merged word is now kept until the shader returns and written inside the pixel interlock only when the pixel's valid mask is still set. The execution test kills a checkerboard both before and after the merging export. Co-Authored-By: Claude Sonnet 5.5 --- .../execution/PackedUnorm4444PartialWrite.cpp | 34 +++++++++---- .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 50 ++++++++++++++++--- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 47df17bf42..7ec9170878 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -51,6 +51,8 @@ constexpr std::uint32_t PositionInputs = 0x300u; constexpr std::uint32_t LateKill = 0x40u; constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; +enum class Kill { None, BeforeExport, AfterExport }; + alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; @@ -80,25 +82,33 @@ std::uint32_t Half(float value) { return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); } -std::span Program(const std::array& vgprs, bool compressed, bool killing = false) { +std::span Program(const std::array& vgprs, bool compressed, Kill kill = Kill::None) { Require(programCount < Programs.size(), "too many test programs"); auto& code = Programs[programCount++]; std::size_t at = 0; - if (killing) { + if (kill == Kill::BeforeExport) { for (const auto word : CheckerboardKill) code[at++] = word; } for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); code[at++] = vgprs[vgpr]; } - code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; - code[at++] = compressed ? 0x00000504u : 0x07060504u; + if (kill == Kill::AfterExport) { + code[at++] = compressed ? 0xf800040fu : 0xf800000fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + for (const auto word : CheckerboardKill) code[at++] = word; + code[at++] = 0xf8001890u; + code[at++] = 0u; + } else { + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + } code[at++] = 0xbf810000u; return std::span(code.data(), at); } -std::span Float32Program(std::array rgba, bool killing = false) { - return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, killing); +std::span Float32Program(std::array rgba, Kill kill = Kill::None) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, kill); } std::span Float16Program(std::array rgba) { @@ -372,12 +382,14 @@ int main() { const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); - for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { - before = Fill(block, 91u + mask); - const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, true), true); - ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, "a late-test checkerboard-killing " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + for (const auto kill : {Kill::BeforeExport, Kill::AfterExport}) { + for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + before = Fill(block, 91u + mask); + const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, kill), true); + ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, std::string(kill == Kill::BeforeExport ? "a late-test checkerboard kill before " : "a late-test checkerboard kill after ") + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + } } - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 2 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 4 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index c210de9ddd..4dde3cba3e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -101,6 +101,7 @@ DppTargetLane EmitDppMirrorTargetLane(SpirvEmitterState& state, std::uint32_t su DppTargetLane EmitDppTargetLane(SpirvEmitterState& state, std::uint32_t control); std::uint32_t InputVariableForKind(const SpirvEmitterState& state, StageInputKind kind); bool OrderedPixelShader(const SpirvEmitterState& state); +void FlushColorTargetMerge(SpirvEmitterState& state); const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location); std::uint32_t EmitVertexParameterComponentU32(SpirvEmitterState& state, const SpirvInputBinding& input, std::uint32_t component); std::uint32_t EmitInputComponentU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 3603bb59b8..953f8330b5 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -217,6 +217,8 @@ struct SpirvEmitterState { std::uint32_t currentLabel = 0; const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; + std::uint32_t mergeWordVariable = 0; + std::uint32_t mergePendingVariable = 0; std::uint32_t quadPixelUncovered = 0; std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index fe330d906a..59824a75cd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -90,6 +90,7 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { RecordBdaFaultWords(state, ConstantU32(state, static_cast(hash)), ConstantU32(state, static_cast(hash >> 32u)), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); }); } + FlushColorTargetMerge(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f657f4122a..c88b3f34ad 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -414,26 +414,35 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target return result; } -void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { - const auto& pixel = PixelInfo(state); +struct MergeTexel { + std::uint32_t resource; + std::uint32_t coord; +}; + +MergeTexel MergeTargetTexel(SpirvEmitterState& state) { const auto& images = state.program.Info().images; const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); if (image == images.end()) FailEmit("the color target merge image was not collected"); if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared"); - const auto resource = static_cast(image - images.begin()); + if (state.mergeWordVariable == 0u || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; const auto x = position(0u); const auto y = position(1u); const auto coord = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); + return {static_cast(image - images.begin()), coord}; +} + +void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { + const auto& pixel = PixelInfo(state); + const auto [resource, coord] = MergeTargetTexel(state); const auto stored = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u); const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); - const auto texel = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); - EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); + state.module.AddFunction(spv::OpStore, state.mergeWordVariable, merged); + state.module.AddFunction(spv::OpStore, state.mergePendingVariable, ConstantU32(state, 1u)); } bool AlphaToCoverage(const SpirvEmitterState& state) { @@ -739,6 +748,26 @@ std::uint32_t ConvertPositionToClipSpace(SpirvEmitterState& state, std::uint32_t } +void FlushColorTargetMerge(SpirvEmitterState& state) { + if (state.mergePendingVariable == 0u) return; + const auto pending = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), pending, state.mergePendingVariable); + auto write = Binary(state, spv::OpINotEqual, TypeBool(state), pending, ConstantU32(state, 0u)); + if (state.pixelValidMaskVariable != 0u) { + const auto valid = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), valid, state.pixelValidMaskVariable); + write = Binary(state, spv::OpLogicalAnd, TypeBool(state), write, Binary(state, spv::OpINotEqual, TypeBool(state), valid, ConstantU32(state, 0u))); + } + EmitIfCondition(state, write, [&] { + const auto [resource, coord] = MergeTargetTexel(state); + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), merged, state.mergeWordVariable); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); + EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); + }); +} + namespace { std::uint32_t MeshArray(SpirvEmitterState& state, std::uint32_t storage, std::uint32_t type, std::uint32_t count) { @@ -1460,6 +1489,10 @@ void EmitProgram(SpirvEmitterState& state) { state.pixelValidMaskVariable = state.module.AllocateId(); state.module.AddName(state.pixelValidMaskVariable, "pixel_valid_mask_active"); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->mergeMask != 0u) { + state.mergeWordVariable = state.module.AllocateId(); + state.mergePendingVariable = state.module.AllocateId(); + } for (const IrBlock* block : program.BlockOrder()) { const auto label = state.module.AllocateId(); state.labels.emplace(block, label); @@ -1508,6 +1541,10 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } + if (state.mergePendingVariable != 0u) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergeWordVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergePendingVariable, spv::StorageClassFunction, ConstantU32(state, 0u)); + } for (std::uint32_t half = 0; half < state.laneCount; half++) { auto& lane = half == 0u ? ctx : high; if (lane.scratchU32Variable != 0u) { @@ -1582,6 +1619,7 @@ void EmitProgram(SpirvEmitterState& state) { if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1)); } if (program.BlockOrder().empty()) { + FlushColorTargetMerge(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } From 8d6d86405399dfa46d2e800e299f2d3ee2438253 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:23:49 +0200 Subject: [PATCH 0129/1360] fix(agc): leave the merged 4_4_4_4 target out of the draw's attachments A merging draw read and wrote its packed target as a storage image while the same image stayed a color attachment of the pass, a feedback loop that NVIDIA only tolerates. The merged slot is now VK_ATTACHMENT_UNUSED in the subpass and VK_FORMAT_UNDEFINED in the rendering formats, its view is left out of the render pass and VkFramebuffer, and dynamic rendering binds no view there. The target stays in the draw's tracked and dirty images. Pipelines and continued passes are keyed on the merged slot. Co-Authored-By: Claude Sonnet 5.5 --- .../Graphics/include/Pipeline.hpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 3 +++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 27 ++++++++++++++----- .../Graphics/src/PipelineJournal.cpp | 5 +++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++ 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 61679c4f59..6c3a04813b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -83,6 +83,8 @@ private: VkPipeline secondChannel = VK_NULL_HANDLE; std::size_t attachments = 0; std::size_t colorAttachments = 0; + std::vector boundAttachments; + std::uint32_t boundColorAttachments = 0; bool depthBounds = false; bool depthBias = false; bool libraries = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 3c3115e9b5..52ef639841 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -50,6 +50,9 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.blends.size()); for (const auto& color : state.colors) mix(color.exportIndex); } + for (const auto& color : state.colors) { + if (color.partialWriteMask != 0u) mix(0x6d65726765000000ull | color.exportIndex); + } mix(state.renderExtent.width); mix(state.renderExtent.height); mix(state.samples); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index eaca4be3f4..f7bef401dc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -278,6 +278,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& for (const auto& color : state.colors) { appendKey(keys.renderPass, color.format); appendKey(keys.renderPass, color.exportIndex); + appendKey(keys.renderPass, color.partialWriteMask != 0u); } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); @@ -314,6 +315,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); + for (const auto& color : state.colors) boundAttachments.push_back(color.partialWriteMask == 0u); + if (state.depth) boundAttachments.push_back(true); sampleLocationsEnabled = ValidateSampleLocations(context, state); sampleLocationGrid = hostSampleLocationGrid(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep @@ -321,7 +324,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput this->context.bufferPool.reset(); Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state"); Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits"); - Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + const bool boundColor = std::ranges::any_of(state.colors, [](const ColorTarget& color) { return color.partialWriteMask == 0u; }); + Require((state.hasColorTarget && (boundColor || state.colors.empty() || state.depth.has_value())) || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast(state.samples)) != 0, "device does not support depth targets with the draw's sample count"); Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test"); Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping"); @@ -393,6 +397,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput std::vector colors; std::vector references(state.blends.size(), VkAttachmentReference{VK_ATTACHMENT_UNUSED, attachmentLayout}); for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].partialWriteMask != 0u) continue; VkAttachmentDescription color{}; color.format = state.colors[index].format; color.samples = state.mergedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); @@ -402,9 +407,10 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; color.initialLayout = attachmentLayout; color.finalLayout = attachmentLayout; + references.at(state.colors[index].exportIndex) = {static_cast(colors.size()), attachmentLayout}; colors.push_back(color); - references.at(state.colors[index].exportIndex) = {index, attachmentLayout}; } + boundColorAttachments = static_cast(colors.size()); VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); @@ -424,7 +430,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pDepthStencilAttachment = &depthReference; } std::vector renderingColors(state.blends.size(), VK_FORMAT_UNDEFINED); - for (const auto& color : state.colors) renderingColors.at(color.exportIndex) = color.format; + for (const auto& color : state.colors) { + if (color.partialWriteMask == 0u) renderingColors.at(color.exportIndex) = color.format; + } VkPipelineRenderingCreateInfoKHR rendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; rendering.colorAttachmentCount = static_cast(renderingColors.size()); rendering.pColorAttachmentFormats = renderingColors.empty() ? nullptr : renderingColors.data(); @@ -440,7 +448,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (dynamicRendering) { depthFormat = state.depth ? HostDepthFormat(context, state.depth->format) : VK_FORMAT_UNDEFINED; colorSlots.assign(state.blends.size(), ~0u); - for (std::uint32_t index = 0; index < state.colors.size(); ++index) colorSlots.at(state.colors[index].exportIndex) = index; + for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].partialWriteMask == 0u) colorSlots.at(state.colors[index].exportIndex) = index; + } this->attachmentLayout = attachmentLayout; } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; @@ -684,7 +694,11 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span(context, renderPass, targets, extent, layers); + std::vector boundViews; + for (std::size_t i = 0; i < targets.size(); ++i) { + if (boundAttachments[i]) boundViews.push_back(targets[i]); + } + auto framebuffer = std::make_shared(context, renderPass, renderPass == VK_NULL_HANDLE ? targets : std::span(boundViews), extent, layers); if (!resident) return framebuffer; // Beyond the bound the least recently used unreferenced entry goes. constexpr std::size_t bound = 8; @@ -742,7 +756,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V const auto locations = SampleLocations(state); const auto grid = sampleLocationGrid; const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), grid, state.samples * grid.width * grid.height, locations.data()}; - const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkAttachmentSampleLocationsEXT initial{boundColorAttachments, info}; const VkSubpassSampleLocationsEXT post{0, info}; VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; @@ -891,6 +905,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& color : state.colors) { append(key, color.format); append(key, color.componentMapping); + append(key, color.partialWriteMask != 0u); } if (state.blends.size() != state.colors.size()) { for (const auto& color : state.colors) append(key, color.exportIndex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 886a63f60b..f78f97d4ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 4u; +constexpr std::uint32_t FileFormat = 5u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -195,6 +195,7 @@ void encodeState(Writer& writer, const State& state) { writer.U32(static_cast(color.format)); writer.U32(color.exportIndex); writer.Raw(color.componentMapping); + writer.Raw(color.partialWriteMask); } writer.Raws(std::span(state.blends)); for (const auto value : state.blendConstants) writer.Raw(value); @@ -254,6 +255,7 @@ void decodeState(Reader& reader, State& state) { color.format = static_cast(reader.U32()); color.exportIndex = reader.U32(); color.componentMapping = reader.Raw(); + color.partialWriteMask = reader.Raw(); state.colors.push_back(color); } reader.Raws(state.blends, 8u); @@ -1026,6 +1028,7 @@ State CanonicalPipelineState(const State& state) { canonicalColor.format = color.format; canonicalColor.exportIndex = color.exportIndex; canonicalColor.componentMapping = color.componentMapping; + canonicalColor.partialWriteMask = color.partialWriteMask; canonical.colors.push_back(canonicalColor); } canonical.blends = state.blends; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c7bc673276..be91393e26 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -10140,6 +10140,15 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { BuildPipelineStoreKey(mergedKey, context, mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); BuildPipelineStoreKey(mergedReplayed, context, mergedDecoded.state, mergedDecoded.vertexInput, layouts.View(), shaders, mergedDecoded.attachmentLayout); Require(mergedKey != fourSamples && mergedKey == mergedReplayed, "the decoded merged coverage state keys another pipeline"); + auto partialState = state; + partialState.colors[0].partialWriteMask = 0xf000u; + GraphicsPipelineRecipe partialDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(partialState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), partialDecoded), "the partial write recipe does not decode"); + Require(partialDecoded.state.colors.size() == partialState.colors.size() && partialDecoded.state.colors[0].partialWriteMask == 0xf000u, "the recipe dropped the merged target's partial write mask"); + std::vector partialKey, partialReplayed; + BuildPipelineStoreKey(partialKey, context, partialState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(partialReplayed, context, partialDecoded.state, partialDecoded.vertexInput, layouts.View(), shaders, partialDecoded.attachmentLayout); + Require(partialKey != original && partialKey == partialReplayed, "the decoded partial write state keys another pipeline"); auto movedState = state; movedState.viewport.x = 5; movedState.colors[0].address = 0x9000; From f3ea4b24075be447ad6d70e8fe0f3f3dd63b9702 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:52:16 +0200 Subject: [PATCH 0130/1360] test(agc): check 4_4_4_4 merges against fixed masks, same-draw overdraw and depth The partial write test now names the word mask of every CB_TARGET_MASK case instead of taking it from PackedWriteMask, draws 16 overlapping layers in one draw whose merged nibble must come from the last layer, and merges behind and in front of a depth target filled by a checkerboard-killing draw: a depth-rejected fragment leaves its word. Co-Authored-By: Claude Sonnet 5.5 --- .../execution/PackedUnorm4444PartialWrite.cpp | 179 ++++++++++++++---- 1 file changed, 144 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 7ec9170878..394411849b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Float16Export = 4; constexpr std::uint32_t PositionInputs = 0x300u; +constexpr std::uint32_t DepthInput = 0x400u; constexpr std::uint32_t LateKill = 0x40u; constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; @@ -115,6 +117,34 @@ std::span Float16Program(std::array rgba) { return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); } +std::span DepthAsXProgram(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x7e080300u; + for (std::uint32_t vgpr = 1; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +const std::array, 3>& TriangleAt(float z) { + static std::map, 3>> triangles; + return triangles.try_emplace(z, std::array, 3>{{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}).first->second; +} + +struct DrawSetup { + std::uint32_t inputs = 0; + std::uint32_t shaderControl = 0; + std::span> vertices = Triangle; + std::uint64_t depth = 0; + bool depthWrite = false; +}; + struct Block { explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { #ifdef _WIN32 @@ -187,12 +217,12 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } -std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, bool killing = false) { +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); - const auto address = reinterpret_cast(Triangle.data()); - const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + const auto address = reinterpret_cast(setup.vertices.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(setup.vertices.size()), 0x01016facu}; std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ @@ -224,9 +254,15 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa state.blends = {blend}; state.blend = blend; state.blendConstants = {}; + if (setup.depth != 0) { + state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = setup.depthWrite; + state.depthCompare = VK_COMPARE_OP_LESS; + } const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, killing ? PositionInputs : 0u}, {0x1b4, killing ? PositionInputs : 0u}, {0x203, killing ? LateKill : 0u}, {0x1c5, exportFormat}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, setup.inputs}, {0x1b4, setup.inputs}, {0x203, setup.shaderControl}, {0x1c5, exportFormat}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, @@ -241,7 +277,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); - Require(fragment.context.pixel->pixelKillEnable == killing && !fragment.context.pixel->earlyZ && !fragment.context.pixel->targetMerge.depthStencilWrites, "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); + Require(fragment.context.pixel->pixelKillEnable == ((setup.shaderControl & LateKill) != 0u) && !fragment.context.pixel->earlyZ && fragment.context.pixel->targetMerge.depthStencilWrites == (partial && setup.depth != 0 && setup.depthWrite), "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); const ShaderRecompiler::SrtRuntime runtime{}; const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); @@ -252,7 +288,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} }}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(setup.vertices.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); return color.partialWriteMask; } @@ -289,15 +325,36 @@ std::vector Fill(Block& block, std::uint32_t seed) { struct Case { std::uint32_t mask; + std::uint32_t wordMask; std::array rgba; }; +std::uint32_t DrawCase(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t mask, std::uint32_t wordMask, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { + const auto drawn = Draw(device, color, mask, exportFormat, pixelCode, setup); + char detail[160]; + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x merged the word mask 0x%04x, not 0x%04x", mask, drawn, mask == 0xfu ? 0u : wordMask); + Require(drawn == (mask == 0xfu ? 0u : wordMask), detail); + return wordMask; +} + +void ExpectWords(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& expected, const std::vector& before, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == expected[index]) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); + throw std::runtime_error(what + detail); + } + } +} + void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, std::span draws, std::uint32_t seed, const std::string& what) { const auto before = Fill(block, seed); auto expected = before; - for (const auto& [mask, rgba] : draws) { - const auto drawn = Draw(device, color, mask, Float32Export, Float32Program(rgba)); - const auto wordMask = mask == 0xfu ? 0xffffu : drawn; + for (const auto& [mask, wordMask, rgba] : draws) { + DrawCase(device, color, mask, wordMask, Float32Export, Float32Program(rgba)); for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < ScissorWidth; ++x) { auto& word = expected[layout.Offset(x, y) / 2u]; @@ -305,16 +362,63 @@ void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTarget } } } - const auto stored = ReadBack(device, block); + ExpectWords(device, block, layout, expected, before, what); +} + +void SameDrawOverdraw(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, float first, float last, std::uint32_t seed) { + constexpr std::uint32_t Layers = 16; + static std::map>> stacks; + auto& vertices = stacks[last]; + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const auto& triangle = TriangleAt(layer % 2u == Layers % 2u ? first : last); + vertices.insert(vertices.end(), triangle.begin(), triangle.end()); + } + const auto before = Fill(block, seed); + auto expected = before; + const std::uint32_t nibble = static_cast(std::nearbyint(static_cast(last) * 15.0)); for (std::uint32_t y = 0; y < Height; ++y) { - for (std::uint32_t x = 0; x < Width; ++x) { - const auto index = layout.Offset(x, y) / 2u; - if (stored[index] == expected[index]) continue; - char detail[160]; - std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); - throw std::runtime_error(what + detail); + for (std::uint32_t x = 0; x < ScissorWidth; ++x) { + auto& word = expected[layout.Offset(x, y) / 2u]; + word = static_cast((word & 0x0fffu) | (nibble << 12u)); } } + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, DepthAsXProgram({0.0f, 0.4f, 0.6f, 1.0f}), {DepthInput, 0u, vertices}); + char what[160]; + std::snprintf(what, sizeof(what), "one draw of %u full-screen triangles alternating z %.2f and %.2f, merging z into X", Layers, first, last); + ExpectWords(device, block, layout, expected, before, what); +} + +void DepthRejection(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color) { + Block depth(static_cast(Width) * Height * 4u); + const std::vector cleared(depth.bytes / 4u, 1.0f); + std::memcpy(depth.data, cleared.data(), depth.bytes); + const std::array nearer{0.6f, 0.2f, 0.8f, 0.4f}; + const std::array behind{0.0f, 1.0f, 1.0f, 1.0f}; + const std::array front{1.0f, 0.0f, 0.0f, 1.0f / 15.0f}; + auto before = Fill(block, 301u); + auto expected = before; + const auto inside = [](std::uint32_t x) { return x < ScissorWidth; }; + const auto survivor = [&](std::uint32_t x, std::uint32_t y) { return inside(x) && ((x ^ y) & 1u) == 0u; }; + const auto expect = [&](const auto& select, std::uint32_t word, std::uint32_t wordMask) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (!select(x, y)) continue; + auto& stored = expected[layout.Offset(x, y) / 2u]; + stored = static_cast((stored & ~wordMask) | (word & wordMask)); + } + } + }; + DrawCase(device, color, 0xfu, 0xffffu, Float32Export, Float32Program(nearer, Kill::BeforeExport), {PositionInputs, LateKill, TriangleAt(0.5f), depth.Address(), true}); + expect(survivor, Packed(nearer), 0xffffu); + ExpectWords(device, block, layout, expected, before, "a full write at z 0.5 that kills a checkerboard and writes depth"); + before = expected; + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(behind), {0u, 0u, TriangleAt(0.75f), depth.Address(), false}); + expect([&](std::uint32_t x, std::uint32_t y) { return inside(x) && !survivor(x, y); }, Packed(behind), 0xf000u); + ExpectWords(device, block, layout, expected, before, "an X merge at z 0.75 behind the depth 0.5 the surviving pixels hold"); + before = expected; + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(front), {0u, 0u, TriangleAt(0.25f), depth.Address(), false}); + expect([&](std::uint32_t x, std::uint32_t) { return inside(x); }, Packed(front), 0xf000u); + ExpectWords(device, block, layout, expected, before, "an X merge at z 0.25 in front of every stored depth"); } void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { @@ -353,43 +457,48 @@ int main() { const auto color = DecodeTarget(block); Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); const std::array cases{{ - {0x1u, {1.0f, 0.0f, 0.0f, 0.0f}}, - {0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, - {0x5u, {0.2f, 0.4f, 0.6f, 0.8f}}, - {0x2u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, - {0x8u, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, - {0x6u, {0.0f, 0.0f, 0.0f, 0.0f}}, - {0xeu, {0.0f, 1.0f, 0.5f, 0.25f}}, - {0xfu, {0.1f, 0.7f, 0.9f, 0.3f}}, - {0x1u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, 0xf000u, {1.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, 0xf000u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, + {0x5u, 0xf0f0u, {0.2f, 0.4f, 0.6f, 0.8f}}, + {0x2u, 0x0f00u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, + {0x8u, 0x000fu, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, + {0x6u, 0x0ff0u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0xeu, 0x0fffu, {0.0f, 1.0f, 0.5f, 0.25f}}, + {0xfu, 0xffffu, {0.1f, 0.7f, 0.9f, 0.3f}}, + {0x1u, 0xf000u, {0.0f, 0.0f, 0.0f, 0.0f}}, }}; + const std::array goldens{0xf000u, 0x7fffu, 0x369cu, 0x02ffu, 0xfffbu, 0x0000u, 0x0f84u, 0x2ad5u, 0x0000u}; auto before = Fill(block, 0); - for (const auto& [mask, rgba] : cases) { - const auto wordMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba)); - ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& [mask, wordMask, rgba] = cases[index]; + DrawCase(*device, color, mask, wordMask, Float32Export, Float32Program(rgba)); + ExpectSurface(*device, block, layout, before, goldens[index], wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); before = ReadBack(*device, block); } const std::array b{2.0f / 15.0f, 1.0f, 1.0f, 1.0f}; const std::array c{0.6f, 0.2f, 0.8f, 0.4f}; const std::array d{1.0f, 0.0f, 9.0f / 15.0f, 0.0f}; - const std::array mergeThenFull{{{0x1u, b}, {0xfu, c}}}; - const std::array fullThenMerge{{{0xfu, c}, {0x1u, b}}}; - const std::array mergeThenMerge{{{0x1u, b}, {0x4u, d}, {0x8u, c}}}; + const std::array mergeThenFull{{{0x1u, 0xf000u, b}, {0xfu, 0xffffu, c}}}; + const std::array fullThenMerge{{{0xfu, 0xffffu, c}, {0x1u, 0xf000u, b}}}; + const std::array mergeThenMerge{{{0x1u, 0xf000u, b}, {0x4u, 0x00f0u, d}, {0x8u, 0x000fu, c}}}; BackToBack(*device, block, layout, color, mergeThenFull, 11, "a merged X write followed at once by a full write"); BackToBack(*device, block, layout, color, fullThenMerge, 12, "a full write followed at once by a merged X write"); BackToBack(*device, block, layout, color, mergeThenMerge, 13, "merged X, Z and W writes drawn back to back"); before = Fill(block, 77); const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; - const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); + const auto wordMask = DrawCase(*device, color, 0x9u, 0xf00fu, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); for (const auto kill : {Kill::BeforeExport, Kill::AfterExport}) { - for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + for (const auto& [mask, wordMask, rgba] : {Case{0x1u, 0xf000u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, 0x0ff0u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { before = Fill(block, 91u + mask); - const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, kill), true); + const auto killedMask = DrawCase(*device, color, mask, wordMask, Float32Export, Float32Program(rgba, kill), {PositionInputs, LateKill}); ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, std::string(kill == Kill::BeforeExport ? "a late-test checkerboard kill before " : "a late-test checkerboard kill after ") + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); } } - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 4 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); + SameDrawOverdraw(*device, block, layout, color, 0.2f, 0.8f, 401u); + SameDrawOverdraw(*device, block, layout, color, 0.8f, 0.2f, 402u); + DepthRejection(*device, block, layout, color); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers and a depth-rejected merge into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From f4adaff1fecd99b7b916a212a6cf89c649142111 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:52:29 +0200 Subject: [PATCH 0131/1360] docs(agc): list the limits of merged single-sample 4_4_4_4 partial writes Occlusion counts are taken at the forced early tests, the refused merge draws and why a late depth write was not used, and the one GPU the merge was measured on. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + docs/dev/debt/shader.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ef2ea9da23..b5ee7296d8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -161,6 +161,7 @@ - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp) (libSceAgcDriver) - the single-sample merge is measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 4353f3bd8e..7d891da817 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,6 +22,8 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) +- [4_4_4_4 partial writes, occlusion queries](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merging pixel shader always declares `EarlyFragmentTests`, so by the Vulkan rules sample counting happens before the shader runs and an occlusion query counts pixels a kill shader then discards. Not measured. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. ### Functional From 7d4776a20b5cf1e35cb172c470d94dcbda0bca7c Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:51:33 +0200 Subject: [PATCH 0132/1360] fix(agc): read and write the merged 4_4_4_4 target through a Coherent image OpBeginInvocationInterlockEXT and OpEndInvocationInterlockEXT order the critical sections of overlapping fragment invocations but perform no availability or visibility operations (Vulkan memory model, fragment shader interlock), so without Coherent a merge could read a word that an earlier overlapping invocation stored but did not make visible. The image variable the merged target is bound through is now decorated Coherent. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 19 +++++++++++++++++++ .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 1 + 2 files changed, 20 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index be91393e26..b33e96effa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7776,6 +7776,24 @@ std::string interlockShape(std::span words) { return shape; } +bool coherentImageRead(std::span words) { + std::map sources; + std::set coherent; + std::uint32_t image = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationCoherent) coherent.insert(words[at + 1]); + if (op == spv::OpLoad || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) sources[words[at + 2]] = words[at + 3]; + if (op == spv::OpImageRead) image = words[at + 3]; + } + for (std::size_t step = 0; image != 0 && step < 8u; ++step) { + if (coherent.contains(image)) return true; + const auto source = sources.find(image); + image = source == sources.end() ? 0u : source->second; + } + return false; +} + std::string interlockExits(std::span words, std::size_t& kills, std::size_t& terminates) { struct Block { std::vector ops; @@ -8877,6 +8895,7 @@ void unorm4_4_4_4TargetTests() { } const auto what = std::string(registration ? "a registration" : "a specialized") + " 4_4_4_4 merge module"; Require(merged.registrationModule == registration && reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(merging), what + " did not read and write the target once inside a pixel interlock after early tests: " + interlockShape(words)); + Require(coherentImageRead(words), what + " does not read the target through a Coherent image variable"); const auto bound = std::ranges::any_of(merged.bindings, [&](const ShaderRecompiler::DescriptorBinding& binding) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) return false; for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index a8068f0415..2fa410afac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -547,6 +547,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto& image = *selected; const auto name = "image_" + std::to_string(static_cast(binding.kind)); state.imageVariables.at(ImageBindingIndex(binding.kind)) = Define(ArrayType(ImageType(state, image)), name.c_str(), spv::StorageClassUniformConstant); + if (std::ranges::any_of(binding.resources, [&](std::uint32_t resource) { return state.program.Info().images.at(resource).targetMerge; })) state.module.AddAnnotation(spv::OpDecorate, state.imageVariables.at(ImageBindingIndex(binding.kind)), spv::DecorationCoherent); if (image.dimension == RdnaImageDimension::Dim1D || image.dimension == RdnaImageDimension::Dim1DArray) { state.module.EmitCapability(image.resourceClass == ImageResourceClass::Sampled ? spv::CapabilitySampled1D : spv::CapabilityImage1D); } From 74376b9172c92f0e6332053ad77637456f9d3004 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:52:05 +0200 Subject: [PATCH 0133/1360] fix(agc): refuse partial 4_4_4_4 writes under merged coverage A one-sample target under a multisampled rasterizer whose OVERRASTERIZATION_AMOUNT matches (radeonsi line and polygon smoothing) decodes with mergedCoverage and is drawn through mixed-samples coverage reduction onto its one-sample attachments. A merging draw leaves the 4_4_4_4 target out of the attachments, a combination that is not tested, so a partial write of such a draw now throws; the limit is listed in the technical debt file. Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 15 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c83ef04670..42b0f2b502 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1491,6 +1491,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t i = 0; i < 3; ++i) { if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i]; } + Require(!result.mergedCoverage || std::ranges::none_of(result.colors, [](const ColorTarget& color) { return color.partialWriteMask != 0; }), "partial writes of a 4_4_4_4 unorm color target under merged coverage (a one-sample target under a multisampled rasterizer) are not implemented"); if (!result.colors.empty()) { result.color = result.colors.front(); result.blend = result.blends[result.colors.front().exportIndex]; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b33e96effa..f02f17e037 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8864,6 +8864,19 @@ void unorm4_4_4_4TargetTests() { Require(partial.depth.has_value() == ((control & 3u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes, "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); } } + { + auto smoothed = queue; + smoothed.context[0x2f8] = 0x0020c002u; + smoothed.context[0x292] = 3; + smoothed.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) smoothed.context[0x2fe + pixel * 4u] = 0xcc44; + smoothed.context[0x31d] = smoothed.context.at(0x31d) & ~0x3f000u; + smoothed.context[0x201] = 0x02130000u; + const auto merged = AgcDriver::Graphics::DecodeState(smoothed); + Require(merged.samples == 4 && merged.mergedCoverage && merged.color.samples == 1 && merged.color.partialWriteMask == 0u, "a full write of a one-sample 4_4_4_4 unorm target under OVERRASTERIZATION_AMOUNT 2 did not decode as merged coverage"); + smoothed.context[0x8e] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(smoothed); }, "under merged coverage"); + } { changed.context[0x8e] = 1; const auto partial = AgcDriver::Graphics::DecodeState(changed); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b5ee7296d8..ee73a1660c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -161,6 +161,7 @@ - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. +- [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp) (libSceAgcDriver) - the single-sample merge is measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. From 1babc10d42eaded55eb38c4bd2d9bdf06801aec1 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:30:39 +0200 Subject: [PATCH 0134/1360] fix(agc): refuse merged 4_4_4_4 writes from shaders that write memory under late tests A partial 4_4_4_4 write merges inside a pixel shader that declares early fragment tests. When the guest runs the shader under late tests, pixels that the depth or stencil test rejects still execute it on the console, so their buffer and image stores, atomics and GDS operations happen there. Under forced early tests they would be dropped. Such a merge now throws, unless the guest asked for early tests itself. The merged target's own image write does not count. A recompile test checks two things for a merging shader that adds to a buffer counter: under late tests it throws, and under the guest's early tests it compiles with its one atomic add beside the merge. The same shader without a merge still compiles under late tests. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 22 +++++++++++++++++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 12 ++++++++++ docs/dev/debt/shader.md | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f02f17e037..ffb2dbcec4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8952,6 +8952,28 @@ void unorm4_4_4_4TargetTests() { auto exporting = merging; exporting.context.pixel->depthExportEnable = true; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); + alignas(16) static std::array counted{}; + static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; + constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto counting = packedExportRequest(changed, partial, countingCode); + counting.context.userData = counter; + counting.context.pixel->targetMerge = merging.context.pixel->targetMerge; + counting.target = merging.target; + counting.target.registrationModules = false; + counting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInt64}; + counting.target.supportedCapabilities = atomicCapabilities; + counting.context.pixel->earlyZ = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "writes guest memory (stores, atomics or GDS) under late tests"); + counting.context.pixel->earlyZ = true; + const auto earlyWords = ShaderRecompiler::Recompile(counting).spirv.Copy(); + std::size_t atomics = 0; + for (std::size_t at = 5; at < earlyWords.size() && (earlyWords[at] >> 16u) != 0; at += earlyWords[at] >> 16u) atomics += static_cast(earlyWords[at] & 0xffffu) == spv::OpAtomicIAdd ? 1u : 0u; + Require(atomics == 1u && interlockShape(earlyWords) == "capability extension ordered begin end " && declaresEarlyFragmentTests(counting), "a 4_4_4_4 merge whose shader adds to a counter under early tests did not keep its one atomic add beside the merge"); + auto lateMerge = counting; + lateMerge.context.pixel->earlyZ = false; + lateMerge.context.pixel->targetMerge = {}; + static_cast(ShaderRecompiler::Recompile(lateMerge)); } changed = queue; changed.context[0x8e] = 0xau; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 1110470d17..7170bb8f77 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -208,6 +208,15 @@ std::uint32_t IndexedWindowSlots(const IrProgram& program) { return static_cast(reached); } +bool WritesGuestMemory(const IrProgram& program) { + const auto& info = program.Info(); + const auto writtenImage = [](const ImageResource& image) { return !image.targetMerge && (image.written || image.atomic || image.atomic64); }; + const auto writtenModes = [&](const std::vector& modes) { return std::ranges::any_of(modes, writtenImage); }; + const auto writtenBuffer = [](const BufferResource& buffer) { return buffer.written || buffer.atomic; }; + const auto gds = [](const IrDescriptorBinding& binding) { return binding.kind == DescriptorBindingKind::Gds; }; + return info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes) || std::ranges::any_of(program.Metadata().bindings.descriptors, gds); +} + } void DefineModule(SpirvEmitterState& state) { @@ -387,6 +396,9 @@ void DefineModule(SpirvEmitterState& state) { if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } + if (merge && !pixel.psEarlyZ && WritesGuestMemory(state.program)) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); + } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7d891da817..3107557cc3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -23,7 +23,7 @@ - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) - [4_4_4_4 partial writes, occlusion queries](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merging pixel shader always declares `EarlyFragmentTests`, so by the Vulkan rules sample counting happens before the shader runs and an occlusion query counts pixels a kill shader then discards. Not measured. -- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). It also throws when its shader writes guest memory (buffer or image stores, atomics, GDS) under late tests, since the early tests would drop the writes of the pixels they reject. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. ### Functional From 7ca6e1e9d213b4a145b35c0da53b37e0dba73fb9 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:30:50 +0200 Subject: [PATCH 0135/1360] fix(agc): refuse killing 4_4_4_4 merges while occlusion queries count samples A merged 4_4_4_4 write runs its pixel shader after early depth and stencil tests. Vulkan counts a sample for an occlusion query at those tests, so pixels the guest shader kills under late tests would be counted, and on the console they are not. Once the game has read an occlusion counter the recorder counts samples for every later draw, so from then on such a merge throws. The check runs where the recorded and recipe draw paths prepare their inputs. The state records DB_SHADER_CONTROL.KILL_ENABLE for it, which fits in the existing padding. The execution test starts sample counting and then checks two cases: a merge that kills a checkerboard under late tests is refused, and a merge that kills nothing still draws. A decode test reads KILL_ENABLE. Co-Authored-By: Claude Sonnet 5.5 --- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/include/Recorder.hpp | 1 + .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +++ .../libSceAgcDriver/Graphics/src/State.cpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 6 +++++ .../execution/PackedUnorm4444PartialWrite.cpp | 26 ++++++++++++++++++- docs/dev/debt/shader.md | 3 +-- 8 files changed, 40 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index c0e563f02e..c2eb095b84 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -263,7 +263,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.targetMerge != q.targetMerge || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; - if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ || s.pixelKill != t.pixelKill) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 169dcabeb6..78c3fc12ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -213,6 +213,7 @@ public: // Submits and waits for every batch, running completions in order. void Sync(); void CountSamples(); + bool CountingSamples() const { return countingSamples; } bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules); std::uint64_t SamplesTotal(); bool DumpSamples(VkDeviceAddress target, std::uint64_t address = 0); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 69802b363a..d7773aeba9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -155,6 +155,7 @@ struct State { std::uint32_t paClVsOutCntl = 0; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; bool pixelEarlyZ = false; + bool pixelKill = false; }; // PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 52ef639841..47f016563f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1093,6 +1093,10 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast(indexBytes), "an index buffer read"); + if (state.pixelKill && !state.pixelEarlyZ && std::ranges::any_of(state.colors, [](const ColorTarget& color) { return color.partialWriteMask != 0; })) { + const auto* recorder = Recorder::Active(); + Require(recorder == nullptr || !recorder->CountingSamples(), "a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests cannot use while occlusion queries count samples: the early tests would count the pixels it kills"); + } APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 42b0f2b502..5593813041 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1352,6 +1352,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); + result.pixelKill = !PixelProgramSkipped(queue) && (read(cx, 0x203) & 0x40u) != 0; if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ffb2dbcec4..82849cb1ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8880,6 +8880,12 @@ void unorm4_4_4_4TargetTests() { { changed.context[0x8e] = 1; const auto partial = AgcDriver::Graphics::DecodeState(changed); + auto killQueue = changed; + killQueue.context[0x203] = 0x40u; + const auto killState = AgcDriver::Graphics::DecodeState(killQueue); + killQueue.context[0x203] = 0x10u; + const auto earlyState = AgcDriver::Graphics::DecodeState(killQueue); + Require(killState.pixelKill && !killState.pixelEarlyZ && !earlyState.pixelKill && earlyState.pixelEarlyZ, "DB_SHADER_CONTROL.KILL_ENABLE of a 4_4_4_4 merge did not decode as a kill under late tests"); constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; auto merging = packedExportRequest(changed, partial, mergeCode); merging.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(partial); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 394411849b..1acad9581f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -254,6 +256,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa state.blends = {blend}; state.blend = blend; state.blendConstants = {}; + state.pixelKill = (setup.shaderControl & LateKill) != 0u; if (setup.depth != 0) { state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; state.depthTest = true; @@ -421,6 +424,26 @@ void DepthRejection(AgcDriver::VulkanDevice& device, Block& block, const ColorTa ExpectWords(device, block, layout, expected, before, "an X merge at z 0.25 in front of every stored depth"); } +void CountedKill(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the test device has no command recorder to count samples"); + recorder->CountSamples(); + } + const auto before = Fill(block, 501u); + const std::array rgba{0.6f, 0.2f, 0.8f, 0.4f}; + std::string refusal; + try { + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(rgba, Kill::BeforeExport), {PositionInputs, LateKill}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("while occlusion queries count samples") != std::string::npos, "a merge that kills under late tests drew while occlusion queries count samples" + (refusal.empty() ? std::string() : ": " + refusal)); + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(rgba)); + ExpectSurface(device, block, layout, before, Packed(rgba), 0xf000u, "a merged X write that kills nothing while occlusion queries count samples"); +} + void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { const auto stored = ReadBack(device, block); for (std::uint32_t y = 0; y < Height; ++y) { @@ -498,7 +521,8 @@ int main() { SameDrawOverdraw(*device, block, layout, color, 0.2f, 0.8f, 401u); SameDrawOverdraw(*device, block, layout, color, 0.8f, 0.2f, 402u); DepthRejection(*device, block, layout, color); - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers and a depth-rejected merge into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); + CountedKill(*device, block, layout, color); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers, a depth-rejected merge and a refused killing merge while samples are counted, into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 3107557cc3..f8bfaff326 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,8 +22,7 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) -- [4_4_4_4 partial writes, occlusion queries](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merging pixel shader always declares `EarlyFragmentTests`, so by the Vulkan rules sample counting happens before the shader runs and an occlusion query counts pixels a kill shader then discards. Not measured. -- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). It also throws when its shader writes guest memory (buffer or image stores, atomics, GDS) under late tests, since the early tests would drop the writes of the pixels they reject. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS) under late tests, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. ### Functional From 6ccb8c052e4c325cf761074f0f4590ce3b051fd4 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:31:05 +0200 Subject: [PATCH 0136/1360] fix(agc): refuse memory-writing 4_4_4_4 merges only where tests can reject pixels The refusal of a merging shader that writes guest memory under late tests fired in two cases where the early tests change nothing. The first was a draw with no depth, stencil or depth-bounds test. Early tests reject no pixel there, so every write the console makes still happens. The target merge now carries whether the draw has such a test, and the refusal applies only then. This adds one byte to the version 40 merge fields of the recompile request. The second was any GDS binding, including a shader that only reads GDS. The refusal now looks for GDS writes, atomics, appends and consumes. Tests: the depth and stencil decode checks the new flag, and the static shader key depends on it. A counter-adding shader under late tests compiles without a test and throws with one. A late-test merge that reads GDS compiles, and one that adds to GDS throws. Co-Authored-By: Claude Sonnet 5.5 --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../libSceAgcDriver/Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/State.cpp | 5 +++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 ++++++++++++++++-- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 10 +++++++--- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 2 ++ .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 16 +++++++++++++--- .../Translation/src/ShaderInputInfoBuilder.cpp | 1 + .../tests/RequestSerializerTests.cpp | 6 +++--- docs/dev/debt/shader.md | 2 +- 13 files changed, 53 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 84a83584ca..35fe9d6955 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -79,6 +79,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index d7773aeba9..4a34835f95 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -173,6 +173,7 @@ std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); bool DepthStencilWrites(const State& state); +bool DepthStencilTests(const State& state); ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5593813041..87348ec646 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1537,6 +1537,10 @@ bool DepthStencilWrites(const State& state) { return state.depthWrite || (state.stencilTest && (writes(state.stencilFront) || writes(state.stencilBack))); } +bool DepthStencilTests(const State& state) { + return state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); +} + ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { @@ -1547,6 +1551,7 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { merge.samples = static_cast(color.samples); merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); merge.depthStencilWrites = DepthStencilWrites(state); + merge.depthStencilTests = DepthStencilTests(state); } return merge; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 82849cb1ee..de1e86f42d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8847,13 +8847,15 @@ void unorm4_4_4_4TargetTests() { for (const auto offset : {0x013u, 0x015u}) depthQueue.context[offset] = 0x200; depthQueue.context[0x005] = 0x300; depthQueue.context[0x01e] = 0x1; + depthQueue.context[0x008] = std::bit_cast(0.25f); + depthQueue.context[0x009] = std::bit_cast(0.75f); struct DepthStencilCase { std::uint32_t control; std::uint32_t ops; std::uint32_t masks; bool writes; }; - for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}}) { + for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700718u, 0x00050050u, 0x01ffff01u, false}}) { depthQueue.context[0x200] = control; depthQueue.context[0x10b] = ops; depthQueue.context[0x10c] = depthQueue.context[0x10d] = masks; @@ -8861,7 +8863,7 @@ void unorm4_4_4_4TargetTests() { Require(AgcDriver::Graphics::DrawRejection(depthQueue, false).empty(), "the draw precheck refused a partial 4_4_4_4 write under " + what); const auto partial = AgcDriver::Graphics::DecodeState(depthQueue); const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); - Require(partial.depth.has_value() == ((control & 3u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes, "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); + Require(partial.depth.has_value() == ((control & 0xbu) != 0u) && partial.depthBoundsTest == ((control & 8u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes && merge.depthStencilTests == ((control & 0xbu) != 0u), "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); } } { @@ -8970,12 +8972,24 @@ void unorm4_4_4_4TargetTests() { static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInt64}; counting.target.supportedCapabilities = atomicCapabilities; counting.context.pixel->earlyZ = false; + static_cast(ShaderRecompiler::Recompile(counting)); + counting.context.pixel->targetMerge.depthStencilTests = true; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "writes guest memory (stores, atomics or GDS) under late tests"); counting.context.pixel->earlyZ = true; const auto earlyWords = ShaderRecompiler::Recompile(counting).spirv.Copy(); std::size_t atomics = 0; for (std::size_t at = 5; at < earlyWords.size() && (earlyWords[at] >> 16u) != 0; at += earlyWords[at] >> 16u) atomics += static_cast(earlyWords[at] & 0xffffu) == spv::OpAtomicIAdd ? 1u : 0u; Require(atomics == 1u && interlockShape(earlyWords) == "capability extension ordered begin end " && declaresEarlyFragmentTests(counting), "a 4_4_4_4 merge whose shader adds to a counter under early tests did not keep its one atomic add beside the merge"); + constexpr std::array gdsReadCode{0xbefc03ffu, 0x00000100u, 0x7e000280u, 0xd8da0000u, 0x04000000u, 0xbf8c0000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto gdsReading = packedExportRequest(changed, partial, gdsReadCode); + gdsReading.context.pixel->targetMerge = counting.context.pixel->targetMerge; + gdsReading.context.pixel->earlyZ = false; + gdsReading.target = counting.target; + static_cast(ShaderRecompiler::Recompile(gdsReading)); + constexpr std::array gdsAddCode{0xbefc03ffu, 0x00000100u, 0x7e000280u, 0xd8020000u, 0x00000000u, 0xbf8c0000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto gdsAdding = gdsReading; + gdsAdding.shader.code = gdsAddCode; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(gdsAdding)); }, "writes guest memory (stores, atomics or GDS) under late tests"); auto lateMerge = counting; lateMerge.context.pixel->earlyZ = false; lateMerge.context.pixel->targetMerge = {}; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 99da8fb457..5838cd62b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4835,6 +4835,10 @@ void verifyPixelRequestSerialization() { depthWritingMerge.context.pixel->targetMerge.depthStencilWrites = true; RecompileCacheKey::Build(depthWritingMerge, replayKey); require(key != replayKey, "the merge draw's depth or stencil writes did not change the static shader identity"); + auto depthTestingMerge = request; + depthTestingMerge.context.pixel->targetMerge.depthStencilTests = true; + RecompileCacheKey::Build(depthTestingMerge, replayKey); + require(key != replayKey, "the merge draw's depth or stencil tests did not change the static shader identity"); auto withoutMerge = request; withoutMerge.context.pixel->targetMerge = {}; RecompileCacheKey::Build(withoutMerge, replayKey); @@ -5118,12 +5122,12 @@ void verifyVertexInfoVersion19() { require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); constexpr std::size_t iterationBytes = 35u; - constexpr std::size_t mergeBytes = 39u; + constexpr std::size_t mergeBytes = 40u; const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); auto merging = pixel; - merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true}; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); - require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 39 bytes the version 40 pixel info adds after the pixel iteration fields"); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 40 pixel info adds after the stencil operation value flag"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); const auto withoutMerge = [&](std::vector bytes) { bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index fea7c67b40..f0ba68b06f 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -221,6 +221,7 @@ private: append(key, value.targetMerge.mask); append(key, value.targetMerge.samples); append(key, value.targetMerge.depthStencilWrites); + append(key, value.targetMerge.depthStencilTests); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 389ef2f0f3..cbe4b7c150 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -360,6 +360,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(info.targetMerge.samples); for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); writer.WriteBool(info.targetMerge.depthStencilWrites); + writer.WriteBool(info.targetMerge.depthStencilTests); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -436,6 +437,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.targetMerge.samples = reader.ReadU8(); for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); info.targetMerge.depthStencilWrites = reader.ReadBool(); + info.targetMerge.depthStencilTests = reader.ReadBool(); } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 4e271d4660..4a6923cfb1 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -199,6 +199,7 @@ struct ShaderPixelInputInfo { std::uint16_t mergeMask = 0; std::uint8_t mergeSamples = 1; bool mergeDepthStencilWrites = false; + bool mergeDepthStencilTests = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 7c56960968..9d508c26f3 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -157,6 +157,7 @@ struct TargetMerge { std::uint8_t samples = 1; std::array descriptor{}; bool depthStencilWrites = false; + bool depthStencilTests = false; bool operator==(const TargetMerge&) const = default; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 7170bb8f77..0f95c839c2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -213,8 +213,18 @@ bool WritesGuestMemory(const IrProgram& program) { const auto writtenImage = [](const ImageResource& image) { return !image.targetMerge && (image.written || image.atomic || image.atomic64); }; const auto writtenModes = [&](const std::vector& modes) { return std::ranges::any_of(modes, writtenImage); }; const auto writtenBuffer = [](const BufferResource& buffer) { return buffer.written || buffer.atomic; }; - const auto gds = [](const IrDescriptorBinding& binding) { return binding.kind == DescriptorBindingKind::Gds; }; - return info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes) || std::ranges::any_of(program.Metadata().bindings.descriptors, gds); + if (info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes)) return true; + const auto& memory = program.Resources().memoryInfo; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None || access == SharedAccess::Read) continue; + const auto index = inst->Flags().index; + if (index >= memory.size()) FailEmit("a shared memory access has no memory metadata"); + if (memory[index].kind == ResourceKind::Gds) return true; + } + } + return false; } } @@ -396,7 +406,7 @@ void DefineModule(SpirvEmitterState& state) { if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } - if (merge && !pixel.psEarlyZ && WritesGuestMemory(state.program)) { + if (merge && !pixel.psEarlyZ && pixel.mergeDepthStencilTests && WritesGuestMemory(state.program)) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 6161de1552..9a05e31dc5 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -160,6 +160,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.mergeMask = pixel.targetMerge.mask; pixelStorage.mergeSamples = pixel.targetMerge.samples; pixelStorage.mergeDepthStencilWrites = pixel.targetMerge.depthStencilWrites; + pixelStorage.mergeDepthStencilTests = pixel.targetMerge.depthStencilTests; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 00f6495be8..ef2304178d 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -54,7 +54,7 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 74u, 0u); + bytes.insert(bytes.begin() + 282u, 75u, 0u); bytes[322] = 1u; } @@ -411,7 +411,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 356u); + if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 357u); upgraded[4] = 40u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -500,7 +500,7 @@ int main() { auto version35 = DecodeBase64(serializer.Serialize(request)); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); - version35.erase(stencilOperationValue, stencilOperationValue + 1 + 39); + version35.erase(stencilOperationValue, stencilOperationValue + 1 + 40); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index f8bfaff326..a4683beb10 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,7 +22,7 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) -- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS) under late tests, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS writes) under late tests in a draw with a depth, stencil or depth-bounds test, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. ### Functional From c5b42dbef1bef853e2db3554d54dd0bac5d3979b Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:19:51 +0200 Subject: [PATCH 0137/1360] feat(agc): merge partial 4_4_4_4 writes into multisampled color targets A partial write of a multisampled 4_4_4_4 target now merges per sample: the draw shades per sample, and the pixel shader reads and writes its own sample (gl_SampleID) of the target through a multisampled storage image whose T# describes the target's surface. Draw makes the multisampled target storable before it takes the attachment view, so the image swap of the first storage access cannot leave the framebuffer on the retired image, and it accepts the target among the draw's multisampled sources only as that one merge binding. An execution test draws a full write into a 2-sample target, merges an X write of a triangle that cuts through pixels, and loads every sample: covered samples hold the merged word, uncovered ones the first draw's. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 12 +- .../Graphics/src/GuestTextureResource.cpp | 8 +- .../libSceAgcDriver/Graphics/src/State.cpp | 17 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 32 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 4 +- ...PackedUnorm4444MultisamplePartialWrite.cpp | 316 ++++++++++++++++++ .../Optimization/src/ShaderInfoCollector.cpp | 3 +- .../SpirvBackend/SpirvEmitterHelpers.hpp | 2 +- .../SpirvMemory/SpirvDescriptors.hpp | 2 +- .../src/SpirvMemory/SpirvDescriptors.cpp | 9 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 14 +- .../src/ShaderInputInfoBuilder.cpp | 2 +- 14 files changed, 401 insertions(+), 23 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e37e75bd3a..f4bf57be49 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1958,6 +1958,8 @@ target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVAT target_include_directories(agc_driver_packed_unorm4444_partial_write_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_multisample_partial_write_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 4a34835f95..091b96701e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -172,6 +172,7 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +GuestTextureResource TargetMergeSurface(const ColorTarget& color); bool DepthStencilWrites(const State& state); bool DepthStencilTests(const State& state); ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 47f016563f..360750c7dd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2212,6 +2212,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (color.samples > 1) { binding.multisampled = true; timer.phase(PhaseSetup); + if (color.partialWriteMask != 0) { + static_cast(MultisampleTargetView(context, color)); + static_cast(MultisampleTargetStorageView(context, TargetMergeSurface(color))); + } targetViews.push_back(MultisampleTargetView(context, color, binding.multisampleOwner, &binding.multisampleImage)); timer.phase(PhaseReadTarget); continue; @@ -2438,10 +2442,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); for (const auto& binding : targets) { - if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + if (!binding.multisampled) continue; + const auto& sources = resources->MultisampleSources(); + const auto reads = std::count(sources.begin(), sources.end(), binding.color.surfaceAddress); + Require(reads == 0 || (reads == 1 && binding.color.partialWriteMask != 0), "sampling a multisampled color target the draw renders to is not implemented"); + Require(binding.color.partialWriteMask == 0 || (reads == 1 && MultisampleTargetStorable(context, binding.color)), "the partial write of a multisampled packed color target does not store into the target's image"); } for (const auto& binding : targets) { - if (binding.color.partialWriteMask == 0) continue; + if (binding.color.partialWriteMask == 0 || binding.multisampled) continue; Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only"); const auto images = resources->StorageImages(); Require(std::ranges::any_of(images, [&](const std::pair& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 1acb77e3d9..3fca810a43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include #include #include @@ -243,7 +244,8 @@ GuestTextureResource DecodeTextureResource(std::span words) } std::array EncodeTextureResource(const GuestTextureResource& resource) { - Require(resource.dimension == TextureDimension::k2D && resource.samples == 1u, "only single-sample 2D surfaces have a texture descriptor encoding"); + const bool multisampled = resource.dimension == TextureDimension::k2DMsaa; + Require((resource.dimension == TextureDimension::k2D && resource.samples == 1u) || (multisampled && resource.samples >= 2u && resource.samples <= 16u && std::has_single_bit(resource.samples)), "only 2D and multisampled 2D surfaces have a texture descriptor encoding"); Require(resource.mipCount == 1u && resource.allocatedMipCount <= 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.dccAddress == 0u && resource.minLod == 0u && !resource.iterate256 && resource.bcSwizzle == 0u, "only single-level, single-layer surfaces without metadata have a texture descriptor encoding"); Require(resource.width >= 1u && resource.width <= 0x4000u && resource.height >= 1u && resource.height <= 0x4000u && resource.format < 0x200u, "the surface's extent or format has no texture descriptor encoding"); const auto xorMask = resource.tileMode == TextureTileMode::kS4KBX || resource.tileMode == TextureTileMode::kD4KBX ? 0xfffull : 0xffffull; @@ -268,11 +270,13 @@ std::array EncodeTextureResource(const GuestTextureResource& r case TextureTileMode::kD64KBX: tileMode = 0x1a; break; case TextureTileMode::kR64KBX: tileMode = 0x1b; break; } + const auto sampleLevel = static_cast(std::countr_zero(resource.samples)); std::array words{}; words[0] = static_cast(base); words[1] = static_cast(base >> 32u) | (resource.format << 20u) | (((resource.width - 1u) & 3u) << 30u); words[2] = ((resource.width - 1u) >> 2u) | ((resource.height - 1u) << 14u); - words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (tileMode << 20u) | (9u << 28u); + words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (sampleLevel << 16u) | (tileMode << 20u) | ((multisampled ? 14u : 9u) << 28u); + words[5] = sampleLevel << 4u; return words; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 87348ec646..5fa91dcea9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1470,11 +1470,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const std::uint32_t packedMask = nibbles ? 0xfu : 7u; Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); if ((exportedMask & packedMask) != packedMask) { - Require(color.samples == 1u, "partial writes of a multisampled " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); color.partialWriteMask = PackedWriteMask(mapping, exportedMask); state.colorWriteMask = 0; + if (color.samples > 1u) result.perSampleShading = true; } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { @@ -1531,6 +1531,16 @@ std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t expor return mask; } +GuestTextureResource TargetMergeSurface(const ColorTarget& color) { + auto surface = SurfaceForTarget(color); + if (color.samples > 1u) { + Require(surface.baseAddress == color.surfaceAddress, "a merged multisampled color target does not start at its surface"); + surface.dimension = TextureDimension::k2DMsaa; + surface.samples = color.samples; + } + return surface; +} + bool DepthStencilWrites(const State& state) { if (!state.depth) return false; const auto writes = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) != 0 && (face.failOp != VK_STENCIL_OP_KEEP || face.passOp != VK_STENCIL_OP_KEEP || face.depthFailOp != VK_STENCIL_OP_KEEP); }; @@ -1545,11 +1555,12 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { if (color.partialWriteMask == 0) continue; - Require(merge.mask == 0 && color.exportIndex < 8u && color.samples == 1u, "a draw merges into one single-sample packed color target at most"); + Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one packed color target at most"); + Require(color.samples == 1u || state.perSampleShading, "a merged multisampled color target needs per-sample shading"); merge.target = static_cast(color.exportIndex); merge.mask = color.partialWriteMask; merge.samples = static_cast(color.samples); - merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); + merge.descriptor = EncodeTextureResource(TargetMergeSurface(color)); merge.depthStencilWrites = DepthStencilWrites(state); merge.depthStencilTests = DepthStencilTests(state); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index de1e86f42d..79d3ed27dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8996,9 +8996,37 @@ void unorm4_4_4_4TargetTests() { static_cast(ShaderRecompiler::Recompile(lateMerge)); } changed = queue; - changed.context[0x8e] = 0xau; + changed.context[0x8e] = 1u; changed.context[0x31d] = (changed.context.at(0x31d) & ~0x3f000u) | (1u << 12u) | (1u << 15u); - expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a multisampled 4_4_4_4 unorm color target"); + { + const auto multisampled = AgcDriver::Graphics::DecodeState(changed); + Require(multisampled.samples == 2u && multisampled.color.samples == 2u && multisampled.color.partialWriteMask == 0xf000u && multisampled.blends[0].colorWriteMask == 0u && multisampled.perSampleShading, "a partial write of a 2-sample 4_4_4_4 unorm target did not merge per sample behind a closed write mask"); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); + const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); + Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); + auto unshaded = multisampled; + unshaded.perSampleShading = false; + expectFailure([&] { static_cast(AgcDriver::Graphics::ExportTargetMerge(unshaded)); }, "needs per-sample shading"); + constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto merging = packedExportRequest(changed, multisampled, mergeCode); + merging.context.pixel->targetMerge = merge; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample}; + merging.target.supportedCapabilities = capabilities; + const auto words = ShaderRecompiler::Recompile(merging).spirv.Copy(); + std::size_t sampled = 0; + bool sampleRate = false; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilitySampleRateShading) sampleRate = true; + if (op == spv::OpImageRead && (words[at] >> 16u) == 7u && words[at + 5] == spv::ImageOperandsSampleMask) ++sampled; + if (op == spv::OpImageWrite && (words[at] >> 16u) == 6u && words[at + 4] == spv::ImageOperandsSampleMask) ++sampled; + } + Require(sampleRate && sampled == 2u && interlockShape(words) == "capability extension ordered begin end ", "a 2-sample 4_4_4_4 merge module does not read and write the shaded sample inside a pixel interlock"); + auto single = merging; + const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + single.target.supportedCapabilities = noMultisampleStorage; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); + } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 5838cd62b6..1790340d6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5125,9 +5125,9 @@ void verifyVertexInfoVersion19() { constexpr std::size_t mergeBytes = 40u; const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); auto merging = pixel; - merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 2u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); - require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 40 pixel info adds after the stencil operation value flag"); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 40 pixel info adds after the stencil operation value flag"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); const auto withoutMerge = [&](std::vector bytes) { bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp new file mode 100644 index 0000000000..0a01c8cf18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -0,0 +1,316 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.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 AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 2; +constexpr std::uint32_t SampleShift = 1; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr double EdgeMargin = 1.0 / 32.0; +constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; +constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +constexpr std::array, 3> Covering{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +constexpr std::array, 3> Cutting{{ + {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 4> Programs{}; +std::size_t programCount = 0; +alignas(256) std::array Buffer{}; + +std::uint32_t Unorm(float value) { + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return 15; + return static_cast(std::nearbyint(static_cast(value) * 15.0)); +} + +std::uint32_t Packed(std::array rgba) { + return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u); +} + +std::span Float32Program(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of 2 samples"); + return color; +} + +AgcDriver::Graphics::State TargetState(AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask) { + const bool partial = exportedMask != 0xfu; + color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = partial ? 0u : 0xfu; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.samples = Samples; + state.perSampleShading = partial; + return state; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, const std::array, 3>& triangle, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture) { + std::vector userData(16, 0u); + const auto address = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(LoadCode.data()), LoadCode, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::vector loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) loaded[first + tid] = Buffer[tid * Words + LoadResult]; + } + return loaded; +} + +double EdgeDistance(double x, double y, std::size_t from) { + const auto pixels = [](const std::array& vertex) { + return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(Cutting[from]); + const auto b = pixels(Cutting[(from + 1u) % Cutting.size()]); + const auto c = pixels(Cutting[(from + 2u) % Cutting.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.storageImageMultisample || !context.sampleRateShading || !TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { + std::puts("skipped, the device lacks storage image multisampling, sample rate shading or fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + Block block(4u * BlockBytes); + const auto color = DecodeTarget(block); + const auto full = TargetState(color, 0xfu); + Draw(*device, full, Covering, Float32Program(First)); + const auto merged = TargetState(color, 0x1u); + Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); + Draw(*device, merged, Cutting, Float32Program(Second)); + const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); + const auto loaded = LoadSamples(*device, texture); + const auto background = Packed(First); + const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); + Require(written != background, "the test colors do not differ in the merged nibble"); + const auto locations = AgcDriver::Graphics::SampleLocations(merged); + std::array pixelsByCoverage{}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto word = loaded[(y * Side + x) * Samples + sample]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); + Require(word == written || word == background, std::string(where) + ", neither the merged word 0x" + std::to_string(written) + " nor the first draw's word"); + covered += word == written ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); + if (std::fabs(distance) < EdgeMargin) continue; + ++checked; + Require((word == written) == (distance > 0), std::string(where) + (distance > 0 ? ", which the merging triangle covers" : ", which the merging triangle does not cover")); + } + ++pixelsByCoverage[covered]; + } + } + Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merging triangle does not leave pixels uncovered, covered and partly covered"); + std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly covered pixels merged bits 15..12 into their covered samples only\n", pixelsByCoverage[1]); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index b56aea041c..d22d470487 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -528,7 +528,7 @@ void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { image.source = std::numeric_limits::max(); image.resourceClass = ImageResourceClass::Storage; image.numericClass = IrTextureNumericClass::Uint; - image.dimension = RdnaImageDimension::Dim2D; + image.dimension = pixel.mergeSamples > 1u ? RdnaImageDimension::Dim2DMsaa : RdnaImageDimension::Dim2D; image.read = true; image.written = true; image.fmaskCompatible = false; @@ -536,6 +536,7 @@ void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { info.images.push_back(image); info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); + if (pixel.mergeSamples > 1u) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); } void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 4dde3cba3e..6c1539608c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -70,7 +70,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); -std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); std::uint32_t ExecutionModelForStage(IrShaderStage stage); std::uint32_t ConstantI32(SpirvEmitterState& state, std::int32_t value); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 766da39c29..5ff7152418 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -21,7 +21,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); -std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); [[noreturn]] void ExitDescriptorBindingFailure(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, const char* reason); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index c9a3717039..7a5fe34450 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -233,7 +233,7 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } -std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord) { +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample) { const auto& image = state.program.Info().images.at(resource); if (image.resourceClass != ImageResourceClass::Storage || image.table != NoTable || image.mipMode == ImageMipMode::DynamicStorage || image.atomic) { FailEmit("storage image read requested for an image that is not one plain storage descriptor"); @@ -245,7 +245,12 @@ std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resou state.module.AddFunction(spv::OpLoad, imageType, descriptor, StorageImageDescriptorPointer(state, resource)); const auto texel = state.module.AllocateId(); const auto texelType = ImageVectorType(state, image.numericClass, 4u); - state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + if (RdnaImageDimensionInfoFor(image.dimension).multisampled != 0u) { + if (sample == 0u) FailEmit("multisampled storage image read requires a sample"); + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord, spv::ImageOperandsSampleMask, sample); + } else { + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + } return texel; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index c88b3f34ad..a070862e06 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -417,27 +417,29 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target struct MergeTexel { std::uint32_t resource; std::uint32_t coord; + std::uint32_t sample; }; MergeTexel MergeTargetTexel(SpirvEmitterState& state) { + const auto& pixel = PixelInfo(state); const auto& images = state.program.Info().images; const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); if (image == images.end()) FailEmit("the color target merge image was not collected"); - if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared"); + if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u || (pixel.mergeSamples > 1u && InputVariableForKind(state, StageInputKind::SampleId) == 0u)) FailEmit("the color target merge inputs were not declared"); if (state.mergeWordVariable == 0u || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; const auto x = position(0u); const auto y = position(1u); const auto coord = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); - return {static_cast(image - images.begin()), coord}; + return {static_cast(image - images.begin()), coord, pixel.mergeSamples > 1u ? EmitBuiltinU32(state, StageInputKind::SampleId, 0u) : 0u}; } void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { const auto& pixel = PixelInfo(state); - const auto [resource, coord] = MergeTargetTexel(state); + const auto [resource, coord, sample] = MergeTargetTexel(state); const auto stored = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord, sample), 0u); const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); @@ -759,12 +761,12 @@ void FlushColorTargetMerge(SpirvEmitterState& state) { write = Binary(state, spv::OpLogicalAnd, TypeBool(state), write, Binary(state, spv::OpINotEqual, TypeBool(state), valid, ConstantU32(state, 0u))); } EmitIfCondition(state, write, [&] { - const auto [resource, coord] = MergeTargetTexel(state); + const auto [resource, coord, sample] = MergeTargetTexel(state); const auto merged = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), merged, state.mergeWordVariable); const auto texel = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); - EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); + EmitStorageImageWrite(state, resource, 0u, coord, texel, sample); }); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 9a05e31dc5..57c126d55a 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -155,7 +155,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psIteratesSamples = pixel.iteratesSamples; pixelStorage.psPositionLocation = pixel.positionLocation; pixelStorage.sampleLocations = pixel.sampleLocations; - if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || pixel.targetMerge.samples != 1u)) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); + if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || (pixel.targetMerge.samples != 1u && pixel.targetMerge.samples != 2u && pixel.targetMerge.samples != 4u && pixel.targetMerge.samples != 8u))) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); pixelStorage.mergeTarget = pixel.targetMerge.target; pixelStorage.mergeMask = pixel.targetMerge.mask; pixelStorage.mergeSamples = pixel.targetMerge.samples; From f0ea4fa5636bac3b59ab4868b6f2f5244c40ebba Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:20:07 +0200 Subject: [PATCH 0138/1360] fix(agc): settle merged multisampled writes before the draws that follow A multisampled target's storage writes are ordered for later draws only through MultisampleTarget::Storage(), which a reused resource build would skip. The merging draw now takes the storage view once more after its resources are resolved, so it is the last access before recording; multisampled draws make no draw recipe. The multisampled test draws a full write right after the merge and checks that it covers the merged samples it overlaps. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + ...PackedUnorm4444MultisamplePartialWrite.cpp | 42 +++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 360750c7dd..673d8a3968 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2447,6 +2447,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto reads = std::count(sources.begin(), sources.end(), binding.color.surfaceAddress); Require(reads == 0 || (reads == 1 && binding.color.partialWriteMask != 0), "sampling a multisampled color target the draw renders to is not implemented"); Require(binding.color.partialWriteMask == 0 || (reads == 1 && MultisampleTargetStorable(context, binding.color)), "the partial write of a multisampled packed color target does not store into the target's image"); + if (binding.color.partialWriteMask != 0) static_cast(MultisampleTargetStorageView(context, TargetMergeSurface(binding.color))); } for (const auto& binding : targets) { if (binding.color.partialWriteMask == 0 || binding.multisampled) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index 0a01c8cf18..0e7f6d3031 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -55,6 +55,7 @@ constexpr std::uint32_t LoadResult = 12; constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; +constexpr std::array Third{1.0f / 15.0f, 1.0f, 0.0f, 4.0f / 15.0f}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -75,6 +76,10 @@ constexpr std::array, 3> Cutting{{ {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} }}; +constexpr std::array, 3> Overlapping{{ + {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} +}}; + alignas(256) std::array, 4> Programs{}; std::size_t programCount = 0; alignas(256) std::array Buffer{}; @@ -248,18 +253,22 @@ std::vector LoadSamples(AgcDriver::VulkanDevice& device, const st return loaded; } -double EdgeDistance(double x, double y, std::size_t from) { +double EdgeDistance(const std::array, 3>& triangle, double x, double y, std::size_t from) { const auto pixels = [](const std::array& vertex) { return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; }; - const auto a = pixels(Cutting[from]); - const auto b = pixels(Cutting[(from + 1u) % Cutting.size()]); - const auto c = pixels(Cutting[(from + 2u) % Cutting.size()]); + const auto a = pixels(triangle[from]); + const auto b = pixels(triangle[(from + 1u) % triangle.size()]); + const auto c = pixels(triangle[(from + 2u) % triangle.size()]); const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); } +double Inside(const std::array, 3>& triangle, double x, double y) { + return std::min({EdgeDistance(triangle, x, y, 0), EdgeDistance(triangle, x, y, 1), EdgeDistance(triangle, x, y, 2)}); +} + } int main() { @@ -278,14 +287,18 @@ int main() { const auto merged = TargetState(color, 0x1u); Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); Draw(*device, merged, Cutting, Float32Program(Second)); + Draw(*device, full, Overlapping, Float32Program(Third)); const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); const auto loaded = LoadSamples(*device, texture); const auto background = Packed(First); const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); - Require(written != background, "the test colors do not differ in the merged nibble"); + const auto over = Packed(Third); + Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); const auto locations = AgcDriver::Graphics::SampleLocations(merged); std::array pixelsByCoverage{}; std::uint32_t checked = 0; + std::uint32_t overdrawn = 0; + std::uint32_t mergedAfterOverdraw = 0; for (std::uint32_t y = 0; y < Side; ++y) { for (std::uint32_t x = 0; x < Side; ++x) { std::uint32_t covered = 0; @@ -293,21 +306,26 @@ int main() { const auto word = loaded[(y * Side + x) * Samples + sample]; char where[160]; std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); - Require(word == written || word == background, std::string(where) + ", neither the merged word 0x" + std::to_string(written) + " nor the first draw's word"); + Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); covered += word == written ? 1u : 0u; const auto px = x + static_cast(locations[sample].x); const auto py = y + static_cast(locations[sample].y); - const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); - if (std::fabs(distance) < EdgeMargin) continue; + const auto distance = Inside(Cutting, px, py); + const auto overDistance = Inside(Overlapping, px, py); + if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; ++checked; - Require((word == written) == (distance > 0), std::string(where) + (distance > 0 ? ", which the merging triangle covers" : ", which the merging triangle does not cover")); + const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; + overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; + mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; + Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); } ++pixelsByCoverage[covered]; } } - Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); - Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merging triangle does not leave pixels uncovered, covered and partly covered"); - std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly covered pixels merged bits 15..12 into their covered samples only\n", pixelsByCoverage[1]); + Require(checked >= Side * Side * Samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); + Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); + std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, and a full write drawn right after covered %u merged samples\n", pixelsByCoverage[1], overdrawn); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 2e8ea10af4216cc22e843d49b6ce648c3b1e047c Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:56:15 +0200 Subject: [PATCH 0139/1360] test(agc): decode the 2-sample 4_4_4_4 merge draws from registers The 2-sample test decodes its draw states from registers and checks the word mask, the closed write mask, per-sample shading and the exported merge, and a skip names the missing device feature. Co-Authored-By: Claude Sonnet 5.5 --- ...PackedUnorm4444MultisamplePartialWrite.cpp | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index 0e7f6d3031..b5fd107ed5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -152,15 +152,47 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } -AgcDriver::Graphics::State TargetState(AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask) { +AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { + AgcDriver::QueueState queue; + queue.userConfig[0x242] = 4; + queue.context = { + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, Float32Export}, {0x1c3, 4}, + {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib}, + {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, (Side << 16u) | Side}, + {0x81, 0x80000000}, {0x82, (Side << 16u) | Side}, + {0x90, 0x80000000}, {0x91, (Side << 16u) | Side}, + {0x94, 0x80000000}, {0x95, (Side << 16u) | Side}, + {0x10f, std::bit_cast(static_cast(Side) / 2.0f)}, {0x110, std::bit_cast(static_cast(Side) / 2.0f)}, + {0x111, std::bit_cast(-static_cast(Side) / 2.0f)}, {0x112, std::bit_cast(static_cast(Side) / 2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask) { const bool partial = exportedMask != 0xfu; - color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; - VkPipelineColorBlendAttachmentState blend{}; - blend.colorWriteMask = partial ? 0u : 0xfu; + const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask)); + const auto& color = decoded.colors.front(); + char detail[256]; + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the 2-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); + Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? 0xf000u : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); + Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? Samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded 2-sample target exported another merge than its own surface"); AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; - state.colors = {color}; - state.color = color; + state.colors = decoded.colors; + state.color = decoded.color; state.hasColorTarget = true; state.renderExtent = {Side, Side}; state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; @@ -169,11 +201,11 @@ AgcDriver::Graphics::State TargetState(AgcDriver::Graphics::ColorTarget color, s state.scissor = {{0, 0}, {Side, Side}}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - state.blends = {blend}; - state.blend = blend; + state.blends = decoded.blends; + state.blend = decoded.blend; state.blendConstants = {}; - state.samples = Samples; - state.perSampleShading = partial; + state.samples = decoded.samples; + state.perSampleShading = decoded.perSampleShading; return state; } @@ -276,15 +308,16 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); - if (!context.storageImageMultisample || !context.sampleRateShading || !TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { - std::puts("skipped, the device lacks storage image multisampling, sample rate shading or fragmentShaderPixelInterlock"); + for (const auto& [present, feature] : {std::pair{context.storageImageMultisample, "shaderStorageImageMultisample"}, std::pair{context.sampleRateShading, "sampleRateShading"}, std::pair{TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT), "fragmentShaderPixelInterlock"}}) { + if (present) continue; + std::printf("skipped, the device lacks %s\n", feature); return VulkanTestSkipped; } Block block(4u * BlockBytes); const auto color = DecodeTarget(block); - const auto full = TargetState(color, 0xfu); + const auto full = TargetState(block, 0xfu); Draw(*device, full, Covering, Float32Program(First)); - const auto merged = TargetState(color, 0x1u); + const auto merged = TargetState(block, 0x1u); Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); Draw(*device, merged, Cutting, Float32Program(Second)); Draw(*device, full, Overlapping, Float32Program(Third)); From 6f0873cde0795d707712eb043001459fa56640f3 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:20:26 +0200 Subject: [PATCH 0140/1360] docs(agc): list the limits of merged 2-sample 4_4_4_4 partial writes Per-sample shading of a merging draw makes centroid and sample-located inputs and the ancillary sample number follow the invocation's sample (centre-located inputs too on devices without interpolation functions and fragment shader barycentrics), and the 2-sample merge was measured on one GPU only. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee73a1660c..4868c8e36d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,9 +160,10 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. Centre-located positions stay at the pixel centre, and so do centre-located inputs on devices with interpolation functions and fragment shader barycentrics (elsewhere they move to the sample position); centroid inputs, sample-located inputs and positions, and the sample number in the ancillary input follow the invocation's sample instead of the pixel's centroid and sample 0. A guest shader written for pixel-rate shading can see slightly different values there. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. -- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp) (libSceAgcDriver) - the single-sample merge is measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. From 3e8f7972074a020c1a63d392d77cbe7a8370ca24 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:53:52 +0200 Subject: [PATCH 0141/1360] fix(agc): refuse coverage input in a merged multisampled 4_4_4_4 draw A partial write of a multisampled 4_4_4_4 target forces per-sample shading after DecodeState checked SAMPLE_COVERAGE against the guest's own iteration, so a pixel-rate shader reading SAMPLE_COVERAGE got only its invocation's sample in gl_SampleMaskIn instead of the whole pixel's coverage. The forced shading now runs the same check and throws its message; a full write of the same target still decodes. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5fa91dcea9..557f8e262d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1474,7 +1474,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); color.partialWriteMask = PackedWriteMask(mapping, exportedMask); state.colorWriteMask = 0; - if (color.samples > 1u) result.perSampleShading = true; + if (color.samples > 1u) { + Require(sampleCoverageSupported(queue, result.samples, true), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); + result.perSampleShading = true; + } } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 79d3ed27dd..c8b7265de4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9001,6 +9001,12 @@ void unorm4_4_4_4TargetTests() { { const auto multisampled = AgcDriver::Graphics::DecodeState(changed); Require(multisampled.samples == 2u && multisampled.color.samples == 2u && multisampled.color.partialWriteMask == 0xf000u && multisampled.blends[0].colorWriteMask == 0u && multisampled.perSampleShading, "a partial write of a 2-sample 4_4_4_4 unorm target did not merge per sample behind a closed write mask"); + auto coverage = changed; + coverage.context[0x1b3] = 0x4002u; + coverage.context[0x1b4] = 0x4002u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(coverage); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); + coverage.context[0x8e] = 0xfu; + Require(!AgcDriver::Graphics::DecodeState(coverage).perSampleShading, "a full write of a 2-sample 4_4_4_4 unorm target with a SAMPLE_COVERAGE input was refused or ran per sample"); const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); From a1814c5853d173bdec36b5a2158feaac3e7f769d Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:58:44 +0200 Subject: [PATCH 0142/1360] fix(agc): refuse per-pixel inputs that move when a 2-sample merge iterates A partial write of a multisampled 4_4_4_4 target makes the draw shade per sample. For a shader the guest runs per pixel, every invocation of a pixel must then see the same inputs. Otherwise its colours and kill decisions follow each sample, while the console runs one invocation. The decode now throws in two cases when the guest did not ask for per-sample shading. The first is a shader that reads the ancillary word, whose sample number would become the invocation's. The second is a shader that reads centroid, sample-located or pull-model barycentrics, Z, 1/W, or a position at the centroid or a sample. That case is allowed when every sample sits at the pixel centre, as in a draw raised from a one-sample rasterizer. The recompiler throws for centre-located barycentrics on a device without interpolation functions and fragment shader barycentrics, where they would not be rebuilt at the centre. Decode tests run each moving input on a 2-sample rasterizer, where it throws, and with PS_ITER_SAMPLE, where it decodes. The raised draw keeps the located inputs and refuses the ancillary word. Centre and upper-left positions, centre barycentrics, the front face and the fixed-point position decode, and a recompile test covers the missing barycentrics. The debt entry now lists what is kept and what throws. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 25 ++++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 38 ++++++++++++++++++- .../Translation/src/InstructionTranslator.cpp | 3 ++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 557f8e262d..827db079ca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -505,6 +505,25 @@ bool sampleCoverageSupported(const QueueState& queue, std::uint32_t samples, boo return (inputs & PixelSampleCoverageInput) == 0u || (!perSampleShading && (inputs & PixelSampleRateInputs) == 0u); } +bool pixelRateInputsKept(const QueueState& queue, const State& state) { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + constexpr std::uint32_t numbered = PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage); + constexpr std::uint32_t located = PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::PerspectiveCentroid) | PixelInputBit(PixelInput::PerspectivePullModel) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::LinearCentroid) | PixelInputBit(PixelInput::PositionZ) | PixelInputBit(PixelInput::PositionW); + const bool centred = state.customSampleLocations && std::ranges::all_of(state.sampleLocations, [](std::uint8_t location) { return location == 0u; }); + const auto moving = numbered | (centred ? 0u : located); + if (PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena == queue.context.end() || addr == queue.context.end()) return true; + const auto inputs = ena->second & addr->second; + if ((inputs & moving) != 0u) return false; + if (centred || (inputs & (PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY))) == 0u) return true; + const auto position = find(queue.context, 0x1b8); + const auto control = position == queue.context.end() ? 0u : position->second; + return (control & 0x100000u) != 0u || ((control >> 16u) & 3u) == 0u; +} + bool coveredCentroidSupported(const QueueState& queue) { const auto config = find(queue.context, 0x2f8); if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; @@ -1476,6 +1495,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { state.colorWriteMask = 0; if (color.samples > 1u) { Require(sampleCoverageSupported(queue, result.samples, true), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); + if (!result.perSampleShading && !pixelRateInputsKept(queue, result)) { + const auto value = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; + char detail[160]; + std::snprintf(detail, sizeof(detail), " (SPI_PS_INPUT_ENA 0x%x, SPI_PS_INPUT_ADDR 0x%x, SPI_BARYC_CNTL 0x%x, sample 1 location 0x%02x)", value(0x1b3), value(0x1b4), value(0x1b8), result.sampleLocations[1]); + Require(false, std::string("partial writes of a multisampled 4_4_4_4 unorm color target run the pixel shader once per sample, so a shader the guest runs once per pixel cannot read the ancillary word or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample: they would follow the invocation's sample") + detail); + } result.perSampleShading = true; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c8b7265de4..35d32755ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9007,6 +9007,36 @@ void unorm4_4_4_4TargetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(coverage); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); coverage.context[0x8e] = 0xfu; Require(!AgcDriver::Graphics::DecodeState(coverage).perSampleShading, "a full write of a 2-sample 4_4_4_4 unorm target with a SAMPLE_COVERAGE input was refused or ran per sample"); + struct InputCase { + std::uint32_t inputs; + std::uint32_t position; + const char* what; + }; + auto rasterized = changed; + rasterized.context[0x2f8] = 0x00100001u; + rasterized.context[0x292] = 3; + rasterized.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) rasterized.context[0x2fe + pixel * 4u] = 0xcc44; + const auto inputState = [](AgcDriver::QueueState queue, std::uint32_t inputs, std::uint32_t position) { + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; + queue.context[0x1b8] = position; + return queue; + }; + for (const auto& [inputs, position, what] : {InputCase{0x0004u, 0u, "PERSP_CENTROID"}, InputCase{0x0040u, 0u, "LINEAR_CENTROID"}, InputCase{0x0001u, 0u, "PERSP_SAMPLE"}, InputCase{0x0008u, 0u, "PERSP_PULL_MODEL"}, InputCase{0x2002u, 0u, "ANCILLARY"}, InputCase{0x0402u, 0u, "POS_Z_FLOAT"}, InputCase{0x0802u, 0u, "POS_W_FLOAT"}, InputCase{0x0302u, 0x20000u, "POS_X_FLOAT and POS_Y_FLOAT at a sample"}, InputCase{0x0102u, 0x10000u, "POS_X_FLOAT at the centroid"}}) { + auto moved = inputState(rasterized, inputs, position); + expectFailure([&] { AgcDriver::Graphics::DecodeState(moved); }, "cannot read the ancillary word or, unless every sample sits at the pixel centre"); + moved.context[0x293] = moved.context.at(0x293) | 0x10000u; + Require(AgcDriver::Graphics::DecodeState(moved).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target whose shader the guest runs per sample was refused for reading ") + what); + if (inputs == 0x2002u) { + expectFailure([&] { AgcDriver::Graphics::DecodeState(inputState(changed, inputs, position)); }, "cannot read the ancillary word"); + continue; + } + Require(AgcDriver::Graphics::DecodeState(inputState(changed, inputs, position)).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target raised from a one-sample rasterizer, every sample at the pixel centre, was refused for reading ") + what); + } + for (const auto& [inputs, position, what] : {InputCase{0x0302u, 0u, "POS_X_FLOAT and POS_Y_FLOAT at the centre"}, InputCase{0x0302u, 0x100000u, "POS_X_FLOAT and POS_Y_FLOAT at the upper-left corner"}, InputCase{0x9022u, 0u, "PERSP_CENTER, LINEAR_CENTER, FRONT_FACE and POS_FIXED_PT"}}) { + Require(AgcDriver::Graphics::DecodeState(inputState(rasterized, inputs, position)).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target whose pixel-rate shader reads ") + what + " was refused"); + } const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); @@ -9016,8 +9046,9 @@ void unorm4_4_4_4TargetTests() { constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; auto merging = packedExportRequest(changed, multisampled, mergeCode); merging.context.pixel->targetMerge = merge; - const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample, spv::CapabilityInterpolationFunction}; merging.target.supportedCapabilities = capabilities; + merging.target.fragmentShaderBarycentricEnabled = true; const auto words = ShaderRecompiler::Recompile(merging).spirv.Copy(); std::size_t sampled = 0; bool sampleRate = false; @@ -9029,9 +9060,12 @@ void unorm4_4_4_4TargetTests() { } Require(sampleRate && sampled == 2u && interlockShape(words) == "capability extension ordered begin end ", "a 2-sample 4_4_4_4 merge module does not read and write the shaded sample inside a pixel interlock"); auto single = merging; - const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInterpolationFunction}; single.target.supportedCapabilities = noMultisampleStorage; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); + auto offCentre = merging; + offCentre.target.fragmentShaderBarycentricEnabled = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index d73689d528..8626dbc6ae 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -367,6 +367,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO program.Metadata().pixelIteratesSamples = iterated; const bool offsets = options.interpolationFunctions && options.fragmentShaderBarycentricEnabled; const bool centers = iterated && offsets; + if (ps->mergeSamples > 1u && !offsets && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { + throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, and without interpolation functions and fragment shader barycentrics its centre-located barycentrics would follow the sample"); + } const bool sampleOffsets = offsets && ps->SampleLocationsOffCenter(); const auto smoothSample = sampleOffsets ? StageInputKind::BaryCoordSmoothSample : StageInputKind::BaryCoordSmooth; const auto linearSample = sampleOffsets ? StageInputKind::BaryCoordNoPerspectiveSample : StageInputKind::BaryCoordNoPerspective; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 4868c8e36d..bef52a2754 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,7 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. Centre-located positions stay at the pixel centre, and so do centre-located inputs on devices with interpolation functions and fragment shader barycentrics (elsewhere they move to the sample position); centroid inputs, sample-located inputs and positions, and the sample number in the ancillary input follow the invocation's sample instead of the pixel's centroid and sample 0. A guest shader written for pixel-rate shading can see slightly different values there. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. For a shader the guest runs once per pixel this keeps only the inputs that every sample invocation of a pixel sees alike: centre-located barycentrics (rebuilt at the centre with interpolation functions and fragment shader barycentrics; without them the draw throws), positions at the centre or the upper-left corner, the fixed-point position and the front face, so each invocation reaches the colour and kill decision of the console's single invocation. The draw throws when such a shader reads the ancillary word, whose sample number would be the invocation's, or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample. Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. From d2a5895b8d1af74bec1eed1fc3b0f9b9b6f7c7dd Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:58:57 +0200 Subject: [PATCH 0143/1360] fix(agc): refuse 2-sample 4_4_4_4 merges of shaders that write memory A merge into a multisampled 4_4_4_4 target runs the pixel shader once per sample. A shader that stores to guest memory, runs atomics or uses GDS would then repeat those writes for every covered sample. An 8-sample pixel would add to a counter eight times. Such a merge now throws. The recompile request carries the merge's sample count, not whether the guest asked for per-sample shading, so this also refuses shaders the guest iterates itself, where the console repeats the writes too. A recompile test checks that a 2-sample merging shader adding to a buffer counter throws, and that the same shader without the merge compiles. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 16 ++++++++++++++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 3 +++ docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 35d32755ca..0ce922005a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9066,6 +9066,22 @@ void unorm4_4_4_4TargetTests() { auto offCentre = merging; offCentre.target.fragmentShaderBarycentricEnabled = false; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); + alignas(16) static std::array counted{}; + static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; + constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto counting = packedExportRequest(changed, multisampled, countingCode); + counting.context.userData = counter; + counting.context.pixel->targetMerge = merge; + counting.context.pixel->iteratesSamples = true; + counting.context.pixel->earlyZ = true; + counting.target = merging.target; + counting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample, spv::CapabilityInterpolationFunction, spv::CapabilityInt64}; + counting.target.supportedCapabilities = atomicCapabilities; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "each covered sample would repeat its writes"); + auto unmergedCounting = counting; + unmergedCounting.context.pixel->targetMerge = {}; + static_cast(ShaderRecompiler::Recompile(unmergedCounting)); } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 0f95c839c2..d1193cc5f7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -406,6 +406,9 @@ void DefineModule(SpirvEmitterState& state) { if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } + if (merge && pixel.mergeSamples > 1u && WritesGuestMemory(state.program)) { + throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, which a shader that writes guest memory (stores, atomics or GDS) cannot use: each covered sample would repeat its writes"); + } if (merge && !pixel.psEarlyZ && pixel.mergeDepthStencilTests && WritesGuestMemory(state.program)) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index bef52a2754..47e51606ff 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,7 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. For a shader the guest runs once per pixel this keeps only the inputs that every sample invocation of a pixel sees alike: centre-located barycentrics (rebuilt at the centre with interpolation functions and fragment shader barycentrics; without them the draw throws), positions at the centre or the upper-left corner, the fixed-point position and the front face, so each invocation reaches the colour and kill decision of the console's single invocation. The draw throws when such a shader reads the ancillary word, whose sample number would be the invocation's, or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample. Not measured on a PS5. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. From c21356d42adb8ad1ecbb350acdee8d341205806b Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:58:57 +0200 Subject: [PATCH 0144/1360] test(agc): draw two 2-sample 4_4_4_4 merges back to back The 2-sample test now draws two merges back to back with no read-back in between: an X merge over one triangle and a W merge over a crossing one. It then loads every sample. Samples inside both triangles must hold both nibbles, so the second merge has to read the word the first one wrote. Each merging draw takes the target's storage view before it records. That view ends any open render pass and records a shader-write barrier, so two merges never share a pass. Co-Authored-By: Claude Sonnet 5.5 --- ...PackedUnorm4444MultisamplePartialWrite.cpp | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index b5fd107ed5..14850a0815 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -56,6 +56,7 @@ constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; constexpr std::array Third{1.0f / 15.0f, 1.0f, 0.0f, 4.0f / 15.0f}; +constexpr std::array Fourth{0.0f, 7.0f / 15.0f, 0.0f, 11.0f / 15.0f}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -76,11 +77,15 @@ constexpr std::array, 3> Cutting{{ {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} }}; +constexpr std::array, 3> Crossing{{ + {-0.6f, -0.2f, 0.5f, 1.0f}, {0.9f, -0.85f, 0.5f, 1.0f}, {0.5f, 0.9f, 0.5f, 1.0f} +}}; + constexpr std::array, 3> Overlapping{{ {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 4> Programs{}; +alignas(256) std::array, 8> Programs{}; std::size_t programCount = 0; alignas(256) std::array Buffer{}; @@ -186,7 +191,7 @@ AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exporte const auto& color = decoded.colors.front(); char detail[256]; std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the 2-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); - Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? 0xf000u : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? Samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded 2-sample target exported another merge than its own surface"); AgcDriver::Graphics::State state{}; @@ -358,7 +363,36 @@ int main() { Require(checked >= Side * Side * Samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); - std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, and a full write drawn right after covered %u merged samples\n", pixelsByCoverage[1], overdrawn); + Draw(*device, full, Covering, Float32Program(First)); + const auto lowMerged = TargetState(block, 0x8u); + Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); + Draw(*device, merged, Cutting, Float32Program(Second)); + Draw(*device, lowMerged, Crossing, Float32Program(Fourth)); + const auto twice = LoadSamples(*device, texture); + std::uint32_t checkedTwice = 0; + std::uint32_t mergedTwice = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto high = Inside(Cutting, px, py); + const auto low = Inside(Crossing, px, py); + if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; + ++checkedTwice; + mergedTwice += high > 0 && low > 0 ? 1u : 0u; + auto expected = background; + if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); + if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); + const auto word = twice[(y * Side + x) * Samples + sample]; + char where[192]; + std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); + Require(word == expected, where); + } + } + } + Require(checkedTwice >= Side * Side * Samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); + std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", pixelsByCoverage[1], overdrawn, mergedTwice); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 69be01d8432cb87fc5a3c2fd276f5fa0e3eb13ed Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:36:17 +0200 Subject: [PATCH 0145/1360] fix(agc): keep centre barycentrics of 2-sample merges whose samples sit at the centre The recompiler refused centre-located barycentrics in a multisampled merge on a device without interpolation functions and fragment shader barycentrics. It did so even when every sample sits at the pixel centre, as in a draw raised from a one-sample rasterizer, where the barycentrics are the centre's anyway. The refusal now applies only when the merge's sample locations are off the centre. The pixel request carries those locations for such a merge. The recompile test runs on two states. On a 2-sample rasterizer, the missing barycentrics throw, and the shader compiles once they are present. On a raised draw, the shader compiles without them. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/Graphics/src/State.cpp | 3 ++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 15 ++++++++++++++- .../Translation/src/InstructionTranslator.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 827db079ca..338e39ae82 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -974,7 +974,8 @@ std::array PixelSampleLocations(const State& state, const Shad std::array locations{}; const bool sampleBarycentrics = (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample))) != 0u; const bool samplePosition = !state.perSampleShading && (pixel.posX || pixel.posY) && pixel.positionLocation == ShaderRecompiler::PixelPositionLocation::Sample; - if (!sampleBarycentrics && !samplePosition) return locations; + const bool mergedCentres = pixel.targetMerge.samples > 1u && (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter))) != 0u; + if (!sampleBarycentrics && !samplePosition && !mergedCentres) return locations; const auto samples = state.perSampleShading ? state.samples : 1u; for (std::uint32_t pixelIndex = 0; pixelIndex < 4u; ++pixelIndex) { for (std::uint32_t sample = 0; sample < samples; ++sample) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0ce922005a..fcba30f695 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9063,9 +9063,22 @@ void unorm4_4_4_4TargetTests() { const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInterpolationFunction}; single.target.supportedCapabilities = noMultisampleStorage; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); - auto offCentre = merging; + auto centred = merging; + centred.target.fragmentShaderBarycentricEnabled = false; + centred.context.pixel->sampleLocations = AgcDriver::Graphics::PixelSampleLocations(multisampled, *centred.context.pixel); + Require(std::ranges::all_of(centred.context.pixel->sampleLocations, [](std::uint8_t location) { return location == 0u; }), "a merge raised from a one-sample rasterizer did not keep every sample at the pixel centre"); + static_cast(ShaderRecompiler::Recompile(centred)); + const auto rasterizedState = AgcDriver::Graphics::DecodeState(rasterized); + auto offCentre = packedExportRequest(rasterized, rasterizedState, mergeCode); + offCentre.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(rasterizedState); + offCentre.context.pixel->iteratesSamples = rasterizedState.perSampleShading; + offCentre.context.pixel->sampleLocations = AgcDriver::Graphics::PixelSampleLocations(rasterizedState, *offCentre.context.pixel); + Require(std::ranges::any_of(offCentre.context.pixel->sampleLocations, [](std::uint8_t location) { return location != 0u; }), "a 2-sample rasterizer's merge did not pass its off-centre sample locations"); + offCentre.target.supportedCapabilities = capabilities; offCentre.target.fragmentShaderBarycentricEnabled = false; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); + offCentre.target.fragmentShaderBarycentricEnabled = true; + static_cast(ShaderRecompiler::Recompile(offCentre)); alignas(16) static std::array counted{}; static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 8626dbc6ae..bae160af93 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -367,7 +367,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO program.Metadata().pixelIteratesSamples = iterated; const bool offsets = options.interpolationFunctions && options.fragmentShaderBarycentricEnabled; const bool centers = iterated && offsets; - if (ps->mergeSamples > 1u && !offsets && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { + if (ps->mergeSamples > 1u && !offsets && ps->SampleLocationsOffCenter() && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, and without interpolation functions and fragment shader barycentrics its centre-located barycentrics would follow the sample"); } const bool sampleOffsets = offsets && ps->SampleLocationsOffCenter(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 47e51606ff..e52b45acc2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,7 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; off-centre samples make centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. From f5edd537e51b1bac4c2ce203e0509b54709bbe8a Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:46:10 +0200 Subject: [PATCH 0146/1360] test(agc): merge partial 4_4_4_4 writes into 4- and 8-sample targets The multisampled merge accepts 2, 4 and 8 samples, but the execution test drew only 2-sample targets. It now runs the same draws on 2, 4 and 8 samples, each count with its own target: a cutting merge under a full write, then an X and a W merge drawn back to back. Every sample is read back through a T# of the target's sample count and checked against the standard sample locations. A partly merged pixel is now any pixel with some, but not all, of its samples merged. A count whose R16_UINT colour attachment with storage use the device does not offer is skipped and printed. The test is skipped when the device offers none of the three. The debt entry on measured hardware now names the 4- and 8-sample merges. Co-Authored-By: Claude Sonnet 5.5 --- ...PackedUnorm4444MultisamplePartialWrite.cpp | 208 ++++++++++-------- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 118 insertions(+), 92 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index 14850a0815..894ccc375a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,11 +44,9 @@ using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Side = 16; -constexpr std::uint32_t Samples = 2; -constexpr std::uint32_t SampleShift = 1; +constexpr std::array SampleCounts{2, 4, 8}; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); -constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Threads = 32; @@ -85,10 +85,15 @@ constexpr std::array, 3> Overlapping{{ {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 8> Programs{}; +alignas(256) std::array, 24> Programs{}; std::size_t programCount = 0; alignas(256) std::array Buffer{}; +std::uint32_t MultisampledAttrib(std::uint32_t samples) { + const auto shift = static_cast(std::countr_zero(samples)); + return (shift << 12u) | (shift << 15u); +} + std::uint32_t Unorm(float value) { if (!(value > 0.0f)) return 0; if (value >= 1.0f) return 15; @@ -143,21 +148,21 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t samples) { AgcDriver::Registers cx; cx[0x318] = static_cast(block.Address() >> 8u); cx[0x31b] = 0; cx[0x31c] = Unorm4_4_4_4Info; - cx[0x31d] = MultisampledAttrib; + cx[0x31d] = MultisampledAttrib(samples); cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); cx[0x3b8] = TiledAttrib3; cx[0x390] = static_cast(block.Address() >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); - Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of 2 samples"); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of " + std::to_string(samples) + " samples"); return color; } -AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { +AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask, std::uint32_t samples) { AgcDriver::QueueState queue; queue.userConfig[0x242] = 4; queue.context = { @@ -169,7 +174,7 @@ AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { {0x8e, targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010}, {0x1c4, 0}, {0x1c5, Float32Export}, {0x1c3, 4}, {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, - {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib}, + {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib(samples)}, {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, {0xc, 0}, {0xd, (Side << 16u) | Side}, {0x81, 0x80000000}, {0x82, (Side << 16u) | Side}, @@ -185,15 +190,15 @@ AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { return queue; } -AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask) { +AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask, std::uint32_t samples) { const bool partial = exportedMask != 0xfu; - const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask)); + const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask, samples)); const auto& color = decoded.colors.front(); char detail[256]; - std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the 2-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); - Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the %u-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, samples, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); + Require(decoded.colors.size() == 1u && decoded.samples == samples && color.samples == samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); - Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? Samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded 2-sample target exported another merge than its own surface"); + Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded " + std::to_string(samples) + "-sample target exported another merge than its own surface"); AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; state.colors = decoded.colors; @@ -258,7 +263,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta device.Draw(state, draw, shaders); } -std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture) { +std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture, std::uint32_t samples) { std::vector userData(16, 0u); const auto address = reinterpret_cast(Buffer.data()); const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; @@ -274,13 +279,13 @@ std::vector LoadSamples(AgcDriver::VulkanDevice& device, const st }; request.useCache = false; const auto program = ShaderRecompiler::Recompile(request); - std::vector loaded(Side * Side * Samples); + std::vector loaded(Side * Side * samples); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { Buffer.fill(0xdeadbeefu); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto index = first + tid; - const auto pixel = index / Samples; - const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + const auto pixel = index / samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % samples}; std::copy(words.begin(), words.end(), &Buffer[tid * Words]); } device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); @@ -306,6 +311,90 @@ double Inside(const std::array, 3>& triangle, double x, dou return std::min({EdgeDistance(triangle, x, y, 0), EdgeDistance(triangle, x, y, 1), EdgeDistance(triangle, x, y, 2)}); } +void RunSamples(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t samples) { + const auto color = DecodeTarget(block, samples); + const auto full = TargetState(block, 0xfu, samples); + Draw(device, full, Covering, Float32Program(First)); + const auto merged = TargetState(block, 0x1u, samples); + Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); + Draw(device, merged, Cutting, Float32Program(Second)); + Draw(device, full, Overlapping, Float32Program(Third)); + const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); + const auto loaded = LoadSamples(device, texture, samples); + const auto background = Packed(First); + const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); + const auto over = Packed(Third); + Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); + const auto locations = AgcDriver::Graphics::SampleLocations(merged); + std::vector pixelsByCoverage(samples + 1u); + std::uint32_t checked = 0; + std::uint32_t overdrawn = 0; + std::uint32_t mergedAfterOverdraw = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto word = loaded[(y * Side + x) * samples + sample]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); + Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); + covered += word == written ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = Inside(Cutting, px, py); + const auto overDistance = Inside(Overlapping, px, py); + if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; + ++checked; + const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; + overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; + mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; + Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); + } + ++pixelsByCoverage[covered]; + } + } + const auto partlyMerged = std::accumulate(pixelsByCoverage.begin() + 1, pixelsByCoverage.end() - 1, 0u); + Require(checked >= Side * Side * samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[samples] != 0 && partlyMerged != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); + Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); + Draw(device, full, Covering, Float32Program(First)); + const auto lowMerged = TargetState(block, 0x8u, samples); + Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); + Draw(device, merged, Cutting, Float32Program(Second)); + Draw(device, lowMerged, Crossing, Float32Program(Fourth)); + const auto twice = LoadSamples(device, texture, samples); + std::uint32_t checkedTwice = 0; + std::uint32_t mergedTwice = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto high = Inside(Cutting, px, py); + const auto low = Inside(Crossing, px, py); + if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; + ++checkedTwice; + mergedTwice += high > 0 && low > 0 ? 1u : 0u; + auto expected = background; + if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); + if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); + const auto word = twice[(y * Side + x) * samples + sample]; + char where[192]; + std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); + Require(word == expected, where); + } + } + } + Require(checkedTwice >= Side * Side * samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); + std::printf("4_4_4_4 unorm %u-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", samples, partlyMerged, overdrawn, mergedTwice); +} + +bool SupportsSamples(const AgcDriver::Graphics::Context& context, std::uint32_t samples) { + VkImageFormatProperties properties{}; + const VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + return (context.limits.framebufferColorSampleCounts & samples) != 0u && context.imageFormatProperties(context.physical, VK_FORMAT_R16_UINT, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0u; +} + } int main() { @@ -318,81 +407,18 @@ int main() { std::printf("skipped, the device lacks %s\n", feature); return VulkanTestSkipped; } - Block block(4u * BlockBytes); - const auto color = DecodeTarget(block); - const auto full = TargetState(block, 0xfu); - Draw(*device, full, Covering, Float32Program(First)); - const auto merged = TargetState(block, 0x1u); - Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); - Draw(*device, merged, Cutting, Float32Program(Second)); - Draw(*device, full, Overlapping, Float32Program(Third)); - const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); - const auto loaded = LoadSamples(*device, texture); - const auto background = Packed(First); - const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); - const auto over = Packed(Third); - Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); - const auto locations = AgcDriver::Graphics::SampleLocations(merged); - std::array pixelsByCoverage{}; - std::uint32_t checked = 0; - std::uint32_t overdrawn = 0; - std::uint32_t mergedAfterOverdraw = 0; - for (std::uint32_t y = 0; y < Side; ++y) { - for (std::uint32_t x = 0; x < Side; ++x) { - std::uint32_t covered = 0; - for (std::uint32_t sample = 0; sample < Samples; ++sample) { - const auto word = loaded[(y * Side + x) * Samples + sample]; - char where[160]; - std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); - Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); - covered += word == written ? 1u : 0u; - const auto px = x + static_cast(locations[sample].x); - const auto py = y + static_cast(locations[sample].y); - const auto distance = Inside(Cutting, px, py); - const auto overDistance = Inside(Overlapping, px, py); - if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; - ++checked; - const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; - overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; - mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; - Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); - } - ++pixelsByCoverage[covered]; + std::vector> blocks; + std::uint32_t tested = 0; + for (const auto samples : SampleCounts) { + if (!SupportsSamples(context, samples)) { + std::printf("%u-sample R16_UINT storage color targets skipped, the device lacks them\n", samples); + continue; } + blocks.push_back(std::make_unique(4u * BlockBytes)); + RunSamples(*device, *blocks.back(), samples); + ++tested; } - Require(checked >= Side * Side * Samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); - Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); - Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); - Draw(*device, full, Covering, Float32Program(First)); - const auto lowMerged = TargetState(block, 0x8u); - Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); - Draw(*device, merged, Cutting, Float32Program(Second)); - Draw(*device, lowMerged, Crossing, Float32Program(Fourth)); - const auto twice = LoadSamples(*device, texture); - std::uint32_t checkedTwice = 0; - std::uint32_t mergedTwice = 0; - for (std::uint32_t y = 0; y < Side; ++y) { - for (std::uint32_t x = 0; x < Side; ++x) { - for (std::uint32_t sample = 0; sample < Samples; ++sample) { - const auto px = x + static_cast(locations[sample].x); - const auto py = y + static_cast(locations[sample].y); - const auto high = Inside(Cutting, px, py); - const auto low = Inside(Crossing, px, py); - if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; - ++checkedTwice; - mergedTwice += high > 0 && low > 0 ? 1u : 0u; - auto expected = background; - if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); - if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); - const auto word = twice[(y * Side + x) * Samples + sample]; - char where[192]; - std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); - Require(word == expected, where); - } - } - } - Require(checkedTwice >= Side * Side * Samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); - std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", pixelsByCoverage[1], overdrawn, mergedTwice); + if (tested == 0) return VulkanTestSkipped; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e52b45acc2..00b4d24034 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,7 +163,7 @@ - [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; off-centre samples make centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. -- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample merges and the 2-, 4- and 8-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. From abc07a1192c0bed708fc71a4e1f68f309dbffb2b Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:09:52 +0530 Subject: [PATCH 0147/1360] feat(libc): implement _Vacopy Copy the 24-byte System V argument list, writing the overflow pointer before the register offsets as the console's libc.prx does. Co-Authored-By: Claude Opus 5 (1M context) --- core/libs/prx/libc/src/RuntimeSupport.cpp | 9 +++++++ core/libs/tests/GuestStdioVariadic.cpp | 31 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 02058dc4af..54d3ed0104 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -95,6 +95,15 @@ void APS5_VABI cxa_finalize_nid_postfix(void* dsoHandle) { CxaFinalize_nid_no_patch(dsoHandle); } +void APS5_VABI _Vacopy_nid_postfix(unsigned char* destination, const unsigned char* source) { + std::uint64_t overflowArea; + std::memcpy(&overflowArea, source + 16, sizeof(overflowArea)); + std::memcpy(destination + 16, &overflowArea, sizeof(overflowArea)); + unsigned char offsets[16]; + std::memcpy(offsets, source, sizeof(offsets)); + std::memcpy(destination, offsets, sizeof(offsets)); +} + unsigned int APS5_VABI _Atomic_fetch_add_4_nid_postfix(volatile unsigned int* target, unsigned int value, int memoryOrder) { (void)memoryOrder; return GccAtomicFetchAdd(target, value); diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp index 62ead8be7f..7bc4ba7cdb 100644 --- a/core/libs/tests/GuestStdioVariadic.cpp +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -16,6 +16,7 @@ int APS5_VABI vfwprintf_nid_postfix(FileStream*, const char16_t*, void*); int APS5_VABI fwprintf_nid_postfix(FileStream*, const char16_t*, ...); int APS5_VABI vswscanf_nid_postfix(const char16_t*, const char16_t*, void*); int APS5_VABI vswscanf_s_nid_postfix(const char16_t*, const char16_t*, void*); +void APS5_VABI _Vacopy_nid_postfix(void*, const void*); } #ifdef _WIN32 @@ -67,6 +68,36 @@ static int APS5_VABI ScanWideSized(const char16_t* input, const char16_t* format return result; } +static long APS5_VABI SumThroughCopy(int count, ...) { + APS5_VA_BEGIN(count); + unsigned char before[24]; + std::memcpy(before, &args, sizeof(before)); +#ifdef _WIN32 + __builtin_sysv_va_list copy; +#else + std::va_list copy; +#endif + _Vacopy_nid_postfix(©, &args); + Require(std::memcmp(©, &args, sizeof(before)) == 0, "_Vacopy copies all 24 bytes"); + long sum = 0; + for (int index = 0; index < count; ++index) sum += __builtin_va_arg(copy, long) * (index + 1); + Require(std::memcmp(&args, before, sizeof(before)) == 0, "_Vacopy leaves the source untouched"); + for (int index = 0; index < count; ++index) sum -= __builtin_va_arg(args, long); + APS5_VA_END(); + return sum; +} + +static void CheckVacopy() { + long expected = 0; + for (long index = 1; index <= 9; ++index) expected += index * 100 * index - index * 100; + Require(SumThroughCopy(9, 100L, 200L, 300L, 400L, 500L, 600L, 700L, 800L, 900L) == expected, "_Vacopy copy reads every argument"); + unsigned char overlap[32]; + for (int index = 0; index < 32; ++index) overlap[index] = static_cast(index); + _Vacopy_nid_postfix(overlap + 8, overlap); + const unsigned char shifted[32] = {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; + Require(std::memcmp(overlap, shifted, sizeof(overlap)) == 0, "_Vacopy copies the overflow area before the offsets"); +} + static std::string Contents(FileStream& stream) { std::fflush(stream.GetHandle()); std::rewind(stream.GetHandle()); From c7032e6acf8c1e5c32e3be15e6b636d923cabea9 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:38:50 +0200 Subject: [PATCH 0148/1360] fix(libSceNet): retire invalidated epoll registrations --- core/libs/prx/libSceNet/CMakeLists.txt | 15 ++ core/libs/prx/libSceNet/Export.cpp | 79 +++++----- core/libs/tests/NetEpollLifetime.cpp | 192 +++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 4 files changed, 253 insertions(+), 34 deletions(-) create mode 100644 core/libs/tests/NetEpollLifetime.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index cdab277419..b9b461a8a5 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -49,4 +49,19 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(net_epoll_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/NetEpollLifetime.cpp) + target_include_directories(net_epoll_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_epoll_lifetime_tests PRIVATE libkernel libc) + configure_windows_unwind(net_epoll_lifetime_tests) + if(WIN32) + target_link_libraries(net_epoll_lifetime_tests PRIVATE ws2_32) + endif() + foreach(operation IN ITEMS reuse close delete modify replace abort destroy) + add_test(NAME net_epoll_lifetime_${operation} COMMAND net_epoll_lifetime_tests ${operation}) + set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index bd4752bdb6..225feba9c8 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -320,7 +320,8 @@ HostMutex g_mutex; HostCondition g_cv; std::map g_socks; std::set g_epolls; -std::map> g_epoll_socks; +using EpollRegistrations = std::map>; +std::map g_epoll_socks; std::set g_epoll_aborted; std::map g_pools; @@ -836,6 +837,7 @@ int APS5_VABI sceNetSocketClose(int s) { if (it != g_socks.end()) { native = it->second.native; g_socks.erase(it); + for (auto& epoll : g_epoll_socks) epoll.second.erase(s); found = true; } } @@ -1539,7 +1541,7 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { std::lock_guard lk(g_mutex); const int id = g_next_epoll++; g_epolls.insert(id); - g_epoll_socks.emplace(id, std::map{}); + g_epoll_socks.emplace(id, EpollRegistrations{}); return id; } @@ -1607,13 +1609,14 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e if (op != NET_EPOLL_CTL_DEL && g_socks.count(id) == 0) return fail(NET_EBADF); auto& registrations = g_epoll_socks[eid]; if (op == NET_EPOLL_CTL_ADD) { - if (!registrations.emplace(id, *event).second) return fail(NET_EADDRINUSE); + if (registrations.count(id) != 0) return fail(NET_EADDRINUSE); + registrations.emplace(id, std::make_shared(*event)); } else if (op == NET_EPOLL_CTL_DEL) { if (registrations.erase(id) == 0) return fail(NET_ENOENT); } else { auto registration = registrations.find(id); if (registration == registrations.end()) return fail(NET_ENOENT); - registration->second = *event; + registration->second = std::make_shared(*event); } g_cv.NotifyAll(); return 0; @@ -1621,12 +1624,16 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) { if (!events || maxevents <= 0) return fail(NET_EINVAL); + struct Watch { + int descriptor; + std::shared_ptr registration; + std::shared_ptr native; + }; const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); for (;;) { - std::vector registered_events; - std::vector> native_sockets; + std::vector watches; { std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); @@ -1636,12 +1643,11 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (const auto& entry : registration->second) { const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; - registered_events.push_back({entry.second.events, 0, static_cast(entry.first), entry.second.data}); - native_sockets.push_back(socket->second.native); + watches.push_back({entry.first, entry.second, socket->second.native}); } } } - if (registered_events.empty()) { + if (watches.empty()) { if (timeout == 0) return 0; std::unique_lock lk(g_mutex); const auto now = std::chrono::steady_clock::now(); @@ -1653,18 +1659,18 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int } #ifdef _WIN32 - std::vector descriptors(registered_events.size()); + std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; - if (registered_events[index].events & 1) descriptors[index].events |= POLLRDNORM; - if (registered_events[index].events & 2) descriptors[index].events |= POLLWRNORM; + descriptors[index].fd = watches[index].native->value; + if (watches[index].registration->events & 1) descriptors[index].events |= POLLRDNORM; + if (watches[index].registration->events & 2) descriptors[index].events |= POLLWRNORM; } #else - std::vector descriptors(registered_events.size()); + std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; - if (registered_events[index].events & 1) descriptors[index].events |= POLLIN; - if (registered_events[index].events & 2) descriptors[index].events |= POLLOUT; + descriptors[index].fd = watches[index].native->value; + if (watches[index].registration->events & 1) descriptors[index].events |= POLLIN; + if (watches[index].registration->events & 2) descriptors[index].events |= POLLOUT; } #endif const auto now = std::chrono::steady_clock::now(); @@ -1685,27 +1691,32 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); if (g_epoll_aborted.erase(eid) != 0) return fail(NET_ECONNABORTED); - } - if (ready_count > 0) { - int count = 0; - for (std::size_t index = 0; index < descriptors.size() && count < maxevents; ++index) { - const auto native_events = descriptors[index].revents; - if (!native_events) continue; - std::uint32_t guest_events = 0; + if (ready_count > 0) { + int count = 0; + const auto& registrations = g_epoll_socks.at(eid); + for (std::size_t index = 0; index < descriptors.size() && count < maxevents; ++index) { + const auto registration = registrations.find(watches[index].descriptor); + if (registration == registrations.end() || registration->second != watches[index].registration) continue; + const auto native_events = descriptors[index].revents; + if (!native_events) continue; + std::uint32_t guest_events = 0; #ifdef _WIN32 - if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; - if (native_events & POLLWRNORM) guest_events |= 2; + if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; + if (native_events & POLLWRNORM) guest_events |= 2; #else - if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; - if (native_events & POLLOUT) guest_events |= 2; + if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; + if (native_events & POLLOUT) guest_events |= 2; #endif - if (native_events & POLLERR) guest_events |= 8; - if (native_events & POLLHUP) guest_events |= 16; - events[count] = registered_events[index]; - events[count].events = guest_events; - ++count; + if (native_events & POLLERR) guest_events |= 8; + if (native_events & POLLHUP) guest_events |= 16; + events[count] = *watches[index].registration; + events[count].ident = static_cast(watches[index].descriptor); + events[count].reserved = 0; + events[count].events = guest_events; + ++count; + } + if (count > 0) return count; } - if (count > 0) return count; } if (timeout == 0) return 0; } diff --git a/core/libs/tests/NetEpollLifetime.cpp b/core/libs/tests/NetEpollLifetime.cpp new file mode 100644 index 0000000000..4a24847949 --- /dev/null +++ b/core/libs/tests/NetEpollLifetime.cpp @@ -0,0 +1,192 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +static int InterceptPoll(WSAPOLLFD* descriptors, ULONG count, INT timeout); +#define WSAPoll InterceptPoll +#else +#include +static int InterceptPoll(pollfd* descriptors, nfds_t count, int timeout); +#define poll InterceptPoll +#endif +#include "../prx/libSceNet/Export.cpp" +#ifdef _WIN32 +#undef WSAPoll +#else +#undef poll +#endif +#include +#include + +static void Require(bool value, const std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "requirement failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); +} + +struct PollGate { + std::promise arrived; + std::future released; + int result = 0; +}; + +static std::atomic pollGate{nullptr}; + +#ifdef _WIN32 +static int InterceptPoll(WSAPOLLFD* descriptors, ULONG count, INT timeout) { + const int result = ::WSAPoll(descriptors, count, timeout); +#else +static int InterceptPoll(pollfd* descriptors, nfds_t count, int timeout) { + const int result = ::poll(descriptors, count, timeout); +#endif + if (auto* gate = pollGate.exchange(nullptr)) { + gate->result = result; + gate->arrived.set_value(); + gate->released.wait(); + } + return result; +} + +class ReuseSlot { +public: +#ifndef _WIN32 + ReuseSlot() : descriptor(::open("/dev/null", O_RDONLY | O_CLOEXEC)) { Require(descriptor >= 0); } + ~ReuseSlot() { if (descriptor >= 0) ::close(descriptor); } + void Release() { Require(::close(descriptor) == 0); descriptor = -1; } +private: + int descriptor; +#else + void Release() {} +#endif +}; + +static constexpr std::uint64_t OldData = 0x123456789abcdef0; +static constexpr std::uint64_t NewData = 0xfedcba9876543210; + +static int Socket() { + const int descriptor = sceNetSocket("epoll-lifetime", 2, 2, 17); + Require(descriptor >= 0); + return descriptor; +} + +static std::array Bind(int descriptor) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(descriptor, address.data(), &size) == 0 && size == address.size()); + return address; +} + +static void Send(int sender, const std::array& address) { + const char payload[] = "epoll-lifetime"; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); +} + +static void Register(int epoll, int descriptor, int operation, std::uint64_t data, std::uint32_t events = 1) { + NetEpollEvent event{}; + event.events = events; + event.ident = 0xaaaaaaaaaaaaaaaa; + event.reserved = 0x55555555; + event.data.u64 = data; + Require(sceNetEpollControl(epoll, operation, descriptor, &event) == 0); +} + +static void CheckEvent(const NetEpollEvent& event, int descriptor, std::uint64_t data, std::uint32_t events = 1) { + Require(event.ident == static_cast(descriptor)); + Require(event.data.u64 == data && event.events == events && event.reserved == 0); +} + +static void CheckReuse() { + ReuseSlot slot; + const int original = Socket(); + const auto retained = g_socks.at(original).native; + const int sender = Socket(); + std::array epolls{sceNetEpollCreate("reuse-a", 0), sceNetEpollCreate("reuse-b", 0)}; + for (const int epoll : epolls) { + Require(epoll >= 0); + Register(epoll, original, 1, OldData); + } + Require(sceNetSocketClose(original) == 0); + slot.Release(); + const int replacement = Socket(); + Require(replacement == original); + Send(sender, Bind(replacement)); + for (const int epoll : epolls) { + NetEpollEvent event{}; + Require(sceNetEpollWait(epoll, &event, 1, 0) == 0); + Register(epoll, replacement, 1, NewData); + Require(sceNetEpollWait(epoll, &event, 1, 1000000) == 1); + CheckEvent(event, replacement, NewData); + Require(sceNetEpollDestroy(epoll) == 0); + } + Require(sceNetSocketClose(replacement) == 0 && sceNetSocketClose(sender) == 0); +} + +struct WaitResult { + int count = -1; + int error = 0; + NetEpollEvent event{}; +}; + +static void CheckWait(const std::string& operation) { + ReuseSlot slot; + int receiver = Socket(); + const int sender = Socket(); + Send(sender, Bind(receiver)); + const int epoll = sceNetEpollCreate("inflight", 0); + Require(epoll >= 0); + Register(epoll, receiver, 1, OldData); + std::promise release; + PollGate gate{{}, release.get_future()}; + auto arrived = gate.arrived.get_future(); + pollGate.store(&gate); + auto waiter = std::async(std::launch::async, [epoll] { + WaitResult result; + result.count = sceNetEpollWait(epoll, &result.event, 1, 200000); + result.error = *sceNetErrnoLoc(); + return result; + }); + Require(arrived.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + Require(gate.result == 1); + if (operation == "close") { + const int original = receiver; + Require(sceNetSocketClose(original) == 0); + slot.Release(); + receiver = Socket(); + Require(receiver == original); + Send(sender, Bind(receiver)); + Register(epoll, receiver, 1, NewData); + } else if (operation == "delete" || operation == "replace") { + Require(sceNetEpollControl(epoll, 3, receiver, nullptr) == 0); + if (operation == "replace") Register(epoll, receiver, 1, NewData); + } else if (operation == "modify") { + Register(epoll, receiver, 2, NewData, 2); + } else if (operation == "abort") { + Require(sceNetEpollAbort(epoll, 0) == 0); + } else { + Require(operation == "destroy"); + Require(sceNetEpollDestroy(epoll) == 0); + } + release.set_value(); + Require(waiter.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + const auto result = waiter.get(); + if (operation == "delete") { + Require(result.count == 0); + } else if (operation == "abort" || operation == "destroy") { + Require(result.count < 0 && result.error == (operation == "abort" ? 53 : 9)); + } else { + Require(result.count == 1); + CheckEvent(result.event, receiver, NewData, operation == "modify" ? 2 : 1); + } + if (operation != "destroy") Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); +} + +int main(int argc, char** argv) { + Require(argc == 2); + Require(sceNetInit_nid_postfix() == 0); + for (int iteration = 0; iteration != 3; ++iteration) { + if (std::string(argv[1]) == "reuse") CheckReuse(); + else CheckWait(argv[1]); + } + std::printf("epoll lifetime %s: 3 rounds passed\n", argv[1]); +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a091bd2810..c59ef8a00f 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -191,6 +191,7 @@ - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [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 +- [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `sceNetRecv` blocked, and a `sceNetSend` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From 89cc8368df7e1ca5e42d9018ed713b02ef6f1da3 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:35:19 +0100 Subject: [PATCH 0149/1360] perf(agc): park freed snapshot buffers in a bounded spare pool --- .../Graphics/src/ShaderResources.cpp | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 68b4c1b480..d0bb065ef3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -120,13 +120,9 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; - std::vector carried; - std::uint64_t carriedPresent = 0; std::uint64_t surfaceBytes = 0; }; -constexpr std::uint64_t CarriedBufferMaxPresents = 16; - // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the // eviction takes the back. APS5_NO_TEXTURE_HASH=1 finds entries by scanning the list (the index is // still kept), the linear lookup of before. @@ -135,6 +131,8 @@ struct TextureCache { std::list entries; std::unordered_map::iterator, TextureKeyHash> index; std::unordered_multimap::iterator> views; + std::vector> spare; + std::uint64_t spareBytes = 0; std::uint64_t bytes = 0; std::size_t passed = 0; std::chrono::steady_clock::time_point passedSince{}; @@ -269,6 +267,33 @@ void touchTexture(TextureCache& cache, std::list::iterator it) { it->present = Recorder::Presents(); } +constexpr std::uint64_t SpareSnapshotBytes = 512ull << 20; + +void parkSnapshot(TextureCache& cache, std::vector& bytes) { + const auto size = bytes.size(); + if (size == 0) return; + while (!cache.spare.empty() && cache.spareBytes + size > SpareSnapshotBytes) { + cache.spareBytes -= cache.spare.front().size(); + cache.spare.erase(cache.spare.begin()); + } + if (cache.spareBytes + size <= SpareSnapshotBytes) { + cache.spareBytes += size; + cache.spare.push_back(std::move(bytes)); + } + bytes = std::vector{}; +} + +std::vector spareSnapshot(TextureCache& cache, std::size_t bytes) { + for (auto spare = cache.spare.begin(); spare != cache.spare.end(); ++spare) { + if (spare->size() != bytes) continue; + cache.spareBytes -= bytes; + std::vector taken = std::move(*spare); + cache.spare.erase(spare); + return taken; + } + return {}; +} + bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t incoming, std::uint64_t budget) { if (cache.bytes + incoming <= budget) return true; if (const auto now = std::chrono::steady_clock::now(); now - cache.passedSince >= std::chrono::seconds(1)) { @@ -857,7 +882,6 @@ std::shared_ptr cachedTexture(const Context& context, std::span recycled; if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { - if (!it->carried.empty() && Recorder::Presents() - it->carriedPresent > CarriedBufferMaxPresents) it->carried = std::vector{}; // The view follows the storage image, whatever the GPU wrote to it since; guest memory // written meanwhile is taken in by refreshing the image. A fast clear the image cannot // see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear. @@ -903,15 +927,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes.size() == bytes) recycled = std::move(it->bytes); - else if (it->carried.size() == bytes && Recorder::Presents() - it->carriedPresent <= CarriedBufferMaxPresents) recycled = std::move(it->carried); + else parkSnapshot(cache, it->bytes); eraseTexture(cache, it); ++cache.counts.stale; } + if (source != nullptr) parkSnapshot(cache, recycled); + else if (recycled.empty()) recycled = spareSnapshot(cache, bytes); CachedTexture entry{key, address, source != nullptr ? std::vector{} : (recycled.empty() ? std::vector(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration}; - if (source != nullptr) { - entry.carried = std::move(recycled); - entry.carriedPresent = Recorder::Presents(); - } entry.committed = std::move(committed); entry.surfaceBytes = bytes; if (source != nullptr) { @@ -949,7 +971,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); constexpr std::size_t SnapshotCompareBytes = 256u << 10u; - if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); + if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) parkSnapshot(cache, entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } const auto budget = sampledBudget(context, cache); @@ -1301,6 +1323,8 @@ void ClearCachedTextures(VkDevice device) { if (it->key.device == device) eraseTexture(sampled, it++); else ++it; } + sampled.spare.clear(); + sampled.spareBytes = 0; } auto& storage = StorageTextures(); std::vector> cleared; From eab9b23a5271b2ccecef1e257065c8e315831861 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Tue, 20 Oct 2026 14:54:02 -0300 Subject: [PATCH 0150/1360] test(relinker): execute the split 256-bit store The stub that splits an unaligned 256-bit store is checked through its bytes but never run, so nothing confirms that the two halves land where the original store would, including across a page boundary, which is the case the split exists for. The x86-64 execution section now runs the stub at three operand offsets and checks the 32 bytes it writes and the pages around them. --- .../codegen/tests/Amd64OnlyConverterTests.cpp | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 3e77c91db6..f4191551c5 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1903,6 +1903,40 @@ void mcommitExecution() { } munmap(code, 4096); } + +void wideStoreExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0xC5, 0xFC, 0x11, 0x00}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "256-bit store stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + alignas(32) std::uint8_t source[32]; + for (std::size_t index = 0; index < sizeof(source); ++index) source[index] = static_cast(index + 1); + auto* pages = static_cast(mmap(nullptr, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + require(pages != MAP_FAILED, "cannot map the destination pages"); + for (const std::size_t offset : {std::size_t{0}, std::size_t{1}, std::size_t{4080}}) { + std::memset(pages, 0, 8192); + auto* destination = pages + offset; + asm volatile( + "vmovups (%[source]), %%ymm0\n\t" + "sub $128, %%rsp\n\t" + "call *%[code]\n\t" + "add $128, %%rsp\n\t" + : + : [source] "r"(source), [code] "r"(code), "a"(destination) + : "memory", "ymm0"); + require(std::memcmp(destination, source, sizeof(source)) == 0, "256-bit store stub did not write both halves"); + require(std::all_of(pages, destination, [](const std::uint8_t value) { return value == 0; }), "256-bit store stub wrote before its operand"); + require(std::all_of(destination + sizeof(source), pages + 8192, [](const std::uint8_t value) { return value == 0; }), + "256-bit store stub wrote past its operand"); + } + munmap(pages, 8192); + munmap(code, 4096); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -1947,6 +1981,7 @@ void rdtscpExecution() { } #else void rdtscpExecution() {} +void wideStoreExecution() {} #endif void scannerZeroTail() { @@ -2005,6 +2040,7 @@ int main() { rosettaLegacyScalarCompares(); converterRosettaLegacyScalarCompare(); rdtscpExecution(); + wideStoreExecution(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); From e5df7a3083ae2806d3f26678fb479632d301fd33 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:28:44 +0200 Subject: [PATCH 0151/1360] fix(libc): leave the error flag alone on the fread overflow and keep the fwrite throw The console's libc.prx (read from PPSA21564 by the maintainer) returns from fread with EINVAL before it touches the stream, so ferror stays clear, and its fwrite has no overflow check at all. fread no longer sets the stream error flag, fwrite keeps throwing, and the test follows. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 4 ++-- core/libs/tests/FileStreams.cpp | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 845acb2ee4..fe5c961e6c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -432,7 +432,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File auto* handle = GetStreamHandle(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); - if (count > std::numeric_limits::max() / size) { errno = 22; stream->SetError(); return 0; } + if (count > std::numeric_limits::max() / size) { errno = 22; return 0; } const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fread: buffer address overflow"); const GuestArena::HostWrite destination(buffer, bytes); @@ -452,7 +452,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fwrite: null buffer"); - if (count > std::numeric_limits::max() / size) { errno = 22; stream->SetError(); return 0; } + if (count > std::numeric_limits::max() / size) throw std::overflow_error("fwrite: buffer size overflow"); const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fwrite: buffer address overflow"); GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, bytes); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index ba7279cfee..82ba06d385 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -141,12 +141,8 @@ int main(int argc, char** argv) { ExpectException([&] { fputs_nid_postfix(nullptr, stream); }); ExpectException([&] { fwrite_nid_postfix(nullptr, 1, sizeof(payload), stream); }); errno = 0; - Require(fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) != 0); - clearerr_nid_postfix(stream); - errno = 0; - Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) != 0); - clearerr_nid_postfix(stream); - Require(ferror_nid_postfix(stream) == 0); + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) == 0); + ExpectException([&] { fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream); }); errno = 0; Require(fseek_nid_postfix(stream, 0, -1) == -1); Require(errno == EINVAL); From c68ffcfd2157025fc734586432506fca2a752777 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:31:41 +0100 Subject: [PATCH 0152/1360] fix(libkernel): fail scePthreadMutexUnlock on a statically initialized mutex with EPERM scePthreadMutexUnlock resolved the slot with resolveMutex(mutex, false), which throws "Mutex is not initialized" while the slot still holds the static initializer (0) or the adaptive static initializer (1), so unlocking a statically initialized mutex that was never locked ended the process. FreeBSD 11.4's mutex_unlock_common returns EPERM for both without initializing the slot, and EINVAL for the destroyed marker. Load the slot once: the destroyed marker still returns 0x80020016, and the two initializers now return 0x80020001 and stay in the slot, so a later lock still creates the mutex. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 8 +++++--- core/libs/tests/GuestPthreadDestroyedObjects.cpp | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 876bd9ee20..4691a0371b 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -189,10 +189,12 @@ int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { } int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { - auto* current = resolveMutex(mutex, false); - if (!current) + if (!mutex) + throw std::invalid_argument("Mutex pointer is null"); + auto* current = std::atomic_ref(*mutex).load(std::memory_order_acquire); + if (current == destroyedMutex()) return SCE_KERNEL_ERROR_EINVAL; - if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) + if (!isInitializedMutex(current) || current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) return sceNotPermitted; bool released = true; if (current->_type == MutexType::Recursive) { diff --git a/core/libs/tests/GuestPthreadDestroyedObjects.cpp b/core/libs/tests/GuestPthreadDestroyedObjects.cpp index a9a041cfc0..ca35d7b099 100644 --- a/core/libs/tests/GuestPthreadDestroyedObjects.cpp +++ b/core/libs/tests/GuestPthreadDestroyedObjects.cpp @@ -19,6 +19,7 @@ int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001u); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); static constexpr int POSIX_EINVAL = 22; @@ -34,6 +35,21 @@ int main() { Require(pthread_mutex_lock_nid_postfix(&mutex) == POSIX_EINVAL); Require(mutex == reinterpret_cast(std::uintptr_t{2})); + PthreadMutex uninitialized = nullptr; + Require(scePthreadMutexUnlock(&uninitialized) == SCE_KERNEL_ERROR_EPERM); + Require(uninitialized == nullptr); + Require(scePthreadMutexLock(&uninitialized) == SCE_OK); + Require(scePthreadMutexUnlock(&uninitialized) == SCE_OK); + Require(scePthreadMutexUnlock(&uninitialized) == SCE_KERNEL_ERROR_EPERM); + Require(scePthreadMutexDestroy(&uninitialized) == SCE_OK); + + PthreadMutex adaptive = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexUnlock(&adaptive) == SCE_KERNEL_ERROR_EPERM); + Require(adaptive == reinterpret_cast(std::uintptr_t{1})); + Require(scePthreadMutexLock(&adaptive) == SCE_OK); + Require(scePthreadMutexUnlock(&adaptive) == SCE_OK); + Require(scePthreadMutexDestroy(&adaptive) == SCE_OK); + PthreadCond cond = nullptr; Require(scePthreadCondInit(&cond, nullptr, "destroyed") == SCE_OK); Require(scePthreadCondDestroy(&cond) == SCE_OK); From 4c150894766a0f8db1e6e65c0c862b5443f33724 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 07:34:01 +1300 Subject: [PATCH 0153/1360] style: address review of the scratch address operand change Format tests/FlatScratch.cpp, drop the doubled blank line before its CMake block and move the technical debt entry into the Functional list. Co-authored-by: Prophet069 <291514246+Prophet069@users.noreply.github.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 - .../prx/libSceAgcDriver/tests/FlatScratch.cpp | 146 +++++++++++++----- docs/dev/debt/shader.md | 5 +- 3 files changed, 109 insertions(+), 43 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 825078f002..d52937934a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3202,7 +3202,6 @@ if(BUILD_TESTING) endif() endif() - if(BUILD_TESTING) add_test_executable(agc_flat_scratch_tests tests/FlatScratch.cpp) target_link_libraries(agc_flat_scratch_tests PRIVATE shader_recompiler_agc_driver libc) diff --git a/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp index 4db6cb7c5b..73aef4bdf0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp @@ -1,48 +1,118 @@ -#include "Translation/TranslationContext.hpp" #include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" +#include "Translation/TranslationContext.hpp" #include #include #include using namespace ShaderRecompiler; -static unsigned tests=0; -#define Require(value) do { if(!(value)){std::fprintf(stderr,"flat scratch assertion line %u: %s\n",__LINE__,#value);throw std::runtime_error("flat scratch regression");}} while(false) -static RdnaInstruction Decode(unsigned seg,unsigned saddr,unsigned addr,unsigned offset=0,unsigned opcode=12){ - const std::array code{(0x37u<<26)|(opcode<<18)|(seg<<14)|offset,(2u<<24)|(saddr<<16)|addr}; - return DecodeRdnaFlat(0x154cu,code,0); +static unsigned tests = 0; +#define Require(value) \ + do { \ + if (!(value)) { \ + std::fprintf(stderr, "flat scratch assertion line %u: %s\n", __LINE__, \ + #value); \ + throw std::runtime_error("flat scratch regression"); \ + } \ + } while (false) +static RdnaInstruction Decode(unsigned seg, unsigned saddr, unsigned addr, + unsigned offset = 0, unsigned opcode = 12) { + const std::array code{(0x37u << 26) | (opcode << 18) | + (seg << 14) | offset, + (2u << 24) | (saddr << 16) | addr}; + return DecodeRdnaFlat(0x154cu, code, 0); } -static void Reject(unsigned seg,unsigned saddr,unsigned addr){try{(void)Decode(seg,saddr,addr);}catch(const std::runtime_error&){++tests;return;}throw std::runtime_error("invalid FLAT accepted");} -static void Scratch(unsigned saddr,unsigned addr,unsigned expectedVector,unsigned expectedScalar){ - auto op=Decode(1,saddr,addr,0xffcu); - Require(op.memorySegment==1 && op.memoryOffset==-4 && op.sourceCount==2); - Require(op.source0.kind==(expectedVector?RdnaOperandKind::VectorRegister:RdnaOperandKind::IntegerInlineConstant)); - Require(op.source1.kind==(expectedScalar?RdnaOperandKind::ScalarRegister:RdnaOperandKind::IntegerInlineConstant)); - if(expectedVector)Require(op.source0.reg==addr);else Require(op.source0.value==0); - if(expectedScalar)Require(op.source1.reg==saddr);else Require(op.source1.value==0); - IrProgram program;auto& block=program.CreateBlock();program.SetEntryBlock(block);TranslationContext context(program,block,256); - context.TranslateInstruction(op); - unsigned vectors=0,addressReads=0,destinationReads=0,scalars=0,loads=0,scratch=0; - for(auto* inst:block.Instructions()){ - if(inst->Opcode()==IrOpcode::GetVectorRegister){ - ++vectors;auto reg=inst->Arguments().at(0)->Register().index;Require(reg<256); - if(reg==addr)++addressReads; - if(reg==2)++destinationReads; +static void Reject(unsigned seg, unsigned saddr, unsigned addr) { + try { + (void)Decode(seg, saddr, addr); + } catch (const std::runtime_error &) { + ++tests; + return; } - if(inst->Opcode()==IrOpcode::GetScalarRegister)++scalars; - if(inst->Opcode()==IrOpcode::LoadAddressU32)++loads; - if(inst->Opcode()==IrOpcode::GetScratchResource)++scratch; - } - std::printf("SV/SS/ST saddr=%u addr=%u vector=%u scalar=%u loads=%u scratch=%u\n",saddr,addr,vectors,scalars,loads,scratch);std::fflush(stdout); - Require(vectors==expectedVector+1 && addressReads==expectedVector && destinationReads==1 && scalars==expectedScalar && loads==1 && scratch==1); - ++tests; + throw std::runtime_error("invalid FLAT accepted"); } -int main(){ +static void Scratch(unsigned saddr, unsigned addr, unsigned expectedVector, + unsigned expectedScalar) { + auto op = Decode(1, saddr, addr, 0xffcu); + Require(op.memorySegment == 1 && op.memoryOffset == -4 && + op.sourceCount == 2); + Require(op.source0.kind == (expectedVector + ? RdnaOperandKind::VectorRegister + : RdnaOperandKind::IntegerInlineConstant)); + Require(op.source1.kind == (expectedScalar + ? RdnaOperandKind::ScalarRegister + : RdnaOperandKind::IntegerInlineConstant)); + if (expectedVector) + Require(op.source0.reg == addr); + else + Require(op.source0.value == 0); + if (expectedScalar) + Require(op.source1.reg == saddr); + else + Require(op.source1.value == 0); + IrProgram program; + auto &block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(op); + unsigned vectors = 0, addressReads = 0, destinationReads = 0, scalars = 0, + loads = 0, scratch = 0; + for (auto *inst : block.Instructions()) { + if (inst->Opcode() == IrOpcode::GetVectorRegister) { + ++vectors; + auto reg = inst->Arguments().at(0)->Register().index; + Require(reg < 256); + if (reg == addr) + ++addressReads; + if (reg == 2) + ++destinationReads; + } + if (inst->Opcode() == IrOpcode::GetScalarRegister) + ++scalars; + if (inst->Opcode() == IrOpcode::LoadAddressU32) + ++loads; + if (inst->Opcode() == IrOpcode::GetScratchResource) + ++scratch; + } + std::printf( + "SV/SS/ST saddr=%u addr=%u vector=%u scalar=%u loads=%u scratch=%u\n", + saddr, addr, vectors, scalars, loads, scratch); + std::fflush(stdout); + Require(vectors == expectedVector + 1 && addressReads == expectedVector && + destinationReads == 1 && scalars == expectedScalar && loads == 1 && + scratch == 1); + ++tests; +} +int main() { - const std::array code{0xdc304000u,0x027d00ffu};auto actual=DecodeRdnaFlat(0x154cu,code,0); - Require(actual.destination.reg==2 && actual.source0.reg==255 && actual.source1.value==0);++tests; - Scratch(125,255,1,0);Scratch(125,254,1,0);Scratch(0,255,0,1);Scratch(105,255,0,1);Scratch(127,255,0,0); - Reject(0,125,255);Reject(2,125,255);Reject(2,127,255);Reject(1,108,0);Reject(3,0,0); - auto flat=Decode(0,125,254);Require(flat.source1.kind==RdnaOperandKind::VectorRegister && flat.source1.reg==255);++tests; - auto global=Decode(2,0,255);Require(global.source0.reg==255 && global.source1.kind==RdnaOperandKind::ScalarRegister);++tests; - try{const std::array truncated{0xdc304000u};(void)DecodeRdnaFlat(0,truncated,0);throw std::logic_error("truncated accepted");}catch(const std::out_of_range&){++tests;} - std::printf("PASS %u flat scratch decode/IR and invalid boundary cases\n",tests); + const std::array code{0xdc304000u, 0x027d00ffu}; + auto actual = DecodeRdnaFlat(0x154cu, code, 0); + Require(actual.destination.reg == 2 && actual.source0.reg == 255 && + actual.source1.value == 0); + ++tests; + Scratch(125, 255, 1, 0); + Scratch(125, 254, 1, 0); + Scratch(0, 255, 0, 1); + Scratch(105, 255, 0, 1); + Scratch(127, 255, 0, 0); + Reject(0, 125, 255); + Reject(2, 125, 255); + Reject(2, 127, 255); + Reject(1, 108, 0); + Reject(3, 0, 0); + auto flat = Decode(0, 125, 254); + Require(flat.source1.kind == RdnaOperandKind::VectorRegister && + flat.source1.reg == 255); + ++tests; + auto global = Decode(2, 0, 255); + Require(global.source0.reg == 255 && + global.source1.kind == RdnaOperandKind::ScalarRegister); + ++tests; + try { + const std::array truncated{0xdc304000u}; + (void)DecodeRdnaFlat(0, truncated, 0); + throw std::logic_error("truncated accepted"); + } catch (const std::out_of_range &) { + ++tests; + } + std::printf("PASS %u flat scratch decode/IR and invalid boundary cases\n", + tests); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b4cfb47d7f..8257fb1be5 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -165,13 +165,10 @@ - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. +- [GFX10 scratch address operands](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp) (shader recompiler) - `core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp` and `core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp` use the SV/SS/ST scratch operand widths described by LLVM FLATInstructions.td at 3eb867f9b63ae2881bbe3c25d3c12842f2bdc55e (operand pseudos L557-L630, GFX10 encoding L2973-L3038). Decoding and IR tests cover the one-dword VGPR offset; PS5 hardware address-result validation remains outstanding. Unsupported scalar registers still throw. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the driver compiled the begin instruction's `exec_lo == 0` as a test of the whole 64-bit mask, so the high half of a wave64 skipped its loop. The begin instruction now tests for an empty mask as `(exec_lo | exec_hi) == 0` rather than `exec_lo == 0 && exec_hi == 0`, and that driver compiles it correctly. The cause of the layered-volume failures was not found. - [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. - -### GFX10 scratch address operands - -`core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp` and `core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp` use the SV/SS/ST scratch operand widths described by LLVM FLATInstructions.td at 3eb867f9b63ae2881bbe3c25d3c12842f2bdc55e (operand pseudos L557-L630, GFX10 encoding L2973-L3038). Decoding and IR tests cover the one-dword VGPR offset; PS5 hardware address-result validation remains outstanding. Unsupported scalar registers still throw. From 1c9a522bef304eff3ec51d21b881101c411c6a38 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 10:07:59 -0400 Subject: [PATCH 0154/1360] fix(shader): decide resource branch conditions from their uniform operand Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 67 +++++++++++++++++++ .../Optimization/SrtWalker/SrtEvaluator.hpp | 1 + .../Optimization/SrtWalker/SrtTape.hpp | 2 +- .../Optimization/src/ResourceMaterializer.cpp | 2 +- .../src/SrtWalker/SrtEvaluator.cpp | 47 +++++++++++-- .../Optimization/src/SrtWalker/SrtTape.cpp | 26 ++++++- 6 files changed, 134 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 09ada8fddd..1d73fa90d0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -944,6 +944,72 @@ void verifyPureFlatSlots() { require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an incomplete plan was classified"); } +void verifyLaneMaskGuardedDescriptorSources() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + constexpr std::uint32_t Zero = 0x00000000u; + constexpr std::uint32_t NegativeZero = 0x80000000u; + constexpr std::uint32_t One = 0x3f800000u; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x43700000u, 0x43070000u, 0x3b888889u, 0x3bf2b9d6u, 0x3b088889u, 0x3b72b9d6u, 0x3f7f7777u, 0x3f7f0d46u}; + const std::array sampler{0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; + }; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xd40d006au, 0x00002080u, 0xbf860006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x24000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + + fill(invalid, Zero); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.samplers.size() == 1u, "lane-mask guarded sources: the image and its sampler were not planned"); + require(!plan->controlFlow.empty() && std::ranges::any_of(plan->controlFlow, [](const ResourceBlock& block) { return block.condition != nullptr; }), "lane-mask guarded sources: the VCC branch around the image kept no condition"); + for (const auto mode : {Zero, NegativeZero}) { + fill(invalid, mode); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "lane-mask guarded sources: the T# behind a branch no lane takes was not left null"); + } + fill(invalid, One); + { + AgcDriver::ShaderMemory memory({}); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "lane-mask guarded sources: an invalid T# behind a branch the lanes may take was accepted"); + } + fill(valid, One); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(valid.begin(), valid.end(), capture->snapshot.images[0].dwords.begin()), "lane-mask guarded sources: the T# behind a branch the lanes may take was not read"); + } +} + void verifyGuardedDescriptorSources() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -7444,6 +7510,7 @@ int main(int argc, char** argv) { verifyMayEvaluate(); verifyPureFlatSlots(); verifyGuardedDescriptorSources(); + verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); verifyImageTableKeys(); verifyImageTables(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index d666acd0f8..2fa129cb49 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -173,6 +173,7 @@ public: BasicEvaluator& operator=(const BasicEvaluator&) = delete; static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); + static bool MayDecide(const IrResourcePlan& program, IrValue* value); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp index 0dcb1a168d..2e833ba3e7 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp @@ -26,7 +26,7 @@ private: static std::shared_ptr Compile(const IrResourcePlan& plan); enum class Op : std::uint8_t { Fail, Const, UserData, ShaderBase, Read, ReadConstBuffer, ExtractHalf, AddCarry, ConstructU64, - ConvertF32U32, ConvertU32F32, FPMul32, FPTrunc32, FPIsNan32, FPOrdLessThanEqual32, FPOrdGreaterThanEqual32, + ConvertF32U32, ConvertU32F32, FPMul32, FPTrunc32, FPIsNan32, FPOrdLessThanEqual32, FPOrdGreaterThanEqual32, FPUnordLessThanEqual32, FPUnordGreaterThanEqual32, IAdd32, IAdd64, ISub32, ISub64, IMul32, IMul64, SMin32, UMin32, BitwiseAnd32, BitwiseAnd64, BitwiseOr32, BitwiseXor32, BitwiseNot32, ShiftLeftLogical32, ShiftLeftLogical64, ShiftRightLogical32, ShiftRightLogical64, ShiftRightArithmetic32, ShiftRightArithmetic64, BitFieldUExtract, BitFieldSExtract, BitFieldInsert, Select, IEqual32, INotEqual32, SLessThan32, ULessThan32, UGreaterThan32, diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..afd94b4e75 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -2281,7 +2281,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const PrepareImageModes(plan.info); plan.uniformFill = source.uniformFill; for (auto& block : plan.controlFlow) { - if (block.condition != nullptr && !Detail::Evaluator::MayEvaluate(source, block.condition)) block.condition = nullptr; + if (block.condition != nullptr && !Detail::Evaluator::MayDecide(source, block.condition)) block.condition = nullptr; } ownPlanValues(plan); const auto addSource = [&plan](std::uint32_t index) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 3369a7457a..1412970d44 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -289,6 +289,8 @@ std::optional evaluatedArguments(IrOpcode opcode) { case IrOpcode::FPMul32: case IrOpcode::FPOrdLessThanEqual32: case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: case IrOpcode::BitwiseAnd32: case IrOpcode::BitwiseAnd64: case IrOpcode::BitwiseOr32: @@ -447,6 +449,20 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* return known.at(value->Resolve()); } +template +bool BasicEvaluator::MayDecide(const IrResourcePlan& program, IrValue* value) { + if (value == nullptr) return false; + if (MayEvaluate(program, value)) return true; + value = value->Resolve(); + const auto decides = [&program, value](std::size_t index) { return value->Argument(index) != nullptr && MayDecide(program, value->Argument(index)); }; + switch (value->Opcode()) { + case IrOpcode::LogicalNot: return value->ArgumentCount() == 1u && decides(0); + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalOr: return value->ArgumentCount() == 2u && (decides(0) || decides(1)); + default: return false; + } +} + template bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& result) { std::uint64_t a = 0; @@ -599,6 +615,18 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul return true; } return false; + case IrOpcode::FPUnordLessThanEqual32: + if (binary()) { + result = !(Float32(a) > Float32(b)) ? 1u : 0u; + return true; + } + return false; + case IrOpcode::FPUnordGreaterThanEqual32: + if (binary()) { + result = !(Float32(a) < Float32(b)) ? 1u : 0u; + return true; + } + return false; case IrOpcode::BitwiseAnd32: if (binary()) { result = static_cast(a & b); @@ -774,17 +802,22 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul } return false; case IrOpcode::LogicalAnd: - if (binary()) { - result = (a != 0u) && (b != 0u) ? 1u : 0u; + case IrOpcode::LogicalOr: { + const bool decisive = inst.Opcode() == IrOpcode::LogicalOr; + const bool first = Arg(inst, 0, a); + if (first && (a != 0u) == decisive) { + result = decisive ? 1u : 0u; return true; } - return false; - case IrOpcode::LogicalOr: - if (binary()) { - result = (a != 0u) || (b != 0u) ? 1u : 0u; + const bool second = Arg(inst, 1, b); + if (second && (b != 0u) == decisive) { + result = decisive ? 1u : 0u; return true; } - return false; + if (!first || !second) return false; + result = decisive ? 0u : 1u; + return true; + } case IrOpcode::LogicalXor: if (binary()) { result = (a != 0u) != (b != 0u) ? 1u : 0u; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp index 681bec7693..13b3cad246 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -245,6 +245,8 @@ private: case IrOpcode::FPIsNan32: return operation(Op::FPIsNan32, value, context, depth, 1); case IrOpcode::FPOrdLessThanEqual32: return operation(Op::FPOrdLessThanEqual32, value, context, depth, 2); case IrOpcode::FPOrdGreaterThanEqual32: return operation(Op::FPOrdGreaterThanEqual32, value, context, depth, 2); + case IrOpcode::FPUnordLessThanEqual32: return operation(Op::FPUnordLessThanEqual32, value, context, depth, 2); + case IrOpcode::FPUnordGreaterThanEqual32: return operation(Op::FPUnordGreaterThanEqual32, value, context, depth, 2); case IrOpcode::BitwiseAnd32: return operation(Op::BitwiseAnd32, value, context, depth, 2); case IrOpcode::BitwiseAnd64: return operation(Op::BitwiseAnd64, value, context, depth, 2); case IrOpcode::BitwiseOr32: return operation(Op::BitwiseOr32, value, context, depth, 2); @@ -343,6 +345,24 @@ bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) } bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) const { + if (node.op == Op::LogicalAnd || node.op == Op::LogicalOr) { + const bool decisive = node.op == Op::LogicalOr; + std::uint64_t first = 0; + std::uint64_t second = 0; + const bool firstEvaluated = evaluate(frame, node.arguments[0], first); + if (firstEvaluated && (first != 0u) == decisive) { + result = decisive ? 1u : 0u; + return true; + } + const bool secondEvaluated = evaluate(frame, node.arguments[1], second); + if (secondEvaluated && (second != 0u) == decisive) { + result = decisive ? 1u : 0u; + return true; + } + if (!firstEvaluated || !secondEvaluated) return false; + result = decisive ? 0u : 1u; + return true; + } std::array operand{}; std::int32_t origin = -1; for (std::uint8_t index = 0; index < node.argumentCount; ++index) { @@ -426,6 +446,8 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con case Op::FPIsNan32: result = std::isnan(float32(a)) ? 1u : 0u; return true; case Op::FPOrdLessThanEqual32: result = float32(a) <= float32(b) ? 1u : 0u; return true; case Op::FPOrdGreaterThanEqual32: result = float32(a) >= float32(b) ? 1u : 0u; return true; + case Op::FPUnordLessThanEqual32: result = !(float32(a) > float32(b)) ? 1u : 0u; return true; + case Op::FPUnordGreaterThanEqual32: result = !(float32(a) < float32(b)) ? 1u : 0u; return true; case Op::IAdd32: result = static_cast(a + b); return true; case Op::IAdd64: result = a + b; return true; case Op::ISub32: result = static_cast(a - b); return true; @@ -485,8 +507,8 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con case Op::SLessThan32: result = std::bit_cast(static_cast(a)) < std::bit_cast(static_cast(b)) ? 1u : 0u; return true; case Op::ULessThan32: result = static_cast(a) < static_cast(b) ? 1u : 0u; return true; case Op::UGreaterThan32: result = static_cast(a) > static_cast(b) ? 1u : 0u; return true; - case Op::LogicalAnd: result = (a != 0u) && (b != 0u) ? 1u : 0u; return true; - case Op::LogicalOr: result = (a != 0u) || (b != 0u) ? 1u : 0u; return true; + case Op::LogicalAnd: + case Op::LogicalOr: return false; case Op::LogicalXor: result = (a != 0u) != (b != 0u) ? 1u : 0u; return true; case Op::LogicalNot: result = a == 0u ? 1u : 0u; return true; } From e5b66f2ddf245c61fdd85d2f58c305969b88ed14 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Tue, 20 Oct 2026 20:12:28 +0100 Subject: [PATCH 0155/1360] fix(libkernel): retry a failed thread context read during a raise GetThreadContext can fail on a target that was just suspended. A raise used to throw "cannot read the target thread context" at the first failure; it now resumes the target and tries again, within the existing 1 s retry limit, before throwing. SONIC SUPERSTARS hit the throw once in three runs. guest_raise_in_host_code prints the longest single raise. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libkernel/System/src/Exception.cpp | 4 +++- core/libs/tests/GuestRaiseInHostCode.cpp | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index de202af371..35b08bbd78 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -414,7 +414,9 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_SEGMENTS; if (!GetThreadContext(native, &delivery.context)) { ResumeThread(native); - throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + SwitchToThread(); + continue; } if (thread->waitCount.load(std::memory_order_seq_cst) > 0) { const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast(queued.get())) != 0; diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp index 955d4605b6..0a41f83ca8 100644 --- a/core/libs/tests/GuestRaiseInHostCode.cpp +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include #include #include #include @@ -54,8 +55,11 @@ int main() { Pthread writer = nullptr; Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "host writer") == 0); while (!started.load()) std::this_thread::yield(); + auto longest = std::chrono::steady_clock::duration::zero(); for (int raise = 1; raise <= Raises; ++raise) { + const auto raisedAt = std::chrono::steady_clock::now(); Require(sceKernelRaiseException(writer, SigUsr1) == 0); + longest = std::max(longest, std::chrono::steady_clock::now() - raisedAt); while (deliveries.load() < raise) std::this_thread::yield(); Require(std::fputc('y', shared) == 'y'); released.store(raise); @@ -64,6 +68,7 @@ int main() { Require(scePthreadJoin(writer, nullptr) == 0); Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); std::fclose(shared); + std::printf("longest raise: %.2f ms\n", std::chrono::duration(longest).count()); done.store(true); watchdog.join(); } From f956527f6739d47dd483f65b1a6eb73e8bd8dea6 Mon Sep 17 00:00:00 2001 From: maximaxi01 Date: Mon, 19 Oct 2026 18:32:57 -0400 Subject: [PATCH 0156/1360] fix(libSceFont): use current style for glyph and kerning scales --- .../prx/libSceFont/include/FontInternal.hpp | 4 +- core/libs/prx/libSceFont/src/Layout.cpp | 4 +- core/libs/prx/libSceFont/src/Open.cpp | 2 - core/libs/prx/libSceFont/src/Render.cpp | 20 ++++-- core/libs/prx/libSceFont/src/Style.cpp | 9 +-- core/libs/tests/GuestFont.cpp | 69 +++++++++++++++++++ 6 files changed, 87 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 8c68010f10..d6dfa76547 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -34,8 +34,6 @@ struct FontState { std::shared_ptr> faceData; FT_Face face = nullptr; std::vector fallbacks; - float scaleW = 16.0f; - float scaleH = 16.0f; std::map scriptLanguages; std::map typographicFeatures; int attribute = 0; @@ -134,7 +132,7 @@ void CachedStyleSetScalar(CachedStyle& style, float value); void ClearRenderOutputs(FontGlyphMetrics* metrics, FontRenderOutput* result); int ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); int ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); -int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle); +int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW = nullptr, float* usedScaleH = nullptr); int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); } diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index a59a3b5f2f..4755cdbe9d 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -365,7 +365,7 @@ int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, return SCE_FONT_OK; } -int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle) { +int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW, float* usedScaleH) { const auto clearMetrics = [&] { if (metrics) *metrics = {}; }; @@ -434,6 +434,8 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe metrics->Vertical.bearingX = static_cast(vertBearingX) / 64.0f * ratioX; metrics->Vertical.bearingY = static_cast(vertBearingY) / 64.0f * ratioY; metrics->Vertical.advance = static_cast(slot->metrics.vertAdvance) / 64.0f * ratioY; + if (usedScaleW) *usedScaleW = scaleW; + if (usedScaleH) *usedScaleH = scaleH; return finish(SCE_FONT_OK); } diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 21e0272990..0c44ecbb9e 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -397,8 +397,6 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon ReleaseFontLock(source, sourceLock); FontState& targetState = ResetState(target); if (FontState* sourceState = TryGetState(fontHandle)) { - targetState.scaleW = sourceState->scaleW; - targetState.scaleH = sourceState->scaleH; if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); for (const FallbackFace& fallback : sourceState->fallbacks) AddStateFallbackFace(targetState, fallback.data, fallback.subFontIndex); } diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 3f78fa635a..eb661bbf40 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -188,7 +188,12 @@ int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, st if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; *kerning = {}; const FontState* state = TryGetState(fontHandle); - if (state) LoadKerning(*state, state->scaleW, state->scaleH, preCode, code, kerning); + if (state) { + float scaleW = 0.0f; + float scaleH = 0.0f; + StyleStateGetScalePixel(&font->style, &scaleW, &scaleH); + LoadKerning(*state, scaleW, scaleH, preCode, code, kerning); + } ReleaseFontLock(font, fontLock); return SCE_FONT_OK; } @@ -373,8 +378,7 @@ int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code *pGlyph = nullptr; if (!fontHandle) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; - const FontState* state = TryGetState(fontHandle); - if (!state) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (!TryGetState(fontHandle)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; const std::uint8_t glyphForm = detail ? detail->glyph_form : 0; const std::uint8_t metricsForm = detail ? detail->metrics_form : 0; const std::uint16_t formOptions = detail ? detail->form_options : 0; @@ -385,7 +389,9 @@ int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code if ((glyphForm != 0 && metricsForm == 0) || glyphForm > 1 || metricsForm > 4) return SCE_FONT_ERROR_INVALID_PARAMETER; if (glyphMemory && glyphMemory->mem_kind != MEMORY_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; FontGlyphMetrics metrics{}; - const int rc = GetCharGlyphMetrics(fontHandle, code, &metrics, false); + float scaleW = 0.0f; + float scaleH = 0.0f; + const int rc = GetCharGlyphMetrics(fontHandle, code, &metrics, false, &scaleW, &scaleH); if (rc != SCE_FONT_OK) return rc; auto* generated = new (std::nothrow) GeneratedGlyph(); if (!generated) return SCE_FONT_ERROR_ALLOCATION_FAILED; @@ -395,13 +401,13 @@ int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code generated->glyph.flags = formOptions; generated->glyph.glyph_form = glyphForm; generated->glyph.metrics_form = metricsForm; - generated->glyph.em_size = ClampToU16(state->scaleH); + generated->glyph.em_size = ClampToU16(scaleH); generated->glyph.baseline = ClampToU16(metrics.Horizontal.bearingY); generated->glyph.height_px = ClampToU16(metrics.height); generated->glyph.origin_x = 0; generated->glyph.origin_y = generated->glyph.baseline; - generated->glyph.scale_x = state->scaleW; - generated->glyph.base_scale = state->scaleH; + generated->glyph.scale_x = scaleW; + generated->glyph.base_scale = scaleH; generated->glyph.memory = glyphMemory; generated->owner = fontHandle; generated->outline.outline_flags = generated->glyph.flags; diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index e4adb89793..10e7602467 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -135,14 +135,7 @@ int APS5_VABI sceFontGetTypographicDesign(FontHandle fontHandle, int typographic } int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h) { - return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { - const int changed = StyleStateSetScalePixel(&font->style, w, h); - if (FontState* state = TryGetState(fontHandle)) { - state->scaleW = w; - state->scaleH = h; - } - return changed; - }); + return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { return StyleStateSetScalePixel(&font->style, w, h); }); } int APS5_VABI sceFontSetScalePoint(FontHandle fontHandle, float w, float h) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 80184223b8..ed0df7d9bc 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,6 +1,7 @@ #include "prx/libSceFont/include/FontDriver.hpp" #include "tests/FontFixture.hpp" #include +#include #include #include #include @@ -30,6 +31,7 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, cons int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontOpenFontFile(FontLibrary, const char*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontOpenFontInstance(FontHandle, FontHandle, FontHandle*); int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); int APS5_VABI sceFontSetTypographicDesign(FontHandle, int, int); @@ -39,6 +41,9 @@ int APS5_VABI sceFontGetAttribute(FontHandle, int, int*); int APS5_VABI sceFontSetResolutionDpi(FontHandle, std::uint32_t, std::uint32_t); int APS5_VABI sceFontGetResolutionDpi(FontHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); +int APS5_VABI sceFontSetScalePoint(FontHandle, float, float); +int APS5_VABI sceFontGetScalePixel(FontHandle, float*, float*); +int APS5_VABI sceFontGlyphGetScalePixel(FontGlyph, float*, float*); int APS5_VABI sceFontBindRenderer(FontHandle, FontRenderer); int APS5_VABI sceFontUnbindRenderer(FontHandle); int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); @@ -198,6 +203,68 @@ static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } +static void CheckGlyphScale(FontHandle font, const FontMemory* memory, float expectedW, float expectedH) { + float width = 0.0f; + float height = 0.0f; + Require(sceFontGetScalePixel(font, &width, &height) == SCE_FONT_OK); + Require(width == expectedW && height == expectedH); + FontGlyph glyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); + Require(sceFontGlyphGetScalePixel(glyph, &width, &height) == SCE_FONT_OK); + if (width != expectedW || height != expectedH) { + std::fprintf(stderr, "Generated glyph scale %.3f x %.3f differs from font scale %.3f x %.3f\n", width, height, expectedW, expectedH); + } + Require(width == expectedW && height == expectedH); + Require(glyph->em_size == static_cast(std::lround(expectedH))); + Require(sceFontDeleteGlyph(memory, &glyph) == SCE_FONT_OK && glyph == nullptr); +} + +static void CheckPointAndDpiScale(FontHandle font, FontLibrary library, const FontMemory* memory, FontRenderer renderer) { + FontKerning kerning{}; + Require(sceFontSetScalePoint(font, 20.0f, 30.0f) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 40.0f, 60.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + Require(sceFontSetResolutionDpi(font, 216, 288) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 60.0f, 120.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -12.0f)); + Require(sceFontSetScalePoint(font, 50.0f, 40.0f) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 150.0f, 160.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -30.0f)); + Require(sceFontSetResolutionDpi(font, 0, 0) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 50.0f, 40.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -10.0f)); + Require(sceFontSetScalePixel(font, 100.0f, 75.0f) == SCE_FONT_OK); + Require(sceFontSetResolutionDpi(font, 144, 216) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 100.0f, 75.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); + Require(sceFontSetScalePoint(font, 20.0f, 30.0f) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 40.0f, 90.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + FontHandle instance = nullptr; + Require(sceFontOpenFontInstance(font, nullptr, &instance) == SCE_FONT_OK && instance != nullptr); + CheckGlyphScale(instance, memory, 40.0f, 90.0f); + Require(sceFontGetKerning(instance, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + Require(sceFontSetScalePoint(instance, 30.0f, 40.0f) == SCE_FONT_OK); + CheckGlyphScale(instance, memory, 60.0f, 120.0f); + CheckGlyphScale(font, memory, 40.0f, 90.0f); + Require(sceFontCloseFont(instance) == SCE_FONT_OK); + Require(sceFontBindRenderer(font, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); + const std::vector data = KerningFont(); + FontHandle fresh = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &fresh) == SCE_FONT_OK); + float width = 0.0f; + float height = 0.0f; + Require(sceFontGetScalePixel(fresh, &width, &height) == SCE_FONT_OK); + CheckGlyphScale(fresh, memory, width, height); + Require(sceFontCloseFont(fresh) == SCE_FONT_OK); + Require(sceFontSetResolutionDpi(font, 144, 144) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); +} + int main() { constexpr std::uint32_t SystemFontSet = 0x18070043u; const std::filesystem::path fontRoot = std::filesystem::temp_directory_path() / ("anyps5_guest_font-" + std::to_string(std::random_device{}())); @@ -421,6 +488,8 @@ int main() { Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + CheckPointAndDpiScale(font, library, &memory, renderer); + ParsedText parsed{"AV", 0, font}; FontTextSource source{}; Require(sceFontTextSourceInit(&source, parsed.text, 2, ParseText, &parsed) == SCE_FONT_OK); From 3f8f81e1752ae4c830296d8c78624b25ce2274eb Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:37:03 +0200 Subject: [PATCH 0157/1360] fix(libkernel): clamp Windows birth time when setting earlier timestamps --- core/libs/prx/libkernel/File/src/Stdio.cpp | 9 ++- core/libs/tests/GuestFileTimestamps.cpp | 83 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index a8124a0976..83d641b030 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -168,10 +168,13 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { errno = EINVAL; return -1; } - return SetFileTime(handle, nullptr, &access, &modified) ? 0 : File::WindowsFileTime::Failure(GetLastError()); + FILETIME creation{}; + if (!GetFileTime(handle, &creation, nullptr, nullptr)) return File::WindowsFileTime::Failure(GetLastError()); + const auto birth = CompareFileTime(&modified, &creation) < 0 ? &modified : nullptr; + return SetFileTime(handle, birth, &access, &modified) ? 0 : File::WindowsFileTime::Failure(GetLastError()); } static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times, bool followLink = true) { - const auto handle = CreateFileW(path.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + const auto handle = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | (followLink ? 0 : FILE_FLAG_OPEN_REPARSE_POINT), nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); const int result = SetTimes(handle, times); @@ -184,7 +187,7 @@ static int NativeFutimes(int descriptor, const KernelTimeval* times) { const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); if (original == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; } if (GetFileType(original) != FILE_TYPE_DISK) { errno = EINVAL; return -1; } - const auto handle = ReOpenFile(original, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0); + const auto handle = ReOpenFile(original, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); const int result = SetTimes(handle, times); CloseHandle(handle); diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 62750ec5f3..ec6ac22fe7 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -14,6 +14,7 @@ extern "C" { int APS5_VABI utimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI lutimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); +int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); @@ -60,6 +61,71 @@ static void CheckTimes(const std::string& path, int fd, const KernelTimeval* exp #endif } + +#ifdef _WIN32 +static std::uint64_t Ticks(const FILETIME& time) { + return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; +} + +static void CheckBirthtime(const std::string& path, int fd, std::uint64_t expected) { + FILETIME creation{}; + Require(GetFileTime(reinterpret_cast(_get_osfhandle(fd)), &creation, nullptr, nullptr), "native creation time"); + Require(Ticks(creation) == expected, "native birth time follows an earlier mtime and never advances"); + FileStat byPath{}, byFd{}; + Require(stat_nid_postfix(path.c_str(), &byPath) == 0, "stat birth time"); + Require(sceKernelFstat(fd, &byFd) == 0, "fstat birth time"); + for (const auto* status : {&byPath, &byFd}) { + Require(status->st_birthtim.tv_sec == static_cast(expected / 10000000) - 11644473600LL && + status->st_birthtim.tv_nsec == static_cast(expected % 10000000) * 100, "guest birth time matches native metadata"); + } +} + +template +static void CheckBirthtimeUpdates(const std::string& path, int fd, TUpdate update) { + const auto handle = CreateFileW(std::filesystem::path(path).c_str(), FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE, "open birth time fixture"); + constexpr std::uint64_t initial = 116444736000000000ULL + 2000000000ULL * 10000000 + 1234567; + const FILETIME creation{static_cast(initial), static_cast(initial >> 32)}; + Require(SetFileTime(handle, &creation, nullptr, nullptr), "seed native birth time"); + const KernelTimeval later[2]{{1900000000, 654321}, {2100000000, 123456}}; + Require(update(later) == 0, "update later modification time"); + CheckTimes(path, fd, later); + CheckBirthtime(path, fd, initial); + const KernelTimeval earlier[2]{{1800000000, 1}, {2000000000, 123456}}; + Require(update(earlier) == 0, "update modification time less than a microsecond before birth"); + CheckTimes(path, fd, earlier); + constexpr std::uint64_t lowered = initial - 7; + CheckBirthtime(path, fd, lowered); + const KernelTimeval equal[2]{{1700000000, 999999}, earlier[1]}; + Require(update(equal) == 0, "update equal modification time"); + CheckTimes(path, fd, equal); + CheckBirthtime(path, fd, lowered); + const KernelTimeval invalid[2]{{1600000000, 0}, {std::numeric_limits::max(), 0}}; + Require(update(invalid) != 0, "reject invalid modification time before changing birth"); + CheckTimes(path, fd, equal); + CheckBirthtime(path, fd, lowered); + FILETIME future{}; + GetSystemTimePreciseAsFileTime(&future); + const auto futureTicks = Ticks(future) + 600ULL * 10000000; + future = {static_cast(futureTicks), static_cast(futureTicks >> 32)}; + Require(SetFileTime(handle, &future, nullptr, nullptr), "seed future birth time"); + Require(CloseHandle(handle), "close birth time fixture"); + const auto before = std::chrono::system_clock::now(); + Require(update(nullptr) == 0, "current time lowers a future birth time"); + const auto after = std::chrono::system_clock::now(); + FILETIME nativeCreation{}, access{}, modified{}; + Require(GetFileTime(reinterpret_cast(_get_osfhandle(fd)), &nativeCreation, &access, &modified), "read current native times"); + Require(Ticks(nativeCreation) == Ticks(access) && Ticks(access) == Ticks(modified), "current times share one snapshot including lowered birth"); + CheckBirthtime(path, fd, Ticks(modified)); + FileStat status{}; + Require(sceKernelFstat(fd, &status) == 0, "fstat current birth time"); + const auto recorded = std::chrono::seconds(status.st_birthtim.tv_sec) + std::chrono::nanoseconds(status.st_birthtim.tv_nsec); + Require(recorded >= before.time_since_epoch() - std::chrono::seconds(1) && + recorded <= after.time_since_epoch() + std::chrono::seconds(1), "current birth time uses host clock"); +} +#endif + int main() { const auto root = std::filesystem::path("anyps5-file-times-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root), "create fixture directory"); @@ -68,6 +134,15 @@ int main() { const int fd = open_nid_postfix(path.c_str(), 0, 0); Require(fd >= 0, "open read-only descriptor"); +#ifdef _WIN32 + for (const auto update : {utimes_nid_postfix, lutimes_nid_postfix, sceKernelUtimes_nid_postfix}) { + CheckBirthtimeUpdates(path, fd, [&](const KernelTimeval* times) { return update(path.c_str(), times); }); + } + for (const auto update : {futimes_nid_postfix, sceKernelFutimes}) { + CheckBirthtimeUpdates(path, fd, [&](const KernelTimeval* times) { return update(fd, times); }); + } +#endif + const KernelTimeval first[2]{{1000000000, 123456}, {1000000001, 500000}}; Require(utimes_nid_postfix(path.c_str(), first) == 0, "utimes fractional timestamp"); CheckTimes(path, fd, first); @@ -145,6 +220,14 @@ int main() { Require(std::filesystem::create_directory(directory), "create timestamp directory"); const int directoryFd = open_nid_postfix(directory.c_str(), 0, 0); Require(directoryFd >= 0, "open directory descriptor"); +#ifdef _WIN32 + for (const auto update : {utimes_nid_postfix, lutimes_nid_postfix, sceKernelUtimes_nid_postfix}) { + CheckBirthtimeUpdates(directory, directoryFd, [&](const KernelTimeval* times) { return update(directory.c_str(), times); }); + } + for (const auto update : {futimes_nid_postfix, sceKernelFutimes}) { + CheckBirthtimeUpdates(directory, directoryFd, [&](const KernelTimeval* times) { return update(directoryFd, times); }); + } +#endif Require(utimes_nid_postfix(directory.c_str(), first) == 0, "directory utimes"); FileStat directoryStatus{}; Require(stat_nid_postfix(directory.c_str(), &directoryStatus) == 0 && Same(directoryStatus.st_mtim, first[1]), "directory stat precision"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e1ebad64ed..b4a2cae251 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -121,7 +121,7 @@ - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here -- [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond +- [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond. Birth-time clamping is not atomic with concurrent host timestamp updates and has not been measured on a console - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink`. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target From 3bdd68cb72d2367deea37817dc793100a8442523 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 21 Oct 2026 02:25:18 +0630 Subject: [PATCH 0158/1360] fix(agc): validate the exact indirect draw record range --- core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index f4a9dbeca6..b026291c63 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -844,7 +844,7 @@ DrawParameters resolveIndirectDraw(std::span packet, const indirect.drawIndexLocation = multi ? location(packet[4] & 0xffffu) : 0x280u; indirect.drawIndexEnabled = multi && (packet[4] >> 31u) != 0; indirect.indxOffset = 0; - require(indirect.count <= (std::numeric_limits::max() - indirect.arguments - indirect.recordBytes) / indirect.stride, "indirect draw record range overflow"); + require(indirect.RangeBytes() <= std::numeric_limits::max() - indirect.arguments, "indirect draw record range overflow"); // The counts are unknown here (the CP takes them from the record; NUM_INSTANCES is ignored). DrawParameters draw{0, 0, 0, 0, packet.back() & 0x20u, indexed, 0, 0}; if (!indexed) { From 17ffa02ef4a8ed565d91ef1a718904cf3d9f560f Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 21 Oct 2026 02:26:01 +0630 Subject: [PATCH 0159/1360] test(agc): cover indirect draw record range boundaries --- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 23279cc92d..6fa510c205 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -321,6 +321,45 @@ void testIndexedDraw() { expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "guest"); } +void testIndirectDrawRange() { + AgcDriver::QueueState state; + alignas(4) std::array indices{}; + state.indexBase = reinterpret_cast(indices.data()); + state.indexBufferSize = 1; + state.indexType = 1; + state.userConfig[0x24a] = 0; + for (const auto opcode : {0x24u, 0x25u, 0x2cu, 0x38u}) { + const bool indexed = opcode == 0x25 || opcode == 0x38; + const bool multi = opcode == 0x2c || opcode == 0x38; + const std::uint32_t recordBytes = indexed ? 20u : 16u; + const std::uint32_t initiator = indexed ? 0u : 2u; + state.drawIndirectBase = std::numeric_limits::max() - 3; + const auto wrapping = multi ? makePacket(opcode, {0, 0x280, 0x280, 0x280, 1, 0, 0, recordBytes, initiator}) + : makePacket(opcode, {0, 0x280, 0x280, initiator}); + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(wrapping, state); }, "record range overflow"); + for (const auto count : {1u, 3u, std::numeric_limits::max()}) { + if (!multi && count != 1) continue; + for (const auto stride : {recordBytes, 64u, 0xfffffffcu}) { + if (!multi && stride != recordBytes) continue; + const auto packet = multi ? makePacket(opcode, {0, 0x280, 0x280, 0x280, count, 0, 0, stride, initiator}) + : makePacket(opcode, {0, 0x280, 0x280, initiator}); + const auto bytes = static_cast(count - 1) * stride + recordBytes; + state.drawIndirectBase = (std::numeric_limits::max() - bytes) & ~std::uint64_t{3}; + const auto draw = AgcDriver::Pm4::ResolveDraw(packet, state); + check(draw.indirect.has_value() && draw.indirect->RangeBytes() == bytes, "indirect draw rejected or changed a fitting record range"); + state.drawIndirectBase += 4; + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "record range overflow"); + } + } + state.drawIndirectBase = std::numeric_limits::max() - 3; + if (multi) { + const auto empty = makePacket(opcode, {0, 0x280, 0x280, 0x280, 0, 0, 0, recordBytes, initiator}); + const auto draw = AgcDriver::Pm4::ResolveDraw(empty, state); + check(draw.indirect.has_value() && draw.indirect->RangeBytes() == 0, "empty indirect draw consumed a record"); + } + } +} + void testIndirectDraw() { check(AgcDriver::Pm4::AccessesMemory(0xc0032400u) && AgcDriver::Pm4::AccessesMemory(0xc0083800u), "indirect draws must synchronize guest memory"); check(AgcDriver::Pm4::UnsupportedReason(0xc0032400u).empty() && AgcDriver::Pm4::UnsupportedReason(0xc0082c00u).empty(), "indirect draws are rejected"); @@ -1886,6 +1925,7 @@ int main(int argc, char** argv) { testContextAndBases(); testIndexedDraw(); testAutoDraw(); + testIndirectDrawRange(); testIndirectDraw(); testIndexAttributesIndirect(); testMemory(); From 6dd3501fba885b468d1b17602dd794290975caf8 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 20 Oct 2026 23:06:19 +0300 Subject: [PATCH 0160/1360] test(agc): skip native FMASK without eight-sample color --- .../tests/execution/SampleMultisampleTarget.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index db7defe1ad..abe81b5503 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -870,7 +870,13 @@ int main(int argc, char** argv) { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; if (argc == 2 && std::string_view(argv[1]) == "--native-fmask") { - if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) return VulkanTestSkipped; + { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.storageImageMultisample || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0) { + std::puts("native FMASK skipped, the device does not render or store 8-sample color"); + return VulkanTestSkipped; + } + } NativeFmask(device); return 0; } From e6ec827647704182df065f5a5f07c99df519d3de Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:24:10 +0200 Subject: [PATCH 0161/1360] fix(libSceNet): reject invalid descriptors for epoll DEL --- core/libs/prx/libSceNet/CMakeLists.txt | 2 +- core/libs/prx/libSceNet/Export.cpp | 2 +- core/libs/tests/NetEpollLifetime.cpp | 20 ++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index b9b461a8a5..dcaedcc786 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -56,7 +56,7 @@ if(BUILD_TESTING) if(WIN32) target_link_libraries(net_epoll_lifetime_tests PRIVATE ws2_32) endif() - foreach(operation IN ITEMS reuse close delete modify replace abort destroy) + foreach(operation IN ITEMS reuse control close delete modify replace abort destroy) add_test(NAME net_epoll_lifetime_${operation} COMMAND net_epoll_lifetime_tests ${operation}) set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 225feba9c8..932d1fe26f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1606,7 +1606,7 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e } if (op != NET_EPOLL_CTL_ADD && op != NET_EPOLL_CTL_MOD && op != NET_EPOLL_CTL_DEL) return fail(NET_EINVAL); if (op != NET_EPOLL_CTL_DEL && !event) return fail(NET_EINVAL); - if (op != NET_EPOLL_CTL_DEL && g_socks.count(id) == 0) return fail(NET_EBADF); + if (g_socks.count(id) == 0) return fail(NET_EBADF); auto& registrations = g_epoll_socks[eid]; if (op == NET_EPOLL_CTL_ADD) { if (registrations.count(id) != 0) return fail(NET_EADDRINUSE); diff --git a/core/libs/tests/NetEpollLifetime.cpp b/core/libs/tests/NetEpollLifetime.cpp index 4a24847949..47d551e7e1 100644 --- a/core/libs/tests/NetEpollLifetime.cpp +++ b/core/libs/tests/NetEpollLifetime.cpp @@ -121,6 +121,25 @@ static void CheckReuse() { Require(sceNetSocketClose(replacement) == 0 && sceNetSocketClose(sender) == 0); } +static void CheckControl() { + const int descriptor = Socket(); + const int epoll = sceNetEpollCreate("control", 0); + Require(epoll >= 0); + Require(sceNetEpollControl(epoll, 3, descriptor, nullptr) == static_cast(0x80410102)); + Require(*sceNetErrnoLoc() == 2); + Register(epoll, descriptor, 1, OldData); + Require(sceNetSocketClose(descriptor) == 0); + NetEpollEvent event{}; + event.events = 1; + for (const int closed : {descriptor, -1}) { + for (const int operation : {3, 1, 2}) { + Require(sceNetEpollControl(epoll, operation, closed, operation == 3 ? nullptr : &event) == static_cast(0x80410109)); + Require(*sceNetErrnoLoc() == 9); + } + } + Require(sceNetEpollDestroy(epoll) == 0); +} + struct WaitResult { int count = -1; int error = 0; @@ -186,6 +205,7 @@ int main(int argc, char** argv) { Require(sceNetInit_nid_postfix() == 0); for (int iteration = 0; iteration != 3; ++iteration) { if (std::string(argv[1]) == "reuse") CheckReuse(); + else if (std::string(argv[1]) == "control") CheckControl(); else CheckWait(argv[1]); } std::printf("epoll lifetime %s: 3 rounds passed\n", argv[1]); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c59ef8a00f..00e049b6f6 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -191,7 +191,7 @@ - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [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 -- [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). Duplicate-descriptor alias lifetime is not modeled by this epoll backend. +- [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `sceNetRecv` blocked, and a `sceNetSend` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From b5a8335fe3c74801fe2acc4d931894f691980e66 Mon Sep 17 00:00:00 2001 From: CPritch Date: Tue, 20 Oct 2026 14:22:22 +0100 Subject: [PATCH 0162/1360] fix(agc): drop queued library prebuilds when the prebuild pool closes Driver::stop closes the preparation pools before the teardown pipeline cache save, and the prebuild pool built every queued library first. The prebuild pool now drops the tasks that have not started and waits only for the running one. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++-- .../src/Driver/Shaders/ShaderRegistry.cpp | 13 +++++++---- .../tests/execution/ShutdownThreads.cpp | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 53dbc67960..ce9448ffb6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2565,15 +2565,16 @@ if(BUILD_TESTING) endif() endforeach() add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) + add_test(NAME agc_driver_shutdown_threads_queued_prebuilds COMMAND agc_driver_shutdown_threads_tests --queued-prebuilds) add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held) add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_queued_prebuilds agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_queued_prebuilds agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b69bccd8fe..8b9b39afe0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1146,9 +1146,8 @@ public: Closing() = true; pools = {Existing(), ExistingPrebuild()}; } - for (auto* pool : pools) { - if (pool != nullptr) pool->Close(); - } + if (pools[0] != nullptr) pools[0]->Close(false); + if (pools[1] != nullptr) pools[1]->Close(true); } private: @@ -1204,11 +1203,17 @@ public: idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); } - void Close() { + void Close(bool dropQueued) { std::vector joined; + std::deque dropped; { std::unique_lock lock(mutex); closed = true; + if (dropQueued) { + dropped.swap(tasks); + for (const auto& task : dropped) unfinished.erase(task.sequence); + idle.notify_all(); + } idle.wait(lock, [&] { return tasks.empty() && running == 0; }); joined.swap(workers); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 8f835d52de..82c2f78612 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -76,6 +76,27 @@ void ClosingJoinsPreparation() { Require(ranHere && RegistrationPreparationThreads() == 0, "a closed preparation pool restarted after being joined"); } +void ClosingDropsQueuedPrebuilds() { + using namespace AgcDriver::DriverDetail; + std::promise started; + auto running = started.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + bool queuedRan = false; + SubmitLibraryPrebuild([&] { started.set_value(); opened.wait(); }); + for (int index = 0; index < 3; ++index) SubmitLibraryPrebuild([&] { queuedRan = true; }); + running.wait(); + auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); + const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + gate.set_value(); + const bool returned = closing.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + closing.get(); + Require(waited, "closing returned while a library prebuild was still running"); + Require(returned, "closing did not return once the running library prebuild ended"); + Require(!queuedRan, "closing ran the queued library prebuilds"); + Require(RegistrationPreparationThreads() == 0, "closing the library prebuilds left their thread running"); +} + void StoppingJoinsCompareHelpers() { using namespace AgcDriver::Graphics; const std::vector first(std::size_t{2} << 20u, std::byte{1}); @@ -168,6 +189,7 @@ int main(int argc, char** argv) { try { const std::string_view mode = argc > 1 ? argv[1] : ""; if (mode == "--joined") ClosingJoinsPreparation(); + else if (mode == "--queued-prebuilds") ClosingDropsQueuedPrebuilds(); else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); From c7710630426366803942567e6476a0cea10d9e04 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:53:20 +0200 Subject: [PATCH 0163/1360] test(libkernel): check the closed-descriptor EBADF of fstatfs on Windows too guest_fstatfs and guest_fstat_fault skipped the "closed descriptor fails with EBADF" checks on Windows (#ifndef _WIN32). Both pass there, so the guards are removed and the checks run on every host. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestFstatFault.cpp | 2 -- core/libs/tests/GuestFstatfs.cpp | 2 -- 2 files changed, 4 deletions(-) diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp index 4f0aa66bb9..06524f7df8 100644 --- a/core/libs/tests/GuestFstatFault.cpp +++ b/core/libs/tests/GuestFstatFault.cpp @@ -60,10 +60,8 @@ int main() { Require(close_nid_postfix(sockets[0]) == 0); Require(close_nid_postfix(sockets[1]) == 0); Require(close_nid_postfix(file) == 0); -#ifndef _WIN32 RequireFailure(file, GuestEbadf); RequireFailure(-1, GuestEbadf); -#endif std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestFstatfs.cpp b/core/libs/tests/GuestFstatfs.cpp index 0c86546ccb..26e06de298 100644 --- a/core/libs/tests/GuestFstatfs.cpp +++ b/core/libs/tests/GuestFstatfs.cpp @@ -124,9 +124,7 @@ int main() { RequireFailure(-1, GuestEbadf); Require(close_nid_postfix(file) == 0); -#ifndef _WIN32 RequireFailure(file, GuestEbadf); -#endif std::filesystem::remove_all(root); } From 703102d6cae9850e8a06fa58df321349e3ea885f Mon Sep 17 00:00:00 2001 From: CPritch Date: Tue, 20 Oct 2026 21:59:25 +0100 Subject: [PATCH 0164/1360] test(agc): a library prebuild waiter returns after closing drops the queued prebuilds --- .../tests/execution/ShutdownThreads.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 82c2f78612..19a6b4218d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -86,13 +86,23 @@ void ClosingDropsQueuedPrebuilds() { SubmitLibraryPrebuild([&] { started.set_value(); opened.wait(); }); for (int index = 0; index < 3; ++index) SubmitLibraryPrebuild([&] { queuedRan = true; }); running.wait(); + auto awaited = std::make_shared>(); + auto woken = awaited->get_future(); + std::thread([awaited] { + AwaitLibraryPrebuilds(); + awaited->set_value(); + }).detach(); + const bool waiting = woken.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; gate.set_value(); const bool returned = closing.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + const bool wokeUp = woken.wait_for(std::chrono::seconds(10)) == std::future_status::ready; closing.get(); + Require(waiting, "a waiter for the library prebuilds returned while one was still running"); Require(waited, "closing returned while a library prebuild was still running"); Require(returned, "closing did not return once the running library prebuild ended"); + Require(wokeUp, "a waiter for the library prebuilds did not return after closing dropped the queued ones"); Require(!queuedRan, "closing ran the queued library prebuilds"); Require(RegistrationPreparationThreads() == 0, "closing the library prebuilds left their thread running"); } From 0b9b519a5e72b3f807399541d6de5ce6b421bd9a Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:59:49 +0200 Subject: [PATCH 0165/1360] fix(agc): copy a sparse heap whose mirror cannot be made, as main does --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../Graphics/src/GuestBufferMemory.cpp | 9 ++- .../libSceAgcDriver/tests/BdaResources.cpp | 57 +++++++++++++++++++ .../prx/libSceAgcDriver/tests/BdaTests.hpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 17 ++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 95677e00d8..0132d5b0a8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2199,6 +2199,7 @@ if(BUILD_TESTING) add_test(NAME agc_heap_mirror_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-unwatch) add_test(NAME agc_sparse_heap_mirror_unwatch COMMAND agc_driver_graphics_tests --sparse-heap-mirror-unwatch) add_test(NAME agc_sparse_heap_mirror_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --sparse-heap-mirror-unwatch) + add_test(NAME agc_sparse_heap_mirror_budget COMMAND agc_driver_graphics_tests --sparse-heap-mirror-budget) add_test(NAME agc_heap_mirror_run_unwatch COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch) add_test(NAME agc_heap_mirror_run_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch) set_tests_properties(agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_SPACE_CACHE=1") diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index d1a1ee5a1e..73788cbfe2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4217,7 +4217,14 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: const auto* range = containingRange(lease, region.begin, neededEnd); if (range == nullptr || !range->releasable) return false; std::vector blocks; - auto mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); + std::shared_ptr mirror; + try { + mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); + } catch (const std::runtime_error& error) { + static std::atomic reported{0}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what()); + return false; + } if (mirror != nullptr && mirror->heap && !mirror->sparse) { std::vector whole{mirror.get()}; refreshHeapMirrors(whole, blocks); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d625cf165..4ecc50eb00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2914,6 +2914,63 @@ void RunSparseHeapMirrorUnwatchTests(const Context& context, const BdaTestAccess std::cout << "an address-based build keeps and refreshes a sparse heap's mirror once its range has left the write watch\n"; } +void RunSparseHeapMirrorBudgetTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t block = 65536; + constexpr std::size_t reserved = 128 * block; + constexpr std::size_t committed = 80 * block; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: sparse heaps past the heap mirror budget not tested\n"; + return; + } + auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(reserved, block)); + Require(guest != nullptr, "cannot allocate the sparse heap in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(guest, committed, PAGE_READWRITE, block); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: sparse heaps past the heap mirror budget not tested\n"; + return; + } + auto* guest = reserveSparse(reserved, committed); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved); +#endif + std::memset(guest, 0x5a, committed); + const auto base = reinterpret_cast(guest); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, reserved, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); + const auto before = MirrorCounters(); + { + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + const auto bytes = access.bytes(view.buffer); + Require(adjustment == 0 && view.range >= committed && bytes[view.offset + committed - 1] == std::byte{0x5a}, "a sparse heap past the heap mirror budget was not copied up to its last committed byte"); + bytes[view.offset + 8] = std::byte{0x77}; + memory.WriteBack(); + } + Require(guest[8] == std::byte{0x77}, "a store to a sparse heap copied past the heap mirror budget was not written back"); + const auto after = MirrorCounters(); + Require(after.sparseBinds == before.sparseBinds && after.heapMirrors == before.heapMirrors && after.heapBytes == before.heapBytes, "a sparse heap past the heap mirror budget was mirrored or left heap mirror bytes held"); + registry(false); + ClearImageMirrors(context.device); + ClearSnapshotCopies(context.device); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(guest, reserved); + GuestArena::GuestArenaRelease_nid_postfix(guest, reserved); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved); + releaseSparse(guest, reserved); +#endif + std::cout << "a sparse heap whose mirror would pass the heap mirror budget is copied\n"; +} + void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t block = 65536; constexpr std::size_t heapBytes = 32 * block; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 6aea082fff..49cd994d6e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -19,6 +19,7 @@ void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaT bool RunSparseBufferHoleTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool throughImport); void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); +void RunSparseHeapMirrorBudgetTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e28a78b241..50fc658663 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -10231,6 +10231,23 @@ int main(int argc, char** argv) { Require(mock.live == 0, "the sparse heap mirror unwatch test leaked Vulkan objects"); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sparse-heap-mirror-budget") { + { + mock = MockVulkan{}; + auto context = mockContext(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + RunSparseHeapMirrorBudgetTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, + {}, + {}, + {}, + {} + }); + } + Require(mock.live == 0, "the sparse heap mirror budget test leaked Vulkan objects"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--heap-mirror-run-unwatch") { { mock = MockVulkan{}; From e3d249fb05dc67dba2c3e8cf9ea029c70b375b3c Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:02:35 +0200 Subject: [PATCH 0166/1360] fix(libkernel): report ELOOP from unlink, mkdir, rmdir and truncate on Windows A path through a self-referencing symbolic link returned ENOTDIR, EIO or EINVAL on Windows; FreeBSD returns ELOOP. The failure paths now use the loop check that sceKernelStat uses. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ --- .../File/include/WindowsSymbolicLink.hpp | 8 ++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 9 ++++----- core/libs/prx/libkernel/File/src/Stdio.cpp | 17 ++++++++++++++++- core/libs/tests/GuestFilesystem.cpp | 10 ++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index cdc089e731..e3a9ebbd6a 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -108,6 +108,14 @@ public: } }; +inline bool WindowsSymbolicLinkLoop(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; + CloseHandle(handle); + return false; +} + } #endif diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 186ce63ff3..213cd0b534 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -114,11 +114,7 @@ static std::filesystem::path NativeMetadataPath(const std::filesystem::path& pat : std::filesystem::path(L"\\\\?\\" + native); } static bool NativeSymbolicLinkLoop(const std::filesystem::path& path) { - const auto handle = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; - CloseHandle(handle); - return false; + return File::WindowsSymbolicLinkLoop(path); } static int MapFlags(int sceFlags) { int f = 0; @@ -419,6 +415,9 @@ int APS5_VABI sceKernelUnlink(const char* path) { std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); if (NativeUnlink(native) != 0) { +#ifdef _WIN32 + if (NativeSymbolicLinkLoop(native.parent_path())) return SCE_KERNEL_ERROR_ELOOP; +#endif return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } RecordWrittenPath_nid_no_patch(native); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 976958be10..dd3e52cb78 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -55,6 +55,7 @@ static constexpr std::uint64_t KERNEL_MNT_NOSUID = 0x8; #ifdef _WIN32 #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #include #include #include @@ -919,6 +920,9 @@ int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeMkdir(native, mode) != 0) { const int nativeError = errno; +#ifdef _WIN32 + if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return PosixFailure(GUEST_ELOOP); +#endif const bool blockedByFile = (nativeError == ENOENT || nativeError == EINVAL) && HasNonDirectoryPrefix(native); return PosixResult(SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : nativeError)); } @@ -1183,6 +1187,9 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(WithoutTrailingSeparator(native).parent_path(), error)) { +#ifdef _WIN32 + if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); +#endif return SceErrorFromErrno(HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT); } if (!std::filesystem::create_directory(native, error)) return SceErrorFromErrno(error.value() ? error.value() : GUEST_EIO); @@ -1417,6 +1424,9 @@ int APS5_VABI sceKernelRmdir(const char* path) { if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(native, error)) { +#ifdef _WIN32 + if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); +#endif const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); } @@ -1471,7 +1481,12 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::exists(status)) return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); - if (const int failure = NativeTruncate(native, length)) return SceErrorFromErrno(failure); + if (const int failure = NativeTruncate(native, length)) { +#ifdef _WIN32 + if (File::WindowsSymbolicLinkLoop(native)) return SceKernelError(GUEST_ELOOP); +#endif + return SceErrorFromErrno(failure); + } RecordWrittenPath_nid_no_patch(native); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 07b86670f3..73b267b8f7 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -962,6 +962,16 @@ int main() { Require(checkRelativeLink(fileLink, 11)); Require(checkRelativeLink(directoryLink, 16)); Require(checkRelativeLink(danglingLink, 11)); + const auto belowStatLoop = statLoop.string() + "\\inside"; + Require(sceKernelUnlink(belowStatLoop.c_str()) == static_cast(0x8002003eu)); + Require(unlink_nid_postfix(belowStatLoop.c_str()) == -1 && *__error_nid_postfix() == 62); + Require(sceKernelMkdir(belowStatLoop.c_str(), 0700) == static_cast(0x8002003eu)); + Require(mkdir_nid_postfix(belowStatLoop.c_str(), 0700) == -1 && *__error_nid_postfix() == 62); + Require(sceKernelRmdir(belowStatLoop.c_str()) == static_cast(0x8002003eu)); + Require(rmdir_nid_postfix(belowStatLoop.c_str()) == -1 && *__error_nid_postfix() == 62); + Require(sceKernelRmdir(statLoop.string().c_str()) == static_cast(0x80020014u)); + Require(sceKernelTruncate_nid_postfix(statLoop.string().c_str(), 0) == static_cast(0x8002003eu)); + Require(truncate_nid_postfix(statLoop.string().c_str(), 0) == -1 && *__error_nid_postfix() == 62); FileStat followed{}; Require(lstat_nid_postfix(fileLink.string().c_str(), &linkStatus) == 0); Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); From a06dc199b73ddb0496144172ac731114e8a857cc Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:45:28 +0200 Subject: [PATCH 0167/1360] fix(libSceAudioPropagation): drop the SDK inline Init helpers from the export list The nine Init helpers are static inline in the SDK header and the bare sceAudioPropagationSystem name is in neither the header nor the stub, so the library no longer exports them. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAudioPropagation/CMakeLists.txt | 2 +- .../prx/libSceAudioPropagation/Export.cpp | 50 ------------------- core/libs/tests/GuestAudioPropagation.cpp | 23 ++++++++- docs/dev/debt/libSceAudioPropagation.md | 2 +- 4 files changed, 23 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/CMakeLists.txt b/core/libs/prx/libSceAudioPropagation/CMakeLists.txt index 29417b8962..0575687fe3 100644 --- a/core/libs/prx/libSceAudioPropagation/CMakeLists.txt +++ b/core/libs/prx/libSceAudioPropagation/CMakeLists.txt @@ -5,7 +5,7 @@ configure_windows_unwind(libSceAudioPropagation) if(BUILD_TESTING) add_test_executable(guest_audio_propagation_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudioPropagation.cpp) target_include_directories(guest_audio_propagation_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc) + target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_audio_propagation_tests) add_test(NAME guest_audio_propagation COMMAND guest_audio_propagation_tests) if(WIN32) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 474117fe6f..f7745e859b 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -345,56 +345,6 @@ int APS5_VABI sceAudioPropagationSystemQueryInfo() { return 0; } -int APS5_VABI sceAudioPropagationAudioPathInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationAudioPathPointInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationMaterialInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationPortalSettingsInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationRayInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSourceRenderInfoInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSourceSetAudioPathsParamInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSystemMemoryInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSystemOptionInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioPropagationReportRegisterHandler() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index c4d112d342..ed6fce8d11 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -6,10 +6,15 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + extern "C" { int APS5_VABI sceAudioPropagationPathGetNumPoints(); int APS5_VABI sceAudioPropagationSystemLock(); -int APS5_VABI sceAudioPropagationMaterialInit(); } namespace { @@ -33,6 +38,16 @@ void RequireThrows(TCall call, const char* message) { Require(false, message); } +void* Resolve(const char* name) { +#ifdef _WIN32 + const HMODULE module = GetModuleHandleA("libSceAudioPropagation.prx"); + Require(module != nullptr, "library module"); + return reinterpret_cast(GetProcAddress(module, name)); +#else + return dlsym(RTLD_DEFAULT, name); +#endif +} + AudioPropagationSystemMemory Memory() { AudioPropagationSystemMemory memory{}; memory.desc = {AudioPropagation::SystemMemoryId, AudioPropagation::SystemMemorySize}; @@ -244,7 +259,11 @@ int main() { RequireThrows([&] { sceAudioPropagationPathGetNumPoints(); }, "path get num points throw"); RequireThrows([&] { sceAudioPropagationSystemLock(); }, "system lock throw"); - RequireThrows([&] { sceAudioPropagationMaterialInit(); }, "material init throw"); + + Require(Resolve("sceAudioPropagationSystemCreate") != nullptr && Resolve("sceAudioPropagationSourceSetAudioPath") != nullptr, "export lookup"); + for (const char* name : {"sceAudioPropagationAudioPathInit", "sceAudioPropagationAudioPathPointInit", "sceAudioPropagationMaterialInit", "sceAudioPropagationPortalSettingsInit", "sceAudioPropagationRayInit", "sceAudioPropagationSourceRenderInfoInit", "sceAudioPropagationSourceSetAudioPathsParamInit", "sceAudioPropagationSystemMemoryInit", "sceAudioPropagationSystemOptionInit", "sceAudioPropagationSystem"}) { + Require(Resolve(name) == nullptr, name); + } std::puts("AudioPropagation tests passed"); return 0; diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 30802ce4c7..ae3167306d 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -8,4 +8,4 @@ - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. - [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 -- [libSceAudioPropagation firmware exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - `sceAudioPropagationPathGetNumPoints`, `sceAudioPropagationReportApi`, `sceAudioPropagationResetAttributes`, `sceAudioPropagationSourceQueryInfo`, `sceAudioPropagationSystemLock`, `sceAudioPropagationSystemQueryInfo`, `sceAudioPropagationAudioPathInit`, `sceAudioPropagationAudioPathPointInit`, `sceAudioPropagationMaterialInit`, `sceAudioPropagationPortalSettingsInit`, `sceAudioPropagationRayInit`, `sceAudioPropagationSourceRenderInfoInit`, `sceAudioPropagationSourceSetAudioPathsParamInit`, `sceAudioPropagationSystem`, `sceAudioPropagationSystemMemoryInit`, `sceAudioPropagationSystemOptionInit`, `sceAudioPropagationReportRegisterHandler`, `sceAudioPropagationReportUnregisterHandler`, `sceAudioPropagationSystemEnumHandles`, `sceAudioPropagationSystemUnlock` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceAudioPropagation firmware exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - `sceAudioPropagationPathGetNumPoints`, `sceAudioPropagationReportApi`, `sceAudioPropagationResetAttributes`, `sceAudioPropagationSourceQueryInfo`, `sceAudioPropagationSystemLock`, `sceAudioPropagationSystemQueryInfo`, `sceAudioPropagationReportRegisterHandler`, `sceAudioPropagationReportUnregisterHandler`, `sceAudioPropagationSystemEnumHandles`, `sceAudioPropagationSystemUnlock` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws From 8420e136885c5a20897c52372ecdeac568ac7dd3 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:40:07 +0200 Subject: [PATCH 0168/1360] fix(libSceNgs2): return INVALID_GEOM_FLAG and the listener-address error from sceNgs2GeomCalcListener A flags value above 1 returns 0x804A8463 and a null listener parameter returns 0x804A8461, both leaving the output untouched, instead of throwing. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceNgs2.native/include/Ngs2Types.hpp | 2 ++ core/libs/prx/libSceNgs2.native/src/Geom.cpp | 4 ++-- .../libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp | 15 ++++++++++++--- docs/dev/debt/libSceNgs2.native.md | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index a604adba3d..7d0bc05b67 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -17,6 +17,8 @@ static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0 static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); +static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); +static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast(0x804A8463); static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index 28f5d9f10c..9b68d818ef 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -41,9 +41,9 @@ Vector PerpendicularHint(const Vector& front) { } int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { - if (param == nullptr) APS5_INVALID_ARG_EX; + if (param == nullptr) return SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS; if (outWork == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - if (flags > 1) throw std::runtime_error("NGS2: unknown listener coordinate flags"); + if (flags > 1) return SCE_NGS2_ERROR_INVALID_GEOM_FLAG; if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; const Vector position = ReadVector(param->position); ReadVector(param->velocity); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 2417e360e0..ae95253a90 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -115,9 +115,18 @@ static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t fla static void TestInvalidListener() { auto param = Listener(); Require(sceNgs2GeomCalcListener(¶m, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - RejectListener(nullptr, 0); - RejectListener(¶m, 2); - RejectListener(¶m, 0xffffffffu); + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + unsigned char before[sizeof(work)]; + std::memcpy(before, &work, sizeof(work)); + Require(sceNgs2GeomCalcListener(nullptr, &work, 0) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(nullptr, &work, 2) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(nullptr, nullptr, 0) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(¶m, &work, 2) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0x80000000u) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0xffffffffu) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, nullptr, 2) == static_cast(0x804A8010)); + Require(std::memcmp(before, &work, sizeof(work)) == 0); for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { param.sound_speed = value; RejectListener(¶m, 0); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 361ee333a9..78c3ea4e5a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -31,7 +31,7 @@ - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits -- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Unknown flags, zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. +- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified From 5702e1f9386a82889ce5783bf37854f06fefab31 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Tue, 20 Oct 2026 18:25:33 -0300 Subject: [PATCH 0169/1360] test(relinker): include algorithm for the converter test std::all_of The new checks call std::all_of, which the file reached only through another header. --- core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index f4191551c5..de30a25dc5 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include From 3ed129ab335f359fbcaa0813401b297fb5c5052e Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:29:09 -0700 Subject: [PATCH 0170/1360] fix(agc-driver): make the presenter wait for a full frame dump queue With APS5_DUMP_FRAMES, FrameDumpWriter::Enqueue threw "frame dump queue exceeded 64 MiB" when the writer thread fell more than 64 MiB behind, which failed the presentation and ended the title. #4320 made that rare at the default APS5_DUMP_SCALE=4; at APS5_DUMP_SCALE=1 a 1920x1080 title reaches it within nine frames and a 3840x2160 one on the third. Enqueue now waits on the writer's condition variable until the frame fits, and the writer notifies after each file; a single frame larger than the queue still throws. The VideoOut frame dump queue test gets a wait mode at scale 1 whose third frame has to wait for the held first one. Co-Authored-By: Claude Fable 5.1 --- .../Execution/src/VulkanDevice.cpp | 11 ++++--- core/libs/prx/libSceVideoOut/CMakeLists.txt | 3 ++ .../libSceVideoOut/tests/FrameDumpQueue.cpp | 31 ++++++++++++++----- docs/dev/GAME_TESTING.md | 3 +- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c7b1d8dced..6b60d40970 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -153,20 +153,22 @@ public: std::lock_guard lock(mutex); stopping = true; } - changed.notify_one(); + changed.notify_all(); if (worker.joinable()) worker.join(); } void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector pixels) { - std::lock_guard lock(mutex); + std::unique_lock lock(mutex); require(!stopping, "frame dump writer has stopped"); - require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; + require(pixels.size() <= maxBytes, "a frame dump exceeds the 64 MiB queue"); + changed.wait(lock, [&] { return stopping || pixels.size() <= maxBytes - queuedBytes; }); + require(!stopping, "frame dump writer has stopped"); if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); queuedBytes += bytes; - changed.notify_one(); + changed.notify_all(); } bool Idle() { @@ -199,6 +201,7 @@ private: else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); + changed.notify_all(); } } } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 9c7f9ae6da..c3a4e6f69f 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -133,6 +133,9 @@ if(BUILD_TESTING AND NOT WIN32) add_test(NAME video_out_frame_dump_queue COMMAND video_out_frame_dump_queue_tests) set_tests_properties(video_out_frame_dump_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4") + add_test(NAME video_out_frame_dump_wait COMMAND video_out_frame_dump_queue_tests wait) + set_tests_properties(video_out_frame_dump_wait PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 + ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=1") endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp index a5105da79f..b98c07e1be 100644 --- a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -28,11 +29,16 @@ namespace { constexpr std::uint32_t Width = 3840; constexpr std::uint32_t Height = 2160; -constexpr std::uint32_t Scale = 4; -constexpr std::uint32_t DumpWidth = Width / Scale; -constexpr std::uint32_t DumpHeight = Height / Scale; -constexpr std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; +std::uint32_t scaleFromEnvironment() { + const char* value = std::getenv("APS5_DUMP_SCALE"); + return value != nullptr ? static_cast(std::strtoul(value, nullptr, 10)) : 4u; +} +const std::uint32_t Scale = scaleFromEnvironment(); +const std::uint32_t DumpWidth = Width / Scale; +const std::uint32_t DumpHeight = Height / Scale; +const std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; constexpr int Frames = 4; +bool WaitMode = false; void check(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); @@ -137,6 +143,13 @@ void testDumpQueue() { check(sceVideoOutRegisterBuffers2(handle, 1, 1, &linearBuffer, 1, &linearAttribute, 0, nullptr) == 0, "linear buffer registration failed"); const HeldDump first(0); const HeldDump last(Frames - 1); + std::vector firstDump; + std::thread reader; + const auto started = std::chrono::steady_clock::now(); + if (WaitMode) reader = std::thread([&] { + std::this_thread::sleep_for(std::chrono::seconds(2)); + firstDump = first.Release(); + }); std::exception_ptr failure; try { for (int frame = 0; frame < Frames; ++frame) { @@ -149,8 +162,10 @@ void testDumpQueue() { } catch (...) { failure = std::current_exception(); } - const auto firstDump = first.Release(); + if (WaitMode) reader.join(); + else firstDump = first.Release(); if (failure) std::rethrow_exception(failure); + if (WaitMode) check(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(1500), "the presenter did not wait for the writer"); checkDump(firstDump, tiledPixels); const auto lastDump = last.Release(); checkDump(lastDump, linearPixels); @@ -164,11 +179,13 @@ void testDumpQueue() { } -int main() { +int main(int argc, char** argv) { + WaitMode = argc > 1 && std::strcmp(argv[1], "wait") == 0; VideoOutTestEnvironment environment; try { + if (WaitMode) check(Scale == 1, "the wait test needs APS5_DUMP_SCALE=1"); testDumpQueue(); - std::puts("VideoOut frame dump queue test passed"); + std::puts(WaitMode ? "VideoOut frame dump wait test passed" : "VideoOut frame dump queue test passed"); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index be0924205b..ea65590512 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -79,7 +79,8 @@ cmd /c '.\app.exe > run.log 2>&1' - Measure the same spot every time: load a save, wait for the scene to settle, then measure a fixed window with no input. - Compare builds on the same machine, alternating them (A, B, A, B), at least two runs each. Throw away a run that was disturbed (another program, a sleeping display) and say so. - `APS5_TRACE_SUMMARY_EVERY=10` writes one line per 10 s to `aps5-summary.jsonl` without slowing the title; `tools/trace_digest.py` turns it into a table, and `tools/performance_compare.py` compares two captures ([BUILD](BUILD.md#performance-trace)). Pull requests labelled `perf` need a capture of `main` and one with the change. -- `APS5_DUMP_FRAMES=` with `APS5_DUMP_FRAMES_EVERY=` saves frames as `frame_.bmp` to check what was on screen. A second process of the same title writes to the same names and overwrites the first one's frames. +- `APS5_DUMP_FRAMES=` with `APS5_DUMP_FRAMES_EVERY=` saves frames as `frame_.bmp` to check what was on screen. A second process of the same title writes to the same names and overwrites the first one's frames. While 64 MiB of frames wait to be written, the presenter waits for the + writer, so a slow disk or `APS5_DUMP_SCALE=1` slows the title down instead of stopping it. ## Title notes From 34e3fbf47c68063079bc6db573cd5cba6fcacbee Mon Sep 17 00:00:00 2001 From: Seif Allah Karoui <21332330+SeifKaroui@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:30:51 +0100 Subject: [PATCH 0171/1360] fix(shader): remove unobserved phi cycles before resource planning Preserve phi graphs with transitive non-phi users and remove unused cycles before building the SRT plan. Extend the IR regression with dead and live cycles, an empty program, and repeated elimination. AI-assisted: OpenAI Codex --- .../prx/libSceAgcDriver/tests/ConstantPhi.cpp | 78 +++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../Optimization/DeadCodeEliminator.hpp | 1 + .../src/UnobservedPhiEliminator.cpp | 42 ++++++++++ core/shader/recompiler/Recompiler.cpp | 2 + docs/dev/debt/shader.md | 2 + 6 files changed, 126 insertions(+) create mode 100644 core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp index 2cc4a8b370..09ef9b6dac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -79,6 +79,83 @@ void CheckDiamond(unsigned mode) { } } +void CheckUnobservedPhiCycles() { + IrProgram empty; + DeadCodeEliminator{}.EliminateUnobservedPhis(empty); + IrProgram program; + for (std::uint32_t id = 0; id < 4u; ++id) { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = id; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + } + auto& entry = *program.BlockOrder()[0]; + auto& header = *program.BlockOrder()[1]; + auto& latch = *program.BlockOrder()[2]; + auto& exit = *program.BlockOrder()[3]; + program.SetEntryBlock(entry); + entry.AddBranch(&header); + header.AddBranch(&latch); + header.AddBranch(&exit); + latch.AddBranch(&header); + program.Metadata().blockInfo[0].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[0].terminator.trueBlock = 1u; + auto& headerInfo = program.Metadata().blockInfo[1]; + headerInfo.terminator.kind = TerminatorKind::ConditionalBranch; + headerInfo.terminator.trueBlock = 2u; + headerInfo.terminator.falseBlock = 3u; + program.Metadata().blockInfo[2].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[2].terminator.trueBlock = 1u; + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + auto& zero = builder.Constant(0u); + auto& producer = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + const auto phi = [&](IrBlock& block, IrType type = IrType::U32) -> IrValue& { + auto& value = program.CreateValue(IrOpcode::Phi, type); + block.AppendInstruction(&value); + return value; + }; + auto& first = phi(header); + auto& second = phi(latch); + auto& tail = phi(header); + auto& self = phi(header); + auto& liveFirst = phi(header); + auto& liveSecond = phi(latch); + auto& condition = phi(header, IrType::Bool); + first.AddPhiOperand(&entry, &producer); + first.AddPhiOperand(&latch, &second); + second.AddPhiOperand(&header, &first); + tail.AddPhiOperand(&entry, &zero); + tail.AddPhiOperand(&latch, &second); + self.AddPhiOperand(&entry, &zero); + self.AddPhiOperand(&latch, &self); + liveFirst.AddPhiOperand(&entry, &zero); + liveFirst.AddPhiOperand(&latch, &liveSecond); + liveSecond.AddPhiOperand(&header, &liveFirst); + condition.AddPhiOperand(&entry, &builder.ConstantBool(true)); + condition.AddPhiOperand(&latch, &condition); + headerInfo.condition = &condition; + builder.SetInsertionPoint(header); + static_cast(builder.Emit(IrOpcode::Reference, IrType::Void, {&condition})); + builder.SetInsertionPoint(latch); + auto& store = builder.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {&zero, &liveSecond}); + ValidateProgram(program, true); + DeadCodeEliminator{}.EliminateUnobservedPhis(program); + ValidateProgram(program, true); + Require(first.Parent() == nullptr && second.Parent() == nullptr && tail.Parent() == nullptr && self.Parent() == nullptr, "phi cycles: an unobserved cycle or its tail survived"); + Require(!first.HasUses() && !second.HasUses() && !tail.HasUses() && !self.HasUses(), "phi cycles: a removed cycle retained uses"); + Require(liveFirst.Parent() == &header && liveSecond.Parent() == &latch && liveFirst.Argument(1) == &liveSecond && liveSecond.Argument(0) == &liveFirst, "phi cycles: an observed cycle was changed"); + Require(store.Parent() == &latch && store.Argument(1) == &liveSecond, "phi cycles: an observable store was changed"); + Require(condition.Parent() == &header && headerInfo.condition == &condition && condition.Argument(1) == &condition, "phi cycles: a branch condition was changed"); + DeadCodeEliminator{}.Eliminate(program); + Require(producer.Parent() == nullptr && !producer.HasUses(), "phi cycles: ordinary DCE retained a released producer"); + DeadCodeEliminator{}.EliminateUnobservedPhis(program); + ValidateProgram(program, true); + Require(entry.Instructions().empty() && header.Instructions().size() == 3u && latch.Instructions().size() == 2u, "phi cycles: repeated elimination changed the live graph"); +} + void CheckLongAncillaryForwarding() { IrProgram program; auto& block = program.CreateBlock(); @@ -127,6 +204,7 @@ int main() { try { for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode); CheckLongAncillaryForwarding(); + CheckUnobservedPhiCycles(); std::puts("constant phi dominance tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index bb32aec198..445d8cf507 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -100,6 +100,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SsaBuilder/SsaRegisterStateCleanup.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ConstantFolder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DeadCodeEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/UnobservedPhiEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/HostInterpolationChecker.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ReadLaneEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/MaskedSelectEliminator.cpp diff --git a/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp index 3aef0d5211..6e820d5fe3 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler { class DeadCodeEliminator { public: void Eliminate(IrProgram& program) const; + void EliminateUnobservedPhis(IrProgram& program) const; void Eliminate(std::span blocks) const; void RemoveIdentities(IrProgram& program) const; void RemoveIdentities(std::span blocks) const; diff --git a/core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp b/core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp new file mode 100644 index 0000000000..a4ef3bc3f8 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp @@ -0,0 +1,42 @@ +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include + +namespace ShaderRecompiler { + +void DeadCodeEliminator::EliminateUnobservedPhis(IrProgram& program) const { + std::unordered_set phis; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* value : block->Instructions()) { + if (value->IsPhi()) phis.insert(value); + } + } + std::unordered_set observed; + std::vector pending; + for (IrValue* phi : phis) { + if (std::ranges::any_of(phi->Uses(), [&](IrValue* user) { return !phis.contains(user); })) { + observed.insert(phi); + pending.push_back(phi); + } + } + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* argument : value->Arguments()) { + if (phis.contains(argument) && observed.insert(argument).second) pending.push_back(argument); + } + } + std::vector unobserved; + for (IrValue* phi : phis) { + if (observed.contains(phi)) continue; + unobserved.push_back(phi); + for (std::size_t index = 0; index < phi->ArgumentCount(); ++index) phi->ReplaceArgument(index, nullptr); + } + for (IrValue* phi : unobserved) { + phi->Parent()->RemoveInstruction(phi); + phi->Invalidate(); + } +} + +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9246de6bda..7176a9414c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -319,6 +319,8 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat tessellationLowering.Lower(program, inputInfo.vertex->tess, request.target.tessellation ? &*request.target.tessellation : nullptr); } + deadCodeEliminator.EliminateUnobservedPhis(program); + deadCodeEliminator.Eliminate(program); constexpr SrtWalker srtWalker; srtWalker.BuildPlan(program); deadCodeEliminator.Eliminate(program); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d2fe3155a5..8657d7b8bb 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -29,6 +29,8 @@ - [Unlinked calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) - the `s_swappc_b64` a table call's compare chain falls back to, when its target matches no linked copy, records the `UnlinkedCall` fault with the target and the program counter and stops the invocation, or continues after the call in programs whose faults do not stop invocations (see `bdaStopsInvocations`). The driver lists those calls in the request (`linkedCallFallbacks`), and it reports the fault like a BDA fault, naming the target: a dispatch that checks its faults when it completes throws, one whose check runs in a deferred write-back logs it and continues. The hardware would jump to the target, which is not modelled. Any other `s_swappc_b64` whose target is not resolved still throws when the shader is compiled. - [Cached source IR](../../../core/shader/recompiler/Recompiler.cpp) releases lazily allocated SSA construction tables after resource preparation and reclaims values unreachable from instructions, metadata and resource plans when retaining a pending source program. Immediate shader preparation skips reclamation because compilation consumes the program. Reclamation must account for every new IR pointer owner; a remaining external use throws. Live IDs are retained. Source and variant entries remain cached without an eviction bound. A pending source program is released after its first variant is obtained; memory beyond the measured title checkpoint and Linux performance remain unmeasured. + +- Cycles through non-phi instructions are not removed by [unobserved phi elimination](../../../core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp). The pass removes phi-only cycles before resource planning when no transitive user is a non-phi instruction. - [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Branching prologs, computed low pointers and continuations reading `s_getpc_b64` remain unsupported - [Raw fragment user-data tails](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) also link a terminal `s_setpc_b64` when the low half is unchanged user data and the last high-half writer is an unconditional immediate `s_mov_b32`. Other computed high halves remain unsupported. This follows AMD RDNA2 ISA section 12.3 and Mesa `aco_select_nir.cpp` (`create_fs_jump_to_epilog`), `aco_isel_helpers.cpp` (`convert_pointer_to_64_bit`) and `aco_lower_to_hw_instr.cpp` (`p_jump_to_epilog`) at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`; console hardware has not been measured From 700be1869477a876cadc09bed94bac2239cfe209 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 19 Oct 2026 21:44:32 -0700 Subject: [PATCH 0172/1360] fix(tools): preserve weak guest module imports in audits --- tools/import_audit.py | 22 +++++++++++++--------- tools/tests/test_import_audit.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 0e47d38e5d..121e3147f1 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -133,7 +133,7 @@ def elf_program_dynamic_symbols(path, data): if symbol_count is None: raise AuditError(f"{path}: dynamic symbol table has no supported hash table") symbol_offset = file_offset(symbol_address, symbol_count * symbol_size, "dynamic symbol table") - imports = set() + imports = {} exports = set() for index in range(symbol_count): name, info, other, section_index, _, _ = unpack("> 4 == 2 elif info >> 4 in (1, 2, 10) and other & 3 in (0, 3): exports.add(symbol) needed = {cstring(data, string_offset + value, string_offset + string_size, path, "needed library name") for value in needed_offsets} @@ -189,7 +189,7 @@ def elf_imports(path, data): imports, _, needed = elf_program_dynamic_symbols(path, data) return imports, needed sections = elf_sections(path, data) - symbols = set() + symbols = {} needed = set() for section in sections: kind, offset, size, link, entry = section[1], section[4], section[5], section[6], section[9] @@ -199,10 +199,11 @@ def elf_imports(path, data): strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") for position in range(0, size, 24): - name, _, _, index, _, _ = unpack("> 4 == 2 elif kind == 6: if entry != 16 or size % 16 or link >= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic section (entry size {entry}, size {size}, string table section {link})") @@ -448,7 +449,7 @@ def read_modules(directories): names = pe_exports(file, data) else: symbols, needed = elf_imports(file, data) - imports[file.name] = (sorted(symbols), sorted(needed)) + imports[file.name] = (sorted(symbols.items()), sorted(needed)) names = elf_exports(file, data) for name in names: exports[name.split(MODULE_SUFFIX, 1)[0]].add(file.name) @@ -574,9 +575,10 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["references"] += 1 record["weak"] = record["weak"] and weak for module, (symbols, needed) in (module_imports or {}).items(): - for nid in symbols: - record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": False}) + for nid, weak in symbols: + record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": weak}) record["references"] += 1 + record["weak"] = record["weak"] and weak for record in records.values(): nid, library = record["nid"], record["library"] providers = exports.get(nid, []) @@ -685,6 +687,8 @@ def label(record): else: mark = "" if record.get("name_verified", True) else " (name does not hash to this NID)" text = f"{record['nid']} {record['name']}{mark}" + if record["weak"]: + text += " (weak import)" return f"{text} (imported by {record['module']})" if record.get("module") else text @@ -692,7 +696,7 @@ def render(summary, records, missing, stops, missing_hosts=()): lines = [f"{summary['references']} references, {summary['unique_imports']} unique imports"] for kind in CLASSES: lines.append(f" {kind:<12}{summary['unique_by_class'][kind]:>6} imports {summary['references_by_class'][kind]:>6} references") - for title, kind in (("Absent: no built library exports these, so the loader fails", "absent"), ("Stub: exported but throws when called", "stub")): + for title, kind in (("Absent: no built library exports these", "absent"), ("Stub: exported but throws when called", "stub")): selected = [record for record in records if record["class"] == kind] if selected: lines.append("") diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 166dc4508f..c30c03e8ae 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -309,7 +309,7 @@ class ImportAuditTests(unittest.TestCase): records, _ = import_audit.audit(imports, exports, stubs, set(exports), set()) self.assertEqual(records[0]["class"], "stub") self.assertEqual(records[0]["providers"], ["literal.prx"]) - module_imports = {"guest.so": (["strlen"], [])} + module_imports = {"guest.so": ([("strlen", False)], [])} records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, {"libc.prx"}, set(module_imports), module_imports, {"strlen": ["other.so"]}) @@ -538,6 +538,28 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("[library not named]", out) self.assertIn("EEEEEEEEEEE (imported by main.prx)", out) + def test_module_import_bindings_survive_the_audit(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + symbols = [("optional", 0, 0x22), ("required", 0, GLOBAL_FUNCTION), + ("mixed#first", 0, 0x22), ("mixed#second", 0, GLOBAL_FUNCTION)] + images = [build_elf(symbols), build_sectionless_elf(symbols), + build_sectionless_elf(symbols, gnu_hash=True)] + for index, image in enumerate(images): + with self.subTest(image=index): + (work.modules / "guest.so").write_bytes(image) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + records = {record["nid"]: record for record in data["imports"]} + self.assertTrue(records["optional"]["weak"]) + self.assertFalse(records["required"]["weak"]) + self.assertFalse(records["mixed"]["weak"]) + self.assertEqual(data["references"], 3) + self.assertIn("optional (weak import)", out) + def test_module_sidecars_do_not_change_the_audit(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) @@ -776,7 +798,7 @@ class ImportAuditTests(unittest.TestCase): ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"]) expected_imports = import_audit.elf_imports(path, image) expected_exports = import_audit.elf_exports(path, image) - self.assertEqual(expected_imports, ({"AAAAAAAAAAA"}, {"libSceA.prx"})) + self.assertEqual(expected_imports, ({"AAAAAAAAAAA": False}, {"libSceA.prx"})) self.assertEqual(expected_exports, {"BBBBBBBBBBB"}) for section_offset in (len(image), len(image) + 64): for section_count in (1, 0): @@ -841,7 +863,7 @@ class ImportAuditTests(unittest.TestCase): ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"], gnu_hash=True) self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), - ({"AAAAAAAAAAA"}, {"BBBBBBBBBBB"}, {"libSceA.prx"})) + ({"AAAAAAAAAAA": False}, {"BBBBBBBBBBB"}, {"libSceA.prx"})) def test_sectionless_hash_metadata_must_be_file_backed(self): for gnu_hash in (False, True): @@ -1298,8 +1320,8 @@ class ImportAuditTests(unittest.TestCase): def test_elf_imports_reader(self): image = build_elf([("one", 1, GLOBAL_FUNCTION), ("two", 0, GLOBAL_FUNCTION), ("three", 0, 0x22)], ["libc.prx", "libSceZip.prx"]) path = Path("mod.prx") - self.assertEqual(import_audit.elf_imports(path, image), ({"two", "three"}, {"libc.prx", "libSceZip.prx"})) - self.assertEqual(import_audit.elf_imports(path, exporting()), (set(), set())) + self.assertEqual(import_audit.elf_imports(path, image), ({"two": False, "three": True}, {"libc.prx", "libSceZip.prx"})) + self.assertEqual(import_audit.elf_imports(path, exporting()), ({}, set())) for cut in (10, 64, len(image) - 5): with self.assertRaisesRegex(import_audit.AuditError, "mod.prx"): import_audit.elf_imports(path, image[:cut]) From cf17f057303109c05ad5957d359b857a8f5c7b0d Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 19 Oct 2026 21:44:47 -0700 Subject: [PATCH 0173/1360] fix(tools): resolve Windows system library suffixes in audits --- tools/import_audit.py | 24 +++++++++++++----- tools/tests/test_import_audit.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 0e47d38e5d..a7fb383320 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -567,7 +567,19 @@ def classify(nid, library, providers, stubs, modules): return "implemented" -def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=()): +def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=(), windows_libraries=()): + def system_library(name): + if name: + for suffix in (".sprx", ".suprx", ".debug_prx"): + if name.endswith(suffix): + normalized = name.removesuffix(suffix) + ".prx" + if normalized in windows_libraries: + return normalized + return name + + def available(name): + return name in modules or system_library(name) in libraries + records = {} for nid, library, weak in imports: record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0, "weak": weak}) @@ -591,21 +603,21 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu kind = classify(resolved_nid, library, providers, stubs, modules) record["class"] = kind record["providers"] = providers - record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers + record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and system_library(library) not in providers and "libc.prx" not in providers record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) for library in needed_libraries: - if library not in libraries and library not in modules: + if not available(library): missing[library] = 0 for record in ordered: library = record["library"] - if library and library not in libraries and library not in modules: + if library and not available(library): missing[library] += 1 for _, needed in (module_imports or {}).values(): for library in needed: - if library not in libraries and library not in modules: + if not available(library): missing[library] += 1 return ordered, dict(sorted(missing.items())) @@ -757,7 +769,7 @@ def main(argv=None): modules = set(module_imports) modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} modules = ModuleNames(modules, fold=bool(windows)) - records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) + records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed, windows_libraries=windows) missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) if args.names: attach_names(records, load_names(args.names)) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 166dc4508f..dbbb1c9578 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -219,6 +219,49 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(result.stdout, b"") + def test_windows_guest_dependencies_use_host_prx_suffixes(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + (work.modules / "guest.so").write_bytes(build_elf([], ["libSceA.sprx"])) + registry, result = work.root / "game.registry.json", work.root / "audit.json" + for suffix in (".sprx", ".suprx", ".debug_prx"): + with self.subTest(suffix=suffix): + name = "libSceA" + suffix + write_registry(registry, [("AAAAAAAAAAA", name)]) + registry.with_name("game.needed.json").write_text(json.dumps([name])) + code, out, err = work.run(registry, "--strict-providers", "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + self.assertEqual(data["library_mismatch"], 0) + self.assertEqual(data["imports"][0]["providers"], ["libSceA.prx"]) + + def test_elf_guest_dependencies_keep_literal_host_names(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, result = work.root / "game.registry.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.sprx")]) + registry.with_name("game.needed.json").write_text(json.dumps(["libSceA.sprx"])) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {"libSceA.sprx": 1}) + self.assertEqual(data["library_mismatch"], 1) + + def test_windows_suffix_lookup_preserves_guest_and_missing_libraries(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + (work.modules / "libSceA.sprx").write_bytes(exporting("BBBBBBBBBBB")) + registry, result = work.root / "game.registry.json", work.root / "audit.json" + write_registry(registry, [("BBBBBBBBBBB", "libSceA.sprx")]) + registry.with_name("game.needed.json").write_text(json.dumps(["libSceA.sprx", "libMissing.sprx"])) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {"libMissing.sprx": 0}) + self.assertEqual(data["imports"][0]["class"], "module") + self.assertEqual(data["library_mismatch"], 0) + def test_registry_weak_flags_are_booleans(self): registry = self.work.root / "game.json" output = self.work.root / "audit.json" From 7caad6117ed5e8f0eae0015fb8a06cb12cdd81e4 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 18:26:35 +0000 Subject: [PATCH 0174/1360] fix(agc): separate possible writes from complete coverage --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 77 +++++++++++++++---- .../Graphics/src/BdaResources.cpp | 2 +- .../Graphics/src/GuestBufferMemory.cpp | 6 +- .../libSceAgcDriver/tests/WriteTracking.cpp | 15 ++++ 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index efa0e9d031..24e0fc7968 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -84,6 +84,7 @@ bool UnchangedCommittedSince(std::span& store); std::uint64_t StoreOwnBytes(std::uint64_t address, std::span source); bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 6e7a43a575..e88cf5b386 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -957,6 +957,10 @@ struct WriteTracker { std::uint32_t dropped = 0; std::uint32_t next = 0; std::array pieces{}; + std::uint32_t possibleWhole = 0; + std::uint32_t possibleDropped = 0; + std::uint32_t possibleNext = 0; + std::array possiblePieces{}; bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const { if (generation == 0 || begin >= end || dropped > generation) return false; @@ -1009,7 +1013,7 @@ struct WriteTracker { #endif } - void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { #ifdef _WIN32 address = std::max(address, base); end = std::min(end, base + size); @@ -1023,23 +1027,33 @@ struct WriteTracker { const auto stop = std::min(end, blockBegin(block) + WriteBlockBytes); if (!blockPartWatched(first, stop)) continue; stamp(block, stampGeneration, StampKind::Driver); - noteDriverStore(block, address, end, stampGeneration); + noteDriverStore(block, address, end, stampGeneration, complete); } } - void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { const auto begin = blockBegin(block); const auto from = static_cast(std::max(address, begin) - begin); const auto to = static_cast(std::min(end, begin + WriteBlockBytes) - begin); if (from == 0 && to == WriteBlockBytes) { auto& entry = driverPieces[block]; - entry = DriverPieces{}; - entry.whole = stampGeneration; + if (complete) { + entry = DriverPieces{}; + entry.whole = stampGeneration; + } else { + entry.possibleWhole = stampGeneration; + entry.possibleDropped = 0; + entry.possibleNext = 0; + entry.possiblePieces = {}; + } return; } auto& entry = driverPieces[block]; - auto& slot = entry.pieces[entry.next++ % entry.pieces.size()]; - entry.dropped = std::max(entry.dropped, slot.generation); + auto& pieces = complete ? entry.pieces : entry.possiblePieces; + auto& dropped = complete ? entry.dropped : entry.possibleDropped; + auto& next = complete ? entry.next : entry.possibleNext; + auto& slot = pieces[next++ % pieces.size()]; + dropped = std::max(dropped, slot.generation); slot = {stampGeneration, from, to}; } @@ -1661,12 +1675,12 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func ++tracker.generation; if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { tracker.noteUnstampedStore(stored.first, stored.second); - if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation); + if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation, true); return 0; } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); - tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); + tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation, true); } return tracker.generation; }; @@ -1699,7 +1713,7 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span so }); } -std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { +static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool complete) { auto& tracker = Tracker(); auto lock = lockTracker(tracker); tracker.initialize(); @@ -1707,7 +1721,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { ++tracker.generation; if (!tracker.watched || !tracker.covers(address, bytes)) { tracker.noteUnstampedStore(address, address + bytes); - if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation); + if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation, complete); return 0; } { @@ -1733,11 +1747,19 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); - tracker.noteDriverStore(block, address, address + bytes, tracker.generation); + tracker.noteDriverStore(block, address, address + bytes, tracker.generation, complete); } return tracker.generation; } +std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { + return markWritten(address, bytes, true); +} + +std::uint64_t MarkPossiblyWritten(std::uint64_t address, std::size_t bytes) { + return markWritten(address, bytes, false); +} + bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { auto& tracker = Tracker(); const auto lock = lockTracker(tracker); @@ -1746,7 +1768,36 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati #ifdef _WIN32 if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return true; #endif - return tracker.storedOver(address, address + bytes, generation, false); + const auto end = address + bytes; + const auto first = tracker.blockOf(address); + const auto last = tracker.blockOf(end - 1); + for (auto block = first; block <= last; ++block) { + if (tracker.stampOf(block) <= generation) continue; + if (const auto found = tracker.cpuPages.find(block); found != tracker.cpuPages.end()) { + const auto begin = tracker.blockBegin(block); + const auto from = std::max(address, begin) - begin; + const auto to = std::min(end, begin + WriteBlockBytes) - begin; + for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) { + if (found->second.pages[page] > generation) return true; + } + } else if (tracker.cpuStampOf(block) > generation) { + return true; + } + const auto found = tracker.driverPieces.find(block); + if (found == tracker.driverPieces.end()) return true; + const auto& entry = found->second; + if (entry.whole > generation || entry.dropped > generation || entry.possibleWhole > generation || entry.possibleDropped > generation) return true; + const auto begin = tracker.blockBegin(block); + const auto from = std::max(address, begin) - begin; + const auto to = std::min(end, begin + WriteBlockBytes) - begin; + for (const auto& piece : entry.pieces) { + if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + } + for (const auto& piece : entry.possiblePieces) { + if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + } + } + return false; } bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 1da043f87f..03865c6a92 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -347,7 +347,7 @@ void BdaResources::markWrittenPages() const { Require(false, message.str()); } } - GuestMemory::MarkWritten(begin, bytes); + GuestMemory::MarkPossiblyWritten(begin, bytes); }), "invalid BDA written-page record"); for (std::uint32_t claim = 0; claim < count; ++claim) { const auto slot = words[Abi::WrittenListWord + claim]; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 82d2b10a04..b019acaf6d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -5267,7 +5267,7 @@ void GuestBufferMemory::MarkDirectWrites() const { const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; - if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast(end - begin)); + if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); } } @@ -5355,7 +5355,7 @@ void GuestBufferMemory::WriteBack() { if (region.direct == nullptr && !(region.gpuCopy && (region.copiedBack || region.deviceLocal)) && !(region.mirror != nullptr && !region.mirror->writable)) Recorder::NoteWrittenBack(begin, static_cast(end - begin)); if (region.direct != nullptr) { // The GPU wrote imported guest memory directly; page write watching did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); continue; } if (region.mirror != nullptr) { @@ -5385,7 +5385,7 @@ void GuestBufferMemory::WriteBack() { if (region.copiedBack) { // The GPU copied the written sub-range back into the import; page write watching // did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); continue; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 700455ddd0..78e128d6ce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -214,6 +214,21 @@ void CheckStoredAllSince() { MarkWritten(base, 64); Require(StoredAllSince_nid_no_patch(base, 64, before), "a complete non-block-aligned range store is absent"); + before = TrackerGeneration(); + MarkPossiblyWritten(base + 2048, 16); + Require(StoredOver(base + 2048, 16, before), "a possible driver write did not invalidate its exact range"); + Require(!StoredOver(base + 2064, 16, before), "a possible driver write invalidated an adjacent range"); + Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "a possible driver write proved complete byte coverage"); + for (unsigned repeat = 0; repeat < 6; ++repeat) MarkPossiblyWritten(base + 2048, 16); + Require(StoredOver(base + 2048, 16, before), "repeated possible writes lost their overlap evidence"); + Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "repeated possible writes proved complete byte coverage"); + + before = TrackerGeneration(); + MarkWritten(base + 2560, 16); + MarkPossiblyWritten(base, Block); + Require(StoredAllSince_nid_no_patch(base + 2560, 16, before), "a possible whole-block range erased an exact partial-store proof"); + Require(!StoredAllSince_nid_no_patch(base, Block, before), "a possible whole-block range proved complete block coverage"); + before = TrackerGeneration(); MarkWritten(base + 512, 16); MarkWritten(base + 544, 16); From a834bbf5fe282cd8960cc8ff6cec244ec0bf7415 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 18:34:59 +0000 Subject: [PATCH 0175/1360] fix(agc): preserve recognized BDA depth clear proof --- .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 9 +++++++-- .../tests/execution/NativeMultisampleDepth.cpp | 13 +++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 7df4340010..ef40194848 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -332,7 +332,7 @@ public: const auto& metadata = sampling.initial[1]; Require(GuestMemory::UnchangedSince(depth.address, depth.bytes, depth.generation), "guest writes over native multisample depth are not implemented"); Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); - Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); + if (!clearKernelProvesCoverage) Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); Require(clearDepth == 0.0f || clearDepth == 1.0f, "native multisample depth clear value is not implemented"); auto current = sampling.initial; current[1].generation = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); @@ -345,6 +345,7 @@ public: RecordClear(VK_IMAGE_ASPECT_DEPTH_BIT); sampling.initial = current; clearGeneration = 0u; + clearKernelProvesCoverage = false; pendingClear = 0u; } Require(GuestMemory::UnchangedSinceAll(sampling.initial), "guest writes over native multisample depth are not implemented"); @@ -746,6 +747,7 @@ public: bool samplingActive = false; DepthSamplingState sampling; std::uint64_t clearGeneration = 0u; + bool clearKernelProvesCoverage = false; std::shared_ptr owned; std::shared_ptr sampleShadow; bool useSampleShadow = false; @@ -1935,7 +1937,10 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz const auto& metadata = surface->sampling.initial[1]; if (nativeBytes != bytes || nativeBytes < metadata.bytes || !GuestMemory::Accessible(reinterpret_cast(metadata.address), metadata.bytes) || !GuestMemory::Watched(metadata.address, metadata.bytes)) surface->sampling.Refuse("native multisample depth incomplete or unproven fast-clear transition is not implemented"); else if (surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); - else surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + else { + surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + surface->clearKernelProvesCoverage = nativeBytes == bytes && nativeBytes >= metadata.bytes; + } existing = true; continue; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index cda5b9280a..34c34e0774 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -600,7 +600,7 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path"); } -enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap }; +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore }; void CheckClearDispatch() { const std::array code{ @@ -771,10 +771,17 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); + if (fault == ClearFault::PossibleStore) { + for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + } NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); - AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { + if (fault == ClearFault::PossibleStore) { + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); + AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes); + Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage"); + } else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; }); @@ -1156,6 +1163,8 @@ int main(int argc, char** argv) { } } } + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::PossibleStore); + ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; } From b3f863a0dc358980ee154828030da2ec1660aa1f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 18:36:43 +0000 Subject: [PATCH 0176/1360] fix(agc): require a tracked HTILE write for recognized clears --- core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ef40194848..1f7a5c129e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -332,7 +332,8 @@ public: const auto& metadata = sampling.initial[1]; Require(GuestMemory::UnchangedSince(depth.address, depth.bytes, depth.generation), "guest writes over native multisample depth are not implemented"); Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); - if (!clearKernelProvesCoverage) Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); + if (clearKernelProvesCoverage) Require(GuestMemory::StoredOver(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no tracked HTILE write"); + else Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); Require(clearDepth == 0.0f || clearDepth == 1.0f, "native multisample depth clear value is not implemented"); auto current = sampling.initial; current[1].generation = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); From 9ce5037ccfd98026cbe1dc611d9b28100904e746 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 20:41:09 +0000 Subject: [PATCH 0177/1360] fix(agc): prove native HTILE clears on completion --- .../Execution/include/VulkanDevice.hpp | 5 +++-- .../src/Driver/Dispatch/Dispatch.cpp | 18 +++++++++------ .../Execution/src/VulkanDevice.cpp | 22 ++++++++++++------- .../Graphics/include/DepthSurface.hpp | 3 ++- .../Graphics/src/DepthSurface.cpp | 22 ++++++++++++++----- .../execution/NativeMultisampleDepth.cpp | 10 +++++---- 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 9df19e2024..ebaa8e03cf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace AgcDriver { @@ -270,7 +271,7 @@ public: // `recipe`, when given, receives the Recipe a successful call built for its dispatch-cache // variant (design_cpu_final M4): only when the resource cache served or took the object // (reusable, cacheable) and recipes are on (APS5_NO_DISPATCH_RECIPE=1 builds none); else null. - void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, std::optional> htileClear = std::nullopt); struct IndirectOutcome { int cpuReason; double argumentReadMs; @@ -353,7 +354,7 @@ private: ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. - IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}); + IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}, std::optional> htileClear = std::nullopt); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); 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 7bba71169d..396263b57c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -15,7 +15,9 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include #include +#include namespace AgcDriver::DriverDetail { @@ -118,6 +120,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); const bool partialLastGroup = partialGroups != std::array{}; + std::optional> htileClearCompletion; if (resolving) { if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; @@ -133,7 +136,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments); - Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u), nativeBytes); + const auto bytes = static_cast(threads * 16u); + if (Graphics::NoteHtileDepthClear(htile, bytes, nativeBytes)) htileClearCompletion = std::pair{htile, bytes}; } if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; @@ -456,18 +460,18 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); const auto queueId = submission.queue; itemCommits.fetch_add(1, std::memory_order_relaxed); - DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch] { + DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch, htileClearCompletion] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(0x15, queueId); const auto& compiledCommit = *result; const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); try { - std::shared_ptr recipeHit = localDevice->PrepareRecipe(recipe, false); + std::shared_ptr recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(recipe, false); std::shared_ptr builtRecipe; bool writersNoted = false; for (;;) { std::shared_ptr prepared; - if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes() || htileClearCompletion.has_value()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); std::lock_guard gpuLock(GuestMemory::GpuMutex()); recordLabelsForPacket(localDevice.get(), queueId); @@ -485,7 +489,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, continue; } } else { - localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); + localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : &builtRecipe, htileClearCompletion); } break; } @@ -511,7 +515,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, std::shared_ptr recipeHit; if (cached && keepVariant != nullptr && !stampValidate()) { - recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); + recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); phaseTiming.Phase(PhaseRecipePrecheck); } @@ -589,7 +593,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { - localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : (attachTo != nullptr ? &builtRecipe : nullptr), htileClearCompletion); } } catch (const std::exception& error) { if (rethrow(error)) return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4dd977afd7..3a349500a2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4349,6 +4349,7 @@ struct RecordedDispatch { // A template hit's revalidate time, charged to the [rescache] refresh accounting. double revalidateMs; DispatchTimer* timer; + std::optional> htileClear; // Whether the data refresh recorded anything. bool refreshed = false; bool resourceKeyMatched = false; @@ -4609,8 +4610,8 @@ std::shared_ptr VulkanDevice::PrepareRecipe(const std::shared_ptr snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe)); +void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, std::optional> htileClear) { + static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe, {}, htileClear)); } VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup) { @@ -4873,17 +4874,22 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordMarks); // Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them // the batch can signal its labels from the GPU. - if (resources.NeedsCompletion() && resources.Reusable()) { + const bool completionNeeded = resources.NeedsCompletion() || record.htileClear.has_value(); + if (completionNeeded && resources.Reusable()) { auto kept = record.resources; - recorder.OnComplete([kept] { kept->CheckFaults(); }); - } else if (resources.NeedsCompletion()) { + recorder.OnComplete([kept, htileClear = record.htileClear] { + kept->CheckFaults(); + if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second); + }); + } else if (completionNeeded) { // Listed for DispatchIndirect until the write-back ran (a non-reusable object, so once). auto writers = state->copiedWriters; auto kept = record.resources; - recorder.OnComplete([kept, writers] { + recorder.OnComplete([kept, writers, htileClear = record.htileClear] { // Delisted before the write-back: one that fails must not keep indirect dispatches on the CPU. writers->erase(std::remove(writers->begin(), writers->end(), kept), writers->end()); kept->WriteBackBuffers(); + if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second); }); // Listed after the registration: a throw there leaves nothing that would pin the CPU path forever. if (resources.HasCopiedWrites()) writers->push_back(kept); @@ -5014,7 +5020,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap return finish(std::move(objects), nullptr); } -VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup) { +VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup, std::optional> htileClear) { startJournalReplay(); for (const auto& binding : shader.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; @@ -5266,7 +5272,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // works; the batch is waited for where the guest expects results (labels, flips, CPU reads). auto& recorder = *state->recorder; IndirectOutcome outcome{0, 0}; - RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; + RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer, htileClear}; record.resourceKeyMatched = fromCache; record.bindings = overlay; record.threadGroup = threadGroup; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 726701cb8c..54599a4f93 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -44,7 +44,8 @@ bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); -void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 1f7a5c129e..f83be002d2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -332,8 +332,7 @@ public: const auto& metadata = sampling.initial[1]; Require(GuestMemory::UnchangedSince(depth.address, depth.bytes, depth.generation), "guest writes over native multisample depth are not implemented"); Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); - if (clearKernelProvesCoverage) Require(GuestMemory::StoredOver(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no tracked HTILE write"); - else Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); + Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); Require(clearDepth == 0.0f || clearDepth == 1.0f, "native multisample depth clear value is not implemented"); auto current = sampling.initial; current[1].generation = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); @@ -346,7 +345,6 @@ public: RecordClear(VK_IMAGE_ASPECT_DEPTH_BIT); sampling.initial = current; clearGeneration = 0u; - clearKernelProvesCoverage = false; pendingClear = 0u; } Require(GuestMemory::UnchangedSinceAll(sampling.initial), "guest writes over native multisample depth are not implemented"); @@ -748,7 +746,6 @@ public: bool samplingActive = false; DepthSamplingState sampling; std::uint64_t clearGeneration = 0u; - bool clearKernelProvesCoverage = false; std::shared_ptr owned; std::shared_ptr sampleShadow; bool useSampleShadow = false; @@ -1928,10 +1925,11 @@ std::size_t HtileDepthClearBytes_nid_no_patch(std::span cod return address <= std::numeric_limits::max() - bytes ? static_cast(bytes) : 0u; } -void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { +bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { std::lock_guard lock(surfacesMutex()); forgetHtileReads(htileAddress, bytes); bool existing = false; + bool completionCandidate = false; for (const auto& surface : surfaces()) { if (!surface->active->load(std::memory_order_acquire)) continue; if (surface->samplingActive && surface->target.htileAddress == htileAddress) { @@ -1940,7 +1938,7 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz else if (surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); else { surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); - surface->clearKernelProvesCoverage = nativeBytes == bytes && nativeBytes >= metadata.bytes; + completionCandidate = true; } existing = true; continue; @@ -1951,6 +1949,18 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz existing = true; } if (!existing) clearedHtiles()[htileAddress] = bytes; + return completionCandidate; +} + +void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u) continue; + const auto& metadata = surface->sampling.initial[1]; + if (bytes < metadata.bytes) continue; + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, surface->clearGeneration), "CPU writes raced native multisample depth clear"); + GuestMemory::MarkWritten(metadata.address, metadata.bytes); + } } void ClearDepthSurfaces(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 34c34e0774..f4d4e03d5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -600,7 +600,7 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path"); } -enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore }; +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore }; void CheckClearDispatch() { const std::array code{ @@ -771,16 +771,17 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); - if (fault == ClearFault::PossibleStore) { + if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); } NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); - if (fault == ClearFault::PossibleStore) { + if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes); Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage"); + if (fault == ClearFault::CompletedPossibleStore) CompleteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); } else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; @@ -1144,6 +1145,7 @@ int main(int argc, char** argv) { std::pair{ClearFault::CpuWrite, "CPU writes raced"}, std::pair{ClearFault::DepthWrite, "guest writes"}, std::pair{ClearFault::BadMetadata, "does not prove its clear value"}, + std::pair{ClearFault::PossibleStore, "no complete tracked HTILE store"}, std::pair{ClearFault::UnprovenDispatch, "incomplete or unproven"}, std::pair{ClearFault::PendingWrite, "external GPU writes"}, std::pair{ClearFault::Overlap, "overlapping native multisample depth"}}) { @@ -1163,7 +1165,7 @@ int main(int argc, char** argv) { } } } - CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::PossibleStore); + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore); ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; From e424d57f9bd496fb14851d760c41f60c90990756 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 00:11:39 +0000 Subject: [PATCH 0178/1360] fix(agc): retain contained possible writes --- .../Execution/src/GuestMemory.cpp | 29 ++++++++++++------- .../libSceAgcDriver/tests/WriteTracking.cpp | 1 + 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index e88cf5b386..bcbc845d8d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -955,11 +955,9 @@ struct WriteTracker { struct DriverPieces { std::uint32_t whole = 0; std::uint32_t dropped = 0; - std::uint32_t next = 0; std::array pieces{}; std::uint32_t possibleWhole = 0; std::uint32_t possibleDropped = 0; - std::uint32_t possibleNext = 0; std::array possiblePieces{}; bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const { @@ -1043,7 +1041,6 @@ struct WriteTracker { } else { entry.possibleWhole = stampGeneration; entry.possibleDropped = 0; - entry.possibleNext = 0; entry.possiblePieces = {}; } return; @@ -1051,10 +1048,22 @@ struct WriteTracker { auto& entry = driverPieces[block]; auto& pieces = complete ? entry.pieces : entry.possiblePieces; auto& dropped = complete ? entry.dropped : entry.possibleDropped; - auto& next = complete ? entry.next : entry.possibleNext; - auto& slot = pieces[next++ % pieces.size()]; - dropped = std::max(dropped, slot.generation); - slot = {stampGeneration, from, to}; + DriverPiece* contained = nullptr; + for (auto& piece : pieces) { + if (piece.generation == 0 || piece.begin < from || piece.end > to) continue; + if (contained == nullptr) contained = &piece; + else piece = {}; + } + if (contained != nullptr) { + *contained = {stampGeneration, from, to}; + return; + } + auto* oldest = &pieces.front(); + for (auto& piece : pieces) { + if (piece.generation < oldest->generation) oldest = &piece; + } + dropped = std::max(dropped, oldest->generation); + *oldest = {stampGeneration, from, to}; } Leaf& leafFor(std::uint64_t block) { @@ -1729,11 +1738,11 @@ static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool const auto blockStop = (address + bytes + WriteBlockBytes - 1) & ~(std::uint64_t{WriteBlockBytes} - 1); StampRuns runs{tracker, StampKind::Fresh}; #ifdef _WIN32 - const bool complete = collectWindowsWrites(runs, blockFirst, blockStop, false); + const bool collected = collectWindowsWrites(runs, blockFirst, blockStop, false); #else - const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); + const bool collected = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); #endif - if (!complete) { + if (!collected) { lock.unlock(); beforeUnwatch(blockFirst, static_cast(blockStop - blockFirst)); lock.lock(); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 78e128d6ce..24ddbb588c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -221,6 +221,7 @@ void CheckStoredAllSince() { Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "a possible driver write proved complete byte coverage"); for (unsigned repeat = 0; repeat < 6; ++repeat) MarkPossiblyWritten(base + 2048, 16); Require(StoredOver(base + 2048, 16, before), "repeated possible writes lost their overlap evidence"); + Require(!StoredOver(base + 2064, 16, before), "repeated possible writes invalidated an adjacent range"); Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "repeated possible writes proved complete byte coverage"); before = TrackerGeneration(); From f32c3685f9034281279a898720073c73914c83a9 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 01:41:16 +0000 Subject: [PATCH 0179/1360] test(agc): cover possible-write eviction --- core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 24ddbb588c..fcca9157d7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -224,6 +224,17 @@ void CheckStoredAllSince() { Require(!StoredOver(base + 2064, 16, before), "repeated possible writes invalidated an adjacent range"); Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "repeated possible writes proved complete byte coverage"); + before = TrackerGeneration(); + constexpr unsigned PossibleIntervals = 8; + for (unsigned interval = 0; interval < PossibleIntervals; ++interval) { + MarkPossiblyWritten(base + 4096 + static_cast(interval) * 32, 16); + } + Require(!StoredOver(base + 4096, 16, before), "an evicted possible-write interval remained in the bounded history"); + Require(StoredOver(base + 4096 + (PossibleIntervals - 1) * 32, 16, before), "the newest possible-write interval was evicted"); + MarkPossiblyWritten(base + 4096, 16); + Require(StoredOver(base + 4096, 16, before), "a possible-write interval was not retained after being re-recorded"); + Require(!StoredAllSince_nid_no_patch(base + 4096, 16, before), "possible-write eviction or re-recording proved complete byte coverage"); + before = TrackerGeneration(); MarkWritten(base + 2560, 16); MarkPossiblyWritten(base, Block); From 6b5c0d5794be38314f57112486066597735a0dd9 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 02:41:32 +0000 Subject: [PATCH 0180/1360] test(agc): cover HTILE clear through driver dispatch --- .../execution/NativeMultisampleDepth.cpp | 87 ++++++++++++++----- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index f4d4e03d5e..b7038503fb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -7,6 +7,8 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -137,7 +139,7 @@ private: class TargetMemory { public: - explicit TargetMemory(std::size_t bytes) : bytes(bytes) { + explicit TargetMemory(std::size_t bytes, bool registerAllocation = false) : bytes(bytes), registered(registerAllocation) { #ifdef _WIN32 pointer = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536u); GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes); @@ -153,8 +155,10 @@ public: #endif Require(pointer != nullptr, "multisample target fixture allocation failed"); std::memset(pointer, 0xa5, bytes); + if (registered) GuestAllocations::Mutation().Add(pointer, bytes, true, true, true); } ~TargetMemory() { + if (registered) GuestAllocations::Mutation().Remove(pointer); #ifdef _WIN32 GuestArena::GuestArenaReset_nid_postfix(pointer, bytes); GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes); @@ -168,8 +172,43 @@ public: private: void* pointer = nullptr; std::size_t bytes; + bool registered = false; }; +alignas(256) constexpr std::array HtileMaskKernel{ + 0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, + 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, + 0xbf810000u, +}; + +std::vector DriverPm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1u) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void SubmitDriverHtileClear(std::uint64_t address) { + const auto shader = reinterpret_cast(HtileMaskKernel.data()); + std::vector commands; + const auto append = [&commands](std::vector packet) { commands.insert(commands.end(), packet.begin(), packet.end()); }; + append(DriverPm4Packet(0x76u, {0x20cu, static_cast(shader >> 8u)})); + append(DriverPm4Packet(0x76u, {0x20du, static_cast(shader >> 40u) & 0xffu})); + append(DriverPm4Packet(0x76u, {0x207u, 64u})); + append(DriverPm4Packet(0x76u, {0x208u, 1u})); + append(DriverPm4Packet(0x76u, {0x209u, 1u})); + append(DriverPm4Packet(0x76u, {0x213u, 8u << 1u})); + append(DriverPm4Packet(0x76u, {0x240u, 0u})); + append(DriverPm4Packet(0x76u, {0x241u, 0u})); + append(DriverPm4Packet(0x76u, {0x242u, static_cast(address)})); + append(DriverPm4Packet(0x76u, {0x243u, static_cast(address >> 32u)})); + append(DriverPm4Packet(0x76u, {0x244u, 0xf0u})); + append(DriverPm4Packet(0x76u, {0x245u, 0xf0u})); + append(DriverPm4Packet(0x15u, {static_cast(1u), 1u, 1u, 0x41u})); + Packet packet{commands.data(), static_cast(commands.size()), 0u, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "native depth HTILE clear: PM4 submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + template void TargetRefuses(Action action, std::string_view expected) { std::string message; @@ -600,7 +639,7 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path"); } -enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore }; +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch }; void CheckClearDispatch() { const std::array code{ @@ -652,7 +691,7 @@ void CheckClearDispatch() { void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); - TargetMemory memory(bytes), metadata(65536u); + TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); const auto metadataBytes = HtileMetadataBytes(extent); const bool depthOnly = depthOnlyOverride.value_or(one); const std::uint32_t pattern = one ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; @@ -770,25 +809,29 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, target.clearDepth = clearOne ? 1.0f : 0.0f; Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; - const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); - if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { - for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + if (fault == ClearFault::DriverDispatch && clearOne) { + SubmitDriverHtileClear(target.htileAddress); + } else { + const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); + if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { + for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + } + NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); + if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); + AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes); + Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage"); + if (fault == ClearFault::CompletedPossibleStore) CompleteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + } else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { + for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; + }); + if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); } - NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); - if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); - const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); - if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { - const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); - AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes); - Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage"); - if (fault == ClearFault::CompletedPossibleStore) CompleteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); - } else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { - for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); - if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; - }); - if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; - if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; - if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view"); recorder.Sync(); Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "completed native depth clear replaced an attachment or cached view"); @@ -1167,6 +1210,8 @@ int main(int argc, char** argv) { } CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore); ++checked; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch); + ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; } From 338ce07778a61f2f5a100c60f43446dabfbc2e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Sun, 18 Oct 2026 04:02:29 +0200 Subject: [PATCH 0181/1360] test(agc): an evicted possible interval marks its block as stored over The bounded-history check expected an evicted possible-write interval to read as untouched since the baseline. The tracker instead remembers the eviction: it raises the possible ring's dropped generation to the evicted piece's, and every byte of the block then reads as stored over since any older baseline, which is what keeps a forgotten write from being taken as unchanged memory. The check now requires that for the evicted interval and for an untouched gap beside it, checks that the newest and the re-recorded interval are retained against baselines taken just before their writes, and keeps the no-complete-coverage check. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index fcca9157d7..37f9a48c54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -226,13 +226,17 @@ void CheckStoredAllSince() { before = TrackerGeneration(); constexpr unsigned PossibleIntervals = 8; + auto beforeNewest = before; for (unsigned interval = 0; interval < PossibleIntervals; ++interval) { + beforeNewest = TrackerGeneration(); MarkPossiblyWritten(base + 4096 + static_cast(interval) * 32, 16); } - Require(!StoredOver(base + 4096, 16, before), "an evicted possible-write interval remained in the bounded history"); - Require(StoredOver(base + 4096 + (PossibleIntervals - 1) * 32, 16, before), "the newest possible-write interval was evicted"); + Require(StoredOver(base + 4096, 16, before), "an evicted possible-write interval was forgotten instead of marking its block"); + Require(StoredOver(base + 4096 + 16, 16, before), "an evicted possible write did not mark the rest of its block"); + Require(StoredOver(base + 4096 + (PossibleIntervals - 1) * 32, 16, beforeNewest), "the newest possible-write interval was evicted"); + const auto beforeRecord = TrackerGeneration(); MarkPossiblyWritten(base + 4096, 16); - Require(StoredOver(base + 4096, 16, before), "a possible-write interval was not retained after being re-recorded"); + Require(StoredOver(base + 4096, 16, beforeRecord), "a possible-write interval was not retained after being re-recorded"); Require(!StoredAllSince_nid_no_patch(base + 4096, 16, before), "possible-write eviction or re-recording proved complete byte coverage"); before = TrackerGeneration(); From 462b69e28c76e8e369d917c3b5ef0e70333f5218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Sun, 18 Oct 2026 03:15:04 +0200 Subject: [PATCH 0182/1360] test(agc): prove the possible ring evicts its oldest piece The bounded-history check stores eight disjoint ranges in generation order, so lowest-generation and round-robin eviction pick the same slots and it cannot tell them apart. This check fills a block's possible ring with four distinct ranges, refreshes the first in place twice, then adds a fifth range: the rest of the block must not read as stored over since the second range's own generation, which holds only when the second range, now the oldest piece, is the one evicted, and must read as stored over since the collect, which pins the dropped generation to it. A round-robin slot evicts the refreshed piece or the third range, and evicting the second or third oldest raises the dropped generation above the second range's. The same check covers an exact store's coverage surviving possible writes beyond the ring, and a possible write that overlaps an earlier piece without containing it, which replaces nothing and again evicts the oldest piece. Co-Authored-By: Claude Fable 5.1 --- .../libSceAgcDriver/tests/WriteTracking.cpp | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 37f9a48c54..ffe7eb3585 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -376,6 +376,42 @@ void CheckSharedBlockProofs() { Require(!UnchangedSince(base + Block + 14 * page + 0x800, 16, beforeNeighbour), "a range in the page an own store dirtied reads as unchanged"); } +void CheckRepeatedPossibleStores() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto boundary = base + 2 * Block - 0x9000; + std::memset(memory, 0x11, 3 * Block); + + const auto synced = CollectWrites(base, 3 * Block); + Require(synced != 0, "the test block is not collected"); + + const auto exact = TrackerGeneration(); + MarkWritten(base + Block, 0x100); + for (std::uint64_t store = 0; store < 5; ++store) MarkPossiblyWritten(boundary + store * 0x1000, 0x100); + Require(StoredAllSince_nid_no_patch(base + Block, 0x100, exact), "possible writes beyond the ring forgot an exact store's coverage"); + + void* fresh = AllocateWatched(2 * Block); + const auto freshBase = reinterpret_cast(fresh); + std::memset(fresh, 0x11, 2 * Block); + const auto freshSynced = CollectWrites(freshBase, 2 * Block); + const auto neighbour = freshBase + Block - 0x9000; + MarkPossiblyWritten(neighbour, 0x100); + const auto secondStored = MarkPossiblyWritten(neighbour + 0x1000, 0x100); + const auto thirdStored = MarkPossiblyWritten(neighbour + 0x2000, 0x100); + MarkPossiblyWritten(neighbour + 0x3000, 0x100); + const auto settled = TrackerGeneration(); + MarkPossiblyWritten(neighbour, 0x100); + MarkPossiblyWritten(neighbour, 0x100); + MarkPossiblyWritten(neighbour + 0x5000, 0x100); + Require(!StoredOver(freshBase, static_cast(neighbour - freshBase), secondStored), "a full possible ring evicted a newer piece instead of its oldest"); + Require(StoredOver(freshBase, static_cast(neighbour - freshBase), freshSynced), "an evicted possible write is not taken as a store over the rest of the block"); + + const auto partial = TrackerGeneration(); + MarkPossiblyWritten(neighbour + 0x5080, 0x100); + Require(StoredOver(neighbour + 0x5000, 0x10, settled) && StoredOver(neighbour + 0x5100, 0x10, partial), "a possible write that overlaps an earlier piece without containing it replaced it"); + Require(!StoredOver(freshBase, static_cast(neighbour - freshBase), thirdStored), "a possible write that overlaps without containing evicted a newer piece instead of the oldest"); +} + void CheckOwnStore() { void* memory = AllocateWatched(2 * Block); const auto base = reinterpret_cast(memory); @@ -1448,6 +1484,7 @@ int main() { CheckSharedBlock(); CheckStoredAllSince(); CheckSharedBlockProofs(); + CheckRepeatedPossibleStores(); CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); From f07ba57d69c1a7185c7e8dc600d31d4c549278a6 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 12:02:05 +0000 Subject: [PATCH 0183/1360] test(agc): correct native HTILE dispatch fixture --- .../tests/execution/NativeMultisampleDepth.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index b7038503fb..7397cacde4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -196,14 +196,14 @@ void SubmitDriverHtileClear(std::uint64_t address) { append(DriverPm4Packet(0x76u, {0x207u, 64u})); append(DriverPm4Packet(0x76u, {0x208u, 1u})); append(DriverPm4Packet(0x76u, {0x209u, 1u})); - append(DriverPm4Packet(0x76u, {0x213u, 8u << 1u})); + append(DriverPm4Packet(0x76u, {0x213u, (6u << 1u) | 0x80u})); append(DriverPm4Packet(0x76u, {0x240u, 0u})); append(DriverPm4Packet(0x76u, {0x241u, 0u})); append(DriverPm4Packet(0x76u, {0x242u, static_cast(address)})); append(DriverPm4Packet(0x76u, {0x243u, static_cast(address >> 32u)})); - append(DriverPm4Packet(0x76u, {0x244u, 0xf0u})); - append(DriverPm4Packet(0x76u, {0x245u, 0xf0u})); - append(DriverPm4Packet(0x15u, {static_cast(1u), 1u, 1u, 0x41u})); + append(DriverPm4Packet(0x76u, {0x244u, 0xfffc00f0u})); + append(DriverPm4Packet(0x76u, {0x245u, 0u})); + append(DriverPm4Packet(0x15u, {32u, 1u, 1u, 0x41u})); Packet packet{commands.data(), static_cast(commands.size()), 0u, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "native depth HTILE clear: PM4 submission failed"); AgcDriverWaitIdle_nid_postfix(); @@ -648,7 +648,7 @@ void CheckClearDispatch() { 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, 0xdc788000, 0x00020800, 0xbf810000, }; - const std::array userData{0u, 0u, 65536u, 0u, 0xf0u, 0xf0u}; + const std::array userData{0u, 0u, 65536u, 0u, 0xfffc00f0u, 0u}; const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; const std::array packet{0u, 32u, 1u, 1u, 1u}; Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == 32768u, "complete HTILE mask dispatch was not recognized"); From 64922bfa0afce1765fc943a2f1caba08c84c5a68 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 05:21:52 +0000 Subject: [PATCH 0184/1360] test(agc): release GPU lock while waiting for driver queue --- .../execution/NativeMultisampleDepth.cpp | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 7397cacde4..91b3aea6d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -187,7 +188,7 @@ std::vector DriverPm4Packet(std::uint32_t opcode, std::initialize return words; } -void SubmitDriverHtileClear(std::uint64_t address) { +void SubmitDriverHtileClear(std::uint64_t address, std::unique_lock& gpuLock) { const auto shader = reinterpret_cast(HtileMaskKernel.data()); std::vector commands; const auto append = [&commands](std::vector packet) { commands.insert(commands.end(), packet.begin(), packet.end()); }; @@ -206,7 +207,10 @@ void SubmitDriverHtileClear(std::uint64_t address) { append(DriverPm4Packet(0x15u, {32u, 1u, 1u, 0x41u})); Packet packet{commands.data(), static_cast(commands.size()), 0u, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "native depth HTILE clear: PM4 submission failed"); - AgcDriverWaitIdle_nid_postfix(); + gpuLock.unlock(); + try { AgcDriverWaitIdle_nid_postfix(); } + catch (...) { gpuLock.lock(); throw; } + gpuLock.lock(); } template @@ -688,7 +692,7 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, bool iterate256 = true) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); @@ -810,7 +814,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; if (fault == ClearFault::DriverDispatch && clearOne) { - SubmitDriverHtileClear(target.htileAddress); + SubmitDriverHtileClear(target.htileAddress, gpuLock); } else { const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { @@ -1180,7 +1184,7 @@ int main(int argc, char** argv) { } CheckDrawFeedback(); const auto context = device->GetContext(); - const std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex()); Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") { for (const auto& [fault, expected] : std::array{ @@ -1192,7 +1196,7 @@ int main(int argc, char** argv) { std::pair{ClearFault::UnprovenDispatch, "incomplete or unproven"}, std::pair{ClearFault::PendingWrite, "external GPU writes"}, std::pair{ClearFault::Overlap, "overlapping native multisample depth"}}) { - TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault); }, expected); + TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault, lock); }, expected); std::printf("Native depth clear fault=%u refused: %s\n", static_cast(fault), expected); } return 0; @@ -1204,13 +1208,13 @@ int main(int argc, char** argv) { for (const auto samples : {2u, 4u, 8u}) { if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } for (const bool depthOnly : {false, true}) { - for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly); ++checked; } + for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly, ClearFault::None, lock); ++checked; } } } } - CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore); + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore, lock); ++checked; - CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch); + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; @@ -1232,7 +1236,7 @@ int main(int argc, char** argv) { for (const auto samples : {2u, 4u, 8u}) { if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } for (const bool one : {false, true}) { - for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, iterate256); ++checked; } + for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, lock, iterate256); ++checked; } } CheckHtileRouting(context, format, samples); } From 05a213953cf0e36bf187c21979af0639f4dc2d18 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:24:19 +0000 Subject: [PATCH 0185/1360] test(agc): require HTILE dispatch completion at driver idle --- .../libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 91b3aea6d6..d361b355c8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -814,7 +814,9 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; if (fault == ClearFault::DriverDispatch && clearOne) { + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); SubmitDriverHtileClear(target.htileAddress, gpuLock); + Require(AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "driver idle returned before recording the completed HTILE dispatch"); } else { const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { From 71decdc7520b949059537899fafcfc1b26ad587c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:27:13 +0000 Subject: [PATCH 0186/1360] fix(agc): drain recorder during driver idle --- .../Execution/src/Driver/Lifecycle.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 55bff4e2ac..ea881102d1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -220,13 +220,17 @@ Driver::Driver() { void Driver::WaitIdle() { require(!onWorkerThread(), "worker cannot wait for itself"); - std::unique_lock lock(mutex); - const auto target = accepted; - ++idleWaiters; - changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); - --idleWaiters; - rethrowFailure(); - checkStopping(); + { + std::unique_lock lock(mutex); + const auto target = accepted; + ++idleWaiters; + changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); + --idleWaiters; + rethrowFailure(); + checkStopping(); + } + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) localDevice->WaitIdle(); } void Driver::CheckFailure() { From 61e5c407796dc5c2d1cadcee6a968e4156545460 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:51:20 +0000 Subject: [PATCH 0187/1360] fix(agc): arm HTILE clear baseline after imports --- .../Execution/src/VulkanDevice.cpp | 1 + .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 3a349500a2..46eeb3d736 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4870,6 +4870,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recorder.EndGpuTiming(trailingTiming); recorder.MarkCovered(dispatchedAccess); recordStep(PhaseRecordBind); + if (record.htileClear.has_value()) Graphics::ArmHtileDepthClear(record.htileClear->first, record.htileClear->second); resources.MarkGpuWrites(recorder); recordStep(PhaseRecordMarks); // Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 54599a4f93..3b95acbed8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -45,6 +45,7 @@ bool DepthSurfaceAt(const Context& context, std::span words std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index f83be002d2..d64d2e5054 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -322,6 +322,7 @@ public: const auto clearLease = clearGeneration == 0u ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); Require(active->load(std::memory_order_acquire), "the native multisample depth owner was released"); Require(sampling.refusal.empty(), sampling.refusal); + Require(!clearPending, "native multisample depth clear has not been recorded"); for (const auto& range : sampling.initial) { Require(GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) && GuestMemory::Watched(range.address, range.bytes), "native multisample depth requires watched accessible guest memory"); if (const auto* recorder = RecorderFor(context)) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "external GPU writes over native multisample depth are not implemented"); @@ -745,6 +746,7 @@ public: bool sampleable = true; bool samplingActive = false; DepthSamplingState sampling; + bool clearPending = false; std::uint64_t clearGeneration = 0u; std::shared_ptr owned; std::shared_ptr sampleShadow; @@ -1935,9 +1937,9 @@ bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz if (surface->samplingActive && surface->target.htileAddress == htileAddress) { const auto& metadata = surface->sampling.initial[1]; if (nativeBytes != bytes || nativeBytes < metadata.bytes || !GuestMemory::Accessible(reinterpret_cast(metadata.address), metadata.bytes) || !GuestMemory::Watched(metadata.address, metadata.bytes)) surface->sampling.Refuse("native multisample depth incomplete or unproven fast-clear transition is not implemented"); - else if (surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); + else if (surface->clearPending || surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); else { - surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + surface->clearPending = true; completionCandidate = true; } existing = true; @@ -1952,6 +1954,18 @@ bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz return completionCandidate; } +void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || !surface->clearPending) continue; + const auto& metadata = surface->sampling.initial[1]; + if (bytes < metadata.bytes) continue; + surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + surface->clearPending = false; + if (surface->clearGeneration == 0u) surface->sampling.Refuse("native multisample depth clear requires tracked HTILE generations"); + } +} + void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { std::lock_guard lock(surfacesMutex()); for (const auto& surface : surfaces()) { From 5bf1a0a4bd5301db139bb47fe773061f5bde38ff Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:51:23 +0000 Subject: [PATCH 0188/1360] test(agc): cover writes before HTILE clear baseline --- .../tests/execution/NativeMultisampleDepth.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index d361b355c8..96d401e528 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -643,7 +643,7 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path"); } -enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch }; +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite }; void CheckClearDispatch() { const std::array code{ @@ -822,7 +822,12 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); } - NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + const bool completionCandidate = NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + if (fault == ClearFault::ImportWindowWrite) { + metadata.Data()[0] ^= std::byte{0x01}; + Require(AgcDriver::GuestMemory::ImportWatched(target.htileAddress, static_cast(metadataBytes), [] { return true; }), "HTILE host import simulation failed"); + } + if (completionCandidate) ArmHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { @@ -1216,6 +1221,8 @@ int main(int argc, char** argv) { } CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore, lock); ++checked; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); + ++checked; CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); From 56be92c64b75c9aca0b2a300aa86f846c9e9aaee Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:01:25 +0000 Subject: [PATCH 0189/1360] test(agc): reproduce HTILE import-window stamp --- .../tests/execution/NativeMultisampleDepth.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 96d401e528..a5eb4b62c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -824,8 +824,12 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, } const bool completionCandidate = NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); if (fault == ClearFault::ImportWindowWrite) { - metadata.Data()[0] ^= std::byte{0x01}; - Require(AgcDriver::GuestMemory::ImportWatched(target.htileAddress, static_cast(metadataBytes), [] { return true; }), "HTILE host import simulation failed"); + const auto beforeImport = AgcDriver::GuestMemory::TrackerGeneration(); + Require(AgcDriver::GuestMemory::ImportWatched(target.htileAddress, static_cast(metadataBytes), [&] { + metadata.Data()[0] ^= std::byte{0x01}; + return true; + }), "HTILE host import simulation failed"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(target.htileAddress, static_cast(metadataBytes)) > beforeImport, "HTILE import window did not stamp its changed page"); } if (completionCandidate) ArmHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); From 60929f2930aa6e8dbe29251ec648af2c7615ecc8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:19:54 +0000 Subject: [PATCH 0190/1360] test(agc): restore recorder after driver idle --- .../libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index a5eb4b62c0..ce0bc83fd8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -816,6 +816,8 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, if (fault == ClearFault::DriverDispatch && clearOne) { const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); SubmitDriverHtileClear(target.htileAddress, gpuLock); + recorder.Activate(); + Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); Require(AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "driver idle returned before recording the completed HTILE dispatch"); } else { const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); From f2992f93bcb5d3e4348b40b04349039bc53ea7f1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:20:05 +0000 Subject: [PATCH 0191/1360] test(agc): pass BDA clear completion to dispatch --- .../tests/execution/NativeDepthBdaClear.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp index d1564e3562..d6b99df4f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include namespace AgcDriver { @@ -153,10 +155,12 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { device.WaitIdle(); target.clearDepth = 1.0f; Require(DepthSurfaceView(context, target) == view, "changing the native clear value replaced its attachment"); - NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes); + std::optional> htileClear; + if (NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes)) htileClear.emplace(target.htileAddress, metadataBytes); + Require(htileClear.has_value(), "the full BDA clear was not registered for completion"); const auto generation = Memory::TrackerGeneration(); userData[4] = 0xfffffff0u; - device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data()), nullptr, nullptr, htileClear); device.WaitIdle(); for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) { std::uint32_t value = 0; From 9f5823eeccf818af437dfd32b5168b0f57df1f7d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:50:40 +0000 Subject: [PATCH 0192/1360] test(agc): include possible writes in unchanged checks --- core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index ffe7eb3585..6bc07c0e24 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -218,6 +218,8 @@ void CheckStoredAllSince() { MarkPossiblyWritten(base + 2048, 16); Require(StoredOver(base + 2048, 16, before), "a possible driver write did not invalidate its exact range"); Require(!StoredOver(base + 2064, 16, before), "a possible driver write invalidated an adjacent range"); + Require(!UnchangedSince(base + 2048, 16, before), "a possible driver write left its exact range unchanged"); + Require(UnchangedSince(base + 2064, 16, before), "a possible driver write changed an adjacent range"); Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "a possible driver write proved complete byte coverage"); for (unsigned repeat = 0; repeat < 6; ++repeat) MarkPossiblyWritten(base + 2048, 16); Require(StoredOver(base + 2048, 16, before), "repeated possible writes lost their overlap evidence"); From 879949c1f8c0bbf1bdb42c87658fe31db2c4f93a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:50:57 +0000 Subject: [PATCH 0193/1360] fix(agc): include possible writes in unchanged checks --- core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index bcbc845d8d..7311dfda98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1174,10 +1174,13 @@ struct WriteTracker { continue; } const auto& entry = found->second; - if (entry.whole > generation || entry.dropped > generation) return true; + if (entry.whole > generation || entry.dropped > generation || entry.possibleWhole > generation || entry.possibleDropped > generation) return true; for (const auto& piece : entry.pieces) { if (piece.generation > generation && piece.begin < to && from < piece.end) return true; } + for (const auto& piece : entry.possiblePieces) { + if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + } } return false; } From b5fc32de9fc726100cf6e7d19d32c884063cf22d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:17:27 +0000 Subject: [PATCH 0194/1360] test(agc): keep stored-over checks page precise --- .../prx/libSceAgcDriver/tests/WriteTracking.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 6bc07c0e24..f66c71fbc5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -171,6 +171,21 @@ void CheckSharedBlock() { if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over"); } +void CheckStoredOverUsesPageEvidence() { + constexpr std::size_t Page = 4096; + auto* memory = static_cast(AllocateWatched(2 * Block)); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 2 * Block); + const auto before = CollectWritesUncached(base, 2 * Block); + Require(before != 0, "the stored-over page fixture was not collected"); + + static_cast(memory)[0] = 0x22; + const auto after = CollectWritesUncached(base, Block); + Require(after > before, "the page-local CPU write was not collected"); + Require(StoredOver(base, Page, before), "a collected CPU write on a page was not reported as stored over"); + Require(!StoredOver(base + Page, Page, before), "a page-local CPU write made an untouched page in the same block count as stored over"); +} + void CheckStoredAllSince() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(AllocateWatched(bytes)); @@ -1484,6 +1499,7 @@ int main() { } #endif CheckSharedBlock(); + CheckStoredOverUsesPageEvidence(); CheckStoredAllSince(); CheckSharedBlockProofs(); CheckRepeatedPossibleStores(); From 01222e06b3b7b4c4ccf6c2216e5085c19ca4d883 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:17:41 +0000 Subject: [PATCH 0195/1360] fix(agc): restore precise stored-over delegation --- .../Execution/src/GuestMemory.cpp | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 7311dfda98..26d113dba4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1780,36 +1780,7 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati #ifdef _WIN32 if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return true; #endif - const auto end = address + bytes; - const auto first = tracker.blockOf(address); - const auto last = tracker.blockOf(end - 1); - for (auto block = first; block <= last; ++block) { - if (tracker.stampOf(block) <= generation) continue; - if (const auto found = tracker.cpuPages.find(block); found != tracker.cpuPages.end()) { - const auto begin = tracker.blockBegin(block); - const auto from = std::max(address, begin) - begin; - const auto to = std::min(end, begin + WriteBlockBytes) - begin; - for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) { - if (found->second.pages[page] > generation) return true; - } - } else if (tracker.cpuStampOf(block) > generation) { - return true; - } - const auto found = tracker.driverPieces.find(block); - if (found == tracker.driverPieces.end()) return true; - const auto& entry = found->second; - if (entry.whole > generation || entry.dropped > generation || entry.possibleWhole > generation || entry.possibleDropped > generation) return true; - const auto begin = tracker.blockBegin(block); - const auto from = std::max(address, begin) - begin; - const auto to = std::min(end, begin + WriteBlockBytes) - begin; - for (const auto& piece : entry.pieces) { - if (piece.generation > generation && piece.begin < to && from < piece.end) return true; - } - for (const auto& piece : entry.possiblePieces) { - if (piece.generation > generation && piece.begin < to && from < piece.end) return true; - } - } - return false; + return tracker.storedOver(address, address + bytes, generation, false); } bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { From 2ca0a77ca4c06f5a11ad4fc6cc544c5982ede43a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:09:17 +0000 Subject: [PATCH 0196/1360] test(agc): skip HTILE dispatch when write tracking is unsuitable --- .../tests/execution/NativeMultisampleDepth.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index ce0bc83fd8..0e8ec6ef64 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -692,7 +692,7 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { +bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); @@ -818,6 +818,10 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, SubmitDriverHtileClear(target.htileAddress, gpuLock); recorder.Activate(); Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); + if (!AgcDriver::GuestMemory::UnchangedSinceCollected(target.htileAddress, metadataBytes, generation)) { + std::puts("skipped native HTILE driver dispatch: this device's host import path does not retain write-watch evidence independently of driver writes"); + return false; + } Require(AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "driver idle returned before recording the completed HTILE dispatch"); } else { const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); @@ -909,6 +913,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, texture.reset(); arrayTexture.reset(); std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one, iterate256); + return true; } void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { @@ -1229,7 +1234,7 @@ int main(int argc, char** argv) { ++checked; CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); ++checked; - CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); + if (!CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock)) return VulkanTestSkipped; ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; From 85e5d16e07e7e137b46d8f1cecf3165377e27538 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:29:47 +0000 Subject: [PATCH 0197/1360] test(agc): retain supported HTILE dispatch coverage --- .../tests/execution/NativeMultisampleDepth.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 0e8ec6ef64..efc2a5e027 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -818,11 +818,13 @@ bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, SubmitDriverHtileClear(target.htileAddress, gpuLock); recorder.Activate(); Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); - if (!AgcDriver::GuestMemory::UnchangedSinceCollected(target.htileAddress, metadataBytes, generation)) { + const bool unchangedSinceSubmit = AgcDriver::GuestMemory::UnchangedSinceCollected(target.htileAddress, metadataBytes, generation); + const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); + if (!unchangedSinceSubmit && !storedAll) { std::puts("skipped native HTILE driver dispatch: this device's host import path does not retain write-watch evidence independently of driver writes"); return false; } - Require(AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "driver idle returned before recording the completed HTILE dispatch"); + Require(storedAll, "driver idle returned before recording the completed HTILE dispatch"); } else { const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { From dc960cf76594cf96ac07b804ddd3f310ecc47c9d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:11:59 +0000 Subject: [PATCH 0198/1360] test(agc): only skip HTILE evidence check on CPU devices --- .../tests/execution/NativeMultisampleDepth.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index efc2a5e027..7ebc8704fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -78,6 +78,7 @@ public: Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); VkPhysicalDeviceProperties selectedProperties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &selectedProperties); + cpuDevice = selectedProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU; std::printf("Selected GPU name=%s vendor=0x%x device=0x%x driver=%u\n", selectedProperties.deviceName, selectedProperties.vendorID, selectedProperties.deviceID, selectedProperties.driverVersion); const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); queues(context.physical, &count, nullptr); @@ -114,6 +115,7 @@ public: } ~Device() { release(); } Context GetContext() const { return context; } + bool IsCpu() const { return cpuDevice; } private: template TFunction function(const char* name) const { const auto result = reinterpret_cast(proc(instance, name)); @@ -136,6 +138,7 @@ private: PFN_vkGetInstanceProcAddr proc = nullptr; VkInstance instance = VK_NULL_HANDLE; Context context{}; + bool cpuDevice = false; }; class TargetMemory { @@ -692,7 +695,7 @@ void CheckClearDispatch() { } } -bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { +bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true, bool cpuDevice = false) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); @@ -820,7 +823,7 @@ bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); const bool unchangedSinceSubmit = AgcDriver::GuestMemory::UnchangedSinceCollected(target.htileAddress, metadataBytes, generation); const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); - if (!unchangedSinceSubmit && !storedAll) { + if (cpuDevice && !unchangedSinceSubmit && !storedAll) { std::puts("skipped native HTILE driver dispatch: this device's host import path does not retain write-watch evidence independently of driver writes"); return false; } @@ -1236,7 +1239,7 @@ int main(int argc, char** argv) { ++checked; CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); ++checked; - if (!CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock)) return VulkanTestSkipped; + if (!CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock, true, device->IsCpu())) return VulkanTestSkipped; ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; From db3c47c87a4d6fae308dd5cdf647dc7e5a053423 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 18:55:11 -0300 Subject: [PATCH 0199/1360] fix(libc): follow the default heap's alignment, errno and realloc to zero as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libc/include/ApplicationHeap.hpp | 1 + core/libs/prx/libc/src/ApplicationHeap.cpp | 18 +++- core/libs/prx/libc/src/FileAndHeap.cpp | 71 +++++++++++--- core/libs/prx/libc/src/GuestHeap.cpp | 17 ++-- core/libs/tests/ApplicationHeap.cpp | 6 +- core/libs/tests/GuestAllocatorFailures.cpp | 96 ++++++++++++++----- core/libs/tests/GuestHeapRounding.cpp | 8 +- core/libs/tests/GuestReallocalign.cpp | 47 +++++++-- docs/dev/debt/libc.md | 4 +- 9 files changed, 207 insertions(+), 61 deletions(-) diff --git a/core/libs/prx/libc/include/ApplicationHeap.hpp b/core/libs/prx/libc/include/ApplicationHeap.hpp index 00033a1d59..bd604dd95a 100644 --- a/core/libs/prx/libc/include/ApplicationHeap.hpp +++ b/core/libs/prx/libc/include/ApplicationHeap.hpp @@ -9,6 +9,7 @@ const void* ApplicationProcessParameters_nid_no_patch(); bool ApplicationGuestHeapApi_nid_no_patch(void** api); void ApplicationHeapInitialize_nid_no_patch(const void* processParameters); void ApplicationHeapRegister_nid_no_patch(void* const* api); +bool ApplicationHeapDefault_nid_no_patch(); void* ApplicationHeapAllocate_nid_no_patch(std::size_t bytes); void ApplicationHeapFree_nid_no_patch(void* pointer); void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes); diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 84a8f46eb7..5fc17a5651 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -27,6 +28,7 @@ using UsableSize = std::size_t (APS5_VABI *)(void*); std::mutex heapMutex; std::array heapApi{}; +std::atomic heapKind{0}; std::once_flag heapInitialization; std::exception_ptr heapFailure; Initialize heapFinalize = nullptr; @@ -145,6 +147,18 @@ void ApplicationHeapRegister_nid_no_patch(void* const* api) { if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] != nullptr && heapApi != replacement) throw std::runtime_error("application heap: cannot replace an active allocator"); heapApi = replacement; + heapKind.store(replacement == defaultApi() ? 1 : 2, std::memory_order_release); +} + +bool ApplicationHeapDefault_nid_no_patch() { + int kind = heapKind.load(std::memory_order_acquire); + if (kind == 0) { + std::unique_lock lock(heapMutex); + ensureInitialized(lock); + if (heapFailure) std::rethrow_exception(heapFailure); + kind = heapKind.load(std::memory_order_acquire); + } + return kind == 1; } void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { @@ -192,7 +206,7 @@ void ApplicationHeapFree_nid_no_patch(void* pointer) { } void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { - if (bytes == 0 && pointer != nullptr) { + if (bytes == 0 && pointer != nullptr && !ApplicationHeapDefault_nid_no_patch()) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } @@ -227,7 +241,7 @@ extern "C" void APS5_VABI scriptingFreeMem_nid_postfix(void* pointer) { } void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std::size_t alignment) { - if (bytes == 0) { + if (bytes == 0 && !ApplicationHeapDefault_nid_no_patch()) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 4b8f5328ba..d1349613d5 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -252,13 +253,13 @@ static void SortElements(unsigned char* base, size_t count, size_t size, Compare namespace { template -void* allocationOrNull(TAllocate allocate) { +void* allocationOrNull(TAllocate allocate, int error = 12) { try { return allocate(); } catch (const std::bad_alloc&) { } catch (const std::length_error&) { } - errno = 12; + errno = error; return nullptr; } @@ -266,6 +267,32 @@ bool validAlignment(std::size_t alignment) { return alignment != 0 && (alignment & (alignment - 1)) == 0; } +constexpr std::size_t DefaultHeapAlignment = 32; +constexpr std::size_t DefaultHeapAlignmentLimit = std::size_t{1} << 63; +constexpr std::size_t DefaultHeapOverhead = 0x1008f; +constexpr std::size_t DefaultHeapAlignedOverhead = 0x100af; + +std::size_t defaultHeapAlignment(std::size_t alignment) { + return std::bit_ceil(std::max(alignment, DefaultHeapAlignment)); +} + +bool defaultHeapRequestOverflows(std::size_t bytes, std::size_t alignment) { + const std::size_t overhead = alignment <= DefaultHeapAlignment ? DefaultHeapOverhead : defaultHeapAlignment(alignment) + DefaultHeapAlignedOverhead; + return bytes > std::numeric_limits::max() - overhead; +} + +template +void* defaultHeapAllocation(std::size_t bytes, std::size_t alignment, TAllocate allocate) { + if (defaultHeapRequestOverflows(bytes, alignment)) return nullptr; + return allocationOrNull(allocate, 22); +} + +template +void* heapAllocation(std::size_t bytes, TAllocate allocate) { + if (!ApplicationHeapDefault_nid_no_patch()) return allocationOrNull(allocate); + return defaultHeapAllocation(bytes, 0, allocate); +} + } extern "C" { @@ -546,7 +573,7 @@ size_t APS5_VABI malloc_usable_size_nid_postfix(void* pointer) { } void* APS5_VABI malloc_nid_postfix(size_t size) { - return allocationOrNull([size] { return ApplicationHeapAllocate_nid_no_patch(size); }); + return heapAllocation(size, [size] { return ApplicationHeapAllocate_nid_no_patch(size); }); } void APS5_VABI free_nid_postfix(void* ptr) { @@ -554,7 +581,7 @@ void APS5_VABI free_nid_postfix(void* ptr) { } void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { - return allocationOrNull([ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); + return heapAllocation(newSize, [ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); } void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { @@ -569,34 +596,50 @@ void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { - if (!validAlignment(alignment)) { - errno = 22; - return nullptr; + if (!ApplicationHeapDefault_nid_no_patch()) { + if (!validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); } - return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); + if (alignment >= DefaultHeapAlignmentLimit) return nullptr; + const std::size_t rounded = defaultHeapAlignment(alignment); + return defaultHeapAllocation(size, alignment, [rounded, size] { return ApplicationHeapAlign_nid_no_patch(rounded, size); }); } void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size) { + if (ApplicationHeapDefault_nid_no_patch() && !validAlignment(alignment)) return nullptr; return memalign_nid_postfix(alignment, size); } void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment) { - if (size != 0 && !validAlignment(alignment)) { - errno = 22; - return nullptr; + if (!ApplicationHeapDefault_nid_no_patch()) { + if (size != 0 && !validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); } - return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); + if (alignment >= DefaultHeapAlignmentLimit) return nullptr; + const std::size_t rounded = defaultHeapAlignment(alignment); + return defaultHeapAllocation(size, alignment, [ptr, size, rounded] { return ApplicationHeapRealign_nid_no_patch(ptr, size, rounded); }); } void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { - return allocationOrNull([count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }); + const auto allocate = [count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }; + if (!ApplicationHeapDefault_nid_no_patch()) return allocationOrNull(allocate); + if (size != 0 && count > std::numeric_limits::max() / size) return nullptr; + return defaultHeapAllocation(count * size, 0, allocate); } 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; const int result = ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); - errno = savedError; + const bool failureSetsError = result != 0 && ApplicationHeapDefault_nid_no_patch() && alignment < DefaultHeapAlignmentLimit && + !defaultHeapRequestOverflows(size, alignment); + errno = failureSetsError ? 22 : savedError; return result; } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index 96c95d1063..6277174a21 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -280,6 +280,12 @@ void* allocate(GuestAllocations::Mutation& mutation, std::size_t alignment, std: return pointer; } +void* emptyInPlace(GuestAllocations::Mutation& mutation, void* pointer, bool gpu) { + mutation.Remove(pointer); + mutation.Add(pointer, 0, true, true, gpu); + return pointer; +} + void free(GuestAllocations::Mutation& mutation, void* pointer) { mutation.Remove(pointer); const auto blockBytes = reinterpret_cast(pointer)[-1]; @@ -307,10 +313,7 @@ void* GuestHeapReallocate_nid_postfix(void* pointer, std::size_t bytes) { GuestAllocations::Mutation mutation; const auto range = mutation.Find(pointer); mutation.RequireUnpinned(pointer, range.bytes); - if (bytes == 0) { - free(mutation, pointer); - return nullptr; - } + if (bytes == 0) return emptyInPlace(mutation, pointer, range.gpu); void* result = allocate(mutation, MinimumAlignment, bytes, range.gpu); std::memcpy(result, pointer, std::min(bytes, range.bytes)); free(mutation, pointer); @@ -323,8 +326,10 @@ void* GuestHeapRealign_nid_postfix(void* pointer, std::size_t bytes, std::size_t const auto range = mutation.Find(pointer); mutation.RequireUnpinned(pointer, range.bytes); if (bytes == 0) { - free(mutation, pointer); - return nullptr; + if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest heap alignment"); + if (reinterpret_cast(pointer) % alignment != 0) + throw std::invalid_argument("reallocalign to 0 bytes with an alignment the block does not have is not measured on the console"); + return emptyInPlace(mutation, pointer, range.gpu); } void* result = allocate(mutation, alignment, bytes, range.gpu); std::memcpy(result, pointer, std::min(bytes, range.bytes)); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index be99345d50..3f36d640ef 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -303,7 +303,8 @@ int main(int argc, char** argv) { for (unsigned i = 0; i < 63; ++i) require(pointer[i] == 0x5a); pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(pointer, 11)); for (unsigned i = 0; i < 11; ++i) require(pointer[i] == 0x5a); - require(ApplicationHeapReallocate_nid_no_patch(pointer, 0) == nullptr); + require(ApplicationHeapReallocate_nid_no_patch(pointer, 0) == pointer && ApplicationHeapUsableSize_nid_no_patch(pointer) == 0); + ApplicationHeapFree_nid_no_patch(pointer); pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); @@ -317,7 +318,8 @@ int main(int argc, char** argv) { require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); *__error_nid_postfix() = 0; - require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + require(reallocf_nid_postfix(pointer, 0) == pointer && *__error_nid_postfix() == 0 && ApplicationHeapUsableSize_nid_no_patch(pointer) == 0); + ApplicationHeapFree_nid_no_patch(pointer); reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); require(pointer != nullptr); diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 5312249a50..ef2ae3d380 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -19,6 +19,7 @@ void* APS5_VABI realloc_nid_postfix(void*, std::size_t); void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); void* APS5_VABI aligned_alloc_nid_postfix(std::size_t, std::size_t); int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +std::size_t APS5_VABI malloc_usable_size_nid_postfix(void*); void APS5_VABI free_nid_postfix(void*); int* APS5_VABI __error_nid_postfix(); } @@ -27,8 +28,10 @@ namespace { constexpr int NoMemory = 12; constexpr int InvalidArgument = 22; +constexpr int Untouched = 77; constexpr std::size_t Max = std::numeric_limits::max(); constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits::max()) + 1; +constexpr std::size_t LastPlainFailure = Max - 0x1008f; int failures = 0; @@ -50,7 +53,7 @@ void report(const char* what, const char* detail) { template void expectNull(const char* what, int expectedError, TCall call) { - *__error_nid_postfix() = 0; + *__error_nid_postfix() = Untouched; void* result = nullptr; try { result = call(); @@ -95,27 +98,57 @@ int main() { void* api[10]{}; ApplicationHeapRegister_nid_no_patch(api); - expectNull("malloc(SIZE_MAX)", NoMemory, [] { return malloc_nid_postfix(Max); }); - expectNull("malloc(PTRDIFF_MAX + 1)", NoMemory, [] { return malloc_nid_postfix(HalfPlusOne); }); - expectNull("calloc(SIZE_MAX, 2)", NoMemory, [] { return calloc_nid_postfix(Max, 2); }); - expectNull("calloc(2^33, 2^33)", NoMemory, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); - expectNull("calloc(PTRDIFF_MAX + 1, 1)", NoMemory, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); - expectNull("realloc(NULL, SIZE_MAX)", NoMemory, [] { return realloc_nid_postfix(nullptr, Max); }); - expectNull("aligned_alloc(64, SIZE_MAX)", NoMemory, [] { return aligned_alloc_nid_postfix(64, Max); }); - expectNull("memalign(64, PTRDIFF_MAX + 1)", NoMemory, [] { return memalign_nid_postfix(64, HalfPlusOne); }); - expectNull("aligned_alloc(24, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(24, 64); }); - expectNull("aligned_alloc(0, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(0, 64); }); - expectNull("memalign(24, 64)", InvalidArgument, [] { return memalign_nid_postfix(24, 64); }); + expectNull("malloc(SIZE_MAX)", Untouched, [] { return malloc_nid_postfix(Max); }); + expectNull("malloc(SIZE_MAX - 0x1008e)", Untouched, [] { return malloc_nid_postfix(LastPlainFailure + 1); }); + expectNull("malloc(SIZE_MAX - 0x1008f)", InvalidArgument, [] { return malloc_nid_postfix(LastPlainFailure); }); + expectNull("malloc(PTRDIFF_MAX + 1)", InvalidArgument, [] { return malloc_nid_postfix(HalfPlusOne); }); + expectNull("calloc(SIZE_MAX, 2)", Untouched, [] { return calloc_nid_postfix(Max, 2); }); + expectNull("calloc(2^33, 2^33)", Untouched, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); + expectNull("calloc(1, SIZE_MAX - 0x1008e)", Untouched, [] { return calloc_nid_postfix(1, LastPlainFailure + 1); }); + expectNull("calloc(PTRDIFF_MAX + 1, 1)", InvalidArgument, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); + expectNull("realloc(NULL, SIZE_MAX)", Untouched, [] { return realloc_nid_postfix(nullptr, Max); }); + expectNull("realloc(NULL, PTRDIFF_MAX + 1)", InvalidArgument, [] { return realloc_nid_postfix(nullptr, HalfPlusOne); }); + expectNull("aligned_alloc(64, SIZE_MAX)", Untouched, [] { return aligned_alloc_nid_postfix(64, Max); }); + expectNull("aligned_alloc(16, PTRDIFF_MAX + 1)", InvalidArgument, [] { return aligned_alloc_nid_postfix(16, HalfPlusOne); }); + expectNull("memalign(64, PTRDIFF_MAX + 1)", InvalidArgument, [] { return memalign_nid_postfix(64, HalfPlusOne); }); + expectNull("memalign(4096, SIZE_MAX - 0x110af)", InvalidArgument, [] { return memalign_nid_postfix(4096, Max - 0x110af); }); + expectNull("memalign(4096, SIZE_MAX - 0x110ae)", Untouched, [] { return memalign_nid_postfix(4096, Max - 0x110ae); }); + expectNull("memalign(100, SIZE_MAX - 0x1012f)", InvalidArgument, [] { return memalign_nid_postfix(100, Max - 0x1012f); }); + expectNull("memalign(100, SIZE_MAX - 0x1012e)", Untouched, [] { return memalign_nid_postfix(100, Max - 0x1012e); }); + expectNull("memalign(3, SIZE_MAX - 0x1008f)", InvalidArgument, [] { return memalign_nid_postfix(3, LastPlainFailure); }); + expectNull("memalign(3, SIZE_MAX - 0x1008e)", Untouched, [] { return memalign_nid_postfix(3, LastPlainFailure + 1); }); + expectNull("memalign(2^63, 16)", Untouched, [] { return memalign_nid_postfix(HalfPlusOne, 16); }); + expectNull("memalign(SIZE_MAX, 16)", Untouched, [] { return memalign_nid_postfix(Max, 16); }); + expectNull("memalign(2^62 + 1, 16)", InvalidArgument, [] { return memalign_nid_postfix((std::size_t{1} << 62) + 1, 16); }); + expectNull("aligned_alloc(24, 64)", Untouched, [] { return aligned_alloc_nid_postfix(24, 64); }); + expectNull("aligned_alloc(0, 64)", Untouched, [] { return aligned_alloc_nid_postfix(0, 64); }); + expectNull("aligned_alloc(2^63, 16)", Untouched, [] { return aligned_alloc_nid_postfix(HalfPlusOne, 16); }); + + struct RoundedAlignment { + std::size_t requested; + std::size_t guaranteed; + }; + for (const RoundedAlignment& alignment : {RoundedAlignment{0, 32}, RoundedAlignment{3, 32}, RoundedAlignment{24, 32}, RoundedAlignment{48, 64}, + RoundedAlignment{100, 128}, RoundedAlignment{3000, 4096}}) { + void* aligned = expectBlock("memalign with an alignment that is not a power of two", [&alignment] { return memalign_nid_postfix(alignment.requested, 40); }); + if (aligned != nullptr && reinterpret_cast(aligned) % alignment.guaranteed != 0) report("memalign", "did not round the alignment up to a power of two"); + free_nid_postfix(aligned); + } auto* block = static_cast(expectBlock("malloc(40)", [] { return malloc_nid_postfix(40); })); if (block != nullptr) { std::memset(block, 0x6b, 40); - expectNull("realloc(block, SIZE_MAX)", NoMemory, [block] { return realloc_nid_postfix(block, Max); }); - expectNull("realloc(block, PTRDIFF_MAX + 1)", NoMemory, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); + expectNull("realloc(block, SIZE_MAX)", Untouched, [block] { return realloc_nid_postfix(block, Max); }); + expectNull("realloc(block, PTRDIFF_MAX + 1)", InvalidArgument, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); for (unsigned index = 0; index < 40; ++index) { if (block[index] != 0x6b) report("realloc failure", "damaged the original block"); } - free_nid_postfix(block); + void* emptied = expectBlock("realloc(block, 0)", [block] { return realloc_nid_postfix(block, 0); }); + if (emptied != block) report("realloc(block, 0)", "did not return the same block"); + if (emptied != nullptr && malloc_usable_size_nid_postfix(emptied) != 0) report("realloc(block, 0)", "left usable bytes in the block"); + void* regrown = expectBlock("realloc(emptied, 64)", [emptied] { return realloc_nid_postfix(emptied, 64); }); + if (regrown != nullptr && malloc_usable_size_nid_postfix(regrown) != 64) report("realloc(emptied, 64)", "did not hold 64 bytes"); + free_nid_postfix(regrown); } #ifdef ANYPS5_ENABLE_ASAN @@ -154,8 +187,8 @@ int main() { if (huge != nullptr) { free_nid_postfix(huge); committed = true; - } else if (*__error_nid_postfix() != NoMemory) { - report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } else if (*__error_nid_postfix() != InvalidArgument) { + report("malloc(300 GiB)", "returned NULL without EINVAL"); } else { ++failedCommits; } @@ -177,7 +210,7 @@ int main() { std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); #endif } else { - if (failedCommits != 10) report("failed heap commits", "did not return ENOMEM for all ten attempts"); + if (failedCommits != 10) report("failed heap commits", "did not return EINVAL for all ten attempts"); void* after = nullptr; try { after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); @@ -198,12 +231,27 @@ int main() { } #endif - void* untouched = &failures; - *__error_nid_postfix() = 77; - if (posix_memalign_nid_postfix(&untouched, HalfPlusOne, 8) != NoMemory) report("posix_memalign(PTRDIFF_MAX + 1)", "did not return ENOMEM"); - if (posix_memalign_nid_postfix(&untouched, 64, HalfPlusOne) != NoMemory) report("posix_memalign(64, PTRDIFF_MAX + 1)", "did not return ENOMEM"); - if (posix_memalign_nid_postfix(&untouched, 64, Max) != NoMemory) report("posix_memalign(64, SIZE_MAX)", "did not return ENOMEM"); - if (untouched != &failures || *__error_nid_postfix() != 77) report("posix_memalign failure", "touched the output or errno"); + struct PosixCase { + const char* what; + std::size_t alignment; + std::size_t size; + int result; + int error; + }; + for (const PosixCase& check : {PosixCase{"posix_memalign(PTRDIFF_MAX + 1, 8)", HalfPlusOne, 8, NoMemory, Untouched}, + PosixCase{"posix_memalign(64, PTRDIFF_MAX + 1)", 64, HalfPlusOne, NoMemory, InvalidArgument}, + PosixCase{"posix_memalign(64, SIZE_MAX)", 64, Max, NoMemory, Untouched}, + PosixCase{"posix_memalign(64, SIZE_MAX - 0x100ef)", 64, Max - 0x100ef, NoMemory, InvalidArgument}, + PosixCase{"posix_memalign(64, SIZE_MAX - 0x100ee)", 64, Max - 0x100ee, NoMemory, Untouched}, + PosixCase{"posix_memalign(4, 8)", 4, 8, InvalidArgument, Untouched}, PosixCase{"posix_memalign(24, 8)", 24, 8, InvalidArgument, Untouched}, + PosixCase{"posix_memalign(PTRDIFF_MAX + 2, 8)", HalfPlusOne + 1, 8, InvalidArgument, Untouched}}) { + void* untouched = &failures; + *__error_nid_postfix() = Untouched; + const int result = posix_memalign_nid_postfix(&untouched, check.alignment, check.size); + if (result != check.result) report(check.what, "returned the wrong error number"); + if (*__error_nid_postfix() != check.error) report(check.what, "left the wrong errno"); + if (untouched != &failures) report(check.what, "touched the output"); + } free_nid_postfix(expectBlock("calloc(3, 5)", [] { return calloc_nid_postfix(3, 5); })); free_nid_postfix(expectBlock("calloc(0, 5)", [] { return calloc_nid_postfix(0, 5); })); diff --git a/core/libs/tests/GuestHeapRounding.cpp b/core/libs/tests/GuestHeapRounding.cpp index d888f7311f..f27c870d0d 100644 --- a/core/libs/tests/GuestHeapRounding.cpp +++ b/core/libs/tests/GuestHeapRounding.cpp @@ -77,14 +77,14 @@ DWORD runChild(const wchar_t* mode) { int runMallocOverflow() { registerDefaultHeap(); - *__error_nid_postfix() = 0; + *__error_nid_postfix() = 77; try { auto* pointer = malloc_nid_postfix(std::numeric_limits::max() - HeaderBytes); if (pointer != nullptr) { free_nid_postfix(pointer); return 1; } - return *__error_nid_postfix() == 12 ? 0 : 4; + return *__error_nid_postfix() == 77 ? 0 : 4; } catch (const std::exception& error) { std::fprintf(stderr, "malloc failed with an unexpected exception: %s\n", error.what()); return 2; @@ -100,14 +100,14 @@ int runReallocOverflow() { auto* original = static_cast(malloc_nid_postfix(bytes)); std::memset(original, 0x5a, bytes); bool rejectedWithExpectedError = false; - *__error_nid_postfix() = 0; + *__error_nid_postfix() = 77; try { auto* result = realloc_nid_postfix(original, std::numeric_limits::max() - HeaderBytes); if (result != nullptr) { free_nid_postfix(result); return 4; } - rejectedWithExpectedError = *__error_nid_postfix() == 12; + rejectedWithExpectedError = *__error_nid_postfix() == 77; } catch (const std::exception& error) { std::fprintf(stderr, "realloc failed with an unexpected exception: %s\n", error.what()); } catch (...) { diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index fded34f311..cd36081ec0 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -7,6 +7,8 @@ extern "C" { void* APS5_VABI reallocalign_nid_postfix(void*, std::size_t, std::size_t); +std::size_t APS5_VABI malloc_usable_size_nid_postfix(void*); +void APS5_VABI free_nid_postfix(void*); int* APS5_VABI __error_nid_postfix(); } @@ -30,12 +32,43 @@ int main() { auto* shrunk = static_cast(reallocalign_nid_postfix(grown, 16, 16)); Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 16) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - *__error_nid_postfix() = 0; - Require(reallocalign_nid_postfix(shrunk, 16, 0) == nullptr && *__error_nid_postfix() == 22); - *__error_nid_postfix() = 0; - Require(reallocalign_nid_postfix(shrunk, 16, 3) == nullptr && *__error_nid_postfix() == 22); - *__error_nid_postfix() = 0; - Require(reallocalign_nid_postfix(shrunk, std::numeric_limits::max(), 16) == nullptr && *__error_nid_postfix() == 12); + shrunk = static_cast(reallocalign_nid_postfix(shrunk, 16, 0)); + Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 32) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - Require(reallocalign_nid_postfix(shrunk, 0, 16) == nullptr); + shrunk = static_cast(reallocalign_nid_postfix(shrunk, 40, 100)); + Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 128) == 0); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); + constexpr std::size_t max = std::numeric_limits::max(); + *__error_nid_postfix() = 77; + Require(reallocalign_nid_postfix(shrunk, max, 16) == nullptr && *__error_nid_postfix() == 77); + Require(reallocalign_nid_postfix(shrunk, max - 0x1008f, 16) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 77; + Require(reallocalign_nid_postfix(shrunk, max - 0x110ae, 4096) == nullptr && *__error_nid_postfix() == 77); + Require(reallocalign_nid_postfix(shrunk, max - 0x110af, 4096) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 77; + Require(reallocalign_nid_postfix(shrunk, 16, std::size_t{1} << 63) == nullptr && *__error_nid_postfix() == 77); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); + Require(reallocalign_nid_postfix(shrunk, 0, 16) == shrunk && malloc_usable_size_nid_postfix(shrunk) == 0); + free_nid_postfix(shrunk); + void* empty = reallocalign_nid_postfix(nullptr, 0, 0); + Require(empty != nullptr); + free_nid_postfix(empty); + for (std::size_t attempt = 0; attempt < 64; ++attempt) { + void* loose = reallocalign_nid_postfix(nullptr, 40, 32); + Require(loose != nullptr); + if (reinterpret_cast(loose) % 4096 == 0) { + free_nid_postfix(loose); + continue; + } + bool threw = false; + try { + reallocalign_nid_postfix(loose, 0, 4096); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + free_nid_postfix(loose); + return 0; + } + throw std::runtime_error("reallocalign check could not get a block without 4096-byte alignment"); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9179b06e02..d9da36364f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -23,8 +23,8 @@ - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks. `Memalign` is inferred as `(handle, alignment, size, owner)`, delegates to `sceLibcMspaceMemalign`, and is unverified on console. `Create` has an extra options pointer with a partly observed layout (version, type 1 or 2 at +2, field at +0x10); non-null options throw because the field's meaning is unknown. Allocation owners are ignored because block footers are not modelled. On PS5 firmware 13.60, heap-owner calls worked; wrong owners in `Reallocalign` or in-place `Realloc` terminate the process, and `MallocStats` returns 1 if non-heap-owned blocks exist. These owner results are not reproduced. `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` remain unimplemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. -- [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception From b3a9fbe3ea95584ffad8f74e92a62b4206a92a4a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:19:07 +0200 Subject: [PATCH 0200/1360] fix(libkernel_sys): fail symlink with ENAMETOOLONG for a target of MAXPATHLEN bytes or more FreeBSD 11.4 kern_symlinkat copies the target with copyinstr(path1, ..., MAXPATHLEN) (sys/kern/vfs_syscalls.c:1490), so a target of 1024 bytes or more fails with ENAMETOOLONG before the link name is looked up. symlink_nid_postfix had no length check on the target. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 1 + core/libs/tests/GuestKernelSys.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index b7e777091a..7663cacdee 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -384,6 +384,7 @@ int APS5_VABI chroot_nid_postfix(const char* path) { int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); + if (std::strlen(target) >= GuestPathMax) return static_cast(Fail(GuestEnametoolong)); if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); const std::filesystem::path contents(target); if (contents.has_root_name() || contents.has_root_directory()) { diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 16bd6770eb..2f751a04e6 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -295,6 +295,12 @@ int main() { Require(symlink_nid_postfix("file.txt", created.c_str()) == -1 && Error() == expected); Require(std::filesystem::hard_link_count(file) == 1 && !std::filesystem::exists(root / "new.txt")); } + for (const std::size_t length : {std::size_t{1024}, std::size_t{1100}}) { + const auto tooLong = std::string(length, 'a'); + const auto created = (root / "target-too-long").string(); + Require(symlink_nid_postfix(tooLong.c_str(), created.c_str()) == -1 && Error() == 63); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "target-too-long"))); + } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); From 55e4819728bedb167bf652defd20a6f418f77027 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 19:27:16 -0300 Subject: [PATCH 0201/1360] fix(libc): build reallocf on realloc as FreeBSD's reallocf.c does Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 11 +++-------- core/libs/tests/ApplicationHeap.cpp | 11 ++++++++--- docs/dev/debt/libc.md | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d1349613d5..2a2d72e0bd 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -585,14 +585,9 @@ void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { } void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { - try { - return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); - } catch (const std::bad_alloc&) { - } catch (const std::length_error&) { - } - ApplicationHeapFree_nid_no_patch(ptr); - errno = 12; - return nullptr; + void* result = realloc_nid_postfix(ptr, newSize); + if (result == nullptr && ptr != nullptr && newSize != 0) ApplicationHeapFree_nid_no_patch(ptr); + return result; } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 3f36d640ef..a563e69556 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -323,10 +323,15 @@ int main(int argc, char** argv) { reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); require(pointer != nullptr); - require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 77; + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 77); reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); - *__error_nid_postfix() = 0; - require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 0x1008f) == nullptr && *__error_nid_postfix() == 22); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 77; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 77); void* empty = ApplicationHeapAllocate_nid_no_patch(0); require(empty != nullptr); ApplicationHeapFree_nid_no_patch(empty); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d9da36364f..561aced31f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -24,7 +24,7 @@ - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception From 037ad5dd967f0f2e8e1ada34482973e607de6efb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:42:36 +0000 Subject: [PATCH 0202/1360] fix(tools): resolve Windows providers case-insensitively --- tools/import_audit.py | 9 ++++++++- tools/tests/test_import_audit.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index a7fb383320..5f02393c33 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -568,13 +568,20 @@ def classify(nid, library, providers, stubs, modules): def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=(), windows_libraries=()): + windows_names = {} + for library in windows_libraries: + windows_names.setdefault(library.casefold(), set()).add(library) + def system_library(name): if name: for suffix in (".sprx", ".suprx", ".debug_prx"): if name.endswith(suffix): normalized = name.removesuffix(suffix) + ".prx" - if normalized in windows_libraries: + candidates = windows_names.get(normalized.casefold(), ()) + if normalized in candidates: return normalized + if len(candidates) == 1: + return next(iter(candidates)) return name def available(name): diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index dbbb1c9578..303dc800e7 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -236,6 +236,20 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["library_mismatch"], 0) self.assertEqual(data["imports"][0]["providers"], ["libSceA.prx"]) + def test_windows_suffix_lookup_matches_provider_case_insensitively(self): + work = self.work + (work.libs / "LibSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + (work.modules / "guest.so").write_bytes(build_elf([], ["libscea.sprx"])) + registry, result = work.root / "game.registry.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "libscea.sprx")]) + registry.with_name("game.needed.json").write_text(json.dumps(["libscea.sprx"])) + code, _, err = work.run(registry, "--strict-providers", "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + self.assertEqual(data["library_mismatch"], 0) + self.assertEqual(data["imports"][0]["providers"], ["LibSceA.prx"]) + def test_elf_guest_dependencies_keep_literal_host_names(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) From bb925032c0cdb40a1b5d0c9d42e36c1b4b39a327 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:43:00 +0100 Subject: [PATCH 0203/1360] perf(agc): scope the snapshot spare pool to reused sizes --- .../Graphics/include/ShaderResources.hpp | 3 + .../Graphics/src/ShaderResources.cpp | 22 ++++--- .../tests/execution/SampledCacheBudget.cpp | 63 ++++++++++++++----- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 382338e94f..cf6765aef0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -36,9 +36,12 @@ std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memor bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t StorageTextureCacheBudget(const Context& context); +inline constexpr std::uint64_t SpareSnapshotPresents = 16; struct TextureCacheUse { std::size_t sampledEntries = 0; + std::size_t spareEntries = 0; std::uint64_t sampledBytes = 0; + std::uint64_t spareBytes = 0; std::uint64_t storageBytes = 0; }; TextureCacheUse TextureCacheUsage(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d0bb065ef3..d2ebc31097 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -131,7 +131,7 @@ struct TextureCache { std::list entries; std::unordered_map::iterator, TextureKeyHash> index; std::unordered_multimap::iterator> views; - std::vector> spare; + std::vector, std::uint64_t>> spare; std::uint64_t spareBytes = 0; std::uint64_t bytes = 0; std::size_t passed = 0; @@ -267,27 +267,33 @@ void touchTexture(TextureCache& cache, std::list::iterator it) { it->present = Recorder::Presents(); } -constexpr std::uint64_t SpareSnapshotBytes = 512ull << 20; +constexpr std::uint64_t SpareSnapshotBytes = 256ull << 20; void parkSnapshot(TextureCache& cache, std::vector& bytes) { const auto size = bytes.size(); if (size == 0) return; - while (!cache.spare.empty() && cache.spareBytes + size > SpareSnapshotBytes) { - cache.spareBytes -= cache.spare.front().size(); + const auto present = Recorder::Presents(); + while (!cache.spare.empty() && (present - cache.spare.front().second > SpareSnapshotPresents || cache.spareBytes + size > SpareSnapshotBytes)) { + cache.spareBytes -= cache.spare.front().first.size(); cache.spare.erase(cache.spare.begin()); } if (cache.spareBytes + size <= SpareSnapshotBytes) { cache.spareBytes += size; - cache.spare.push_back(std::move(bytes)); + cache.spare.emplace_back(std::move(bytes), present); } bytes = std::vector{}; } std::vector spareSnapshot(TextureCache& cache, std::size_t bytes) { + const auto present = Recorder::Presents(); + while (!cache.spare.empty() && present - cache.spare.front().second > SpareSnapshotPresents) { + cache.spareBytes -= cache.spare.front().first.size(); + cache.spare.erase(cache.spare.begin()); + } for (auto spare = cache.spare.begin(); spare != cache.spare.end(); ++spare) { - if (spare->size() != bytes) continue; + if (spare->first.size() != bytes) continue; cache.spareBytes -= bytes; - std::vector taken = std::move(*spare); + std::vector taken = std::move(spare->first); cache.spare.erase(spare); return taken; } @@ -1280,6 +1286,8 @@ TextureCacheUse TextureCacheUsage() { dropUncachedViews(sampled); use.sampledEntries = sampled.entries.size(); use.sampledBytes = sampled.bytes; + use.spareEntries = sampled.spare.size(); + use.spareBytes = sampled.spareBytes; } use.storageBytes = storageCacheBytes(); return use; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 51b16af243..7a0efc3c5b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -281,38 +281,38 @@ private: class WatchedSurface { public: - explicit WatchedSurface(const Context& context) : context(context) { + explicit WatchedSurface(const Context& context, std::size_t bytes = SurfaceBytes) : context(context), span(bytes) { #ifdef _WIN32 - memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); - if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, bytes); #else constexpr std::uintptr_t alignment = 65536; - void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mapped != MAP_FAILED) { const auto begin = reinterpret_cast(mapped); const auto aligned = (begin + alignment - 1) & ~(alignment - 1); if (aligned != begin) munmap(mapped, aligned - begin); - if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); memory = reinterpret_cast(aligned); - GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, bytes); } #endif Require(memory != nullptr, "cannot allocate the watched surface"); - Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); - std::memset(memory, 0x5a, SurfaceBytes); - GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); - AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + Require(AgcDriver::GuestMemory::Watched(Address(), bytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, bytes); + GuestAllocations::Mutation().Add(memory, bytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), bytes); } ~WatchedSurface() { ClearCachedTextures(context.device); GuestAllocations::Mutation().Remove(memory); #ifdef _WIN32 - GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaReset_nid_postfix(memory, span); + GuestArena::GuestArenaRelease_nid_postfix(memory, span); #else - munmap(memory, SurfaceBytes); - GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); + munmap(memory, span); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, span); #endif } @@ -323,6 +323,7 @@ public: private: const Context& context; + std::size_t span = 0; std::uint8_t* memory = nullptr; }; @@ -686,6 +687,39 @@ void recycleTests(const Context& context) { const auto third = Sampled(context, address); Require(third != second, "the replaced snapshot's size-equal buffer was not reread from guest memory"); Require(Sampled(context, address) == third, "an unchanged surface did not serve its remade snapshot"); + constexpr std::uint32_t BigSide = Side * 2; + constexpr std::size_t BigBytes = std::size_t{BigSide} * BigSide * 4u; + const WatchedSurface watchedFirst(context, BigBytes); + const WatchedSurface watchedSecond(context, BigBytes); + const auto bigFirst = Sampled(context, watchedFirst.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the first big snapshot did not park its dropped " + std::to_string(BigBytes) + "-byte buffer in the spare pool"); + const auto bigSecond = Sampled(context, watchedSecond.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the second big snapshot did not take the parked spare of its size"); +} + +void spareAgeTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + constexpr std::uint32_t BigSide = Side * 2; + constexpr std::size_t BigBytes = std::size_t{BigSide} * BigSide * 4u; + { + const WatchedSurface watched(context, BigBytes); + const auto first = Sampled(context, watched.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the big snapshot did not park its dropped buffer in the spare pool"); + for (std::uint64_t present = 0; present <= SpareSnapshotPresents; ++present) Recorder::CountPresent(); + const auto small = Sampled(context, block.Surface(0)); + Require(TextureCacheUsage().spareEntries == 0 && TextureCacheUsage().spareBytes == 0, "a spare idle for " + std::to_string(SpareSnapshotPresents + 1u) + " presents was not released"); + } + ClearCachedTextures(context.device); + { + const WatchedSurface watchedFirst(context, BigBytes); + const WatchedSurface watchedSecond(context, BigBytes); + const auto second = Sampled(context, watchedFirst.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the second big snapshot did not park its dropped buffer in the spare pool"); + for (std::uint64_t present = 0; present < SpareSnapshotPresents; ++present) Recorder::CountPresent(); + const auto third = Sampled(context, watchedSecond.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "a spare taken within " + std::to_string(SpareSnapshotPresents) + " presents was released instead of reused"); + } } void heldTests(const Context& context) { @@ -1114,6 +1148,7 @@ int main(int argc, char** argv) { secondUseTests(context); earlyMissTests(context); collidingKeyTests(context); + spareAgeTests(context); ClearCachedTextures(context.device); } std::puts("sampled texture cache budget tests passed"); From eb04828a69cb72faab780b37a9d05017f716b52a Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:50:55 +0300 Subject: [PATCH 0204/1360] perf(shader): use table-driven genericFormattedLoad for descriptor buffer loads --- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index aa7dc2bbe8..8ce3cc9ca7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -466,10 +466,15 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue values[component] = EmitValueOrZeroIfCondition(state, both(state, inBounds, required[component]), [&] { const auto width = Select(state, u32, packed, constant(32u), bits[component]); const auto dword = equal(state, width, constant(32u)); - const auto raw = EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { - return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); - }); - const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), constant(0u)); + const auto raw = memory.gpuDescriptor + ? EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { + return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); + }) + : readBuffer(context, instruction, addresses[component].guest, 32u); + const auto subwordOffset = memory.gpuDescriptor + ? constant(0u) + : Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, addresses[component].guest, 3u), constant(3u)); + const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), subwordOffset); const auto unsignedField = EmitBitFieldUExtract(state, raw, bitOffset, bits[component]); const auto signedField = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), signedField, Unary(state, spv::OpBitcast, TypeI32(state), raw), bitOffset, bits[component]); @@ -539,15 +544,15 @@ std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instru const auto& memory = context.Memory(instruction); const auto format = field(state, resource.word3, 12u, 7u); constexpr std::uint32_t count = static_cast(IrBufferFormat::Format32_32_32_32Float); - if (!store && memory.gpuDescriptor) { + if (!store) { const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); const auto unsupported = Binary(state, spv::OpLogicalOr, TypeBool(state), equal(state, format, ConstantU32(state, 0u)), Binary(state, spv::OpUGreaterThan, TypeBool(state), format, ConstantU32(state, count))); const auto result = EmitValueIfElse(state, unsupported, type, [&] { - RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); + if (memory.gpuDescriptor) RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); return zero; }, [&] { return genericFormattedLoad(context, instruction, resource, components, format, count); }); - StopBdaInvocationIf(state, unsupported); + if (memory.gpuDescriptor) StopBdaInvocationIf(state, unsupported); return result; } const auto merge = state.module.AllocateId(); From e3b24e45b13a554bba87a7489b51f1a2c7139b04 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:51:02 +0300 Subject: [PATCH 0205/1360] perf(shader): treat OpExtInst as pure in SpecializeSpirv dead-code elimination --- .../recompiler/SpirvBackend/src/SpirvSpecialization.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index d77a063d72..2efa6064de 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -203,6 +203,7 @@ bool PureInstruction(const Instruction& instruction) { case spv::OpInBoundsPtrAccessChain: case spv::OpArrayLength: case spv::OpPhi: + case spv::OpExtInst: return true; case spv::OpLoad: return instruction.size() == 4u || (instruction.size() == 5u && instruction[4] == spv::MemoryAccessMaskNone); @@ -216,6 +217,9 @@ bool VisitInputs(const Instruction& instruction, const TVisitor& visit) { std::size_t first = 3u; std::size_t end = instruction.size(); switch (Opcode(instruction)) { + case spv::OpExtInst: + first = 5u; + break; case spv::OpVectorShuffle: case spv::OpCompositeInsert: end = 5u; From cfc2c5ff0f47ba7714e3fd2e107b2ee9bca0da1f Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:51:09 +0300 Subject: [PATCH 0206/1360] perf(shader): fold constant private table loads and logical operations in SpecializeSpirv --- .../SpirvBackend/src/SpirvSpecialization.cpp | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 2efa6064de..a2ef8729d9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -584,6 +584,8 @@ public: vectorTypes.Reset(bound); removed.Reset(bound); aliases.Reset(bound); + composites.Reset(bound); + privateTables.Reset(bound); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared SPIR-V instruction"); @@ -600,8 +602,25 @@ public: if (op == spv::OpConstant && types.contains(instruction.at(1)) && types.at(instruction[1]).width <= 32u) values.emplace(instruction.at(2), instruction.at(3)); if (op == spv::OpConstantTrue || op == spv::OpConstantFalse) values.emplace(instruction.at(2), op == spv::OpConstantTrue ? 1u : 0u); if ((op == spv::OpConstant || op == spv::OpConstantTrue || op == spv::OpConstantFalse) && values.contains(instruction.at(2))) canonical.emplace(std::pair{instruction.at(1), values.at(instruction[2])}, instruction[2]); + if (op == spv::OpConstantComposite && instruction.size() >= 3u) composites.emplace(instruction.at(2), std::vector(instruction.begin() + 3, instruction.end())); + if (op == spv::OpVariable && instruction.size() == 5u && instruction[3] == spv::StorageClassPrivate && composites.contains(instruction[4])) privateTables.emplace(instruction.at(2), instruction[4]); cursor += count; } + IdTable tablePointers; + tablePointers.Reset(bound); + for (const auto& instruction : instructions) { + const auto op = Opcode(instruction); + if ((op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) && instruction.size() == 5u && privateTables.contains(instruction[3])) { + tablePointers.emplace(instruction[2], instruction[3]); + continue; + } + if (op == spv::OpLoad && instruction.size() == 4u && tablePointers.contains(instruction[3])) continue; + if (op == spv::OpName || op == spv::OpDecorate || op == spv::OpVariable || op == spv::OpEntryPoint) continue; + for (std::size_t index = 1; index < instruction.size(); ++index) { + privateTables.erase(instruction[index]); + if (const auto* table = tablePointers.find(instruction[index])) privateTables.erase(*table); + } + } } std::vector Run() { @@ -641,12 +660,30 @@ private: return found != nullptr && *found == type; } - std::optional evaluate(const Instruction& instruction) const { + std::optional evaluate(const Instruction& instruction, const IdTable>& constPointers) const { const auto op = Opcode(instruction); if (instruction.size() < 4u) return std::nullopt; const auto* type = types.find(instruction[1]); if (type == nullptr || type->width > 32u) return std::nullopt; + if (op == spv::OpLoad && instruction.size() == 4u) { + const auto* pointer = constPointers.find(instruction[3]); + if (pointer == nullptr) return std::nullopt; + const auto& elements = composites.at(pointer->first); + return pointer->second < elements.size() ? value(elements[pointer->second]) : std::nullopt; + } const auto left = value(instruction[3]); + if (op == spv::OpLogicalAnd && instruction.size() == 5u) { + const auto right = value(instruction[4]); + if ((left && *left == 0u) || (right && *right == 0u)) return 0u; + if (left && right) return *left != 0u && *right != 0u; + return std::nullopt; + } + if (op == spv::OpLogicalOr && instruction.size() == 5u) { + const auto right = value(instruction[4]); + if ((left && *left != 0u) || (right && *right != 0u)) return 1u; + if (left && right) return *left != 0u || *right != 0u; + return std::nullopt; + } if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; if (op == spv::OpLogicalNot) return *left == 0u; @@ -694,16 +731,42 @@ private: extracts.Reset(bound); IdTable bitcasts; bitcasts.Reset(bound); + IdTable> constPointers; + constPointers.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); if (op == spv::OpFunction) function = true; if (op == spv::OpFunctionEnd) function = false; if (!function) continue; + if ((op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) && instruction.size() == 5u && privateTables.contains(instruction[3])) { + if (const auto index = value(instruction[4])) constPointers.emplace(instruction[2], std::pair{privateTables.at(instruction[3]), *index}); + } if (op == spv::OpSelect && instruction.size() == 6u) { if (const auto condition = value(instruction[3])) { instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[*condition != 0u ? 4u : 5u]}); changed = true; + } else if (instruction[4] == instruction[5]) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[4]}); + changed = true; + } + } + if (op == spv::OpLogicalAnd && instruction.size() == 5u) { + if (const auto left = value(instruction[3]); left && *left != 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[4]}); + changed = true; + } else if (const auto right = value(instruction[4]); right && *right != 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[3]}); + changed = true; + } + } + if (op == spv::OpLogicalOr && instruction.size() == 5u) { + if (const auto left = value(instruction[3]); left && *left == 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[4]}); + changed = true; + } else if (const auto right = value(instruction[4]); right && *right == 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[3]}); + changed = true; } } if (op == spv::OpVectorExtractDynamic && instruction.size() == 5u) { @@ -746,7 +809,7 @@ private: } } if (op == spv::OpCopyObject && values.contains(instruction.at(2))) continue; - const auto result = evaluate(instruction); + const auto result = evaluate(instruction, constPointers); if (!result) continue; const auto type = types.at(instruction[1]); const auto bits = type.width == 32u ? *result : *result & ((1u << type.width) - 1u); @@ -1096,6 +1159,8 @@ private: IdSet vectorTypes; IdSet removed; IdTable aliases; + IdTable> composites; + IdTable privateTables; std::map, std::uint32_t> canonical; }; From 780284c150a918f6e2bc9ea1993a976bd3b32944 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:51:16 +0300 Subject: [PATCH 0207/1360] perf(shader): drop redundant post-compaction spirv-opt pass in buildSpecializedModule --- core/shader/recompiler/Recompiler.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9246de6bda..0d76ccb36d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -819,9 +819,6 @@ std::shared_ptr buildSpecializedModule(const CompiledSh cursor += count; } materialized = std::move(compact); -#if ANYPS5_ENABLE_SPIRV_TOOLS - materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false); -#endif auto module = std::make_shared(); std::map bindingNumbers; for (std::size_t cursor = 5; cursor < materialized.size();) { From c5415e0845a8568cb4dccaf6c667275f2411752d Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 11:58:08 +1300 Subject: [PATCH 0208/1360] fix(libc): rescale _Stod text that overflows before scaling Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/DinkumwareInternals.cpp | 17 ++++++++++++----- core/libs/tests/GuestDinkumwareInternals.cpp | 8 ++++++-- docs/dev/debt/libc.md | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index 82af11ba5e..163a7362fa 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -28,6 +28,7 @@ float APS5_VABI log2f_nid_postfix(float x); namespace { constexpr long long ExponentLimit = 1000000000; +constexpr int RangeError = 34; bool Hexadecimal(const char* text) { while (std::isspace(static_cast(*text))) ++text; @@ -39,12 +40,18 @@ bool Hexadecimal(const char* text) { extern "C" { -double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten) { - const int saved = *__error_nid_postfix(); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten) { + int& guestErrno = *__error_nid_postfix(); + const int saved = guestErrno; + guestErrno = 0; char* stop = nullptr; const double value = strtod_nid_postfix(text, &stop); if (end != nullptr) *end = stop; - if (pten == 0 || stop == text || !std::isfinite(value)) return value; + const bool overflowed = std::isinf(value) && guestErrno == RangeError; + if (pten == 0 || stop == text || !(std::isfinite(value) || overflowed)) { + if (guestErrno == 0) guestErrno = saved; + return value; + } if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); std::string number(text, static_cast(stop)); long long exponent = 0; @@ -53,10 +60,10 @@ double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten) { exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); number.resize(marker); } - exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); number += 'e'; number += std::to_string(exponent); - *__error_nid_postfix() = saved; + guestErrno = saved; return strtod_nid_postfix(number.c_str(), nullptr); } diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index ccd3e04276..aa660dabdf 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -1,12 +1,13 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include extern "C" { int* APS5_VABI __error_nid_postfix(); -double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten); long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); @@ -26,7 +27,7 @@ void Require(bool condition, const char* message) { } } -void CheckStod(const char* text, long pten, double expected, long consumed, int error, const char* message) { +void CheckStod(const char* text, std::int64_t pten, double expected, long consumed, int error, const char* message) { *__error_nid_postfix() = 0; char* end = nullptr; const double value = _Stod_nid_postfix(text, &end, pten); @@ -45,6 +46,9 @@ void TestStod() { CheckStod("1e300", 8, 1e308, 5, 0, "a scale up to the largest decades"); CheckStod("5e-324", -1, 0.0, 6, RangeError, "a scale that underflows to zero"); CheckStod("1", 2147483647, HUGE_VAL, 1, RangeError, "the largest scale"); + CheckStod("5", std::int64_t{1} << 40, HUGE_VAL, 1, RangeError, "a scale above 32 bits"); + CheckStod("1e400", -100, 1e300, 5, 0, "an overflowing text scaled back into range"); + CheckStod("0.01e41", -2, 1e37, 7, 0, "a negative scale from leading fraction zeros"); CheckStod("1e-2147483648", -1, 0.0, 13, RangeError, "the smallest exponent and scale"); CheckStod("-0", 5, -0.0, 2, 0, "negative zero keeps its sign"); CheckStod("infinity", 3, HUGE_VAL, 8, 0, "infinity is not scaled"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 931d7d1746..dded6b4b39 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -69,6 +69,7 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there +- [_Stod](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999 - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. From b456725205d5ebd82ac8b7804617425ce2476432 Mon Sep 17 00:00:00 2001 From: teepean Date: Wed, 21 Oct 2026 02:01:57 +0300 Subject: [PATCH 0209/1360] perf(agc): skip an address space's pending CPU write scan when none are pending GuestBufferMemory::completeSpaceWrites ran at every upload of an address-based build: it copied every pending write range of the open and in-flight batches, searched each one in the space's sorted regions and asked CompletePendingCpuWrites about every region it overlapped. That call returns false at once when no copied writer and no completion label is pending, so without one the whole scan found nothing. The scan now starts only when AnyPendingCpuWrites says one of them is pending. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index c0d65d1f74..a74db39947 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -46,6 +46,7 @@ void OverrideCacheBudgetForTest(std::uint64_t bytes); std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); bool SampledTexturesShareEntry(std::span first, std::span second); +bool AnyPendingCpuWrites(const Context& context); bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); bool SpirvStoresThroughAddresses(std::span words); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 82d2b10a04..1a9d1028cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4529,7 +4529,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { void GuestBufferMemory::completeSpaceWrites() const { auto* recorder = Recorder::Active(); - if (recorder == nullptr || Recorder::InCompletion()) return; + if (recorder == nullptr || Recorder::InCompletion() || !AnyPendingCpuWrites(context)) return; const auto& base = space->base; std::vector> writes; std::vector candidates; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index c75781da18..95dd210ed1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3233,6 +3233,11 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span return OwnRefreshFallback::Count; } +bool AnyPendingCpuWrites(const Context& context) { + if (Recorder::InCompletion() || Recorder::Active() == nullptr) return false; + return (context.copiedWriters != nullptr && !context.copiedWriters->empty()) || !DrawCopiedWriters()->empty() || Recorder::PendingCompletionLabels() != 0; +} + bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { if (bytes == 0 || Recorder::InCompletion()) return false; auto* recorder = Recorder::Active(); From e321a04dca999ae22e37b735c47648ecda3f004b Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 12:06:22 +1300 Subject: [PATCH 0210/1360] docs(libc): move the _Stod debt entry clear of open edits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 1e7f5c75bb..18b7022d96 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -71,8 +71,8 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there -- [_Stod](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999 - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) +- [_Stod](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999 - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From acaff99f4460eef47ff3218667334fd9edb29c02 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:14:26 +0000 Subject: [PATCH 0211/1360] test(agc): shut down driver after HTILE dispatch check --- .../tests/execution/NativeMultisampleDepth.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 7ebc8704fd..df5f73601d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -817,6 +817,11 @@ bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; if (fault == ClearFault::DriverDispatch && clearOne) { + struct ShutdownDriver { + ~ShutdownDriver() { + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + } + } shutdownDriver; const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); SubmitDriverHtileClear(target.htileAddress, gpuLock); recorder.Activate(); From 966018783f0644f601b747b6e22e3ffee9cbe760 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 12:13:06 +1300 Subject: [PATCH 0212/1360] fix(agc): keep the registered-stream throw for the workload calls Review on #8302: the ACB workload calls and the DCB stream-inactive call throw again for a registered stream until a title is shown stopping on them (as #3789 did for the DCB calls, and as #4709 kept). The debt entry keeps the firmware's packet layout as the record of what is missing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 21 ++++++-- .../libSceAgc/DcbState/include/Workload.hpp | 11 ---- .../prx/libSceAgc/DcbState/src/Workload.cpp | 50 ++++++------------- core/libs/prx/libSceAgc/tests/Commands.cpp | 17 +++---- docs/dev/debt/libSceAgc.md | 2 +- 5 files changed, 40 insertions(+), 61 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 539066325a..567bfd5e18 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,13 +4,23 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" -#include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(function); + return nullptr; +} + +} + extern "C" { std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { @@ -73,15 +83,18 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { - return Agc::Workload::Active(buf, streamId, workloadIds, workloadCount, __func__); + (void)workloadIds; + (void)workloadCount; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { - return Agc::Workload::Complete(buf, streamId, workloadId, __func__); + (void)workloadId; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { - return Agc::Workload::StreamInactive(buf, streamId, __func__); + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { diff --git a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp index 75ae9c3979..3d5d6cac71 100644 --- a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp +++ b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp @@ -1,15 +1,4 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP #define CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP -#include -#include "SceTypes.hpp" - -namespace Agc::Workload { - -std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function); -std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function); -std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function); - -} - #endif diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 06d5578a81..0f739d9055 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -13,7 +13,6 @@ namespace { constexpr std::uint32_t OpcodeNop = 0x10; constexpr std::uint32_t ActivePacketWords = 18; constexpr std::uint32_t CompletePacketWords = 12; -constexpr std::uint32_t InactivePacketWords = 9; constexpr std::uint32_t MaxWorkloadId = 63; constexpr std::uint32_t MaxActiveCount = 63; @@ -26,57 +25,40 @@ uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* functio } -namespace Agc::Workload { +extern "C" { -std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function) { - if (buf == nullptr || workloadId > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - const std::uint64_t clearMask = ~(std::uint64_t{1} << workloadId); - auto* packet = NewPacket(buf, CompletePacketWords, function); - packet[1] = streamId; - packet[2] = workloadId; +uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { + if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); + auto* packet = NewPacket(buf, CompletePacketWords, __func__); + packet[1] = stream_id; + packet[2] = workload_id; packet[3] = static_cast(clearMask); packet[4] = static_cast(clearMask >> 32); return packet; } -std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function) { - if (buf == nullptr || workloadIds == nullptr || workloadCount == 0 || workloadCount > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; +uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { + if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; std::uint64_t mask = 0; - for (std::uint32_t index = 0; index < workloadCount; ++index) { - const auto id = workloadIds[index]; + for (std::uint32_t index = 0; index < workload_count; ++index) { + const auto id = workload_ids[index]; if (id > MaxWorkloadId) return nullptr; const std::uint64_t bit = std::uint64_t{1} << id; if ((mask & bit) != 0) return nullptr; mask |= bit; } - auto* packet = NewPacket(buf, ActivePacketWords, function); - packet[1] = streamId; + auto* packet = NewPacket(buf, ActivePacketWords, __func__); + packet[1] = stream_id; packet[2] = static_cast(mask); packet[3] = static_cast(mask >> 32); return packet; } -std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function) { - if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - auto* packet = NewPacket(buf, InactivePacketWords, function); - packet[1] = streamId; - return packet; -} - -} - -extern "C" { - -uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - return Agc::Workload::Complete(buf, stream_id, workload_id, __func__); -} - -uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - return Agc::Workload::Active(buf, stream_id, workload_ids, workload_count, __func__); -} - uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { - return Agc::Workload::StreamInactive(buf, streamId, __func__); + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(__func__); + return nullptr; } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index cbf13610e2..960dbb36cc 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -294,17 +294,12 @@ void testWorkloadStreams() { check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); Storage registered; registered.words.fill(0xdeadbeefu); - const auto* inactive = sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); - check(inactive == registered.words.data() && inactive[0] == Agc::Command::Header(0x10, 9, 0) && inactive[1] == 1, "DCB workload stream inactive packet"); - for (std::uint32_t word = 2; word < 9; ++word) check(inactive[word] == 0, "DCB workload stream inactive packet tail is not zero"); - check(registered.buffer.cursor_up == registered.words.data() + 9, "DCB workload stream inactive cursor advance"); - Storage dcb; - Storage acb; - check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) != nullptr && sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 2) == acb.words.data(), "ACB workloads active packet"); - check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) != nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 3) == acb.words.data() + 18, "ACB workload complete packet"); - check(sceAgcDcbSetWorkloadStreamInactive(&dcb.buffer, 1) != nullptr && sceAgcAcbSetWorkloadStreamInactive(&acb.buffer, 1) == acb.words.data() + 30, "ACB workload stream inactive packet"); - check(acb.buffer.cursor_up == acb.words.data() + 39 && dcb.words == acb.words, "ACB workload packets differ from the DCB ones"); - check(sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 0) == nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 64) == nullptr, "ACB workload argument checks"); + const auto untouched = registered.words; + expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); + expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); + expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, "a workload call with a null command buffer for a registered stream returned a packet"); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index d9c6839824..7725620d8f 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -42,6 +42,6 @@ - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. -- [sceAgcDcbSetWorkloadsActive, sceAgcDcbSetWorkloadComplete, sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp), [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the console packets are not written. The firmware builds them with libSceAgcDriver's sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete and sceAgcDriverSetWorkloadStreamInactive: a SET_UCONFIG_REG of a tag to 0x342 (`0xCC000000 | stream` for active and inactive, `0xCD000000 + workload * 32 + stream` for complete) that carries the first dword of an 8-byte payload (the active mask, zero for inactive), a WRITE_DATA of the rest to register 0xC343, and for active and complete a 9-dword ATOMIC_MEM that ORs the mask into, or ANDs the cleared bit out of, the stream's 64-bit word in a table the driver takes at module start (control 0x267 or 0x275, bit 30 set on the DCB). The executor has no ATOMIC_MEM, the table is not allocated, and an unpredicated SET_UCONFIG_REG is rejected on a compute queue, so the calls write an `IT_NOP` of the console size instead: 18 dwords for active (stream id, mask), 12 for complete (stream id, workload id, clear mask) and 9 for inactive (stream id); the ACB calls write the same packets as the DCB calls. The argument checks are the firmware's: null for a null command buffer, a stream outside 1 to 31 or not registered, a workload id above 63, and for active a null id list, a duplicate id or a count of 0 or above 63; the stream is checked before the ids are read. The driver registration records only the stream id; the console also keeps a 32-byte name per stream and pre-registers stream 0 as "System", and its error codes for an invalid or repeated registration are not returned, so registration never fails here. +- [sceAgcDcbSetWorkloads*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console packets are not written. DCB active and complete write a console-sized `IT_NOP` (18 and 12 dwords, [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) layout); DCB stream inactive and the three ACB calls throw for a registered stream. The firmware writes, through the driver's SetWorkload* builders, a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete), a WRITE_DATA to 0xC343, and for active and complete an ATOMIC_MEM on a per-stream 64-bit mask in a driver table. The executor has no ATOMIC_MEM or table. Registration records only the stream id and never fails - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From bfb76ebda5c217951831bf8fdab6d66e323cfc11 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:27:21 +0000 Subject: [PATCH 0213/1360] test(agc): gate HTILE coverage check by watched range --- .../execution/NativeMultisampleDepth.cpp | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index df5f73601d..3765fb0a6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -78,7 +78,6 @@ public: Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); VkPhysicalDeviceProperties selectedProperties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &selectedProperties); - cpuDevice = selectedProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU; std::printf("Selected GPU name=%s vendor=0x%x device=0x%x driver=%u\n", selectedProperties.deviceName, selectedProperties.vendorID, selectedProperties.deviceID, selectedProperties.driverVersion); const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); queues(context.physical, &count, nullptr); @@ -115,7 +114,6 @@ public: } ~Device() { release(); } Context GetContext() const { return context; } - bool IsCpu() const { return cpuDevice; } private: template TFunction function(const char* name) const { const auto result = reinterpret_cast(proc(instance, name)); @@ -138,7 +136,6 @@ private: PFN_vkGetInstanceProcAddr proc = nullptr; VkInstance instance = VK_NULL_HANDLE; Context context{}; - bool cpuDevice = false; }; class TargetMemory { @@ -695,7 +692,7 @@ void CheckClearDispatch() { } } -bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true, bool cpuDevice = false) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); @@ -826,13 +823,12 @@ bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, SubmitDriverHtileClear(target.htileAddress, gpuLock); recorder.Activate(); Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); - const bool unchangedSinceSubmit = AgcDriver::GuestMemory::UnchangedSinceCollected(target.htileAddress, metadataBytes, generation); - const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); - if (cpuDevice && !unchangedSinceSubmit && !storedAll) { - std::puts("skipped native HTILE driver dispatch: this device's host import path does not retain write-watch evidence independently of driver writes"); - return false; + if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) { + std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; clear output checks still run"); + } else { + const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); + Require(storedAll, "driver idle returned before recording the completed HTILE dispatch"); } - Require(storedAll, "driver idle returned before recording the completed HTILE dispatch"); } else { const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { @@ -923,7 +919,6 @@ bool CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, texture.reset(); arrayTexture.reset(); std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one, iterate256); - return true; } void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { @@ -1244,7 +1239,7 @@ int main(int argc, char** argv) { ++checked; CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); ++checked; - if (!CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock, true, device->IsCpu())) return VulkanTestSkipped; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; From 89f829b5d8356783431796408cf27246422e422e Mon Sep 17 00:00:00 2001 From: janzezno Date: Wed, 21 Oct 2026 01:29:18 +0200 Subject: [PATCH 0214/1360] fix(agc): sync a host read on a store recorded after its snapshot check FlushForAccess checked the pending-write snapshot before it looked for overlapping pending and flushing images. A flusher moves an image the other way: pending, flushing, recorded into the open batch, landed. A reader descheduled after the snapshot check while the store was recorded and the image unregistered found nothing flushing and read guest memory before the recorded copy ran. Sync again when the snapshot overlaps the range after the image checks. --- core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9f03343a67..0e37bddcd8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1064,7 +1064,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } else { stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published); } - if (stored || published) { + if (stored || published || SnapshotOverlaps(address, bytes)) { // Stores into imported memory were only recorded; the CPU is about to read them. The wait // targets the batch holding them (the open one: SyncThrough submits it) and runs with the // mutex released when this acquisition is the outermost; a nested hook waits locked through From e87e649a4904bb557922b1d37b5578ca753058fe Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:35:40 +0000 Subject: [PATCH 0215/1360] test(agc): gate HTILE skip by import watch policy --- .../libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 3765fb0a6c..58820395bf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -824,6 +824,8 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, recorder.Activate(); Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) { + Require(PrepareImportWatch(context) == ImportWatch::Unwatch, + "the HTILE metadata range lost write-watch coverage on a device whose host imports stay watched"); std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; clear output checks still run"); } else { const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); From efc3bd2efd1621ec6670a4604e68aa4fcaa5aad9 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:12:57 +0300 Subject: [PATCH 0216/1360] test(shader): cover private table specialization folding and subword formatted loads --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 81 +++++++++++++++++ .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 85 +++++++++++++++++ .../tests/execution/BufferFormat.cpp | 91 +++++++++++++++++++ .../SpirvBackend/src/SpirvSpecialization.cpp | 11 ++- 4 files changed, 265 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 1be75623e3..00796cd2b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -298,6 +298,86 @@ void testIdsBeyondTheBoundAreRejected() { check(rejected, "a module whose ids reach its bound was specialized"); } +void testPrivateCompositeTableLoadFolding() { + for (const auto accessOp : {spv::OpAccessChain, spv::OpInBoundsAccessChain}) { + const auto build = [&](bool dynamicIndex, bool storeThroughChain) { + SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + const auto typeVoid = module.Type(spv::OpTypeVoid); + const auto typeBool = module.Type(spv::OpTypeBool); + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto falseVal = module.Constant(spv::OpConstantFalse, typeBool); + const auto zero = module.Constant(spv::OpConstant, typeU32, 0u); + const auto one = module.Constant(spv::OpConstant, typeU32, 1u); + const auto three = module.Constant(spv::OpConstant, typeU32, 3u); + const auto sixteen = module.Constant(spv::OpConstant, typeU32, 16u); + const auto twenty = module.Constant(spv::OpConstant, typeU32, 20u); + const auto thirty = module.Constant(spv::OpConstant, typeU32, 30u); + const auto typeArray = module.Type(spv::OpTypeArray, typeU32, three); + const auto initial = module.Constant(spv::OpConstantComposite, typeArray, std::vector{sixteen, twenty, thirty}); + const auto typeInputPtr = module.Type(spv::OpTypePointer, spv::StorageClassInput, typeU32); + const auto typeTablePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeArray); + const auto typePrivatePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeU32); + const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); + const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); + const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); + const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); + const auto mainId = module.AllocateId(); + const auto lane = module.AllocateId(); + const auto rawCond = module.AllocateId(); + const auto cond = module.AllocateId(); + const auto baseIndex = module.AllocateId(); + const auto index = module.AllocateId(); + const auto element = module.AllocateId(); + const auto loaded = module.AllocateId(); + const auto sum = module.AllocateId(); + module.AddFunction(spv::OpFunction, typeVoid, mainId, spv::FunctionControlMaskNone, typeFunction); + module.AddFunction(spv::OpLabel, module.AllocateId()); + module.AddFunction(spv::OpLoad, typeU32, lane, input); + if (storeThroughChain) { + const auto writePtr = module.AllocateId(); + module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); + module.AddFunction(spv::OpStore, writePtr, lane); + } + module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); + module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); + module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); + module.AddFunction(spv::OpSelect, typeU32, index, cond, baseIndex, baseIndex); + module.AddFunction(accessOp, typePrivatePtr, element, table, dynamicIndex ? lane : index); + module.AddFunction(spv::OpLoad, typeU32, loaded, element); + module.AddFunction(spv::OpIAdd, typeU32, sum, lane, loaded); + module.AddFunction(spv::OpStore, output, sum); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + return std::pair{ShaderRecompiler::SpecializeSpirv(module.Finalize()), thirty}; + }; + const auto [folded, expectedConstant] = build(false, false); + const auto foldedCounts = opcodeCounts(folded); + check(!foldedCounts.contains(accessOp), "a constant-index private composite table access chain was not removed"); + check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); + check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); + check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); + bool addedThirty = false; + for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { + if ((folded[cursor] >> 16u) == 0u) break; + if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; + } + check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); + + const auto [dynamic, unusedDynamic] = build(true, false); + static_cast(unusedDynamic); + const auto dynamicCounts = opcodeCounts(dynamic); + check(dynamicCounts.contains(accessOp) && dynamicCounts.at(accessOp) == 1u, "a dynamic-index private composite table access chain was removed"); + check(dynamicCounts.contains(spv::OpLoad) && dynamicCounts.at(spv::OpLoad) == 2u, "a dynamic-index private composite table load was folded"); + + const auto [mutated, unusedMutated] = build(false, true); + static_cast(unusedMutated); + const auto mutatedCounts = opcodeCounts(mutated); + check(mutatedCounts.contains(accessOp) && mutatedCounts.at(accessOp) == 2u, "a mutated private composite table access chain was removed"); + check(mutatedCounts.contains(spv::OpLoad) && mutatedCounts.at(spv::OpLoad) == 2u, "a mutated private composite table load was folded despite a store through an access chain"); + } +} + void testBlockBallotsOfOnePredicateShareOneBallot() { SpirvModule module; const auto typeBool = module.Type(spv::OpTypeBool); @@ -360,6 +440,7 @@ int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); testStructExtractsAreNotShuffled(); + testPrivateCompositeTableLoadFolding(); testRepeatedTypesShareOneId(); testOperandShapesDoNotCollide(); testOperandKindsAgree(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index eb5a967d31..9534ce7716 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -936,6 +936,89 @@ OpStore %slot2 %second check(opcodeCount(acrossCall, spv::OpGroupNonUniformBallot) == 2, "a ballot was shared across a function call"); } +std::string privateTableModuleSource(const std::string& body) { + return R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index +OpExecutionMode %main LocalSize 8 1 1 +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%false = OpConstantFalse %bool +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%three = OpConstant %uint 3 +%sixteen = OpConstant %uint 16 +%twenty = OpConstant %uint 20 +%thirty = OpConstant %uint 30 +%array = OpTypeArray %uint %three +%initial = OpConstantComposite %array %sixteen %twenty %thirty +%input = OpTypePointer Input %uint +%privateArray = OpTypePointer Private %array +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%index = OpVariable %input Input +%table = OpVariable %privateArray Private %initial +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +)" + body + R"(OpReturn +OpFunctionEnd +)"; +} + +void testSpecializedPrivateCompositeTableLoad() { + for (const bool inBounds : {false, true}) { + const auto accessName = inBounds ? "OpInBoundsAccessChain" : "OpAccessChain"; + const auto accessOp = inBounds ? spv::OpInBoundsAccessChain : spv::OpAccessChain; + try { + const auto folded = assemble(privateTableModuleSource( + "%rawCond = OpUGreaterThan %bool %lane %zero\n" + "%cond = OpLogicalOr %bool %rawCond %false\n" + "%baseOffset = OpIAdd %uint %one %one\n" + "%offset = OpSelect %uint %cond %baseOffset %baseOffset\n" + "%element = " + std::string(accessName) + " %private %table %offset\n" + "%loaded = OpLoad %uint %element\n" + "%sum = OpIAdd %uint %lane %loaded\n" + "OpStore %output %sum\n")); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(folded, Vulkan11, Spirv13, false, false)); + const auto specializedFolded = ShaderRecompiler::SpecializeSpirv(folded); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedFolded, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedFolded, accessOp) == 0u, std::string(accessName) + ": constant-index private table access chain was not removed"); + check(opcodeCount(specializedFolded, spv::OpLogicalOr) == 0u, std::string(accessName) + ": right-false OpLogicalOr was not simplified"); + check(opcodeCount(specializedFolded, spv::OpSelect) == 0u, std::string(accessName) + ": identical-arm OpSelect was not simplified"); + check(opcodeCount(specializedFolded, spv::OpLoad) == 1u, std::string(accessName) + ": constant-index private table load was not folded"); + check(storedLaneFunction(specializedFolded) == std::optional>({1u, 30u}), std::string(accessName) + ": constant-index private table load folded to the wrong value"); + + const auto dynamic = assemble(privateTableModuleSource( + "%element = " + std::string(accessName) + " %private %table %lane\n" + "%loaded = OpLoad %uint %element\n" + "OpStore %output %loaded\n")); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(dynamic, Vulkan11, Spirv13, false, false)); + const auto specializedDynamic = ShaderRecompiler::SpecializeSpirv(dynamic); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedDynamic, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedDynamic, accessOp) == 1u, std::string(accessName) + ": dynamic-index private table access chain was removed"); + check(opcodeCount(specializedDynamic, spv::OpLoad) == 2u, std::string(accessName) + ": dynamic-index private table load was folded"); + + const auto mutated = assemble(privateTableModuleSource( + "%writeElement = " + std::string(accessName) + " %private %table %zero\n" + "OpStore %writeElement %lane\n" + "%readElement = " + std::string(accessName) + " %private %table %one\n" + "%loaded = OpLoad %uint %readElement\n" + "OpStore %output %loaded\n")); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(mutated, Vulkan11, Spirv13, false, false)); + const auto specializedMutated = ShaderRecompiler::SpecializeSpirv(mutated); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedMutated, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedMutated, accessOp) == 2u, std::string(accessName) + ": mutated private table access chain was removed"); + check(opcodeCount(specializedMutated, spv::OpLoad) == 2u, std::string(accessName) + ": mutated private table load was folded despite a store through an access chain"); + } catch (const std::exception& error) { + check(false, std::string(accessName) + " private table specialization: " + error.what()); + } + } +} + } int main() { @@ -953,6 +1036,7 @@ int main() { testFoldedConstantUsedByUndecodedInstructionIsKept(); testSharedConstantsAreNotCopiedForUndecodedUsers(); testSharedBooleanConstantsKeepTheirValues(); + testSpecializedPrivateCompositeTableLoad(); testBallotsMergedIntoOneBlockShareOneBallot(optimizationEnabled); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); @@ -961,3 +1045,4 @@ int main() { std::puts("SPIR-V optimizer checks passed"); return 0; } + diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp index 7ba31cd239..5a93b53303 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp @@ -171,6 +171,94 @@ void Check() { } } +constexpr std::array SubwordWord3{0x31005facu, 0x31006facu, 0x3100bfacu, 0x3100cfacu, 0x31012facu}; + +constexpr const char* SubwordNames[14] = { + "buffer_load_format_x 8_UINT +1", + "buffer_load_format_x 8_UINT +2", + "buffer_load_format_x 8_UINT +3", + "buffer_load_format_x 8_SINT +1", + "buffer_load_format_x 8_SINT +2", + "buffer_load_format_x 8_SINT +3", + "buffer_load_format_x 16_UINT +1", + "buffer_load_format_x 16_UINT +2", + "buffer_load_format_x 16_SINT +1", + "buffer_load_format_x 16_SINT +2", + "buffer_load_format_xy 8_8_UINT +1 x", + "buffer_load_format_xy 8_8_UINT +1 y", + "buffer_load_format_xy 8_8_UINT +2 x", + "buffer_load_format_xy 8_8_UINT +2 y", +}; + +std::vector SubwordCode() { + std::vector code{0x34060087u, 0x34500085u}; + std::uint32_t vdata = 10u; + const auto emitLoad = [&](std::uint32_t op, std::uint32_t srsrc, std::uint32_t offset, std::uint32_t words) { + code.push_back(0xe0001000u | (op << 18u) | offset); + code.push_back(0x80000028u | (srsrc << 16u) | (vdata << 8u)); + vdata += words; + }; + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 2u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 3u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 4u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 5u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(1u, 6u, offset, 2u); + code.push_back(0xbf8c3f70u); + for (std::uint32_t index = 0u; index < 14u; ++index) { + code.push_back(0xe0701000u | (index * 4u)); + code.push_back(0x80010003u | ((10u + index) << 8u)); + } + code.push_back(0xbf810000u); + return code; +} + +void RunSubwordOffsets(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); + std::vector userData(8u + 4u * SubwordWord3.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 < SubwordWord3.size(); ++index) { + const auto descriptor = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u), SubwordWord3[index]); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 8 + 4 * index); + } + static const auto program = SubwordCode(); + const std::span code(program); + 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 CheckSubwordOffsets() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t word = Rows[tid][0]; + const auto u8 = [&](std::uint32_t offset) { return (word >> (offset * 8u)) & 0xffu; }; + const auto s8 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u8(offset)))); }; + const auto u16 = [&](std::uint32_t offset) { return (word >> (offset * 8u)) & 0xffffu; }; + const auto s16 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u16(offset)))); }; + const std::array expected{ + u8(1u), u8(2u), u8(3u), + s8(1u), s8(2u), s8(3u), + u16(1u), u16(2u), + s16(1u), s16(2u), + u8(1u), u8(2u), + u8(2u), u8(3u), + }; + for (std::uint32_t index = 0; index < expected.size(); ++index) { + Expect(tid, Output[tid * Results + index], expected[index], SubwordNames[index]); + } + } +} + } int main() { @@ -179,6 +267,8 @@ int main() { if (!device) return VulkanTestSkipped; Run(*device); Check(); + RunSubwordOffsets(*device); + CheckSubwordOffsets(); std::puts("buffer format tests passed"); return 0; } catch (const std::exception& error) { @@ -186,3 +276,4 @@ int main() { return 1; } } + diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index a2ef8729d9..569298ea62 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -615,11 +615,16 @@ public: continue; } if (op == spv::OpLoad && instruction.size() == 4u && tablePointers.contains(instruction[3])) continue; - if (op == spv::OpName || op == spv::OpDecorate || op == spv::OpVariable || op == spv::OpEntryPoint) continue; - for (std::size_t index = 1; index < instruction.size(); ++index) { + if (op == spv::OpVariable || op == spv::OpEntryPoint) continue; + std::uint32_t switchLiteralWords = 1u; + if (op == spv::OpSwitch && instruction.size() > 1u) { + const auto* selectorType = resultTypes.find(instruction[1]); + if (selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u) switchLiteralWords = 2u; + } + VisitIdOperands(instruction, switchLiteralWords, [&](std::size_t index) { privateTables.erase(instruction[index]); if (const auto* table = tablePointers.find(instruction[index])) privateTables.erase(*table); - } + }); } } From 7fc3fd65c764a37a73142671f1f652650325720c Mon Sep 17 00:00:00 2001 From: Dothre <64598464+dotaku22@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:14:44 +0200 Subject: [PATCH 0217/1360] fix(tools): reject invalid trace event container shapes --- tools/tests/test_trace_digest.py | 21 +++++++++++++++++++++ tools/trace_digest.py | 10 ++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index a7fa3619db..7df5e64fae 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -46,6 +46,25 @@ STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll class TraceDigestTests(unittest.TestCase): + def test_cli_rejects_invalid_event_container_shapes_without_partial_output(self): + cases = [(value, "trace must be an event array or object") for value in (None, True, 7, "trace")] + cases += [({"traceEvents": value}, "traceEvents must be an array") + for value in (None, False, 0, "", {}, "events")] + cases += [([value], "trace event 0 must be an object") for value in (None, True, 7, "event", [])] + cases.append(({"traceEvents": [{}, None]}, "trace event 1 must be an object")) + for document, message in cases: + for compressed in (False, True): + with self.subTest(document=document, compressed=compressed): + path = Path(self.directory.name) / ("invalid.json.gz" if compressed else "invalid.json") + data = json.dumps(document).encode("utf-8") + path.write_bytes(gzip.compress(data) if compressed else data) + result = subprocess.run([sys.executable, trace_digest.__file__, str(path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("FAIL: " + message, result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + def test_cli_reports_unreadable_inputs_without_partial_output(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -123,6 +142,8 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(empty_dict.presents, []) empty_list = trace_digest.Trace([]) self.assertEqual(empty_list.presents, []) + empty_events = trace_digest.Trace({"traceEvents": []}) + self.assertEqual(empty_events.presents, []) def result(self): return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 6d03d2200c..d259fbb62d 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -33,8 +33,14 @@ class Trace: self.presents = [] self.folded = collections.defaultdict(float) self.batched = [] - events = document if isinstance(document, list) else (document.get("traceEvents") or []) - for event in events: + if not isinstance(document, (list, dict)): + raise ValueError("trace must be an event array or object") + events = document if isinstance(document, list) else document.get("traceEvents", []) + if not isinstance(events, list): + raise ValueError("traceEvents must be an array") + for index, event in enumerate(events): + if not isinstance(event, dict): + raise ValueError(f"trace event {index} must be an object") phase = event.get("ph") if phase == "M" and event.get("name") == "thread_name": self.threads[event["tid"]] = event["args"]["name"] From 077e2d43d523ec7433a01edfcb794e5f72ba73c2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:25:11 +0000 Subject: [PATCH 0218/1360] test(agc): query HTILE import policy on driver device --- .../tests/execution/NativeMultisampleDepth.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 58820395bf..dff5cf51ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -692,7 +693,7 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true, const Context* importPolicyContext = nullptr) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); @@ -824,7 +825,8 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, recorder.Activate(); Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) { - Require(PrepareImportWatch(context) == ImportWatch::Unwatch, + const auto& policyContext = importPolicyContext != nullptr ? *importPolicyContext : context; + Require(PrepareImportWatch(policyContext) == ImportWatch::Unwatch, "the HTILE metadata range lost write-watch coverage on a device whose host imports stay watched"); std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; clear output checks still run"); } else { @@ -1241,7 +1243,10 @@ int main(int argc, char** argv) { ++checked; CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); ++checked; - CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); + const auto driverDevice = AgcDriver::DriverDetail::Driver::Get().device.Load(); + Require(driverDevice != nullptr, "native depth clear fixture did not initialize the driver Vulkan device"); + const auto driverContext = driverDevice->graphicsContext(); + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock, true, &driverContext); ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; From eb55d58728f367f527253b3ae5b30ed45030fdf3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:33:40 +0000 Subject: [PATCH 0219/1360] test(agc): use driver context for HTILE import policy --- .../Execution/include/Driver/Driver.hpp | 1 + .../execution/NativeMultisampleDepth.cpp | 28 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 06125edcac..22810c28c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -85,6 +85,7 @@ private: friend class SampledReadScope; friend struct PendingView; friend struct PacketTimer; + friend struct DriverTestAccess; void stop(); Driver(); void rethrowFailure() const; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index dff5cf51ca..d8bc68faf4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -39,6 +39,12 @@ #include #endif +namespace AgcDriver::DriverDetail { +struct DriverTestAccess { + static std::shared_ptr GetDevice() { return Driver::Get().device.Load(); } +}; +} + namespace { using namespace AgcDriver::Graphics; @@ -189,7 +195,7 @@ std::vector DriverPm4Packet(std::uint32_t opcode, std::initialize return words; } -void SubmitDriverHtileClear(std::uint64_t address, std::unique_lock& gpuLock) { +Context SubmitDriverHtileClear(std::uint64_t address, std::unique_lock& gpuLock) { const auto shader = reinterpret_cast(HtileMaskKernel.data()); std::vector commands; const auto append = [&commands](std::vector packet) { commands.insert(commands.end(), packet.begin(), packet.end()); }; @@ -212,6 +218,9 @@ void SubmitDriverHtileClear(std::uint64_t address, std::unique_lockgraphicsContext(); } template @@ -693,7 +702,7 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true, const Context* importPolicyContext = nullptr) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); @@ -821,14 +830,16 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, } } shutdownDriver; const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); - SubmitDriverHtileClear(target.htileAddress, gpuLock); + const auto driverContext = SubmitDriverHtileClear(target.htileAddress, gpuLock); recorder.Activate(); Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) { - const auto& policyContext = importPolicyContext != nullptr ? *importPolicyContext : context; - Require(PrepareImportWatch(policyContext) == ImportWatch::Unwatch, + Require(PrepareImportWatch(driverContext) == ImportWatch::Unwatch, "the HTILE metadata range lost write-watch coverage on a device whose host imports stay watched"); - std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; clear output checks still run"); + std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; the production depth-sampling path may still decline this clear"); + TargetRefuses([&] { Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "native depth owner was not retained"); }, + "native multisample depth clear requires tracked HTILE generations"); + return; } else { const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); Require(storedAll, "driver idle returned before recording the completed HTILE dispatch"); @@ -1243,10 +1254,7 @@ int main(int argc, char** argv) { ++checked; CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); ++checked; - const auto driverDevice = AgcDriver::DriverDetail::Driver::Get().device.Load(); - Require(driverDevice != nullptr, "native depth clear fixture did not initialize the driver Vulkan device"); - const auto driverContext = driverDevice->graphicsContext(); - CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock, true, &driverContext); + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; From 3a78b97f5169b4b477380dc478be4d207045ee04 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:21:04 +0200 Subject: [PATCH 0220/1360] fix(libc): check the fread size overflow before the stream is touched The console's fread returns EINVAL and 0 for an overflowing size * count before it reads the stream, so a null or closed stream with an overflowing request answers EINVAL instead of throwing or EBADF. Move the check ahead of the open check and the handle lookup, keeping the zero size and zero count result, and use EINVAL instead of the literal 22. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 2 +- core/libs/tests/FileStreams.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index fe5c961e6c..b702f30cf7 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -428,11 +428,11 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const } size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { + if (size != 0 && count > std::numeric_limits::max() / size) { errno = EINVAL; return 0; } if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetStreamHandle(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); - if (count > std::numeric_limits::max() / size) { errno = 22; return 0; } const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fread: buffer address overflow"); const GuestArena::HostWrite destination(buffer, bytes); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 82ba06d385..45369e6811 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -142,6 +142,8 @@ int main(int argc, char** argv) { ExpectException([&] { fwrite_nid_postfix(nullptr, 1, sizeof(payload), stream); }); errno = 0; Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) == 0); + errno = 0; + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, nullptr) == 0 && errno == EINVAL); ExpectException([&] { fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream); }); errno = 0; Require(fseek_nid_postfix(stream, 0, -1) == -1); From 338e2cbe97b2ff854cce582682e92447fb2dc0bd Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:46:51 -0700 Subject: [PATCH 0221/1360] test(libkernel): skip the reparse point checks when setting one is refused for rights Both fixtures required FSCTL_SET_REPARSE_POINT to succeed; a user without the right would have aborted guest_filesystem and guest_kernel_sys. They now print that the checks are skipped and carry on, as the symbolic link checks do without Developer Mode. On this PC the control succeeds at medium integrity, so the checks ran both elevated and unelevated. Co-Authored-By: Claude Fable 5.1 --- core/libs/tests/GuestFilesystem.cpp | 40 +++++++++++++++++------------ core/libs/tests/GuestKernelSys.cpp | 31 ++++++++++++++-------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 6085dc7d51..bfe173e221 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1006,24 +1006,30 @@ int main() { reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); DWORD returned = 0; - Require(DeviceIoControl(taggedHandle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), - nullptr, 0, &returned, nullptr) != 0); + const bool taggedSet = DeviceIoControl(taggedHandle, FSCTL_SET_REPARSE_POINT, reparseData.data(), + static_cast(reparseData.size()), nullptr, 0, &returned, nullptr) != 0; + if (!taggedSet) Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD || GetLastError() == ERROR_ACCESS_DENIED); Require(CloseHandle(taggedHandle) != 0); - Require((GetFileAttributesW(tagged.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); - FileStat taggedStatus{}; - FileStat taggedFollowed{}; - const int taggedResult = lstat_nid_postfix(tagged.string().c_str(), &taggedStatus); - const int taggedError = *__error_nid_postfix(); - Require(stat_nid_postfix(tagged.string().c_str(), &taggedFollowed) == taggedResult); - Require(taggedResult == 0 ? (taggedStatus.st_ino == taggedFollowed.st_ino && taggedStatus.st_mode == taggedFollowed.st_mode) - : *__error_nid_postfix() == taggedError); - const HANDLE taggedCleanup = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, - FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); - Require(taggedCleanup != INVALID_HANDLE_VALUE); - reparseHeader.ReparseDataLength = 0; - Require(DeviceIoControl(taggedCleanup, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, - nullptr, 0, &returned, nullptr) != 0); - Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); + if (!taggedSet) { + Require(std::filesystem::remove(tagged)); + std::fprintf(stderr, "Windows reparse point checks skipped: setting a reparse point on a file needs rights this user lacks\n"); + } else { + Require((GetFileAttributesW(tagged.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + FileStat taggedStatus{}; + FileStat taggedFollowed{}; + const int taggedResult = lstat_nid_postfix(tagged.string().c_str(), &taggedStatus); + const int taggedError = *__error_nid_postfix(); + Require(stat_nid_postfix(tagged.string().c_str(), &taggedFollowed) == taggedResult); + Require(taggedResult == 0 ? (taggedStatus.st_ino == taggedFollowed.st_ino && taggedStatus.st_mode == taggedFollowed.st_mode) + : *__error_nid_postfix() == taggedError); + const HANDLE taggedCleanup = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedCleanup != INVALID_HANDLE_VALUE); + reparseHeader.ReparseDataLength = 0; + Require(DeviceIoControl(taggedCleanup, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); + } #endif FileStat identity{}; FileStat byDescriptor{}; diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a6bbadefc6..a2e7bd23b9 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -316,10 +316,17 @@ int main() { reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); DWORD returned = 0; - Require(DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), - nullptr, 0, &returned, nullptr) != 0); + const bool set = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), + nullptr, 0, &returned, nullptr) != 0; + if (!set) Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD || GetLastError() == ERROR_ACCESS_DENIED); Require(CloseHandle(handle) != 0); + if (!set) { + Require(std::filesystem::remove(path)); + std::fprintf(stderr, "Windows reparse point checks skipped: setting a reparse point on a file needs rights this user lacks\n"); + return false; + } Require((GetFileAttributesW(path.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + return true; }; const auto clearReparseTag = [](const std::filesystem::path& path, DWORD tag) { const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, @@ -335,17 +342,19 @@ int main() { }; const auto tagged = root / "tagged.bin"; std::ofstream(tagged, std::ios::binary) << "abc"; - setReparseTag(tagged, 0x00000ABC); - Require(readlink_nid_postfix(tagged.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); - clearReparseTag(tagged, 0x00000ABC); + if (setReparseTag(tagged, 0x00000ABC)) { + Require(readlink_nid_postfix(tagged.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + clearReparseTag(tagged, 0x00000ABC); + } const auto surrogate = root / "surrogate.bin"; std::ofstream(surrogate, std::ios::binary) << "abc"; - setReparseTag(surrogate, 0x20000ABC); - Require(readlink_nid_postfix(surrogate.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); - FileStat surrogateStatus{}; - Require(lstat_nid_postfix(surrogate.string().c_str(), &surrogateStatus) == 0); - Require(surrogateStatus.st_mode == 0100666 && surrogateStatus.st_nlink == 1); - clearReparseTag(surrogate, 0x20000ABC); + if (setReparseTag(surrogate, 0x20000ABC)) { + Require(readlink_nid_postfix(surrogate.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + FileStat surrogateStatus{}; + Require(lstat_nid_postfix(surrogate.string().c_str(), &surrogateStatus) == 0); + Require(surrogateStatus.st_mode == 0100666 && surrogateStatus.st_nlink == 1); + clearReparseTag(surrogate, 0x20000ABC); + } #endif std::error_code error; const auto link = (root / "link").string(); From 5804207c50c582980960332b0fa7fce6fd9a48f9 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:22:51 +0200 Subject: [PATCH 0222/1360] perf(agc): stage the written elements of address-based compute builds in device memory An address-based compute build bound its written V# elements in place in the host imports of the cached address space, and its BDA table sent the same addresses there, so every access went over PCIe. A written element inside one import region now becomes a region of its own, staged like any written element (copy-in, device-local shadow, copy-back), and AddressRanges cuts the import's table entry around it so pointers into the element reach the shadow as its descriptor does. resolveOverlays keeps a cluster only when it lies inside one direct import region, is stagingEligible and is crossed by no in-place range or own region. Dispatches under APS5_BDA_STAGING_MIN_THREADS (512) threads keep the imports; APS5_NO_BDA_STAGING=1 turns it off. Test: agc_bda_staging. The copy-back trade-off of staged elements is listed as technical debt. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Graphics/include/GuestBufferMemory.hpp | 12 +- .../Graphics/src/GuestBufferMemory.cpp | 70 ++++++++++- .../Graphics/src/ShaderResources.cpp | 5 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 114 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 + docs/dev/debt/libSceAgcDriver.md | 1 + 7 files changed, 203 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 38bbf8564c..69d7a18e3b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1442,6 +1442,12 @@ if(BUILD_TESTING) set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_bda_staging COMMAND agc_driver_recorder_tests --bda-staging-only) + set_tests_properties(agc_bda_staging PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_bda_staging PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_read_written_staging COMMAND agc_driver_recorder_tests --read-written-staging-only) set_tests_properties(agc_read_written_staging PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..139a81df16 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -284,6 +284,7 @@ public: // calls RecordCopyBacks (a dispatch), since a staged region's results reach guest memory by // that copy alone. Call before Upload. void AllowDeviceStaging() { stagingAllowed = true; } + void DisallowAddressStaging() { addressStaging = false; } void AllowAdjustedRegions() { adjustedRegions = true; } // Records the copy-in of every staged region anew for another use of this upload (a resource // cache hit, from ShaderResources::Revalidate, under GuestMemory::GpuMutex, once the imports @@ -420,6 +421,7 @@ private: // The device refused the shadow (memory or allocations exhausted): the region takes the // path it would take without staging, in both upload stages. bool unstaged = false; + bool overlay = false; }; // How [begin, end) lies against the space's base regions. @@ -461,10 +463,12 @@ private: // APS5_GPU_COPY_MAX_KIB allows, and APS5_CPU_COPIES unset. bool gpuCopyEligible(const Region& region) const; // Whether a region inside a host import is staged in device memory instead of bound in place - // (see GuestBufferMemory.cpp): staging allowed, not address-based, a written element inside, - // and an atomic element or a size within the written-shadow window. Independent of the import, - // so UploadPrepare and UploadFinish decide alike. + // (see GuestBufferMemory.cpp): staging allowed, not address-based unless an overlay region, a + // written element inside, and an atomic element or a size within the written-shadow window. + // Independent of the import, so UploadPrepare and UploadFinish decide alike. bool stagingEligible(const Region& region, bool addressable) const; + bool overlayCandidate(const Region& inside, bool readOnly) const; + void resolveOverlays(); bool bindableInPlace(std::uint64_t offset, bool addressable) const; // Records the import-to-buffer copies of the given gpuCopy regions into the open batch, with // the barriers that order them after earlier recorded writes and before the shaders reading them. @@ -472,6 +476,7 @@ private: void takeHeapReferences(); Context context; bool stagingAllowed = false; + bool addressStaging = true; bool adjustedRegions = false; GuestAllocations::Lease lease; // The cached address space this build maps through (its lease pins the ranges); `regions` then @@ -490,6 +495,7 @@ private: bool regionsOverlap = false; std::vector> writes; std::vector> readsOverBase; + std::vector> servedInsideBase; std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7944e8b199..4ae5b0674e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -3969,6 +3969,11 @@ const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address, namespace { +bool bdaStagingEnabled() { + static const bool disabled = std::getenv("APS5_NO_BDA_STAGING") != nullptr; + return !disabled; +} + template bool mirrorOverlaps(const Regions& base, std::uint64_t begin, std::uint64_t end) { auto found = std::upper_bound(base.begin(), base.end(), begin, [](std::uint64_t value, const auto& region) { return value < region.begin; }); @@ -3986,12 +3991,18 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b const auto begin = address & ~std::uint64_t{3}; const auto end = begin == address ? address + bytes : (address + bytes + 3) & ~std::uint64_t{3}; const Region* inside = nullptr; + bool overlay = false; switch (baseOverlap(begin, end, &inside)) { case BaseOverlap::Inside: if (!servesView(*inside, begin)) { dissolveSpace(false); break; } + if (overlayCandidate(*inside, readOnly) && GuestMemory::DescribeCommitted(begin, static_cast(end - begin)).whole) { + overlay = true; + break; + } + if (bdaStagingEnabled() && stagingAllowed) servedInsideBase.emplace_back(begin, end); return; case BaseOverlap::Partial: if (readOnly && !mirrorOverlaps(space->base, begin, end) && !WritesOverlap(begin, static_cast(end - begin))) { @@ -4010,6 +4021,7 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b region.atomic = atomic; region.swept = swept; region.lastView = begin; + region.overlay = overlay; auto committed = GuestMemory::DescribeCommitted(begin, static_cast(end - begin)); if (!committed.whole) { // A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left @@ -4204,8 +4216,10 @@ VkBufferUsageFlags gpuCopyUsage(bool addressable) { // copied back by RecordCopyBacks after the work, with the pending-write note and the direct-write // marks of an in-place write. Only builds whose every use records the work and calls // RecordCopyBacks take it (dispatches, see AllowDeviceStaging; a synchronous draw would store the -// staging bytes from the CPU, which a device-local buffer cannot serve), and never address-based -// ones (their pointers read the import in place, and AddressRanges needs a device address). The +// staging bytes from the CPU, which a device-local buffer cannot serve). In an address-based build +// a written element inside one direct import region of the space becomes an overlay region +// (resolveOverlays), and AddressRanges cuts the import's range around it so that its pointers reach +// the shadow as its descriptor does. The // shadow is per use, not resident: a reused build records its copy-in anew (RecordStagingCopies). // Accepted trade-off, as for the misaligned copies above: a CPU write into a staged range between // the copy-in and the copy-back is rolled back by the copy-back (the whole written element range, @@ -4353,16 +4367,56 @@ bool GuestBufferMemory::bindableInPlace(std::uint64_t offset, bool addressable) } bool GuestBufferMemory::stagingEligible(const Region& region, bool addressable) const { - if (!stagingAllowed || addressable || region.unstaged || !gpuCopiesEnabled() || region.sparse || region.mirror != nullptr) return false; + if (!stagingAllowed || (addressable && !region.overlay) || region.unstaged || !gpuCopiesEnabled() || region.sparse || region.mirror != nullptr) return false; const auto bytes = region.end - region.begin; if (!WritesOverlap(region.begin, static_cast(bytes))) return false; if (region.atomic && atomicStagingEnabled() && bytes <= atomicStageMax()) return true; return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(), SweptShadowMax) : writtenShadowMax()); } +bool GuestBufferMemory::overlayCandidate(const Region& inside, bool readOnly) const { + return !readOnly && bdaStagingEnabled() && stagingAllowed && addressStaging && gpuCopiesEnabled() && writtenShadowEnabled() && inside.direct != nullptr && inside.mirror == nullptr && !inside.sparse && Recorder::Active() != nullptr; +} + +void GuestBufferMemory::resolveOverlays() { + std::vector members; + for (std::size_t index = 0; index < regions.size(); ++index) { + if (regions[index].overlay) members.push_back(index); + } + if (members.empty()) return; + std::vector drop(regions.size(), space == nullptr); + std::sort(members.begin(), members.end(), [&](std::size_t left, std::size_t right) { return regions[left].begin < regions[right].begin; }); + for (std::size_t first = 0; space != nullptr && first < members.size();) { + Region cluster{regions[members[first]].begin, regions[members[first]].end, true, {}, nullptr}; + cluster.overlay = true; + auto last = first; + for (; last < members.size() && (last == first || regions[members[last]].begin < cluster.end); ++last) { + cluster.end = std::max(cluster.end, regions[members[last]].end); + cluster.atomic = cluster.atomic || regions[members[last]].atomic; + cluster.swept = cluster.swept || regions[members[last]].swept; + } + const auto crosses = [&](std::uint64_t begin, std::uint64_t end) { return begin < cluster.end && cluster.begin < end && (begin < cluster.begin || end > cluster.end); }; + const Region* inside = nullptr; + bool keep = baseOverlap(cluster.begin, cluster.end, &inside) == BaseOverlap::Inside && inside != nullptr && inside->direct != nullptr && inside->mirror == nullptr && stagingEligible(cluster, true); + keep = keep && std::none_of(servedInsideBase.begin(), servedInsideBase.end(), [&](const auto& range) { return crosses(range.first, range.second); }); + keep = keep && std::none_of(regions.begin(), regions.end(), [&](const Region& region) { return !region.overlay && region.begin < cluster.end && cluster.begin < region.end; }); + for (auto member = first; member < last; ++member) drop[members[member]] = !keep; + first = last; + } + std::size_t kept = 0; + for (std::size_t index = 0; index < regions.size(); ++index) { + if (drop[index]) continue; + if (kept != index) regions[kept] = std::move(regions[index]); + ++kept; + } + regions.erase(regions.begin() + static_cast(kept), regions.end()); +} + void GuestBufferMemory::UploadPrepare(bool addressable) { Require(!prepared && !uploaded, "guest memory was already uploaded"); prepared = true; + resolveOverlays(); + servedInsideBase.clear(); std::vector order(regions.size()); std::iota(order.begin(), order.end(), 0u); std::sort(order.begin(), order.end(), [&](std::uint32_t left, std::uint32_t right) { return regions[left].begin < regions[right].begin; }); @@ -5127,12 +5181,20 @@ std::vector GuestBufferMemory::AddressRanges() std::vector result; if (space != nullptr) { std::vector own; + std::vector overlays; own.reserve(regions.size()); - for (const auto& region : regions) own.push_back(addressRange(region)); + for (const auto& region : regions) (region.overlay ? overlays : own).push_back(addressRange(region)); std::vector uncovered; uncovered.reserve(own.size()); AppendUncovered(own, space->ranges, uncovered); result = MergeByBegin(space->ranges, uncovered); + if (!overlays.empty()) { + std::sort(overlays.begin(), overlays.end(), [](const auto& left, const auto& right) { return left.begin < right.begin; }); + std::vector cut; + cut.reserve(result.size() + overlays.size()); + AppendUncovered(result, overlays, cut); + result = MergeByBegin(cut, overlays); + } } else if (regionsOverlap) { std::vector live; std::vector copies; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d0f6029601..f24a39cd00 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1527,6 +1527,11 @@ ShaderResources::ShaderResources(const Context& context, const CompiledShader& c // Every use of a compute build is a recorded dispatch that calls MarkGpuWrites, which staged // buffers need (a synchronous draw's use would not). guestMemory.AllowDeviceStaging(); + static const std::uint64_t addressStagingMinThreads = [] { + const char* value = std::getenv("APS5_BDA_STAGING_MIN_THREADS"); + return value != nullptr ? std::strtoull(value, nullptr, 10) : 512ull; + }(); + if (dispatchThreads != 0 && dispatchThreads < addressStagingMinThreads) guestMemory.DisallowAddressStaging(); const std::span shaders(&deferredCompute, 1); if (!deferred) { prepareAddressBindings(shaders, snapshots); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 9c1ae9ae14..922825d250 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3064,6 +3064,114 @@ void resourceReadTests(const Device& device, Recorder& recorder) { HostImportFor(context, address, bytes); } +bool bdaStagingTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0 || !context.bufferDeviceAddress || !AddressSpaceCounters().enabled || std::getenv("APS5_NO_BDA_STAGING") != nullptr) { + std::cout << "skipped, host imports are unavailable (no device addresses or address space cache)\n"; + return false; + } + constexpr std::size_t bytes = 4u << 20u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the test block"); + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr || import->address == 0) { + std::cout << "skipped, host imports are unavailable: the test block's import was refused\n"; + return false; + } + const auto importBase = import->base; + const auto importAddress = import->address; + const auto importBuffer = import->buffer; + auto* words = static_cast(block); + const auto fill = context.Function("vkCmdFillBuffer"); + const auto element = address + 8192; + constexpr std::size_t elementBytes = 64u << 10u; + constexpr std::size_t first = 8192 / 4; + const auto checkTable = [&](const std::vector& ranges, bool staged) { + for (std::size_t index = 1; index < ranges.size(); ++index) Require(ranges[index - 1].end <= ranges[index].begin, "the BDA table of a staged build has overlapping or unsorted ranges"); + bool covered = false; + std::uint64_t reached = address; + for (const auto& range : ranges) { + if (range.end <= address || range.begin >= address + bytes) continue; + Require(range.begin == reached, "the BDA table leaves a hole in the registered block"); + reached = range.end; + const bool inImport = range.deviceAddress == importAddress + (range.begin - importBase); + const bool overElement = range.begin < element + elementBytes && element < range.end; + covered = covered || overElement; + if (overElement && staged) { + Require(range.begin == element && range.end == element + elementBytes && !inImport, "the staged element's BDA range does not send its addresses to the shadow"); + Require((range.permissions & ShaderRecompiler::BdaAbi::Write) != 0u, "the staged element's BDA range is not writable"); + } else { + Require(inImport, "a BDA range outside the staged element left the import"); + } + } + Require(covered, "no BDA range covers the written element"); + Require(reached == address + bytes, "the BDA table does not reach the end of the registered block"); + }; + { + GuestBufferMemory memory(context); + memory.AllowDeviceStaging(); + memory.AcquireRegistered(); + memory.AddWritable(element, elementBytes, false, false); + memory.AddReadable(element + 16, 64); + memory.AddReadable(address + (1u << 20u), 64); + memory.Upload(true); + checkTable(memory.AddressRanges(), true); + const auto cached = memory.CachedAddressTable(); + Require(cached.has_value() && cached->ranges != nullptr && cached->ranges->size() == memory.AddressRanges().size(), "the cached BDA table of a staged build differs from its ranges"); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(element, elementBytes, adjustment); + Require(view.buffer != importBuffer, "the written element of an address-based build is bound in place"); + std::uint32_t innerAdjustment = 0; + Require(memory.Descriptor(element + 16, 64, innerAdjustment).buffer == view.buffer, "a read inside the staged element does not read its shadow"); + recorder.Sync(); + auto commands = recorder.Commands(); + fill(commands, view.buffer, view.offset + adjustment + 64u * 4u, 64, 0x22222222u); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT); + memory.RecordCopyBacks(recorder); + recorder.Sync(); + memory.MarkDirectWrites(); + std::size_t wrong = 0; + for (std::size_t index = 0; index < bytes / 4u; ++index) { + const auto at = index >= first ? index - first : bytes; + const auto expected = at >= 64 && at < 80 ? 0x22222222u : 0x11111111u; + if (words[index] != expected && wrong++ < 8) std::cout << "address-based staging copy-back dword " << index << " holds 0x" << std::hex << words[index] << ", expected 0x" << expected << std::dec << "\n"; + } + Require(wrong == 0, "an address-based staged copy-back left the wrong guest bytes"); + memory.WriteBack(); + } + std::memset(block, 0x11, bytes); + { + GuestBufferMemory memory(context); + memory.AllowDeviceStaging(); + memory.AcquireRegistered(); + memory.AddReadable(element - 16, 64); + memory.AddWritable(element, elementBytes, false, false); + memory.Upload(true); + checkTable(memory.AddressRanges(), false); + std::uint32_t adjustment = 0; + Require(memory.Descriptor(element, elementBytes, adjustment).buffer == importBuffer, "an element crossed by an in-place read was staged"); + memory.RecordCopyBacks(recorder); + recorder.Sync(); + memory.WriteBack(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + return true; +} + bool readWrittenStagingTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { @@ -10125,6 +10233,11 @@ int main(int argc, char** argv) { std::cout << "Completion label storage and ordering tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--bda-staging-only") { + if (!bdaStagingTests(device, recorder)) return 77; + std::cout << "address-based staging tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--read-written-staging-only") { if (!readWrittenStagingTests(device, recorder)) return 77; std::cout << "read and written staging tests passed\n"; @@ -10315,6 +10428,7 @@ int main(int argc, char** argv) { refreshProofTests(device, recorder); resourceReadTests(device, recorder); readWrittenStagingTests(device, recorder); + bdaStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); multisampleGuestAccessTests(device, recorder); tiledResolveTests(device, recorder); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 58eb4f894d..d1bb91585a 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -81,6 +81,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_BARRIER_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts the barriers a hazard-tracking validator would emit and skip, by class and hazard kind | | `APS5_BATCH_CAP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 64 | recorded work since the last submit at which a label batch, flush or reap is forced (0 disables the cap) | | `APS5_BDA_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | also flushes pending storage results over address-based shader regions, which are otherwise exempt | +| `APS5_BDA_STAGING_MIN_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 512 | smallest dispatch, in threads, whose address-based build stages its written elements device-local; an indirect dispatch always does | | `APS5_BDA_TABLE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | 8 | number of recently used BDA page tables kept in the cache (1 is the previous behaviour) | | `APS5_BLOCK_WRITEBACK_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | stores the touched units only on every write-back instead of widening repeated partial stores | | `APS5_BLOCK_WRITEBACK_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | 8 | window of presents within which repeated partial stores trigger the whole-image widening | @@ -190,6 +191,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_ATOMIC_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds atomic shader elements in place instead of staging them through a device-local buffer | | `APS5_NO_BARRIER_BATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the staging, write-back and label-run leading barriers that a covering barrier already orders, instead of leaving them out | | `APS5_NO_BARRIER_ELISION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every leading barrier instead of merging it into the following submission | +| `APS5_NO_BDA_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds the written elements of address-based compute builds in place in the host imports instead of staging them device-local | | `APS5_NO_BDA_TABLE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | | builds every BDA descriptor table instead of reusing the recent-table cache | | `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | | `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3b7bc09d9c..8d5eaac385 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -198,6 +198,7 @@ - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. From 23a8673cfbe6434867ca0529a21a0e196bc8364a Mon Sep 17 00:00:00 2001 From: frankea Date: Tue, 20 Oct 2026 21:56:44 -0400 Subject: [PATCH 0223/1360] docs(libScePad): keep the touchpad entry under 1000 characters --- docs/dev/debt/libScePad.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 7381662570..02db26a1ad 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -163,4 +163,4 @@ ### Functional -- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. On a PS5 (firmware 13.40), `scePadGetControllerInformation` called inside a retail title on its DualSense handle returned 1920 by 1080 at 44.86, and stick dead zones of 13 on both sides, which the library also reports +- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. A PS5 on firmware 13.40 reported 1920 by 1080 at 44.86 and stick dead zones of 13 to a retail title From 62b29b6e770357f2c6c1b41bee8b3cd8f50118bc Mon Sep 17 00:00:00 2001 From: frankea Date: Tue, 20 Oct 2026 22:10:29 -0400 Subject: [PATCH 0224/1360] docs(debt): say where the 13.40 values were measured without naming the title --- docs/dev/debt/libSceAgcDriver.md | 6 +++--- docs/dev/debt/libSceSystemService.md | 2 +- docs/dev/debt/libSceVideoOut.md | 8 ++++---- docs/dev/debt/shader.md | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 39b5600312..cc653ff1b4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -8,7 +8,7 @@ - [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified -- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside Gran Turismo 7, PPSA01317) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured +- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured - [Initial point context](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - new queues and CLEAR_STATE seed PA_SU_POINT_SIZE 0x00080008 and PA_SU_POINT_MINMAX 0xffff0000, matching every public context table in libSceAgc/Command/include/RegisterDefaultsData.hpp. Cocoon (PPSA08766) reaches geometry point output without writing these registers in the observed startup. Applying the SDK context defaults to initial/CLEAR_STATE driver context is inferred; GPU power-on reset state is not measured and is not the source of these values. Explicit writes and pushed/restored contexts retain their own values. @@ -116,7 +116,7 @@ - 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. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. -- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5, which a PS5 does not: on firmware 13.40 (measured 2026-10-19 with buffers submitted through `sceAgcDriverSubmitDcb` inside Gran Turismo 7) a DST_SEL 3 `WRITE_DATA` (control 0x00100300, or 0x06100300 with cache policy 3) left its label in direct memory unwritten, also after a `RELEASE_MEM` with GL2 write-back and invalidate (0x00300514), and a GDS `COPY_DATA` (SRC_SEL 3) at that address read 0. The console's own `sceAgcDcbWriteData(dst 6)` builds DST_SEL 3 (0x00110300) as here, and Killzone: Liberation (PPSA11142) fences its frame with it and waits on that label with `WAIT_REG_MEM`, so the write is kept until the path that lands the label is known. Closes when: that path is measured +- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5, which a PS5 does not: on firmware 13.40 (measured 2026-10-19 with buffers submitted through `sceAgcDriverSubmitDcb` inside a retail title) a DST_SEL 3 `WRITE_DATA` (control 0x00100300, or 0x06100300 with cache policy 3) left its label in direct memory unwritten, also after a `RELEASE_MEM` with GL2 write-back and invalidate (0x00300514), and a GDS `COPY_DATA` (SRC_SEL 3) at that address read 0. The console's own `sceAgcDcbWriteData(dst 6)` builds DST_SEL 3 (0x00110300) as here, and Killzone: Liberation (PPSA11142) fences its frame with it and waits on that label with `WAIT_REG_MEM`, so the write is kept until the path that lands the label is known. Closes when: that path is measured - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and writes that copy. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so a copy that still holds one throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips its invalid entries and writes its valid ones. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. @@ -163,7 +163,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp. On a PS5 (firmware 13.40, measured 2026-10-19 on the GPU through `sceAgcDriverSubmitDcb` from inside Gran Turismo 7, PPSA01317) `SRC_SEL` 9 and the `RELEASE_MEM` DATA_SEL 3 timestamp were the same counter (0x1C and 0x5C ticks apart within one buffer), and it ran at 98.0 and 99.8 MHz over two 1.1 s intervals timed by the CPU, i.e. 100 MHz within the submission jitter, as the 10 ns ticks here; only its starting value is the host's. The `COPY_DATA` cache policy fields are accepted and have no effect. +- [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp. On a PS5 (firmware 13.40, measured 2026-10-19 on the GPU through `sceAgcDriverSubmitDcb` from inside a retail title) `SRC_SEL` 9 and the `RELEASE_MEM` DATA_SEL 3 timestamp were the same counter (0x1C and 0x5C ticks apart within one buffer), and it ran at 98.0 and 99.8 MHz over two 1.1 s intervals timed by the CPU, i.e. 100 MHz within the submission jitter, as the 10 ns ticks here; only its starting value is the host's. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 33051812f0..a8ba8da6e0 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -15,7 +15,7 @@ - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 and 400 answer; every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside Gran Turismo 7 (PPSA01317) on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0, which is returned here too, although what the parameter means is not known. Whether the console writes the value on that error is not measured +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 and 400 answer; every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0, which is returned here too, although what the parameter means is not known. Whether the console writes the value on that error is not measured - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [libSceSystemService firmware 3.20 pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - 477 symbol names are present in [the PS5 3.20 libSceSystemService export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemService.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 2314b540dc..240ed48bce 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -13,17 +13,17 @@ - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. -- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - bytes 48-53 are written as zero, where a PS5 (firmware 13.40, measured 2026-10-19 inside Gran Turismo 7, PPSA01317) with a 4K TV that takes 120 Hz returned the 16-bit values 48, 60 and 60 (0x30, 0x3C, 0x3C); their meaning is not established, so the value at byte 52 never adjusts `refreshRate`. `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746: bytes 0-47 are `sceVideoOutGetOutputStatus`'s and the rest 0, a null `status` returns `0x80290002` and an unknown handle `0x8029000B`, all as that console answered. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. +- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - bytes 48-53 are written as zero, where a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) with a 4K TV that takes 120 Hz returned the 16-bit values 48, 60 and 60 (0x30, 0x3C, 0x3C); their meaning is not established, so the value at byte 52 never adjusts `refreshRate`. `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746: bytes 0-47 are `sceVideoOutGetOutputStatus`'s and the rest 0, a null `status` returns `0x80290002` and an unknown handle `0x8029000B`, all as that console answered. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside Gran Turismo 7), whose answers for every other mode match the ones here. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 -- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; a PS5 (firmware 13.40, measured 2026-10-19 inside Gran Turismo 7, PPSA01317) reported its TV's 60.0f. The 48-byte layout, from the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), is confirmed there: full size and pane 3840 x 2160, `refreshRate` 3, `flags` 0 and the rest 0 on GT7's handle, 0x8029000B for handles 0 to 8. `flags` is 0 here too; its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside a retail title), whose answers for every other mode match the ones here. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 +- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) reported its TV's 60.0f. The 48-byte layout, from the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), is confirmed there: full size and pane 3840 x 2160, `refreshRate` 3, `flags` 0 and the rest 0 on the title's handle, 0x8029000B for handles 0 to 8. `flags` is 0 here too; its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceVideoOutSubmitFlip](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut, libSceAgcDriver) - in the [AGC driver](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp), a CPU flip waits for the graphics-queue submissions ahead of it when the calling thread has submitted graphics-queue work itself: a title that submits a frame and flips it without waiting relies on that order, since emulated GPU work finishes later than the console's. A flip from a thread that has never submitted graphics-queue work is presented without waiting for queued submissions, unless a command-buffer or EOP flip ahead of it has not been reached. Such a thread can only know its frame is finished by waiting for it, as the PS5 RADV WSI's flip thread waits on the frame's fence. Whether the console ever orders a CPU flip behind the graphics queue is not measured; `APS5_CPU_FLIP_IN_ORDER=1` orders every CPU flip -- [sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the third 64-bit word of the PS5 status (`reserved` in [SceTypes.hpp](../../../core/libs/SceTypes.hpp)) is written 0. On a PS5 (firmware 13.40, measured 2026-10-19) Gran Turismo 7's own call, caught inside the game, got count 0xb16b, `processTime` 0x381a8790 (941 s), third word 0x1c485ffaadc, `processTimeCounter` 0x15dd63c364b, flags 0x04 and phase 0: the third word is a running counter, about 970 s at ~2 GHz, consistent with a TSC, not zero. Its clock and what it counts from are not established. Closes when: the third word's source is measured and written +- [sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the third 64-bit word of the PS5 status (`reserved` in [SceTypes.hpp](../../../core/libs/SceTypes.hpp)) is written 0. On a PS5 (firmware 13.40, measured 2026-10-19) a retail title's own call, caught inside it, got count 0xb16b, `processTime` 0x381a8790 (941 s), third word 0x1c485ffaadc, `processTimeCounter` 0x15dd63c364b, flags 0x04 and phase 0: the third word is a running counter, about 970 s at ~2 GHz, consistent with a TSC, not zero. Its clock and what it counts from are not established. Closes when: the third word's source is measured and written - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sceVideoOutRegisterBuffers, sceVideoOutGetFlipStatus, sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - PS4 callers. A handle is treated as PS4 once buffers are registered through the PS4 `sceVideoOutRegisterBuffers`; from then on its flip and vblank status are written in the PS4 64- and 32-byte layouts, with `tsc` and `submitTsc` 0. A PS4 title that reads a status before it registers buffers gets the larger PS5 layout. The PS4 function accepts only linear A8R8G8B8_SRGB (0x80000000) and A8B8G8R8_SRGB (0x80002200) buffers without options; tiled PS4 buffers throw, as their layout differs from the PS5 tiling. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample registers its second 1920x1080 buffer 8,294,400 bytes after the first. The PS4 alignment rule is not known diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b15f710909..7e50b337d9 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -64,7 +64,7 @@ - [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)). - [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane. - [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. On gfx1100 (RDNA3, [hardware oracle](../HW_ORACLE.md), wave32 and wave64), all 13 integer image atomics, `swap` to `dec`, return the old dword and store the raw-dword result identically on 32_UINT, 32_SINT and 32_FLOAT T#s, over 121 old/data pairs per op, among them 0x80000000, 0xffffffff, 1.0, -1.0 and NaN: `add` adds the bits and `umin` is unsigned on every format. A 16_UINT control differs, so the format is read. gfx1031 (RDNA2) gives the same rows, and its `fmin`, `fmax` and `fcmpswap`, which gfx1100 lacks, also match on the three formats ([measured by thuanquang](https://github.com/boykopovar/AnyPS5/pull/7939#issuecomment-6275099482)). The console's gfx1013 is unmeasured. Other formats throw. -- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are 22.35 times smaller than on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7), where it counts the ~2.23 GHz core clock (44,041,632 against 1,970,792 `s_memrealtime` ticks). The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. +- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are 22.35 times smaller than on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title), where it counts the ~2.23 GHz core clock (44,041,632 against 1,970,792 `s_memrealtime` ticks). The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. - [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. @@ -81,7 +81,7 @@ - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples -- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from GTA V's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values +- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from GTA V's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - 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. - [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. @@ -143,8 +143,8 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. @@ -155,7 +155,7 @@ - [2D biased sampling through a one-slice, single-level 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) supplies a zero third coordinate and retains the bias operation, with normalized point minification and magnification and no linear mip filtering. Measured on RDNA2 (AMD integrated gfx1036, driver 32.0.21036.18, Windows 11) by running an OpenCL-loaded native kernel with `image_sample_b` DIM 2D against 16x16x1 R32_UINT and RGBA8_UNORM 3D images: biases -16, -0.5, 0, 0.5 and 16 read the (u,v) texel in slice zero, including wrapped coordinates, point min/max reduction and the title's sampler with Z border addressing. Wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off; wave64 and the PS5's gfx1013 are not measured. Pixel-stage, direct, non-depth, unpacked samples whose only use is the plain 32-bit bias form are accepted; extra slices, any nonzero mip range, linear or anisotropic filtering and unnormalized coordinates throw. Vulkan readback is checked on NVIDIA RTX 5070 and AMD integrated graphics for positive and negative bias and point weighted-average/min/max reduction. - [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. -- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff, as a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside GT7) read them (0x80000000_00000000 / 0x80000000_FFFFFFFF shared, 0x70000000_00000000 / 0x70000000_FFFFFFFF private). A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read the low dword, after LLVM's `SIRegisterInfo.td` and `getSegmentAperture` and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). The 32-bit read and the wave's address mode (taken to be 64-bit, PTR32 clear) were not measured on the console +- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff, as a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) read them (0x80000000_00000000 / 0x80000000_FFFFFFFF shared, 0x70000000_00000000 / 0x70000000_FFFFFFFF private). A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read the low dword, after LLVM's `SIRegisterInfo.td` and `getSegmentAperture` and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). The 32-bit read and the wave's address mode (taken to be 64-bit, PTR32 clear) were not measured on the console - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated barycentric geometry stage do not write it, the generated rect-list and barycentric tessellation stages forward the vertex shader's array, and a vertex or tessellation evaluation shader whose guest clip and cull distances already take all eight does not write it either, so NVIDIA 615.78.08 draws such a vertex at the viewport centre; a device without `shaderClipDistance` cannot compile vertex shaders that write it (the compile throws). The PS5 rasteriser was not measured. - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) are the barycentrics at the iterated sample, which in a pixel shader the guest does not iterate is taken to be sample 0, inferred from the ancillary's iterated sample number and not measured. They are interpolated with `InterpolateAtOffset`, which [GLSL_EXT_fragment_shader_barycentric](https://github.com/KhronosGroup/GLSL/blob/0099bf83b0288469917fc2e2972cb0a2b74d6711/extensions/ext/GLSL_EXT_fragment_shader_barycentric.txt#L286-L299) names for barycentrics away from the default position, at the sample's offset from the pixel centre, read per pixel of the 2x2 quad from `PA_SC_AA_SAMPLE_LOCS` (the standard pattern unless the draw sets its own): sample 0's in a shader the guest does not iterate, and in an iterated shader the offset of the host's `SampleId` from a table of the guest's locations, because Vulkan leaves the position of plain barycentrics under sample shading implementation-defined (lavapipe uses the pixel centre). That needs the device's `sampleRateShading`; without it, and when every location is the pixel centre (a single-sample draw), they keep the host's default position, the pixel centre once per pixel and an implementation-defined position under sample shading. PERSP_CENTER and LINEAR_CENTER are interpolated at offset 0 in an iterated shader, so they stay at the pixel centre as on the console; the centroid pairs keep the host's default position. With barycentric emulation the offsets apply to the emulated barycentrics. Devices that interpolate attributes on the host instead (no usable fragment-shader barycentrics, as on MoltenVK) keep the default position for all four pairs. - [Pixel positions](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - POS_X_FLOAT and POS_Y_FLOAT follow `SPI_BARYC_CNTL.POS_FLOAT_LOCATION` (bits 17:16) and `POS_FLOAT_ULC` (bit 20), whose positions come from [PAL's GFX9+ register header](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h) and whose meanings from [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/RegisterMetadataBuilder.cpp#L767-L779): the pixel centre (0), the iterated sample (2), or the upper-left corner (ULC). Under host sample shading the centre is `floor(FragCoord) + 0.5`, and a non-iterated shader's sample is the centre plus sample 0's offset. The centroid (1) keeps `FragCoord`, the pixel centre or under sample shading the sample, not the centroid of the covered samples. A missing `SPI_BARYC_CNTL` reads as the pixel centre and the reserved location 3 throws. POS_Z_FLOAT and POS_W_FLOAT keep `FragCoord`'s depth and 1/w at the host's position. `PERSP_CENTER_CNTL`, `PERSP_CENTROID_CNTL`, `LINEAR_CENTER_CNTL` and `LINEAR_CENTROID_CNTL` (bits 0, 4, 8 and 12) are not read: PAL and LLPC never set them. From 65544dc239f8415c835a7eab9f3d681b35a889e7 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:21:23 +1300 Subject: [PATCH 0225/1360] fix(libc): stop wcstombs_s at its limit and keep wcsftime's null buffer wcstombs_s ends without error when a character does not fit a limit below the capacity, as the console's does; wcsrtombs_s still reports ERANGE there. Both paths became reachable with UTF-8. wcsftime keeps returning 0 for a null buffer, and the debt entries that described the single-byte locale now describe the UTF-8 one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestWideFormatting.cpp | 1 + docs/dev/debt/libc.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 637084256d..30f62dc841 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -270,6 +270,7 @@ static void CheckWcsftime() { Require(wcsftime_nid_postfix(buffer, 32, u"%Y \u20ac", &time) == 6 && buffer == std::u16string(u"2024 \u20ac"), "wcsftime passes a character above 0xff through UTF-8"); Require(wcsftime_nid_postfix(buffer, 7, u"%Y \u20ac", &time) == 6, "wcsftime sizes the narrow buffer for three-byte characters"); + for (auto& unit : buffer) unit = u'x'; error = 5; Require(wcsftime_nid_postfix(buffer, SIZE_MAX, u"%Y", &time) == 0 && error == 22 && buffer[0] == u'x', "wcsftime oversized buffer"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d48d92f404..137424843d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -119,7 +119,7 @@ - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written -- [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it. A null format or `tm` throws +- [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it, and a size too large for the narrow buffer returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title From 813f5c4cf9c72668d5e93d9eb6aef982cd7903d4 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:22:54 +1300 Subject: [PATCH 0226/1360] docs(libc): leave the wcsrtombs_s entry to #7841 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 137424843d..4caf6d39aa 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -104,7 +104,7 @@ - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. -- [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here +- [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1), as this libc does. Titles bundle their own `libc.prx`, whose locale was not measured - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not go through the guest's `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc From 34a729529a64b52ded9eadc8023030fde8d61cec Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:40:09 +1300 Subject: [PATCH 0227/1360] fix(libc): keep the sign of a failed strtof and drop hex ERANGE like the console - strtof, strtold, wcstof and wcstold return -0.0 when nothing converts after a minus sign, unless the next character starts an inf or nan spelling; strtod, wcstod and atof keep +0.0. - strtof, strtold, wcstof and wcstold leave errno alone for hexadecimal text that overflows or underflows. - strtod and wcstod set ERANGE for hexadecimal text whose value lies below the smallest subnormal and rounds up to it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/Strings.cpp | 20 +++++++++++++++- core/libs/tests/GuestMath.cpp | 34 ++++++++++++++++++++++------ core/libs/tests/GuestWideStrings.cpp | 4 ++++ docs/dev/debt/libc.md | 1 + 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 65d6dfb9c6..80c513fbc0 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -187,7 +187,25 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha const TFloat value = parse(text, &parsedEnd); if (errno == ERANGE && value != 0 && (value < 0 ? -value : value) < std::numeric_limits::min()) errno = saved; if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : value; + const char* subject = text; + while (std::isspace(static_cast(*subject))) ++subject; + if (parsedEnd == text) { + const bool special = std::strchr("iInN", subject[1]) != nullptr && subject[1] != '\0'; + return !std::is_same_v && subject[0] == '-' && !special ? -TFloat{0} : TFloat{0}; + } + if (*subject == '+' || *subject == '-') ++subject; + const bool hexadecimal = subject[0] == '0' && (subject[1] == 'x' || subject[1] == 'X') && parsedEnd > subject + 2; + if (!hexadecimal) return value; + if constexpr (std::is_same_v) { + constexpr double smallest = std::numeric_limits::denorm_min(); + if ((value < 0 ? -value : value) == smallest) { + const long double exact = std::strtold(text, nullptr); + if ((exact < 0 ? -exact : exact) < smallest) errno = ERANGE; + } + } else if (errno == ERANGE) { + errno = saved; + } + return value; } } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index b027130f96..c848552aa2 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -494,6 +494,19 @@ static void CheckFloatUnderflowErrno() { Require(strtod_nid_postfix("1e400", &end) == HUGE_VAL && errno == ERANGE); errno = 0; Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); + Require(strtod_nid_postfix("0x1.8p-1075", &end) == 0x1p-1074 && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("-0x1.000001p-1075", &end) == -0x1p-1074 && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("0x1p-1074", &end) == 0x1p-1074 && strtod_nid_postfix("0x1.8p-1074", &end) == 0x1p-1073 && errno == 0); + Require(strtod_nid_postfix("0x1p-1076", &end) == 0.0 && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("0x1p+1100", &end) == HUGE_VAL && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == 7); + Require(strtold_nid_postfix("0x1p-16446", &end) == 0.0L && strtold_nid_postfix("0x1p+20000", &end) == HUGE_VALL && errno == 7); + Require(strtof_nid_postfix("1e39", &end) == HUGE_VALF && errno == ERANGE); + errno = 0; } static void CheckHypotSpecialValue() { @@ -1206,16 +1219,23 @@ static void CheckLongDoubleRounding(long double value, std::int64_t expected, bo } } -static void CheckNoConversionIsPositiveZero() { +static void CheckNoConversionZero() { for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; const double parsed = strtod_nid_postfix(text, &end); Require(parsed == 0. && !std::signbit(parsed) && end == text); + const bool negative = std::strchr(text, '-') != nullptr; + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(parsedFloat == 0.f && std::signbit(parsedFloat) == negative && end == text); + const long double parsedLong = strtold_nid_postfix(text, &end); + Require(parsedLong == 0.L && std::signbit(parsedLong) == negative && end == text); + Require(!std::signbit(atof_nid_postfix(text))); + } + for (const char* text : {"-i", "-in", "-N", "-nax"}) { + char* end = nullptr; const float parsedFloat = strtof_nid_postfix(text, &end); Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); - const long double parsedLong = strtold_nid_postfix(text, &end); - Require(parsedLong == 0.L && !std::signbit(parsedLong) && end == text); - Require(!std::signbit(atof_nid_postfix(text))); + Require(!std::signbit(strtold_nid_postfix(text, nullptr))); } char* end = nullptr; const char negativeZero[] = "-0.0x"; @@ -1241,9 +1261,9 @@ static void CheckNoConversionIsPositiveZero() { const double parsedWide = wcstod_nid_postfix(wide, &wideEnd); Require(parsedWide == 0. && !std::signbit(parsedWide) && wideEnd == wide); const float parsedWideFloat = wcstof_nid_postfix(wide, &wideEnd); - Require(parsedWideFloat == 0.f && !std::signbit(parsedWideFloat) && wideEnd == wide); + Require(parsedWideFloat == 0.f && std::signbit(parsedWideFloat) && wideEnd == wide); const long double parsedWideLong = wcstold_nid_postfix(wide, &wideEnd); - Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); + Require(parsedWideLong == 0.L && std::signbit(parsedWideLong) && wideEnd == wide); } template @@ -1275,7 +1295,7 @@ static void CheckDinkumwareFloatCharacteristics() { int main() { CheckFloatClassification(); CheckDinkumwareConstants(); - CheckNoConversionIsPositiveZero(); + CheckNoConversionZero(); CheckMissingClassificationExports(); CheckFloatMathExports(); CheckIntegerConversions(); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 809d47fe3e..b92c7b9d9f 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -319,6 +319,10 @@ int main() { require(wcstod_nid_postfix(scientific, &end) == 350.0 && end == scientific + 5); require(wcstod_nid_postfix(letters, &end) == 0.0 && end == letters); require(wcstof_nid_postfix(u"0.25", nullptr) == 0.25f); + const char16_t* sign = u" -x"; + require(std::signbit(wcstof_nid_postfix(sign, &end)) && end == sign && std::signbit(wcstold_nid_postfix(sign, &end)) && !std::signbit(wcstod_nid_postfix(sign, &end))); + *__error_nid_postfix() = 0; + require(wcstof_nid_postfix(u"0x1p+200", nullptr) == HUGE_VALF && *__error_nid_postfix() == 0); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6f00311858..b4625e5bb4 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -74,6 +74,7 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. From 810054bdf19a8000a95e4168285b3bab968f0c7c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:40:55 +1300 Subject: [PATCH 0228/1360] docs(libc): move the strtof rounding entry clear of open edits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index b4625e5bb4..30f0df3026 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -74,8 +74,8 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there -- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From e38179eaab86303399bf1a60fbdece6f17d75d3b Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:40:49 -0700 Subject: [PATCH 0229/1360] feat(libkernel): export _sigintr and let signal honour it The public SDK's libkernel, libkernel_sys and libkernel_web stub tables declare _sigintr, the sigset_t that FreeBSD's libc shares between signal and siginterrupt, and no built library provided it (#8846). libkernel now exports the 16-byte object, zero at start, and signal records a handler with SA_RESTART unless its signal is in the set, as lib/libc/gen/signal.c does. siginterrupt, which fills the set, is a libSceLibcInternal export; libc does not link libkernel and cannot reach the dispositions, so it stays out and a debt entry for signal says so. Co-Authored-By: Claude Fable 5.1 --- .../libs/prx/libkernel/System/src/Signals.cpp | 4 +++- core/libs/tests/GuestSignals.cpp | 21 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 231d3d2643..2a60f977c7 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -32,6 +32,8 @@ struct GuestSigaction { }; static_assert(sizeof(GuestSigaction) == 32 && offsetof(GuestSigaction, flags) == 8 && offsetof(GuestSigaction, mask) == 12); +extern "C" GuestSignalSet _sigintr_nid_postfix{}; + namespace { using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; @@ -481,7 +483,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); if (guest < 1 || guest > MaxSignal || ((guest == GuestSigkill || guest == GuestSigstop) && handler != nullptr)) { *__error_nid_postfix() = 22; return invalid; } std::lock_guard lock(registration); - const GuestSigaction action{reinterpret_cast(handler), SaRestart, {}}; + const GuestSigaction action{reinterpret_cast(handler), SignalInSet(_sigintr_nid_postfix, guest) ? 0 : SaRestart, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } const auto previous = reinterpret_cast(dispositions[guest].handler); dispositions[guest] = action; diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 5183ec41b0..69409b2383 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -36,6 +36,7 @@ struct GuestSigaction { GuestSignalSet mask; }; static_assert(sizeof(GuestSigaction) == 32); +extern GuestSignalSet _sigintr_nid_postfix; static bool Same(const GuestSigaction& left, const GuestSigaction& right) { return left.handler == right.handler && left.flags == right.flags && std::memcmp(&left.mask, &right.mask, sizeof(left.mask)) == 0; } @@ -365,4 +366,24 @@ int main() { Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); + Require(_sigintr_nid_postfix.bits[0] == 0 && _sigintr_nid_postfix.bits[1] == 0 && _sigintr_nid_postfix.bits[2] == 0 && + _sigintr_nid_postfix.bits[3] == 0); + GuestSigaction interruptible{}; + Require(signal_nid_postfix(30, Callback) == nullptr); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[0] = 1u << 29; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0); + signal_nid_postfix(31, Callback); + Require(sigaction_nid_postfix(31, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[3] = 1u << 31; + Require(signal_nid_postfix(128, Callback) == nullptr); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.flags == 0); + _sigintr_nid_postfix.bits[0] = 0; + _sigintr_nid_postfix.bits[3] = 0; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + Require(signal_nid_postfix(30, nullptr) == Callback && signal_nid_postfix(31, nullptr) == Callback && + signal_nid_postfix(128, nullptr) == Callback); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2883d5c3d4..727f71aefa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -206,6 +206,7 @@ - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From 0b52caf71887b2ac2c8a8aa605b49b43e7d5ae54 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:42:08 +1300 Subject: [PATCH 0230/1360] docs(libc): place the strtof rounding entry beside memmem Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 30f0df3026..8f46a9a874 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -58,6 +58,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. @@ -75,7 +76,6 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) -- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From 1c96d7ca1d9f4beb97f702a5a42d181d1926adbd Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:47:20 +1300 Subject: [PATCH 0231/1360] test(libc): pin mbrtowc and mbrlen with a null string to the console Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestMultibyteLocale.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestMultibyteLocale.cpp b/core/libs/tests/GuestMultibyteLocale.cpp index b604579966..60a4a52808 100644 --- a/core/libs/tests/GuestMultibyteLocale.cpp +++ b/core/libs/tests/GuestMultibyteLocale.cpp @@ -61,8 +61,21 @@ static void CheckState() { Require(mbrtowc_nid_postfix(&value, "\xac", 1, state) == 1 && value == 0x20ac && mbsinit_nid_postfix(state), "the last byte completes it"); Require(mbrtowc_nid_postfix(&value, "\xc3", 1, state) == Incomplete, "pending again"); Require(mbrtowc_nid_postfix(&value, "A", 1, state) == Invalid && !mbsinit_nid_postfix(state), "an error leaves the state pending"); - Require(mbrtowc_nid_postfix(&value, nullptr, 1, state) == Invalid, "a null string in a pending state is an error"); - Require(mbrlen_nid_postfix(nullptr, 1, state) == 0 && mbsinit_nid_postfix(state), "mbrlen with a null string resets the state"); + unsigned char pending[16]; + std::memcpy(pending, state, sizeof(pending)); + value = 0x7777; + Require(mbrtowc_nid_postfix(&value, nullptr, 0, state) == Incomplete && std::memcmp(state, pending, sizeof(pending)) == 0, + "a null string with no bytes to read is incomplete and leaves a pending state"); + *__error_nid_postfix() = 0; + Require(mbrtowc_nid_postfix(&value, nullptr, 1, state) == Invalid && *__error_nid_postfix() == 86 && value == 0x7777 + && std::memcmp(state, pending, sizeof(pending)) == 0, + "a null string in a pending state is an error and keeps the state"); + Require(mbrlen_nid_postfix(nullptr, 0, state) == 0 && mbsinit_nid_postfix(state), "mbrlen with a null string resets the state"); + unsigned char initial[16]{}; + Require(mbrtowc_nid_postfix(&value, nullptr, 0, state) == Incomplete && std::memcmp(state, initial, sizeof(initial)) == 0, + "a null string with no bytes to read is incomplete in the initial state"); + Require(mbrtowc_nid_postfix(&value, nullptr, 1, state) == 0 && value == 0x7777 && std::memcmp(state, initial, sizeof(initial)) == 0, + "a null string in the initial state converts nothing"); Require(mbsinit_nid_postfix(nullptr) == 1, "a null state is initial"); mbtowc_nid_postfix(nullptr, nullptr, 0); From 46c36c304fed873b9226831cce426b89312930e5 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:13:08 -0700 Subject: [PATCH 0232/1360] feat(libc): export the num_get and time_get locale objects as data The public SDK's libSceLibcInternal table declares num_get::id, time_get::id and the num_get vtable as objects; libSceAmpr exported their NIDs as functions that throw, which a data consumer reads as code (#8875). libc now defines them the way the other facet ids and the num_put and codecvt vtables are kept: the ids zero, the vtable 15 zeroed slots (two header entries, the two destructor entries and Dinkumware's eleven do_get), and libSceAmpr drops the functions. An import qualified to libSceAmpr.prx falls back to libc, so each object has one identity. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAmpr/Export.cpp | 15 --------------- core/libs/prx/libc/src/LocaleSupport.cpp | 3 +++ core/libs/tests/GuestLocale.cpp | 14 ++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 6e55ecbd7b..c8b1fccf03 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -847,11 +847,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } @@ -860,21 +855,11 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { return _Cnd_init_nid_postfix(handle); } -int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNKSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE3getES3_S3_RSt8ios_baseRNSt5_IosbIiE8_IostateEP2tmPKcSE__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a4388c8dfe..f50ca75eea 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -298,6 +298,9 @@ std::uint64_t _ZNSt7codecvtIcc9_MbstatetE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {}; std::uint64_t _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[12] {}; +std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; +std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[15] {}; +std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; std::fpos_t _ZSt4_Fpz_nid_postfix {}; diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 71b71a1669..ae8292b410 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -34,6 +34,9 @@ void APS5_VABI _Locksyslock_nid_postfix(); void APS5_VABI _Unlocksyslock_nid_postfix(); std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t desc); int* APS5_VABI __error_nid_postfix(); +extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[15]; } @@ -52,6 +55,16 @@ static void RequireException(TAction action) { Require(caught); } +static void CheckInputFacetObjects() { + Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0); + _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 7; + _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 9; + Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 7 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 9); + _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + for (const auto slot : _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) Require(slot == 0); +} + static void CheckGuestCalls() { auto* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix()); @@ -249,6 +262,7 @@ int main() { CheckStreamWidth(); CheckStandardStreams(); CheckWideCaseTranslation(); + CheckInputFacetObjects(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6f00311858..ac407615bb 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -131,3 +131,4 @@ - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 15 zeroed slots (two header entries, two destructor entries and Dinkumware's eleven `do_get`), so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. From 2d3ed2c8bf32b66f7571e7b5f4ed73e2dca0af6a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:39:45 +0000 Subject: [PATCH 0233/1360] feat(agc): count a registered shader's prepared entries Driver::PreparedEntries returns how many artifacts the registered header of a shader holds prepared, after its pending registration preparation settles, so tests can tell a prepared shader from one left to its first use. --- .../Execution/include/Driver/Driver.hpp | 1 + .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 3fb9f473b3..2d647d4f62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -67,6 +67,7 @@ public: void ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void SettleShaderPreparations(std::span stages); + [[nodiscard]] std::size_t PreparedEntries(const Shader* shader); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); 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 b69bccd8fe..bef29257b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1574,6 +1574,19 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std transaction.Commit(); } +std::size_t Driver::PreparedEntries(const Shader* shader) { + std::shared_ptr snapshot; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return 0; + snapshot = RegisteredHeader(*shaders, shader); + } + if (snapshot == nullptr) return 0; + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + return snapshot->prepared->entries.size(); +} + void Driver::ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive) { BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); From 3253ab49bb34542f41d46f4dd689065836a866b9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:55:30 +0000 Subject: [PATCH 0234/1360] perf(agc): leave a lazily registered shader's resolved ABI to its first use Under APS5_LAZY_SHADER_PREPARE=1 registration prepares nothing, but ResolveShaderAbi still planned and prepared every compute or pixel stage whose ABI the title resolved, so a title that resolves every shader's ABI prepared them all at boot whether they ever drew. With lazy preparation ResolveShaderAbi now plans the stage with the ABI's state (its register and entry-point checks, as lazy registration keeps them), records the ABI key, and queues the ABI's context and primitive registers on the shader. The shader's first draw or dispatch plans and prepares the queued ABIs with that state before it looks for its artifact, so a shader that never draws or dispatches stays unprepared and one that does gets the entries eager resolution would have made. A stage whose plan fails is deferred as before. Without the switch nothing changes; graphics-stage ABIs are still prepared when they are resolved. Test: agc_driver_lazy_resolved_abi resolves the ABI of four lazily registered shaders and draws two. None holds prepared entries after resolution, the two that never drew still hold none, and a drawn one draws the same bytes as with eager preparation in a child process. --- .../include/Driver/Shaders/ShaderRegistry.hpp | 7 + .../src/Driver/Shaders/ShaderRegistry.cpp | 78 ++- .../tests/execution/LazyResolvedAbi.cpp | 455 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 203940d7c3..07718b5ad6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -153,6 +153,13 @@ struct PreparedShaderState { bool deferred = false; }; struct PreparedShaders : PreparedShaderState { + struct DeferredAbi { + std::vector key; + std::vector> context; + std::vector> primitive; + std::weak_ptr device; + }; + std::vector deferredAbis; std::mutex mutex; std::condition_variable settled; std::exception_ptr failure; 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 bef29257b4..c56915a0dd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -591,6 +591,13 @@ void PublishRegisteredShader(std::shared_ptr& registry, const st namespace { +bool LazyShaderPreparation() { + static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; + return lazy; +} + +void PrepareDeferredAbis(const ShaderSnapshot& snapshot); + void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { if (request.context.convertedSamples) { APS5_LOG_OUT_DEBUG("Shader 0x%llx samples a converted image; preparing its converted-sample variant at use", static_cast(request.shader.codeAddress)); @@ -626,6 +633,11 @@ std::shared_ptr SourceHandleFor(const Shad ShaderRecompiler::BuildPreparedShaderKey(request, key); std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); + if (!snapshot.prepared->deferredAbis.empty()) { + lock.unlock(); + PrepareDeferredAbis(snapshot); + lock.lock(); + } for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle; } @@ -779,6 +791,11 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); + if (!snapshot.prepared->deferredAbis.empty()) { + lock.unlock(); + PrepareDeferredAbis(snapshot); + lock.lock(); + } for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); @@ -1071,6 +1088,39 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } +void PrepareDeferredAbis(const ShaderSnapshot& snapshot) { + std::vector deferred; + { + std::lock_guard lock(snapshot.prepared->mutex); + deferred.swap(snapshot.prepared->deferredAbis); + } + for (const auto& abi : deferred) { + const auto device = abi.device.lock(); + if (device == nullptr || snapshot.registeredState == nullptr) continue; + QueueState state{}; + state.shader = snapshot.registeredState->shader; + state.context = snapshot.registeredState->context; + state.userConfig = snapshot.registeredState->userConfig; + for (const auto& [offset, value] : abi.context) state.context[offset] = value; + for (const auto& [offset, value] : abi.primitive) state.userConfig[offset] = value; + std::vector entries; + try { + const auto plan = PlanRegistered(snapshot, *device, state, false); + if (plan == nullptr) continue; + plan->prebuildDevice = device; + entries = PrepareRegistered(*plan); + } catch (const std::exception& error) { + APS5_LOG_ERR("Shader 0x%llx could not be prepared at its first use for its resolved ABI (%s); preparing it for the use instead", static_cast(snapshot.codeAddress), error.what()); + continue; + } + std::lock_guard lock(snapshot.prepared->mutex); + for (auto& entry : entries) { + const auto duplicate = std::ranges::any_of(snapshot.prepared->entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); + if (!duplicate) snapshot.prepared->entries.push_back(std::move(entry)); + } + } +} + void DeferPipelineStage(PreparedShaderState& prepared, const ShaderSnapshot& snapshot, const char* reason) { if (!prepared.deferred) APS5_LOG_ERR("Shader 0x%llx could not be prepared when the title created its pipeline (%s); preparing it when it is used", static_cast(snapshot.codeAddress), reason); prepared.deferred = true; @@ -1624,6 +1674,31 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + if (LazyShaderPreparation()) { + const auto& resolved = transaction.Read(*snapshot).registeredAbis; + if (std::ranges::find(resolved, key) != resolved.end()) { + transaction.Commit(); + return; + } + std::optional invalid; + try { + static_cast(PlanRegistered(*snapshot, *localDevice, state, false)); + } catch (const std::exception& error) { + invalid = error.what(); + } + auto& prepared = transaction.Edit(*snapshot); + if (invalid) DeferPipelineStage(prepared, *snapshot, invalid->c_str()); + prepared.registeredAbis.push_back(key); + prepared.deferred = true; + transaction.Commit(); + if (invalid) return; + PreparedShaders::DeferredAbi deferred{key, {}, {}, localDevice}; + for (const auto reg : context) deferred.context.emplace_back(reg.offset, reg.value); + for (const auto reg : primitive) deferred.primitive.emplace_back(reg.offset, reg.value); + std::lock_guard lock(snapshot->prepared->mutex); + snapshot->prepared->deferredAbis.push_back(std::move(deferred)); + return; + } std::optional deferral; const auto prepare = [&] { try { @@ -1864,8 +1939,7 @@ void Driver::RegisterShader(const Shader* shader) { registered.context = snapshot.registeredState->context; registered.userConfig = snapshot.registeredState->userConfig; std::shared_ptr preparation; - static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; - if (lazy) { + if (LazyShaderPreparation()) { static_cast(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred)); snapshot.prepared->deferred = true; } else if (AsyncRegistrationPrepare()) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp new file mode 100644 index 0000000000..c48494d6ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -0,0 +1,455 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Textures = 16; +constexpr std::uint32_t VertexBuffers = 3; +constexpr std::uint32_t MaxDraws = 2048; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::uint32_t TexelByte = 4; +constexpr std::size_t SrtOffset = 0; +constexpr std::size_t SamplerOffset = 0x200; +constexpr std::size_t FrameConstantsOffset = 0x210; +constexpr std::size_t VertexTableOffset = 0x300; +constexpr std::size_t FrameDataOffset = 0x400; +constexpr std::size_t VertexDataOffset = 0x800; +constexpr std::size_t TextureOffset = 0x1000; +constexpr std::size_t TextureBytes = 4096; +constexpr std::size_t ObjectOffset = TextureOffset + Textures * TextureBytes; +constexpr std::size_t ObjectBytes = 256; +constexpr std::size_t ColorOffset = (ObjectOffset + MaxDraws * ObjectBytes + 65535) & ~static_cast(65535); +constexpr std::size_t ColorBytes = 65536; +constexpr std::uint32_t RenderTargetTileMode = 0x1b; +constexpr std::size_t BlockBytes = (ColorOffset + ColorBytes + 65535) & ~static_cast(65535); + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, + 0xbf8cc07fu, + 0xe00c2000u, 0x80010a05u, 0xe00c2000u, 0x80020e05u, 0xe00c2000u, 0x80031205u, + 0xbf8c3f70u, + 0x0602030au, 0x0604050bu, 0x0606070cu, 0x0608090du, + 0x0602030eu, 0x0604050fu, 0x06060710u, 0x06080911u, + 0x06020312u, 0x06040513u, 0x06060714u, 0x06080915u, + 0xf80008cfu, 0x04030201u, + 0xbf810000u, 0xbf810000u, +}; + +constexpr std::size_t PixelWords = 10 + Textures * 10 + 5 + 8 + 3; + +constexpr std::array MakePixelCode() { + std::array code{}; + std::size_t n = 0; + for (const auto word : {0xf4080200u, 0xfa000200u, 0xf4080300u, 0xfa000210u, 0x7e0002f0u, 0x7e0202f0u, 0x7e100280u, 0x7e120280u, 0x7e140280u, 0x7e160280u}) code[n++] = word; + for (std::uint32_t texture = 0; texture < Textures; ++texture) { + for (const auto word : {0xf40c0400u, 0xfa000000u | (texture * 32u), 0xbf8cc07fu, 0xf09c0f08u, 0x00440400u, 0xbf8c3f70u, 0x06101104u, 0x06121305u, 0x06141506u, 0x06161707u}) code[n++] = word; + } + for (const auto word : {0xf4200606u, 0xfa000000u, 0xf4200642u, 0xfa000000u, 0xbf8cc07fu}) code[n++] = word; + for (const auto word : {0x10101018u, 0x10121218u, 0x10141418u, 0x10161618u, 0x10101019u, 0x10121219u, 0x10141419u, 0x10161619u}) code[n++] = word; + for (const auto word : {0xf800180fu, 0x0b0a0908u, 0xbf810000u}) code[n++] = word; + return code; +} + +alignas(256) constexpr std::array PixelCode = MakePixelCode(); + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "lazy resolved ABI: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Layout { + const GuestBlock& data; + const GuestBlock& textures; + const GuestBlock& target; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records, std::uint32_t word3) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, word3}; +} + +void Prepare(const Layout& layout) { + const auto& block = layout.data; + for (std::uint32_t texture = 0; texture < Textures; ++texture) { + const auto address = layout.textures.At(TextureOffset + texture * TextureBytes); + std::memset(layout.textures.Bytes(TextureOffset + texture * TextureBytes), static_cast(TexelByte), TextureBytes); + const std::array words{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 7u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + std::memcpy(block.Bytes(SrtOffset + texture * 32u), words.data(), sizeof(words)); + } + const std::array sampler{ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; + std::memcpy(block.Bytes(SamplerOffset), sampler.data(), sizeof(sampler)); + const auto frame = BufferDescriptor(block.At(FrameDataOffset), 0, 256, 0x31016facu); + std::memcpy(block.Bytes(FrameConstantsOffset), frame.data(), sizeof(frame)); + const float one = 1.0f; + std::memcpy(block.Bytes(FrameDataOffset), &one, sizeof(one)); + for (std::uint32_t buffer = 0; buffer < VertexBuffers; ++buffer) { + const auto vertices = BufferDescriptor(block.At(VertexDataOffset + buffer * 0x100u), 16, 3, 0x0104dfacu); + std::memcpy(block.Bytes(VertexTableOffset + buffer * 16u), vertices.data(), sizeof(vertices)); + } + for (std::uint32_t draw = 0; draw < MaxDraws; ++draw) std::memcpy(block.Bytes(ObjectOffset + draw * ObjectBytes), &one, sizeof(one)); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector Commands(const Layout& layout, std::uint32_t draws, std::uintptr_t pixel = reinterpret_cast(PixelCode.data()), std::uint32_t blend = 0) { + const auto& block = layout.data; + const auto color = layout.target.At(ColorOffset); + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto srt = block.At(SrtOffset); + const auto table = block.At(VertexTableOffset); + const float scale = 32.0f; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, blend}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(scale)}, {0x110, std::bit_cast(scale)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, + {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 8u << 1u}, + {0x00c, static_cast(srt)}, {0x00d, static_cast(srt >> 32u)}, {0x00e, 0}, {0x00f, 0}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + for (std::uint32_t draw = 0; draw < draws; ++draw) { + const auto object = BufferDescriptor(block.At(ObjectOffset + draw * ObjectBytes), 0, 16, 0x31016facu); + Append(commands, 0x76, {0x010, object[0], object[1], object[2], object[3]}); + Append(commands, 0x2d, {3, 2}); + } + return commands; +} + +double Submit(std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + const auto start = std::chrono::steady_clock::now(); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "lazy resolved ABI: the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +unsigned ColorByte(const GuestBlock& block) { + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(block.At(ColorOffset)), ColorBytes); + unsigned brightest = 0; + for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast(block.Bytes(ColorOffset)[i])); + return brightest; +} + +void Check(const Layout& layout) { + std::memset(layout.target.Bytes(ColorOffset), 0, ColorBytes); + auto commands = Commands(layout, 16); + Submit(commands); + const auto byte = ColorByte(layout.target); + Require(byte == Textures * TexelByte, "lazy resolved ABI: 16 draws sampling 16 textures of byte " + std::to_string(TexelByte) + " wrote " + std::to_string(byte) + ", expected " + std::to_string(Textures * TexelByte)); +} + + +std::vector SyntheticPixelCode(std::uint32_t index, std::uint32_t blocks) { + std::vector code; + code.reserve(PixelCode.size() + 2 + blocks * 8); + code.push_back(0x7e1802ffu); + code.push_back(0x3f800000u + index); + for (const auto word : PixelCode) { + if (word == 0xf800180fu) { + for (std::uint32_t block = 0; block < blocks; ++block) { + for (const auto multiply : {0x10101018u, 0x10121218u, 0x10141418u, 0x10161618u, 0x10101019u, 0x10121219u, 0x10141419u, 0x10161619u}) code.push_back(multiply); + } + } + code.push_back(word); + } + return code; +} + +struct SyntheticHeader { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; + std::array context{{{0x1b6, 0}}}; +}; + +struct Synthetic { + SyntheticHeader header; + std::uint32_t* code = nullptr; +}; + +class ProgramArena { +public: + explicit ProgramArena(std::size_t size) : bytes((size + 65535u) & ~std::size_t{65535}) { +#ifdef _WIN32 + base = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (base != nullptr) GuestArena::GuestArenaCommit_nid_postfix(base, bytes, PAGE_READWRITE, bytes); +#else + void* mapped = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + base = mapped != MAP_FAILED ? static_cast(mapped) : nullptr; +#endif + Require(base != nullptr, "lazy resolved ABI: cannot allocate the program arena"); + GuestAllocations::Mutation().Add(base, bytes, true, true, true); + } + ProgramArena(const ProgramArena&) = delete; + ProgramArena& operator=(const ProgramArena&) = delete; + std::uint32_t* Place(std::span code) { + const auto aligned = (used + 255u) & ~std::size_t{255}; + Require(aligned + code.size_bytes() <= bytes, "lazy resolved ABI: the program arena is full"); + auto* destination = reinterpret_cast(base + aligned); + std::memcpy(destination, code.data(), code.size_bytes()); + used = aligned + code.size_bytes(); + return destination; + } + +private: + std::size_t bytes; + std::size_t used = 0; + std::uint8_t* base = nullptr; +}; + +void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vector& code) { + program.code = arena.Place(code); + const auto address = reinterpret_cast(program.code); + auto& header = program.header; + header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 8u << 1u}, {0x00a, 0}}}; + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(SyntheticHeader); + header.shader.shader_size = static_cast(code.size() * sizeof(std::uint32_t)); + header.shader.code = program.code; + header.shader.type = 1; + header.shader.user_data = &header.userData; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = static_cast(header.registers.size()); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = static_cast(header.context.size()); + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +constexpr std::uint32_t Programs = 4; +constexpr std::uint32_t Drawn = 2; + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +void Settle(std::span programs) { + std::vector registered; + for (const auto& program : programs) registered.push_back(&program.header.shader); + AgcDriverEndShaderPreparation_nid_postfix(AgcDriverBeginShaderPreparation_nid_postfix(registered.data(), registered.size())); + AgcDriverWaitIdle_nid_postfix(); +} + +std::vector Draw(const Layout& layout, const Synthetic& program) { + std::memset(layout.target.Bytes(ColorOffset), 0, ColorBytes); + auto commands = Commands(layout, 1, reinterpret_cast(program.code)); + Submit(commands); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(layout.target.At(ColorOffset)), ColorBytes); + return {layout.target.Bytes(ColorOffset), layout.target.Bytes(ColorOffset) + ColorBytes}; +} + +std::size_t Entries(const Synthetic& program) { + return AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&program.header.shader); +} + +void EagerReference(const Layout& layout, const std::filesystem::path& output) { + ProgramArena arena(1u << 20u); + std::vector programs(1); + RegisterSynthetic(programs.front(), arena, SyntheticPixelCode(1, 0)); + AgcDriverResolveShaderAbi_nid_postfix(&programs.front().header.shader, {}, {}); + Settle(programs); + Require(Entries(programs.front()) != 0, "lazy resolved ABI: eager preparation left the resolved shader unprepared"); + const auto bytes = Draw(layout, programs.front()); + std::ofstream file(output, std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(file.good(), "lazy resolved ABI: cannot write the eager reference"); +} + +void LazyResolution(const Layout& layout, const std::filesystem::path& reference) { + std::vector expected(ColorBytes); + { + std::ifstream file(reference, std::ios::binary); + file.read(reinterpret_cast(expected.data()), static_cast(expected.size())); + Require(file.gcount() == static_cast(expected.size()), "lazy resolved ABI: the eager reference is missing or short"); + } + ProgramArena arena(1u << 20u); + std::vector programs(Programs); + for (std::uint32_t index = 0; index < Programs; ++index) { + RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); + AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); + } + Settle(programs); + for (std::uint32_t index = 0; index < Programs; ++index) { + const auto entries = Entries(programs[index]); + Require(entries == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(entries) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + } + std::vector first; + for (std::uint32_t index = 0; index < Drawn; ++index) { + auto bytes = Draw(layout, programs[index]); + if (index == 0) first = std::move(bytes); + Require(Entries(programs[index]) != 0, "lazy resolved ABI: drawn shader " + std::to_string(index) + " holds no prepared entries"); + } + for (std::uint32_t index = Drawn; index < Programs; ++index) { + const auto entries = Entries(programs[index]); + Require(entries == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(entries) + " prepared entries"); + } + Require(std::ranges::any_of(first, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: the drawn shader wrote nothing"); + Require(first == expected, "lazy resolved ABI: the shader prepared at its first draw drew other bytes than with eager preparation"); + std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries, and a drawn one drew the same %zu bytes as with eager preparation\n", Drawn, Programs, Programs - Drawn, first.size()); +} + +} + +int main(int argc, char** argv) { + const bool eager = argc == 2 && std::string(argv[1]) == "--eager-reference"; + std::filesystem::path reference; + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (eager) { + const char* path = std::getenv("LAZY_RESOLVED_ABI_REFERENCE"); + Require(path != nullptr, "lazy resolved ABI: the eager child has no reference path"); + reference = path; + } else { + reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + std::to_string(std::random_device{}()) + ".bin"); + SetEnvironment("LAZY_RESOLVED_ABI_REFERENCE", reference.string()); + const std::string command = "\"" + std::string(argv[0]) + "\" --eager-reference"; + Require(std::system(command.c_str()) == 0, "lazy resolved ABI: the eager reference process failed"); + SetEnvironment("APS5_LAZY_SHADER_PREPARE", "1"); + } + Register(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + GuestBlock data; + GuestBlock textures; + GuestBlock target; + const Layout layout{data, textures, target}; + Prepare(layout); + Check(layout); + if (eager) EagerReference(layout, reference); + else LazyResolution(layout, reference); + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (...) { + } + if (!eager && !reference.empty()) { + std::error_code ignored; + std::filesystem::remove(reference, ignored); + } + return 1; + } + if (!eager) { + std::error_code ignored; + std::filesystem::remove(reference, ignored); + } + return 0; +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 574244a6da..ced76416f2 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -120,7 +120,7 @@ flowchart LR - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. -- `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. Explicit ABI resolution still prepares its requested stages. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. +- `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. A compute or pixel stage's explicit ABI resolution (`ResolveShaderAbi`) plans the stage with the ABI's state, records the ABI and leaves its preparation to the shader's first use, which prepares it with that state (`PreparedShaders::deferredAbis`), so a shader that never draws or dispatches stays unprepared; a graphics-stage ABI is still prepared when it is resolved. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. - On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. From 6e0066b5dbe12bd2134a22749622201b9bfb9691 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:36:22 +0000 Subject: [PATCH 0235/1360] test(agc): run the lazy resolved ABI test's eager reference without the switch agc_driver_lazy_resolved_abi spawns itself with --eager-reference to draw the reference bytes with eager preparation, and only sets APS5_LAZY_SHADER_PREPARE for itself after the child returns. Run with the switch already exported, as a CI run of the whole suite with lazy preparation does, the child inherited it, prepared nothing at its ABI resolution and failed with "eager preparation left the resolved shader unprepared". The test now removes the variable before it spawns the child. agc_driver_lazy_resolved_abi_switch_set runs it with the switch exported. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++++ .../libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 21cbf9e3e8..10d7e03a0e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2175,6 +2175,11 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_lazy_resolved_abi_switch_set COMMAND agc_driver_lazy_resolved_abi_tests) + set_tests_properties(agc_driver_lazy_resolved_abi_switch_set PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_LAZY_SHADER_PREPARE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_lazy_resolved_abi_switch_set PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index c48494d6ea..1c1f0b21d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -338,6 +338,14 @@ void SetEnvironment(const char* name, const std::string& value) { #endif } +void ClearEnvironment(const char* name) { +#ifdef _WIN32 + _putenv_s(name, ""); +#else + unsetenv(name); +#endif +} + void Settle(std::span programs) { std::vector registered; for (const auto& program : programs) registered.push_back(&program.header.shader); @@ -420,6 +428,7 @@ int main(int argc, char** argv) { } else { reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + std::to_string(std::random_device{}()) + ".bin"); SetEnvironment("LAZY_RESOLVED_ABI_REFERENCE", reference.string()); + ClearEnvironment("APS5_LAZY_SHADER_PREPARE"); const std::string command = "\"" + std::string(argv[0]) + "\" --eager-reference"; Require(std::system(command.c_str()) == 0, "lazy resolved ABI: the eager reference process failed"); SetEnvironment("APS5_LAZY_SHADER_PREPARE", "1"); From 9f6bb2392cf0b4914b69f7afe899673a36a525df Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:43:12 +0000 Subject: [PATCH 0236/1360] test(agc): compare a lazily resolved ABI's entries, libraries and cache files with eager preparation agc_driver_lazy_resolved_abi compared only the bytes one lazily resolved shader drew with eager preparation's. Both processes now run the same scenario, and the lazy one compares what it ends with against the eager one's report: - five synthetic pixel shaders, four resolved with empty ABI registers and one with its draw's own context and primitive registers, three of them drawn; - each drawn shader's prepared entries, fingerprinted by the SPIR-V content hash and the source key, equal what eager resolution and the draw made: a draw with the ABI's state finds the ABI's entries, a draw with another state adds its own. Entries eager registration alone made for a state no ABI names are left out, as lazy registration makes none; - the shaders that never drew hold no entries, and nothing is prepared or prebuilt at resolution; - the pipeline libraries built, after the prebuild pool has drained, equal eager preparation's, so the first use prebuilds the registration module's library as resolution did; - both runs share one shader disk cache, and the lazy run writes no file the eager run did not. Driver::PreparedEntryHashes and Driver::PrebuiltLibraries expose the fingerprints and the library counts to the test. Each check fails when its part of the first-use preparation is removed: without the prebuild device the libraries differ (eager 1 2 2 1, lazy 1 1 1 1), and without the ABI's context registers the shader drawn with them holds other entries. --- .../Execution/include/Driver/Driver.hpp | 2 + .../src/Driver/Shaders/ShaderRegistry.cpp | 30 +++ .../tests/execution/LazyResolvedAbi.cpp | 210 ++++++++++++++---- 3 files changed, 200 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 2d647d4f62..ca5f882eb9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -68,6 +68,8 @@ public: void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void SettleShaderPreparations(std::span stages); [[nodiscard]] std::size_t PreparedEntries(const Shader* shader); + [[nodiscard]] std::vector> PreparedEntryHashes(const Shader* shader); + [[nodiscard]] std::array PrebuiltLibraries(); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); 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 c56915a0dd..6cdad1b526 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "CacheKey.hpp" @@ -1637,6 +1638,35 @@ std::size_t Driver::PreparedEntries(const Shader* shader) { return snapshot->prepared->entries.size(); } +std::vector> Driver::PreparedEntryHashes(const Shader* shader) { + std::shared_ptr snapshot; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return {}; + snapshot = RegisteredHeader(*shaders, shader); + } + if (snapshot == nullptr) return {}; + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + std::vector> hashes; + for (const auto& entry : snapshot->prepared->entries) { + if (entry.handle == nullptr || entry.handle->artifact == nullptr) continue; + const auto hash = entry.handle->artifact->artifact.spirv.ContentHash(); + std::uint64_t key = 0xcbf29ce484222325ull; + for (const auto word : entry.handle->staticKey) key = (key ^ word) * 0x100000001b3ull; + hashes.push_back({hash.low, hash.high ^ key}); + } + std::ranges::sort(hashes); + return hashes; +} + +std::array Driver::PrebuiltLibraries() { + if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); + const auto localDevice = device.Load(); + if (localDevice == nullptr) return {}; + return Graphics::PipelineLibraryCountersOf(localDevice->Device()).built; +} + void Driver::ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive) { BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index 1c1f0b21d8..636deb9230 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -29,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -179,14 +183,12 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector Commands(const Layout& layout, std::uint32_t draws, std::uintptr_t pixel = reinterpret_cast(PixelCode.data()), std::uint32_t blend = 0) { - const auto& block = layout.data; +constexpr std::pair PrimitiveType{0x242, 4}; + +std::vector> DrawContext(const Layout& layout, std::uint32_t blend = 0) { const auto color = layout.target.At(ColorOffset); - const auto vertex = reinterpret_cast(VertexCode.data()); - const auto srt = block.At(SrtOffset); - const auto table = block.At(VertexTableOffset); const float scale = 32.0f; - const std::vector> context{ + return { {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, @@ -208,6 +210,14 @@ std::vector Commands(const Layout& layout, std::uint32_t draws, s {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, }; +} + +std::vector Commands(const Layout& layout, std::uint32_t draws, std::uintptr_t pixel = reinterpret_cast(PixelCode.data()), std::uint32_t blend = 0) { + const auto& block = layout.data; + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto srt = block.At(SrtOffset); + const auto table = block.At(VertexTableOffset); + const auto context = DrawContext(layout, blend); const std::vector> shader{ {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, @@ -215,7 +225,7 @@ std::vector Commands(const Layout& layout, std::uint32_t draws, s {0x00c, static_cast(srt)}, {0x00d, static_cast(srt >> 32u)}, {0x00e, 0}, {0x00f, 0}, }; std::vector commands; - Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {PrimitiveType.first, PrimitiveType.second}); for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); for (std::uint32_t draw = 0; draw < draws; ++draw) { @@ -365,50 +375,153 @@ std::size_t Entries(const Synthetic& program) { return AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&program.header.shader); } -void EagerReference(const Layout& layout, const std::filesystem::path& output) { +constexpr std::uint32_t Matching = Programs; +constexpr std::array DrawnPrograms{0, 1, Matching}; + +using Report = std::map; + +std::string Text(std::span> hashes) { + std::string text = std::to_string(hashes.size()); + char word[40]; + for (const auto& hash : hashes) { + std::snprintf(word, sizeof(word), " %016llx%016llx", static_cast(hash[1]), static_cast(hash[0])); + text += word; + } + return text; +} + +std::string Text(const std::array& libraries) { + std::string text; + for (const auto count : libraries) text += (text.empty() ? "" : " ") + std::to_string(count); + return text; +} + +std::size_t Count(const std::string& text) { + return static_cast(std::stoull(text.substr(0, text.find(' ')))); +} + +std::vector Prints(const std::string& text) { + std::vector prints; + std::istringstream stream(text); + std::string word; + stream >> word; + while (stream >> word) prints.push_back(word); + std::ranges::sort(prints); + return prints; +} + +std::string Joined(std::span prints) { + std::string text = std::to_string(prints.size()); + for (const auto& print : prints) text += " " + print; + return text; +} + +std::vector EagerResolutionEntries(const Report& eager, std::uint32_t index) { + const auto id = std::to_string(index); + const auto registered = Prints(eager.at("registered " + id)); + const auto drawn = Prints(eager.at("drawn " + id)); + if (Count(eager.at("resolved " + id)) == registered.size()) return drawn; + std::vector made; + std::ranges::set_difference(drawn, registered, std::back_inserter(made)); + return made; +} + +Report Scenario(const Layout& layout, std::vector& first) { + auto& driver = AgcDriver::DriverDetail::Driver::Get(); + Report report; + report["libraries before"] = Text(driver.PrebuiltLibraries()); ProgramArena arena(1u << 20u); - std::vector programs(1); - RegisterSynthetic(programs.front(), arena, SyntheticPixelCode(1, 0)); - AgcDriverResolveShaderAbi_nid_postfix(&programs.front().header.shader, {}, {}); + std::vector programs(Matching + 1); + for (std::uint32_t index = 0; index <= Matching; ++index) RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); Settle(programs); - Require(Entries(programs.front()) != 0, "lazy resolved ABI: eager preparation left the resolved shader unprepared"); - const auto bytes = Draw(layout, programs.front()); + for (std::uint32_t index = 0; index <= Matching; ++index) report["registered " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + for (std::uint32_t index = 0; index < Programs; ++index) AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); + std::vector context; + for (const auto& [offset, value] : DrawContext(layout)) context.push_back({offset, value}); + const std::array primitive{{{PrimitiveType.first, PrimitiveType.second}}}; + AgcDriverResolveShaderAbi_nid_postfix(&programs[Matching].header.shader, context, primitive); + Settle(programs); + for (std::uint32_t index = 0; index <= Matching; ++index) report["resolved " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + report["libraries resolved"] = Text(driver.PrebuiltLibraries()); + for (const auto index : DrawnPrograms) { + auto bytes = Draw(layout, programs[index]); + Require(std::ranges::any_of(bytes, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: drawn shader " + std::to_string(index) + " wrote nothing"); + if (index == 0) first = std::move(bytes); + } + for (std::uint32_t index = 0; index <= Matching; ++index) report["drawn " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + report["libraries drawn"] = Text(driver.PrebuiltLibraries()); + return report; +} + +std::filesystem::path ReportPath(const std::filesystem::path& reference) { + auto path = reference; + path += ".report"; + return path; +} + +std::set CacheFiles(const std::filesystem::path& cache) { + std::set files; + for (const auto& entry : std::filesystem::recursive_directory_iterator(cache)) { + if (entry.is_regular_file() && entry.path().filename().string().rfind("vk-pipelines-", 0) != 0) files.insert(std::filesystem::relative(entry.path(), cache).generic_string()); + } + return files; +} + +void EagerReference(const Layout& layout, const std::filesystem::path& output) { + std::vector bytes; + const auto report = Scenario(layout, bytes); + for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(report.at("resolved " + std::to_string(index))) != 0, "lazy resolved ABI: eager preparation left resolved shader " + std::to_string(index) + " unprepared"); std::ofstream file(output, std::ios::binary | std::ios::trunc); file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); Require(file.good(), "lazy resolved ABI: cannot write the eager reference"); + std::ofstream text(ReportPath(output), std::ios::trunc); + for (const auto& [key, value] : report) text << key << '=' << value << '\n'; + Require(text.good(), "lazy resolved ABI: cannot write the eager report"); } -void LazyResolution(const Layout& layout, const std::filesystem::path& reference) { +void LazyResolution(const Layout& layout, const std::filesystem::path& reference, const std::set& cached, const std::filesystem::path& cache) { std::vector expected(ColorBytes); { std::ifstream file(reference, std::ios::binary); file.read(reinterpret_cast(expected.data()), static_cast(expected.size())); Require(file.gcount() == static_cast(expected.size()), "lazy resolved ABI: the eager reference is missing or short"); } - ProgramArena arena(1u << 20u); - std::vector programs(Programs); - for (std::uint32_t index = 0; index < Programs; ++index) { - RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); - AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); - } - Settle(programs); - for (std::uint32_t index = 0; index < Programs; ++index) { - const auto entries = Entries(programs[index]); - Require(entries == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(entries) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + Report eager; + { + std::ifstream text(ReportPath(reference)); + for (std::string line; std::getline(text, line);) { + const auto split = line.find('='); + if (split != std::string::npos) eager[line.substr(0, split)] = line.substr(split + 1); + } + Require(eager.contains("libraries drawn"), "lazy resolved ABI: the eager report is missing or short"); } std::vector first; - for (std::uint32_t index = 0; index < Drawn; ++index) { - auto bytes = Draw(layout, programs[index]); - if (index == 0) first = std::move(bytes); - Require(Entries(programs[index]) != 0, "lazy resolved ABI: drawn shader " + std::to_string(index) + " holds no prepared entries"); + const auto lazy = Scenario(layout, first); + for (std::uint32_t index = 0; index <= Matching; ++index) { + const auto key = "resolved " + std::to_string(index); + Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(Count(lazy.at(key))) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + } + Require(lazy.at("libraries resolved") == lazy.at("libraries before"), "lazy resolved ABI: ResolveShaderAbi built pipeline libraries under APS5_LAZY_SHADER_PREPARE (" + lazy.at("libraries before") + " before, " + lazy.at("libraries resolved") + " after)"); + for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(lazy.at("registered " + std::to_string(index))) == 0, "lazy resolved ABI: registration prepared shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + for (const auto index : DrawnPrograms) { + const auto expected = EagerResolutionEntries(eager, index); + const auto held = Prints(lazy.at("drawn " + std::to_string(index))); + Require(held == expected, "lazy resolved ABI: drawn shader " + std::to_string(index) + " holds other prepared entries than eager resolution and its draw made (eager " + Joined(expected) + "; lazy " + Joined(held) + ")"); } for (std::uint32_t index = Drawn; index < Programs; ++index) { - const auto entries = Entries(programs[index]); - Require(entries == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(entries) + " prepared entries"); + const auto key = "drawn " + std::to_string(index); + Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(Count(lazy.at(key))) + " prepared entries"); } - Require(std::ranges::any_of(first, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: the drawn shader wrote nothing"); + Require(eager.at("drawn " + std::to_string(Matching)) == eager.at("resolved " + std::to_string(Matching)), "lazy resolved ABI: the draw whose state is its resolved ABI's did not find the ABI's entries with eager preparation"); + for (std::uint32_t index = 0; index < Drawn; ++index) Require(Count(eager.at("drawn " + std::to_string(index))) == Count(eager.at("resolved " + std::to_string(index))) + 1, "lazy resolved ABI: a draw whose state differs from its resolved ABI did not prepare its own entry with eager preparation"); + Require(lazy.at("libraries drawn") == eager.at("libraries drawn"), "lazy resolved ABI: the pipeline libraries built differ from eager preparation (eager " + eager.at("libraries drawn") + "; lazy " + lazy.at("libraries drawn") + ")"); Require(first == expected, "lazy resolved ABI: the shader prepared at its first draw drew other bytes than with eager preparation"); - std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries, and a drawn one drew the same %zu bytes as with eager preparation\n", Drawn, Programs, Programs - Drawn, first.size()); + AgcDriverWaitIdle_nid_postfix(); + const auto files = CacheFiles(cache); + std::vector added; + std::ranges::set_difference(files, cached, std::back_inserter(added)); + Require(added.empty(), "lazy resolved ABI: preparation at first use wrote " + std::to_string(added.size()) + " shader cache files eager preparation had not, the first " + (added.empty() ? std::string() : added.front())); + std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries; the drawn ones hold eager resolution's entries (the one drawn with its ABI's state found them, the others prepared their own), the pipeline libraries built match eager's (%s), a drawn one drew the same %zu bytes, and the lazy run wrote none of the %zu shader cache files eager wrote again\n", static_cast(DrawnPrograms.size()), Matching + 1, Programs - Drawn, lazy.at("libraries drawn").c_str(), first.size(), cached.size()); } } @@ -416,6 +529,17 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference int main(int argc, char** argv) { const bool eager = argc == 2 && std::string(argv[1]) == "--eager-reference"; std::filesystem::path reference; + std::filesystem::path cache; + std::set cached; + const auto clean = [&] { + if (eager) return; + std::error_code ignored; + if (!reference.empty()) { + std::filesystem::remove(reference, ignored); + std::filesystem::remove(ReportPath(reference), ignored); + } + if (!cache.empty()) std::filesystem::remove_all(cache, ignored); + }; try { { const auto device = OpenVulkanTestDevice(); @@ -426,11 +550,19 @@ int main(int argc, char** argv) { Require(path != nullptr, "lazy resolved ABI: the eager child has no reference path"); reference = path; } else { - reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + std::to_string(std::random_device{}()) + ".bin"); + const auto run = std::to_string(std::random_device{}()); + reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + run + ".bin"); + cache = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-cache-" + run); + std::filesystem::create_directories(cache); SetEnvironment("LAZY_RESOLVED_ABI_REFERENCE", reference.string()); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", cache.string()); + SetEnvironment("APS5_NO_PIPELINE_JOURNAL", "1"); ClearEnvironment("APS5_LAZY_SHADER_PREPARE"); const std::string command = "\"" + std::string(argv[0]) + "\" --eager-reference"; Require(std::system(command.c_str()) == 0, "lazy resolved ABI: the eager reference process failed"); + cached = CacheFiles(cache); + Require(!cached.empty(), "lazy resolved ABI: the eager reference process wrote no shader cache files"); SetEnvironment("APS5_LAZY_SHADER_PREPARE", "1"); } Register(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); @@ -442,7 +574,7 @@ int main(int argc, char** argv) { Prepare(layout); Check(layout); if (eager) EagerReference(layout, reference); - else LazyResolution(layout, reference); + else LazyResolution(layout, reference, cached, cache); AgcDriverShutdown_nid_postfix(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; @@ -450,15 +582,9 @@ int main(int argc, char** argv) { AgcDriverShutdown_nid_postfix(); } catch (...) { } - if (!eager && !reference.empty()) { - std::error_code ignored; - std::filesystem::remove(reference, ignored); - } + clean(); return 1; } - if (!eager) { - std::error_code ignored; - std::filesystem::remove(reference, ignored); - } + clean(); return 0; } From 31cfb593e32fe0f5f30fcc5c988c76efdd9556b5 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:45:29 +0000 Subject: [PATCH 0237/1360] test(agc): compare stale-state cases of a deferred ABI with eager preparation agc_driver_lazy_resolved_abi's eager/lazy comparison now also covers the ways the state a deferred ABI keeps could differ at its first use: - the ABI's context and primitive register arrays are overwritten after resolution and before any draw; - one shader is resolved with two ABIs, the registered state and its draw's, before any use; - a second header is registered for a shader whose ABI is queued, a draw uses it, then the first header is registered again and drawn. Each drawn shader still ends with the entries eager resolution and its draws made: drawn with the ABI's state it finds them, the second header prepares its own, and the re-registered header finds its queued ABI's. --- .../tests/execution/LazyResolvedAbi.cpp | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index 636deb9230..4a2a829146 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -318,15 +318,15 @@ private: std::uint8_t* base = nullptr; }; -void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vector& code) { - program.code = arena.Place(code); +void RegisterHeader(Synthetic& program, std::uint32_t* code, std::size_t words) { + program.code = code; const auto address = reinterpret_cast(program.code); auto& header = program.header; header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 8u << 1u}, {0x00a, 0}}}; header.shader.file_header = 0x34333231u; header.shader.version = 0x18u; header.shader.header_size = sizeof(SyntheticHeader); - header.shader.shader_size = static_cast(code.size() * sizeof(std::uint32_t)); + header.shader.shader_size = static_cast(words * sizeof(std::uint32_t)); header.shader.code = program.code; header.shader.type = 1; header.shader.user_data = &header.userData; @@ -337,6 +337,10 @@ void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vecto AgcDriverRegisterShader_nid_postfix(&header.shader); } +void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vector& code) { + RegisterHeader(program, arena.Place(code), code.size()); +} + constexpr std::uint32_t Programs = 4; constexpr std::uint32_t Drawn = 2; @@ -376,7 +380,14 @@ std::size_t Entries(const Synthetic& program) { } constexpr std::uint32_t Matching = Programs; -constexpr std::array DrawnPrograms{0, 1, Matching}; +constexpr std::uint32_t Twice = Programs + 1; +constexpr std::uint32_t Replaced = Programs + 2; +constexpr std::uint32_t Replacement = Programs + 3; +constexpr std::array DrawnPrograms{0, 1, Matching, Twice, Replaced, Replacement}; + +bool RegistrationAbi(std::uint32_t index) { + return index < Programs || index == Twice; +} using Report = std::map; @@ -420,7 +431,7 @@ std::vector EagerResolutionEntries(const Report& eager, std::uint32 const auto id = std::to_string(index); const auto registered = Prints(eager.at("registered " + id)); const auto drawn = Prints(eager.at("drawn " + id)); - if (Count(eager.at("resolved " + id)) == registered.size()) return drawn; + if (RegistrationAbi(index)) return drawn; std::vector made; std::ranges::set_difference(drawn, registered, std::back_inserter(made)); return made; @@ -431,24 +442,40 @@ Report Scenario(const Layout& layout, std::vector& first) { Report report; report["libraries before"] = Text(driver.PrebuiltLibraries()); ProgramArena arena(1u << 20u); - std::vector programs(Matching + 1); - for (std::uint32_t index = 0; index <= Matching; ++index) RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); - Settle(programs); - for (std::uint32_t index = 0; index <= Matching; ++index) report["registered " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); - for (std::uint32_t index = 0; index < Programs; ++index) AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); + std::vector programs(Replacement + 1); + const auto record = [&](const char* phase, std::uint32_t index) { + report[std::string(phase) + " " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + }; + const auto resolved = std::span(programs).first(Replaced + 1); + for (std::uint32_t index = 0; index <= Replaced; ++index) RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); + Settle(resolved); + for (std::uint32_t index = 0; index <= Replaced; ++index) record("registered", index); std::vector context; for (const auto& [offset, value] : DrawContext(layout)) context.push_back({offset, value}); - const std::array primitive{{{PrimitiveType.first, PrimitiveType.second}}}; + std::array primitive{{{PrimitiveType.first, PrimitiveType.second}}}; + for (std::uint32_t index = 0; index < Programs; ++index) AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); AgcDriverResolveShaderAbi_nid_postfix(&programs[Matching].header.shader, context, primitive); - Settle(programs); - for (std::uint32_t index = 0; index <= Matching; ++index) report["resolved " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + AgcDriverResolveShaderAbi_nid_postfix(&programs[Twice].header.shader, {}, {}); + AgcDriverResolveShaderAbi_nid_postfix(&programs[Twice].header.shader, context, primitive); + AgcDriverResolveShaderAbi_nid_postfix(&programs[Replaced].header.shader, context, primitive); + for (auto& reg : context) reg.value = ~reg.value; + primitive.front().value = ~primitive.front().value; + Settle(resolved); + for (std::uint32_t index = 0; index <= Replaced; ++index) record("resolved", index); report["libraries resolved"] = Text(driver.PrebuiltLibraries()); - for (const auto index : DrawnPrograms) { - auto bytes = Draw(layout, programs[index]); + RegisterHeader(programs[Replacement], programs[Replaced].code, programs[Replaced].header.shader.shader_size / sizeof(std::uint32_t)); + Settle(std::span(programs).subspan(Replacement, 1)); + record("registered", Replacement); + const auto draw = [&](std::uint32_t index, const Synthetic& program) { + auto bytes = Draw(layout, program); Require(std::ranges::any_of(bytes, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: drawn shader " + std::to_string(index) + " wrote nothing"); if (index == 0) first = std::move(bytes); - } - for (std::uint32_t index = 0; index <= Matching; ++index) report["drawn " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + }; + for (const auto index : {0u, 1u, Matching, Twice}) draw(index, programs[index]); + draw(Replacement, programs[Replacement]); + AgcDriverRegisterShader_nid_postfix(&programs[Replaced].header.shader); + draw(Replaced, programs[Replaced]); + for (std::uint32_t index = 0; index <= Replacement; ++index) record("drawn", index); report["libraries drawn"] = Text(driver.PrebuiltLibraries()); return report; } @@ -470,7 +497,7 @@ std::set CacheFiles(const std::filesystem::path& cache) { void EagerReference(const Layout& layout, const std::filesystem::path& output) { std::vector bytes; const auto report = Scenario(layout, bytes); - for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(report.at("resolved " + std::to_string(index))) != 0, "lazy resolved ABI: eager preparation left resolved shader " + std::to_string(index) + " unprepared"); + for (std::uint32_t index = 0; index <= Replaced; ++index) Require(Count(report.at("resolved " + std::to_string(index))) != 0, "lazy resolved ABI: eager preparation left resolved shader " + std::to_string(index) + " unprepared"); std::ofstream file(output, std::ios::binary | std::ios::trunc); file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); Require(file.good(), "lazy resolved ABI: cannot write the eager reference"); @@ -497,12 +524,12 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference } std::vector first; const auto lazy = Scenario(layout, first); - for (std::uint32_t index = 0; index <= Matching; ++index) { + for (std::uint32_t index = 0; index <= Replaced; ++index) { const auto key = "resolved " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(Count(lazy.at(key))) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); } Require(lazy.at("libraries resolved") == lazy.at("libraries before"), "lazy resolved ABI: ResolveShaderAbi built pipeline libraries under APS5_LAZY_SHADER_PREPARE (" + lazy.at("libraries before") + " before, " + lazy.at("libraries resolved") + " after)"); - for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(lazy.at("registered " + std::to_string(index))) == 0, "lazy resolved ABI: registration prepared shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + for (std::uint32_t index = 0; index <= Replacement; ++index) Require(Count(lazy.at("registered " + std::to_string(index))) == 0, "lazy resolved ABI: registration prepared shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); for (const auto index : DrawnPrograms) { const auto expected = EagerResolutionEntries(eager, index); const auto held = Prints(lazy.at("drawn " + std::to_string(index))); @@ -512,8 +539,9 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference const auto key = "drawn " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(Count(lazy.at(key))) + " prepared entries"); } - Require(eager.at("drawn " + std::to_string(Matching)) == eager.at("resolved " + std::to_string(Matching)), "lazy resolved ABI: the draw whose state is its resolved ABI's did not find the ABI's entries with eager preparation"); + for (const auto index : {Matching, Twice, Replaced}) Require(eager.at("drawn " + std::to_string(index)) == eager.at("resolved " + std::to_string(index)), "lazy resolved ABI: shader " + std::to_string(index) + ", drawn with its resolved ABI's state, did not find the ABI's entries with eager preparation"); for (std::uint32_t index = 0; index < Drawn; ++index) Require(Count(eager.at("drawn " + std::to_string(index))) == Count(eager.at("resolved " + std::to_string(index))) + 1, "lazy resolved ABI: a draw whose state differs from its resolved ABI did not prepare its own entry with eager preparation"); + Require(Count(eager.at("drawn " + std::to_string(Replacement))) == Count(eager.at("registered " + std::to_string(Replacement))) + 1, "lazy resolved ABI: the header registered over a resolved one did not prepare its own entry at its draw with eager preparation"); Require(lazy.at("libraries drawn") == eager.at("libraries drawn"), "lazy resolved ABI: the pipeline libraries built differ from eager preparation (eager " + eager.at("libraries drawn") + "; lazy " + lazy.at("libraries drawn") + ")"); Require(first == expected, "lazy resolved ABI: the shader prepared at its first draw drew other bytes than with eager preparation"); AgcDriverWaitIdle_nid_postfix(); @@ -521,7 +549,7 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference std::vector added; std::ranges::set_difference(files, cached, std::back_inserter(added)); Require(added.empty(), "lazy resolved ABI: preparation at first use wrote " + std::to_string(added.size()) + " shader cache files eager preparation had not, the first " + (added.empty() ? std::string() : added.front())); - std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries; the drawn ones hold eager resolution's entries (the one drawn with its ABI's state found them, the others prepared their own), the pipeline libraries built match eager's (%s), a drawn one drew the same %zu bytes, and the lazy run wrote none of the %zu shader cache files eager wrote again\n", static_cast(DrawnPrograms.size()), Matching + 1, Programs - Drawn, lazy.at("libraries drawn").c_str(), first.size(), cached.size()); + std::printf("lazy resolved ABI: %u shaders drew, %u of them ABI-resolved; the %u resolved ones that never drew hold no prepared entries; the drawn ones hold the entries eager resolution and their draws made: drawn with their ABI's state they found them, with the ABI register arrays overwritten after resolution (one resolved with two ABIs, one whose header was registered over and again before its draw), drawn with another state they prepared their own; the pipeline libraries built match eager's (%s), a drawn one drew the same %zu bytes, and the lazy run wrote none of the %zu shader cache files eager wrote again\n", static_cast(DrawnPrograms.size()), static_cast(DrawnPrograms.size() - 1), Programs - Drawn, lazy.at("libraries drawn").c_str(), first.size(), cached.size()); } } From c01832242d8997ac451ce5dcdef3d404e60fe75a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:46:24 +0000 Subject: [PATCH 0238/1360] test(agc): check a deferred compute ABI under lazy preparation agc_driver_prepared_shaders_abi_deferred_compute resolves the ABI of a compute shader whose s_swappc_b64 calls a user-data pointer and requires the resolution to attempt its front end once. Under APS5_LAZY_SHADER_PREPARE=1 the resolution only plans the stage and leaves the attempt to the first dispatch, so with the switch exported the test failed on that expectation before reaching the dispatch. With the switch set the test now requires that neither resolution builds a front end, and still requires the dispatch to run both callees. agc_driver_prepared_shaders_abi_deferred_compute_lazy runs it so: the first dispatch logs the queued ABI's failure once, links the callees and writes 3 * (5 + 64) in every lane. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++++ .../prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 10d7e03a0e..630a22b411 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2660,6 +2660,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_prepared_shaders_abi_deferred_compute_lazy COMMAND agc_driver_prepared_shaders_tests --abi-deferred-compute) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_compute_lazy PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FAILURE_MEMO=1;APS5_LAZY_SHADER_PREPARE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_compute_lazy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 734a7c6825..6203b1706c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -958,11 +958,13 @@ void AbiDeferredCompute() { caller.shader.specials = &caller.specials; caller.specials.dispatch_modifier = 0x8000u; AgcDriverRegisterShader_nid_postfix(&caller.shader); + const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; const auto before = ShaderRecompiler::FrontEndBuilds(); AgcDriverResolveShaderAbi_nid_postfix(&caller.shader, {}, {}); const auto attempted = ShaderRecompiler::FrontEndBuilds(); AgcDriverResolveShaderAbi_nid_postfix(&caller.shader, {}, {}); - Require(attempted > before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI deferred to its dispatch was prepared again for the same state"); + if (lazy) Require(attempted == before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI resolved under lazy preparation built its front end before its dispatch"); + else Require(attempted > before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI deferred to its dispatch was prepared again for the same state"); const auto first = reinterpret_cast(AddFiveCallee.data()); const auto second = reinterpret_cast(AddSixtyFourCallee.data()); const auto output = reinterpret_cast(CallerOutput.data()); From dae468d260f1ed35d810ac3306a76639972ebca1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:27:51 +0000 Subject: [PATCH 0239/1360] feat(agc): count whether a deferred ABI's first use finds its artifact Under APS5_LAZY_SHADER_PREPARE=1 a shader's first draw or dispatch prepares the ABIs ResolveShaderAbi queued on it, then looks for its own artifact. Whether that artifact is among the entries the queued ABIs just prepared decides whether the queued preparation is the use's own work or extra. Each first use that prepared queued ABIs now counts as found when the artifact it uses is among the entries that preparation added, or as own when it prepares its artifact itself. Entries prepared from queued ABIs are marked, and the first lookup that matches one counts it, so the entries no use ever matched are counted too. The counters are relaxed atomics touched only by a lookup that matched or prepared. On the first change and then at most once a minute stderr shows [shaders] deferred ABIs: N prepared at first use, F found their artifact there, O prepared their own; U of the E entries prepared from them never matched a use Driver::DeferredAbiUses returns the same counts. Test: agc_driver_lazy_resolved_abi checks that its two draws whose state differs from the resolved ABI count as own with every queued entry unmatched, and resolves a fifth shader's ABI with its draw's context and primitive registers, whose draw counts as found and matches one entry. --- .../Execution/include/Driver/Driver.hpp | 8 ++ .../include/Driver/Shaders/ShaderRegistry.hpp | 2 + .../src/Driver/Shaders/ShaderRegistry.cpp | 92 +++++++++++++++++-- .../tests/execution/LazyResolvedAbi.cpp | 24 ++++- 4 files changed, 114 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ca5f882eb9..623130c420 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -68,6 +68,14 @@ public: void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void SettleShaderPreparations(std::span stages); [[nodiscard]] std::size_t PreparedEntries(const Shader* shader); + struct DeferredAbiUseCounts { + std::uint64_t firstUses; + std::uint64_t found; + std::uint64_t own; + std::uint64_t entries; + std::uint64_t unmatchedEntries; + }; + [[nodiscard]] static DeferredAbiUseCounts DeferredAbiUses(); [[nodiscard]] std::vector> PreparedEntryHashes(const Shader* shader); [[nodiscard]] std::array PrebuiltLibraries(); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 07718b5ad6..6929f83cb2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -127,6 +127,8 @@ struct PreparedShaderState { struct Entry { std::size_t codeOffset; std::shared_ptr handle; + bool fromDeferredAbi = false; + bool matched = false; }; struct Rectangle { std::uint64_t vertexId; 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 6cdad1b526..54286b1e7a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -597,7 +597,57 @@ bool LazyShaderPreparation() { return lazy; } -void PrepareDeferredAbis(const ShaderSnapshot& snapshot); +using PreparedAtUse = std::optional>; + +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot); + +struct DeferredAbiUseCounters { + std::atomic firstUses{0}; + std::atomic found{0}; + std::atomic own{0}; + std::atomic entries{0}; + std::atomic matchedEntries{0}; + std::atomic lastReport{0}; +}; + +DeferredAbiUseCounters& DeferredAbiCounters() { + static DeferredAbiUseCounters counters; + return counters; +} + +void ReportDeferredAbiUses() { + auto& counters = DeferredAbiCounters(); + const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto previous = counters.lastReport.load(std::memory_order_relaxed); + if ((previous != 0 && now - previous < 60000) || !counters.lastReport.compare_exchange_strong(previous, now, std::memory_order_relaxed)) return; + const auto entries = counters.entries.load(std::memory_order_relaxed); + const auto matched = counters.matchedEntries.load(std::memory_order_relaxed); + std::fprintf(stderr, "[shaders] deferred ABIs: %llu prepared at first use, %llu found their artifact there, %llu prepared their own; %llu of the %llu entries prepared from them never matched a use\n", static_cast(counters.firstUses.load(std::memory_order_relaxed)), static_cast(counters.found.load(std::memory_order_relaxed)), static_cast(counters.own.load(std::memory_order_relaxed)), static_cast(entries - std::min(entries, matched)), static_cast(entries)); +} + +void NoteMatchedEntry(PreparedShaders::Entry& entry, const PreparedAtUse& preparedAtUse) { + auto& counters = DeferredAbiCounters(); + bool changed = false; + if (entry.fromDeferredAbi && !entry.matched) { + entry.matched = true; + counters.matchedEntries.fetch_add(1, std::memory_order_relaxed); + changed = true; + } + if (preparedAtUse) { + counters.firstUses.fetch_add(1, std::memory_order_relaxed); + if (std::ranges::find(*preparedAtUse, entry.handle.get()) != preparedAtUse->end()) counters.found.fetch_add(1, std::memory_order_relaxed); + changed = true; + } + if (changed) ReportDeferredAbiUses(); +} + +void NoteOwnPreparation(const PreparedAtUse& preparedAtUse) { + if (!preparedAtUse) return; + auto& counters = DeferredAbiCounters(); + counters.firstUses.fetch_add(1, std::memory_order_relaxed); + counters.own.fetch_add(1, std::memory_order_relaxed); + ReportDeferredAbiUses(); +} void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { if (request.context.convertedSamples) { @@ -634,14 +684,19 @@ std::shared_ptr SourceHandleFor(const Shad ShaderRecompiler::BuildPreparedShaderKey(request, key); std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); + PreparedAtUse preparedAtUse; if (!snapshot.prepared->deferredAbis.empty()) { lock.unlock(); - PrepareDeferredAbis(snapshot); + preparedAtUse = PrepareDeferredAbis(snapshot); lock.lock(); } - for (const auto& entry : snapshot.prepared->entries) { - if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle; + for (auto& entry : snapshot.prepared->entries) { + if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) { + NoteMatchedEntry(entry, preparedAtUse); + return entry.handle; + } } + NoteOwnPreparation(preparedAtUse); ReportPreparedAtUse(snapshot, request); auto handle = PrepareShaderWithDiagnostics(request); snapshot.prepared->entries.push_back({codeOffset, handle}); @@ -792,17 +847,22 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); + PreparedAtUse preparedAtUse; if (!snapshot.prepared->deferredAbis.empty()) { lock.unlock(); - PrepareDeferredAbis(snapshot); + preparedAtUse = PrepareDeferredAbis(snapshot); lock.lock(); } - for (const auto& entry : snapshot.prepared->entries) { + for (auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); if (!linked.code.empty() && !std::ranges::equal(invocationRequest.shader.code, linked.code)) continue; - if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation); + if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) { + NoteMatchedEntry(entry, preparedAtUse); + return std::move(*invocation); + } } + NoteOwnPreparation(preparedAtUse); ReportPreparedAtUse(snapshot, source); auto handle = PrepareShaderWithDiagnostics(source); invocationRequest = source; @@ -1089,12 +1149,14 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } -void PrepareDeferredAbis(const ShaderSnapshot& snapshot) { +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot) { std::vector deferred; { std::lock_guard lock(snapshot.prepared->mutex); deferred.swap(snapshot.prepared->deferredAbis); } + if (deferred.empty()) return std::nullopt; + std::vector added; for (const auto& abi : deferred) { const auto device = abi.device.lock(); if (device == nullptr || snapshot.registeredState == nullptr) continue; @@ -1117,9 +1179,14 @@ void PrepareDeferredAbis(const ShaderSnapshot& snapshot) { std::lock_guard lock(snapshot.prepared->mutex); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(snapshot.prepared->entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); - if (!duplicate) snapshot.prepared->entries.push_back(std::move(entry)); + if (duplicate) continue; + entry.fromDeferredAbi = true; + added.push_back(entry.handle.get()); + snapshot.prepared->entries.push_back(std::move(entry)); } } + DeferredAbiCounters().entries.fetch_add(added.size(), std::memory_order_relaxed); + return added; } void DeferPipelineStage(PreparedShaderState& prepared, const ShaderSnapshot& snapshot, const char* reason) { @@ -1638,6 +1705,13 @@ std::size_t Driver::PreparedEntries(const Shader* shader) { return snapshot->prepared->entries.size(); } +Driver::DeferredAbiUseCounts Driver::DeferredAbiUses() { + auto& counters = DeferredAbiCounters(); + const auto entries = counters.entries.load(std::memory_order_relaxed); + const auto matched = counters.matchedEntries.load(std::memory_order_relaxed); + return {counters.firstUses.load(std::memory_order_relaxed), counters.found.load(std::memory_order_relaxed), counters.own.load(std::memory_order_relaxed), entries, entries - std::min(entries, matched)}; +} + std::vector> Driver::PreparedEntryHashes(const Shader* shader) { std::shared_ptr snapshot; { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index 4a2a829146..aad7c87ddb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -379,6 +379,10 @@ std::size_t Entries(const Synthetic& program) { return AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&program.header.shader); } +std::string Uses(const AgcDriver::DriverDetail::Driver::DeferredAbiUseCounts& uses) { + return std::to_string(uses.firstUses) + " first uses, " + std::to_string(uses.found) + " found, " + std::to_string(uses.own) + " own, " + std::to_string(uses.unmatchedEntries) + " of " + std::to_string(uses.entries) + " entries unmatched"; +} + constexpr std::uint32_t Matching = Programs; constexpr std::uint32_t Twice = Programs + 1; constexpr std::uint32_t Replaced = Programs + 2; @@ -437,7 +441,7 @@ std::vector EagerResolutionEntries(const Report& eager, std::uint32 return made; } -Report Scenario(const Layout& layout, std::vector& first) { +Report Scenario(const Layout& layout, std::vector& first, std::array* uses = nullptr) { auto& driver = AgcDriver::DriverDetail::Driver::Get(); Report report; report["libraries before"] = Text(driver.PrebuiltLibraries()); @@ -471,7 +475,15 @@ Report Scenario(const Layout& layout, std::vector& first) { Require(std::ranges::any_of(bytes, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: drawn shader " + std::to_string(index) + " wrote nothing"); if (index == 0) first = std::move(bytes); }; - for (const auto index : {0u, 1u, Matching, Twice}) draw(index, programs[index]); + const auto count = [&](std::size_t slot) { + if (uses != nullptr) (*uses)[slot] = AgcDriver::DriverDetail::Driver::DeferredAbiUses(); + }; + count(0); + for (const auto index : {0u, 1u}) draw(index, programs[index]); + count(1); + draw(Matching, programs[Matching]); + count(2); + draw(Twice, programs[Twice]); draw(Replacement, programs[Replacement]); AgcDriverRegisterShader_nid_postfix(&programs[Replaced].header.shader); draw(Replaced, programs[Replaced]); @@ -523,7 +535,8 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference Require(eager.contains("libraries drawn"), "lazy resolved ABI: the eager report is missing or short"); } std::vector first; - const auto lazy = Scenario(layout, first); + std::array uses{}; + const auto lazy = Scenario(layout, first, &uses); for (std::uint32_t index = 0; index <= Replaced; ++index) { const auto key = "resolved " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(Count(lazy.at(key))) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); @@ -539,6 +552,11 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference const auto key = "drawn " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(Count(lazy.at(key))) + " prepared entries"); } + const auto& [start, unmatched, matched] = uses; + Require(unmatched.firstUses - start.firstUses == Drawn && unmatched.own - start.own == Drawn && unmatched.found == start.found, "lazy resolved ABI: draws whose state differs from the resolved ABI did not count as preparing their own artifact (before: " + Uses(start) + "; after: " + Uses(unmatched) + ")"); + Require(unmatched.entries > start.entries && unmatched.unmatchedEntries - start.unmatchedEntries == unmatched.entries - start.entries, "lazy resolved ABI: entries prepared for an ABI no draw used do not count as unmatched (before: " + Uses(start) + "; after: " + Uses(unmatched) + ")"); + Require(matched.firstUses == unmatched.firstUses + 1 && matched.found == unmatched.found + 1 && matched.own == unmatched.own, "lazy resolved ABI: a draw whose state matches the resolved ABI did not count as finding its artifact (before: " + Uses(unmatched) + "; after: " + Uses(matched) + ")"); + Require(matched.entries > unmatched.entries && matched.unmatchedEntries == unmatched.unmatchedEntries + (matched.entries - unmatched.entries) - 1, "lazy resolved ABI: the entry a matching draw used still counts as unmatched (before: " + Uses(unmatched) + "; after: " + Uses(matched) + ")"); for (const auto index : {Matching, Twice, Replaced}) Require(eager.at("drawn " + std::to_string(index)) == eager.at("resolved " + std::to_string(index)), "lazy resolved ABI: shader " + std::to_string(index) + ", drawn with its resolved ABI's state, did not find the ABI's entries with eager preparation"); for (std::uint32_t index = 0; index < Drawn; ++index) Require(Count(eager.at("drawn " + std::to_string(index))) == Count(eager.at("resolved " + std::to_string(index))) + 1, "lazy resolved ABI: a draw whose state differs from its resolved ABI did not prepare its own entry with eager preparation"); Require(Count(eager.at("drawn " + std::to_string(Replacement))) == Count(eager.at("registered " + std::to_string(Replacement))) + 1, "lazy resolved ABI: the header registered over a resolved one did not prepare its own entry at its draw with eager preparation"); From 241c8d59738747b93efafc29a7473997601b015f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:27:44 +0200 Subject: [PATCH 0240/1360] test(libkernel_sys): keep a symlink target of 1023 bytes valid The largest target below MAXPATHLEN must pass the length check, so the comparison cannot slip from >= 1024 to >= 1023. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKernelSys.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 2f751a04e6..5d92a89854 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -301,6 +301,13 @@ int main() { Require(symlink_nid_postfix(tooLong.c_str(), created.c_str()) == -1 && Error() == 63); Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "target-too-long"))); } + { + const auto longest = std::string(1023, 'a'); + const auto created = (root / "target-longest").string(); + const int made = symlink_nid_postfix(longest.c_str(), created.c_str()); + Require(made == 0 || Error() != 63); + if (made == 0) Require(RemoveSymlink(created)); + } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); From 2657a81218eabed8771e261be40f89d3b9687674 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:28:05 -0700 Subject: [PATCH 0241/1360] test(relinker): resolve a libSceAmpr-qualified import of the locale objects against libc's windows_import_identity generates two probes with the entry stub builder, as the diagnostics test does, and runs them next to the patched libraries: same.exe imports the three locale objects of #8875 twice, qualified to libSceAmpr.prx and to libc.prx, and exits 0 only if each pair resolved to one address; different.exe compares the first with libkernel's getpagesize and must exit 1. That pins the fallback from a host module to libc at run time, independently of how the stub builds its search order. Co-Authored-By: Claude Fable 5.1 --- core/relinker/CMakeLists.txt | 8 + .../tests/WindowsImportIdentityTests.cpp | 162 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index e9d1087390..4917347134 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -157,6 +157,14 @@ if(WIN32) add_test(NAME windows_dependency_diagnostics COMMAND windows_dependency_diagnostics_tests) endif() + add_test_executable(windows_import_identity_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsImportIdentityTests.cpp) + target_link_libraries(windows_import_identity_tests PRIVATE relinker_core) + if(BUILD_TESTING) + set_target_properties(windows_import_identity_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_dependencies(windows_import_identity_tests libs) + add_test(NAME windows_import_identity COMMAND windows_import_identity_tests "${CMAKE_BINARY_DIR}/core/libs/libs") + endif() + add_test_executable(amd64_only_windows_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/Amd64OnlyWindowsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp diff --git a/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp new file mode 100644 index 0000000000..ffe753bfaf --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp @@ -0,0 +1,162 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Fs = std::filesystem; +using namespace Elfpatcher::Windows; + +constexpr std::uint32_t GotRva = LoadRva + 0x1000; +constexpr std::array LocaleObjects{"-mLzBSk-VGs", "a54t8+k7KpY", "KfcTPbeaOqg"}; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Fs::path createFixtureDirectory(const Fs::path& parent) { + std::random_device random; + for (int attempt = 0; attempt < 100; ++attempt) { + auto name = Fs::path("windows-import-identity-"); + name += std::to_string(GetCurrentProcessId()) + "-" + std::to_string(random()); + const auto directory = parent / name; + if (Fs::create_directory(directory)) return directory; + } + throw std::runtime_error("Cannot create a unique import identity fixture directory"); +} + +void appendDisplacement(std::vector& code, const std::int32_t displacement) { + for (int i = 0; i < 4; ++i) code.push_back(static_cast((static_cast(displacement) >> (8 * i)) & 0xffu)); +} + +// For each pair of GOT slots: mov rax,[a]; cmp rax,[b]; jne fail. Then xor eax,eax; ret. fail: mov eax,1; ret. +std::vector compareCode(const std::vector>& pairs) { + std::vector code; + const auto failOffset = static_cast(pairs.size() * 16 + 3); + for (std::size_t i = 0; i < pairs.size(); ++i) { + const auto base = static_cast(i * 16); + code.insert(code.end(), {0x48, 0x8b, 0x05}); + appendDisplacement(code, static_cast(GotRva + 8u * static_cast(pairs[i].first)) - static_cast(LoadRva + base + 7)); + code.insert(code.end(), {0x48, 0x3b, 0x05}); + appendDisplacement(code, static_cast(GotRva + 8u * static_cast(pairs[i].second)) - static_cast(LoadRva + base + 14)); + code.push_back(0x75); + code.push_back(static_cast(failOffset - static_cast(base + 16))); + } + code.insert(code.end(), {0x31, 0xc0, 0xc3}); + code.insert(code.end(), {0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3}); + return code; +} + +std::uint32_t slot(const int index) { + return GotRva + 8u * static_cast(index); +} + +void generateProbe(const Fs::path& file, const std::string& libraries, const std::vector& peImports, const std::vector>& pairs) { + const std::vector loaded{"libSceAmpr.prx", "libkernel.prx", "libc.prx"}; + PeSection entry{".guest", LoadRva, SectionRead | SectionExecute | 0x20u, compareCode(pairs)}; + PeSection got{".got", GotRva, SectionRead | SectionWrite | 0x40u, std::vector(8 * 8)}; + const auto imports = WindowsImportBuilder().Build(LoadRva + 0x2000); + const auto startup = WindowsEntryStubBuilder().Build(LoadRva + 0x3000, entry.Rva, imports, loaded, peImports, libraries, false, false); + std::array directories{}; + directories[1] = imports.Directory; + directories[3] = startup.ExceptionDirectory; + directories[12] = imports.AddressTable; + const auto image = WindowsPeWriter().Write({entry, got, imports.Section, startup.Data, startup.Code}, startup.Code.Rva, directories, false, ImageBase, false); + std::ofstream stream(file, std::ios::binary); + stream.write(reinterpret_cast(image.data()), static_cast(image.size())); + require(static_cast(stream), "Cannot write " + file.string()); +} + +DWORD runProbe(const Fs::path& probe, std::string& output) { + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + HANDLE input = nullptr; + HANDLE written = nullptr; + require(CreatePipe(&input, &written, &security, 0) && SetHandleInformation(input, HANDLE_FLAG_INHERIT, 0), "Cannot create the probe's pipe"); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdOutput = written; + startup.hStdError = written; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + PROCESS_INFORMATION process{}; + const auto filename = probe.wstring(); + require(CreateProcessW(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, probe.parent_path().wstring().c_str(), &startup, &process) != 0, + "Cannot start " + probe.filename().string() + ": " + std::to_string(GetLastError())); + CloseHandle(written); + if (WaitForSingleObject(process.hProcess, 120000) != WAIT_OBJECT_0) { + TerminateProcess(process.hProcess, 1); + throw std::runtime_error(probe.filename().string() + " timed out"); + } + DWORD status = 0; + require(GetExitCodeProcess(process.hProcess, &status) != 0, "Cannot read the probe's exit code"); + char buffer[4096]; + DWORD size = 0; + while (ReadFile(input, buffer, sizeof(buffer), &size, nullptr) && size != 0) output.append(buffer, size); + CloseHandle(input); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return status; +} + +// The generated probes load libc.prx next to the other libraries; its compiler runtime DLLs sit next to this test. +void copyRuntime(const Fs::path& directory) { + wchar_t self[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, self, MAX_PATH); + if (length == 0 || length >= MAX_PATH) return; + const auto own = Fs::path(self).parent_path(); + for (const char* name : {"libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"}) { + std::error_code error; + if (Fs::exists(own / name, error)) Fs::copy_file(own / name, directory / name, Fs::copy_options::overwrite_existing, error); + } +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: windows_import_identity_tests \n"; + return 2; + } + try { + const auto libraries = Fs::absolute(argv[1]).string(); + const auto directory = createFixtureDirectory(Fs::temp_directory_path()); + copyRuntime(directory); + std::vector same; + for (int i = 0; i < 3; ++i) { + same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i), .Addend = 0, .Library = "libSceAmpr.prx"}); + same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i + 1), .Addend = 0, .Library = "libc.prx"}); + } + generateProbe(directory / "same.exe", libraries, same, {{0, 1}, {2, 3}, {4, 5}}); + const std::vector different{ + {.Name = LocaleObjects[0], .TargetRva = slot(0), .Addend = 0, .Library = "libSceAmpr.prx"}, + {.Name = "k+AXqu2-eBc", .TargetRva = slot(6), .Addend = 0, .Library = "libkernel.prx"}, + }; + generateProbe(directory / "different.exe", libraries, different, {{0, 6}}); + std::string output; + const auto sameStatus = runProbe(directory / "same.exe", output); + require(sameStatus == 0, "A libSceAmpr.prx-qualified import of the locale objects did not resolve to libc's object (exit " + std::to_string(sameStatus) + ")\n" + output); + output.clear(); + const auto differentStatus = runProbe(directory / "different.exe", output); + require(differentStatus == 1, "The probe did not tell two different addresses apart (exit " + std::to_string(differentStatus) + ")\n" + output); + std::error_code error; + Fs::remove_all(directory, error); + std::cout << "Windows import identity tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} From 7157ebb4e8c83c608c725b00ce523bc3cf5bee5c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:26:13 +0000 Subject: [PATCH 0242/1360] test(relinker): cover libc fallback for Ampr imports (cherry picked from commit babde2b5bd9ea9af9fec545fcff33f00d1583fd6) --- .../elfpatcher/tests/EntryStubTests.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp index 5b3b8a78b1..68d88bf8be 100644 --- a/core/relinker/elfpatcher/tests/EntryStubTests.cpp +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -116,6 +117,23 @@ void windowsStubRejectsAnOverlongAllocatorPath() { throw std::runtime_error("an allocator path past the startup buffer was accepted"); } +void windowsLibSceAmprObjectImportsFallBackToTheLibcProvider() { + using namespace Elfpatcher::Windows; + const auto nativeImports = WindowsImportBuilder().Build(0x1000); + const std::vector libraries{"libSceAmpr.prx", "libc.prx"}; + const std::vector amprImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libSceAmpr.prx"}}; + const auto amprStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, amprImport, "$ORIGIN/libs", false, false); + const std::vector fallbackOrder{0, 0, 0, 0, 1, 0, 0, 0}; + require(std::search(amprStub.Data.Data.begin(), amprStub.Data.Data.end(), fallbackOrder.begin(), fallbackOrder.end()) != amprStub.Data.Data.end(), + "a libSceAmpr-qualified locale object import does not fall back to libc"); + + const std::vector libcImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libc.prx"}}; + const auto libcStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, libcImport, "$ORIGIN/libs", false, false); + const std::vector libcOrder{1, 0, 0, 0}; + require(std::search(libcStub.Data.Data.begin(), libcStub.Data.Data.end(), libcOrder.begin(), libcOrder.end()) != libcStub.Data.Data.end(), + "a libc-qualified locale object import does not resolve through libc"); +} + } int main() { @@ -126,6 +144,7 @@ int main() { windowsStubLoadsTheAllocatorOnRequest("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); windowsStubLoadsTheAllocatorOnRequest("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35}); windowsStubRejectsAnOverlongAllocatorPath(); + windowsLibSceAmprObjectImportsFallBackToTheLibcProvider(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; From 0c8fb06573787709ed1a5a4b4eceb2dc7dcef189 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:41:34 -0700 Subject: [PATCH 0243/1360] test(relinker): register windows_import_identity only when the libraries are built A relinker-only configuration has no libs target to depend on, so the Windows relinker job could not generate; the test needs the patched libraries and is left out there. Co-Authored-By: Claude Fable 5.1 --- core/relinker/CMakeLists.txt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4917347134..31d3b831c7 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -157,12 +157,14 @@ if(WIN32) add_test(NAME windows_dependency_diagnostics COMMAND windows_dependency_diagnostics_tests) endif() - add_test_executable(windows_import_identity_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsImportIdentityTests.cpp) - target_link_libraries(windows_import_identity_tests PRIVATE relinker_core) - if(BUILD_TESTING) - set_target_properties(windows_import_identity_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) - add_dependencies(windows_import_identity_tests libs) - add_test(NAME windows_import_identity COMMAND windows_import_identity_tests "${CMAKE_BINARY_DIR}/core/libs/libs") + if(NOT ANYPS5_RELINKER_ONLY) + add_test_executable(windows_import_identity_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsImportIdentityTests.cpp) + target_link_libraries(windows_import_identity_tests PRIVATE relinker_core) + if(BUILD_TESTING) + set_target_properties(windows_import_identity_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_dependencies(windows_import_identity_tests libs) + add_test(NAME windows_import_identity COMMAND windows_import_identity_tests "${CMAKE_BINARY_DIR}/core/libs/libs") + endif() endif() add_test_executable(amd64_only_windows_tests EXCLUDE_FROM_ALL From 4eeeeb6438b466f3c229deb8d9425f3c7f2f9bd3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:39:40 +0000 Subject: [PATCH 0244/1360] fix(agc): prepare a deferred ABI once when queues make its first use at once The first use of a shader whose ABI was deferred swapped the deferred list out and prepared it unlocked. A second queue reaching the shader meanwhile found the list empty and no entry yet, so it prepared an artifact of its own under the lock, and the first use then matched that artifact instead of the one its preparation made. Mark the shader while its deferred ABIs are prepared and have other uses wait on the settled condition until the entries are published. The entries of all deferred ABIs are published together under the lock. agc_driver_prepared_shaders_abi_deferred_concurrent_lazy submits the first dispatches of one deferred ABI from four queues at once and requires one first use that found its artifact, no own preparation, every front-end build and entry to come from the deferred ABI, and all four dispatches to use the same entry of it. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 62 ++++++++------ .../tests/execution/PreparedShaders.cpp | 85 ++++++++++++++++++- 4 files changed, 124 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 630a22b411..567029b6c7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2660,6 +2660,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_prepared_shaders_abi_deferred_concurrent_lazy COMMAND agc_driver_prepared_shaders_tests --abi-deferred-concurrent) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_concurrent_lazy PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FAILURE_MEMO=1;APS5_LAZY_SHADER_PREPARE=1;APS5_SUSPEND_DRAIN=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_concurrent_lazy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_prepared_shaders_abi_deferred_compute_lazy COMMAND agc_driver_prepared_shaders_tests --abi-deferred-compute) set_tests_properties(agc_driver_prepared_shaders_abi_deferred_compute_lazy PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FAILURE_MEMO=1;APS5_LAZY_SHADER_PREPARE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 6929f83cb2..8bd5c76db5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -162,6 +162,7 @@ struct PreparedShaders : PreparedShaderState { std::weak_ptr device; }; std::vector deferredAbis; + bool deferredPreparing = false; std::mutex mutex; std::condition_variable settled; std::exception_ptr failure; 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 54286b1e7a..1dcc43ad67 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -599,7 +599,7 @@ bool LazyShaderPreparation() { using PreparedAtUse = std::optional>; -PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot); +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot, std::unique_lock& lock); struct DeferredAbiUseCounters { std::atomic firstUses{0}; @@ -685,11 +685,7 @@ std::shared_ptr SourceHandleFor(const Shad std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); PreparedAtUse preparedAtUse; - if (!snapshot.prepared->deferredAbis.empty()) { - lock.unlock(); - preparedAtUse = PrepareDeferredAbis(snapshot); - lock.lock(); - } + if (!snapshot.prepared->deferredAbis.empty() || snapshot.prepared->deferredPreparing) preparedAtUse = PrepareDeferredAbis(snapshot, lock); for (auto& entry : snapshot.prepared->entries) { if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) { NoteMatchedEntry(entry, preparedAtUse); @@ -848,11 +844,7 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); PreparedAtUse preparedAtUse; - if (!snapshot.prepared->deferredAbis.empty()) { - lock.unlock(); - preparedAtUse = PrepareDeferredAbis(snapshot); - lock.lock(); - } + if (!snapshot.prepared->deferredAbis.empty() || snapshot.prepared->deferredPreparing) preparedAtUse = PrepareDeferredAbis(snapshot, lock); for (auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); @@ -1149,14 +1141,27 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } -PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot) { - std::vector deferred; - { - std::lock_guard lock(snapshot.prepared->mutex); - deferred.swap(snapshot.prepared->deferredAbis); +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot, std::unique_lock& lock) { + auto& prepared = *snapshot.prepared; + if (prepared.deferredPreparing) { + prepared.settled.wait(lock, [&] { return !prepared.deferredPreparing; }); + return std::nullopt; } - if (deferred.empty()) return std::nullopt; - std::vector added; + if (prepared.deferredAbis.empty()) return std::nullopt; + std::vector deferred; + deferred.swap(prepared.deferredAbis); + prepared.deferredPreparing = true; + struct Preparing { + PreparedShaders& prepared; + std::unique_lock& lock; + ~Preparing() { + if (!lock.owns_lock()) lock.lock(); + prepared.deferredPreparing = false; + prepared.settled.notify_all(); + } + } preparing{prepared, lock}; + lock.unlock(); + std::vector entries; for (const auto& abi : deferred) { const auto device = abi.device.lock(); if (device == nullptr || snapshot.registeredState == nullptr) continue; @@ -1166,24 +1171,25 @@ PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot) { state.userConfig = snapshot.registeredState->userConfig; for (const auto& [offset, value] : abi.context) state.context[offset] = value; for (const auto& [offset, value] : abi.primitive) state.userConfig[offset] = value; - std::vector entries; try { const auto plan = PlanRegistered(snapshot, *device, state, false); if (plan == nullptr) continue; plan->prebuildDevice = device; - entries = PrepareRegistered(*plan); + auto made = PrepareRegistered(*plan); + std::ranges::move(made, std::back_inserter(entries)); } catch (const std::exception& error) { APS5_LOG_ERR("Shader 0x%llx could not be prepared at its first use for its resolved ABI (%s); preparing it for the use instead", static_cast(snapshot.codeAddress), error.what()); continue; } - std::lock_guard lock(snapshot.prepared->mutex); - for (auto& entry : entries) { - const auto duplicate = std::ranges::any_of(snapshot.prepared->entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); - if (duplicate) continue; - entry.fromDeferredAbi = true; - added.push_back(entry.handle.get()); - snapshot.prepared->entries.push_back(std::move(entry)); - } + } + lock.lock(); + std::vector added; + for (auto& entry : entries) { + const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); + if (duplicate) continue; + entry.fromDeferredAbi = true; + added.push_back(entry.handle.get()); + prepared.entries.push_back(std::move(entry)); } DeferredAbiCounters().entries.fetch_add(added.size(), std::memory_order_relaxed); return added; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 6203b1706c..c2aede2905 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -983,6 +983,88 @@ void AbiDeferredCompute() { AgcDriverShutdown_nid_postfix(); } +void AbiDeferredConcurrent() { + using namespace NarrowConstantStoreFixture; + Require(std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr, "lazy preparation is not enabled"); + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + }; + static Header header; + const auto address = reinterpret_cast(Code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(header); + header.shader.shader_size = sizeof(Code); + header.shader.code = Code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.specials = &header.specials; + header.specials.dispatch_modifier = 0x8000; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); + constexpr std::size_t Queues = 4; + constexpr std::size_t OutputBytes = 65536; +#ifdef _WIN32 + auto* allocation = static_cast(VirtualAlloc(nullptr, Queues * OutputBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* allocation = static_cast(std::aligned_alloc(OutputBytes, Queues * OutputBytes)); +#endif + Require(allocation != nullptr, "cannot allocate the concurrent first use outputs"); + const auto release = [](std::uint8_t* block) { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const std::unique_ptr ownedOutputs(allocation, release); + GuestAllocations::Mutation().Add(allocation, Queues * OutputBytes, true, true, true); + std::fill_n(allocation, Queues * OutputBytes, Fill); + std::vector> commands(Queues); + std::vector packets; + for (std::size_t queue = 0; queue < Queues; ++queue) { + const auto output = reinterpret_cast(allocation + queue * OutputBytes); + for (const auto reg : header.registers) commands[queue].insert(commands[queue].end(), {0xc0017600u, reg.offset, reg.value}); + commands[queue].insert(commands[queue].end(), {0xc0027600u, 0x240, static_cast(output), static_cast(output >> 32u), 0xc0031500u, 1, 1, 1, 0x8041}); + packets.push_back({commands[queue].data(), static_cast(commands[queue].size()), 0, {}}); + } + const auto builds = ShaderRecompiler::FrontEndBuilds(); + const auto uses = AgcDriver::DriverDetail::Driver::DeferredAbiUses(); + std::barrier start(static_cast(Queues)); + std::vector threads; + for (std::size_t queue = 0; queue < Queues; ++queue) { + threads.emplace_back([&, queue] { + start.arrive_and_wait(); + sceAgcDriverSubmitAcb(0x20u + static_cast(queue), &packets[queue]); + }); + } + for (auto& thread : threads) thread.join(); + AgcDriverWaitIdle_nid_postfix(); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + for (std::size_t queue = 0; queue < Queues; ++queue) { + const auto* output = allocation + queue * OutputBytes; + for (std::size_t offset = 0; offset < OutputBytes; ++offset) { + const auto expected = offset < Threads * LaneBytes ? ExpectedLane[offset % LaneBytes] : Fill; + if (output[offset] != expected) throw std::runtime_error("concurrent first use: queue " + std::to_string(queue) + " byte " + std::to_string(offset) + " is " + std::to_string(output[offset]) + ", expected " + std::to_string(expected)); + } + } + const auto after = AgcDriver::DriverDetail::Driver::DeferredAbiUses(); + const auto built = ShaderRecompiler::FrontEndBuilds() - builds; + const auto entries = AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&header.shader); + const auto matched = (after.entries - after.unmatchedEntries) - (uses.entries - uses.unmatchedEntries); + const auto counts = std::to_string(built) + " front-end builds, " + std::to_string(entries) + " prepared entries, " + std::to_string(after.firstUses - uses.firstUses) + " first uses, " + std::to_string(after.found - uses.found) + " found, " + std::to_string(after.own - uses.own) + " own, " + std::to_string(matched) + " entries used"; + std::cout << "concurrent first use of a deferred ABI: " << counts << '\n'; + Require(after.firstUses == uses.firstUses + 1 && after.found == uses.found + 1 && after.own == uses.own, ("four queues making the first use of one deferred ABI at once did not prepare it once (" + counts + ")").c_str()); + Require(built == after.entries - uses.entries && entries == built, ("four queues making the first use of one deferred ABI at once prepared an artifact besides its preparation (" + counts + ")").c_str()); + Require(matched == 1, ("four queues making the first use of one deferred ABI at once did not all use the same entry of its preparation (" + counts + ")").c_str()); + AgcDriverShutdown_nid_postfix(); +} + void AbiDeferredRegistrationFailure() { static AbiHeader store; DescribeAbi(store, WaterfallStoreCode, 0, {{0x20c, ProgramLow(WaterfallStoreCode.data())}, {0x20d, ProgramHigh(WaterfallStoreCode.data())}, {0x207, 64}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 2u << 1u}}, {}); @@ -1268,11 +1350,12 @@ void RelocatedHeaders() { int main(int argc, char** argv) { try { const std::string_view abiMode = argc == 2 ? argv[1] : ""; - const bool abiDeferred = abiMode == "--abi-deferred-compute" || abiMode == "--abi-deferred-pixel" || abiMode == "--abi-deferred-stages" || abiMode == "--abi-deferred-rectangle" || abiMode == "--abi-deferred-undecodable-state" || abiMode == "--abi-deferred-registration-failure" || abiMode == "--abi-deferred-vertex-inputs" || abiMode == "--abi-deferred-rectangle-pairs"; + const bool abiDeferred = abiMode == "--abi-deferred-compute" || abiMode == "--abi-deferred-concurrent" || abiMode == "--abi-deferred-pixel" || abiMode == "--abi-deferred-stages" || abiMode == "--abi-deferred-rectangle" || abiMode == "--abi-deferred-undecodable-state" || abiMode == "--abi-deferred-registration-failure" || abiMode == "--abi-deferred-vertex-inputs" || abiMode == "--abi-deferred-rectangle-pairs"; Require(argc == 1 || abiDeferred || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); if (abiDeferred) { if (!OpenVulkanTestDevice()) return VulkanTestSkipped; if (abiMode == "--abi-deferred-compute") AbiDeferredCompute(); + else if (abiMode == "--abi-deferred-concurrent") AbiDeferredConcurrent(); else if (abiMode == "--abi-deferred-pixel") AbiDeferredPixel(); else if (abiMode == "--abi-deferred-stages") AbiDeferredStages(); else if (abiMode == "--abi-deferred-undecodable-state") AbiDeferredUndecodableState(); From 1ba22061e3d648af18612080f70489c426073fb9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:40:23 +0000 Subject: [PATCH 0245/1360] fix(agc): report deferred ABI uses only under APS5_PROFILE_DRAW The deferred ABI line went to stderr in every run that resolved an ABI at its first use. Print it through the profile output, like the [rescache] and [gpu] lines, and only when APS5_PROFILE_DRAW is set. The counters keep counting either way. --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 1dcc43ad67..841b4b8610 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/LogMacros.hpp" #include "ControlFlow/GraphBuilder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" @@ -616,13 +617,15 @@ DeferredAbiUseCounters& DeferredAbiCounters() { } void ReportDeferredAbiUses() { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) return; auto& counters = DeferredAbiCounters(); const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); auto previous = counters.lastReport.load(std::memory_order_relaxed); if ((previous != 0 && now - previous < 60000) || !counters.lastReport.compare_exchange_strong(previous, now, std::memory_order_relaxed)) return; const auto entries = counters.entries.load(std::memory_order_relaxed); const auto matched = counters.matchedEntries.load(std::memory_order_relaxed); - std::fprintf(stderr, "[shaders] deferred ABIs: %llu prepared at first use, %llu found their artifact there, %llu prepared their own; %llu of the %llu entries prepared from them never matched a use\n", static_cast(counters.firstUses.load(std::memory_order_relaxed)), static_cast(counters.found.load(std::memory_order_relaxed)), static_cast(counters.own.load(std::memory_order_relaxed)), static_cast(entries - std::min(entries, matched)), static_cast(entries)); + AgcDriver::ProfilePrint_nid_no_patch("[shaders] deferred ABIs: %llu prepared at first use, %llu found their artifact there, %llu prepared their own; %llu of the %llu entries prepared from them never matched a use\n", static_cast(counters.firstUses.load(std::memory_order_relaxed)), static_cast(counters.found.load(std::memory_order_relaxed)), static_cast(counters.own.load(std::memory_order_relaxed)), static_cast(entries - std::min(entries, matched)), static_cast(entries)); } void NoteMatchedEntry(PreparedShaders::Entry& entry, const PreparedAtUse& preparedAtUse) { From c44f3b59f741f50485b36af225639c6edcd6a547 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:44:07 +0100 Subject: [PATCH 0246/1360] feat(libkernel): implement pthread_timedjoin_np --- core/libs/prx/libkernel/CMakeLists.txt | 15 ++ .../prx/libkernel/Pthread/Posix/Thread.cpp | 7 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 24 ++- core/libs/tests/GuestTimedJoin.cpp | 183 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 + 5 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestTimedJoin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index bf78477ff7..41cf94083b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1869,6 +1869,21 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) + add_test_executable(guest_timed_join_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimedJoin.cpp) + target_include_directories(guest_timed_join_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_timed_join_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_timed_join_tests) + foreach(joinMode IN ITEMS normal coarse) + add_test(NAME guest_timed_join_${joinMode} COMMAND guest_timed_join_tests $) + set_tests_properties(guest_timed_join_${joinMode} PROPERTIES TIMEOUT 15) + if(joinMode STREQUAL coarse) + set_tests_properties(guest_timed_join_${joinMode} PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + endif() + if(WIN32) + set_tests_properties(guest_timed_join_${joinMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_sleep_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepCancel.cpp) target_include_directories(guest_sleep_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sleep_cancel_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index dc71c1cebc..e33d5d0b2d 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -18,6 +18,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadDetach(Pthread thread); void APS5_VABI scePthreadExit(void* retval); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI JoinThread_nid_no_patch(Pthread thread, void** retval, const KernelTimespec* abstime); int APS5_VABI scePthreadRename(Pthread thread, const char* name); int APS5_VABI scePthreadGetname(Pthread thread, char* name); Pthread APS5_VABI scePthreadSelf(); @@ -234,6 +235,12 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +int APS5_VABI pthread_timedjoin_np_nid_postfix(Pthread thread, void** value, const KernelTimespec* abstime) { + if (abstime == nullptr || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) + return PosixThread::GUEST_EINVAL; + return PosixThread::ToErrno(JoinThread_nid_no_patch(thread, value, abstime)); +} + namespace { struct RegisterPthreadOnLoad { RegisterPthreadOnLoad() { diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index bd61a61fc9..31b784005d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -2,6 +2,7 @@ #include "../include/ThreadLifecycle.hpp" #include "../include/WindowsThreadLocal.hpp" #include "../include/Cancel.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -926,7 +927,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread return SCE_OK; } -int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { +int APS5_VABI JoinThread_nid_no_patch(Pthread thread, void** retval, const KernelTimespec* abstime) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; @@ -938,7 +939,22 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { if (thread->_joining) return SCE_KERNEL_ERROR_EOPNOTSUPP; thread->_joining = true; while (!thread->_finished.load()) { - ThreadCancel::Wait(thread->_join_cv, lock, std::nullopt); + std::optional deadline; + if (abstime != nullptr) { + KernelTimespec now{}; + if (clock_gettime_nid_postfix(0, &now) != 0) { + thread->_joining = false; + throw std::runtime_error("Joining guest thread: realtime clock unavailable"); + } + if (abstime->tv_sec < now.tv_sec || (abstime->tv_sec == now.tv_sec && abstime->tv_nsec <= now.tv_nsec)) { + thread->_joining = false; + return SCE_KERNEL_ERROR_ETIMEDOUT; + } + const auto seconds = static_cast(abstime->tv_sec) - static_cast(now.tv_sec); + const auto nanos = seconds > 1 ? std::int64_t{1000000000} : static_cast(seconds) * 1000000000 + abstime->tv_nsec - now.tv_nsec; + deadline = TimedWait::DeadlineNanos(static_cast((nanos + 999) / 1000)); + } + ThreadCancel::Wait(thread->_join_cv, lock, deadline); if (ThreadCancel::Requested()) { thread->_joining = false; lock.unlock(); @@ -964,6 +980,10 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { return SCE_OK; } +int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { + return JoinThread_nid_no_patch(thread, retval, nullptr); +} + int APS5_VABI scePthreadDetach(Pthread thread) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; { diff --git a/core/libs/tests/GuestTimedJoin.cpp b/core/libs/tests/GuestTimedJoin.cpp new file mode 100644 index 0000000000..34ce27fdf0 --- /dev/null +++ b/core/libs/tests/GuestTimedJoin.cpp @@ -0,0 +1,183 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +struct Timespec { std::int64_t seconds, nanoseconds; }; +static_assert(sizeof(Timespec)==16 && alignof(Timespec)==8 && offsetof(Timespec,nanoseconds)==8); +extern "C" { +int APS5_VABI pthread_create_nid_postfix(void**,const void*,void* (APS5_VABI*)(void*),void*); +int APS5_VABI pthread_join_nid_postfix(void*,void**); +int APS5_VABI pthread_detach_nid_postfix(void*); +int APS5_VABI pthread_cancel_nid_postfix(void*); +void* APS5_VABI pthread_self_nid_postfix(); +int APS5_VABI clock_gettime_nid_postfix(int,Timespec*); +int* APS5_VABI __error_nid_postfix(); +} +using TimedJoin=int (APS5_VABI*)(void*,void**,const Timespec*); +static TimedJoin timedJoin; + +void Require(bool value,const char* message) { + if (!value) { std::fprintf(stderr,"timed join: %s\n",message); std::abort(); } +} + +Timespec After(int milliseconds) { + Timespec result{}; + Require(clock_gettime_nid_postfix(0,&result)==0,"read realtime"); + result.nanoseconds+=static_cast(milliseconds)*1000000; + result.seconds+=result.nanoseconds/1000000000; + result.nanoseconds%=1000000000; + return result; +} + +struct Gate { std::atomic release{false}, entered{false}, leaving{false}; }; +void* APS5_VABI Worker(void* argument) { + auto& gate=*static_cast(argument); + gate.entered.store(true,std::memory_order_release); + while (!gate.release.load(std::memory_order_acquire)) std::this_thread::yield(); + gate.leaving.store(true,std::memory_order_release); + return reinterpret_cast(std::uintptr_t{0x12345678}); +} + +void Wait(const std::atomic& flag) { + const auto deadline=std::chrono::steady_clock::now()+std::chrono::seconds(3); + while (!flag.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() entered{false}; int result=-1; void* value=reinterpret_cast(std::uintptr_t{0x55}); }; +void* APS5_VABI Joiner(void* argument) { + auto& task=*static_cast(argument); + const auto deadline=After(3000); + task.entered.store(true,std::memory_order_release); + task.result=timedJoin(task.target,&task.value,&deadline); + return nullptr; +} + +void AwaitClaim(void* target) { + const Timespec past{0,0}; + const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(2); + for (;;) { + const int result=timedJoin(target,nullptr,&past); + if (result==45) return; + Require(result==60,"claim probe returns timeout or ENOTSUP"); + Require(std::chrono::steady_clock::now()(std::uintptr_t{0x55}); std::uint64_t after=0x5a5a5a5a; } result; + const Timespec past{0,0}; + *__error_nid_postfix()=123; + Require(timedJoin(thread,&result.value,&past)==60,"past deadline expires"); + Require(result.value==reinterpret_cast(std::uintptr_t{0x55}),"timeout preserves return slot"); + Require(*__error_nid_postfix()==123,"POSIX error preserves errno"); + const auto deadline=After(50); + const auto originalDeadline=deadline; + const auto started=std::chrono::steady_clock::now(); + Require(timedJoin(thread,&result.value,&deadline)==60,"future deadline expires"); + Require(std::chrono::steady_clock::now()-started>=std::chrono::milliseconds(25),"wait did not expire immediately"); + Require(deadline.seconds==originalDeadline.seconds && deadline.nanoseconds==originalDeadline.nanoseconds,"deadline input is unchanged"); + gate.release.store(true,std::memory_order_release); + const Timespec distant{std::numeric_limits::max(),999999999}; + Require(timedJoin(thread,&result.value,&distant)==0,"timeout leaves target joinable; distant deadline does not overflow"); + Require(result.value==reinterpret_cast(std::uintptr_t{0x12345678}),"joined return value"); + Require(result.before==0xa5a5a5a5 && result.after==0x5a5a5a5a,"return-slot bounds"); + Require(timedJoin(thread,nullptr,&valid)==3,"already joined target"); + Require(timedJoin(thread,nullptr,nullptr)==22,"deadline validation precedes joined state"); + Gate done; + void* completed=Start(done); + done.release.store(true,std::memory_order_release); + Wait(done.leaving); + const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(2); + for (;;) { + const int joined=timedJoin(completed,nullptr,&past); + if (joined==0) break; + Require(joined==60 && std::chrono::steady_clock::now()(std::uintptr_t{1}),"joiner is canceled"); + Require(task.result==-1 && task.value==reinterpret_cast(std::uintptr_t{0x55}),"canceled join did not return or write result"); + const Timespec past{0,0}; + Require(timedJoin(target,nullptr,&past)==60,"cancellation releases join claim"); + gate.release.store(true,std::memory_order_release); + const auto deadline=After(1000); + Require(timedJoin(target,nullptr,&deadline)==0,"cancellation preserves target joinability"); + } else { + gate.release.store(true,std::memory_order_release); + Require(pthread_join_nid_postfix(joiner,nullptr)==0,"join successful joiner"); + Require(task.result==0 && task.value==reinterpret_cast(std::uintptr_t{0x12345678}),"competing join succeeds once"); + } +} + +int main(int argc,char** argv) { + Require(argc==2,"expected kernel path"); +#ifdef _WIN32 + const auto library=LoadLibraryA(argv[1]); + Require(library!=nullptr,"load kernel"); + timedJoin=reinterpret_cast(GetProcAddress(library,"pthread_timedjoin_np_nid_postfix")); +#else + const auto library=dlopen(argv[1],RTLD_NOW); + Require(library!=nullptr,"load kernel"); + timedJoin=reinterpret_cast(dlsym(library,"pthread_timedjoin_np_nid_postfix")); +#endif + Require(timedJoin!=nullptr,"pthread_timedjoin_np export is missing"); + Basic(); + Contention(false); + Contention(true); +#ifdef _WIN32 + Require(FreeLibrary(library)!=0,"release kernel lookup handle"); +#else + Require(dlclose(library)==0,"release kernel lookup handle"); +#endif + std::puts("timed join checks passed"); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2883d5c3d4..765760fd8f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -113,6 +113,8 @@ - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [pthread_timedjoin_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel, NID `PkS44IGrDkM`) - the signature follows [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/pthread_np.h#L71); deadline validation, absolute realtime deadlines, error precedence and join ownership follow [FreeBSD libthr](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_join.c). The [public 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c) lists this NID, but PS5 behavior and module/library versions have not been measured. Realtime is rechecked at most two seconds apart while waiting. After guest cleanup completes, final native thread teardown uses the existing unbounded join and may finish after the deadline; host teardown is not bounded by this implementation. + ### Functional - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling From aab806939ea6cb11c9baf0feb3403260e8805c44 Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 Date: Wed, 21 Oct 2026 06:56:42 +0200 Subject: [PATCH 0247/1360] feat(libkernel): implement getresuid and getresgid Follow FreeBSD 11.4 sys_getresuid and sys_getresgid (sys/kern/kern_prot.c): retrieve real, effective, and saved user and group IDs for the guest process, which is assumed to run as root with IDs 0. Pointers that are not null receive 0, null pointers are ignored, and 0 is returned leaving errno unchanged on success. Cover both in guest_credentials test and update libkernel debt documentation. --- .../libs/prx/libkernel/System/src/Process.cpp | 14 ++++++++++ core/libs/tests/GuestCredentials.cpp | 28 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 348464a177..89f55ec429 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -314,6 +314,20 @@ int APS5_VABI issetugid_nid_postfix(void) { return 0; } +int APS5_VABI getresuid_nid_postfix(std::uint32_t* ruid, std::uint32_t* euid, std::uint32_t* suid) { + if (ruid != nullptr) *ruid = 0; + if (euid != nullptr) *euid = 0; + if (suid != nullptr) *suid = 0; + return 0; +} + +int APS5_VABI getresgid_nid_postfix(std::uint32_t* rgid, std::uint32_t* egid, std::uint32_t* sgid) { + if (rgid != nullptr) *rgid = 0; + if (egid != nullptr) *egid = 0; + if (sgid != nullptr) *sgid = 0; + return 0; +} + void APS5_VABI exit_nid_postfix(int code) { LibcExit_nid_no_patch(code); } diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index 887f69c727..214cde5292 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -18,6 +18,8 @@ int APS5_VABI geteuid_nid_postfix(void); int APS5_VABI getgid_nid_postfix(void); int APS5_VABI getegid_nid_postfix(void); int APS5_VABI issetugid_nid_postfix(void); +int APS5_VABI getresuid_nid_postfix(std::uint32_t*, std::uint32_t*, std::uint32_t*); +int APS5_VABI getresgid_nid_postfix(std::uint32_t*, std::uint32_t*, std::uint32_t*); int* APS5_VABI __error_nid_postfix(); } @@ -68,6 +70,32 @@ int main() { Require(Throws(setgid_nid_postfix, Unchanged)); Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); Require(getgid_nid_postfix() == 0 && getegid_nid_postfix() == 0 && issetugid_nid_postfix() == 0); + + std::uint32_t ruid = 99, euid = 99, suid = 99; + *__error_nid_postfix() = 13; + Require(getresuid_nid_postfix(&ruid, &euid, &suid) == 0); + Require(ruid == 0 && euid == 0 && suid == 0); + Require(*__error_nid_postfix() == 13); + ruid = 99; + euid = 99; + suid = 99; + Require(getresuid_nid_postfix(&ruid, nullptr, nullptr) == 0 && ruid == 0); + Require(getresuid_nid_postfix(nullptr, &euid, nullptr) == 0 && euid == 0); + Require(getresuid_nid_postfix(nullptr, nullptr, &suid) == 0 && suid == 0); + Require(getresuid_nid_postfix(nullptr, nullptr, nullptr) == 0); + + std::uint32_t rgid = 99, egid = 99, sgid = 99; + *__error_nid_postfix() = 13; + Require(getresgid_nid_postfix(&rgid, &egid, &sgid) == 0); + Require(rgid == 0 && egid == 0 && sgid == 0); + Require(*__error_nid_postfix() == 13); + rgid = 99; + egid = 99; + sgid = 99; + Require(getresgid_nid_postfix(&rgid, nullptr, nullptr) == 0 && rgid == 0); + Require(getresgid_nid_postfix(nullptr, &egid, nullptr) == 0 && egid == 0); + Require(getresgid_nid_postfix(nullptr, nullptr, &sgid) == 0 && sgid == 0); + Require(getresgid_nid_postfix(nullptr, nullptr, nullptr) == 0); Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); Require(setreuid_nid_postfix(0, Unchanged) == 0); Require(setreuid_nid_postfix(Unchanged, 0) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2883d5c3d4..91dd5c81c2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -26,7 +26,7 @@ - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified - [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws -- [getuid, geteuid, getgid, getegid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified +- [getuid, geteuid, getgid, getegid, getresuid, getresgid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. `getresuid` and `getresgid` follow FreeBSD 11.4's `sys_getresuid` and `sys_getresgid`, writing 0 to non-null pointers and leaving errno untouched on success. The values a title sees on a console are unverified - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` From 849c71646f9218cbe76b55895bcfcff5c2a172dd Mon Sep 17 00:00:00 2001 From: Mehran Date: Wed, 21 Oct 2026 07:58:49 +0300 Subject: [PATCH 0248/1360] fix(agc): keep the write-back ready barrier while hole copies are pending --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index eb824c85d1..5f9c20af02 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3415,7 +3415,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; toRead.newLayout = VK_IMAGE_LAYOUT_GENERAL; const BarrierMask ready{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, importReady.srcAccessMask | toRead.srcAccessMask, importReady.dstAccessMask | toRead.dstAccessMask, toRead.oldLayout != toRead.newLayout, toRead.srcQueueFamilyIndex != toRead.dstQueueFamilyIndex}; - const bool ordered = recorder != nullptr && seeds.empty() && padding.copies.empty() && Recorder::ElideCovered(Recorder::CommandClass::StorageWriteBack, ready, covered); + const bool ordered = recorder != nullptr && seeds.empty() && padding.copies.empty() && holeCopies.empty() && Recorder::ElideCovered(Recorder::CommandClass::StorageWriteBack, ready, covered); if (!ordered) context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toRead); for (std::size_t i = 0; i < windows.size(); ++i) { const auto& window = windows[i]; From ca376e7545f5d03a19892ef1777be9094e406d46 Mon Sep 17 00:00:00 2001 From: THEBIGTOMMY13DEV Date: Wed, 21 Oct 2026 01:05:56 -0400 Subject: [PATCH 0249/1360] fix(tools): reject SELF program tables overlapping the ELF header --- tools/tests/test_unwrap_self.py | 21 +++++++++++++++++++++ tools/unwrap_self.py | 2 ++ 2 files changed, 23 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 3ef6bc8e65..323056d523 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -122,6 +122,27 @@ class UnwrapTest(unittest.TestCase): def tearDown(self): self.temporary.cleanup() + def test_program_table_overlapping_elf_header_preserves_output(self): + for offset in (0, 8, 32, 63): + for previous in (None, b"previous output"): + with self.subTest(offset=offset, previous=previous): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + struct.pack_into(" len(data): raise Refused("ELF program header table does not fit the file") headers = [] From e70813c3951e03b4f1dcb59d36999d070ea2fb7c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 18:18:07 +1300 Subject: [PATCH 0250/1360] feat(libSceWebBrowserDialog): check the cookie calls as the console does sceWebBrowserDialogSetCookie and sceWebBrowserDialogResetCookie threw. They now check their parameters in the console's order and with its codes (PS5 12.00 module), then succeed: there is no cookie store. Open sets the open-dialog flag that makes both return 0x80B80006 until the dialog is updated to finished, closed or terminated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../prx/libSceWebBrowserDialog/CMakeLists.txt | 11 ++ .../prx/libSceWebBrowserDialog/Export.cpp | 57 ++++++- .../tests/GuestWebBrowserDialogCookies.cpp | 145 ++++++++++++++++++ docs/dev/debt/libSceWebBrowserDialog.md | 1 + 4 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 core/libs/tests/GuestWebBrowserDialogCookies.cpp diff --git a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt index 2c498e5df1..641f60463e 100644 --- a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt +++ b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt @@ -12,3 +12,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_web_browser_dialog_cookies_tests ${LIBS_INCLUDE_DIR}/tests/GuestWebBrowserDialogCookies.cpp) + target_include_directories(guest_web_browser_dialog_cookies_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_web_browser_dialog_cookies_tests PRIVATE libSceWebBrowserDialog) + add_test(NAME guest_web_browser_dialog_cookies COMMAND guest_web_browser_dialog_cookies_tests) + if(WIN32) + set_tests_properties(guest_web_browser_dialog_cookies 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 8ccf318370..2637230010 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,11 +1,13 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { std::atomic g_status{0}; +std::atomic g_open{false}; constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; @@ -14,8 +16,36 @@ constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_INVALID_STATE = static_cast(0x80B80006u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_PARAM_INVALID = static_cast(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); + +struct ResetCookieParam { + std::uint64_t size; + std::uint8_t reserved[256]; +}; + +struct SetCookieParam { + std::uint64_t size; + const char* url; + const char* cookie; + std::uint8_t reserved[256]; +}; + +static_assert(sizeof(ResetCookieParam) == 0x108 && sizeof(SetCookieParam) == 0x118); + +bool ReservedClear(const std::uint8_t (&reserved)[256]) { + for (const auto byte : reserved) + if (byte != 0) return false; + return true; +} + +bool LengthWithin(const char* text, std::size_t limit) { + if (!text) return false; + const auto length = strnlen(text, limit); + return length != 0 && length < limit; +} } extern "C" { @@ -28,9 +58,9 @@ int APS5_VABI sceWebBrowserDialogInitialize(void) { int APS5_VABI sceWebBrowserDialogTerminate(void) { int expected = 1; - if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) { g_open = false; return 0; } expected = COMMON_DIALOG_STATUS_FINISHED; - if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) { g_open = false; return 0; } if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return COMMON_DIALOG_ERROR_BUSY; } @@ -38,6 +68,7 @@ int APS5_VABI sceWebBrowserDialogTerminate(void) { int APS5_VABI sceWebBrowserDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; g_status = COMMON_DIALOG_STATUS_FINISHED; + g_open = false; return 0; } @@ -60,16 +91,24 @@ int APS5_VABI sceWebBrowserDialogOpen(const void* param) { 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; + g_open = true; return 0; } int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { - return g_status.load(); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_FINISHED) g_open = false; + return status; } -int APS5_VABI sceWebBrowserDialogSetCookie(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceWebBrowserDialogSetCookie(const SetCookieParam* param) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_open.load()) return COMMON_DIALOG_ERROR_INVALID_STATE; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (param->size != sizeof(SetCookieParam) || !LengthWithin(param->url, 0x800) || !LengthWithin(param->cookie, 0x1000) || + !ReservedClear(param->reserved)) + return COMMON_DIALOG_ERROR_PARAM_INVALID; return 0; } @@ -79,11 +118,15 @@ int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param) 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; + g_open = true; return 0; } -int APS5_VABI sceWebBrowserDialogResetCookie() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceWebBrowserDialogResetCookie(const ResetCookieParam* param) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_open.load()) return COMMON_DIALOG_ERROR_INVALID_STATE; + if (param->size != sizeof(ResetCookieParam) || !ReservedClear(param->reserved)) return COMMON_DIALOG_ERROR_PARAM_INVALID; return 0; } diff --git a/core/libs/tests/GuestWebBrowserDialogCookies.cpp b/core/libs/tests/GuestWebBrowserDialogCookies.cpp new file mode 100644 index 0000000000..608f054c97 --- /dev/null +++ b/core/libs/tests/GuestWebBrowserDialogCookies.cpp @@ -0,0 +1,145 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct ResetCookieParam { + std::uint64_t size; + std::uint8_t reserved[256]; +}; + +struct SetCookieParam { + std::uint64_t size; + const char* url; + const char* cookie; + std::uint8_t reserved[256]; +}; + +extern "C" { +int APS5_VABI sceWebBrowserDialogInitialize(void); +int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogClose(void); +int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +int APS5_VABI sceWebBrowserDialogSetCookie(const SetCookieParam* param); +int APS5_VABI sceWebBrowserDialogResetCookie(const ResetCookieParam* param); +} + +namespace { + +constexpr int NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int INVALID_STATE = static_cast(0x80B80006u); +constexpr int PARAM_INVALID = static_cast(0x80B8000Au); +constexpr int ARG_NULL = static_cast(0x80B8000Du); + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +SetCookieParam Cookie(const char* url, const char* cookie) { + SetCookieParam param {}; + param.size = sizeof(param); + param.url = url; + param.cookie = cookie; + return param; +} + +ResetCookieParam Reset() { + ResetCookieParam param {}; + param.size = sizeof(param); + return param; +} + +void CheckSetCookieParameters() { + auto valid = Cookie("https://example.com", "a=b"); + Require(sceWebBrowserDialogSetCookie(&valid) == 0, "valid SetCookie"); + Require(sceWebBrowserDialogSetCookie(nullptr) == ARG_NULL, "SetCookie null"); + + auto wrongSize = valid; + wrongSize.size = sizeof(ResetCookieParam); + Require(sceWebBrowserDialogSetCookie(&wrongSize) == PARAM_INVALID, "SetCookie size"); + auto noUrl = Cookie(nullptr, "a=b"); + Require(sceWebBrowserDialogSetCookie(&noUrl) == PARAM_INVALID, "SetCookie null url"); + auto emptyUrl = Cookie("", "a=b"); + Require(sceWebBrowserDialogSetCookie(&emptyUrl) == PARAM_INVALID, "SetCookie empty url"); + auto noCookie = Cookie("https://example.com", nullptr); + Require(sceWebBrowserDialogSetCookie(&noCookie) == PARAM_INVALID, "SetCookie null cookie"); + auto emptyCookie = Cookie("https://example.com", ""); + Require(sceWebBrowserDialogSetCookie(&emptyCookie) == PARAM_INVALID, "SetCookie empty cookie"); + + const std::string longestUrl(0x7ff, 'u'); + const std::string longUrl(0x800, 'u'); + auto longest = Cookie(longestUrl.c_str(), "a=b"); + Require(sceWebBrowserDialogSetCookie(&longest) == 0, "SetCookie 2047-byte url"); + auto tooLong = Cookie(longUrl.c_str(), "a=b"); + Require(sceWebBrowserDialogSetCookie(&tooLong) == PARAM_INVALID, "SetCookie 2048-byte url"); + + const std::string longestCookie(0xfff, 'c'); + const std::string longCookie(0x1000, 'c'); + auto longestValue = Cookie("https://example.com", longestCookie.c_str()); + Require(sceWebBrowserDialogSetCookie(&longestValue) == 0, "SetCookie 4095-byte cookie"); + auto tooLongValue = Cookie("https://example.com", longCookie.c_str()); + Require(sceWebBrowserDialogSetCookie(&tooLongValue) == PARAM_INVALID, "SetCookie 4096-byte cookie"); + + auto reserved = valid; + reserved.reserved[255] = 1; + Require(sceWebBrowserDialogSetCookie(&reserved) == PARAM_INVALID, "SetCookie reserved"); +} + +void CheckResetCookieParameters() { + auto valid = Reset(); + Require(sceWebBrowserDialogResetCookie(&valid) == 0, "valid ResetCookie"); + Require(sceWebBrowserDialogResetCookie(nullptr) == ARG_NULL, "ResetCookie null"); + auto wrongSize = valid; + wrongSize.size = sizeof(SetCookieParam); + Require(sceWebBrowserDialogResetCookie(&wrongSize) == PARAM_INVALID, "ResetCookie size"); + auto reserved = valid; + reserved.reserved[0] = 1; + Require(sceWebBrowserDialogResetCookie(&reserved) == PARAM_INVALID, "ResetCookie reserved"); +} + +void CheckOpenDialog() { + std::uint8_t open[64] = {}; + auto cookie = Cookie("https://example.com", "a=b"); + auto reset = Reset(); + Require(sceWebBrowserDialogOpen(open) == 0, "Open"); + Require(sceWebBrowserDialogSetCookie(&cookie) == INVALID_STATE, "SetCookie while open"); + Require(sceWebBrowserDialogSetCookie(nullptr) == INVALID_STATE, "SetCookie checks the open dialog before null"); + Require(sceWebBrowserDialogResetCookie(nullptr) == ARG_NULL, "ResetCookie checks null before the open dialog"); + Require(sceWebBrowserDialogResetCookie(&reset) == INVALID_STATE, "ResetCookie while open"); + Require(sceWebBrowserDialogUpdateStatus() == 3, "UpdateStatus finished"); + Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after UpdateStatus"); + Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after UpdateStatus"); + + Require(sceWebBrowserDialogOpen(open) == 0, "second Open"); + Require(sceWebBrowserDialogClose() == 0, "Close"); + Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after Close"); + + Require(sceWebBrowserDialogOpen(open) == 0, "third Open"); + Require(sceWebBrowserDialogTerminate() == 0, "Terminate"); + Require(sceWebBrowserDialogInitialize() == 0, "Initialize again"); + Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after Terminate"); +} + +} + +int main() { + auto cookie = Cookie("https://example.com", "a=b"); + auto reset = Reset(); + Require(sceWebBrowserDialogSetCookie(&cookie) == NOT_INITIALIZED, "SetCookie before Initialize"); + Require(sceWebBrowserDialogSetCookie(nullptr) == NOT_INITIALIZED, "SetCookie null before Initialize"); + Require(sceWebBrowserDialogResetCookie(&reset) == NOT_INITIALIZED, "ResetCookie before Initialize"); + Require(sceWebBrowserDialogResetCookie(nullptr) == NOT_INITIALIZED, "ResetCookie null before Initialize"); + + Require(sceWebBrowserDialogInitialize() == 0, "Initialize"); + CheckSetCookieParameters(); + CheckResetCookieParameters(); + CheckOpenDialog(); + Require(sceWebBrowserDialogTerminate() == 0, "final Terminate"); + std::puts("guest web browser dialog cookies: PASS"); + return 0; +} diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index 3d44927711..e5ee1dde70 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -4,6 +4,7 @@ - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The real export takes a second argument, the allow-list of up to twenty addresses (SharpProspero `docs/dialogs.md`); it is not read here, so no address filtering is modelled, and the exact validation order and codes with a null second argument are unverified. - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogClose` transitions an initialized dialog directly to `COMMON_DIALOG_STATUS_FINISHED` even when called before `Open`, following the Signin / NetCtlAp / PlayerSelection convention; this behavior is unverified against real hardware. +- [sceWebBrowserDialogSetCookie, sceWebBrowserDialogResetCookie](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - there is no browser cookie store. Both calls check their parameters in the same order and with the same codes as the PS5 12.00 module, then return 0 without storing or clearing anything. On the console they then forward the request to the browser service. While an opened dialog has not been updated to finished, closed or terminated, they return `0x80B80006` (the console's open-dialog flag). ### Unknown function info From c680e28f4f1feb02466acb85570c9859e069fb32 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 18:26:47 +1300 Subject: [PATCH 0251/1360] fix(libkernel): give sceCoredumpSetUserDataType its signature and result The call threw for lack of a signature. In the PS5 12.00 libkernel it takes one int, passes it to syscall 601 (command 29) and returns the raw kernel result. No dump is produced here, so it returns 0, as sceCoredumpConfigDumpMode does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libkernel/System/src/Coredump.cpp | 4 ++-- core/libs/tests/GuestCoredumpUserData.cpp | 2 ++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 3bc229914c..c1a74bd344 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -69,8 +69,8 @@ int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpSetUserDataType(void) { - NotImplemented_nid_no_patch("Uxqkdta7wEg"); +int APS5_VABI sceCoredumpSetUserDataType(int type) { + (void)type; return 0; } diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index 30776ebbcc..bc23674815 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -14,6 +14,7 @@ int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name); +int APS5_VABI sceCoredumpSetUserDataType(int type); } namespace { @@ -61,6 +62,7 @@ int main() { const char text[] = "coredump text\n"; Require(NotInCoredumpHandler < 0); + for (const int type : {0, 1, 15, -1, 0x7fffffff}) Require(sceCoredumpSetUserDataType(type) == 0); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0aa068f431..27cd3a38fa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -5,6 +5,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority +- [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 12.00 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) From 054f8ff4de13a9bcd8329e762d8f38a609d74fc4 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 11:16:03 +0700 Subject: [PATCH 0252/1360] fix(shader): index tessellation attributes at a constant address with constants TessellationPointer built the vertex, attribute and component indices of an attribute access from its address with OpUDiv, OpUMod, OpShiftRightLogical and OpBitwiseAnd even when the address was an immediate, and without SPIRV-Tools nothing folded them. AMD's Windows driver (32.0.21045.5002 on an RX 6700 XT, 32.0.21042.62 on an RDNA 2 iGPU) reads a per-vertex hull output indexed that way wrongly: a merged hull program's level read came back wrong, and agc_driver_tessellation drew a different wrong patch on each run. A read of the program's own record through a constant index failed the same way, and the test passes once the indices are OpConstants, as glslang emits them. The merged hull case now also checks that no access chain index of the hull stage is arithmetic on constants, so the shape is caught on every device. Fixes #8861 Co-Authored-By: Claude --- .../tests/execution/Tessellation.cpp | 24 +++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 14 +++++++++++ 2 files changed, 38 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 87c935f0d2..8eee7b840b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -946,6 +947,28 @@ void RequireMergedHullWithoutSubgroups(const ShaderRecompiler::SpirvTarget& targ } } +void RequireMergedHullConstantIndices(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles); + const auto words = hull.spirv.Words(); + std::unordered_set constants; + std::unordered_set folded; + for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + Require(count != 0u && at + count <= words.size(), "the merged hull program compiled to a malformed SPIR-V instruction"); + const auto opcode = words[at] & 0xffffu; + if (opcode == spv::OpConstant && count >= 3u) constants.insert(words[at + 2u]); + const bool integer = opcode == spv::OpIAdd || opcode == spv::OpIMul || opcode == spv::OpUDiv || opcode == spv::OpUMod || opcode == spv::OpShiftRightLogical || opcode == spv::OpBitwiseAnd; + const auto operand = [&](std::uint32_t id) { return constants.contains(id) || folded.contains(id); }; + if (integer && count == 5u && operand(words[at + 3u]) && operand(words[at + 4u])) folded.insert(words[at + 2u]); + if (opcode == spv::OpAccessChain) { + for (std::uint32_t index = 4u; index < count; ++index) { + Require(!folded.contains(words[at + index]), "the merged hull program indexes a tessellation attribute with arithmetic on constants instead of a constant, which AMD's Windows driver reads wrongly"); + } + } + } +} + void RequireRegisteredMergedStages() { const auto low = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 8u); }; const auto high = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 40u); }; @@ -1003,6 +1026,7 @@ int main() { ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullOutsidePatch, "reads LDS outside its patch's control point records", MergedTriangles); ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullReadLane, "another lane's register", MergedTriangles); RequireMergedHullWithoutSubgroups(target); + RequireMergedHullConstantIndices(target); RequirePatches(*device, target, MergedHullCode, "merged hull program", MergedTriangles, MergedHullCode); RequirePatches(*device, target, MergedHullRegisters, "merged hull program storing its local part's registers", MergedTriangles, MergedHullRegisters); RequireCounterClockwise(*device, target); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 8449663a77..0ef03a1698 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -811,6 +811,20 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); return pointer; } + if (inst.Argument(1)->HasImmediate()) { + const auto value = inst.Argument(1)->ImmediateU32(); + const auto offset = patch ? value : value % stride; + const auto attribute = ConstantU32(state, offset >> 4u); + const auto component = ConstantU32(state, (offset >> 2u) & 3u); + if (kind == TessellationAttribute::LocalOutput || patch) { + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); + return pointer; + } + const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); + const auto controlPoint = kind == TessellationAttribute::ControlInput ? std::min(value / stride, state.inputInfo.vertex->tess.inputControlPoints - 1u) : value / stride; + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, own ? EmitLaneId(state) : ConstantU32(state, controlPoint), attribute, component); + return pointer; + } const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); const auto attribute = EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4u)); const auto component = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2u)), ConstantU32(state, 3u)); From df2e2760c9c0570e9b8996fe284643f26f2c8006 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 18:32:26 +1300 Subject: [PATCH 0253/1360] feat(libSceHmd2): answer the gaze results without an open headset as the console does sceHmd2GazeGetResult and sceHmd2GazeGetResultForFoveatedRendering threw. In the PS5 12.00 module both check for null arguments (0x8a720008) and the sizes of the parameter (0x10) and result (0x38 or 0x18; 0x8a720009), then, for a handle that is not the open headset, fill the result with an invalid record and return 0x8a720003. libSceHmd2 never opens a headset here, so that is the answer; an open handle still throws. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceHmd2/CMakeLists.txt | 12 +++ core/libs/prx/libSceHmd2/Export.cpp | 42 ++++++++-- core/libs/tests/GuestHmd2Gaze.cpp | 106 ++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestHmd2Gaze.cpp diff --git a/core/libs/prx/libSceHmd2/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index fc0c32cbf4..6d7565ac4a 100644 --- a/core/libs/prx/libSceHmd2/CMakeLists.txt +++ b/core/libs/prx/libSceHmd2/CMakeLists.txt @@ -14,3 +14,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_hmd2_gaze_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmd2Gaze.cpp) + target_include_directories(guest_hmd2_gaze_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hmd2_gaze_tests PRIVATE libSceHmd2 libc) + configure_windows_unwind(guest_hmd2_gaze_tests) + add_test(NAME guest_hmd2_gaze COMMAND guest_hmd2_gaze_tests) + if(WIN32) + set_tests_properties(guest_hmd2_gaze PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index f6f53d804c..17a1809ffd 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -94,14 +94,46 @@ std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { return 0; } -int APS5_VABI sceHmd2GazeGetResult() { - NotImplemented_nid_no_patch(__func__); +struct Hmd2GazeParam { + std::uint32_t size; + std::int32_t handle; + std::uint64_t reserved; +}; + +static std::int32_t Hmd2GazeHandle(const Hmd2GazeParam* param, const void* result, std::uint32_t resultSize, const char* name) { + if (!param || !result) return Hmd2::ParameterNull; + std::uint32_t size; + std::memcpy(&size, result, sizeof(size)); + if (param->size != sizeof(Hmd2GazeParam) || size != resultSize) return Hmd2::ParameterInvalid; + std::lock_guard lock(stateMutex); + if (openHandle == 0 || param->handle != openHandle) return Hmd2::InvalidHandle; + NotImplemented_nid_no_patch(name); return 0; } -int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { - NotImplemented_nid_no_patch(__func__); - return 0; +static void Hmd2StoreInvalidGaze(std::byte* result, std::size_t offset) { + const std::int32_t invalidIndex = -1; + const float invalidPair[2] = {-0.0f, 0.0f}; + const float invalidScalar = -1.0f; + std::memcpy(result + 4, &invalidIndex, sizeof(invalidIndex)); + std::memcpy(result + offset, invalidPair, sizeof(invalidPair)); + std::memcpy(result + offset + sizeof(invalidPair), &invalidScalar, sizeof(invalidScalar)); +} + +std::int32_t APS5_VABI sceHmd2GazeGetResult(const Hmd2GazeParam* param, void* result) { + const std::int32_t status = Hmd2GazeHandle(param, result, 0x38, __func__); + if (status != Hmd2::InvalidHandle) return status; + auto* bytes = static_cast(result); + std::memset(bytes + 4, 0, 0x38 - 4); + Hmd2StoreInvalidGaze(bytes, 0x14); + return status; +} + +std::int32_t APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const Hmd2GazeParam* param, void* result) { + const std::int32_t status = Hmd2GazeHandle(param, result, 0x18, __func__); + if (status != Hmd2::InvalidHandle) return status; + Hmd2StoreInvalidGaze(static_cast(result), 8); + return status; } std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { diff --git a/core/libs/tests/GuestHmd2Gaze.cpp b/core/libs/tests/GuestHmd2Gaze.cpp new file mode 100644 index 0000000000..e3cce92206 --- /dev/null +++ b/core/libs/tests/GuestHmd2Gaze.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GazeParam { + std::uint32_t size; + std::int32_t handle; + std::uint64_t reserved; +}; + +extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param); +std::int32_t APS5_VABI sceHmd2GazeGetResult(const GazeParam* param, void* result); +std::int32_t APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const GazeParam* param, void* result); +} + +namespace { + +constexpr std::int32_t InvalidHandle = static_cast(0x8a720003); +constexpr std::int32_t ParameterNull = static_cast(0x8a720008); +constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +template +T Read(const unsigned char* bytes, std::size_t offset) { + T value; + std::memcpy(&value, bytes + offset, sizeof(value)); + return value; +} + +bool NegativeZero(float value) { + return value == 0.0f && std::signbit(value); +} + +void CheckGazeResult() { + GazeParam param {sizeof(GazeParam), 0x0e010000, 0}; + unsigned char result[0x38]; + std::memset(result, 0x5a, sizeof(result)); + const std::uint32_t size = 0x38; + std::memcpy(result, &size, sizeof(size)); + + Require(sceHmd2GazeGetResult(nullptr, result) == ParameterNull, "null param"); + Require(sceHmd2GazeGetResult(¶m, nullptr) == ParameterNull, "null result"); + GazeParam wrongParam = param; + wrongParam.size = 0x18; + Require(sceHmd2GazeGetResult(&wrongParam, result) == ParameterInvalid, "param size"); + const std::uint32_t wrongSize = 0x18; + std::memcpy(result, &wrongSize, sizeof(wrongSize)); + Require(sceHmd2GazeGetResult(¶m, result) == ParameterInvalid, "result size"); + Require(Read(result, 4) == 0x5a5a5a5a, "result untouched after a size error"); + + std::memcpy(result, &size, sizeof(size)); + Require(sceHmd2GazeGetResult(¶m, result) == InvalidHandle, "unopened handle"); + Require(Read(result, 0) == 0x38, "size kept"); + Require(Read(result, 4) == -1, "+4 is -1"); + for (const std::size_t offset : {0x8u, 0xcu, 0x10u, 0x20u, 0x24u, 0x28u, 0x2cu, 0x30u, 0x34u}) + Require(Read(result, offset) == 0, "cleared word"); + Require(NegativeZero(Read(result, 0x14)), "+0x14 is -0.0"); + Require(Read(result, 0x18) == 0, "+0x18 is 0.0"); + Require(Read(result, 0x1c) == -1.0f, "+0x1c is -1.0"); + + GazeParam zero {sizeof(GazeParam), 0, 0}; + Require(sceHmd2GazeGetResult(&zero, result) == InvalidHandle, "handle 0"); +} + +void CheckFoveatedResult() { + GazeParam param {sizeof(GazeParam), 0x0e010000, 0}; + unsigned char result[0x18]; + std::memset(result, 0x5a, sizeof(result)); + const std::uint32_t size = 0x18; + std::memcpy(result, &size, sizeof(size)); + + Require(sceHmd2GazeGetResultForFoveatedRendering(nullptr, result) == ParameterNull, "foveated null param"); + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, nullptr) == ParameterNull, "foveated null result"); + unsigned char large[0x38] = {}; + const std::uint32_t largeSize = 0x38; + std::memcpy(large, &largeSize, sizeof(largeSize)); + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, large) == ParameterInvalid, "foveated result size"); + + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, result) == InvalidHandle, "foveated unopened handle"); + Require(Read(result, 0) == 0x18, "foveated size kept"); + Require(Read(result, 4) == -1, "foveated +4 is -1"); + Require(NegativeZero(Read(result, 8)), "foveated +8 is -0.0"); + Require(Read(result, 0xc) == 0, "foveated +0xc is 0.0"); + Require(Read(result, 0x10) == -1.0f, "foveated +0x10 is -1.0"); + Require(Read(result, 0x14) == 0x5a5a5a5a, "foveated +0x14 untouched"); +} + +} + +int main() { + Require(sceHmd2Initialize(nullptr) != 0, "initialize reports no headset"); + CheckGazeResult(); + CheckFoveatedResult(); + std::puts("guest Hmd2 gaze results without a headset: PASS"); + return 0; +} From c12ca91ed5fc125b9663495d5063ea7553727cc7 Mon Sep 17 00:00:00 2001 From: DProkos Date: Wed, 21 Oct 2026 08:40:39 +0300 Subject: [PATCH 0254/1360] docs(libSceNpCppWebApi): correct the silent stub counts and drop the title ID Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpCppWebApi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 7583e31fe5..e52a3699ac 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (15 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (13 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state - [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` - [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` - [sce::Np::CppWebApi::TitleCloudStorage::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData::{initialize,terminate}` succeed without storing state @@ -15,5 +15,5 @@ - [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. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count - [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object -- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by the Epic Online Services module of PPSA24003, whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response +- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 73 exports are named from aerolib, so each name recomputes to the NID a title imports (`tools/nid_names.py`), and each throws. They are the `Common` template members (`IntrusivePtr`, `Vector`, `Iterator`, `ConstIterator`, `Transaction`, `TransactionBase`) and the `TitleCloudStorage::V1` and `InGameCatalog::V5` accessors: a mangled name gives the class, the method and the argument types, but the layout of the object the call is made on, the values the caller reads back and the console's errors are unknown, so no call can be answered without writing to memory no source describes. No call site is recorded for any of them From 765058d430d79038ca786ebed9807d1447458125 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 18:47:46 +1300 Subject: [PATCH 0255/1360] feat(libSceAudioIn): open ports through the Ex calls and accept the two setters sceAudioInOpenEx and sceAudioInHqOpenEx threw. In the PS5 12.00 module both call the shared open routine that sceAudioInOpen uses, with one extra argument in fourth place that only reaches the console's Mbus handle registration. HqOpenEx sets the high-quality flag but skips the length, rate and parameter checks of sceAudioInHqOpen, so both open like sceAudioInOpen here. sceAudioInSetCompressorPreGain and sceAudioInSetNoiseGateThreshold return 0 on the console without reading their arguments. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAudioIn/Export.cpp | 14 ++++++-------- core/libs/tests/GuestAudioIn.cpp | 13 +++++++++++++ docs/dev/debt/libSceAudioIn.md | 6 ++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 68fa257250..72eaba5300 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -258,14 +258,14 @@ int APS5_VABI sceAudioInGetHandleStatusInfo(void) { return 0; } -int APS5_VABI sceAudioInOpenEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioInOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) { + (void)extra; + return sceAudioInOpen(user_id, type, index, len, freq, param); } -int APS5_VABI sceAudioInHqOpenEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioInHqOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) { + (void)extra; + return sceAudioInOpen(user_id, type, index, len, freq, param); } int APS5_VABI sceAudioInDeviceOpenEx(void) { @@ -289,12 +289,10 @@ int APS5_VABI sceAudioInSetSparkSideTone(void) { } int APS5_VABI sceAudioInSetCompressorPreGain(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index 124bc9a880..bad2d3cf94 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -16,6 +16,10 @@ int APS5_VABI sceAudioInInit(); int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInOpenEx(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInHqOpenEx(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInSetCompressorPreGain(); +int APS5_VABI sceAudioInSetNoiseGateThreshold(); int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t); int32_t APS5_VABI sceAudioInGetGain(int32_t, float*); int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t); @@ -105,6 +109,15 @@ void TestValidation() { port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 0x10); gain = 0.5f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); + Require(sceAudioInOpenEx(user, 2, 0, 7, 256, 48000, 2) == invalidType); + Require(sceAudioInOpenEx(user, 0, 0, 7, 512, 48000, 2) == invalidSize); + Require(sceAudioInOpenEx(user, 0, 0, 7, 256, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpenEx(user, 0, 0, 7, 128, 44100, 2) == invalidFreq); + port = sceAudioInOpenEx(user, 0, 0, 7, 256, 48000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + port = sceAudioInHqOpenEx(user, 0, 0, 7, 256, 16000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + Require(sceAudioInSetCompressorPreGain() == 0 && sceAudioInSetNoiseGateThreshold() == 0); Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); } diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 04f555333b..7802cc1d62 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -17,14 +17,12 @@ - [sceAudioInSetPortConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetPortStatuses](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInGetHandleStatusInfo](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInHqOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInDeviceOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one - [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature From 51c5f528be6dce945f71aab2842284615bfb7ea2 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 18:56:28 +1300 Subject: [PATCH 0256/1360] feat(audio3d): forward the speaker-array calls to AudioOut2 sceAudio3dGetSpeakerArrayMemorySize, CreateSpeakerArray, DeleteSpeakerArray and GetSpeakerArrayMixCoefficients(2) forward to the AudioOut2 speaker-array calls as the console does: the memory size asks for no ambisonics, create rejects a null handle or parameters with SCE_AUDIO3D_ERROR_INVALID_PARAMETER and passes no ambisonics parameters, and MixCoefficients passes height_aware 0 and a 0.1 downmix spread radius. AudioOut2 create no longer rejects a null ambisonics pointer, which the console accepts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAudio3d/CMakeLists.txt | 9 +++ core/libs/prx/libSceAudio3d/Export.cpp | 43 +++++++---- .../src/AudioOut2SpeakerArray.cpp | 2 +- core/libs/tests/GuestAudio3dSpeakerArray.cpp | 75 +++++++++++++++++++ docs/dev/debt/libSceAudio3d.md | 6 +- 5 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 core/libs/tests/GuestAudio3dSpeakerArray.cpp diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 791d2fac7e..2d4e024734 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_speaker_array_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dSpeakerArray.cpp) + target_include_directories(guest_audio3d_speaker_array_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio3d_speaker_array_tests PRIVATE libSceAudio3d libSceAudioOut libc) + configure_windows_unwind(guest_audio3d_speaker_array_tests) + add_test(NAME guest_audio3d_speaker_array COMMAND guest_audio3d_speaker_array_tests) + if(WIN32) + set_tests_properties(guest_audio3d_speaker_array PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) target_include_directories(guest_audio3d_mixer_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 28854734cc..aa5334abf2 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -477,29 +477,42 @@ int APS5_VABI sceAudio3dReportUnregisterHandler(void) { return 0; } -int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics); +int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params); +int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle); +int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius); + +struct Audio3dSpeakerArrayParameters { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; +}; + +static_assert(sizeof(Audio3dSpeakerArrayParameters) == 0x20); + +size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d) { + return sceAudioOut2GetSpeakerArrayMemorySize(num_speakers, is_3d, 0); } -int APS5_VABI sceAudio3dCreateSpeakerArray(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters) { + if (!handle || !parameters) return AUDIO3D_ERROR_INVALID_PARAMETER; + unsigned char extended[0x28] = {}; + std::memcpy(extended, parameters, sizeof(*parameters)); + return sceAudioOut2SpeakerArrayCreate(handle, extended, nullptr); } -int APS5_VABI sceAudio3dDeleteSpeakerArray(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle) { + return sceAudioOut2SpeakerArrayDestroy(handle); } -int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients) { + return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, 0, 0.1f); } -int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius) { + return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, height_aware, downmix_spread_radius); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 26170f4622..7b6a07017e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -68,7 +68,7 @@ extern "C" { int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) { const auto* params = static_cast(vbap_params); - if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr || + if (handle == nullptr || params == nullptr || params->positions == nullptr || params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr || params->memorySize < sizeof(SpeakerArray) || reinterpret_cast(params->memory) % alignof(SpeakerArray) != 0) { APS5_INVALID_ARG_EX; diff --git a/core/libs/tests/GuestAudio3dSpeakerArray.cpp b/core/libs/tests/GuestAudio3dSpeakerArray.cpp new file mode 100644 index 0000000000..7ef0c9942e --- /dev/null +++ b/core/libs/tests/GuestAudio3dSpeakerArray.cpp @@ -0,0 +1,75 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +struct Audio3dSpeakerArrayParameters { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; +}; + +extern "C" { +size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(std::uint32_t num_speakers, std::uint8_t is_3d); +int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters); +int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle); +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, std::uint32_t num_coefficients); +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, std::uint32_t num_coefficients, std::uint8_t height_aware, float downmix_spread_radius); +size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(std::uint32_t num_speakers, std::uint8_t is_3d, std::uint8_t is_ambisonics); +} + +namespace { + +constexpr int INVALID_PARAMETER = static_cast(0x80EA0004); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +void RequireThrows(F f, const char* message) { + bool threw = false; + try { + f(); + } catch (const std::exception&) { + threw = true; + } + Require(threw, message); +} + +} + +int main() { + Require(sceAudio3dGetSpeakerArrayMemorySize(4, 1) == sceAudioOut2GetSpeakerArrayMemorySize(4, 1, 0), "memory size forwards to AudioOut2 without ambisonics"); + + const AudioOut2Position positions[4] = {{1, 1, 0}, {1, -1, 0}, {-1, 1, 0}, {-1, -1, 0}}; + alignas(16) unsigned char memory[1024] = {}; + const std::size_t size = sceAudio3dGetSpeakerArrayMemorySize(4, 0); + Require(size <= sizeof(memory), "speaker array fits the test buffer"); + const Audio3dSpeakerArrayParameters parameters{positions, 4, 0, memory, size}; + + AudioOut2SpeakerArrayHandle handle = nullptr; + Require(sceAudio3dCreateSpeakerArray(nullptr, ¶meters) == INVALID_PARAMETER, "null handle is an invalid parameter"); + Require(sceAudio3dCreateSpeakerArray(&handle, nullptr) == INVALID_PARAMETER, "null parameters are an invalid parameter"); + Require(handle == nullptr, "a rejected create leaves the handle"); + Require(sceAudio3dCreateSpeakerArray(&handle, ¶meters) == 0, "create succeeds without ambisonics parameters"); + Require(handle != nullptr, "create stores the handle"); + + float coefficients[4] = {-1, -1, -1, -1}; + Require(sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 4) == 0, "mix coefficients succeed"); + Require(coefficients[2] == 0.0f && coefficients[3] == 0.0f, "mix coefficients are written"); + float coefficients2[4] = {-1, -1, -1, -1}; + Require(sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients2, 4, 1, 0.5f) == 0, "mix coefficients 2 succeed"); + Require(coefficients2[3] == 0.0f, "mix coefficients 2 are written"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3); }, "a coefficient count that differs from the array is rejected"); + + Require(sceAudio3dDeleteSpeakerArray(handle) == 0, "delete succeeds"); + RequireThrows([&] { sceAudio3dDeleteSpeakerArray(handle); }, "a deleted array is no longer valid"); + return 0; +} diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index c7d94441a4..56dba9367c 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -20,11 +20,7 @@ - [sceAudio3dPortGetStatus](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dReportRegisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dReportUnregisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dGetSpeakerArrayMemorySize](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dDeleteSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dGetSpeakerArrayMixCoefficients](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dGetSpeakerArrayMixCoefficients2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the speaker-array calls return the AudioOut2 result unchanged: the console translates AudioOut2 speaker-array errors into Audio3d codes, but this project's AudioOut2 speaker-array calls throw instead of returning error codes, so there is nothing to translate yet - [Audio3d bed downmix coefficients](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the centre and the surrounds fold into stereo with a 0.7071 gain and the LFE channel is dropped, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4): its `DOWNMIX_CENTER` and `DOWNMIX_SURROUND` are 0.7071 and its `DOWNMIX_LFE` is 0, so the `if constexpr` that adds the LFE is compiled out. [prosper](https://github.com/mattias800/prosper) disagrees on the LFE only: it folds it into both sides with a 0.5 gain, in its AudioOut2 main bed path, which is not an Audio3d path. KytyPS5 has no Audio3d mixing at all. The absolute level is unmeasured: fpPS4 scales the front by 1 + 3/sqrt(2) instead of 1 - [Audio3d bed channel order](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the buffer is read as L, R, C, LFE, Ls, Rs, Lrs, Rrs, the standard order, which three references agree on: [shadPS4](https://github.com/shadps4-emu/shadPS4) reads the front, centre, LFE and surround samples at those indices, [prosper](https://github.com/mattias800/prosper) documents FL, FR, FC, LFE and then surround pairs, and fpPS4 names them `_FL=0 _FR=1 _FC=2 _LF=3 _SL=4 _SR=5 _BL=6 _BR=7`. fpPS4's `STD` layout swaps the side and back pairs, which this library does not model - [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the return is the number of frames per channel in the queued block, which follows [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [prosper](https://github.com/mattias800/prosper), whose `sceAudioOutOutput` returns the grain, and this project's own `sceAudioOutOutput`, which returns the port's `samplesNum` (the buffer length in frames). [shadPS4](https://github.com/shadps4-emu/shadPS4) returns the total samples instead (`len * channels`) and fpPS4 returns 0; no console measurement settles it From a2a77714d5293fe364270f55349b553bf5f9a4c4 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:53:21 +0200 Subject: [PATCH 0257/1360] fix(libkernel): fail unlink and rmdir of a path that ends in a dot component with EINVAL FreeBSD lookup() refuses "." and ".." as the last component of a DELETE lookup with EINVAL (sys/kern/vfs_lookup.c:750-754 and 823-826), before the unlink and rmdir checks run. unlink and sceKernelUnlink answered EPERM, rmdir and sceKernelRmdir answered ENOTEMPTY for "..", and unlinkat without AT_REMOVEDIR answered EPERM. Reuse the RenameDot check added for rename, so a missing or non-directory parent still reports ENOENT or ENOTDIR first. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 2 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/prx/libkernel_sys/Export.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 11 +++++++++++ core/libs/tests/GuestKernelSys.cpp | 3 ++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index e028dbbc28..660ea75d68 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" @@ -416,6 +417,7 @@ int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, int APS5_VABI sceKernelUnlink(const char* path) { if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); + if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2146bbe2d6..97b7843b04 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1447,6 +1447,7 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { int APS5_VABI sceKernelRmdir(const char* path) { if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; @@ -1456,7 +1457,6 @@ int APS5_VABI sceKernelRmdir(const char* path) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); } - if (FinalPathComponentIsDot_nid_no_patch(path)) return SceErrorFromErrno(GUEST_EINVAL); if (!std::filesystem::is_empty(native, error)) return SceErrorFromErrno(GUEST_ENOTEMPTY); if (!RemoveEmptyDirectory(native)) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(native); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..1b32bd1645 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -576,8 +576,8 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code statusError; const auto symlinkStatus = std::filesystem::symlink_status(native, statusError); + if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (flag & GuestAtRemovedir) { - if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); if (!std::filesystem::is_directory(symlinkStatus)) { if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 92fc0a42ca..ead96ec602 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -248,6 +248,17 @@ static void TestParentTraversal() { Require(stat_nid_postfix("/parent-test-mount/missing/.", &status) == -1 && *__error_nid_postfix() == 2); Require(stat_nid_postfix("/parent-test-mount/existing/.", &status) == 0 && (status.st_mode & 0170000) == 0040000); Require(access_nid_postfix("/parent-test-mount/existing/./", 0) == 0); + std::filesystem::create_directory(root / "dot-victim"); + for (const char* name : {"/parent-test-mount/dot-victim/.", "/parent-test-mount/dot-victim/..", "/parent-test-mount/dot-victim/./", "/parent-test-mount/dot-victim/../"}) { + Require(unlink_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); + Require(rmdir_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelUnlink(name) == static_cast(0x80020016u)); + Require(sceKernelRmdir(name) == static_cast(0x80020016u)); + } + Require(unlink_nid_postfix("/parent-test-mount/present.txt/.") == -1 && *__error_nid_postfix() == 20); + Require(rmdir_nid_postfix("/parent-test-mount/missing/..") == -1 && *__error_nid_postfix() == 2); + Require(std::filesystem::is_directory(root / "dot-victim") && std::filesystem::is_regular_file(root / "present.txt")); + std::filesystem::remove(root / "dot-victim"); #ifndef _WIN32 for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a60abca020..8de31c867d 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -546,7 +546,8 @@ int main() { Require(unlinkat_nid_postfix(base, "file.txt", 0x800) == -1 && Error() == 20); Require(unlinkat_nid_postfix(base, ".", 0x800) == -1 && Error() == 22); Require(unlinkat_nid_postfix(base, "..", 0x800) == -1 && Error() == 22); - Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 1); + Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, "..", 0) == -1 && Error() == 22); std::filesystem::create_directory(root / "usub"); Require(unlinkat_nid_postfix(base, "usub", 0) == -1 && Error() == 1); std::filesystem::create_directory(root / "nonempty"); From 288bd35d0dd98e226dfa44b110966739323caf22 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:23:55 +0200 Subject: [PATCH 0258/1360] fix(libc): resolve the parent of a dot component without its trailing separator RenameDotError_nid_no_patch resolved the parent with the separator kept ("file/"). On Linux std::filesystem::exists("file/") is false for a regular file, so unlink, rmdir and rename of "file/." answered ENOENT instead of ENOTDIR. Trim the trailing separators first (a lone "/" stays) and cover rename of "file/." and "file/.." in guest_filesystem. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/RenameDot.hpp | 3 ++- core/libs/tests/GuestFilesystem.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp index 4364a80d39..ff2622b08c 100644 --- a/core/libs/prx/libc/include/RenameDot.hpp +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -15,7 +15,8 @@ inline int RenameDotError_nid_no_patch(const char* path) { const bool dot = name == 1 && path[start] == '.'; const bool dotDot = name == 2 && path[start] == '.' && path[start + 1] == '.'; if (!dot && !dotDot) return 0; - const std::string parent = start == 0 ? std::string(".") : std::string(path, start); + std::string parent = start == 0 ? std::string(".") : std::string(path, start); + while (parent.size() > 1 && separator(parent.back())) parent.pop_back(); std::filesystem::path resolved; if (const int error = ResolvePathError_nid_no_patch(parent.c_str(), resolved)) return error; std::error_code status; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index ead96ec602..9e39742add 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -256,6 +256,8 @@ static void TestParentTraversal() { Require(sceKernelRmdir(name) == static_cast(0x80020016u)); } Require(unlink_nid_postfix("/parent-test-mount/present.txt/.") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/present.txt/.", "/parent-test-mount/dot-renamed") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/present.txt/..", "/parent-test-mount/dot-renamed") == -1 && *__error_nid_postfix() == 20); Require(rmdir_nid_postfix("/parent-test-mount/missing/..") == -1 && *__error_nid_postfix() == 2); Require(std::filesystem::is_directory(root / "dot-victim") && std::filesystem::is_regular_file(root / "present.txt")); std::filesystem::remove(root / "dot-victim"); From fa6f246a9b1695c03dd8ada3e987096b635a0a90 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 01:59:31 +0700 Subject: [PATCH 0259/1360] fix(shader): flush the operands of image, buffer and global float atomics On RDNA2 the image, buffer and global float atomics (fmin, fmax, fcmpswap and the _x2 forms) flush both operands to a signed zero when bit 0 of the shader's denormal field is clear, as the LDS forms do: the f32 field for the 32-bit forms, the f16/f64 field for _x2. Only the LDS forms flushed here. floatAtomicFlush (was sharedFloatFlush) now sets MemoryInfo::flushDenormals for every float atomic, and AtomicFloatBits applies it in minMax and compareSwap. The float atomic tests from #332, #506 and #562 were measured with denormals kept, so they now set FLOAT_MODE 0xf0. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/BufferAtomicsFloat.cpp | 1 + .../tests/execution/GlobalAtomicsFloat.cpp | 1 + .../tests/execution/ImageAtomicsFloat.cpp | 1 + .../execution/MemoryFloatAtomicModes.cpp | 452 ++++++++++++++++++ .../SpirvBackend/SpirvEmitterHelpers.hpp | 4 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 4 +- .../SpirvMemory/SpirvMemoryInstructions.cpp | 66 +-- .../Translation/TranslationContext.hpp | 2 +- .../MemoryInstructions/BufferInstructions.cpp | 3 +- .../MemoryInstructions/FlatInstructions.cpp | 3 +- .../MemoryInstructions/ImageInstructions.cpp | 3 +- .../MemoryResourceHelpers.cpp | 31 ++ .../SharedMemoryInstructions.cpp | 22 +- docs/dev/debt/shader.md | 5 +- 14 files changed, 538 insertions(+), 60 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp index 99a286d6ac..01b015a01b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp @@ -149,6 +149,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp index 503ccf662f..b4827a3b53 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp @@ -684,6 +684,7 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const std {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp index a7b492f8e8..78b36356c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp @@ -504,6 +504,7 @@ ShaderRecompiler::RecompileResult Compile(std::uint32_t waveSize, const ShaderRe {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}; return ShaderRecompiler::Recompile(request); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp new file mode 100644 index 0000000000..aa4f0afb67 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp @@ -0,0 +1,452 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 12; +constexpr std::uint32_t Results = 32; +constexpr std::uint32_t TargetDwords = 512; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t BlockBytes = 4096; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array BufferCode{ + 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, + 0xbf8c3f70u, 0x341a0082u, 0x4a1a1affu, 0x00001000u, 0x341c0083u, 0x4a1c1cffu, 0x00001200u, 0xe0701000u, + 0x8001020du, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xe0fc5000u, 0x8001100du, 0xbf8c3f70u, 0xe0305000u, + 0x8001120du, 0xbf8c3f70u, 0xe0701000u, 0x8001100cu, 0xe0701004u, 0x8001120cu, 0xe0701080u, 0x8001020du, + 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xe1005080u, 0x8001100du, 0xbf8c3f70u, 0xe0305080u, 0x8001120du, + 0xbf8c3f70u, 0xe0701008u, 0x8001100cu, 0xe070100cu, 0x8001120cu, 0xe0701100u, 0x8001020du, 0xbbfd0000u, + 0x7e200303u, 0x7e220304u, 0xe0f85100u, 0x8001100du, 0xbf8c3f70u, 0xe0305100u, 0x8001120du, 0xbf8c3f70u, + 0xe0701010u, 0x8001100cu, 0xe0701014u, 0x8001120cu, 0xe0741000u, 0x8001060eu, 0xbbfd0000u, 0x7e280308u, + 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xe17c5000u, 0x8001140eu, 0xbf8c3f70u, 0xe0345000u, 0x8001180eu, + 0xbf8c3f70u, 0xe0741018u, 0x8001140cu, 0xe0741020u, 0x8001180cu, 0xe0741100u, 0x8001060eu, 0xbbfd0000u, + 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xe1805100u, 0x8001140eu, 0xbf8c3f70u, 0xe0345100u, + 0x8001180eu, 0xbf8c3f70u, 0xe0741028u, 0x8001140cu, 0xe0741030u, 0x8001180cu, 0xe0741200u, 0x8001060eu, + 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xe1785200u, 0x8001140eu, 0xbf8c3f70u, + 0xe0345200u, 0x8001180eu, 0xbf8c3f70u, 0xe0741038u, 0x8001140cu, 0xe0741040u, 0x8001180cu, 0xbf810000u, +}; + +alignas(256) constexpr std::array ImageCode{ + 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, + 0xbf8c3f70u, 0x7e3c0300u, 0x7e3e0280u, 0x7e260302u, 0xf03c0108u, 0x0002131eu, 0x7e200303u, 0x7e220304u, + 0xf0782108u, 0x0002101eu, 0x7e240280u, 0xf0442108u, 0x0002121eu, 0xbf8c3f70u, 0xe0701000u, 0x8001100cu, + 0xe0701004u, 0x8001120cu, 0x7e3e0281u, 0x7e260302u, 0xf03c0108u, 0x0002131eu, 0x7e200303u, 0x7e220304u, + 0xf07c2108u, 0x0002101eu, 0x7e240280u, 0xf0442108u, 0x0002121eu, 0xbf8c3f70u, 0xe0701008u, 0x8001100cu, + 0xe070100cu, 0x8001120cu, 0x7e3e0282u, 0x7e260302u, 0xf03c0108u, 0x0002131eu, 0x7e200303u, 0x7e220304u, + 0xf0742308u, 0x0002101eu, 0x7e240280u, 0xf0442108u, 0x0002121eu, 0xbf8c3f70u, 0xe0701010u, 0x8001100cu, + 0xe0701014u, 0x8001120cu, 0xbf810000u, +}; + +alignas(256) constexpr std::array GlobalCode{ + 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, + 0xbf8c3f70u, 0x34340082u, 0xd70f6a1au, 0x00023408u, 0x7e360209u, 0x50363680u, 0x34380083u, 0xd70f6a1cu, + 0x00023808u, 0x7e3a0209u, 0x503a3a80u, 0xdc708000u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, + 0xdcfd8000u, 0x137d101au, 0xbf8c3f70u, 0xdc318000u, 0x127d001au, 0xbf8c3f70u, 0xe0701000u, 0x8001130cu, + 0xe0701004u, 0x8001120cu, 0xdc708080u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdd018080u, + 0x137d101au, 0xbf8c3f70u, 0xdc318080u, 0x127d001au, 0xbf8c3f70u, 0xe0701008u, 0x8001130cu, 0xe070100cu, + 0x8001120cu, 0xdc708100u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdcf98100u, 0x137d101au, + 0xbf8c3f70u, 0xdc318100u, 0x127d001au, 0xbf8c3f70u, 0xe0701010u, 0x8001130cu, 0xe0701014u, 0x8001120cu, + 0xdc748200u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xdd7d8200u, + 0x207d141cu, 0xbf8c3f70u, 0xdc358200u, 0x187d001cu, 0xbf8c3f70u, 0xe0741018u, 0x8001200cu, 0xe0741020u, + 0x8001180cu, 0xdc748300u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, + 0xdd818300u, 0x207d141cu, 0xbf8c3f70u, 0xdc358300u, 0x187d001cu, 0xbf8c3f70u, 0xe0741028u, 0x8001200cu, + 0xe0741030u, 0x8001180cu, 0xdc748400u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, + 0x7e2e030bu, 0xdd798400u, 0x207d141cu, 0xbf8c3f70u, 0xdc358400u, 0x187d001cu, 0xbf8c3f70u, 0xe0741038u, + 0x8001200cu, 0xe0741040u, 0x8001180cu, 0xbf810000u, +}; + +constexpr std::uint32_t Rows32[Threads][3] = { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000000u, 0x80000001u, 0x80000001u}, + {0x00000001u, 0x00000000u, 0x00000001u}, + {0x00000001u, 0x3f800000u, 0x3f800000u}, + {0x80000001u, 0x80000000u, 0x80000000u}, + {0x00000001u, 0x80000001u, 0x80000001u}, + {0x007fffffu, 0x00800000u, 0x007fffffu}, + {0x807fffffu, 0xbf800000u, 0x807fffffu}, + {0x7fc00000u, 0x00000001u, 0x7fc00000u}, + {0x00000001u, 0x7fc00000u, 0x00000001u}, + {0x7f800001u, 0x00000001u, 0x7f800001u}, + {0x00000001u, 0x7f800001u, 0x00000001u}, + {0x3f800000u, 0xbf800000u, 0x3f800000u}, + {0x7f800000u, 0xff800000u, 0xff800000u}, + {0x80000000u, 0x00000001u, 0x80000000u}, + {0x7fc12345u, 0x7fc00000u, 0x7fc12345u}, + {0xffc00000u, 0x3f800000u, 0x3f800000u}, + {0x7fa00000u, 0x3f800000u, 0x3f800000u}, + {0x40490fdbu, 0x007fffffu, 0x40490fdbu}, + {0x807fffffu, 0x80000001u, 0x80000001u}, + {0x00800000u, 0x007fffffu, 0x00800000u}, + {0x00000000u, 0xff800001u, 0xff800001u}, + {0x80000001u, 0x00000001u, 0x80000001u}, + {0x7f7fffffu, 0xff7fffffu, 0xff7fffffu}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x007fffffu, 0x80000000u, 0x80000000u}, + {0xff800000u, 0x80000001u, 0x80000001u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x3f800000u, 0x00000001u, 0x3f800000u} +}; + +constexpr std::uint64_t Rows64[Threads][3] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x0000000000000001ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull}, + {0x8000000000000001ull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x000fffffffffffffull, 0x0010000000000000ull, 0x000fffffffffffffull}, + {0x800fffffffffffffull, 0xbff0000000000000ull, 0x800fffffffffffffull}, + {0x7ff8000000000000ull, 0x0000000000000001ull, 0x7ff8000000000000ull}, + {0x0000000000000001ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x7ff0000000000001ull, 0x0000000000000001ull, 0x7ff0000000000001ull}, + {0x0000000000000001ull, 0x7ff0000000000001ull, 0x7ff0000000000001ull}, + {0x3ff0000000000000ull, 0xbff0000000000000ull, 0x3ff0000000000000ull}, + {0x7ff0000000000000ull, 0xfff0000000000000ull, 0x7ff0000000000000ull}, + {0x8000000000000000ull, 0x0000000000000001ull, 0x8000000000000000ull}, + {0x7ff8000000012345ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0xfff8000000000000ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull}, + {0xfff0000000000001ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull}, + {0x400921fb54442d18ull, 0x000fffffffffffffull, 0x400921fb54442d18ull}, + {0x800fffffffffffffull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x0010000000000000ull, 0x000fffffffffffffull, 0x0010000000000000ull}, + {0x0000000000000000ull, 0xfff0000000000001ull, 0x0000000000000000ull}, + {0x8000000000000001ull, 0x0000000000000001ull, 0x8000000000000001ull}, + {0x7fefffffffffffffull, 0xfff0000000000000ull, 0x7fefffffffffffffull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000001ull, 0x3ff0000000000000ull} +}; + +constexpr std::uint32_t Flushed32[Threads][3] = { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000001u}, + {0x80000000u, 0x00000000u, 0x80000001u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3f800000u, 0x00000001u}, + {0x80000000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x80000001u}, + {0x00000000u, 0x00800000u, 0x00800000u}, + {0xbf800000u, 0x80000000u, 0xbf800000u}, + {0x00000000u, 0x00000000u, 0x7fc00000u}, + {0x00000000u, 0x00000000u, 0x7fc00000u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0xbf800000u, 0x3f800000u, 0xbf800000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u}, + {0x80000000u, 0x00000000u, 0x00000001u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc12345u}, + {0x3f800000u, 0x3f800000u, 0xffc00000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fa00000u}, + {0x00000000u, 0x40490fdbu, 0x007fffffu}, + {0x80000000u, 0x80000000u, 0x80000001u}, + {0x00000000u, 0x00800000u, 0x007fffffu}, + {0xffc00001u, 0xffc00001u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x00000001u}, + {0xff7fffffu, 0x7f7fffffu, 0x7f7fffffu}, + {0x00000000u, 0x00000000u, 0x00000001u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0xff800000u, 0x80000000u, 0xff800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x00000000u, 0x3f800000u, 0x00000001u} +}; + +constexpr std::uint32_t Kept32[Threads][3] = { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000001u}, + {0x80000001u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000001u, 0x3f800000u, 0x00000001u}, + {0x80000001u, 0x80000000u, 0x80000001u}, + {0x80000001u, 0x00000001u, 0x00000001u}, + {0x007fffffu, 0x00800000u, 0x00800000u}, + {0xbf800000u, 0x807fffffu, 0xbf800000u}, + {0x00000001u, 0x00000001u, 0x7fc00000u}, + {0x00000001u, 0x00000001u, 0x7fc00000u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0xbf800000u, 0x3f800000u, 0xbf800000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u}, + {0x80000000u, 0x00000001u, 0x00000001u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc12345u}, + {0x3f800000u, 0x3f800000u, 0xffc00000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fa00000u}, + {0x007fffffu, 0x40490fdbu, 0x007fffffu}, + {0x807fffffu, 0x80000001u, 0x807fffffu}, + {0x007fffffu, 0x00800000u, 0x007fffffu}, + {0xffc00001u, 0xffc00001u, 0x00000000u}, + {0x80000001u, 0x00000001u, 0x00000001u}, + {0xff7fffffu, 0x7f7fffffu, 0x7f7fffffu}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x80000000u, 0x007fffffu, 0x007fffffu}, + {0xff800000u, 0x80000001u, 0xff800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x00000001u, 0x3f800000u, 0x00000001u} +}; + +constexpr std::uint64_t Flushed64[Threads][3] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x3ff0000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000000ull, 0x0010000000000000ull, 0x0010000000000000ull}, + {0xbff0000000000000ull, 0x8000000000000000ull, 0xbff0000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x7ff8000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x7ff0000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x0000000000000001ull}, + {0xbff0000000000000ull, 0x3ff0000000000000ull, 0xbff0000000000000ull}, + {0xfff0000000000000ull, 0x7ff0000000000000ull, 0xfff0000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000012345ull}, + {0x3ff0000000000000ull, 0x3ff0000000000000ull, 0xfff8000000000000ull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x0000000000000000ull, 0x400921fb54442d18ull, 0x000fffffffffffffull}, + {0x8000000000000000ull, 0x8000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000000ull, 0x0010000000000000ull, 0x000fffffffffffffull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0xfff0000000000000ull, 0x7fefffffffffffffull, 0xfff0000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000000ull, 0xfff0000000000000ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x0000000000000000ull, 0x3ff0000000000000ull, 0x0000000000000001ull} +}; + +constexpr std::uint64_t Kept64[Threads][3] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x8000000000000001ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000000ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull, 0x0000000000000001ull}, + {0x8000000000000001ull, 0x8000000000000000ull, 0x8000000000000001ull}, + {0x8000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x0010000000000000ull, 0x0010000000000000ull}, + {0xbff0000000000000ull, 0x800fffffffffffffull, 0xbff0000000000000ull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x7ff8000000000000ull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x7ff0000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x0000000000000001ull}, + {0xbff0000000000000ull, 0x3ff0000000000000ull, 0xbff0000000000000ull}, + {0xfff0000000000000ull, 0x7ff0000000000000ull, 0xfff0000000000000ull}, + {0x8000000000000000ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000012345ull}, + {0x3ff0000000000000ull, 0x3ff0000000000000ull, 0xfff8000000000000ull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x000fffffffffffffull, 0x400921fb54442d18ull, 0x000fffffffffffffull}, + {0x800fffffffffffffull, 0x8000000000000001ull, 0x800fffffffffffffull}, + {0x000fffffffffffffull, 0x0010000000000000ull, 0x000fffffffffffffull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x8000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0xfff0000000000000ull, 0x7fefffffffffffffull, 0xfff0000000000000ull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x000fffffffffffffull, 0x000fffffffffffffull}, + {0xfff0000000000000ull, 0x8000000000000001ull, 0xfff0000000000000ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull, 0x0000000000000001ull} +}; + +constexpr std::array Names{"fmin", "fmax", "fcmpswap"}; + +enum class Kind { Buffer, Image, Global }; + +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, "memory float atomic modes: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Threads - 1u) & 3u) << 30u), + ((Threads - 1u) >> 2u) | (2u << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::string Hex(std::uint64_t value, int digits) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%0*llx", digits, static_cast(value)); + return text; +} + +const char* KindName(Kind kind) { + return kind == Kind::Buffer ? "buffer" : kind == Kind::Image ? "image" : "global"; +} + +void FillInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* row = &Input[tid * Inputs]; + row[0] = Rows32[tid][0]; + row[1] = Rows32[tid][1]; + row[2] = Rows32[tid][2]; + for (std::uint32_t i = 0; i < 3u; ++i) { + row[4u + 2u * i] = static_cast(Rows64[tid][i]); + row[5u + 2u * i] = static_cast(Rows64[tid][i] >> 32u); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, Kind kind, GuestBlock& guest, const std::optional& floatMode) { + Output.fill(0xdeadbeefu); + Texels.fill(0xdeadbeefu); + std::memset(guest.Data(), 0xcd, BlockBytes); + 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)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + if (kind == Kind::Image) { + const auto texture = TextureDescriptor(Texels.data()); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + } else if (kind == Kind::Global) { + const auto address = static_cast(reinterpret_cast(guest.Data())); + userData[8] = static_cast(address); + userData[9] = static_cast(address >> 32u); + } + const std::span code = kind == Kind::Buffer ? std::span(BufferCode) : kind == Kind::Image ? std::span(ImageCode) : std::span(GlobalCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(Kind kind, const char* mode, bool flush32, bool flush64) { + const auto where = std::string("memory float atomic modes: ") + KindName(kind) + ", " + mode + ", lane "; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t op = 0; op < 3u; ++op) { + const std::uint32_t expected = flush32 ? Flushed32[tid][op] : Kept32[tid][op]; + Require(out[2u * op] == Rows32[tid][0], where + std::to_string(tid) + " " + Names[op] + " returned " + Hex(out[2u * op], 8) + ", expected " + Hex(Rows32[tid][0], 8)); + Require(out[2u * op + 1u] == expected, where + std::to_string(tid) + " " + Names[op] + " stored " + Hex(out[2u * op + 1u], 8) + ", expected " + Hex(expected, 8)); + } + if (kind == Kind::Image) continue; + for (std::uint32_t op = 0; op < 3u; ++op) { + const std::uint64_t returned = out[6u + 4u * op] | (static_cast(out[7u + 4u * op]) << 32u); + const std::uint64_t stored = out[8u + 4u * op] | (static_cast(out[9u + 4u * op]) << 32u); + const std::uint64_t expected = flush64 ? Flushed64[tid][op] : Kept64[tid][op]; + Require(returned == Rows64[tid][0], where + std::to_string(tid) + " " + Names[op] + "_x2 returned " + Hex(returned, 16) + ", expected " + Hex(Rows64[tid][0], 16)); + Require(stored == expected, where + std::to_string(tid) + " " + Names[op] + "_x2 stored " + Hex(stored, 16) + ", expected " + Hex(expected, 16)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityInt64Atomics)) { + std::puts("skipped, the device has no shaderBufferInt64Atomics"); + return VulkanTestSkipped; + } + GuestBlock guest; + FillInput(); + struct Mode { + std::optional floatMode; + const char* name; + bool flush32; + bool flush64; + }; + const std::array modes{{ + {std::nullopt, "no float mode", true, false}, + {ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, "FLOAT_MODE 0xc0", true, false}, + {ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, "FLOAT_MODE 0xf0", false, false}, + {ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}, "FLOAT_MODE 0x00", true, true}, + {ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}, "FLOAT_MODE 0x50", false, false}, + {ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}, "FLOAT_MODE 0xa0", true, true}, + }}; + for (const auto kind : {Kind::Buffer, Kind::Image, Kind::Global}) { + for (const auto& mode : modes) { + Run(*device, kind, guest, mode.floatMode); + Check(kind, mode.name, mode.flush32, mode.flush64); + } + } + std::puts("memory float atomic modes tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 96951cf815..34213a41b6 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -161,8 +161,8 @@ std::uint32_t Select(SpirvEmitterState& state, std::uint32_t type, std::uint32_t std::uint32_t AtomicIncrement(SpirvEmitterState& state, std::uint32_t old, std::uint32_t limit); std::uint32_t AtomicDecrement(SpirvEmitterState& state, std::uint32_t old, std::uint32_t limit); std::uint32_t AtomicUSubSat(SpirvEmitterState& state, std::uint32_t old, std::uint32_t value); -std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue); -std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator); +std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue, bool flush); +std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator, bool flush); template std::uint32_t EmitNative(SpirvEmitterState& state, TArguments... arguments) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 772f4f8f4c..feaaa4ce80 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1122,13 +1122,13 @@ void EmitAtomicOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { } if (opcode == IrOpcode::ImageAtomicFMin32 || opcode == IrOpcode::ImageAtomicFMax32) { return AtomicUpdate(state, pointer, ResourceKind::Image, [&](std::uint32_t current) { - return AtomicFloatMinMax(state, current, value, opcode == IrOpcode::ImageAtomicFMax32); + return AtomicFloatMinMax(state, current, value, opcode == IrOpcode::ImageAtomicFMax32, access.mem.flushDenormals); }); } if (opcode == IrOpcode::ImageAtomicFCmpSwap32) { const auto comparator = ctx.Arg(access.inst, 3); return AtomicUpdate(state, pointer, ResourceKind::Image, [&](std::uint32_t current) { - return AtomicFloatCompareSwap(state, current, value, comparator); + return AtomicFloatCompareSwap(state, current, value, comparator, access.mem.flushDenormals); }); } const auto old = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index d997a174eb..cd82fa2998 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -957,6 +957,7 @@ std::uint32_t BufferAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst) { struct AtomicFloatBits { SpirvEmitterState& state; bool wide; + bool flushInputs = false; std::uint32_t type() const { return wide ? TypeScalarU64(state) : TypeU32(state); } std::uint32_t constant(std::uint64_t value) const { return wide ? state.module.Constant(spv::OpConstant, TypeScalarU64(state), static_cast(value), static_cast(value >> 32u)) : ConstantU32(state, static_cast(value)); @@ -986,6 +987,8 @@ struct AtomicFloatBits { return Binary(state, spv::OpLogicalOr, TypeBool(state), same, zeros); } std::uint32_t minMax(std::uint32_t old, std::uint32_t source, bool maxValue) const { + old = flush(old, flushInputs); + source = flush(source, flushInputs); const auto better = test(maxValue ? spv::OpUGreaterThan : spv::OpULessThan, orderKey(source), orderKey(old)); auto result = select(better, source, old); result = select(nan(old), source, result); @@ -995,7 +998,7 @@ struct AtomicFloatBits { return select(signalingNan(source), op(spv::OpBitwiseOr, source, constant(quiet())), result); } std::uint32_t compareSwap(std::uint32_t old, std::uint32_t comparator, std::uint32_t desired) const { - return select(equal(old, comparator), desired, old); + return select(equal(flush(old, flushInputs), flush(comparator, flushInputs)), desired, old); } std::uint32_t increment(std::uint32_t old, std::uint32_t limit) const { return select(test(spv::OpUGreaterThanEqual, old, limit), constant(0u), op(spv::OpIAdd, old, constant(1u))); @@ -1007,8 +1010,9 @@ struct AtomicFloatBits { }; std::uint32_t BufferFloatAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, bool maxValue) { - return EmitAtomicUpdate(ctx, inst, BufferMemory(ctx, inst), [maxValue](SpirvEmitterState& state, std::uint32_t old, std::uint32_t value) { - return AtomicFloatBits{state, false}.minMax(old, value, maxValue); + const auto& mem = BufferMemory(ctx, inst); + return EmitAtomicUpdate(ctx, inst, mem, [maxValue, flush = mem.flushDenormals](SpirvEmitterState& state, std::uint32_t old, std::uint32_t value) { + return AtomicFloatBits{state, false, flush}.minMax(old, value, maxValue); }); } @@ -1044,8 +1048,7 @@ std::uint32_t SharedFloatMinMax(SpirvValueEmitContext& ctx, const IrValue& inst, const auto data = ctx.Arg(inst, 1); const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const AtomicFloatBits bits{state, false}; - return bits.minMax(bits.flush(old, flush), bits.flush(data, flush), maxValue); + return AtomicFloatBits{state, false, flush}.minMax(old, data, maxValue); }); } @@ -1089,7 +1092,7 @@ std::uint32_t AddressAtomicOpcode(IrOpcode opcode) { } } -std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { +std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator, bool flush) { switch (opcode) { case IrOpcode::AddressAtomicSwap32: case IrOpcode::AddressAtomicCmpSwap32: @@ -1110,11 +1113,11 @@ std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::u case IrOpcode::AddressAtomicUSubSat32: return AtomicUSubSat(state, old, value); case IrOpcode::AddressAtomicFMin32: - return AtomicFloatBits{state, false}.minMax(old, value, false); + return AtomicFloatBits{state, false, flush}.minMax(old, value, false); case IrOpcode::AddressAtomicFMax32: - return AtomicFloatBits{state, false}.minMax(old, value, true); + return AtomicFloatBits{state, false, flush}.minMax(old, value, true); case IrOpcode::AddressAtomicFCmpSwap32: - return AtomicFloatBits{state, false}.compareSwap(old, comparator, value); + return AtomicFloatBits{state, false, flush}.compareSwap(old, comparator, value); default: throw std::runtime_error("FlatAtomicNext32: opcode has no aperture lowering"); } @@ -1122,6 +1125,7 @@ std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::u std::uint32_t ApertureAtomic32(SpirvValueEmitContext& ctx, const IrValue& inst, ResourceKind kind, std::uint32_t address, std::uint32_t byteOffset, std::uint32_t value, std::uint32_t comparator) { auto& state = ctx.state; + const bool flush = ctx.Memory(inst).flushDenormals; const auto instruction = ConstantU32(state, inst.Flags().pc); const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 3u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); @@ -1136,20 +1140,20 @@ std::uint32_t ApertureAtomic32(SpirvValueEmitContext& ctx, const IrValue& inst, return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&] { const auto pointer = EmitMemoryElementPointer(state, resource, index); if (kind == ResourceKind::Lds) { - const auto update = [&]() { return AtomicUpdate(state, pointer, kind, [&](std::uint32_t current) { return FlatAtomicNext32(state, opcode, current, value, comparator); }); }; + const auto update = [&]() { return AtomicUpdate(state, pointer, kind, [&](std::uint32_t current) { return FlatAtomicNext32(state, opcode, current, value, comparator, flush); }); }; return state.requirements.ldsLock ? LockedLdsUpdate(state, TypeU32(state), ConstantU32(state, 0u), update) : update(); } const auto old = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); - state.module.AddFunction(spv::OpStore, pointer, FlatAtomicNext32(state, opcode, old, value, comparator)); + state.module.AddFunction(spv::OpStore, pointer, FlatAtomicNext32(state, opcode, old, value, comparator, flush)); return old; }); }); } -std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { +std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator, bool flush) { const auto u64 = TypeScalarU64(state); - const AtomicFloatBits bits{state, true}; + const AtomicFloatBits bits{state, true, flush}; const auto winner = [&](spv::Op compare) { return Select(state, u64, Binary(state, compare, TypeBool(state), old, value), old, value); }; @@ -1193,6 +1197,7 @@ std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::u std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, ResourceKind kind, std::uint32_t address, std::uint32_t byteOffset, std::uint32_t value, std::uint32_t comparator) { auto& state = ctx.state; + const bool flush = ctx.Memory(inst).flushDenormals; const auto instruction = ConstantU32(state, inst.Flags().pc); const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 7u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 8u), instruction, BdaAbi::FaultReason::Unaligned); }); @@ -1207,7 +1212,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto opcode = inst.Opcode(); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&] { return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&] { - if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator); }); + if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator, flush); }); const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1217,7 +1222,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, state.module.AddFunction(spv::OpLoad, TypeU32(state), highValue, highPointer); const auto old = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU64(state), old, lowValue, highValue); - const auto next = Unary(state, spv::OpBitcast, TypeU64(state), FlatAtomicNext64(state, opcode, Unary(state, spv::OpBitcast, TypeScalarU64(state), old), value, comparator)); + const auto next = Unary(state, spv::OpBitcast, TypeU64(state), FlatAtomicNext64(state, opcode, Unary(state, spv::OpBitcast, TypeScalarU64(state), old), value, comparator, flush)); const auto nextLow = state.module.AllocateId(); const auto nextHigh = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), nextLow, next, 0u); @@ -1254,7 +1259,7 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { if (inst.Opcode() == IrOpcode::AddressAtomicUSubSat32) { return AtomicUpdate(state, pointer, mem.kind, [&](std::uint32_t current) { return AtomicUSubSat(state, current, value); }); } - const AtomicFloatBits bits{state, wide}; + const AtomicFloatBits bits{state, wide, mem.flushDenormals}; const auto update = [&](auto&& replacement) { return AtomicUpdateTyped(state, pointer, mem.kind, scalarType, replacement); }; switch (inst.Opcode()) { case IrOpcode::AddressAtomicFCmpSwap32: @@ -1491,8 +1496,7 @@ std::uint32_t SharedAtomic64Select(SpirvValueEmitContext& ctx, const IrValue& in std::uint32_t SharedFloatMinMax64(SpirvValueEmitContext& ctx, const IrValue& inst, bool maxValue) { const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomic64Binary(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old, std::uint32_t data) { - const AtomicFloatBits bits{state, true}; - return bits.minMax(bits.flush(old, flush), bits.flush(data, flush), maxValue); + return AtomicFloatBits{state, true, flush}.minMax(old, data, maxValue); }); } @@ -1522,13 +1526,12 @@ std::uint32_t AtomicUSubSat(SpirvEmitterState& state, std::uint32_t old, std::ui return Select(state, TypeU32(state), fits, difference, ConstantU32(state, 0u)); } -std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue) { - return AtomicFloatBits{state, false}.minMax(old, source, maxValue); +std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue, bool flush) { + return AtomicFloatBits{state, false, flush}.minMax(old, source, maxValue); } -std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator) { - const AtomicFloatBits bits{state, false}; - return bits.select(bits.equal(old, comparator), desired, old); +std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator, bool flush) { + return AtomicFloatBits{state, false, flush}.compareSwap(old, comparator, desired); } std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { @@ -1948,8 +1951,7 @@ std::uint32_t EmitSharedAtomicCmpstF32(SpirvValueEmitContext& ctx, const IrValue const auto source = ctx.Arg(inst, 2); const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const AtomicFloatBits bits{state, false}; - return bits.select(bits.equal(bits.flush(old, flush), bits.flush(compare, flush)), source, old); + return AtomicFloatBits{state, false, flush}.compareSwap(old, compare, source); }); } @@ -2051,8 +2053,7 @@ std::uint32_t EmitSharedAtomicCmpstF64(SpirvValueEmitContext& ctx, const IrValue const auto source = ScalarU64Argument(ctx, inst, 2u); const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomic64(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const AtomicFloatBits bits{state, true}; - return bits.select(bits.equal(bits.flush(old, flush), bits.flush(compare, flush)), source, old); + return AtomicFloatBits{state, true, flush}.compareSwap(old, compare, source); }); } @@ -2118,24 +2119,27 @@ std::uint32_t EmitBufferAtomicFCmpSwap32(SpirvValueEmitContext& ctx, const IrVal const auto comparator = ctx.Arg(inst, inst.ArgumentCount() - 2u); const auto& mem = BufferMemory(ctx, inst); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, false}.compareSwap(old, comparator, desired); }); + return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, false, mem.flushDenormals}.compareSwap(old, comparator, desired); }); }); } std::uint32_t EmitBufferAtomicFCmpSwap64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto desired = ScalarU64Argument(ctx, inst, 3u); const auto comparator = ScalarU64Argument(ctx, inst, 2u); - return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true}.compareSwap(old, comparator, desired); }); + const bool flush = BufferMemory(ctx, inst).flushDenormals; + return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true, flush}.compareSwap(old, comparator, desired); }); } std::uint32_t EmitBufferAtomicFMin64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto value = ScalarU64Argument(ctx, inst, 2u); - return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true}.minMax(old, value, false); }); + const bool flush = BufferMemory(ctx, inst).flushDenormals; + return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true, flush}.minMax(old, value, false); }); } std::uint32_t EmitBufferAtomicFMax64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto value = ScalarU64Argument(ctx, inst, 2u); - return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true}.minMax(old, value, true); }); + const bool flush = BufferMemory(ctx, inst).flushDenormals; + return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true, flush}.minMax(old, value, true); }); } std::uint32_t EmitBufferAtomicInc64(SpirvValueEmitContext& ctx, const IrValue& inst) { diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index a7748044c4..d856fad819 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -112,6 +112,7 @@ private: std::array writeMask(const RdnaOperand& operand, IrU1 value, bool write64 = false); void writeCompareResult(const RdnaOperand& operand, IrU1 value); MemoryFlags addMemoryInfo(const MemoryInfo& memory, std::uint32_t pc); + bool floatAtomicFlush(IrOpcode opcode) const; ExportFlags addExportInfo(const RdnaInstruction& inst); AddressOperands readAddressOperands(const RdnaInstruction& inst, std::uint32_t firstSource); IrU32 getResourceDword(std::uint32_t index, std::uint32_t dword); @@ -157,7 +158,6 @@ private: bool dsWrite2(const RdnaInstruction& inst); bool dsSrc2(const RdnaInstruction& inst, IrOpcode opcode); bool dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); - bool sharedFloatFlush(IrOpcode opcode) const; bool dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); bool dsCondxchg32(const RdnaInstruction& inst); bool dsAppendConsume(const RdnaInstruction& inst, IrOpcode opcode); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp index 0b6d08a2c3..680ad0ab17 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp @@ -203,7 +203,8 @@ bool TranslationContext::bufferStore(const RdnaInstruction& inst) { } bool TranslationContext::bufferAtomic(const RdnaInstruction& inst, IrOpcode opcode) { - const MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + memory.flushDenormals = floatAtomicFlush(opcode); IrValue* resource = getBufferResource(memory); const BufferAddress address = readBufferAddress(inst); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp index 862c65f29b..1115a0615a 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp @@ -179,7 +179,8 @@ bool TranslationContext::flatStore(const RdnaInstruction& inst) { } bool TranslationContext::flatAtomic(const RdnaInstruction& inst, IrOpcode opcode) { - const MemoryInfo memory = flatMemoryInfoFromInstruction(inst); + MemoryInfo memory = flatMemoryInfoFromInstruction(inst); + memory.flushDenormals = floatAtomicFlush(opcode); const AddressOperands address = readAddressOperands(inst, 0u); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); IrValue& active = ir.GetExec(); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index c20f9b798c..7815ee9ccd 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -92,7 +92,8 @@ bool TranslationContext::imageAtomic(const RdnaInstruction& inst, IrOpcode opcod if (inst.dataBits == 64u) { opcode = imageAtomic64Opcode(opcode); } - const MemoryInfo memory = imageMemoryInfoFromInstruction(inst); + MemoryInfo memory = imageMemoryInfoFromInstruction(inst); + memory.flushDenormals = floatAtomicFlush(opcode); IrValue* resource = getImageResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp index bff55b085e..c04d0770fe 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp @@ -32,6 +32,37 @@ MemoryFlags TranslationContext::addMemoryInfo(const MemoryInfo& memory, std::uin return MemoryFlags{index, pc}; } +bool TranslationContext::floatAtomicFlush(IrOpcode opcode) const { + switch (opcode) { + case IrOpcode::SharedAtomicFMin32: + case IrOpcode::SharedAtomicFMax32: + case IrOpcode::SharedAtomicCmpstF32: + case IrOpcode::SharedAtomicFAdd32: + case IrOpcode::BufferAtomicFMin32: + case IrOpcode::BufferAtomicFMax32: + case IrOpcode::BufferAtomicFCmpSwap32: + case IrOpcode::AddressAtomicFMin32: + case IrOpcode::AddressAtomicFMax32: + case IrOpcode::AddressAtomicFCmpSwap32: + case IrOpcode::ImageAtomicFMin32: + case IrOpcode::ImageAtomicFMax32: + case IrOpcode::ImageAtomicFCmpSwap32: + return !floatMode.has_value() || ((floatMode->floatMode >> 4u) & 1u) == 0u; + case IrOpcode::SharedAtomicFMin64: + case IrOpcode::SharedAtomicFMax64: + case IrOpcode::SharedAtomicCmpstF64: + case IrOpcode::BufferAtomicFMin64: + case IrOpcode::BufferAtomicFMax64: + case IrOpcode::BufferAtomicFCmpSwap64: + case IrOpcode::AddressAtomicFMin64: + case IrOpcode::AddressAtomicFMax64: + case IrOpcode::AddressAtomicFCmpSwap64: + return floatMode.has_value() && ((floatMode->floatMode >> 6u) & 1u) == 0u; + default: + return false; + } +} + TranslationContext::AddressOperands TranslationContext::readAddressOperands(const RdnaInstruction& inst, std::uint32_t firstSource) { const ResourceKind kind = flatSegmentResourceKind(inst); const IrU32 low = readU32(sourceAt(inst, firstSource)); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp index a781713ddc..a79d94da4c 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -32,7 +32,7 @@ MemoryInfo sharedMemoryInfoFromInstruction(const RdnaInstruction& inst) { bool TranslationContext::dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); - memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormals = floatAtomicFlush(opcode); memory.flushDenormalResults = opcode == IrOpcode::SharedAtomicFAdd32 && (!floatMode.has_value() || ((floatMode->floatMode >> 5u) & 1u) == 0u); const IrU32 address = readU32(inst.source0); const IrU32 value = readU32(inst.source1); @@ -213,25 +213,9 @@ bool TranslationContext::dsSrc2(const RdnaInstruction& inst, IrOpcode opcode) { return true; } -bool TranslationContext::sharedFloatFlush(IrOpcode opcode) const { - switch (opcode) { - case IrOpcode::SharedAtomicFMin32: - case IrOpcode::SharedAtomicFMax32: - case IrOpcode::SharedAtomicCmpstF32: - case IrOpcode::SharedAtomicFAdd32: - return !floatMode.has_value() || ((floatMode->floatMode >> 4u) & 1u) == 0u; - case IrOpcode::SharedAtomicFMin64: - case IrOpcode::SharedAtomicFMax64: - case IrOpcode::SharedAtomicCmpstF64: - return floatMode.has_value() && ((floatMode->floatMode >> 6u) & 1u) == 0u; - default: - return false; - } -} - bool TranslationContext::dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); - memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormals = floatAtomicFlush(opcode); const IrU32 address = readU32(inst.source0); const IrU32 data0 = readU32(inst.source1); const IrU32 data1 = readU32(inst.source2); @@ -246,7 +230,7 @@ bool TranslationContext::dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode throw std::runtime_error("64-bit GDS atomics are not supported"); } MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); - memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormals = floatAtomicFlush(opcode); const IrU32 address = readU32(inst.source0); const IrU64 data0 = readU64(inst.source1); IrValue& active = ir.GetExec(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c842891c66..0d8ac810ee 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -110,7 +110,7 @@ - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. -- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. @@ -141,7 +141,8 @@ - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. -- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. +- [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. +- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 06c1c0300f7bf1f8646d23f1b84ea618ff62fc43 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 10:54:59 +0700 Subject: [PATCH 0260/1360] test(agc): run the global float atomic modes case once, with one op per width The global kind of agc_driver_memory_float_atomic_modes compiled three shaders with six global float atomics each, and llvmpipe needs minutes to compile them cold, which pushed the test past the CI timeout. The global kind now runs FLOAT_MODE 0x00 only, where both widths flush, and only global_atomic_fmin and global_atomic_fmin_x2; their stored value depends on the flush in 13 of the 32 rows at each width. Buffer and image keep all three operations in all six modes. With the global flush turned off, the global case still fails. Co-Authored-By: Claude --- .../execution/MemoryFloatAtomicModes.cpp | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp index aa4f0afb67..572e59d66f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp @@ -65,22 +65,14 @@ alignas(256) constexpr std::array ImageCode{ 0xe0701014u, 0x8001120cu, 0xbf810000u, }; -alignas(256) constexpr std::array GlobalCode{ +alignas(256) constexpr std::array GlobalCode{ 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, 0xbf8c3f70u, 0x34340082u, 0xd70f6a1au, 0x00023408u, 0x7e360209u, 0x50363680u, 0x34380083u, 0xd70f6a1cu, 0x00023808u, 0x7e3a0209u, 0x503a3a80u, 0xdc708000u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdcfd8000u, 0x137d101au, 0xbf8c3f70u, 0xdc318000u, 0x127d001au, 0xbf8c3f70u, 0xe0701000u, 0x8001130cu, - 0xe0701004u, 0x8001120cu, 0xdc708080u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdd018080u, - 0x137d101au, 0xbf8c3f70u, 0xdc318080u, 0x127d001au, 0xbf8c3f70u, 0xe0701008u, 0x8001130cu, 0xe070100cu, - 0x8001120cu, 0xdc708100u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdcf98100u, 0x137d101au, - 0xbf8c3f70u, 0xdc318100u, 0x127d001au, 0xbf8c3f70u, 0xe0701010u, 0x8001130cu, 0xe0701014u, 0x8001120cu, - 0xdc748200u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xdd7d8200u, - 0x207d141cu, 0xbf8c3f70u, 0xdc358200u, 0x187d001cu, 0xbf8c3f70u, 0xe0741018u, 0x8001200cu, 0xe0741020u, - 0x8001180cu, 0xdc748300u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, - 0xdd818300u, 0x207d141cu, 0xbf8c3f70u, 0xdc358300u, 0x187d001cu, 0xbf8c3f70u, 0xe0741028u, 0x8001200cu, - 0xe0741030u, 0x8001180cu, 0xdc748400u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, - 0x7e2e030bu, 0xdd798400u, 0x207d141cu, 0xbf8c3f70u, 0xdc358400u, 0x187d001cu, 0xbf8c3f70u, 0xe0741038u, - 0x8001200cu, 0xe0741040u, 0x8001180cu, 0xbf810000u, + 0xe0701004u, 0x8001120cu, 0xdc748200u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, + 0x7e2e030bu, 0xdd7d8200u, 0x207d141cu, 0xbf8c3f70u, 0xdc358200u, 0x187d001cu, 0xbf8c3f70u, 0xe0741018u, + 0x8001200cu, 0xe0741020u, 0x8001180cu, 0xbf810000u, }; constexpr std::uint32_t Rows32[Threads][3] = { @@ -393,15 +385,16 @@ void Run(AgcDriver::VulkanDevice& device, Kind kind, GuestBlock& guest, const st void Check(Kind kind, const char* mode, bool flush32, bool flush64) { const auto where = std::string("memory float atomic modes: ") + KindName(kind) + ", " + mode + ", lane "; + const std::uint32_t ops = kind == Kind::Global ? 1u : 3u; for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t op = 0; op < 3u; ++op) { + for (std::uint32_t op = 0; op < ops; ++op) { const std::uint32_t expected = flush32 ? Flushed32[tid][op] : Kept32[tid][op]; Require(out[2u * op] == Rows32[tid][0], where + std::to_string(tid) + " " + Names[op] + " returned " + Hex(out[2u * op], 8) + ", expected " + Hex(Rows32[tid][0], 8)); Require(out[2u * op + 1u] == expected, where + std::to_string(tid) + " " + Names[op] + " stored " + Hex(out[2u * op + 1u], 8) + ", expected " + Hex(expected, 8)); } if (kind == Kind::Image) continue; - for (std::uint32_t op = 0; op < 3u; ++op) { + for (std::uint32_t op = 0; op < ops; ++op) { const std::uint64_t returned = out[6u + 4u * op] | (static_cast(out[7u + 4u * op]) << 32u); const std::uint64_t stored = out[8u + 4u * op] | (static_cast(out[9u + 4u * op]) << 32u); const std::uint64_t expected = flush64 ? Flushed64[tid][op] : Kept64[tid][op]; @@ -428,17 +421,19 @@ int main() { const char* name; bool flush32; bool flush64; + bool global; }; const std::array modes{{ - {std::nullopt, "no float mode", true, false}, - {ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, "FLOAT_MODE 0xc0", true, false}, - {ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, "FLOAT_MODE 0xf0", false, false}, - {ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}, "FLOAT_MODE 0x00", true, true}, - {ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}, "FLOAT_MODE 0x50", false, false}, - {ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}, "FLOAT_MODE 0xa0", true, true}, + {std::nullopt, "no float mode", true, false, false}, + {ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, "FLOAT_MODE 0xc0", true, false, false}, + {ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, "FLOAT_MODE 0xf0", false, false, false}, + {ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}, "FLOAT_MODE 0x00", true, true, true}, + {ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}, "FLOAT_MODE 0x50", false, false, false}, + {ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}, "FLOAT_MODE 0xa0", true, true, false}, }}; for (const auto kind : {Kind::Buffer, Kind::Image, Kind::Global}) { for (const auto& mode : modes) { + if (kind == Kind::Global && !mode.global) continue; Run(*device, kind, guest, mode.floatMode); Check(kind, mode.name, mode.flush32, mode.flush64); } From de573c6d1ef287fe20a5e90f728712fef8e3bb0c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:14:13 +0000 Subject: [PATCH 0261/1360] test(net): bound Linux blocking-send receiver buffer --- core/libs/tests/NetBlockingSend.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp index fb64d834ba..86ccdde3f5 100644 --- a/core/libs/tests/NetBlockingSend.cpp +++ b/core/libs/tests/NetBlockingSend.cpp @@ -107,6 +107,9 @@ int main(int argc, char** argv) { } { Connection connection; +#ifndef _WIN32 + SetOption(connection.receiver, NET_SO_RCVBUF, 4096); +#endif SetOption(connection.sender, 0x1105, 100000); auto started = std::chrono::steady_clock::now(); const auto partial = Send(api, connection.sender, bytes); From 7d8776713787264b2c2f821a40cc4bf6691aa634 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:16:59 +0200 Subject: [PATCH 0262/1360] fix(libc): keep the root when trimming the parent of a dot component Trimming every trailing separator turned the Windows drive root "C:\" into the drive-relative "C:". Trim only while the parent is longer than its root_path(), and cover "/", "C:\" and "C:/" in guest_filesystem. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/RenameDot.hpp | 11 +++++++++-- core/libs/tests/GuestFilesystem.cpp | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp index ff2622b08c..3a9e1a99c1 100644 --- a/core/libs/prx/libc/include/RenameDot.hpp +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -5,6 +5,14 @@ #include #include "FilesystemError.hpp" +// Drops trailing separators but keeps the root: "/", "C:\" or a UNC share root. +inline std::string TrimTrailingSeparators_nid_no_patch(std::string path) { + const auto separator = [](char character) { return character == '/' || character == '\\'; }; + const std::size_t root = std::filesystem::path(path).root_path().native().size(); + while (path.size() > root && path.size() > 1 && separator(path.back())) path.pop_back(); + return path; +} + inline int RenameDotError_nid_no_patch(const char* path) { std::size_t length = std::char_traits::length(path); const auto separator = [](char character) { return character == '/' || character == '\\'; }; @@ -15,8 +23,7 @@ inline int RenameDotError_nid_no_patch(const char* path) { const bool dot = name == 1 && path[start] == '.'; const bool dotDot = name == 2 && path[start] == '.' && path[start + 1] == '.'; if (!dot && !dotDot) return 0; - std::string parent = start == 0 ? std::string(".") : std::string(path, start); - while (parent.size() > 1 && separator(parent.back())) parent.pop_back(); + const std::string parent = start == 0 ? std::string(".") : TrimTrailingSeparators_nid_no_patch(std::string(path, start)); std::filesystem::path resolved; if (const int error = ResolvePathError_nid_no_patch(parent.c_str(), resolved)) return error; std::error_code status; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 9e39742add..5a5dd838d8 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -248,6 +249,14 @@ static void TestParentTraversal() { Require(stat_nid_postfix("/parent-test-mount/missing/.", &status) == -1 && *__error_nid_postfix() == 2); Require(stat_nid_postfix("/parent-test-mount/existing/.", &status) == 0 && (status.st_mode & 0170000) == 0040000); Require(access_nid_postfix("/parent-test-mount/existing/./", 0) == 0); + Require(TrimTrailingSeparators_nid_no_patch("/") == "/"); + Require(TrimTrailingSeparators_nid_no_patch("/mount/dir//") == "/mount/dir"); + Require(TrimTrailingSeparators_nid_no_patch("dir/") == "dir"); +#ifdef _WIN32 + Require(TrimTrailingSeparators_nid_no_patch("C:\\") == "C:\\"); + Require(TrimTrailingSeparators_nid_no_patch("C:/") == "C:/"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\dir\\") == "C:\\dir"); +#endif std::filesystem::create_directory(root / "dot-victim"); for (const char* name : {"/parent-test-mount/dot-victim/.", "/parent-test-mount/dot-victim/..", "/parent-test-mount/dot-victim/./", "/parent-test-mount/dot-victim/../"}) { Require(unlink_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); From 9e6a3f9127fa0a3f45a1a2819f0b29de2a348fd2 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:18:49 +0200 Subject: [PATCH 0263/1360] style(libc): drop the comment above TrimTrailingSeparators Comments are only allowed for technical debt in this repository. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/RenameDot.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp index 3a9e1a99c1..a792ac7f32 100644 --- a/core/libs/prx/libc/include/RenameDot.hpp +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -5,7 +5,6 @@ #include #include "FilesystemError.hpp" -// Drops trailing separators but keeps the root: "/", "C:\" or a UNC share root. inline std::string TrimTrailingSeparators_nid_no_patch(std::string path) { const auto separator = [](char character) { return character == '/' || character == '\\'; }; const std::size_t root = std::filesystem::path(path).root_path().native().size(); From f2bdca611f527318f8187e23e220d0e9d01cfc4c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 19:31:48 +1300 Subject: [PATCH 0264/1360] feat(libSceScreenShot): track capture disabling and check overlay images sceScreenShotDisable, sceScreenShotEnable and sceScreenShotIsDisabled threw. Disable and enable now set and clear a process-wide flag that IsDisabled reports, rejecting an id of -1 or a null result with 0x80BE0001 as the PS5 module does. The notification toggles return 0. sceScreenShotSetOverlayImage(WithOrigin) check their arguments and the image file in the module's order: origin 1 to 9, offsets within 1919x1079 (3839x2159 for SDK 3.50 and later), a path under 1024 bytes, a readable file that is not a directory (0x80BE0003) and a size from 1 byte to 2 MiB (8 MiB for SDK 3.50 and later, 0x80BE000E). An accepted image is not kept, since no screenshots are taken. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceScreenShot/CMakeLists.txt | 16 +++ core/libs/prx/libSceScreenShot/Export.cpp | 90 ++++++++++--- core/libs/tests/GuestScreenShot.cpp | 120 ++++++++++++++++++ docs/dev/debt/libSceScreenShot.md | 9 +- 4 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 core/libs/prx/libSceScreenShot/CMakeLists.txt create mode 100644 core/libs/tests/GuestScreenShot.cpp diff --git a/core/libs/prx/libSceScreenShot/CMakeLists.txt b/core/libs/prx/libSceScreenShot/CMakeLists.txt new file mode 100644 index 0000000000..0300d678fa --- /dev/null +++ b/core/libs/prx/libSceScreenShot/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceScreenShot SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceScreenShot PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceScreenShot) + +if(BUILD_TESTING) + add_test_executable(guest_screenshot_tests ${LIBS_INCLUDE_DIR}/tests/GuestScreenShot.cpp) + target_include_directories(guest_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_screenshot_tests PRIVATE libSceScreenShot libkernel libc) + configure_windows_unwind(guest_screenshot_tests) + add_test(NAME guest_screenshot COMMAND guest_screenshot_tests small) + add_test(NAME guest_screenshot_large_overlay COMMAND guest_screenshot_tests large) + if(WIN32) + set_tests_properties(guest_screenshot guest_screenshot_large_overlay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp index ae99fefcf6..dbc4cf09af 100644 --- a/core/libs/prx/libSceScreenShot/Export.cpp +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -1,35 +1,89 @@ +#include #include #include +#include #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" + +extern "C" int APS5_VABI sceKernelFstat(int d, FileStat* sb); + +namespace { + +constexpr int ErrorArgument = static_cast(0x80BE0001); +constexpr int ErrorOverlayFile = static_cast(0x80BE0003); +constexpr int ErrorOverlaySize = static_cast(0x80BE000E); +constexpr std::uint32_t LargeOverlaySdkVersion = 0x3500000; +constexpr std::size_t MaxOverlayPath = 0x400; +constexpr std::uint16_t FileTypeMask = 0xF000; +constexpr std::uint16_t DirectoryType = 0x4000; + +std::atomic disabled{false}; + +std::uint32_t CompiledSdkVersion() { + static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, processParameters + 0x10, sizeof(version)); + return version; +} + +int SetOverlayImage(const char* path, int offsetX, int offsetY) { + const bool large = CompiledSdkVersion() >= LargeOverlaySdkVersion; + const int maxX = large ? 3839 : 1919; + const int maxY = large ? 2159 : 1079; + const std::int64_t maxSize = large ? 8 * 1024 * 1024 : 2 * 1024 * 1024; + if (!path || offsetX < 0 || offsetX > maxX || offsetY < 0 || offsetY > maxY) return ErrorArgument; + if (strnlen(path, MaxOverlayPath) >= MaxOverlayPath) return ErrorArgument; + const int descriptor = sceKernelOpen(path, 0, 0); + if (descriptor < 0) return ErrorOverlayFile; + FileStat stat{}; + int result = ErrorOverlayFile; + if (sceKernelFstat(descriptor, &stat) == 0 && (stat.st_mode & FileTypeMask) != DirectoryType) { + result = stat.st_size > 0 && stat.st_size <= maxSize ? 0 : ErrorOverlaySize; + } + sceKernelClose(descriptor); + return result; +} + +} extern "C" { -int APS5_VABI sceScreenShotSetOverlayImage() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int offsetX, int offsetY) { + return SetOverlayImage(path, offsetX, offsetY); } -int APS5_VABI sceScreenShotSetOverlayImageWithOrigin() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceScreenShotCapture() { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int offsetX, int offsetY, int origin) { + if (origin < 1 || origin > 9) return ErrorArgument; + return SetOverlayImage(path, offsetX, offsetY); } + int APS5_VABI sceScreenShotDisable() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceScreenShotDisableNotification() { - NotImplemented_nid_no_patch(__func__); + disabled = true; return 0; } + int APS5_VABI sceScreenShotEnable() { - NotImplemented_nid_no_patch(__func__); + disabled = false; return 0; } + +int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled) { + if (id == -1 || !isDisabled) return ErrorArgument; + *isDisabled = disabled ? 1 : 0; + return 0; +} + +int APS5_VABI sceScreenShotDisableNotification() { + return 0; +} + int APS5_VABI sceScreenShotEnableNotification() { + return 0; +} + +int APS5_VABI sceScreenShotCapture() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -41,10 +95,6 @@ int APS5_VABI sceScreenShotGetDrcParam() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceScreenShotIsDisabled() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceScreenShotIsVshScreenCaptureDisabled() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestScreenShot.cpp b/core/libs/tests/GuestScreenShot.cpp new file mode 100644 index 0000000000..29355509ff --- /dev/null +++ b/core/libs/tests/GuestScreenShot.cpp @@ -0,0 +1,120 @@ +#include "ProcessParametersFixture.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceScreenShotDisable(); +int APS5_VABI sceScreenShotEnable(); +int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled); +int APS5_VABI sceScreenShotDisableNotification(); +int APS5_VABI sceScreenShotEnableNotification(); +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int offsetX, int offsetY); +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int offsetX, int offsetY, int origin); +} + +namespace { + +constexpr int ErrorArgument = static_cast(0x80BE0001); +constexpr int ErrorOverlayFile = static_cast(0x80BE0003); +constexpr int ErrorOverlaySize = static_cast(0x80BE000E); + +alignas(16) std::array process{}; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ScreenShot: %s\n", message); + std::abort(); +} + +void InstallProcessParameters(std::uint32_t sdkVersion) { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +class TempPath { +public: + explicit TempPath(const char* tag) : name(std::string("anyps5-screenshot-") + tag + "-" + std::to_string(std::rand())) {} + ~TempPath() { + std::error_code error; + std::filesystem::remove_all(name, error); + } + const char* c_str() const { return name.c_str(); } + void Fill(std::size_t bytes) const { + std::ofstream file(name, std::ios::binary | std::ios::trunc); + const std::string chunk(4096, 'x'); + for (std::size_t written = 0; written < bytes; written += chunk.size()) { + file.write(chunk.data(), static_cast(std::min(chunk.size(), bytes - written))); + } + } + +private: + std::string name; +}; + +void TestDisableState() { + int value = -1; + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 0, "capture starts enabled"); + Require(sceScreenShotDisable() == 0, "disable"); + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 1, "disable is reported"); + Require(sceScreenShotEnable() == 0, "enable"); + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 0, "enable is reported"); + value = 7; + Require(sceScreenShotIsDisabled(-1, &value) == ErrorArgument && value == 7, "id -1 is rejected"); + Require(sceScreenShotIsDisabled(0, nullptr) == ErrorArgument, "null result is rejected"); + Require(sceScreenShotDisableNotification() == 0 && sceScreenShotEnableNotification() == 0, "notification toggles"); +} + +void TestOverlay(bool large) { + const int maxX = large ? 3839 : 1919; + const int maxY = large ? 2159 : 1079; + const std::size_t maxSize = large ? 8u << 20 : 2u << 20; + TempPath image("image"); + image.Fill(4096); + Require(sceScreenShotSetOverlayImage(image.c_str(), 0, 0) == 0, "overlay at the origin"); + Require(sceScreenShotSetOverlayImage(image.c_str(), maxX, maxY) == 0, "overlay at the last offsets"); + Require(sceScreenShotSetOverlayImage(image.c_str(), maxX + 1, 0) == ErrorArgument, "x past the screen"); + Require(sceScreenShotSetOverlayImage(image.c_str(), 0, maxY + 1) == ErrorArgument, "y past the screen"); + Require(sceScreenShotSetOverlayImage(image.c_str(), -1, 0) == ErrorArgument, "negative x"); + Require(sceScreenShotSetOverlayImage(nullptr, 0, 0) == ErrorArgument, "null path"); + const std::string longPath(1024, 'a'); + Require(sceScreenShotSetOverlayImage(longPath.c_str(), 0, 0) == ErrorArgument, "path of 1024 characters"); + TempPath missing("missing"); + Require(sceScreenShotSetOverlayImage(missing.c_str(), 0, 0) == ErrorOverlayFile, "missing file"); + TempPath directory("directory"); + std::filesystem::create_directory(directory.c_str()); + Require(sceScreenShotSetOverlayImage(directory.c_str(), 0, 0) == ErrorOverlayFile, "directory"); + TempPath empty("empty"); + empty.Fill(0); + Require(sceScreenShotSetOverlayImage(empty.c_str(), 0, 0) == ErrorOverlaySize, "empty file"); + TempPath full("full"); + full.Fill(maxSize); + Require(sceScreenShotSetOverlayImage(full.c_str(), 0, 0) == 0, "file at the size limit"); + full.Fill(maxSize + 1); + Require(sceScreenShotSetOverlayImage(full.c_str(), 0, 0) == ErrorOverlaySize, "file past the size limit"); + for (int origin = 1; origin <= 9; ++origin) Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, origin) == 0, "origins 1 to 9"); + Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, 0) == ErrorArgument, "origin 0"); + Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, 10) == ErrorArgument, "origin 10"); + Require(sceScreenShotSetOverlayImageWithOrigin(nullptr, 0, 0, 0) == ErrorArgument, "origin checked first"); +} + +} + +int main(int argc, char** argv) { + const bool large = argc > 1 && std::strcmp(argv[1], "large") == 0; + InstallProcessParameters(large ? 0x3500000u : 0x34fffffu); + TestDisableState(); + TestOverlay(large); + return 0; +} diff --git a/docs/dev/debt/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index 568cb899b6..a109ca5bb9 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -1,6 +1,11 @@ # libSceScreenShot technical debt +### Silent stubs + +- [sceScreenShotEnableNotification and sceScreenShotDisableNotification](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - return 0 without effect: on the console they ask the system software to show or hide the capture notification, which this project does not display +- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - check the arguments and the file as the PS5 module does, then return 0 without keeping the image, since this project takes no screenshots to overlay +- [sceScreenShotIsDisabled](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - reports one process-wide flag set by sceScreenShotDisable and cleared by sceScreenShotEnable for any id other than -1; on the console the system software answers, and what the id selects is unknown + ### Unknown function info -- [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c). The PS4 compatibility implementation also exposes the names but declares no parameters; no PS5 prototype was found. The declarations have no parameters solely to resolve known imports and make no ABI claim +- [sceScreenShotCapture, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. From 48549d1a9c6ca5a6f0ea48ca0908e924c9ec9831 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 05:44:42 +0000 Subject: [PATCH 0265/1360] perf(agc): resolve a committed shader ABI without the preparation lock Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/ShaderPreparation.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 45 ++++++++++++++++-- .../tests/execution/PreparedPublication.cpp | 29 ++++++++++++ .../tests/execution/PreparedShaders.cpp | 46 +++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp index 98fd3cbadd..70d81c98a5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp @@ -16,6 +16,7 @@ public: ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete; PreparedShaderState& Edit(const ShaderSnapshot& snapshot); const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const; + bool Tracks(const ShaderSnapshot& snapshot) const; void Hold(); void Commit(); 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 b69bccd8fe..0a44eb264a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -220,6 +220,24 @@ bool CommittedAbi(const ShaderSnapshot& snapshot, std::span return std::ranges::any_of(snapshot.prepared->registeredAbis, [&](const auto& abi) { return std::ranges::equal(abi, key); }); } +std::optional>> CommittedGraphicsAbi(const ShaderSnapshot& owner, std::span key, const std::shared_ptr& registry) { + std::unique_lock lock(owner.prepared->mutex); + AwaitRegisteredPreparation(*owner.prepared, lock); + const auto& abis = owner.prepared->graphicsAbis; + const auto found = std::ranges::find_if(abis, [&](const auto& abi) { return std::ranges::equal(abi.key, key) && abi.registry.lock() == registry; }); + if (found == abis.end()) return std::nullopt; + return found->stages; +} + +bool CommittedLink(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType) { + if (primitiveType == 7 || primitiveType == 17) return false; + std::unique_lock lock(front.prepared->mutex); + AwaitRegisteredPreparation(*front.prepared, lock); + const auto& fragments = front.prepared->fragments; + if (front.prepared->rectangleRequested || std::ranges::any_of(fragments, [](const auto& entry) { return entry.expired(); })) return false; + return fragment == nullptr || std::ranges::any_of(fragments, [&](const auto& entry) { return entry.lock() == fragment; }); +} + } bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) { @@ -351,6 +369,10 @@ const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapsh return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, false, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared; } +bool ShaderPreparationTransaction::Tracks(const ShaderSnapshot& snapshot) const { + return root->changes.contains(snapshot.prepared.get()); +} + void ShaderPreparationTransaction::Hold() { require(!committed, "shader preparation transaction is already committed"); root->Hold(); @@ -1531,14 +1553,23 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std auto& key = HostThreadLocal, GraphicsAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); const auto primitiveType = state.userConfig.find(0x242u); - for (const auto& abi : transaction.Read(*owner).graphicsAbis) { - if (abi.key != key || abi.registry.lock() != registry) continue; - for (const auto& stage : abi.stages) { + const auto resolveStages = [&](const std::vector>& stages) { + for (const auto& stage : stages) { const auto snapshot = stage.lock(); require(snapshot != nullptr, "prepared graphics ABI lost a registered stage"); if (snapshot->type != 1) ResolvePreparedGraphics(*snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target(), true); } transaction.Commit(); + }; + if (!transaction.Tracks(*owner)) { + if (const auto committed = CommittedGraphicsAbi(*owner, key, registry)) { + resolveStages(*committed); + return; + } + } + for (const auto& abi : transaction.Read(*owner).graphicsAbis) { + if (abi.key != key || abi.registry.lock() != registry) continue; + resolveStages(abi.stages); return; } DrawDecode decoded{}; @@ -1624,6 +1655,10 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span{} : prepare(); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { @@ -1709,6 +1744,10 @@ void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr< return; } } + if (!transaction.Tracks(front) && CommittedLink(front, fragment, primitiveType)) { + transaction.Commit(); + return; + } std::vector> fragments; { const auto& current = transaction.Read(front); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index beaed11d60..1273ff05c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -371,6 +371,34 @@ void TransactionLock() { Require(excluded, "a transaction ran beside one that had read prepared state"); } +void CommittedLinks() { + ShaderSnapshot front{}; + auto pixel = std::make_shared(); + ResolvePreparedGraphics(front, pixel, 0, {}); + Require(front.prepared->fragments.size() == 1 && front.prepared->fragments.front().lock() == pixel, "a resolved link was not published"); + ShaderSnapshot unrelated{}; + auto other = std::make_shared(); + std::optional open(std::in_place); + static_cast(open->Read(unrelated)); + auto repeated = std::async(std::launch::async, [&] { ResolvePreparedGraphics(front, pixel, 0, {}); }); + const bool unblocked = repeated.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + auto linking = std::async(std::launch::async, [&] { ResolvePreparedGraphics(front, other, 0, {}); }); + const bool excluded = linking.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open->Commit(); + open.reset(); + repeated.get(); + linking.get(); + Require(unblocked, "resolving a link the shader already had waited for another thread's transaction"); + Require(excluded && front.prepared->fragments.size() == 2, "a new link was published beside a transaction that had read prepared state"); + { + ShaderPreparationTransaction transaction; + transaction.Edit(front).fragments.clear(); + ResolvePreparedGraphics(front, pixel, 0, {}); + transaction.Commit(); + } + Require(front.prepared->fragments.size() == 1 && front.prepared->fragments.front().lock() == pixel, "a link resolved in a transaction that had changed the shader was checked against the published links"); +} + void PreparationPriority(bool beforeSubmit) { const auto workers = std::max(2u, std::thread::hardware_concurrency() / 2u); std::barrier started(static_cast(workers + 1u)); @@ -415,6 +443,7 @@ int main() { RegistryVersions(); FusedHullHeaderSelection(); TransactionLock(); + CommittedLinks(); PreparationPriority(false); PreparationPriority(true); auto device = OpenVulkanTestDevice(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 734a7c6825..b72d48eb52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1198,6 +1198,51 @@ void AbiDeferredRectanglePairs() { ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "custom per-vertex interpolation is unsupported"); } +void CommittedAbis() { + static AbiHeader front; + static AbiHeader pixel; + DescribeFront(front); + DescribeAbi(pixel, PassthroughPixelCode, 1, {{0x8, ProgramLow(PassthroughPixelCode.data())}, {0x9, ProgramHigh(PassthroughPixelCode.data())}, {0xa, 0}, {0xb, 0}}, {{0x1b6, 1}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0}, {0x1c5, 4}}); + for (auto* header : {&front, &pixel}) AgcDriverRegisterShader_nid_postfix(&header->shader); + const std::array stages{&front.shader, &pixel.shader}; + const std::array primitive{{{0x242, 4}}}; + const std::array interpolant{{{0x191, 0x20}}}; + const auto resolve = [&] { + AgcDriver::ShaderPreparationScope scope({&front.shader, &pixel.shader}); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, {}, primitive); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &pixel.shader, 0); + scope.Commit(); + }; + const auto resolveOther = [&] { + AgcDriver::ShaderPreparationScope scope({&pixel.shader}); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, interpolant, {}); + scope.Commit(); + }; + resolve(); + const auto before = ShaderRecompiler::FrontEndBuilds(); + bool unblocked = false; + bool excluded = false; + std::uint64_t repeatedBuilds = 0; + std::future repeated; + std::future other; + { + AgcDriver::DriverDetail::ShaderSnapshot unrelated{}; + AgcDriver::DriverDetail::ShaderPreparationTransaction open; + static_cast(open.Read(unrelated)); + repeated = std::async(std::launch::async, resolve); + unblocked = repeated.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + repeatedBuilds = ShaderRecompiler::FrontEndBuilds() - before; + other = std::async(std::launch::async, resolveOther); + excluded = other.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open.Commit(); + } + repeated.get(); + other.get(); + Require(unblocked && repeatedBuilds == 0, "resolving ABIs the shaders already had waited for another thread's shader preparation, or prepared them again"); + Require(excluded, "a new ABI was published beside a shader preparation that had read prepared state"); +} + void RelocatedHeaders() { alignas(256) static const std::array code{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; struct Header { @@ -1318,6 +1363,7 @@ int main(int argc, char** argv) { device.reset(); SharedCodeHeaders(); RelocatedHeaders(); + CommittedAbis(); RegistrationWithoutSpecials(); Registration(argc == 2); std::cout << "prepared shader and transactional registration tests passed\n"; From 0de8266c6f4fb606f0fb94bec18896cff50eeebb Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:32:44 +0200 Subject: [PATCH 0266/1360] test(libc): cover repeated root separators in the trailing separator trim The two Windows cases, a doubled separator after the drive root and after a folder name, come from MarcZX10's follow-up d68de63527 to the root review on this PR. Co-Authored-By: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestFilesystem.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 5a5dd838d8..983d1e8cfd 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -256,6 +256,8 @@ static void TestParentTraversal() { Require(TrimTrailingSeparators_nid_no_patch("C:\\") == "C:\\"); Require(TrimTrailingSeparators_nid_no_patch("C:/") == "C:/"); Require(TrimTrailingSeparators_nid_no_patch("C:\\dir\\") == "C:\\dir"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\\\") == "C:\\"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\folder\\\\") == "C:\\folder"); #endif std::filesystem::create_directory(root / "dot-victim"); for (const char* name : {"/parent-test-mount/dot-victim/.", "/parent-test-mount/dot-victim/..", "/parent-test-mount/dot-victim/./", "/parent-test-mount/dot-victim/../"}) { From bf66eeccfdc98c741a8181daece38dd86a4bcfb1 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 20 Oct 2026 20:14:33 +0200 Subject: [PATCH 0267/1360] perf(agc): import a range mapped again on first use instead of at map A guest that keeps remapping the same buffer paid a whole-range host import at every map and its retirement at every unmap. SuperTuxKart maps and unmaps one 2 MiB direct-memory buffer about 48 times a frame; the first GPU use imports only the window it reads. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 4 ++++ core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 6 +++++- .../libSceAgcDriver/tests/execution/HostImportAtMap.cpp | 8 +++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 610c437369..caf60a6122 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -239,6 +239,7 @@ struct HostImports { std::map failedJoins; std::map sparseRunEnds; std::map sparseRunJoins; + std::set> importedAtMap; std::map, ImportFailure> failedBlocks; std::map, ImportRefusal> refusals; std::map, EvictingRefusal> evictingRefusals; @@ -1991,6 +1992,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc state.reportNoted = 0; state.sparseRunEnds.clear(); state.sparseRunJoins.clear(); + state.importedAtMap.clear(); state.device = context.device; state.destroyBuffer = context.Function("vkDestroyBuffer"); state.freeMemory = context.Function("vkFreeMemory"); @@ -3119,6 +3121,7 @@ void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); state.parked.erase(device); + state.importedAtMap.clear(); #ifdef _WIN32 if (device == state.device) drainSettles(state); #endif @@ -3545,6 +3548,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& const std::array, 1> span{{{run.base, run.base + run.bytes}}}; if (wouldRetireImport(state, run) || StorageTexture::AnyPendingOverlaps(span)) return false; } + if (!state.importedAtMap.emplace(range->address, range->bytes).second && findImport(state, range->address, range->address + range->bytes) == nullptr) continue; importAllocation(context, state, range->address, current, ImportRequester::Map, 0, false); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast(range->address), range->bytes, error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index ceacffde72..49f9032e9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -840,8 +840,10 @@ void importMappedFailureGenerationTests(const Context& context) { { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "the replacement owner map import was deferred"); - Require(probe.calls.size() == 2u, "a replacement owner did not retry its predecessor's failed import"); + Require(probe.calls.size() == 1u, "a replacement owner was imported again at map instead of on first use"); } + HostImportFor(range.importing, range.First(), range.bytes); + Require(probe.calls.size() == 2u, "a replacement owner did not retry its predecessor's failed import on first use"); ClearHostImports(context.device); } @@ -979,6 +981,7 @@ void importNeighbouringEdgesTests(const Context& context) { // A range mapped while the imports are at the budget is refused at map time without retiring an import: the map // observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit). void importAtMapOverBudgetTests(const Context& context) { + ClearHostImports(context.device); constexpr std::size_t bytes = 65536; auto importing = context; importing.hostImportAlignment = bytes; @@ -1023,6 +1026,7 @@ void importAtMapOverBudgetTests(const Context& context) { } void importRequesterTests(const Context& context, const BdaTestAccess& access) { + ClearHostImports(context.device); constexpr std::size_t piece = 65536; constexpr std::size_t pieces = 8; auto* block = static_cast(::operator new(pieces * piece, std::align_val_t{piece})); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp index 00a4f23f96..6bc76a11e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp @@ -147,7 +147,13 @@ int main() { remapped.Unregister(); remapped.Register(); Import(*device, remapped.Ranges()); - Require(Imported(handle, remapped.Address(), AllocationBytes), "host import at map: a range mapped again was not imported"); + Require(!Imported(handle, remapped.Address(), AllocationBytes), "host import at map: a range mapped again was imported at map instead of on first use"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{9u, 10u, 11u, 12u}; + Require(device->FillBuffer(remapped.Address(), BlockBytes, pattern), "host import at map: the first use of a range mapped again was not imported"); + device->WaitIdle(); + } GuestAllocation gone; const auto goneRanges = gone.Ranges(); gone.Unregister(); From 8cd100e486b9d278f8469499cae802a00229255d Mon Sep 17 00:00:00 2001 From: CPritch Date: Wed, 21 Oct 2026 07:37:09 +0100 Subject: [PATCH 0268/1360] test(agc): start the prebuild waiter before checking that it blocks --- .../prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 19a6b4218d..82832ba244 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -86,12 +86,16 @@ void ClosingDropsQueuedPrebuilds() { SubmitLibraryPrebuild([&] { started.set_value(); opened.wait(); }); for (int index = 0; index < 3; ++index) SubmitLibraryPrebuild([&] { queuedRan = true; }); running.wait(); + auto entering = std::make_shared>(); + auto entered = entering->get_future(); auto awaited = std::make_shared>(); auto woken = awaited->get_future(); - std::thread([awaited] { + std::thread([entering, awaited] { + entering->set_value(); AwaitLibraryPrebuilds(); awaited->set_value(); }).detach(); + entered.wait(); const bool waiting = woken.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; From 99255c6144a6e5e6883ea3b14693f17186b360d4 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 19:40:35 +1300 Subject: [PATCH 0269/1360] feat(libSceAudioIn): answer sceAudioInIsPadEmbeddedMic for open ports The PS5 module returns 1 when the port captures from the controller's built-in microphone, 0 for another input device, and an error for a handle that names no open port. Ports here capture from the host's default input device, so an open port answers 0 and any other handle returns 0x80260101, as the other handle calls do. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAudioIn/Export.cpp | 6 +++--- core/libs/tests/GuestAudioIn.cpp | 5 ++++- docs/dev/debt/libSceAudioIn.md | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 68fa257250..2f5c6585b0 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -191,9 +191,9 @@ int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t i } int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!find(handle)) return AUDIO_IN_ERROR_INVALID_HANDLE; + return 0; } int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index 124bc9a880..49933c3713 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -105,7 +105,10 @@ void TestValidation() { port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 0x10); gain = 0.5f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); - Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); + Require(sceAudioInIsPadEmbeddedMic(1) == static_cast(0x80260101)); + const int mic = sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); + Require(mic > 0 && sceAudioInIsPadEmbeddedMic(mic) == 0 && sceAudioInClose(mic) == 0); + Require(sceAudioInIsPadEmbeddedMic(mic) == static_cast(0x80260101)); } void InputSound(int handle, std::vector& block) { diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 04f555333b..2fd8d599b2 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -6,7 +6,6 @@ ### Unknown function info -- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the PPSA26344 call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one - [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature @@ -42,5 +41,6 @@ ### Functional +- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 for every open port: on the console it returns 1 when the port captures from the controller's built-in microphone and 0 for another input device, but ports here capture from the host's default input device - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) describe the higher-quality path: a longer grain returns the invalid-size error and a lower rate the invalid-frequency error. AsyncOpen also accepts the 384-sample grain, the largest [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) documents for the low-latency path, and returns the invalid-size error for every other size; the console's shorter average return interval of an async port's blocks is not modelled, so like a synchronous port `sceAudioInInput` paces one grain at a time. Param 0x10 opens a float mono port like 0x11, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) accepts both, while SharpProspero documents only 0x11. Host capture pacing and console behavior remain unverified. - [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - signatures and semantics follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs#L92-L110). `sceAudioInGetGain` writes the unattenuated linear gain `1.0f` into `*gain` before validating `handle`, returning `0` for an open port and `0x80260101` (`SCE_AUDIO_IN_ERROR_INVALID_HANDLE`) otherwise; a null `gain` pointer throws because the console result is unverified, and host or system input attenuation is not modelled. `sceAudioInGetRerouteCount` returns `0` for an open port and `0x80260101` otherwise, as each port opens its SDL capture device once and capture-device hot-swapping is not modelled. From 50985e208dcf41fcf8cbed81c21f249b27464357 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 19:44:26 +1300 Subject: [PATCH 0270/1360] test(libSceAudioIn): check the Ex opens in their own function Keeps the OpenEx and setter checks apart from the lines that follow them in TestValidation, so changes to those lines merge cleanly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestAudioIn.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index bad2d3cf94..bca44317f9 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -62,6 +62,21 @@ namespace { constexpr int user = 0x10000000; +void TestOpenEx() { + constexpr int invalidSize = static_cast(0x80260102); + constexpr int invalidFreq = static_cast(0x80260103); + constexpr int invalidType = static_cast(0x80260104); + Require(sceAudioInOpenEx(user, 2, 0, 7, 256, 48000, 2) == invalidType); + Require(sceAudioInOpenEx(user, 0, 0, 7, 512, 48000, 2) == invalidSize); + Require(sceAudioInOpenEx(user, 0, 0, 7, 256, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpenEx(user, 0, 0, 7, 128, 44100, 2) == invalidFreq); + int port = sceAudioInOpenEx(user, 0, 0, 7, 256, 48000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + port = sceAudioInHqOpenEx(user, 0, 0, 7, 256, 16000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + Require(sceAudioInSetCompressorPreGain() == 0 && sceAudioInSetNoiseGateThreshold() == 0); +} + void TestValidation() { constexpr int invalidHandle = static_cast(0x80260101); constexpr int invalidSize = static_cast(0x80260102); @@ -109,15 +124,6 @@ void TestValidation() { port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 0x10); gain = 0.5f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); - Require(sceAudioInOpenEx(user, 2, 0, 7, 256, 48000, 2) == invalidType); - Require(sceAudioInOpenEx(user, 0, 0, 7, 512, 48000, 2) == invalidSize); - Require(sceAudioInOpenEx(user, 0, 0, 7, 256, 44100, 2) == invalidFreq); - Require(sceAudioInHqOpenEx(user, 0, 0, 7, 128, 44100, 2) == invalidFreq); - port = sceAudioInOpenEx(user, 0, 0, 7, 256, 48000, 2); - Require(port > 0 && sceAudioInClose(port) == 0); - port = sceAudioInHqOpenEx(user, 0, 0, 7, 256, 16000, 2); - Require(port > 0 && sceAudioInClose(port) == 0); - Require(sceAudioInSetCompressorPreGain() == 0 && sceAudioInSetNoiseGateThreshold() == 0); Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); } @@ -138,6 +144,7 @@ void TestCapture() { SetEnvironment("SDL_DISKAUDIOFILEIN", path.string()); TestValidation(); + TestOpenEx(); const int handle = sceAudioInOpen(user, 0, 0, 256, 48000, 2); Require(handle > 0 && sceAudioInGetSilentState(handle) == 0); std::vector block(256 * 2 + 1, 0x5555); @@ -165,6 +172,7 @@ void TestNoDevice() { SetEnvironment("SDL_AUDIODRIVER", "none"); TestValidation(); + TestOpenEx(); const int handle = sceAudioInOpen(user, 0, 0, 256, 16000, 0x12); Require(handle > 0 && sceAudioInGetSilentState(handle) == 1); Require(sceAudioInGetSilentState(handle + 1) == invalidHandle); From 1be7853e0ab14e55f86d7f467021303c96953707 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 19:51:30 +1300 Subject: [PATCH 0271/1360] test(libSceScreenShot): give each test process its own file names Both CTest entries run the same executable, and std::rand() starts from the same seed in each, so parallel runs shared and removed each other's files. The names now carry the mode and a random_device value. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestScreenShot.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestScreenShot.cpp b/core/libs/tests/GuestScreenShot.cpp index 29355509ff..836b70da94 100644 --- a/core/libs/tests/GuestScreenShot.cpp +++ b/core/libs/tests/GuestScreenShot.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include extern "C" { @@ -27,6 +28,7 @@ constexpr int ErrorOverlayFile = static_cast(0x80BE0003); constexpr int ErrorOverlaySize = static_cast(0x80BE000E); alignas(16) std::array process{}; +std::string pathPrefix; void Require(bool condition, const char* message) { if (condition) return; @@ -45,7 +47,7 @@ void InstallProcessParameters(std::uint32_t sdkVersion) { class TempPath { public: - explicit TempPath(const char* tag) : name(std::string("anyps5-screenshot-") + tag + "-" + std::to_string(std::rand())) {} + explicit TempPath(const char* tag) : name(pathPrefix + tag) {} ~TempPath() { std::error_code error; std::filesystem::remove_all(name, error); @@ -113,6 +115,7 @@ void TestOverlay(bool large) { int main(int argc, char** argv) { const bool large = argc > 1 && std::strcmp(argv[1], "large") == 0; + pathPrefix = std::string("anyps5-screenshot-") + (large ? "large-" : "small-") + std::to_string(std::random_device{}()) + "-"; InstallProcessParameters(large ? 0x3500000u : 0x34fffffu); TestDisableState(); TestOverlay(large); From fde789cca887e37e059d98c4b880af1a8252d80c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 19:52:47 +1300 Subject: [PATCH 0272/1360] test(libkernel): assert sceCoredumpSetUserDataType only for type 0 Which types the console kernel accepts is not measured, so the test no longer asserts success for -1 and INT_MAX. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestCoredumpUserData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index bc23674815..2b363d6c07 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -62,7 +62,7 @@ int main() { const char text[] = "coredump text\n"; Require(NotInCoredumpHandler < 0); - for (const int type : {0, 1, 15, -1, 0x7fffffff}) Require(sceCoredumpSetUserDataType(type) == 0); + Require(sceCoredumpSetUserDataType(0) == 0); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); From 9f32653dcf6564b397a9301af1f27e2bd08f5fc3 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:21:52 +0530 Subject: [PATCH 0273/1360] refactor(libSceZlib): track results by request ID Keep finished requests in a map keyed by request ID and their IDs in a completion queue instead of a single optional result. This prepares for several outstanding requests. sceZlibWaitForDone now pops the completion queue, so it returns each finished request once and no longer depends on whether its result was read, as in shadPS4's done queue. A second wait for the same request throws as before when nothing else is pending. --- core/libs/prx/libSceZlib/Export.cpp | 42 ++++++++++++++++++----------- core/libs/tests/GuestZlib.cpp | 1 + 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 22135e465c..497edffc2e 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "zlib.h" +#include #include +#include #include #include -#include #include +#include namespace { @@ -16,7 +18,6 @@ constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); constexpr std::int32_t FATAL = static_cast(0x811200FF); struct Result { - std::uint64_t id; std::uint32_t produced; std::int32_t status; bool retrieved = false; @@ -25,7 +26,12 @@ struct Result { std::mutex serviceMutex; bool initialized = false; std::uint64_t nextId = 1; -std::optional result; +std::unordered_map results; +std::deque completed; + +bool hasUnreadResult() { + return std::any_of(results.begin(), results.end(), [](const auto& entry) { return !entry.second.retrieved; }); +} } // namespace @@ -42,8 +48,9 @@ std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t lengt std::int32_t APS5_VABI sceZlibFinalize() { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (result && !result->retrieved) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); - result.reset(); + if (hasUnreadResult()) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); + results.clear(); + completed.clear(); initialized = false; return 0; } @@ -56,7 +63,7 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe if (!source || !sourceLength || !destination || !requestId || !destinationLength || destinationLength > 65536 || reinterpret_cast(destination) % 2048 != 0) throw std::runtime_error("sceZlibInflate: invalid buffer or size"); - if (result && !result->retrieved) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); + if (hasUnreadResult()) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); if (nextId == std::numeric_limits::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); z_stream stream{}; stream.next_in = const_cast(static_cast(source)); @@ -70,8 +77,10 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe (decoded == Z_BUF_ERROR && stream.avail_out == 0 && stream.avail_in != 0 ? NO_SPACE : FATAL); const int ended = inflateEnd(&stream); if (ended != Z_OK) throw std::runtime_error("sceZlibInflate: decoder cleanup failed"); - result = Result{nextId++, produced, status}; - *requestId = result->id; + const std::uint64_t id = nextId++; + results[id] = Result{produced, status}; + completed.push_back(id); + *requestId = id; return 0; } @@ -79,8 +88,9 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_ std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); - if (!requestId || !result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); - *requestId = result->id; + if (!requestId || completed.empty()) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + *requestId = completed.front(); + completed.pop_front(); return 0; } @@ -88,12 +98,14 @@ std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* std::int32_t* status) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (!destinationLength || !status || !result || result->id != requestId) + const auto found = results.find(requestId); + if (!destinationLength || !status || found == results.end()) throw std::runtime_error("sceZlibGetResult: invalid request or output"); - if (result->retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); - *destinationLength = result->produced; - *status = result->status; - result->retrieved = true; + Result& result = found->second; + if (result.retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); + *destinationLength = result.produced; + *status = result.status; + result.retrieved = true; return 0; } diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 50a9688420..6ea8596699 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -52,6 +52,7 @@ int main() { std::uint64_t completed; Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); + RequireThrows([&] { sceZlibWaitForDone(&completed, nullptr); }); RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); Require(sceZlibGetResult(id, &produced, &status) == 0); Require(status == expectedStatus && produced <= capacity); From 7322375923409d40baa4d3281810362245456182 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:22:32 +0530 Subject: [PATCH 0274/1360] feat(libSceZlib): accept requests while earlier results are unread sceZlibInflate threw when an earlier request's result had not been read. Accept the request instead: every finished request keeps its result, and sceZlibWaitForDone returns the finished IDs in submission order, as shadPS4's done queue does. The PS5 completion order is unverified and recorded in the technical debt entry. --- core/libs/prx/libSceZlib/Export.cpp | 1 - core/libs/tests/GuestZlib.cpp | 16 +++++++++++++++- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 497edffc2e..6c284515e7 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -63,7 +63,6 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe if (!source || !sourceLength || !destination || !requestId || !destinationLength || destinationLength > 65536 || reinterpret_cast(destination) % 2048 != 0) throw std::runtime_error("sceZlibInflate: invalid buffer or size"); - if (hasUnreadResult()) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); if (nextId == std::numeric_limits::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); z_stream stream{}; stream.next_in = const_cast(static_cast(source)); diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 6ea8596699..1b4dd2369a 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -45,7 +45,6 @@ int main() { Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); Require(id > previous); previous = id; - RequireThrows([&] { sceZlibInflate(input, inputLength, output, capacity, &id); }); RequireThrows([&] { sceZlibFinalize(); }); std::uint32_t timeout = 0; RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); @@ -71,6 +70,21 @@ int main() { decode(compressed.data(), compressed.size() - 1, 65536, static_cast(0x811200FF)); decode(compressed.data(), compressed.size() - 4, expectedLength, static_cast(0x811200FF)); decode(compressed.data(), compressed.size() - 1, expectedLength, static_cast(0x811200FF)); + alignas(2048) std::array batch{}; + std::array ids{}; + for (std::size_t index = 0; index < ids.size(); ++index) + Require(sceZlibInflate(compressed.data(), compressed.size(), batch.data() + index * 8192, 8192, &ids[index]) == 0); + Require(ids[0] < ids[1] && ids[1] < ids[2]); + for (std::uint64_t expected : ids) { + std::uint64_t completed = 0; + Require(sceZlibWaitForDone(&completed, nullptr) == 0); + Require(completed == expected); + } + for (std::size_t index = 0; index < ids.size(); ++index) { + Require(sceZlibGetResult(ids[index], &produced, &status) == 0); + Require(status == 0 && produced == expectedLength); + Require(std::memcmp(batch.data() + index * 8192, text, sizeof(text) - 1) == 0); + } Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 5d289d575b..2acdecf034 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits and one result retrieval. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. Timed waits, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From a5279134a199dfeab58e42963d13e675dc1aa8d6 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:22:53 +0530 Subject: [PATCH 0275/1360] feat(libSceZlib): allow repeated result retrieval sceZlibGetResult threw on a second call for the same request and on an ID it did not hold. Return the same length and status on every call, and 0x81120002 (not found) for an unknown ID, as shadPS4 does. Null output pointers still throw. --- core/libs/prx/libSceZlib/Export.cpp | 6 +++--- core/libs/tests/GuestZlib.cpp | 8 ++++++-- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 6c284515e7..b0943dd9cb 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -12,6 +12,7 @@ namespace { static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof(std::int32_t) == 4); +constexpr std::int32_t NOT_FOUND = static_cast(0x81120002); constexpr std::int32_t NOT_INITIALIZED = static_cast(0x81120032); constexpr std::int32_t ALREADY_INITIALIZED = static_cast(0x81120033); constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); @@ -97,11 +98,10 @@ std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* std::int32_t* status) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; + if (!destinationLength || !status) throw std::runtime_error("sceZlibGetResult: invalid output"); const auto found = results.find(requestId); - if (!destinationLength || !status || found == results.end()) - throw std::runtime_error("sceZlibGetResult: invalid request or output"); + if (found == results.end()) return NOT_FOUND; Result& result = found->second; - if (result.retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); *destinationLength = result.produced; *status = result.status; result.retrieved = true; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 1b4dd2369a..1f5c9a3dda 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -52,10 +52,14 @@ int main() { Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); RequireThrows([&] { sceZlibWaitForDone(&completed, nullptr); }); - RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); + Require(sceZlibGetResult(id + 1, &produced, &status) == static_cast(0x81120002)); + RequireThrows([&] { sceZlibGetResult(id, nullptr, &status); }); Require(sceZlibGetResult(id, &produced, &status) == 0); Require(status == expectedStatus && produced <= capacity); - RequireThrows([&] { sceZlibGetResult(id, &produced, &status); }); + std::uint32_t producedAgain = 0; + std::int32_t statusAgain = 0; + Require(sceZlibGetResult(id, &producedAgain, &statusAgain) == 0); + Require(producedAgain == produced && statusAgain == status); Require(std::all_of(storage.begin(), storage.begin() + 2048, [](auto byte) { return byte == 0xA5; })); Require(std::all_of(storage.begin() + 2048 + capacity, storage.end(), [](auto byte) { return byte == 0xA5; })); }; diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 2acdecf034..6a16242558 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits and one result retrieval. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. Timed waits, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Timed waits and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 18e406653077bc4c1ba306f97745a1652a7dc501 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:23:19 +0530 Subject: [PATCH 0276/1360] feat(libSceZlib): discard unread results on finalize sceZlibFinalize threw while a finished request's result was unread. Discard the stored results and the finished IDs not yet returned by sceZlibWaitForDone instead. shadPS4 also returns success here and never reports the abandoned requests; its next initialization clears them. An ID from before finalization reports 0x81120002 afterwards. --- core/libs/prx/libSceZlib/Export.cpp | 13 ++----------- core/libs/tests/GuestZlib.cpp | 5 ++++- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index b0943dd9cb..743dd271b1 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,6 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "zlib.h" -#include #include #include #include @@ -21,7 +20,6 @@ constexpr std::int32_t FATAL = static_cast(0x811200FF); struct Result { std::uint32_t produced; std::int32_t status; - bool retrieved = false; }; std::mutex serviceMutex; @@ -30,10 +28,6 @@ std::uint64_t nextId = 1; std::unordered_map results; std::deque completed; -bool hasUnreadResult() { - return std::any_of(results.begin(), results.end(), [](const auto& entry) { return !entry.second.retrieved; }); -} - } // namespace extern "C" { @@ -49,7 +43,6 @@ std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t lengt std::int32_t APS5_VABI sceZlibFinalize() { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (hasUnreadResult()) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); results.clear(); completed.clear(); initialized = false; @@ -101,10 +94,8 @@ std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* if (!destinationLength || !status) throw std::runtime_error("sceZlibGetResult: invalid output"); const auto found = results.find(requestId); if (found == results.end()) return NOT_FOUND; - Result& result = found->second; - *destinationLength = result.produced; - *status = result.status; - result.retrieved = true; + *destinationLength = found->second.produced; + *status = found->second.status; return 0; } diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 1f5c9a3dda..b847e3dd70 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -45,7 +45,6 @@ int main() { Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); Require(id > previous); previous = id; - RequireThrows([&] { sceZlibFinalize(); }); std::uint32_t timeout = 0; RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); std::uint64_t completed; @@ -89,6 +88,10 @@ int main() { Require(status == 0 && produced == expectedLength); Require(std::memcmp(batch.data() + index * 8192, text, sizeof(text) - 1) == 0); } + Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); + Require(sceZlibFinalize() == 0); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120002)); Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 6a16242558..d56d88c21f 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Timed waits and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. Timed waits throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 4020c844cc5b079506914e218f7a6ad68ebaaeea Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:24:05 +0530 Subject: [PATCH 0277/1360] feat(libSceZlib): support timed waits in sceZlibWaitForDone sceZlibWaitForDone threw for any timeout and when no finished request was pending. Wait on a condition variable that sceZlibInflate signals: a null timeout blocks until a request finishes, and a timeout returns 0x81120027 (timed out) on expiry, as in shadPS4. The timeout is read as microseconds, as SharpProspero documents for the PS5; shadPS4 reads milliseconds. --- core/libs/prx/libSceZlib/Export.cpp | 13 ++++++++++--- core/libs/tests/GuestZlib.cpp | 22 +++++++++++++++++++--- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 743dd271b1..1bb8114ec7 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "zlib.h" +#include +#include #include #include #include @@ -14,6 +16,7 @@ static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof constexpr std::int32_t NOT_FOUND = static_cast(0x81120002); constexpr std::int32_t NOT_INITIALIZED = static_cast(0x81120032); constexpr std::int32_t ALREADY_INITIALIZED = static_cast(0x81120033); +constexpr std::int32_t TIMED_OUT = static_cast(0x81120027); constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); constexpr std::int32_t FATAL = static_cast(0x811200FF); @@ -27,6 +30,7 @@ bool initialized = false; std::uint64_t nextId = 1; std::unordered_map results; std::deque completed; +std::condition_variable completion; } // namespace @@ -73,15 +77,18 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe const std::uint64_t id = nextId++; results[id] = Result{produced, status}; completed.push_back(id); + completion.notify_one(); *requestId = id; return 0; } std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_t* timeout) { - std::lock_guard lock(serviceMutex); + std::unique_lock lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); - if (!requestId || completed.empty()) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + if (!requestId) throw std::runtime_error("sceZlibWaitForDone: invalid output"); + const auto finished = [] { return !completed.empty(); }; + if (!timeout) completion.wait(lock, finished); + else if (!completion.wait_for(lock, std::chrono::microseconds(*timeout), finished)) return TIMED_OUT; *requestId = completed.front(); completed.pop_front(); return 0; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index b847e3dd70..4422d865ea 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include #include +#include extern "C" { std::int32_t APS5_VABI sceZlibInitialize(const void*, std::uint64_t); @@ -45,12 +47,11 @@ int main() { Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); Require(id > previous); previous = id; - std::uint32_t timeout = 0; - RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); std::uint64_t completed; Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); - RequireThrows([&] { sceZlibWaitForDone(&completed, nullptr); }); + std::uint32_t timeout = 0; + Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); Require(sceZlibGetResult(id + 1, &produced, &status) == static_cast(0x81120002)); RequireThrows([&] { sceZlibGetResult(id, nullptr, &status); }); Require(sceZlibGetResult(id, &produced, &status) == 0); @@ -88,6 +89,21 @@ int main() { Require(status == 0 && produced == expectedLength); Require(std::memcmp(batch.data() + index * 8192, text, sizeof(text) - 1) == 0); } + std::uint64_t completed = 0; + std::uint32_t timeout = 0; + Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); + Require(sceZlibWaitForDone(&completed, &timeout) == 0); + Require(completed == id); + timeout = 20000; + const auto start = std::chrono::steady_clock::now(); + Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); + const auto waited = std::chrono::steady_clock::now() - start; + Require(waited >= std::chrono::microseconds(20000) && waited < std::chrono::seconds(1)); + std::thread waiter([&] { Require(sceZlibWaitForDone(&completed, nullptr) == 0); }); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); + waiter.join(); + Require(completed == id); Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); Require(sceZlibFinalize() == 0); Require(sceZlibInitialize(nullptr, 0) == 0); diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index d56d88c21f..19a43d2761 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. Timed waits throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 729723a60f1414fbd36d9280b90c9fb614a018a5 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:24:48 +0530 Subject: [PATCH 0278/1360] feat(libSceZlib): throw when finalize interrupts a waiter A thread blocked in sceZlibWaitForDone was never woken by sceZlibFinalize, so it waited for its whole timeout or forever. Finalization now wakes every waiter, and a waiter that sees the service finalized throws, since no source says what the console returns. shadPS4 leaves such a waiter blocked. --- core/libs/prx/libSceZlib/Export.cpp | 7 ++++++- core/libs/tests/GuestZlib.cpp | 14 ++++++++++++++ docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 1bb8114ec7..1f43d71b4f 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -28,6 +28,7 @@ struct Result { std::mutex serviceMutex; bool initialized = false; std::uint64_t nextId = 1; +std::uint64_t session = 0; std::unordered_map results; std::deque completed; std::condition_variable completion; @@ -50,6 +51,8 @@ std::int32_t APS5_VABI sceZlibFinalize() { results.clear(); completed.clear(); initialized = false; + ++session; + completion.notify_all(); return 0; } @@ -86,9 +89,11 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_ std::unique_lock lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; if (!requestId) throw std::runtime_error("sceZlibWaitForDone: invalid output"); - const auto finished = [] { return !completed.empty(); }; + const std::uint64_t waitSession = session; + const auto finished = [waitSession] { return !completed.empty() || session != waitSession; }; if (!timeout) completion.wait(lock, finished); else if (!completion.wait_for(lock, std::chrono::microseconds(*timeout), finished)) return TIMED_OUT; + if (session != waitSession) throw std::runtime_error("sceZlibWaitForDone: finalization during a wait semantics unknown"); *requestId = completed.front(); completed.pop_front(); return 0; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 4422d865ea..37f445e8ae 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -108,7 +108,21 @@ int main() { Require(sceZlibFinalize() == 0); Require(sceZlibInitialize(nullptr, 0) == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120002)); + bool interrupted = false; + std::thread interruptedWaiter([&] { + std::uint32_t longTimeout = 5000000; + const auto waitStart = std::chrono::steady_clock::now(); + try { + interrupted = sceZlibWaitForDone(&completed, &longTimeout) == static_cast(0x81120032); + } catch (const std::runtime_error&) { + interrupted = true; + } + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(1)); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); Require(sceZlibFinalize() == 0); + interruptedWaiter.join(); + Require(interrupted); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; Require(sceZlibInitialize(scratch.data(), scratch.size()) == 0); diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 19a43d2761..fdd75fb074 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. A wait interrupted by `sceZlibFinalize` throws: shadPS4 leaves such a waiter blocked, and the console's result is unknown. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 704e872d36ede3777a1ae765aaf0695091195cf1 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:25:29 +0530 Subject: [PATCH 0279/1360] docs(libSceZlib): state the remaining gaps first Rewrite the technical debt entry to list what is unverified on the PS5 and what still throws, with the source each behaviour follows, and drop the descriptions of behaviour that is no longer a gap. The entry is now under the 1000 character limit. --- docs/dev/debt/libSceZlib.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index fdd75fb074..758138279b 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. A wait interrupted by `sceZlibFinalize` throws: shadPS4 leaves such a waiter blocked, and the console's result is unknown. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) is unverified on PS5: several outstanding requests in submission order, repeated `sceZlibGetResult`, `0x81120002` for an unknown ID, `0x81120027` on timeout, results dropped by `sceZlibFinalize` and status codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp) (PS4); the microsecond timeout, 64 KiB output limit and 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs). `0x8112000B` (not ready) is never returned: requests finish at submission. Invalid arguments and a wait interrupted by `sceZlibFinalize` throw; shadPS4 returns `0x81120016` and requires output lengths in 2 KiB multiples. Error precedence and scratch memory needs are unknown. The payload SDK's kernel `z_stream` and errno are not this ABI. From 4c2dc2e99f1f4403d868a580591cee3942f28d84 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:59:29 +0200 Subject: [PATCH 0280/1360] fix(agc): read 128-bit T#s that start past MAX_MIP from their resident levels An R128 image access reads only words 0-3 of its T#; the recompiler passes words 4-7 as zero, so the decoder saw MAX_MIP 0 and refused a 128-bit T# with BASE_LEVEL above 0 ("starts past the surface's last mip level at levels that would move the surface's own"). Streamed textures such as a 2048x2048 BC3 T# naming its resident levels 2..11 hit this. Where a T# with words 4-7 zero would be refused that way, it is now read as a 128-bit T#: MAX_MIP = LAST_LEVEL, with levels BASE..LAST described as their own chain (size and MIN_LOD rebased), but only for 1D and 2D T#s whose rebased levels have the same offsets, sizes and tail slots as in the full chain. Everything else still throws as before. AI-assisted: yes --- .../Graphics/src/GuestTextureResource.cpp | 47 +++++++++++++++++- .../tests/GuestTextureResource.cpp | 49 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3c81c9c97e..7c1ea7aeea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include #include #include @@ -50,6 +52,45 @@ TextureDimension resolveDimension(std::uint32_t raw) { } } +bool ZeroTail(std::span words) { + return words[4] == 0u && words[5] == 0u && words[6] == 0u && words[7] == 0u; +} + +bool SameLevels(const TileMipLayout& a, const TileMipLayout& b) { + return a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.pitchBytes == b.pitchBytes && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY; +} + +std::optional> R128ResidentLevelWords(std::span words) { + const auto baseLevel = (words[3] >> 12u) & 0xfu; + const auto lastLevel = (words[3] >> 16u) & 0xfu; + const auto typeRaw = (words[3] >> 28u) & 0xfu; + if (baseLevel == 0u || baseLevel > lastLevel || (typeRaw != 8u && typeRaw != 9u)) return std::nullopt; + const std::array chainWords{words[0], words[1], words[2], words[3], 0u, lastLevel << 4u, 0u, 0u}; + const auto width = (((words[1] >> 30u) & 0x3u) | ((words[2] & 0xfffu) << 2u)) + 1u; + const auto height = ((words[2] >> 14u) & 0x3fffu) + 1u; + const auto minLod = (words[1] >> 8u) & 0xfffu; + const auto widthMinus1 = std::max(width >> baseLevel, 1u) - 1u; + const auto heightMinus1 = std::max(height >> baseLevel, 1u) - 1u; + const auto rebasedMinLod = minLod > baseLevel * 256u ? minLod - baseLevel * 256u : 0u; + const auto levels = lastLevel - baseLevel; + auto rebased = chainWords; + rebased[1] = (words[1] & ~((0xfffu << 8u) | (0x3u << 30u))) | (rebasedMinLod << 8u) | ((widthMinus1 & 0x3u) << 30u); + rebased[2] = (words[2] & ~(0xfffu | (0x3fffu << 14u))) | ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u); + rebased[3] = (words[3] & ~(0xffu << 12u)) | (levels << 16u); + rebased[5] = levels << 4u; + try { + const auto chain = DescribeSurface(DecodeTextureResource(chainWords)); + const auto view = DescribeSurface(DecodeTextureResource(rebased)); + if (chain.mips.size() != lastLevel + 1u || view.mips.size() != levels + 1u || chain.layers != view.layers) return std::nullopt; + for (std::uint32_t level = baseLevel; level <= lastLevel; ++level) { + if (!SameLevels(chain.mips[level], view.mips[level - baseLevel])) return std::nullopt; + } + } catch (const std::exception&) { + return std::nullopt; + } + return rebased; +} + } bool IsNullTextureResource(std::span words) { @@ -236,7 +277,11 @@ GuestTextureResource DecodeTextureResource(std::span words) result.minLod = minLod; result.samples = samples; if (baseLevel > maxMip) { - Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); + const bool fits = LevelsFitAllocation(result, lastLevel + 1u); + if (!fits && ZeroTail(words)) { + if (const auto resident = R128ResidentLevelWords(words)) return DecodeTextureResource(*resident); + } + Require(fits, "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); result.mipCount = lastLevel + 1u; } return result; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index db38d183f5..13983a2d4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -95,6 +95,52 @@ void rejectFields(const Fields& f, std::string_view reason) { } +static void RunR128TextureTests() { + constexpr std::array streamed{0xa0c18800u, 0xcad00000u, 0x01ffc1ffu, 0x905b2facu, 0u, 0u, 0u, 0u}; + const auto view = DecodeTextureResource(streamed); + Require(view.baseAddress == 0xa0c1880000ull && view.format == 173u && view.tileMode == TextureTileMode::kStandard4KB && view.width == 512u && view.height == 512u && view.baseLevel == 0u && view.lastLevel == 9u && view.mipCount == 10u && view.allocatedMipCount == 10u && view.minLod == 0u, + "a 128-bit BC3 T# (2048x2048, levels 2..11, words 4-7 zero) must read as the 512x512 chain of its 10 resident levels, got " + std::to_string(view.width) + "x" + std::to_string(view.height) + " levels " + std::to_string(view.baseLevel) + ".." + std::to_string(view.lastLevel) + " of " + std::to_string(view.mipCount)); + auto chainWords = streamed; + chainWords[3] &= ~(0xfu << 12u); + chainWords[5] = 11u << 4u; + const auto full = DecodeTextureResource(chainWords); + Require(full.width == 2048u && full.mipCount == 12u, "the same surface with MAX_MIP 11 must be the 12-level 2048x2048 chain"); + const auto chain = DescribeSurface(full); + const auto resident = DescribeSurface(view); + Require(chain.mips.size() == 12u && resident.mips.size() == 10u, "the chain and the resident levels must have 12 and 10 levels"); + for (std::uint32_t level = 0; level < 10u; ++level) { + const auto& a = chain.mips[level + 2u]; + const auto& b = resident.mips[level]; + Require(a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY, "resident level " + std::to_string(level) + " must be level " + std::to_string(level + 2u) + " of the 12-level chain"); + } + Require(resident.guestBytes <= chain.mips[1].tiledOffset && chain.mips[0].tiledOffset > chain.mips[1].tiledOffset, "the resident levels must end before the chain's levels 1 and 0, which come last in a GFX10 swizzled chain"); + auto levelZero = streamed; + levelZero[3] &= ~(0xfu << 12u); + const auto single = DecodeTextureResource(levelZero); + Require(single.mipCount == 1u && single.lastLevel == 0u, "a T# with words 4-7 zero that starts at level 0 must stay one level"); + auto tailed = streamed; + tailed[5] = 4u << 20u; + reject([&] { DecodeTextureResource(tailed); }, "would move the surface's own"); + + constexpr std::array tall{0xa0c1de00u, 0xcad00000u, 0x01ffc0ffu, 0x905b4facu, 0u, 0u, 0u, 0u}; + const auto tallView = DecodeTextureResource(tall); + Require(tallView.width == 64u && tallView.height == 128u && tallView.lastLevel == 7u && tallView.mipCount == 8u, "a 128-bit 1024x2048 T# of levels 4..11 must read as its 64x128 chain of 8 levels"); + + Fields clamped; + clamped.base40 = 0x55ea000ull; + clamped.format = 20; + clamped.width = 1024; + clamped.height = 1024; + clamped.tileModeRaw = 0x1b; + clamped.baseLevel = 2; + clamped.lastLevel = 10; + clamped.minLod = 2u * 256u + 64u; + const auto clampedView = DecodeTextureResource(pack(clamped)); + Require(clampedView.width == 256u && clampedView.mipCount == 9u && clampedView.minLod == 64u, "reading a 128-bit T# from its resident levels must move its MIN_LOD clamp with the base level"); + clamped.minLod = 256u; + Require(DecodeTextureResource(pack(clamped)).minLod == 0u, "a MIN_LOD clamp below the base level must vanish when read from the resident levels"); +} + void RunGuestTextureResourceTests() { Fields base; const auto baseWords = pack(base); @@ -620,6 +666,7 @@ void RunGuestTextureResourceTests() { pastUntailed.maxMip = 0; pastUntailed.baseLevel = 1; pastUntailed.lastLevel = 1; + pastUntailed.perfMod = 4; rejectFields(pastUntailed, "would move the surface's own"); auto untailedChain = tailView; untailedChain.mipCount = 1; @@ -627,6 +674,8 @@ void RunGuestTextureResourceTests() { untailedChain.mipCount = 2; Require(untailedAllocated == 0xff0000u && DescribeSurface(untailedChain).guestBytes == 0x1470000u, "a second level must grow a 1920x1080 64 bpp SW_64KB_R_X surface from addrlib's 0xff0000 to 0x1470000 bytes"); + RunR128TextureTests(); + std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); From 455529710c28c7261e0c9024a62556a29c5fcba6 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 21 Oct 2026 14:42:11 +0800 Subject: [PATCH 0281/1360] feat(libressl3): implement CRYPTO_memcmp --- core/libs/prx/libSceLibreSsl3/CMakeLists.txt | 15 +++ core/libs/prx/libSceLibreSsl3/Export.cpp | 10 +- core/libs/tests/GuestCryptoMemcmp.cpp | 125 +++++++++++++++++++ docs/dev/debt/libSceLibreSsl3.md | 6 +- 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl3/CMakeLists.txt create mode 100644 core/libs/tests/GuestCryptoMemcmp.cpp diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt new file mode 100644 index 0000000000..8fb846319c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLibreSsl3) + +if(BUILD_TESTING) + add_test_executable(guest_crypto_memcmp_tests ${LIBS_INCLUDE_DIR}/tests/GuestCryptoMemcmp.cpp) + target_include_directories(guest_crypto_memcmp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_crypto_memcmp_tests PRIVATE libSceLibreSsl3) + add_test(NAME guest_crypto_memcmp COMMAND guest_crypto_memcmp_tests) + set_tests_properties(guest_crypto_memcmp PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_crypto_memcmp PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp index d3289b659b..e44d5b5673 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -31,9 +31,13 @@ int APS5_VABI BIO_vfree_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_memcmp_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI CRYPTO_memcmp_nid_postfix(const void* first, const void* second, std::size_t length) { + const auto* firstBytes = static_cast(first); + const auto* secondBytes = static_cast(second); + unsigned int difference = 0; + for (std::size_t index = 0; index < length; ++index) + difference |= firstBytes[index] ^ secondBytes[index]; + return static_cast(difference); } int APS5_VABI DH_get0_key_nid_postfix(void) { diff --git a/core/libs/tests/GuestCryptoMemcmp.cpp b/core/libs/tests/GuestCryptoMemcmp.cpp new file mode 100644 index 0000000000..578b4a4645 --- /dev/null +++ b/core/libs/tests/GuestCryptoMemcmp.cpp @@ -0,0 +1,125 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI CRYPTO_memcmp_nid_postfix(const void*, const void*, std::size_t); + +namespace { + +void Require(bool value) { + if (!value) { + std::fprintf(stderr, "CRYPTO_memcmp check failed\n"); + std::abort(); + } +} + +class GuardedPage { +public: + explicit GuardedPage(bool different) { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + pageSize = info.dwPageSize; + memory = static_cast(VirtualAlloc(nullptr, pageSize * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); + DWORD previous; + Require(VirtualProtect(memory + pageSize, pageSize, PAGE_NOACCESS, &previous) != 0); +#else + const long nativePageSize = sysconf(_SC_PAGESIZE); + Require(nativePageSize > 0); + pageSize = static_cast(nativePageSize); + void* allocation = mmap(nullptr, pageSize * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(allocation != MAP_FAILED); + memory = static_cast(allocation); + Require(mprotect(memory + pageSize, pageSize, PROT_NONE) == 0); +#endif + std::memset(memory, 0xA5, pageSize); + if (different) memory[pageSize - 1] ^= 0xFF; +#ifdef _WIN32 + Require(VirtualProtect(memory, pageSize, PAGE_READONLY, &previous) != 0); +#else + Require(mprotect(memory, pageSize, PROT_READ) == 0); +#endif + } + + GuardedPage(const GuardedPage&) = delete; + GuardedPage& operator=(const GuardedPage&) = delete; + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, pageSize * 2); +#endif + } + + const unsigned char* Tail(std::size_t length) const { + Require(length <= pageSize); + return memory + pageSize - length; + } + +private: + unsigned char* memory = nullptr; + std::size_t pageSize = 0; +}; + +} + +int main() { + Require(CRYPTO_memcmp_nid_postfix(nullptr, nullptr, 0) == 0); + unsigned char byte = 0xA5; + Require(CRYPTO_memcmp_nid_postfix(nullptr, &byte, 0) == 0); + Require(CRYPTO_memcmp_nid_postfix(&byte, nullptr, 0) == 0); + for (unsigned int left = 0; left < 256; ++left) { + for (unsigned int right = 0; right < 256; ++right) { + const unsigned char a = static_cast(left); + const unsigned char b = static_cast(right); + Require((CRYPTO_memcmp_nid_postfix(&a, &b, 1) == 0) == (left == right)); + } + } + std::array first; + for (std::size_t index = 0; index < first.size(); ++index) first[index] = static_cast(index * 37); + auto second = first; + const auto original = first; + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data(), first.size()) == 0); + Require(CRYPTO_memcmp_nid_postfix(first.data() + 1, second.data() + 1, 65) == 0); + for (std::size_t index = 1; index <= 65; ++index) { + second[index] ^= 0x80; + const auto changed = second; + Require(CRYPTO_memcmp_nid_postfix(first.data() + 1, second.data() + 1, 65) != 0); + Require(CRYPTO_memcmp_nid_postfix(second.data() + 1, first.data() + 1, 65) != 0); + Require(first == original && second == changed); + second[index] ^= 0x80; + } + second[32] ^= 0x80; + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), 32) == 0); + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), 33) != 0); + second = first; + second[1] ^= 0x80; + second[65] ^= 0x80; + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), first.size()) != 0); + first.fill(0xCC); + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data() + 1, first.size() - 1) == 0); + first[32] = 0xCD; + const auto overlap = first; + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data() + 1, first.size() - 1) != 0); + Require(first == overlap); + const GuardedPage equal(false); + const GuardedPage other(false); + const GuardedPage different(true); + for (const std::size_t length : {0, 1, 2, 7, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129}) { + Require(CRYPTO_memcmp_nid_postfix(equal.Tail(length), other.Tail(length), length) == 0); + Require((CRYPTO_memcmp_nid_postfix(equal.Tail(length), different.Tail(length), length) == 0) == (length == 0)); + } +} diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index 3fbd00e901..b3c055be23 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,4 +2,8 @@ ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 87 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 86 named callable exports, excluding `CRYPTO_memcmp`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. From a3dee63e2643654dfa869d6e655694557a57f4dd Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 01:38:08 -0500 Subject: [PATCH 0282/1360] test(libSceSystemService): stage the Tracy client with the loadexec_arguments libraries With APS5_ENABLE_TRACY=ON libSceAgcDriver.prx imports libTracyClient.dll. The test copies the libraries it is given into its own libs/ directory, so on Windows the relinked app fails to load guest_loadexec_host.prx with error 126. Pass the Tracy client along with the other libraries when it is built. --- core/libs/prx/libSceSystemService/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 3542172d1b..20a06f0cd9 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -22,10 +22,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_loadexec_host) find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_FOUND) + set(loadexecLibraries $ $ $ $) + if(APS5_ENABLE_TRACY) + list(APPEND loadexecLibraries $) + endif() add_test(NAME loadexec_arguments COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_loadexec_arguments.py $ $ - $ $ $ $ ${CMAKE_CXX_COMPILER}) + ${loadexecLibraries} ${CMAKE_CXX_COMPILER}) set_tests_properties(loadexec_arguments PROPERTIES TIMEOUT 30) add_asan_test_options(loadexec_arguments verify_asan_link_order=0) endif() From 60cff59d8ab84082764e7a8a6c2378a99e962cd6 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 07:14:31 +0000 Subject: [PATCH 0283/1360] feat(libSceNpCppWebApi): cover SessionManager PlayerSessionsApi and InGameCatalog ContainerApi imports --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 907 +++++++++++++++++++++ core/libs/tests/GuestNpCppWebApiString.cpp | 72 ++ docs/dev/debt/libSceNpCppWebApi.md | 3 +- 3 files changed, 981 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 5c53984a9f..a56c268f8f 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1940,16 +1940,114 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V118GetFriendsResponse10getFrien return 0; } +// -------------------------------------------------------------------------- +// SessionManager2V1 PlayerSessionsApi + InGameCatalog2V5 ContainerApi and Common +// container helpers imported by a title (PPSA20397): request entry points and +// transactions fail SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN offline; parameter +// bookkeeping succeeds; response accessors stay unimplemented. +// -------------------------------------------------------------------------- + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBody14setInvitationsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_30RequestPlayerSessionInvitationEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28sendPlayerSessionInvitationsEiRKNS4_39ParameterToSendPlayerSessionInvitationsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_50PostPlayerSessionsSessionIdInvitationsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi17getPlayerSessionsEiRKNS4_28ParameterToGetPlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_29GetPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession14setCustomData2EPKvm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession14setCustomData1EPKvm() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead16customData2IsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V124GetBlockingUsersResponse9getBlocksEv() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V146PatchPlayerSessionsSessionIdRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V137RequestPlayerSessionInvitationFactory6createEPNS1_6Common10LibContextENS5_12IntrusivePtrINS3_2ToEEEPNS8_INS3_30RequestPlayerSessionInvitationEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139PatchPlayerSessionsSessionIdRequestBody19setJoinableUserTypeERKNS3_22CustomJoinableUserTypeE() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V127ResponsePlayerSessionPlayer11getPlatformEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetBasicPresencesResponse17getBasicPresencesEv() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9setfieldsENS1_6Common12IntrusivePtrINS6_6VectorINS6_6StringEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead14getCustomData1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } @@ -1967,6 +2065,152 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBloc return 0; } +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions9terminateEv() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead16customData1IsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38addPlayerSessionJoinableSpecifiedUsersEiRKNS4_49ParameterToAddPlayerSessionJoinableSpecifiedUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_61PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead9getLeaderEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead28getExclusiveLeaderPrivilegesEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi26setPlayerSessionPropertiesEiRKNS4_37ParameterToSetPlayerSessionPropertiesERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi25changePlayerSessionLeaderEiRKNS4_36ParameterToChangePlayerSessionLeaderERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V150PutPlayerSessionsSessionIdLeaderRequestBodyFactory6createEPNS1_6Common10LibContextERKNS_4Json5ValueEPNS5_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V119JoinableUserFactory6createEPNS1_6Common10LibContextERKmPNS5_12IntrusivePtrINS3_12JoinableUserEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V156PostPlayerSessionsSessionIdInvitationsRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V133ResponsePlayerSessionMemberPlayer10getPlayersEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139PatchPlayerSessionsSessionIdRequestBody28setExclusiveLeaderPrivilegesERKNS1_6Common6VectorINS5_6StringEEE() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V113PlayerSession9getMemberEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V19ToFactory6createEPNS1_6Common10LibContextERKmPNS5_12IntrusivePtrINS3_2ToEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V167PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_12JoinableUserEEEEEPNS9_INS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties9terminateEv() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V118LeaderWithOnlineId12getAccountIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139PatchPlayerSessionsSessionIdRequestBody20setInvitableUserTypeERKNS3_23CustomInvitableUserTypeE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V129GetPlayerSessionsResponseBody17getPlayerSessionsEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersC1Ev() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead14getCustomData2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V127ResponsePlayerSessionPlayer12getAccountIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V112JoinableUserEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void* self) { (void)self; return 0; @@ -2127,11 +2371,108 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_ return 0; } +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V118LeaderWithOnlineIdEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V160PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V143PutPlayerSessionsSessionIdLeaderRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorImED2Ev(void* self) { (void)self; return 0; } +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEC1ERS7_(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEaSERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x00, __func__); + CopyIntrusivePtr(self, source, 0x00); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V160PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEC1ERS7_(IntrusivePtr* self) { + *self = {}; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V120PlayerSessionForReadEEEEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { return 0; } @@ -2197,6 +2538,11 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPK return 0; } +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE3endEv() { NotImplemented_nid_no_patch(__func__); return 0; @@ -2333,28 +2679,589 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserP return 0; } +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEEE5emptyEv(const IntrusivePtr* self) { + const auto* body = static_cast(self->object); + if (body == nullptr) return 1; + if (body->magic != VectorBodyMagic) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return body->size == 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V143PutPlayerSessionsSessionIdLeaderRequestBodyEEC1ERS7_(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V139PatchPlayerSessionsSessionIdRequestBodyEEC1ERS7_(IntrusivePtr* self) { + *self = {}; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V139PatchPlayerSessionsSessionIdRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x00, __func__); + CopyIntrusivePtr(self, source, 0x00); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEEC1ERS7_(IntrusivePtr* self) { + *self = {}; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V160PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEdeEv() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEixEm(IntrusivePtr* self, unsigned long index) { + auto* body = static_cast(self->object); + if (body == nullptr || body->magic != VectorBodyMagic || body->elementKind != 1) { + NotImplemented_nid_no_patch(__func__); + return nullptr; + } + return static_cast(body->elements) + index; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V143PutPlayerSessionsSessionIdLeaderRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEneERKS9_() { NotImplemented_nid_no_patch(__func__); return 0; } +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V139PatchPlayerSessionsSessionIdRequestBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V139PatchPlayerSessionsSessionIdRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V133ResponsePlayerSessionMemberPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V133ResponsePlayerSessionMemberPlayerEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEdeEv() { NotImplemented_nid_no_patch(__func__); return 0; } +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V118LeaderWithOnlineIdEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEneERKS4_() { NotImplemented_nid_no_patch(__func__); return 0; } + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEE4sizeEv(const IntrusivePtr* self) { + const auto* body = static_cast(self->object); + if (body == nullptr) return 0; + if (body->magic != VectorBodyMagic) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return body->size; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V120PlayerSessionForReadEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEE8copyFromERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku10labelIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku10priceIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku8getPriceEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku15getDisplayPriceEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer7setsortENS5_4SortE() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku18originalPriceIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product14getDescriptionEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product14getReleaseDateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku23getDisplayOriginalPriceEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product14getDisplayNameEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku16getOriginalPriceEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku25displayOriginalPriceIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku8getLabelEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9setoffsetEi() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product16releaseDateIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setcontainerIdsEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer8setlimitEi() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku17displayPriceIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEaSERKSE_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE5emptyEv(const IntrusivePtr* self) { + const auto* body = static_cast(self->object); + if (body == nullptr) return 1; + if (body->magic != VectorBodyMagic) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return body->size == 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEneERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEEneERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V53SkuEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1ERKSA_(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1ERKSE_(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE5emptyEv(const IntrusivePtr* self) { + const auto* body = static_cast(self->object); + if (body == nullptr) return 1; + if (body->magic != VectorBodyMagic) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return body->size == 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x00, __func__); + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_6StringEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8copyFromERKS5_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, const char* other) { + return self->buffer != nullptr && other != nullptr && std::strcmp(self->buffer, other) == 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x30, __func__); + CopyIntrusivePtr(self, source, 0x30); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5emptyEv(const CommonString* self) { + return self->buffer == nullptr || self->buffer[0] == '\0'; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp index 18a4dd25a6..80899cf24e 100644 --- a/core/libs/tests/GuestNpCppWebApiString.cpp +++ b/core/libs/tests/GuestNpCppWebApiString.cpp @@ -13,6 +13,13 @@ const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const void* se void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(void* self, void* libContext); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(void* self, const void* element); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5emptyEv(const void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const void* self, const char* other); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEEC1EPNS2_10LibContextE(void* self, void* libContext); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEED1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(void* self, const void* element); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi17getPlayerSessionsEiRKNS4_28ParameterToGetPlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_29GetPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); } namespace { @@ -180,6 +187,68 @@ void TestVectorPushBackCopiesStringContent() { Require(VectorGuardIntact(vector)); } +void TestStringEmptyAndEquals() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + Require(_ZNK3sce2Np9CppWebApi6Common6String5emptyEv(storage.bytes) == 1); + Require(_ZN3sce2Np9CppWebApi6Common6StringeqEPKc(storage.bytes, "rank") == 0); + + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, "rank") == 0); + Require(_ZNK3sce2Np9CppWebApi6Common6String5emptyEv(storage.bytes) == 0); + Require(_ZN3sce2Np9CppWebApi6Common6StringeqEPKc(storage.bytes, "rank") == 1); + Require(_ZN3sce2Np9CppWebApi6Common6StringeqEPKc(storage.bytes, "rank2") == 0); + Require(_ZN3sce2Np9CppWebApi6Common6StringeqEPKc(storage.bytes, nullptr) == 0); + Require(GuardIntact(storage)); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); +} + +int g_elementDeleterCalls = 0; +void APS5_VABI ElementDeleter(void*) { ++g_elementDeleterCalls; } + +void TestIntrusivePtrVectorPushBack() { + std::uint64_t libContext = 1; + VectorStorage vector; + std::memset(vector.bytes, kFill, sizeof(vector.bytes)); + + alignas(8) unsigned char element[0x18]; + alignas(8) unsigned char elementObject[0x10]; + std::memset(element, 0, sizeof(element)); + std::memset(elementObject, 0, sizeof(elementObject)); + g_elementDeleterCalls = 0; + + void* object = elementObject; + auto* deleter = &ElementDeleter; + std::memcpy(element + 0x00, &object, sizeof(void*)); + std::memcpy(element + 0x08, &deleter, sizeof(void*)); + std::memcpy(element + 0x10, &libContext, sizeof(void*)); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEEC1EPNS2_10LibContextE(vector.bytes, &libContext); + Require(VectorGuardIntact(vector)); + + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(vector.bytes, element) == 0); + + void* body; + std::memcpy(&body, vector.bytes + kObjectOffset, sizeof(body)); + Require(body != nullptr); + Require(LoadBody(body, kBodyReferenceCountOffset) == 1); + Require(LoadBody(body, kBodyElementKindOffset) == 1); + Require(LoadBody(body, kBodySizeOffset) == 1); + Require(*reinterpret_cast(elementObject) == 1); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEED1Ev(vector.bytes); + Require(g_elementDeleterCalls == 1); + Require(VectorGuardIntact(vector)); +} + +void TestOfflineResponses() { + Require(_ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() == 0); + Require(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi17getPlayerSessionsEiRKNS4_28ParameterToGetPlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_29GetPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() == static_cast(0x80552907)); +} + } int main() { @@ -188,4 +257,7 @@ int main() { TestAppendBuildsAndClearsBuffer(); TestAppendNullKeepsEmpty(); TestVectorPushBackCopiesStringContent(); + TestStringEmptyAndEquals(); + TestIntrusivePtrVectorPushBack(); + TestOfflineResponses(); } diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index f18925c392..2df18be6bf 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -13,7 +13,8 @@ - [sce::Np::CppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - every request entry point (API calls, transaction `start`/`getResponse`/`readData`, the `*Factory::create`s) returns `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN` offline. The constant's name and value follow the reference tables in shadPS4 `np_error.h` and fpPS4 `np_error.pas`, where `0x80552907` is `ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN` and `0x80552901` is `OUT_OF_MEMORY` (no `UNAVAILABLE` exists in the `0x80552901`-`0x8055291F` range); it is the code a signed-out console returns from these entry points. The earlier `0x80552901` value came from the module the previous implementation shipped, which used the same incorrect constant - [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 - [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. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count -- [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one +- [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `copyFrom` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one. `empty`, `size` and `operator[]` read the host-created body honestly and throw on title-provided bodies - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them +- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib; the title's relinker registry records the call sites that were missing above. `PlayerSessionsApi`/`ContainerApi` calls, `*Factory::create`s and transaction `start`/`getResponse`/`getResponseHeaders`/`getResponseInfo` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; `Transaction::hasResponse` honestly answers 0, and parameter/model bookkeeping (`initialize`, `terminate`, `set*`, ctors, dtors, `finish`) succeeds without keeping state, matching the sections above. `IntrusivePtr` ctors/dtor/deref/assignment/copy follow the PPSA21564-measured layout; `Vector::empty`/`size`/`operator[]` read the host-created `VectorBody` and throw on a title-provided body whose element layout is unknown. Response-body accessors (`get*`, `*IsSet`), iterators, `Vector::begin`/`end`/`copyFrom` and transaction `abort`/`getId`/`operator=` still throw: a failed request produces no response object to read, and aborting or renumbering a transaction that never ran has no defined outcome From 54f45af1108a031e1817a3a7dda632d5fe2175b2 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 07:20:27 +0000 Subject: [PATCH 0284/1360] fix(libSceNpCppWebApi): drop title ID from section marker and shorten the debt entry --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 6 +++--- docs/dev/debt/libSceNpCppWebApi.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index a56c268f8f..146502cdfc 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1942,9 +1942,9 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V118GetFriendsResponse10getFrien // -------------------------------------------------------------------------- // SessionManager2V1 PlayerSessionsApi + InGameCatalog2V5 ContainerApi and Common -// container helpers imported by a title (PPSA20397): request entry points and -// transactions fail SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN offline; parameter -// bookkeeping succeeds; response accessors stay unimplemented. +// container helpers imported by titles whose dumps lack libSceNpCppWebApi.prx: +// request entry points and transactions fail SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN +// offline; parameter bookkeeping succeeds; response accessors stay unimplemented. // -------------------------------------------------------------------------- int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBody14setInvitationsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_30RequestPlayerSessionInvitationEEEEE() { diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 2df18be6bf..ee3994c483 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -17,4 +17,4 @@ - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them -- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib; the title's relinker registry records the call sites that were missing above. `PlayerSessionsApi`/`ContainerApi` calls, `*Factory::create`s and transaction `start`/`getResponse`/`getResponseHeaders`/`getResponseInfo` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; `Transaction::hasResponse` honestly answers 0, and parameter/model bookkeeping (`initialize`, `terminate`, `set*`, ctors, dtors, `finish`) succeeds without keeping state, matching the sections above. `IntrusivePtr` ctors/dtor/deref/assignment/copy follow the PPSA21564-measured layout; `Vector::empty`/`size`/`operator[]` read the host-created `VectorBody` and throw on a title-provided body whose element layout is unknown. Response-body accessors (`get*`, `*IsSet`), iterators, `Vector::begin`/`end`/`copyFrom` and transaction `abort`/`getId`/`operator=` still throw: a failed request produces no response object to read, and aborting or renumbering a transaction that never ran has no defined outcome +- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `abort`/`getId`/`operator=` throw: a failed request produces no response object, and aborting or renumbering a transaction that never ran has no defined outcome From 00f7cb9c2192ea915345aa4d094def42ba48cbe4 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:41:37 +0300 Subject: [PATCH 0285/1360] fix(shader): restore compaction validation, shift subword loads and gate OpExtInst purity --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 11 +++++ .../tests/execution/BufferFormat.cpp | 16 ++++---- core/shader/recompiler/Recompiler.cpp | 3 ++ .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 7 +--- .../SpirvBackend/src/SpirvSpecialization.cpp | 40 ++++++++++--------- 5 files changed, 47 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 00796cd2b5..d78cb62a9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -322,6 +322,8 @@ void testPrivateCompositeTableLoadFolding() { const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); + const auto glsl = module.Import("GLSL.std.450"); + const auto customSet = module.Import("NonSemantic.Test"); const auto mainId = module.AllocateId(); const auto lane = module.AllocateId(); const auto rawCond = module.AllocateId(); @@ -339,6 +341,11 @@ void testPrivateCompositeTableLoadFolding() { module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); module.AddFunction(spv::OpStore, writePtr, lane); } + const auto liveGlsl = module.AllocateId(); + module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), glsl, 37u, lane, zero); + module.AddFunction(spv::OpExtInst, typeU32, liveGlsl, glsl, 37u, lane, one); + module.AddFunction(spv::OpStore, output, liveGlsl); + module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), customSet, 1u, lane); module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); @@ -357,12 +364,16 @@ void testPrivateCompositeTableLoadFolding() { check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); + check(foldedCounts.contains(spv::OpExtInst) && foldedCounts.at(spv::OpExtInst) == 2u, "an unused GLSL.std.450 OpExtInst was not removed or a live/non-GLSL.std.450 OpExtInst was removed"); bool addedThirty = false; + bool keptCustomExtInst = false; for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { if ((folded[cursor] >> 16u) == 0u) break; if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; + if ((folded[cursor] & 0xffffu) == spv::OpExtInst && folded[cursor + 4u] == 1u) keptCustomExtInst = true; } check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); + check(keptCustomExtInst, "a non-GLSL.std.450 OpExtInst was not preserved"); const auto [dynamic, unusedDynamic] = build(true, false); static_cast(unusedDynamic); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp index 5a93b53303..7394269e39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp @@ -173,7 +173,7 @@ void Check() { constexpr std::array SubwordWord3{0x31005facu, 0x31006facu, 0x3100bfacu, 0x3100cfacu, 0x31012facu}; -constexpr const char* SubwordNames[14] = { +constexpr const char* SubwordNames[16] = { "buffer_load_format_x 8_UINT +1", "buffer_load_format_x 8_UINT +2", "buffer_load_format_x 8_UINT +3", @@ -182,8 +182,10 @@ constexpr const char* SubwordNames[14] = { "buffer_load_format_x 8_SINT +3", "buffer_load_format_x 16_UINT +1", "buffer_load_format_x 16_UINT +2", + "buffer_load_format_x 16_UINT +3", "buffer_load_format_x 16_SINT +1", "buffer_load_format_x 16_SINT +2", + "buffer_load_format_x 16_SINT +3", "buffer_load_format_xy 8_8_UINT +1 x", "buffer_load_format_xy 8_8_UINT +1 y", "buffer_load_format_xy 8_8_UINT +2 x", @@ -200,11 +202,11 @@ std::vector SubwordCode() { }; for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 2u, offset, 1u); for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 3u, offset, 1u); - for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 4u, offset, 1u); - for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 5u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 4u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 5u, offset, 1u); for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(1u, 6u, offset, 2u); code.push_back(0xbf8c3f70u); - for (std::uint32_t index = 0u; index < 14u; ++index) { + for (std::uint32_t index = 0u; index < 16u; ++index) { code.push_back(0xe0701000u | (index * 4u)); code.push_back(0x80010003u | ((10u + index) << 8u)); } @@ -245,11 +247,11 @@ void CheckSubwordOffsets() { const auto s8 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u8(offset)))); }; const auto u16 = [&](std::uint32_t offset) { return (word >> (offset * 8u)) & 0xffffu; }; const auto s16 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u16(offset)))); }; - const std::array expected{ + const std::array expected{ u8(1u), u8(2u), u8(3u), s8(1u), s8(2u), s8(3u), - u16(1u), u16(2u), - s16(1u), s16(2u), + u16(1u), u16(2u), u16(3u), + s16(1u), s16(2u), s16(3u), u8(1u), u8(2u), u8(2u), u8(3u), }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 0d76ccb36d..9246de6bda 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -819,6 +819,9 @@ std::shared_ptr buildSpecializedModule(const CompiledSh cursor += count; } materialized = std::move(compact); +#if ANYPS5_ENABLE_SPIRV_TOOLS + materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false); +#endif auto module = std::make_shared(); std::map bindingNumbers; for (std::size_t cursor = 5; cursor < materialized.size();) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 8ce3cc9ca7..433c58aad1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -470,11 +470,8 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue ? EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); }) - : readBuffer(context, instruction, addresses[component].guest, 32u); - const auto subwordOffset = memory.gpuDescriptor - ? constant(0u) - : Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, addresses[component].guest, 3u), constant(3u)); - const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), subwordOffset); + : Binary(state, spv::OpShiftRightLogical, u32, readBuffer(context, instruction, addresses[component].guest, 32u), Select(state, u32, dword, constant(0u), Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, addresses[component].guest, 3u), constant(3u)))); + const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), constant(0u)); const auto unsignedField = EmitBitFieldUExtract(state, raw, bitOffset, bits[component]); const auto signedField = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), signedField, Unary(state, spv::OpBitcast, TypeI32(state), raw), bitOffset, bits[component]); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 569298ea62..2b7b1eb792 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -85,7 +85,22 @@ Instruction Make(spv::Op op, std::initializer_list operands) { return result; } -bool PureInstruction(const Instruction& instruction) { +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector present; +}; + +bool PureInstruction(const Instruction& instruction, const IdSet& glslExtInstSets = {}) { switch (Opcode(instruction)) { case spv::OpVectorExtractDynamic: case spv::OpVectorInsertDynamic: @@ -203,8 +218,9 @@ bool PureInstruction(const Instruction& instruction) { case spv::OpInBoundsPtrAccessChain: case spv::OpArrayLength: case spv::OpPhi: - case spv::OpExtInst: return true; + case spv::OpExtInst: + return instruction.size() >= 5u && glslExtInstSets.contains(instruction[3]); case spv::OpLoad: return instruction.size() == 4u || (instruction.size() == 5u && instruction[4] == spv::MemoryAccessMaskNone); default: @@ -557,21 +573,6 @@ private: std::size_t count = 0; }; -class IdSet { -public: - void Reset(std::size_t bound) { present.assign(bound, 0u); } - bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } - bool insert(std::uint32_t id) { - if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); - if (present[id] != 0u) return false; - present[id] = 1u; - return true; - } - -private: - std::vector present; -}; - class Specialization { public: explicit Specialization(std::span words) : arena(words.size()) { @@ -582,6 +583,7 @@ public: values.Reset(bound); resultTypes.Reset(bound); vectorTypes.Reset(bound); + glslExtInstSets.Reset(bound); removed.Reset(bound); aliases.Reset(bound); composites.Reset(bound); @@ -596,6 +598,7 @@ public: bool hasType = false; spv::HasResultAndType(op, &hasResult, &hasType); if (hasResult && hasType) resultTypes.emplace(instruction.at(2), instruction.at(1)); + if (op == spv::OpExtInstImport && instruction.size() == 6u && instruction[2] == 0x4c534c47u && instruction[3] == 0x6474732eu && instruction[4] == 0x3035342eu && instruction[5] == 0u) glslExtInstSets.insert(instruction.at(1)); if (op == spv::OpTypeBool) types.emplace(instruction.at(1), ScalarType{1u, true}); if (op == spv::OpTypeInt) types.emplace(instruction.at(1), ScalarType{instruction.at(2), false}); if (op == spv::OpTypeVector) vectorTypes.insert(instruction.at(1)); @@ -1125,7 +1128,7 @@ private: } continue; } - if (PureInstruction(instruction)) { + if (PureInstruction(instruction, glslExtInstSets)) { definitions.emplace(Result(instruction), index); continue; } @@ -1162,6 +1165,7 @@ private: IdTable values; IdTable resultTypes; IdSet vectorTypes; + IdSet glslExtInstSets; IdSet removed; IdTable aliases; IdTable> composites; From d3542d5eed49086e1273da35db0503d543914d21 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:07:16 +0530 Subject: [PATCH 0286/1360] fix(savedata): return parameter errors for null arguments --- core/libs/prx/libSceSaveData.native/Export.cpp | 6 +++--- core/libs/tests/GuestSaveDataDelete.cpp | 3 +++ core/libs/tests/GuestSaveDataMountStatus.cpp | 6 ++++++ docs/dev/debt/libSceSaveData.native.md | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index fa43b4ee0a..70a5dd1754 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -280,7 +280,7 @@ extern "C" { int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { if (backup == nullptr || backup->dir_name == nullptr) { - throw std::runtime_error("sceSaveDataBackup: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } // The system copies the save directory to its backup area asynchronously; the copy itself is // not observable by the title, only its completion event. @@ -327,7 +327,7 @@ static bool isMountedDirectory(const std::string& path) { static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { - throw std::runtime_error("sceSaveDataDelete: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } const auto* nameEnd = static_cast(std::memchr(del->dir_name->data, '\0', sizeof(del->dir_name->data))); if (nameEnd == nullptr) { @@ -477,7 +477,7 @@ int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond, Sa int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* event) { (void)event_param; if (event == nullptr) { - throw std::runtime_error("sceSaveDataGetEventResult: null event"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_state_mutex); if (g_events.empty()) { diff --git a/core/libs/tests/GuestSaveDataDelete.cpp b/core/libs/tests/GuestSaveDataDelete.cpp index 0ebbce1462..4fda8b7d4e 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -89,6 +89,9 @@ int main() { char unterminated[sizeof(SceSaveDataDirName::data)]; std::memset(unterminated, 'a', sizeof(unterminated)); Check(Delete(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + Check(sceSaveDataDelete(nullptr) == SaveDataErrorParameter, "null delete pointer is rejected"); + SaveDataDelete nullDirDelete{}; + Check(sceSaveDataDelete(&nullDirDelete) == SaveDataErrorParameter, "null delete dir_name is rejected"); constexpr char mountedPayload[] = "live mounted save"; constexpr char updatedPayload[] = "still mounted"; diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 5c744255bc..a33ad5960f 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -17,6 +17,8 @@ int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint*, SaveDataIcon*); int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint*, const SaveDataIcon*); +int APS5_VABI sceSaveDataBackup(const SaveDataBackup*); +int APS5_VABI sceSaveDataGetEventResult(const void*, SaveDataEvent*); } namespace { @@ -59,6 +61,10 @@ void CheckUnmountedCalls() { Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataBackup(nullptr) == SaveDataErrorParameter, "backup null backup is PARAMETER"); + SaveDataBackup nullDirBackup{}; + Check(sceSaveDataBackup(&nullDirBackup) == SaveDataErrorParameter, "backup null dir_name is PARAMETER"); + Check(sceSaveDataGetEventResult(nullptr, nullptr) == SaveDataErrorParameter, "getEventResult null event is PARAMETER"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index db7aeb7660..2d1b1d1d66 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -19,6 +19,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob From 4a6febc3f06b0ae9d244795920c2ddcff42bd4ba Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 07:54:30 +0000 Subject: [PATCH 0287/1360] fix(libSceNpCppWebApi): report setPlayerSessionProperties as a failed offline request --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 146502cdfc..15d8b1bf8e 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -2097,7 +2097,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead28get } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi26setPlayerSessionPropertiesEiRKNS4_37ParameterToSetPlayerSessionPropertiesERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() { - return 0; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi25changePlayerSessionLeaderEiRKNS4_36ParameterToChangePlayerSessionLeaderERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() { From 0a2ba6ed9a9fceceeba4f842bd5a352c16641a73 Mon Sep 17 00:00:00 2001 From: Rookie Date: Wed, 21 Oct 2026 06:23:46 +0100 Subject: [PATCH 0288/1360] fix(libc): count call trace offsets from the first page of a non-PIE executable --- core/libs/prx/libc/CMakeLists.txt | 11 +++++++++++ core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index b5ca381e89..fb8f561740 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -667,6 +667,17 @@ if(BUILD_TESTING) set_tests_properties(guest_call_trace_abort_race PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "last traced system library calls[^~]*#102 thread [0-9]+ libTraced.prx Integers from[^~]*most called traced imports \\(102 calls in total\\):[^~]* 101 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_call_trace_fixed_base_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) + target_include_directories(guest_call_trace_fixed_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_call_trace_fixed_base_tests PRIVATE libc) + set_target_properties(guest_call_trace_fixed_base_tests PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_compile_options(guest_call_trace_fixed_base_tests PRIVATE -fno-pie) + target_link_options(guest_call_trace_fixed_base_tests PRIVATE -no-pie -fuse-ld=bfd) + add_test(NAME guest_call_trace_fixed_base_abort COMMAND guest_call_trace_fixed_base_tests abort) + set_tests_properties(guest_call_trace_fixed_base_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "#1 thread [0-9]+ libTraced.prx Integers from 0x00000000004[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] guest_call_trace_fixed_base_tests\\+0x[0-9a-f]?[0-9a-f]?[0-9a-f]?[0-9a-f]?[0-9a-f][^0-9a-f]") + endif() endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp index 96d6c872a9..c81b91a39a 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -117,6 +118,7 @@ struct ModuleSegment { struct ModuleMap { std::size_t count; + std::uint64_t pageSize; ModuleSegment segments[256]; }; @@ -135,16 +137,20 @@ void SnapshotModules() { if (changes == g_moduleChanges) return; g_moduleChanges = changes; auto* map = new ModuleMap{}; + map->pageSize = static_cast(sysconf(_SC_PAGESIZE)); dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { auto& modules = *static_cast(data); const char* path = info->dlpi_name != nullptr && *info->dlpi_name != '\0' ? info->dlpi_name : program_invocation_short_name; const char* slash = std::strrchr(path, '/'); const char* name = slash == nullptr ? path : slash + 1; + std::uint64_t first = std::numeric_limits::max(); + for (int index = 0; index < info->dlpi_phnum; ++index) + if (info->dlpi_phdr[index].p_type == PT_LOAD) first = std::min(first, info->dlpi_phdr[index].p_vaddr & ~(modules.pageSize - 1)); for (int index = 0; index < info->dlpi_phnum && modules.count < std::size(modules.segments); ++index) { const auto& header = info->dlpi_phdr[index]; if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; auto& segment = modules.segments[modules.count++]; - segment.base = info->dlpi_addr; + segment.base = info->dlpi_addr + first; segment.start = info->dlpi_addr + header.p_vaddr; segment.end = segment.start + header.p_memsz; std::strncpy(segment.name, name, sizeof(segment.name) - 1); From afc7e26ce9b9666ca98b4bfdb21c226a3df0b2e5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:32:45 +0000 Subject: [PATCH 0289/1360] fix(libkernel): return EFAULT for invalid getresid pointers --- .../libs/prx/libkernel/System/src/Process.cpp | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 89f55ec429..262531ff8f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -8,6 +8,12 @@ #else #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif #if defined(__linux__) && defined(__x86_64__) #include @@ -18,6 +24,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/HostEntropy.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" @@ -46,6 +53,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, std::uint64_t info_size); namespace { @@ -59,6 +67,37 @@ std::mutex supplementaryGroupsMutex; std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; +bool StoreGuestId(std::uint32_t* output) { + if (output == nullptr) return true; + const auto address = reinterpret_cast(output); + if (address > std::numeric_limits::max() - sizeof(*output)) return false; + const auto end = address + sizeof(*output); + for (auto cursor = address; cursor < end;) { + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(reinterpret_cast(cursor), 0, &info, sizeof(info)) != 0 || + info.end <= cursor || (info.protection & 2) == 0) + return false; + cursor = std::min(end, info.end); + } + const GuestArena::HostWrite guard(output, sizeof(*output)); + if (!guard.Open()) return false; + constexpr std::uint32_t zero = 0; +#ifdef _WIN32 + SIZE_T written = 0; + return WriteProcessMemory(GetCurrentProcess(), output, &zero, sizeof(zero), &written) != 0 && written == sizeof(zero); +#elif defined(__linux__) + iovec local{const_cast(&zero), sizeof(zero)}; + iovec remote{output, sizeof(zero)}; + return ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0) == static_cast(sizeof(zero)); +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(output), + reinterpret_cast(const_cast(&zero)), sizeof(zero)) == KERN_SUCCESS; +#else + *output = zero; + return true; +#endif +} + std::atomic gpoBits{0}; constexpr std::array openPsId{'A', 'n', 'y', 'P', 'S', '5', 'O', 'p', 'e', 'n', 'P', 's', 'I', 'd', 0, 1}; @@ -315,16 +354,18 @@ int APS5_VABI issetugid_nid_postfix(void) { } int APS5_VABI getresuid_nid_postfix(std::uint32_t* ruid, std::uint32_t* euid, std::uint32_t* suid) { - if (ruid != nullptr) *ruid = 0; - if (euid != nullptr) *euid = 0; - if (suid != nullptr) *suid = 0; + if (!StoreGuestId(ruid) || !StoreGuestId(euid) || !StoreGuestId(suid)) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } return 0; } int APS5_VABI getresgid_nid_postfix(std::uint32_t* rgid, std::uint32_t* egid, std::uint32_t* sgid) { - if (rgid != nullptr) *rgid = 0; - if (egid != nullptr) *egid = 0; - if (sgid != nullptr) *sgid = 0; + if (!StoreGuestId(rgid) || !StoreGuestId(egid) || !StoreGuestId(sgid)) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } return 0; } From 30167dace13b80ceac8fe607dfb143eb3f909b9b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:32:49 +0000 Subject: [PATCH 0290/1360] test(libkernel): cover invalid getresid output pointers --- core/libs/tests/GuestCredentials.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index 214cde5292..df8a2966c9 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -115,6 +115,11 @@ int main() { Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); Require(groups[0] == static_cast(getegid_nid_postfix())); + *__error_nid_postfix() = 13; + RequireFailure(getresuid_nid_postfix(reinterpret_cast(1), nullptr, nullptr), GuestEfault); + *__error_nid_postfix() = 13; + RequireFailure(getresgid_nid_postfix(nullptr, reinterpret_cast(1), nullptr), GuestEfault); + const std::array wanted{0, 5, 7}; Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); Require(getgroups_nid_postfix(0, nullptr) == 3); From b68c03b4e40515034bcb149cf1b709f40cdf7fef Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 08:10:32 +0000 Subject: [PATCH 0291/1360] test(agc): check the excluded ABI by its committed state, after its front end The still-waits check in CommittedAbis timed out after 200 ms, which the other thread's front-end build alone could reach before it ever waited on the preparation lock. It now waits until that build has started, gives the thread 500 ms, and also requires the pixel shader's committed ABIs to be unchanged. The build counter is read after background registration preparation has settled, so an earlier test's preparation can't count. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/PreparedShaders.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index b72d48eb52..bbd698465b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1220,7 +1220,17 @@ void CommittedAbis() { scope.Commit(); }; resolve(); + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "committed ABI resolution published no shader registry"); + const auto pixelSnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &pixel.shader); + Require(pixelSnapshot != nullptr, "committed ABI resolution published no pixel snapshot"); + const auto committedAbis = [&] { + std::lock_guard lock(pixelSnapshot->prepared->mutex); + return pixelSnapshot->prepared->registeredAbis.size(); + }; + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); const auto before = ShaderRecompiler::FrontEndBuilds(); + const auto abisBefore = committedAbis(); bool unblocked = false; bool excluded = false; std::uint64_t repeatedBuilds = 0; @@ -1234,7 +1244,9 @@ void CommittedAbis() { unblocked = repeated.wait_for(std::chrono::seconds(10)) == std::future_status::ready; repeatedBuilds = ShaderRecompiler::FrontEndBuilds() - before; other = std::async(std::launch::async, resolveOther); - excluded = other.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + const auto started = std::chrono::steady_clock::now(); + while (ShaderRecompiler::FrontEndBuilds() == before + repeatedBuilds && std::chrono::steady_clock::now() - started < std::chrono::seconds(10)) std::this_thread::yield(); + excluded = other.wait_for(std::chrono::milliseconds(500)) == std::future_status::timeout && committedAbis() == abisBefore; open.Commit(); } repeated.get(); From 7961f813cce28deabe8a8e0bb2a7e431f944c401 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 08:15:58 +0000 Subject: [PATCH 0292/1360] test(libSceNpCppWebApi): pin setPlayerSessionProperties offline error Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/tests/GuestNpCppWebApiString.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp index 80899cf24e..eb127730a9 100644 --- a/core/libs/tests/GuestNpCppWebApiString.cpp +++ b/core/libs/tests/GuestNpCppWebApiString.cpp @@ -20,6 +20,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(void* self, const void* element); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi17getPlayerSessionsEiRKNS4_28ParameterToGetPlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_29GetPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi26setPlayerSessionPropertiesEiRKNS4_37ParameterToSetPlayerSessionPropertiesERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(); } namespace { @@ -247,6 +248,7 @@ void TestIntrusivePtrVectorPushBack() { void TestOfflineResponses() { Require(_ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() == 0); Require(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi17getPlayerSessionsEiRKNS4_28ParameterToGetPlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_29GetPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() == static_cast(0x80552907)); + Require(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi26setPlayerSessionPropertiesEiRKNS4_37ParameterToSetPlayerSessionPropertiesERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() == static_cast(0x80552907)); } } From 91b2911ca1527295007bf656649f6b2909d34aab Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:28:49 +0000 Subject: [PATCH 0293/1360] fix(libkernel): check guest pointers in group credentials --- .../libs/prx/libkernel/System/src/Process.cpp | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 262531ff8f..74a135266f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -95,6 +95,55 @@ bool StoreGuestId(std::uint32_t* output) { #else *output = zero; return true; + #endif +} + +bool GuestRange(const void* pointer, std::size_t bytes) { + const auto start = reinterpret_cast(pointer); + return start != 0 && bytes <= std::numeric_limits::max() - start; +} + +bool ReadGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(source, bytes)) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), source, destination, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const auto copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("credentials: process_vm_readv failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + throw std::runtime_error("credentials: checked guest reads are not implemented on this host"); +#endif +} + +bool WriteGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(destination, bytes)) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + SIZE_T copied = 0; + return WriteProcessMemory(GetCurrentProcess(), destination, source, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + const auto copied = ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("credentials: process_vm_writev failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; +#else + throw std::runtime_error("credentials: checked guest writes are not implemented on this host"); #endif } @@ -834,14 +883,22 @@ int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { *__error_nid_postfix() = errnoBadAddress; return -1; } - if (count > 0 && groups[0] != 0) - throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(groups[0])); + std::vector requested; + if (count > 0) { + requested.resize(static_cast(count)); + if (!ReadGuest(requested.data(), groups, requested.size() * sizeof(requested[0]))) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + } + if (count > 0 && requested[0] != 0) + throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(requested[0])); std::lock_guard lock(supplementaryGroupsMutex); if (count == 0) { supplementaryGroups.resize(1); return 0; } - supplementaryGroups.assign(groups, groups + count); + supplementaryGroups.swap(requested); return 0; } @@ -857,7 +914,10 @@ int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { *__error_nid_postfix() = errnoBadAddress; return -1; } - std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + if (!WriteGuest(groups, supplementaryGroups.data(), supplementaryGroups.size() * sizeof(supplementaryGroups[0]))) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } return count; } From 67db9d371f51c52e4bfd17ea7c347540e57075e1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:28:52 +0000 Subject: [PATCH 0294/1360] test(libkernel): cover invalid group credential pointers --- core/libs/prx/libkernel/System/src/Process.cpp | 2 +- core/libs/tests/GuestCredentials.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 74a135266f..88513c28d8 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -95,7 +95,7 @@ bool StoreGuestId(std::uint32_t* output) { #else *output = zero; return true; - #endif +#endif } bool GuestRange(const void* pointer, std::size_t bytes) { diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index df8a2966c9..0e36f390a8 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -114,6 +114,11 @@ int main() { Require(getgroups_nid_postfix(0, nullptr) == 1); Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); Require(groups[0] == static_cast(getegid_nid_postfix())); + *__error_nid_postfix() = 13; + RequireFailure(setgroups_nid_postfix(1, reinterpret_cast(1)), GuestEfault); + *__error_nid_postfix() = 13; + RequireFailure(getgroups_nid_postfix(1, reinterpret_cast(1)), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); *__error_nid_postfix() = 13; RequireFailure(getresuid_nid_postfix(reinterpret_cast(1), nullptr, nullptr), GuestEfault); From 2b1eb36139670c37bb55f22e5b250a15caecc231 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:06:38 +0530 Subject: [PATCH 0295/1360] fix(savedata): validate backup and event result arguments --- core/libs/prx/libSceSaveData.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataDelete.cpp | 3 --- core/libs/tests/GuestSaveDataMountStatus.cpp | 4 ++-- docs/dev/debt/libSceSaveData.native.md | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 70a5dd1754..6f8fda20c6 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -327,7 +327,7 @@ static bool isMountedDirectory(const std::string& path) { static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; + throw std::runtime_error("sceSaveDataDelete: null argument"); } const auto* nameEnd = static_cast(std::memchr(del->dir_name->data, '\0', sizeof(del->dir_name->data))); if (nameEnd == nullptr) { diff --git a/core/libs/tests/GuestSaveDataDelete.cpp b/core/libs/tests/GuestSaveDataDelete.cpp index 4fda8b7d4e..0ebbce1462 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -89,9 +89,6 @@ int main() { char unterminated[sizeof(SceSaveDataDirName::data)]; std::memset(unterminated, 'a', sizeof(unterminated)); Check(Delete(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); - Check(sceSaveDataDelete(nullptr) == SaveDataErrorParameter, "null delete pointer is rejected"); - SaveDataDelete nullDirDelete{}; - Check(sceSaveDataDelete(&nullDirDelete) == SaveDataErrorParameter, "null delete dir_name is rejected"); constexpr char mountedPayload[] = "live mounted save"; constexpr char updatedPayload[] = "still mounted"; diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index a33ad5960f..21b227859f 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -10,6 +10,8 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); +int APS5_VABI sceSaveDataBackup(const SaveDataBackup*); +int APS5_VABI sceSaveDataGetEventResult(const void*, SaveDataEvent*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); @@ -17,8 +19,6 @@ int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint*, SaveDataIcon*); int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint*, const SaveDataIcon*); -int APS5_VABI sceSaveDataBackup(const SaveDataBackup*); -int APS5_VABI sceSaveDataGetEventResult(const void*, SaveDataEvent*); } namespace { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 2d1b1d1d66..5868dc966a 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -19,6 +19,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataBackup, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob From 29d50d189ef7e11f5a1ec5a140e8b640a755c404 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:38:01 +0000 Subject: [PATCH 0296/1360] fix(libkernel): check getrlimit output pointers --- .../libs/prx/libkernel/System/src/Process.cpp | 38 +++++++++++-------- core/libs/tests/GuestGetrlimit.cpp | 7 ++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 88513c28d8..33f7b773b1 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -721,20 +721,21 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { *__error_nid_postfix() = guestFault; return -1; } + GuestResourceLimit result{}; switch (resource) { case guestRlimitData: { #ifdef _WIN32 const std::int64_t memory = hostMemoryLimit(); - limit->rlim_cur = memory; - limit->rlim_max = memory; + result.rlim_cur = memory; + result.rlim_max = memory; #else rlimit native{}; if (::getrlimit(RLIMIT_DATA, &native) != 0) throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); - limit->rlim_cur = toGuestLimit(native.rlim_cur); - limit->rlim_max = toGuestLimit(native.rlim_max); + result.rlim_cur = toGuestLimit(native.rlim_cur); + result.rlim_max = toGuestLimit(native.rlim_max); #endif - return 0; + break; } case guestRlimitNofile: { std::lock_guard lock(openFileLimitMutex); @@ -749,17 +750,17 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { guestOpenFileLimit = GuestResourceLimit{toGuestLimit(native.rlim_cur), toGuestLimit(native.rlim_max)}; #endif } - *limit = *guestOpenFileLimit; - return 0; + result = *guestOpenFileLimit; + break; } case guestRlimitStack: - limit->rlim_cur = guestStackLimit; - limit->rlim_max = guestStackLimit; - return 0; + result.rlim_cur = guestStackLimit; + result.rlim_max = guestStackLimit; + break; case guestRlimitNproc: - limit->rlim_cur = guestProcessLimit; - limit->rlim_max = guestProcessLimit; - return 0; + result.rlim_cur = guestProcessLimit; + result.rlim_max = guestProcessLimit; + break; case guestRlimitCpu: case guestRlimitFsize: case guestRlimitCore: @@ -771,12 +772,17 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { case guestRlimitSwap: case guestRlimitKqueues: case guestRlimitUmtxp: - limit->rlim_cur = guestRlimitInfinity; - limit->rlim_max = guestRlimitInfinity; - return 0; + result.rlim_cur = guestRlimitInfinity; + result.rlim_max = guestRlimitInfinity; + break; default: throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); } + if (!WriteGuest(limit, &result, sizeof(result))) { + *__error_nid_postfix() = guestFault; + return -1; + } + return 0; } int APS5_VABI getpriority_nid_postfix(int which, int who) { diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 5d6d5d2999..14b00ab71a 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -141,6 +141,11 @@ static void RequireError(int resource, ResourceLimit* limit, int error) { Require(*__error_nid_postfix() == error); } +static void RequireInvalidOutputPointer(int resource) { + auto* const invalid = reinterpret_cast(static_cast(1)); + RequireError(resource, invalid, GUEST_EFAULT); +} + #ifndef _WIN32 static std::int64_t Guest(rlim_t value) { return value == RLIM_INFINITY ? GUEST_RLIM_INFINITY : static_cast(value); @@ -196,6 +201,8 @@ int main() { RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EINVAL); RequireError(-1, nullptr, GUEST_EINVAL); RequireError(GUEST_RLIMIT_DATA, nullptr, GUEST_EFAULT); + RequireInvalidOutputPointer(GUEST_RLIMIT_DATA); + RequireInvalidOutputPointer(GUEST_RLIMIT_NOFILE); const ResourceLimit stack = GuestLimit(GUEST_RLIMIT_STACK); Require(stack.rlim_cur == 0x200000 && stack.rlim_max == 0x200000); RequireError(GUEST_RLIMIT_STACK, nullptr, GUEST_EFAULT); From 6131ec265d3269b130185cd7a2cf26929d304de8 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 10:40:25 +0200 Subject: [PATCH 0297/1360] feat(libkernel_sys): implement the scePthreadBarrier, cpuclockid, concurrency and msync forms Forward scePthreadBarrier*, scePthreadBarrierattr*, scePthreadGetcpuclockid, scePthreadGetconcurrency and sceKernelMsync to libkernel's POSIX implementations and return POSIX errors as SCE kernel errors. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 8 +- .../prx/libkernel_sys/Export.Discovery.cpp | 74 ++++++++----- core/libs/tests/GuestKernelSysBarrier.cpp | 103 ++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 4 + 4 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysBarrier.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2854a5fa08..f430d97a01 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -300,12 +300,18 @@ if(BUILD_TESTING) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_tests) add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests) + add_test_executable(guest_kernel_sys_barrier_tests tests/GuestKernelSysBarrier.cpp) + target_include_directories(guest_kernel_sys_barrier_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_barrier_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_barrier_tests) + add_test(NAME guest_kernel_sys_barrier COMMAND guest_kernel_sys_barrier_tests) + set_tests_properties(guest_kernel_sys_barrier PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) endif() if(WIN32) - set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_barrier PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(APPLE) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..f56381b92b 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1,5 +1,29 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI pthread_barrier_init_nid_postfix(void** barrier, void* const* attr, unsigned count); +int APS5_VABI pthread_barrier_wait_nid_postfix(void** barrier); +int APS5_VABI pthread_barrier_destroy_nid_postfix(void** barrier); +int APS5_VABI pthread_barrierattr_init_nid_postfix(void** attr); +int APS5_VABI pthread_barrierattr_destroy_nid_postfix(void** attr); +int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(void* const* attr, int* pshared); +int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(void** attr, int pshared); +int APS5_VABI pthread_getcpuclockid_nid_postfix(void* thread, int* clockId); +int APS5_VABI pthread_getconcurrency_nid_postfix(); +int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags); +} + +namespace { + +int SceResult(int error) { + return error == 0 ? 0 : SceKernelError(error); +} + +} extern "C" { @@ -1738,9 +1762,8 @@ int APS5_VABI sceKernelMlockall(void) { return 0; } -int APS5_VABI sceKernelMsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelMsync(void* address, std::size_t length, int flags) { + return msync_nid_postfix(address, length, flags) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); } int APS5_VABI sceKernelNormalizePath(void) { @@ -2308,39 +2331,34 @@ int APS5_VABI scePthreadAttrSetscope(void) { return 0; } -int APS5_VABI scePthreadBarrierDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierDestroy(void** barrier) { + return SceResult(pthread_barrier_destroy_nid_postfix(barrier)); } -int APS5_VABI scePthreadBarrierInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierInit(void** barrier, void* const* attr, unsigned count, const char* name) { + (void)name; + return SceResult(pthread_barrier_init_nid_postfix(barrier, attr, count)); } -int APS5_VABI scePthreadBarrierWait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierWait(void** barrier) { + const int result = pthread_barrier_wait_nid_postfix(barrier); + return result > 0 ? SceKernelError(result) : result; } -int APS5_VABI scePthreadBarrierattrDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrDestroy(void** attr) { + return SceResult(pthread_barrierattr_destroy_nid_postfix(attr)); } -int APS5_VABI scePthreadBarrierattrGetpshared(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrGetpshared(void* const* attr, int* pshared) { + return SceResult(pthread_barrierattr_getpshared_nid_postfix(attr, pshared)); } -int APS5_VABI scePthreadBarrierattrInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrInit(void** attr) { + return SceResult(pthread_barrierattr_init_nid_postfix(attr)); } -int APS5_VABI scePthreadBarrierattrSetpshared(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrSetpshared(void** attr, int pshared) { + return SceResult(pthread_barrierattr_setpshared_nid_postfix(attr, pshared)); } int APS5_VABI scePthreadCondattrGetclock(void) { @@ -2349,13 +2367,11 @@ int APS5_VABI scePthreadCondattrGetclock(void) { } int APS5_VABI scePthreadGetconcurrency(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return pthread_getconcurrency_nid_postfix(); } -int APS5_VABI scePthreadGetcpuclockid(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadGetcpuclockid(void* thread, int* clockId) { + return SceResult(pthread_getcpuclockid_nid_postfix(thread, clockId)); } int APS5_VABI scePthreadMain(void) { diff --git a/core/libs/tests/GuestKernelSysBarrier.cpp b/core/libs/tests/GuestKernelSysBarrier.cpp new file mode 100644 index 0000000000..f5787c511e --- /dev/null +++ b/core/libs/tests/GuestKernelSysBarrier.cpp @@ -0,0 +1,103 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +struct GuestBarrier; +struct GuestBarrierattr; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadBarrierattrInit(GuestBarrierattr** attr); +int APS5_VABI scePthreadBarrierattrDestroy(GuestBarrierattr** attr); +int APS5_VABI scePthreadBarrierattrGetpshared(GuestBarrierattr* const* attr, int* pshared); +int APS5_VABI scePthreadBarrierattrSetpshared(GuestBarrierattr** attr, int pshared); +int APS5_VABI scePthreadBarrierInit(GuestBarrier** barrier, GuestBarrierattr* const* attr, unsigned count, const char* name); +int APS5_VABI scePthreadBarrierWait(GuestBarrier** barrier); +int APS5_VABI scePthreadBarrierDestroy(GuestBarrier** barrier); +int APS5_VABI scePthreadGetcpuclockid(Pthread thread, int* clockId); +int APS5_VABI pthread_getcpuclockid_nid_postfix(Pthread thread, int* clockId); +int APS5_VABI scePthreadGetconcurrency(); +int APS5_VABI pthread_setconcurrency_nid_postfix(int level); +int APS5_VABI sceKernelMsync(void* address, std::size_t length, int flags); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); +static constexpr int SCE_KERNEL_ERROR_ENOMEM = static_cast(0x8002000C); +static constexpr int PROCESS_PRIVATE = 0; +static constexpr int PROCESS_SHARED = 1; +static constexpr int BARRIER_SERIAL_THREAD = -1; +static constexpr int MS_ASYNC = 0x1; +static constexpr int MS_INVALIDATE = 0x2; +static constexpr unsigned WAITERS = 4; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Context { + GuestBarrier* barrier = nullptr; + std::atomic serial{0}; + std::atomic released{0}; +}; + +static void* APS5_VABI Waiter(void* arg) { + auto& context = *static_cast(arg); + const int result = scePthreadBarrierWait(&context.barrier); + if (result == BARRIER_SERIAL_THREAD) ++context.serial; + else if (result == 0) ++context.released; + else std::abort(); + return nullptr; +} + +int main() { + GuestBarrierattr* attr = nullptr; + Require(scePthreadBarrierattrInit(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrInit(&attr) == 0 && attr != nullptr); + int pshared = -1; + Require(scePthreadBarrierattrGetpshared(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Require(scePthreadBarrierattrSetpshared(&attr, PROCESS_SHARED) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrSetpshared(&attr, PROCESS_PRIVATE) == 0); + + Context context; + Require(scePthreadBarrierInit(&context.barrier, &attr, 0, "zero") == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierInit(nullptr, &attr, WAITERS, "null") == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierInit(&context.barrier, &attr, WAITERS, "barrier") == 0 && context.barrier != nullptr); + Require(scePthreadBarrierattrDestroy(&attr) == 0 && attr == nullptr); + Require(scePthreadBarrierattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + + for (int round = 0; round < 2; ++round) { + context.serial = 0; + context.released = 0; + std::array threads{}; + for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, Waiter, &context, nullptr) == 0); + for (auto& thread : threads) Require(scePthreadJoin(thread, nullptr) == 0); + Require(context.serial == 1 && context.released == static_cast(WAITERS) - 1); + } + Require(scePthreadBarrierDestroy(&context.barrier) == 0 && context.barrier == nullptr); + Require(scePthreadBarrierDestroy(&context.barrier) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierWait(nullptr) == SCE_KERNEL_ERROR_EINVAL); + + GuestBarrier* single = nullptr; + Require(scePthreadBarrierInit(&single, nullptr, 1, nullptr) == 0); + Require(scePthreadBarrierWait(&single) == BARRIER_SERIAL_THREAD); + Require(scePthreadBarrierDestroy(&single) == 0); + + int clock = 0; + int posixClock = 0; + Require(scePthreadGetcpuclockid(scePthreadSelf(), &clock) == 0); + Require(pthread_getcpuclockid_nid_postfix(scePthreadSelf(), &posixClock) == 0 && clock == posixClock); + Require(scePthreadGetcpuclockid(scePthreadSelf(), nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(scePthreadGetcpuclockid(nullptr, &clock) == SCE_KERNEL_ERROR_EINVAL); + + Require(pthread_setconcurrency_nid_postfix(3) == 0 && scePthreadGetconcurrency() == 3); + Require(pthread_setconcurrency_nid_postfix(0) == 0 && scePthreadGetconcurrency() == 0); + + auto* unmapped = reinterpret_cast(static_cast(0x4000)); + Require(sceKernelMsync(unmapped, 0x4000, 0) == SCE_KERNEL_ERROR_ENOMEM); + Require(sceKernelMsync(unmapped, 0x4000, MS_ASYNC | MS_INVALIDATE) == SCE_KERNEL_ERROR_EINVAL); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..57f8251ae5 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -18,3 +18,7 @@ - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it + +### Unknown function info + +- [scePthreadBarrier*, scePthreadBarrierattr*, scePthreadGetcpuclockid, scePthreadGetconcurrency and sceKernelMsync](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_barrier*`, `pthread_getcpuclockid`, `pthread_getconcurrency` and `msync` and turn a POSIX error into `0x80020000 | errno`; that the console's `sce` forms do the same is not measured. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); `scePthreadBarrierInit` ignores its name, and `scePthreadBarrierWait` returns -1 to the serial thread, both as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/barrier.cpp) (PS4). `scePthreadGetconcurrency` returns the level unchanged From 103484d0be3e359f2fe577af09b4bf59ca61b7eb Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 03:54:02 -0500 Subject: [PATCH 0298/1360] fix(tools): read the EnvironmentNumber environment reader in env_reference The Windows crash report reads APS5_CRASH_STACK_BYTES through EnvironmentNumber, which env_reference did not recognize, so the name had no row in docs/dev/ENVIRONMENT.md and neither the reference check nor the conventions check reported it. Recognize the reader as an integer reader and add the row. --- docs/dev/ENVIRONMENT.md | 3 ++- tools/env_reference.py | 5 +++-- tools/tests/test_env_reference.py | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7e95e1fac5..7b62e1dc64 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -1,6 +1,6 @@ # Environment variables -The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount`, `EnvironmentNumber` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. ### core/libs @@ -532,6 +532,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `APS5_CRASH_INDIRECT_MEMORY` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | dumps the words the faulting registers point at in the Windows crash report | +| `APS5_CRASH_STACK_BYTES` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | 0 | bytes of the faulting thread's stack from rsp, decimal or 0x hexadecimal and at most 1048576, that the Windows crash report prints with the data at the stack words pointing outside the stack; 0 prints none | | `APS5_HANG_DUMP_SECS` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | seconds after which a watchdog dumps all threads and aborts | | `APS5_HEAP_CACHE_MIB` | integer | `core/libs/prx/libc/src/GuestHeap.cpp` | 4096 | size in MiB of the cache of freed large blocks before the oldest are decommitted | | `APS5_HELPER_AFFINITY_MASK` | integer | `core/libs/prx/libc/include/CpuTopology.hpp` | | hex core mask that helper threads (deferred release, mirror refresh) are pinned to | diff --git a/tools/env_reference.py b/tools/env_reference.py index 683b1e9168..1b82ce47a9 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -10,12 +10,12 @@ ROOTS = ("core",) EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp", ".py"} SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} -INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} +INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount", "EnvironmentNumber"} STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' - r'|configured|PipelineSwitch|environmentCount|addWideString)' + r'|configured|PipelineSwitch|environmentCount|EnvironmentNumber|addWideString)' r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') @@ -36,6 +36,7 @@ def forms(prefixes=PREFIXES): ("configured", re.compile(r'\b(?:configured|PipelineSwitch)\(\s*"(?P' + name + r')"' r'(?:\s*,\s*(?P[0-9]+)[uUlL]*)?')), ("environmentCount", re.compile(r'\benvironmentCount\(\s*"(?P' + name + r')"\s*,\s*(?P[0-9]+)')), + ("EnvironmentNumber", re.compile(r'\bEnvironmentNumber\(\s*"(?P' + name + r')"\s*\)')), ("GetEnvironmentVariableA", re.compile(r'GetEnvironmentVariableA\(\s*"(?P' + name + r')"')), ("GetEnvironmentVariableW", re.compile(r'GetEnvironmentVariableW\(\s*L"(?P' + name + r')"')), ("addWideString", re.compile(r'\baddWideString\(\s*u"(?P' + name + r')"\s*\)')), diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index c414e314c0..7ac385e9cc 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -30,6 +30,8 @@ static bool q = _wgetenv(L"APS5_WIDE_PRESENT") != nullptr; static int r = _wtoi(_wgetenv(L"APS5_WIDE_INT")); static const wchar_t* s = _wgetenv(L"ANYPS5_WIDE_CONFIG"); static const char* t = std::getenv("NOT_AN_ENV"); +static std::uint64_t u = std::min(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20); +static std::uint64_t v = MyEnvironmentNumber("APS5_NOT_A_NUMBER_READ"); """ @@ -52,7 +54,8 @@ class EnvReferenceTests(unittest.TestCase): "APS5_CRASH_INDIRECT_MEMORY", "APS5_PIPELINE_JOURNAL_MAX_MIB", "APS5_PIPELINE_STORE_ENTRIES", "APS5_PIPELINE_JOURNAL_THREADS", "APS5_PM4_RING", "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT", - "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT"}) + "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT", + "APS5_CRASH_STACK_BYTES"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) @@ -65,6 +68,7 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["ANYPS5_WIDE_CONFIG"][0][2:], ("string", "")) self.assertEqual(found["APS5_WIDE_PRESENT"][0][2:], ("presence", "")) self.assertEqual(found["APS5_WIDE_INT"][0][2:], ("integer", "")) + self.assertEqual(found["APS5_CRASH_STACK_BYTES"][0][2:], ("integer", "")) def test_the_default_prefixes_cover_both(self): root = self.fixture() From 2fc26b970fdc04b2b78aef256432c2fa1e39a783 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 05:18:18 -0400 Subject: [PATCH 0299/1360] fix(libkernel): return ESRCH for a null semaphore handle Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 19 ++++++++++++++----- core/libs/tests/GuestSemaphore.cpp | 9 +++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 725f297e67..eab1d9b52d 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -31,7 +31,10 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at } int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { - if (sem == nullptr || need <= 0) { + if (sem == nullptr) { + return SCE_KERNEL_ERROR_ESRCH; + } + if (need <= 0) { APS5_INVALID_ARG_EX; } @@ -44,7 +47,10 @@ int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { } int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { - if (sem == nullptr || count < 0) { + if (sem == nullptr) { + return SCE_KERNEL_ERROR_ESRCH; + } + if (count < 0) { APS5_INVALID_ARG_EX; } @@ -60,7 +66,10 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { } int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) { - if (sem == nullptr || need <= 0) { + if (sem == nullptr) { + return SCE_KERNEL_ERROR_ESRCH; + } + if (need <= 0) { APS5_INVALID_ARG_EX; } @@ -129,7 +138,7 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { if (sem == nullptr) { - APS5_INVALID_ARG_EX; + return SCE_KERNEL_ERROR_ESRCH; } std::lock_guard lock(sem->mutex); @@ -148,7 +157,7 @@ int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { int APS5_VABI sceKernelDeleteSema(KernelSema sem) { if (sem == nullptr) { - APS5_INVALID_ARG_EX; + return SCE_KERNEL_ERROR_ESRCH; } std::unique_lock lock(sem->mutex); diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index 5392fa1603..0ba58d4520 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -15,6 +15,7 @@ int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); @@ -36,6 +37,14 @@ static void* APS5_VABI Wait(void* arg) { } int main() { + KernelUseconds nullTimeout = 1000; + int nullWaiters = -1; + Require(sceKernelWaitSema(nullptr, 1, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelWaitSema(nullptr, 1, &nullTimeout) == SCE_KERNEL_ERROR_ESRCH && nullTimeout == 1000); + Require(sceKernelPollSema(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSignalSema(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCancelSema(nullptr, 0, &nullWaiters) == SCE_KERNEL_ERROR_ESRCH && nullWaiters == -1); + Require(sceKernelDeleteSema(nullptr) == SCE_KERNEL_ERROR_ESRCH); KernelSema sem = nullptr; Require(sceKernelCreateSema(&sem, "cancel", 0, 1, 3, nullptr) == SCE_OK); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6384b00c9f..d1d8a5c3ac 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -61,6 +61,7 @@ - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. +- [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) From a2e07a5eb57572d36a9923e38f88ac229024e416 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:45:51 -0300 Subject: [PATCH 0300/1360] test(libc): check host stdout after freopen --- core/libs/tests/GuestStandardStreams.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index c37c580e6b..c5700d00b9 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -17,6 +17,7 @@ #include #ifdef _WIN32 #include +#include #else #include #endif @@ -920,6 +921,14 @@ int main(int argc, char** argv) { CheckStandardStreamDeleteSharing(); const std::string standardFilename = "anyps5-standard-reopen-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); Require(freopen_nid_postfix(standardFilename.c_str(), "w", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + const HANDLE hostOutput = ::GetStdHandle(STD_OUTPUT_HANDLE); + Require(hostOutput != nullptr && hostOutput != INVALID_HANDLE_VALUE); + Require(hostOutput != reinterpret_cast(::_get_osfhandle(::_fileno(stdout)))); + DWORD handleFlags = 0; + Require(::GetHandleInformation(hostOutput, &handleFlags) != 0); + constexpr char hostMessage[] = "host stdout preserved\n"; + DWORD hostWritten = 0; + Require(::WriteFile(hostOutput, hostMessage, sizeof(hostMessage) - 1, &hostWritten, nullptr) != 0 && hostWritten == sizeof(hostMessage) - 1); Require(printf_nid_postfix("standard output") == 15 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); { std::ifstream input(standardFilename, std::ios::binary); std::string contents; std::getline(input, contents); Require(contents == "standard output"); } From 0f5f35166aaed82d4cf894e4bc05837755f6b164 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 09:24:32 +0000 Subject: [PATCH 0301/1360] feat(libSceNpCppWebApi): cover the ParameterToGetContainer lifecycle and getContainer Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 15d8b1bf8e..974a7f0403 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1759,8 +1759,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToG return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE() { return 0; } @@ -1775,19 +1774,17 @@ int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToG return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv() { return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void* self) { + (void)self; return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } const char* APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product8getLabelEv(const void* self) { From 29f1a34ccb898ed0012dc54a299ef3395cded938 Mon Sep 17 00:00:00 2001 From: verusferro <99999109+verusferro@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:28:58 +0300 Subject: [PATCH 0302/1360] feat(libkernel_sys): implement fstat fstat was a NotImplemented stub in libkernel_sys. It now calls libkernel's _fstat, as fstatfs already calls _fstatfs, so it reports the same status, EFAULT for a null buffer and EBADF for a descriptor that is not open. --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..69addad85d 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -98,11 +98,6 @@ int APS5_VABI fchownat_nid_postfix(void) { return 0; } -int APS5_VABI fstat_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI futimesat_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..ba324a42fb 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -274,6 +274,12 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { return _fstatfs_nid_postfix(d, buf); } +std::int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb); + +int APS5_VABI fstat_nid_postfix(int d, FileStat* sb) { + return static_cast(_fstat_nid_postfix(d, sb)); +} + std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); From 6826a4bd9c295008d34efb9fd82371b428244921 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:32:44 +0300 Subject: [PATCH 0303/1360] feat(libSceNpTus): check the arguments of the variable requests and fail them signed out The add-and-get, try-and-set, multi-user and friends variable calls (NpId, account, virtual user and cross-save forms) run shadPS4's argument checks and then finish with SCE_NP_ERROR_SIGNED_OUT like the multi-slot variable calls; the synchronous forms wait for their asynchronous ones. --- core/libs/prx/libSceNpTus/Export.cpp | 870 +++++++++++++++------------ 1 file changed, 488 insertions(+), 382 deletions(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index a69e6cb5a6..b1f42b8162 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -11,12 +12,24 @@ static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = static_cast( static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast(0x80550707); static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast(0x8055070E); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_TYPE = static_cast(0x80550711); static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID = static_cast(0x80550718); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID = static_cast(0x80550719); static constexpr int POLL_FINISHED = 0; static constexpr int POLL_RUNNING = 1; static constexpr std::size_t MAX_TITLE_CONTEXTS = 32; static constexpr std::size_t MAX_REQUESTS = 256; static constexpr int MAX_SLOTS = 64; +static constexpr int MAX_USERS = 101; +static constexpr int MAX_FRIENDS = 100; +static constexpr std::int32_t TSS_MAX_SLOT_ID = 15; +static constexpr std::uint64_t TSS_MAX_SIZE = 64 * 1024; +static constexpr std::int32_t OPETYPE_EQUAL = 1; +static constexpr std::int32_t OPETYPE_LESS_OR_EQUAL = 6; +static constexpr std::int32_t DATASTATUS_SORTTYPE_DESCENDING_DATE = 1; +static constexpr std::int32_t DATASTATUS_SORTTYPE_ASCENDING_DATE = 2; +static constexpr std::int32_t VARIABLE_SORTTYPE_DESCENDING_DATE = 1; +static constexpr std::int32_t VARIABLE_SORTTYPE_ASCENDING_VALUE = 4; namespace { struct Request { @@ -79,6 +92,236 @@ int Complete(int reqId, int asyncResult) { extern "C" { +int APS5_VABI sceNpTusSetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { (void)serviceLabel; (void)userId; @@ -292,176 +535,16 @@ int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(int reqId, const NpOnlineId* return Complete(reqId, sceNpTusDeleteMultiSlotVariableVUserAsync(reqId, targetVirtualUserId, slotIds, arrayLen, option)); } -int APS5_VABI sceNpTusGetMultiUserVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusDeleteMultiSlotData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusSetDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTssGetSmallStorage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTssGetSmallStorageAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTssGetStorage(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -477,111 +560,6 @@ int APS5_VABI sceNpTssGetDataAsync(void) { return 0; } -int APS5_VABI sceNpTusAddAndGetVariableA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -592,86 +570,11 @@ int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { return 0; } -int APS5_VABI sceNpTusGetMultiUserVariableA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetFriendsVariableA(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -712,74 +615,277 @@ int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { return 0; } -int APS5_VABI sceNpTusSetDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); } -int APS5_VABI sceNpTusSetDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusAddAndGetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetNpId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); } -int APS5_VABI sceNpTusSetDataAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetAccountId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(reqId); } -int APS5_VABI sceNpTusSetDataAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); } -int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); } -int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)includeSelf; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_FRIENDS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (sortType < VARIABLE_SORTTYPE_DESCENDING_DATE || sortType > VARIABLE_SORTTYPE_ASCENDING_VALUE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiUserVariableAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetNpIdArray || !variableArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)targetAccountId; + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + return StartOffline(reqId); } -int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetNpId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)targetAccountId; + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAsync(reqId, targetNpId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAAsync(reqId, targetAccountId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableForCrossSaveAsync(reqId, targetAccountId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetFriendsVariableAsync(reqId, slotId, includeSelf, sortType, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAsync(reqId, targetNpIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAAsync(reqId, targetAccountIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAsync(reqId, targetNpId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAAsync(reqId, targetAccountId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableForCrossSaveAsync(reqId, targetAccountId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); } } From 943438f5f5ef17a40f00bc5e3ebdbeecb8875420 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:32:53 +0300 Subject: [PATCH 0304/1360] feat(libSceNpTus): check the arguments of the data requests and fail them signed out The set, get and multi-slot delete data calls in all their forms run shadPS4's argument checks and finish with SCE_NP_ERROR_SIGNED_OUT. --- core/libs/prx/libSceNpTus/Export.cpp | 232 ++++++++++++++++----------- 1 file changed, 142 insertions(+), 90 deletions(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index b1f42b8162..7998cac270 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -92,36 +92,6 @@ int Complete(int reqId, int asyncResult) { extern "C" { -int APS5_VABI sceNpTusSetDataVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -252,66 +222,6 @@ int APS5_VABI sceNpTusGetFriendsDataStatus(void) { return 0; } -int APS5_VABI sceNpTusGetDataVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusDeleteMultiSlotData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTssGetSmallStorageAsync(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -672,6 +582,72 @@ int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* t return StartOffline(reqId); } +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + (void)targetAccountId; + if (!slotIds) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetNpId || !slotIds || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetAccountId; + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetNpId; + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetAccountId; + (void)data; + (void)recvSize; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { (void)includeSelf; (void)variableArraySize; @@ -739,6 +715,54 @@ int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId return StartOffline(reqId); } +int APS5_VABI sceNpTusSetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)targetAccountId; + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)targetNpId; + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { (void)targetAccountId; (void)variable; @@ -840,6 +864,26 @@ int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* target return Complete(reqId, sceNpTusAddAndGetVariableVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); } +int APS5_VABI sceNpTusDeleteMultiSlotData(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotDataAsync(reqId, targetNpId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotDataAAsync(reqId, targetAccountId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAVUserAsync(reqId, targetVirtualUserId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataForCrossSaveAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { return Complete(reqId, sceNpTusGetFriendsVariableAsync(reqId, slotId, includeSelf, sortType, variableArray, variableArraySize, arrayLen, option)); } @@ -864,6 +908,14 @@ int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* tar return Complete(reqId, sceNpTusGetMultiUserVariableVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); } +int APS5_VABI sceNpTusSetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAAsync(reqId, targetAccountId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); +} + +int APS5_VABI sceNpTusSetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAVUserAsync(reqId, targetVirtualUserId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); +} + int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { return Complete(reqId, sceNpTusTryAndSetVariableAsync(reqId, targetNpId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); } From f4d740b8f34d4296b133f6a3ec687941aab2be0b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:32:55 +0300 Subject: [PATCH 0305/1360] feat(libSceNpTus): check the arguments of the data status requests and fail them signed out The multi-slot, multi-user and friends data status calls run shadPS4's argument checks and finish with SCE_NP_ERROR_SIGNED_OUT. --- core/libs/prx/libSceNpTus/Export.cpp | 308 ++++++++++++++++----------- 1 file changed, 178 insertions(+), 130 deletions(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 7998cac270..7e4c38226c 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -92,136 +92,6 @@ int Complete(int reqId, int asyncResult) { extern "C" { -int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTssGetSmallStorageAsync(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -648,6 +518,16 @@ int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtu return StartOffline(reqId); } +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)includeSelf; + (void)statusArraySize; + if (!statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_FRIENDS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (sortType < DATASTATUS_SORTTYPE_DESCENDING_DATE || sortType > DATASTATUS_SORTTYPE_ASCENDING_DATE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { (void)includeSelf; (void)variableArraySize; @@ -658,6 +538,122 @@ int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, st return StartOffline(reqId); } +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)statusArraySize; + if (!slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetNpId || !slotIds || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)statusArraySize; + if (!slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetAccountIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetNpIdArray || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetAccountIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { (void)variableArraySize; if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; @@ -884,10 +880,62 @@ int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccount return Complete(reqId, sceNpTusGetDataForCrossSaveAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); } +int APS5_VABI sceNpTusGetFriendsDataStatus(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetFriendsDataStatusAsync(reqId, slotId, includeSelf, sortType, statusArray, statusArraySize, arrayLen, option)); +} + int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { return Complete(reqId, sceNpTusGetFriendsVariableAsync(reqId, slotId, includeSelf, sortType, variableArray, variableArraySize, arrayLen, option)); } +int APS5_VABI sceNpTusGetMultiSlotDataStatus(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAsync(reqId, targetNpId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAAsync(reqId, targetAccountId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(reqId, targetAccountId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAsync(reqId, targetNpIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAAsync(reqId, targetAccountIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusForCrossSaveAsync(reqId, targetAccountIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { return Complete(reqId, sceNpTusGetMultiUserVariableAsync(reqId, targetNpIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); } From facfb248462f2a4d9d7cf774898d5d3fdb8de3ff Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:32:56 +0300 Subject: [PATCH 0306/1360] feat(libSceNpTus): check the arguments of the TSS small storage request and fail it signed out sceNpTssGetSmallStorageAsync refuses an option and a size above 64 KiB as shadPS4 does, and sceNpTssGetSmallStorage waits for it. --- core/libs/prx/libSceNpTus/Export.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 7e4c38226c..3054cd42a5 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -92,16 +92,6 @@ int Complete(int reqId, int asyncResult) { extern "C" { -int APS5_VABI sceNpTssGetSmallStorageAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTssGetSmallStorage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { (void)serviceLabel; (void)userId; @@ -395,6 +385,14 @@ int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { return 0; } +int APS5_VABI sceNpTssGetSmallStorageAsync(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option) { + (void)data; + (void)contentLength; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (maxSize > TSS_MAX_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { (void)inVariable; (void)isLastChangedAuthor; @@ -836,6 +834,10 @@ int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* t return StartOffline(reqId); } +int APS5_VABI sceNpTssGetSmallStorage(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option) { + return Complete(reqId, sceNpTssGetSmallStorageAsync(reqId, data, maxSize, contentLength, option)); +} + int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { return Complete(reqId, sceNpTusAddAndGetVariableAsync(reqId, targetNpId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); } From 6c88a69a2496271d7106f41dcef34fefa8e54010 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:33:07 +0300 Subject: [PATCH 0307/1360] test(libSceNpTus): run every new request through its arguments and its signed-out result Each asynchronous call must start with valid arguments and finish signed out, refuse an unknown request and a non-null option; each synchronous call must return SCE_NP_ERROR_SIGNED_OUT. Targeted cases cover empty virtual users, the operation and sort types and the slot, user, friend and TSS size limits. --- core/libs/prx/libSceNpTus/CMakeLists.txt | 8 +- core/libs/tests/GuestNpTusOffline.cpp | 268 +++++++++++++++++++++++ 2 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestNpTusOffline.cpp diff --git a/core/libs/prx/libSceNpTus/CMakeLists.txt b/core/libs/prx/libSceNpTus/CMakeLists.txt index 1cb8f4a676..6046093997 100644 --- a/core/libs/prx/libSceNpTus/CMakeLists.txt +++ b/core/libs/prx/libSceNpTus/CMakeLists.txt @@ -9,8 +9,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_np_tus_tests) add_test(NAME guest_np_tus COMMAND guest_np_tus_tests) set_tests_properties(guest_np_tus PROPERTIES TIMEOUT 20) + add_test_executable(guest_np_tus_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTusOffline.cpp) + target_include_directories(guest_np_tus_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_tus_offline_tests PRIVATE libSceNpTus) + configure_windows_unwind(guest_np_tus_offline_tests) + add_test(NAME guest_np_tus_offline COMMAND guest_np_tus_offline_tests) + set_tests_properties(guest_np_tus_offline PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_np_tus PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_np_tus guest_np_tus_offline PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/tests/GuestNpTusOffline.cpp b/core/libs/tests/GuestNpTusOffline.cpp new file mode 100644 index 0000000000..af24d244c8 --- /dev/null +++ b/core/libs/tests/GuestNpTusOffline.cpp @@ -0,0 +1,268 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t, int); +int APS5_VABI sceNpTusCreateRequest(int); +int APS5_VABI sceNpTusDeleteRequest(int); +int APS5_VABI sceNpTusWaitAsync(int, int*); +int APS5_VABI sceNpTssGetSmallStorage(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option); +int APS5_VABI sceNpTssGetSmallStorageAsync(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option); +int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotData(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusGetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetFriendsDataStatus(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatus(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatus(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +} + +namespace { + +constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidArgument = static_cast(0x80550704u); +constexpr int InvalidId = static_cast(0x8055070Eu); +constexpr int InvalidType = static_cast(0x80550711u); +constexpr int TooManySlotId = static_cast(0x80550718u); +constexpr int TooManyNpId = static_cast(0x80550719u); + +int context = 0; +alignas(8) unsigned char buffer[1024]; +unsigned char npId[64]; +NpOnlineId virtualUser{}; +NpOnlineId virtualUsers[2]{}; +NpOnlineId emptyUser{}; +unsigned char npIds[2][64]; +std::uint64_t accounts[2] = {1, 2}; +std::int32_t slots[2] = {1, 2}; +std::int32_t manySlots[65] = {}; +std::uint64_t length = 0; +int failures = 0; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +int NewRequest() { + const int req = sceNpTusCreateRequest(context); + if (req <= 0) { + std::fprintf(stderr, "sceNpTusCreateRequest failed\n"); + std::abort(); + } + return req; +} + +void CheckAsync(const char* name, const std::function& valid, const std::function& unknownRequest, const std::function& withOption) { + int req = NewRequest(); + Require(valid(req) == 0, name, "valid arguments did not start the request"); + int result = 0; + Require(sceNpTusWaitAsync(req, &result) == 0 && result == SignedOut, name, "the request did not finish signed out"); + sceNpTusDeleteRequest(req); + Require(unknownRequest() == InvalidId, name, "an unknown request was not refused with INVALID_ID"); + if (withOption) { + req = NewRequest(); + Require(withOption(req) == InvalidArgument, name, "a non-null option was not refused with INVALID_ARGUMENT"); + sceNpTusDeleteRequest(req); + } +} + +void CheckSync(const char* name, const std::function& call) { + const int req = NewRequest(); + Require(call(req) == SignedOut, name, "the synchronous call did not return SIGNED_OUT"); + sceNpTusDeleteRequest(req); +} + +} + +int main() { + std::memset(npId, 'n', sizeof(npId)); + std::memset(npIds, 'n', sizeof(npIds)); + std::strcpy(virtualUser.data, "virtual"); + std::strcpy(virtualUsers[0].data, "first"); + std::strcpy(virtualUsers[1].data, "second"); + context = sceNpTusCreateNpTitleCtxA(1, 1); + if (context <= 0) return 1; + CheckAsync("sceNpTssGetSmallStorageAsync", [&](int req) { return sceNpTssGetSmallStorageAsync(req, buffer, 16, &length, nullptr); }, [&]() { int req = 9999; return sceNpTssGetSmallStorageAsync(9999, buffer, 16, &length, nullptr); }, [&](int req) { return sceNpTssGetSmallStorageAsync(req, buffer, 16, &length, &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableAVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableAVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableAVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableAVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableAsync", [&](int req) { return sceNpTusAddAndGetVariableAsync(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableAsync(9999, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableAsync(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableForCrossSaveAsync", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveAsync(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableForCrossSaveAsync(9999, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveAsync(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusDeleteMultiSlotDataAAsync", [&](int req) { return sceNpTusDeleteMultiSlotDataAAsync(req, 1, slots, 2, nullptr); }, [&]() { int req = 9999; return sceNpTusDeleteMultiSlotDataAAsync(9999, 1, slots, 2, nullptr); }, [&](int req) { return sceNpTusDeleteMultiSlotDataAAsync(req, 1, slots, 2, &buffer); }); + CheckAsync("sceNpTusDeleteMultiSlotDataAsync", [&](int req) { return sceNpTusDeleteMultiSlotDataAsync(req, &npId, slots, 2, nullptr); }, [&]() { int req = 9999; return sceNpTusDeleteMultiSlotDataAsync(9999, &npId, slots, 2, nullptr); }, [&](int req) { return sceNpTusDeleteMultiSlotDataAsync(req, &npId, slots, 2, &buffer); }); + CheckAsync("sceNpTusGetDataAAsync", [&](int req) { return sceNpTusGetDataAAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAAsync(9999, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataAVUserAsync", [&](int req) { return sceNpTusGetDataAVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAVUserAsync(9999, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataAsync", [&](int req) { return sceNpTusGetDataAsync(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAsync(9999, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAsync(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataForCrossSaveAsync", [&](int req) { return sceNpTusGetDataForCrossSaveAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataForCrossSaveAsync(9999, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataForCrossSaveAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataVUserAsync", [&](int req) { return sceNpTusGetDataVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataVUserAsync(9999, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetFriendsDataStatusAsync", [&](int req) { return sceNpTusGetFriendsDataStatusAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetFriendsDataStatusAsync(9999, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetFriendsDataStatusAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetFriendsVariableAsync", [&](int req) { return sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetFriendsVariableAsync(9999, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAAsync(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAAsync(9999, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAAsync(req, 1, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAsync(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAsync(9999, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAsync(req, &npId, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(9999, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(req, 1, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAsync(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAsync(9999, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAsync(req, npIds, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAAsync", [&](int req) { return sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAsync", [&](int req) { return sceNpTusGetMultiUserVariableAsync(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAsync(9999, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAsync(req, npIds, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableForCrossSaveAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusSetDataAAsync", [&](int req) { return sceNpTusSetDataAAsync(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAAsync(9999, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAAsync(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataAVUserAsync", [&](int req) { return sceNpTusSetDataAVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAVUserAsync(9999, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataAsync", [&](int req) { return sceNpTusSetDataAsync(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAsync(9999, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAsync(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataVUserAsync", [&](int req) { return sceNpTusSetDataVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataVUserAsync(9999, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAAsync", [&](int req) { return sceNpTusTryAndSetVariableAAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAAsync(9999, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableAVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAsync", [&](int req) { return sceNpTusTryAndSetVariableAsync(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAsync(9999, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAsync(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableForCrossSaveAsync", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableForCrossSaveAsync(9999, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckSync("sceNpTssGetSmallStorage", [&](int req) { return sceNpTssGetSmallStorage(req, buffer, 16, &length, nullptr); }); + CheckSync("sceNpTusAddAndGetVariable", [&](int req) { return sceNpTusAddAndGetVariable(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableA", [&](int req) { return sceNpTusAddAndGetVariableA(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableAVUser", [&](int req) { return sceNpTusAddAndGetVariableAVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableForCrossSave", [&](int req) { return sceNpTusAddAndGetVariableForCrossSave(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableForCrossSaveVUser", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableVUser", [&](int req) { return sceNpTusAddAndGetVariableVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusDeleteMultiSlotData", [&](int req) { return sceNpTusDeleteMultiSlotData(req, &npId, slots, 2, nullptr); }); + CheckSync("sceNpTusDeleteMultiSlotDataA", [&](int req) { return sceNpTusDeleteMultiSlotDataA(req, 1, slots, 2, nullptr); }); + CheckSync("sceNpTusGetDataA", [&](int req) { return sceNpTusGetDataA(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataAVUser", [&](int req) { return sceNpTusGetDataAVUser(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataForCrossSave", [&](int req) { return sceNpTusGetDataForCrossSave(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetFriendsDataStatus", [&](int req) { return sceNpTusGetFriendsDataStatus(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetFriendsVariable", [&](int req) { return sceNpTusGetFriendsVariable(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatus", [&](int req) { return sceNpTusGetMultiSlotDataStatus(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusA", [&](int req) { return sceNpTusGetMultiSlotDataStatusA(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusAVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSave(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatus", [&](int req) { return sceNpTusGetMultiUserDataStatus(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusA", [&](int req) { return sceNpTusGetMultiUserDataStatusA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusAVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSave(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariable", [&](int req) { return sceNpTusGetMultiUserVariable(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableA", [&](int req) { return sceNpTusGetMultiUserVariableA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableAVUser", [&](int req) { return sceNpTusGetMultiUserVariableAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableVUser", [&](int req) { return sceNpTusGetMultiUserVariableVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusSetDataA", [&](int req) { return sceNpTusSetDataA(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusSetDataAVUser", [&](int req) { return sceNpTusSetDataAVUser(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusTryAndSetVariable", [&](int req) { return sceNpTusTryAndSetVariable(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableA", [&](int req) { return sceNpTusTryAndSetVariableA(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableAVUser", [&](int req) { return sceNpTusTryAndSetVariableAVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableForCrossSave", [&](int req) { return sceNpTusTryAndSetVariableForCrossSave(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableForCrossSaveVUser", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableVUser", [&](int req) { return sceNpTusTryAndSetVariableVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + { + int req = NewRequest(); + Require(sceNpTusAddAndGetVariableVUserAsync(req, &emptyUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusAddAndGetVariableVUserAsync", "an empty virtual user was taken"); + NpOnlineId partlyEmpty[2]{}; + std::strcpy(partlyEmpty[0].data, "first"); + Require(sceNpTusGetMultiUserVariableVUserAsync(req, partlyEmpty, 1, buffer, sizeof(buffer), 2, nullptr) == InvalidArgument, "sceNpTusGetMultiUserVariableVUserAsync", "an empty virtual user in the array was taken"); + Require(sceNpTusTryAndSetVariableAAsync(req, 1, 1, 0, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidType, "sceNpTusTryAndSetVariableAAsync", "operation type 0 was taken"); + Require(sceNpTusTryAndSetVariableAAsync(req, 1, 1, 7, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidType, "sceNpTusTryAndSetVariableAAsync", "operation type 7 was taken"); + Require(sceNpTusTryAndSetVariableAsync(req, npId, 1, 7, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusTryAndSetVariableAsync", "operation type 7 was not refused with INVALID_ARGUMENT"); + Require(sceNpTssGetSmallStorageAsync(req, buffer, 64 * 1024 + 1, &length, nullptr) == InvalidArgument, "sceNpTssGetSmallStorageAsync", "a size above 64 KiB was taken"); + Require(sceNpTusDeleteMultiSlotDataAAsync(req, 1, manySlots, 65, nullptr) == TooManySlotId, "sceNpTusDeleteMultiSlotDataAAsync", "65 slots were taken"); + Require(sceNpTusGetMultiSlotDataStatusAsync(req, npId, manySlots, buffer, sizeof(buffer), 65, nullptr) == TooManyNpId, "sceNpTusGetMultiSlotDataStatusAsync", "65 slots did not return TOO_MANY_NPID"); + const std::int32_t negative[2] = {1, -1}; + Require(sceNpTusDeleteMultiSlotDataAAsync(req, 1, negative, 2, nullptr) == InvalidArgument, "sceNpTusDeleteMultiSlotDataAAsync", "a negative slot id was taken"); + Require(sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 102, nullptr) == TooManyNpId, "sceNpTusGetMultiUserVariableAAsync", "102 users were taken"); + Require(sceNpTusGetFriendsVariableAsync(req, 1, 0, 5, buffer, sizeof(buffer), 2, nullptr) == InvalidArgument, "sceNpTusGetFriendsVariableAsync", "sort type 5 was taken"); + Require(sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 101, nullptr) == TooManyNpId, "sceNpTusGetFriendsVariableAsync", "101 friends were taken"); + sceNpTusDeleteRequest(req); + } + if (failures != 0) return 1; + std::puts("NpTus offline request tests passed"); + return 0; +} From 1f407e5970411881fc20f14835df729c721693c1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:33:20 +0300 Subject: [PATCH 0308/1360] docs(debt): note where the new NpTus request checks come from --- docs/dev/debt/libSceNpTus.md | 83 +----------------------------------- 1 file changed, 1 insertion(+), 82 deletions(-) diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 141e50a1cc..f21274d8ab 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -4,88 +4,21 @@ - [TUS variable outputs](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layouts of the variable arrays of the `sceNpTusGetMultiSlotVariable*` calls and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` are not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked; shadPS4, and RPCS3 in its PS3 `sceNpTusGetMultiSlotVariable*` and `sceNpTusAddAndGetVariable*` calls, return `INVALID_ALIGNMENT` unless the size is `arrayLen` variables of the call's type, or one variable for `outVariable`. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. - [Multi-slot variable calls](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the multi-slot get, set and delete calls are not measured on a console; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_tus/np_tus.cpp), where they differ between functions: `arrayLen < 1` is `INSUFFICIENT_ARGUMENT` in the NpId and virtual-user delete calls and in `sceNpTusGetMultiSlotVariableVUserAsync`, and `INVALID_ARGUMENT` in the others; a null NpId is accepted except in delete; an empty virtual user ID is `INVALID_ARGUMENT`. RPCS3's [PS3 module](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/Cell/Modules/sceNpTus.cpp) has only the NpId and virtual-user calls; it returns `INVALID_ARGUMENT` for an empty array in all of them, rejects a null target in get and set and does not check for an empty virtual user ID. +- [Variable, data, data status and TSS requests](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the 82 add-and-get, try-and-set, data, data status, multi-user, friends and `sceNpTssGetSmallStorage` calls are not measured on a console; their signatures, checks and order follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_tus/np_tus.cpp), including its differences between forms: the NpId try-and-set calls return `INVALID_ARGUMENT` for an operation type outside 1-6 where the others return `INVALID_TYPE`, and the NpId and virtual-user multi-slot data status calls return `TOO_MANY_NPID` for more than 64 slots. As for the calls above, the output and structure sizes are not checked, and every request finishes with `SCE_NP_ERROR_SIGNED_OUT` - [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotData](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssGetSmallStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssGetSmallStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssGetStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssGetStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature @@ -94,20 +27,6 @@ - [sceNpTusGetFriendsDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusDeleteNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the PS5 signature, error codes and child-request lifetime have not been measured on a console. Deletion invalidates the context and all its requests, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e7626e3c7de8d0f0fda6a91f620b42f6c762/src/core/libraries/np/np_tus/np_tus.cpp#L158-L180). ### Functional From 1860f903040b49cf0e227e500faaa3d28aea72b1 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 22:47:12 +1300 Subject: [PATCH 0309/1360] feat(libc): add std::system_category and store error_code values at offset 4 The console error_code and error_condition keep the value in the second dword: the shared error_category::equivalent reads it at +4, the default_error_condition overrides return it shifted into the high half, and title code built against the SDK headers stores 0 at +0 and reads value() at +4. The generic category returned and compared the value at +0. system_category returns a static _System_error_category named "system" that shares the generic message table and maps a value with a message to the generic category in default_error_condition. The equivalent overloads are shared by both categories and go through the object's own default_error_condition, as the console's base implementation does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../libc/src/exception/StandardExceptions.cpp | 56 +++++++++++++------ core/libs/tests/GuestAmprCppError.cpp | 8 +-- core/libs/tests/GuestAmprThreads.cpp | 1 + core/libs/tests/GuestGenericCategory.cpp | 44 ++++++++++++++- 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 31feeb3d8e..bf6b539f82 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -129,9 +129,10 @@ ExceptionObject* Assign(ExceptionObject* object, const ExceptionObject* source) } struct CategoryObject { const void* vtable; }; -struct ErrorCode { int value; const CategoryObject* category; }; +struct ErrorCode { int reserved; int value; const CategoryObject* category; }; struct SystemErrorObject { ExceptionObject base; ErrorCode code; }; -static_assert(sizeof(ErrorCode) == 16 && sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); +static_assert(sizeof(ErrorCode) == 16 && offsetof(ErrorCode, value) == 4 && offsetof(ErrorCode, category) == 8); +static_assert(sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); struct CppError { int value; const char* message; }; static const char* GenericErrorMessage(int value) { @@ -216,13 +217,15 @@ static const char* GenericErrorMessage(int value) { const TypeRecord ErrorCategoryType {ClassTypeVtable + 2, "St14error_category", nullptr}; const TypeRecord GenericCategoryType {SingleTypeVtable + 2, "St23_Generic_error_category", &ErrorCategoryType}; -static void APS5_VABI GenericCategoryDestroy_nid_no_patch(CategoryObject*) {} -static void APS5_VABI GenericCategoryDelete_nid_no_patch(CategoryObject*) { - throw std::invalid_argument("std::_Generic_error_category: the generic category object is not heap allocated"); +const TypeRecord SystemCategoryType {SingleTypeVtable + 2, "St22_System_error_category", &ErrorCategoryType}; +static void APS5_VABI CategoryDestroy_nid_no_patch(CategoryObject*) {} +static void APS5_VABI CategoryDelete_nid_no_patch(CategoryObject*) { + throw std::invalid_argument("std::error_category: the generic and system category objects are not heap allocated"); } static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; } -static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { - if (!result) throw std::invalid_argument("std::_Generic_error_category::message: null result"); +static const char* APS5_VABI SystemCategoryName_nid_no_patch(const CategoryObject*) { return "system"; } +static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + if (!result) throw std::invalid_argument("std::error_category::message: null result"); const char* message = GenericErrorMessage(value); if (!message) message = "unknown error"; const auto length = std::strlen(message); @@ -235,22 +238,36 @@ static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestL result->capacity = capacity; return result; } -static ErrorCode APS5_VABI GenericCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {value, self}; } -static bool APS5_VABI GenericCategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { - return condition->category == self && condition->value == value; +static ErrorCode APS5_VABI CategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {0, value, self}; } +static bool APS5_VABI CategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { + using Condition = ErrorCode (APS5_VABI *)(const CategoryObject*, int); + const auto mapped = reinterpret_cast(static_cast(self->vtable)[4])(self, value); + return condition->category == mapped.category && condition->value == mapped.value; } -static bool APS5_VABI GenericCategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { +static bool APS5_VABI CategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { return code->category == self && code->value == value; } const void* const GenericCategoryVtable[] { nullptr, &GenericCategoryType, - reinterpret_cast(GenericCategoryDestroy_nid_no_patch), reinterpret_cast(GenericCategoryDelete_nid_no_patch), - reinterpret_cast(GenericCategoryName_nid_no_patch), reinterpret_cast(GenericCategoryMessage_nid_no_patch), - reinterpret_cast(GenericCategoryCondition_nid_no_patch), - reinterpret_cast(GenericCategoryEquivalentCondition_nid_no_patch), - reinterpret_cast(GenericCategoryEquivalentCode_nid_no_patch) + reinterpret_cast(CategoryDestroy_nid_no_patch), reinterpret_cast(CategoryDelete_nid_no_patch), + reinterpret_cast(GenericCategoryName_nid_no_patch), reinterpret_cast(CategoryMessage_nid_no_patch), + reinterpret_cast(CategoryCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCode_nid_no_patch) }; const CategoryObject GenericCategory {GenericCategoryVtable + 2}; +static ErrorCode APS5_VABI SystemCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { + return {0, value, GenericErrorMessage(value) ? &GenericCategory : self}; +} +const void* const SystemCategoryVtable[] { + nullptr, &SystemCategoryType, + reinterpret_cast(CategoryDestroy_nid_no_patch), reinterpret_cast(CategoryDelete_nid_no_patch), + reinterpret_cast(SystemCategoryName_nid_no_patch), reinterpret_cast(CategoryMessage_nid_no_patch), + reinterpret_cast(SystemCategoryCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCode_nid_no_patch) +}; +const CategoryObject SystemCategory {SystemCategoryVtable + 2}; } extern "C" { @@ -260,9 +277,12 @@ const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value) { const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { return &LibcException::GenericCategory; } +const LibcException::CategoryObject* APS5_VABI _ZSt15system_categoryv_nid_postfix() { + return &LibcException::SystemCategory; +} GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( GuestLocale::String* result, const LibcException::CategoryObject* self, int value) { - return LibcException::GenericCategoryMessage_nid_no_patch(result, self, value); + return LibcException::CategoryMessage_nid_no_patch(result, self, value); } LibcException::TypeRecord _ZTISt8ios_base_nid_postfix {LibcException::ClassTypeVtable + 2, "St8ios_base", nullptr}; LibcException::TypeRecord _ZTISt9exception_nid_postfix {LibcException::ClassTypeVtable + 2, "St9exception", nullptr}; @@ -509,7 +529,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep std::snprintf(message, sizeof(message), "%s: %s", error.message, LibcException::GenericErrorMessage(error.value)); auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::SystemErrorObject))); LibcException::Construct(&object->base, _ZTVSt12system_error_nid_postfix, message); - object->code = {error.value, &LibcException::GenericCategory}; + object->code = {0, error.value, &LibcException::GenericCategory}; __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12system_error_nid_postfix), reinterpret_cast(LibcException::DestroyGuestMessage_nid_no_patch)); } diff --git a/core/libs/tests/GuestAmprCppError.cpp b/core/libs/tests/GuestAmprCppError.cpp index 8e2407c199..2cd8eceea5 100644 --- a/core/libs/tests/GuestAmprCppError.cpp +++ b/core/libs/tests/GuestAmprCppError.cpp @@ -12,7 +12,7 @@ struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; struct Category { const void* const* vtable; }; -struct ErrorCode { int value; const Category* category; }; +struct ErrorCode { int reserved; int value; const Category* category; }; struct SystemError { const void* const* vtable; const char* message; ErrorCode code; }; static_assert(sizeof(SystemError) == 32 && offsetof(SystemError, code) == 16); @@ -80,8 +80,8 @@ static void CheckCategory(const SystemError& error) { Require(std::strcmp(Slot(category->vtable, 2)(category), "generic") == 0, 23); const ErrorCode condition = Slot(category->vtable, 4)(category, error.code.value); Require(condition.value == error.code.value && condition.category == category, 24); - const ErrorCode other {error.code.value + 1, category}; - const ErrorCode foreign {error.code.value, nullptr}; + const ErrorCode other {0, error.code.value + 1, category}; + const ErrorCode foreign {0, error.code.value, nullptr}; auto equivalentCondition = Slot(category->vtable, 5); Require(equivalentCondition(category, error.code.value, &condition), 25); Require(!equivalentCondition(category, error.code.value, &other), 26); @@ -111,7 +111,7 @@ static void CheckCopies(const SystemError& error) { Require(copy.code.value == error.code.value && copy.code.category == error.code.category, 41); SystemError assigned {}; _ZNSt12system_errorC1ERKS__nid_postfix(&assigned, &error); - assigned.code = {0, nullptr}; + assigned.code = {0, 0, nullptr}; Require(_ZNSt12system_erroraSERKS__nid_postfix(&assigned, &error) == &assigned, 42); Require(assigned.code.value == error.code.value && assigned.code.category == error.code.category, 43); _ZNSt12system_errorD1Ev_nid_postfix(&assigned); diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 403c445304..5467abcebd 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -38,6 +38,7 @@ struct GuestTypeInfo { struct GuestSystemError { const void* vtable; const char* message; + int reserved; int code; const void* category; }; diff --git a/core/libs/tests/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp index b4f3f98687..bc279a1a56 100644 --- a/core/libs/tests/GuestGenericCategory.cpp +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -9,14 +9,19 @@ #include struct Category { void* const* vtable; }; -struct ErrorCode { int value; const Category* category; }; +struct ErrorCode { int reserved; int value; const Category* category; }; using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); using Name = const char* (APS5_VABI *)(const Category*); using Condition = ErrorCode (APS5_VABI *)(const Category*, int); +using EquivalentCondition = bool (APS5_VABI *)(const Category*, int, const ErrorCode*); +using EquivalentCode = bool (APS5_VABI *)(const Category*, const ErrorCode*, int); +using Delete = void (APS5_VABI *)(const Category*); +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; extern "C" { const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value); const Category* APS5_VABI _ZSt16generic_categoryv_nid_postfix(); +const Category* APS5_VABI _ZSt15system_categoryv_nid_postfix(); GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( GuestLocale::String* result, const Category* self, int value); int* APS5_VABI __error_nid_postfix(); @@ -48,6 +53,36 @@ static void CheckMessage(Message message, const Category* category, int value, c if (length > 15) _ZdlPv_nid_postfix(guarded.result.pointer); } +static void CheckSystemCategory(const Category* generic) { + const Category* system = _ZSt15system_categoryv_nid_postfix(); + REQUIRE(system && system != generic && system == _ZSt15system_categoryv_nid_postfix()); + const auto* type = static_cast(system->vtable[-1]); + REQUIRE(std::strcmp(type->name, "St22_System_error_category") == 0); + REQUIRE(type->base && std::strcmp(type->base->name, "St14error_category") == 0); + REQUIRE(std::strcmp(reinterpret_cast(system->vtable[2])(system), "system") == 0); + const auto condition = reinterpret_cast(system->vtable[4]); + const ErrorCode known = condition(system, 2); + std::uint64_t head = 0; + std::memcpy(&head, &known, sizeof(head)); + REQUIRE(head == std::uint64_t{2} << 32 && known.value == 2 && known.category == generic); + const ErrorCode unknown = condition(system, 44); + REQUIRE(unknown.reserved == 0 && unknown.value == 44 && unknown.category == system); + const auto equivalentCondition = reinterpret_cast(system->vtable[5]); + REQUIRE(equivalentCondition(system, 2, &known)); + const ErrorCode sameCategory {0, 2, system}; + REQUIRE(!equivalentCondition(system, 2, &sameCategory)); + REQUIRE(equivalentCondition(system, 44, &unknown)); + const auto equivalentCode = reinterpret_cast(system->vtable[6]); + REQUIRE(equivalentCode(system, &sameCategory, 2) && !equivalentCode(system, &known, 2)); + const auto message = reinterpret_cast(system->vtable[3]); + CheckMessage(message, system, 2, "no such file or directory"); + CheckMessage(message, system, 44, "unknown error"); + bool rejected = false; + try { reinterpret_cast(system->vtable[1])(system); } + catch (const std::invalid_argument&) { rejected = true; } + REQUIRE(rejected); +} + int main() { const std::array api {}; ApplicationHeapRegister_nid_no_patch(api.data()); @@ -56,7 +91,10 @@ int main() { REQUIRE(std::strcmp(reinterpret_cast(category->vtable[2])(category), "generic") == 0); const auto message = reinterpret_cast(category->vtable[3]); const auto condition = reinterpret_cast(category->vtable[4])(category, 35); - REQUIRE(condition.value == 35 && condition.category == category); + REQUIRE(condition.reserved == 0 && condition.value == 35 && condition.category == category); + const auto equivalent = reinterpret_cast(category->vtable[5]); + REQUIRE(equivalent(category, 35, &condition)); + CheckSystemCategory(category); struct Expected { int value; const char* text; }; const Expected expected[] { @@ -95,5 +133,5 @@ int main() { try { message(nullptr, category, 22); } catch (const std::invalid_argument&) { rejected = true; } REQUIRE(rejected); - std::puts("guest generic category, FreeBSD error map and message ownership: PASS"); + std::puts("guest generic and system categories, FreeBSD error map and message ownership: PASS"); } From c909d51447204a400a40ace51998c34ca4684336 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 06:55:03 -0300 Subject: [PATCH 0310/1360] test(libSceFont): open the external font through a guest path alias sceFontOpenFontFile resolves guest paths, so a host-absolute temporary path resolved beneath the run directory on Linux. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFont.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index b59ca51abb..a8a920a57c 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,4 +1,5 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libc/include/General.hpp" #include "tests/FontFixture.hpp" #include #include @@ -282,10 +283,13 @@ int main() { file.write(reinterpret_cast(fontData.data()), static_cast(fontData.size())); Require(static_cast(file)); } - const std::u8string fileFontText = fileFont.u8string(); + const std::u8string fontRootText = fontRoot.u8string(); + AddPathAlias_nid_no_patch("/font-test", reinterpret_cast(fontRootText.c_str())); + const std::u8string fileFontText = u8"/font-test/external-a\u00e7\u00e3o-\u65e5\u672c/font.ttf"; FontHandle fileHandle = nullptr; Require(sceFontOpenFontFile(library, reinterpret_cast(fileFontText.c_str()), 1, nullptr, &fileHandle) == SCE_FONT_OK && fileHandle != nullptr); Require(sceFontCloseFont(fileHandle) == SCE_FONT_OK); + RemovePathAlias_nid_no_patch("/font-test"); Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); int setting = -1; Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_OK && setting == 0); From 30785885f761c311c3352e577951950fcc754294 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 22:56:09 +1300 Subject: [PATCH 0311/1360] fix(libSceAudioOut): return what the firmware returns from three info calls sceAudioOutGetInfo and sceAudioOutGetInfoOpenNum return 0 and sceAudioOutGetSimulatedHandleStatusInfo returns 0x809B00FF without reading their arguments. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 ++++++++ .../prx/libSceAudioOut/src/Unimplemented.cpp | 5 +--- .../prx/libSceAudioOut/tests/AudioOutInfo.cpp | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutInfo.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index af79a20cf1..38a5382161 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -171,6 +171,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_info_tests tests/AudioOutInfo.cpp) + target_include_directories(audio_out_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_info_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_info_tests) + add_test(NAME audio_out_info COMMAND audio_out_info_tests) + if(WIN32) + set_tests_properties(audio_out_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_port_layouts_tests tests/AudioOut2PortLayouts.cpp) target_include_directories(audio_out2_port_layouts_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_port_layouts_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 02abcf4fd9..18ebf5f692 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -36,8 +36,7 @@ int APS5_VABI sceAudioOutChangeAppModuleState(void) { } int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x809B00FF); } int APS5_VABI sceAudioOutGetSimulatedBusUsableStatusByBusType(void) { @@ -91,12 +90,10 @@ int APS5_VABI sceAudioOutExGetLastOutputTime(void) { } int APS5_VABI sceAudioOutGetInfoOpenNum(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutGetInfo(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutInfo.cpp new file mode 100644 index 0000000000..78d9fe32cb --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutInfo.cpp @@ -0,0 +1,23 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(); +int APS5_VABI sceAudioOutGetInfoOpenNum(); +int APS5_VABI sceAudioOutGetInfo(); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + Require(sceAudioOutGetSimulatedHandleStatusInfo() == static_cast(0x809B00FF), "simulated handle status info is not supported"); + Require(sceAudioOutGetInfoOpenNum() == 0, "open port count info returns 0"); + Require(sceAudioOutGetInfo() == 0, "port info returns 0"); + std::puts("audio out info calls: PASS"); +} From 52a439bc709e801f162b0b886c014bc30223dd0c Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:59:04 +0000 Subject: [PATCH 0312/1360] feat(libSceLibreSsl): implement the MD4, MD5, SHA-1 and SHA-2 digests MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and the SHA256 one-shot threw. They now hash as LibreSSL does, on the caller's MD4_CTX, MD5_CTX, SHA_CTX, SHA256_CTX and SHA512_CTX laid out as in LibreSSL's md4.h, md5.h and sha.h, so a context copied or inspected by the title stays valid: the bit count in Nl/Nh, the partial block in data with its length in num, and md_len selecting the truncated digest. Final pads and clears the buffer as LibreSSL's sources do. Cases that differ between LibreSSL versions throw: SHA256 with a null digest buffer and SHA512_Final with the SHA-512/224 or SHA-512/256 length. The test checks the RFC 1320, RFC 1321 and FIPS 180 vectors over several chunk sizes, the context fields, copied contexts and those cases. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 16 + core/libs/prx/libSceLibreSsl/Export.cpp | 110 ---- core/libs/prx/libSceLibreSsl/src/Digest.cpp | 549 ++++++++++++++++++++ core/libs/tests/GuestLibreSslDigest.cpp | 195 +++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 762 insertions(+), 111 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/CMakeLists.txt create mode 100644 core/libs/prx/libSceLibreSsl/src/Digest.cpp create mode 100644 core/libs/tests/GuestLibreSslDigest.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt new file mode 100644 index 0000000000..d9d266270c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLibreSsl) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDigest.cpp) + target_include_directories(guest_libressl_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_digest_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_digest_tests) + add_test(NAME guest_libressl_digest COMMAND guest_libressl_digest_tests) + set_tests_properties(guest_libressl_digest PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 93b05e0fec..670bb8911e 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -846,36 +846,6 @@ int APS5_VABI HMAC_Update_nid_postfix(void) { return 0; } -int APS5_VABI MD4_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD4_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD4_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OBJ_cleanup_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1116,86 +1086,6 @@ int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { return 0; } -int APS5_VABI SHA1_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA1_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA1_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Digest.cpp b/core/libs/prx/libSceLibreSsl/src/Digest.cpp new file mode 100644 index 0000000000..e58974597e --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Digest.cpp @@ -0,0 +1,549 @@ +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { +struct Md4Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Md5Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha1Context { + std::uint32_t h[5]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha256Context { + std::uint32_t h[8]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num, mdLength; +}; + +struct Sha512Context { + std::uint64_t h[8]; + std::uint64_t Nl, Nh; + std::uint64_t data[16]; + std::uint32_t num, mdLength; +}; + +static_assert(sizeof(Md4Context) == 92); +static_assert(sizeof(Md5Context) == 92); +static_assert(sizeof(Sha1Context) == 96); +static_assert(sizeof(Sha256Context) == 112); +static_assert(sizeof(Sha512Context) == 216); + +constexpr std::size_t Sha224Length = 28; +constexpr std::size_t Sha256Length = 32; +constexpr std::size_t Sha384Length = 48; +constexpr std::size_t Sha512Length = 64; + +std::uint32_t LoadLittle32(const unsigned char* p) { + return std::uint32_t(p[0]) | std::uint32_t(p[1]) << 8 | std::uint32_t(p[2]) << 16 | std::uint32_t(p[3]) << 24; +} + +std::uint32_t LoadBig32(const unsigned char* p) { + return std::uint32_t(p[0]) << 24 | std::uint32_t(p[1]) << 16 | std::uint32_t(p[2]) << 8 | std::uint32_t(p[3]); +} + +std::uint64_t LoadBig64(const unsigned char* p) { + return std::uint64_t(LoadBig32(p)) << 32 | LoadBig32(p + 4); +} + +void StoreLittle32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast(value >> (8 * i)); +} + +void StoreBig32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast(value >> (24 - 8 * i)); +} + +void StoreBig64(unsigned char* p, std::uint64_t value) { + StoreBig32(p, static_cast(value >> 32)); + StoreBig32(p + 4, static_cast(value)); +} + +void Md4Block(Md4Context& c, const unsigned char* block) { + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + const auto f = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (~x1 & z); }; + const auto g = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (x1 & z) | (y & z); }; + const auto h = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return x1 ^ y ^ z; }; + static constexpr int shifts1[4] {3, 7, 11, 19}; + static constexpr int shifts2[4] {3, 5, 9, 13}; + static constexpr int shifts3[4] {3, 9, 11, 15}; + static constexpr int order2[16] {0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}; + static constexpr int order3[16] {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}; + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + f(b, cc, d) + x[i], shifts1[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + g(b, cc, d) + x[order2[i]] + 0x5a827999u, shifts2[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + h(b, cc, d) + x[order3[i]] + 0x6ed9eba1u, shifts3[i % 4]); + a = d; d = cc; cc = b; b = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Md5Block(Md5Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static constexpr int shifts[16] {7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + for (int i = 0; i < 64; ++i) { + std::uint32_t mix; + int index; + switch (i / 16) { + case 0: mix = (b & cc) | (~b & d); index = i; break; + case 1: mix = (d & b) | (~d & cc); index = (5 * i + 1) % 16; break; + case 2: mix = b ^ cc ^ d; index = (3 * i + 5) % 16; break; + default: mix = cc ^ (b | ~d); index = (7 * i) % 16; break; + } + const auto t = d; + d = cc; + cc = b; + b += std::rotl(a + mix + constants[i] + x[index], shifts[(i / 16) * 4 + i % 4]); + a = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Sha1Block(Sha1Context& c, const unsigned char* block) { + std::uint32_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 80; ++i) w[i] = std::rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + std::uint32_t a = c.h[0], b = c.h[1], cc = c.h[2], d = c.h[3], e = c.h[4]; + for (int i = 0; i < 80; ++i) { + std::uint32_t f, k; + if (i < 20) { f = (b & cc) | (~b & d); k = 0x5a827999; } + else if (i < 40) { f = b ^ cc ^ d; k = 0x6ed9eba1; } + else if (i < 60) { f = (b & cc) | (b & d) | (cc & d); k = 0x8f1bbcdc; } + else { f = b ^ cc ^ d; k = 0xca62c1d6; } + const auto t = std::rotl(a, 5) + f + e + k + w[i]; + e = d; d = cc; cc = std::rotl(b, 30); b = a; a = t; + } + c.h[0] += a; c.h[1] += b; c.h[2] += cc; c.h[3] += d; c.h[4] += e; +} + +void Sha256Block(Sha256Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + std::uint32_t w[64]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 64; ++i) { + const auto s0 = std::rotr(w[i - 15], 7) ^ std::rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const auto s1 = std::rotr(w[i - 2], 17) ^ std::rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint32_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 64; ++i) { + const auto s1 = std::rotr(v[4], 6) ^ std::rotr(v[4], 11) ^ std::rotr(v[4], 25); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 2) ^ std::rotr(v[0], 13) ^ std::rotr(v[0], 22); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +void Sha512Block(Sha512Context& c, const unsigned char* block) { + static constexpr std::uint64_t constants[80] { + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, + 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, + 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, + 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, + 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, + 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, + 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, + 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, + 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, + }; + std::uint64_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig64(block + 8 * i); + for (int i = 16; i < 80; ++i) { + const auto s0 = std::rotr(w[i - 15], 1) ^ std::rotr(w[i - 15], 8) ^ (w[i - 15] >> 7); + const auto s1 = std::rotr(w[i - 2], 19) ^ std::rotr(w[i - 2], 61) ^ (w[i - 2] >> 6); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint64_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 80; ++i) { + const auto s1 = std::rotr(v[4], 14) ^ std::rotr(v[4], 18) ^ std::rotr(v[4], 41); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 28) ^ std::rotr(v[0], 34) ^ std::rotr(v[0], 39); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +template TContext& RequireContext(TContext* context, const char* function) { + if (!context) throw std::invalid_argument(std::string(function) + ": null context"); + return *context; +} + +void RequireInput(const void* data, std::size_t length, const char* function) { + if (!data && length) throw std::invalid_argument(std::string(function) + ": null input"); +} + +void RequireDigest(const unsigned char* digest, const char* function) { + if (!digest) throw std::invalid_argument(std::string(function) + ": null digest buffer"); +} + +void RequireBuffered(std::uint32_t buffered, std::size_t block, const char* function) { + if (buffered >= block) throw std::invalid_argument(std::string(function) + ": buffered byte count is not below the block size"); +} + +template void Update32(TContext& c, const void* input, std::size_t length, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast(c.data); + auto* data = static_cast(input); + const std::uint64_t bits = (std::uint64_t(c.Nh) << 32 | c.Nl) + (std::uint64_t(length) << 3); + c.Nl = static_cast(bits); + c.Nh = static_cast(bits >> 32); + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast(length); + return; + } + std::memcpy(buffer + c.num, data, room); + block(c, buffer); + data += room; + length -= room; + c.num = 0; + std::memset(buffer, 0, blockSize); + } + for (; length >= blockSize; data += blockSize, length -= blockSize) block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast(length); + } +} + +template void Final32(TContext& c, bool bigEndian, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 8) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 8 - n); + if (bigEndian) { + StoreBig32(buffer + 56, c.Nh); + StoreBig32(buffer + 60, c.Nl); + } else { + StoreLittle32(buffer + 56, c.Nl); + StoreLittle32(buffer + 60, c.Nh); + } + block(c, buffer); + c.num = 0; + std::memset(buffer, 0, blockSize); +} + +void Sha512Update(Sha512Context& c, const void* input, std::size_t length) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast(c.data); + auto* data = static_cast(input); + const std::uint64_t low = c.Nl + (std::uint64_t(length) << 3); + if (low < c.Nl) ++c.Nh; + c.Nh += std::uint64_t(length) >> 61; + c.Nl = low; + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast(length); + return; + } + std::memcpy(buffer + c.num, data, room); + Sha512Block(c, buffer); + data += room; + length -= room; + c.num = 0; + } + for (; length >= blockSize; data += blockSize, length -= blockSize) Sha512Block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast(length); + } +} + +int Sha512Final(unsigned char* digest, Sha512Context& c) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 16) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + Sha512Block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 16 - n); + StoreBig64(buffer + 112, c.Nh); + StoreBig64(buffer + 120, c.Nl); + Sha512Block(c, buffer); + if (!digest) return 0; + if (c.mdLength == Sha224Length || c.mdLength == Sha256Length) + NotImplemented_nid_no_patch("SHA512_Final: SHA-512/224 and SHA-512/256 digest lengths"); + if (c.mdLength != Sha384Length && c.mdLength != Sha512Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 8; ++i) StoreBig64(digest + 8 * i, c.h[i]); + return 1; +} + +int Sha256Final(unsigned char* digest, Sha256Context& c, const char* function) { + RequireDigest(digest, function); + RequireBuffered(c.num, 64, function); + Final32(c, true, Sha256Block); + if (c.mdLength > Sha256Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 4; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} +} + +extern "C" { + +int APS5_VABI MD4_Init_nid_postfix(Md4Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD4_Update_nid_postfix(Md4Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md4Block); + return 1; +} + +int APS5_VABI MD4_Final_nid_postfix(unsigned char* digest, Md4Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md4Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI MD5_Init_nid_postfix(Md5Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD5_Update_nid_postfix(Md5Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md5Block); + return 1; +} + +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, Md5Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md5Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI SHA1_Init_nid_postfix(Sha1Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.h[0] = 0x67452301; + c.h[1] = 0xefcdab89; + c.h[2] = 0x98badcfe; + c.h[3] = 0x10325476; + c.h[4] = 0xc3d2e1f0; + return 1; +} + +int APS5_VABI SHA1_Update_nid_postfix(Sha1Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha1Block); + return 1; +} + +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, Sha1Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, true, Sha1Block); + for (int i = 0; i < 5; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} + +int APS5_VABI SHA224_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha224Length; + return 1; +} + +int APS5_VABI SHA256_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha256Length; + return 1; +} + +int APS5_VABI SHA256_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha256Block); + return 1; +} + +int APS5_VABI SHA224_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + return SHA256_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA256_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +int APS5_VABI SHA224_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + RequireInput(data, length, __func__); + if (!digest) NotImplemented_nid_no_patch("SHA256: null digest buffer"); + Sha256Context c; + SHA256_Init_nid_postfix(&c); + if (length) Update32(c, data, length, Sha256Block); + Sha256Final(digest, c, __func__); + return digest; +} + +int APS5_VABI SHA384_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, + 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha384Length; + return 1; +} + +int APS5_VABI SHA512_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha512Length; + return 1; +} + +int APS5_VABI SHA512_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 128, __func__); + Sha512Update(c, data, length); + return 1; +} + +int APS5_VABI SHA384_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + return SHA512_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA512_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + auto& c = RequireContext(context, __func__); + RequireBuffered(c.num, 128, __func__); + return Sha512Final(digest, c); +} + +int APS5_VABI SHA384_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + return SHA512_Final_nid_postfix(digest, context); +} + +} diff --git a/core/libs/tests/GuestLibreSslDigest.cpp b/core/libs/tests/GuestLibreSslDigest.cpp new file mode 100644 index 0000000000..8b9ba6183b --- /dev/null +++ b/core/libs/tests/GuestLibreSslDigest.cpp @@ -0,0 +1,195 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI MD4_Init_nid_postfix(void*); +int APS5_VABI MD4_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD4_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI MD5_Init_nid_postfix(void*); +int APS5_VABI MD5_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD5_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA1_Init_nid_postfix(void*); +int APS5_VABI SHA1_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA224_Init_nid_postfix(void*); +int APS5_VABI SHA224_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA224_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA256_Init_nid_postfix(void*); +int APS5_VABI SHA256_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA256_Final_nid_postfix(unsigned char*, void*); +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +int APS5_VABI SHA384_Init_nid_postfix(void*); +int APS5_VABI SHA384_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA384_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA512_Init_nid_postfix(void*); +int APS5_VABI SHA512_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA512_Final_nid_postfix(unsigned char*, void*); +} + +using InitFunction = int (APS5_VABI*)(void*); +using UpdateFunction = int (APS5_VABI*)(void*, const void*, std::size_t); +using FinalFunction = int (APS5_VABI*)(unsigned char*, void*); + +struct Algorithm { + const char* name; + InitFunction init; + UpdateFunction update; + FinalFunction final; + std::size_t contextSize; + std::size_t blockSize; + std::size_t digestSize; + const char* expected[6]; +}; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +template static void RequireThrows(TAction action, const char* what) { + try { action(); } catch (const std::exception&) { return; } + Require(false, what); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +static std::string Digest(const Algorithm& algorithm, const std::string& message, std::size_t chunk) { + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(algorithm.init(context) == 1, "init returns 1"); + for (std::size_t offset = 0; offset < message.size(); offset += chunk) { + const auto length = std::min(chunk, message.size() - offset); + Require(algorithm.update(context, message.data() + offset, length) == 1, "update returns 1"); + } + Require(algorithm.final(digest, context) == 1, "final returns 1"); + return Hex(digest, algorithm.digestSize); +} + +int main() { + const std::string messages[6] { + "", + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + std::string(1000000, 'a'), + }; + const Algorithm algorithms[] { + {"MD4", MD4_Init_nid_postfix, MD4_Update_nid_postfix, MD4_Final_nid_postfix, 92, 64, 16, { + "31d6cfe0d16ae931b73c59d7e0c089c0", "a448017aaf21d8525fc10ae87aa6729d", "4691a9ec81b1a6bd1ab8557240b245c5", + nullptr, "e33b4ddc9c38f2199c3e7b164fcc0536", nullptr}}, + {"MD5", MD5_Init_nid_postfix, MD5_Update_nid_postfix, MD5_Final_nid_postfix, 92, 64, 16, { + "d41d8cd98f00b204e9800998ecf8427e", "900150983cd24fb0d6963f7d28e17f72", "8215ef0796a20bcaaae116d3876c664a", + "03dd8807a93175fb062dfb55dc7d359c", "57edf4a22be3c955ac49da2e2107b67a", "7707d6ae4e027c70eea2a935c2296f21"}}, + {"SHA1", SHA1_Init_nid_postfix, SHA1_Update_nid_postfix, SHA1_Final_nid_postfix, 96, 64, 20, { + "da39a3ee5e6b4b0d3255bfef95601890afd80709", "a9993e364706816aba3e25717850c26c9cd0d89d", "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "a49b2446a02c645bf419f995b67091253a04a259", "50abf5706a150990a08b2c5ea40fa0e585554732", "34aa973cd4c4daa4f61eeb2bdbad27316534016f"}}, + {"SHA224", SHA224_Init_nid_postfix, SHA224_Update_nid_postfix, SHA224_Final_nid_postfix, 112, 64, 28, { + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", "23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7", + "75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525", "c97ca9a559850ce97a04a96def6d99a9e0e0e2ab14e6b8df265fc0b3", + "b50aecbe4e9bb0b57bc5f3ae760a8e01db24f203fb3cdcd13148046e", "20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67"}}, + {"SHA256", SHA256_Init_nid_postfix, SHA256_Update_nid_postfix, SHA256_Final_nid_postfix, 112, 64, 32, { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1", + "f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e", "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"}}, + {"SHA384", SHA384_Init_nid_postfix, SHA384_Update_nid_postfix, SHA384_Final_nid_postfix, 216, 128, 48, { + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + "3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b", + "09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039", + "b12932b0627d1c060942f5447764155655bd4da0c9afa6dd9b9ef53129af1b8fb0195996d2de9ca0df9d821ffee67026", + "9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985"}}, + {"SHA512", SHA512_Init_nid_postfix, SHA512_Update_nid_postfix, SHA512_Final_nid_postfix, 216, 128, 64, { + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445", + "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909", + "72ec1ef1124a45b047e8b7c75a932195135bb61de24ec0d1914042246e0aec3a2354e093d76f3048b456764346900cb130d2a4fd5dd16abb5e30bcb850dee843", + "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b"}}, + }; + + for (const auto& algorithm : algorithms) { + for (int i = 0; i < 6; ++i) { + if (!algorithm.expected[i]) continue; + const auto& message = messages[i]; + const std::size_t chunks[] {message.size() ? message.size() : 1, 1, algorithm.blockSize - 1, algorithm.blockSize, algorithm.blockSize + 1, 1000}; + for (auto chunk : chunks) { + if (i == 5 && chunk < 64) continue; + Require(Digest(algorithm, message, chunk) == algorithm.expected[i], algorithm.name); + } + } + + alignas(16) unsigned char context[256] {}; + alignas(16) unsigned char copy[256] {}; + unsigned char digest[64] {}; + unsigned char copyDigest[64] {}; + const bool wide = algorithm.blockSize == 128; + const auto num = [&](const unsigned char* c) { + std::uint32_t value; + std::memcpy(&value, c + (wide ? 208 : (algorithm.contextSize == 112 ? 104 : algorithm.contextSize - 4)), 4); + return value; + }; + const auto bitsLow = [&](const unsigned char* c) { + const std::size_t offset = wide ? 64 : (algorithm.contextSize == 96 ? 20 : algorithm.contextSize == 112 ? 32 : 16); + std::uint64_t value = 0; + std::memcpy(&value, c + offset, wide ? 8 : 4); + return value; + }; + Require(algorithm.init(context) == 1, "init"); + Require(algorithm.update(context, nullptr, 0) == 1, "empty update with null data"); + Require(algorithm.update(context, "ab", 2) == 1, "partial update"); + Require(num(context) == 2 && bitsLow(context) == 16, "context records the buffered bytes and the bit count"); + std::memcpy(copy, context, sizeof(copy)); + Require(algorithm.update(context, "c", 1) == 1 && algorithm.update(copy, "c", 1) == 1, "update of a copied context"); + Require(algorithm.final(digest, context) == 1 && algorithm.final(copyDigest, copy) == 1, "final of a copied context"); + Require(Hex(digest, algorithm.digestSize) == algorithm.expected[1] && !std::memcmp(digest, copyDigest, sizeof(digest)), "copied context digest"); + if (!wide) Require(num(context) == 0, "final clears the buffered count"); + RequireThrows([&] { algorithm.init(nullptr); }, "null context"); + RequireThrows([&] { algorithm.update(context, nullptr, 1); }, "null data"); + if (!wide) RequireThrows([&] { algorithm.final(nullptr, context); }, "null digest"); + } + + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(SHA512_Init_nid_postfix(context) == 1 && SHA512_Final_nid_postfix(nullptr, context) == 0, "SHA512_Final without a digest buffer"); + SHA256_Init_nid_postfix(context); + std::uint32_t length = 33; + std::memcpy(context + 108, &length, 4); + Require(SHA256_Final_nid_postfix(digest, context) == 0, "SHA256_Final with an oversized digest length"); + SHA256_Init_nid_postfix(context); + length = 16; + std::memcpy(context + 108, &length, 4); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA256_Final_nid_postfix(digest, context) == 1 && Hex(digest, 16) == "e3b0c44298fc1c149afbf4c8996fb924" && digest[16] == 0xee, "SHA256_Final truncates to the digest length"); + SHA512_Init_nid_postfix(context); + length = 32; + std::memcpy(context + 212, &length, 4); + RequireThrows([&] { SHA512_Final_nid_postfix(digest, context); }, "SHA-512/256 digest length"); + SHA256_Init_nid_postfix(context); + length = 64; + std::memcpy(context + 104, &length, 4); + RequireThrows([&] { SHA256_Update_nid_postfix(context, "a", 1); }, "corrupt buffered count"); + + const auto* abc = reinterpret_cast("abc"); + Require(SHA256_nid_postfix(abc, 3, digest) == digest && Hex(digest, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot"); + Require(SHA256_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot of nothing"); + RequireThrows([&] { SHA256_nid_postfix(abc, 3, nullptr); }, "SHA256 one-shot without a digest buffer"); + std::puts("libSceLibreSsl digest tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index b8330484a5..bcaab509a1 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,4 +2,5 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 377 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw From 5a7ef7610021fb35ac1f44a5ba35885cf9368a1b Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:02:52 +1300 Subject: [PATCH 0313/1360] fix(libSceAudioOut): track mastering initialization as the console does The PS5 module keeps one mastering-initialized flag. A repeated initialization returns 0 before the flags are checked; otherwise sceAudioOut2MasteringInit accepts only 0 and sceAudioOut2MasteringInitEx also bit 31, and other flags return 0x80268201. Term, GetState and SetParam return 0x80268203 before initialization, GetState returns 0x8026800C for a null state and SetParam 0x80268205 for flags above 1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../libSceAudioOut/src/AudioOut2Mastering.cpp | 30 ++++++++++++++++--- .../prx/libSceAudioOut/src/Unimplemented.cpp | 5 ---- .../libSceAudioOut/tests/AudioOut2Latency.cpp | 28 ++++++++++++++--- docs/dev/debt/libSceAudioOut.md | 5 ++-- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp index e48537db95..8f7e6403dc 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -7,6 +8,21 @@ namespace { constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = static_cast(0x80268201); constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = static_cast(0x80268204); +constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED = static_cast(0x80268203); +constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_FLAG = static_cast(0x80268205); +constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_POINTER = static_cast(0x8026800C); +constexpr std::uint32_t AUDIO_OUT2_MASTERING_INIT_EX_MODE = 0x80000000u; +constexpr std::uint32_t AUDIO_OUT2_MASTERING_SET_PARAM_FLAGS_MAX = 1; + +std::atomic g_masteringInitialized{false}; + +int InitializeMastering(uint32_t flags, uint32_t accepted) { + if (g_masteringInitialized.load()) return 0; + if ((flags & ~accepted) != 0) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; + g_masteringInitialized.store(true); + return 0; +} + constexpr std::uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1; constexpr std::uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2; constexpr std::uint32_t AUDIO_OUT2_MASTERING_STRUCT_ID_COMPRESSOR_DEFAULT = 0x01010001; @@ -18,18 +34,23 @@ constexpr std::uint32_t AUDIO_OUT2_MASTERING_STRUCT_ID_LIMITER_DEFAULT = 0x01010 extern "C" { int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) { - if (flags != 0) NotImplemented_nid_no_patch(__func__); - return 0; + return InitializeMastering(flags, 0); +} + +int APS5_VABI sceAudioOut2MasteringInitEx(uint32_t flags) { + return InitializeMastering(flags, AUDIO_OUT2_MASTERING_INIT_EX_MODE); } int APS5_VABI sceAudioOut2MasteringTerm(void) { + if (!g_masteringInitialized.exchange(false)) return SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED; return 0; } int APS5_VABI sceAudioOut2MasteringGetState(AudioOut2MasteringStatesHeader* state, uint32_t output, AudioOut2UserHandle user) { (void)output; (void)user; - if (state == nullptr) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; + if (!g_masteringInitialized.load()) return SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED; + if (state == nullptr) return SCE_AUDIO_OUT2_ERROR_INVALID_POINTER; switch (state->states_id) { case AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT: { auto* full = static_cast(static_cast(state)); @@ -58,8 +79,9 @@ int APS5_VABI sceAudioOut2MasteringGetState(AudioOut2MasteringStatesHeader* stat int APS5_VABI sceAudioOut2MasteringSetParam(const AudioOut2MasteringParamsHeader* param, uint32_t output, uint32_t flags) { (void)output; - (void)flags; + if (!g_masteringInitialized.load()) return SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED; if (param == nullptr) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; + if (flags > AUDIO_OUT2_MASTERING_SET_PARAM_FLAGS_MAX) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_FLAG; return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 02abcf4fd9..4119c896d7 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -365,11 +365,6 @@ int APS5_VABI sceAudioOut2GetSystemStateEx(void) { return 0; } -int APS5_VABI sceAudioOut2MasteringInitEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { 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 9a09ec4fba..20426a0f00 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceAudioOut2LoInit(); int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); +int APS5_VABI sceAudioOut2MasteringInitEx(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioOut2MasteringGetState(void*, std::uint32_t, std::uint64_t); @@ -51,6 +52,9 @@ constexpr int errorInvalidParam = static_cast(0x80268001); constexpr int errorInvalidArgument = static_cast(0x80260502); constexpr int errorMasteringInvalidApiParam = static_cast(0x80268201); constexpr int errorMasteringInvalidStatesId = static_cast(0x80268204); +constexpr int errorMasteringNotInitialized = static_cast(0x80268203); +constexpr int errorMasteringInvalidFlag = static_cast(0x80268205); +constexpr int errorInvalidPointer = static_cast(0x8026800C); void TestSet3DLatency() { Require(sceAudioOut2Set3DLatency(systemUser, 0) == 0, "output 0 for the system user must be accepted"); @@ -63,14 +67,27 @@ void TestSet3DLatency() { } void TestMasteringInit() { + const std::uint32_t params[4] = {1u, 0u, 0u, 0u}; + alignas(16) std::uint8_t storage[512] {}; + Require(sceAudioOut2MasteringTerm() == errorMasteringNotInitialized, "termination before initialization is refused"); + Require(sceAudioOut2MasteringSetParam(params, 0, 0) == errorMasteringNotInitialized, "parameters before initialization are refused"); + Require(sceAudioOut2MasteringGetState(nullptr, 0, 0) == errorMasteringNotInitialized, "the initialization check comes before the null check"); + Require(sceAudioOut2MasteringGetState(storage, 0, 0) == errorMasteringNotInitialized, "states before initialization are refused"); + Require(sceAudioOut2MasteringInit(1) == errorMasteringInvalidApiParam, "flags 1 are refused"); + Require(sceAudioOut2MasteringInit(0x80000000u) == errorMasteringInvalidApiParam, "sceAudioOut2MasteringInit accepts no flag"); Require(sceAudioOut2MasteringInit(0) == 0, "flags 0 must be accepted"); - Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInit(1); }), "flags 1 must throw"); + Require(sceAudioOut2MasteringInit(1) == 0, "an initialized stage returns 0 before the flags are checked"); + Require(sceAudioOut2MasteringInit(0) == 0, "a second initialization must be accepted"); } void TestMasteringTerm() { - Require(sceAudioOut2MasteringInit(0) == 0, "a second initialization must be accepted"); Require(sceAudioOut2MasteringTerm() == 0, "termination must be accepted"); - Require(sceAudioOut2MasteringTerm() == 0, "termination must be accepted for each initialization"); + Require(sceAudioOut2MasteringTerm() == errorMasteringNotInitialized, "initializations are not counted"); + Require(sceAudioOut2MasteringInitEx(1) == errorMasteringInvalidApiParam, "sceAudioOut2MasteringInitEx refuses the low bits"); + Require(sceAudioOut2MasteringInitEx(0x80000001u) == errorMasteringInvalidApiParam, "sceAudioOut2MasteringInitEx refuses the low bits with the mode bit"); + Require(sceAudioOut2MasteringInitEx(0x80000000u) == 0, "sceAudioOut2MasteringInitEx accepts the mode bit"); + Require(sceAudioOut2MasteringTerm() == 0, "termination after sceAudioOut2MasteringInitEx must be accepted"); + Require(sceAudioOut2MasteringInitEx(0) == 0, "sceAudioOut2MasteringInitEx accepts flags 0"); } void TestMasteringSetParam() { @@ -78,11 +95,14 @@ void TestMasteringSetParam() { Require(sceAudioOut2MasteringSetParam(params, 0, 0) == 0, "mastering parameters must be accepted"); Require(sceAudioOut2MasteringSetParam(params, 2, 0) == 0, "mastering parameters for output 2 must be accepted"); Require(sceAudioOut2MasteringSetParam(nullptr, 0, 0) == errorMasteringInvalidApiParam, "null mastering parameters return the invalid API parameter error"); + Require(sceAudioOut2MasteringSetParam(nullptr, 0, 2) == errorMasteringInvalidApiParam, "the null check comes before the flag check"); + Require(sceAudioOut2MasteringSetParam(params, 0, 1) == 0, "flags 1 must be accepted"); + Require(sceAudioOut2MasteringSetParam(params, 0, 2) == errorMasteringInvalidFlag, "flags above 1 are refused"); } void TestMasteringGetState() { alignas(16) std::uint8_t storage[512]; - Require(sceAudioOut2MasteringGetState(nullptr, 0, 0) == errorMasteringInvalidApiParam, "a null state returns the invalid API parameter error"); + Require(sceAudioOut2MasteringGetState(nullptr, 0, 0) == errorInvalidPointer, "a null state returns the invalid pointer error"); auto* header = reinterpret_cast(storage); auto* words = reinterpret_cast(storage); *header = 1u; diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index f6e6901f1f..f8ebf06333 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -8,8 +8,8 @@ - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, output)`: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) reads `(user_id, output, latency_us)`, but the known calls set just edi and esi. The validation follows KytyPS5: an output above 2 returns `0x80268001` and every user id is accepted, where before only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) were; the latency value of the three-argument reading is never received, and there is no 3D rendering stage to apply anything to -- [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result -- [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the errors `0x80268201` and `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does; a null parameter returns `0x80268201`, the mastering invalid-API-parameter error KytyPS5 returns for a null state, which is an inference rather than a measurement. The meaning of `output` and `user` is unknown and both are ignored +- [sceAudioOut2MasteringInit and sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the initialized flag and the flag checks follow the PS5 12.00 module (0x22360): a repeated initialization returns 0, other flags return `0x80268201`, and InitEx also accepts bit 31, which the console passes to the audio server as a mastering mode. Nothing is sent to an audio server, and the `0x80268006` the console returns before `sceAudioOut2Initialize` is not returned, since that initialization is not tracked. PPSA01289 calls Init twice before Term twice; by the module its second Term gets `0x80268203`, and the title only logs a non-zero result +- [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the error `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does. The checks before them follow the PS5 12.00 module: both return `0x80268203` before mastering is initialized, GetState returns `0x8026800C` for a null state, SetParam returns `0x80268201` for a null parameter and `0x80268205` for flags above 1. The console's checks of `output` and `user` are not reproduced; both are ignored - [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. The debug state ids are unknown, so every id is accepted, and a null param returns `0x80260502` like the other AudioOut2 calls. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted @@ -97,7 +97,6 @@ - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From 98e8a2b791a03b55677d7ed35f30f64d33cdff6d Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:05:42 +1300 Subject: [PATCH 0314/1360] feat(libSceGameLiveStreaming): answer sceGameLiveStreamingGetProgramInfo with an empty program The PS5 module returns 0x80A00002 for a null pointer and otherwise clears the 0x134C-byte program info before filling it from the system service. Nothing is broadcast here, so the info stays zero. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceGameLiveStreaming/Export.cpp | 8 ++++++-- core/libs/tests/GuestGameLiveStreaming.cpp | 15 ++++++++------- docs/dev/debt/libSceGameLiveStreaming.md | 4 ++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index aa280dcad4..f4ff23ca08 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -17,6 +17,9 @@ struct GameLiveStreamingStatus2 { }; static_assert(sizeof(GameLiveStreamingStatus2) == 72); +constexpr int GAME_LIVE_STREAMING_ERROR_INVALID_ARGUMENT = static_cast(0x80A00002); +constexpr std::size_t GAME_LIVE_STREAMING_PROGRAM_INFO_SIZE = 0x134C; + extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { @@ -35,8 +38,9 @@ int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* st return 0; } -int APS5_VABI sceGameLiveStreamingGetProgramInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingGetProgramInfo(void* info) { + if (info == nullptr) return GAME_LIVE_STREAMING_ERROR_INVALID_ARGUMENT; + std::memset(info, 0, GAME_LIVE_STREAMING_PROGRAM_INFO_SIZE); return 0; } diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp index a60c2538cd..d49e2cd3c6 100644 --- a/core/libs/tests/GuestGameLiveStreaming.cpp +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -21,7 +22,7 @@ extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(std::size_t heap_size); int APS5_VABI sceGameLiveStreamingTerminate(void); int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status); -int APS5_VABI sceGameLiveStreamingGetProgramInfo(void); +int APS5_VABI sceGameLiveStreamingGetProgramInfo(void* info); } namespace { @@ -67,13 +68,13 @@ int main() { Require(byte == 0); } - bool threwProgramInfo = false; - try { - sceGameLiveStreamingGetProgramInfo(); - } catch (const std::runtime_error&) { - threwProgramInfo = true; + Require(sceGameLiveStreamingGetProgramInfo(nullptr) == static_cast(0x80A00002)); + std::array programInfo{}; + programInfo.fill(0x7f); + Require(sceGameLiveStreamingGetProgramInfo(programInfo.data()) == 0); + for (std::size_t i = 0; i < programInfo.size(); ++i) { + Require(programInfo[i] == (i < 0x134C ? 0 : 0x7f)); } - Require(threwProgramInfo); Require(sceGameLiveStreamingTerminate() == 0); } diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index 24dfaa238f..d953af25e8 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -1,5 +1,9 @@ # libSceGameLiveStreaming technical debt +### Functional + +- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 12.00 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown. Initialization is not tracked, so `0x80A00004` is never returned + ### Unknown function info - [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws From f347e96e38cc32284b8becc695424d2a7e7b6537 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:10:16 +1300 Subject: [PATCH 0315/1360] fix(libSceAudioOut): return 0 from sceAudioOut2LoTerminate and forward sceAudioOut2InitializeEx The PS5 module's sceAudioOut2LoTerminate stops the low-latency service and returns 0 whether or not it runs; sceAudioOut2LoInit starts no service here. sceAudioOut2InitializeEx stores its argument and runs sceAudioOut2Initialize. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp | 4 ++++ core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp | 5 +++++ core/libs/prx/libSceAudioOut/src/Unimplemented.cpp | 10 ---------- .../libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp | 5 ++++- docs/dev/debt/libSceAudioOut.md | 4 ++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp index eaba6e102d..eac5204b6d 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -22,6 +22,10 @@ int APS5_VABI sceAudioOut2LoInit(void) { return sceAudioOut2Initialize(); } +int APS5_VABI sceAudioOut2LoTerminate(void) { + return 0; +} + int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { return sceAudioOut2ContextQueryMemory(params, memory_size); } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index c647f607ea..fbe40625b2 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -15,6 +15,11 @@ int APS5_VABI sceAudioOut2Initialize(void) { return 0; } +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t param) { + (void)param; + return sceAudioOut2Initialize(); +} + int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { (void)userId; if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 02abcf4fd9..3457538766 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -380,11 +380,6 @@ int APS5_VABI sceAudioOut2DebugStateCtrl(void) { return 0; } -int APS5_VABI sceAudioOut2LoTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2ContextBedWrite(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -395,11 +390,6 @@ int APS5_VABI sceAudioOut2PortCreateEx(void) { return 0; } -int APS5_VABI sceAudioOut2InitializeEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2Terminate(void) { 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 9a09ec4fba..cbca1cf604 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -9,6 +9,7 @@ extern "C" { int APS5_VABI sceAudioOut2Initialize(); +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t); int APS5_VABI sceAudioOut2LoInit(); int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); @@ -122,7 +123,9 @@ void TestEnableChat() { void TestLowLatencyService() { Require(sceAudioOut2LoInit() == 0, "starting the low-latency service must be accepted"); Require(sceAudioOut2LoInit() == 0, "starting the low-latency service again must be accepted"); - Require(ThrowsRuntimeError([] { sceAudioOut2LoTerminate(); }), "stopping the low-latency service must throw"); + Require(sceAudioOut2LoTerminate() == 0, "stopping the low-latency service returns 0"); + Require(sceAudioOut2LoTerminate() == 0, "stopping a stopped low-latency service returns 0"); + Require(sceAudioOut2InitializeEx(0) == 0 && sceAudioOut2InitializeEx(1) == 0, "the extended initialization succeeds"); } } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index f6e6901f1f..9d41d61a53 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -91,7 +91,7 @@ - [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) throws. It takes no arguments and returns an int, and stops the low-latency service ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L497-L498)), but what it does to open contexts and ports and what it returns when the service is not started are unknown, and its counterpart `sceAudioOut2Terminate` is unimplemented. +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -102,7 +102,7 @@ - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the PS5 12.00 module (0x2a640) stores its 32-bit argument and runs `sceAudioOut2Initialize`; the argument's meaning is unknown and it is ignored - [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From e0f3e860a2f021103d782ce68fcd3430528e6d99 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:11:19 +1300 Subject: [PATCH 0316/1360] docs(libSceAudioOut): list sceAudioOut2LoTerminate under Silent stubs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libSceAudioOut.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 9d41d61a53..b63e38cdf9 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -1,5 +1,9 @@ # libSceAudioOut technical debt +### Silent stubs + +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are + ### Unknown function info - [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. @@ -91,7 +95,6 @@ - [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From 3b426ed9a41095304ed44535fa7015ed09adaa7c Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:47:06 +0530 Subject: [PATCH 0317/1360] test(libc): run CheckVacopy in guest_stdio_variadic main never called it, so the _Vacopy checks were dropped from the Release build and never ran. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/tests/GuestStdioVariadic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp index 7bc4ba7cdb..99e28777ff 100644 --- a/core/libs/tests/GuestStdioVariadic.cpp +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -141,5 +141,6 @@ int main() { char16_t letters[4]{}; Require(ScanWideSized(u"abcdef", u"%3ls", letters, 4u) == 1 && std::u16string(letters) == u"abc", "vswscanf_s %ls with a size"); Require(ScanWideSized(u"abcdef", u"%ls", letters, 4u) == 0 && letters[0] == 0, "vswscanf_s rejects a too small buffer"); + CheckVacopy(); return 0; } From 573113e8af258a3a98b7436f4f132317c9496760 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:17:45 +1300 Subject: [PATCH 0318/1360] feat(libSceAjm.native): build the WVorbis header packet sceAjmDecWVorbisCreateHeaderPacket checks its 0x48-byte parameter as the PS5 module does and writes a five-entry length table, a Vorbis identification header from the parameters, an empty comment header and the setup packet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAjm.native/CMakeLists.txt | 7 +- core/libs/prx/libSceAjm.native/Export.cpp | 90 ++++++++++++++- core/libs/tests/GuestAjmWVorbis.cpp | 107 ++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 3 +- 4 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestAjmWVorbis.cpp diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 51b7c2507f..6abdf89e98 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -71,6 +71,11 @@ if(BUILD_TESTING) target_link_libraries(guest_ajm_vibration_tests PRIVATE libSceAjm.native libc) configure_windows_unwind(guest_ajm_vibration_tests) add_test(NAME guest_ajm_vibration COMMAND guest_ajm_vibration_tests) + add_test_executable(guest_ajm_wvorbis_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmWVorbis.cpp) + target_include_directories(guest_ajm_wvorbis_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_wvorbis_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_wvorbis_tests) + add_test(NAME guest_ajm_wvorbis COMMAND guest_ajm_wvorbis_tests) if(UNIX) add_test_executable(guest_ajm_teardown_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmTeardown.cpp) target_include_directories(guest_ajm_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -79,7 +84,7 @@ if(BUILD_TESTING) add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $) endif() if(WIN32) - set_tests_properties(guest_ajm guest_ajm_vibration PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ajm guest_ajm_vibration guest_ajm_wvorbis PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 2df65a236d..5ee300cc79 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include // Entry points this library does not implement: reported before the unimplemented-call exception so an // APS5_TRACE_AJM run shows which one a title reached. @@ -12,6 +13,54 @@ static void AjmStub(const char* name) { NotImplemented_nid_no_patch(name); } +struct AjmDecWVorbisHeaderPacketParam { + std::uint64_t size; + std::uint32_t channels; + std::uint32_t sampleRate; + std::int32_t bytesPerSecond; + std::int32_t blockSize0Log2; + std::int32_t blockSize1Log2; + std::uint32_t reserved; + const std::uint8_t* setupPacket; + std::uint64_t setupPacketSize; + std::uint8_t* output; + std::uint64_t outputSize; + std::uint64_t writtenSize; +}; +static_assert(sizeof(AjmDecWVorbisHeaderPacketParam) == 0x48); + +namespace { + +constexpr int AJM_WVORBIS_ERROR_INVALID_PARAMETER = static_cast(0x8A6E0001); +constexpr int AJM_WVORBIS_ERROR_INVALID_SIZE = static_cast(0x8A6E0002); +constexpr int AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL = static_cast(0x8A6E0003); +constexpr int AJM_WVORBIS_ERROR_INVALID_SETUP = static_cast(0x8A6E0004); +constexpr std::size_t WVORBIS_LENGTH_TABLE_SIZE = 10; +constexpr std::size_t VORBIS_IDENTIFICATION_SIZE = 30; +constexpr std::size_t VORBIS_COMMENT_SIZE = 16; +constexpr std::size_t WVORBIS_SETUP_MIN_SIZE = 9; +constexpr std::size_t WVORBIS_SETUP_PART1_LENGTH_OFFSET = 7; +constexpr std::int32_t VORBIS_BLOCK_SIZE_MIN = 64; +constexpr std::int32_t VORBIS_BLOCK_SIZE_MAX = 8192; +constexpr std::uint8_t VORBIS_MAGIC[] {'v', 'o', 'r', 'b', 'i', 's'}; + +std::int32_t WVorbisBlockSize(std::int32_t log2) { + return static_cast(1u << (static_cast(log2) & 31u)); +} + +std::uint8_t* PutLittle(std::uint8_t* out, std::uint32_t value, std::size_t bytes) { + for (std::size_t i = 0; i < bytes; ++i) out[i] = static_cast(value >> (8 * i)); + return out + bytes; +} + +std::uint8_t* PutVorbisHeaderStart(std::uint8_t* out, std::uint8_t type) { + *out++ = type; + std::memcpy(out, VORBIS_MAGIC, sizeof(VORBIS_MAGIC)); + return out + sizeof(VORBIS_MAGIC); +} + +} + extern "C" { int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const void* pcm_input, size_t pcm_input_size, void* bitstream_output, size_t bitstream_output_size, void* result) { @@ -32,8 +81,45 @@ const char* APS5_VABI sceAjmStrError(int error) { return nullptr; } -int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(AjmDecWVorbisHeaderPacketParam* param) { + if (param == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->size != sizeof(AjmDecWVorbisHeaderPacketParam)) return AJM_WVORBIS_ERROR_INVALID_SIZE; + if (param->bytesPerSecond < 0) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (WVorbisBlockSize(param->blockSize0Log2) < VORBIS_BLOCK_SIZE_MIN) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->blockSize1Log2 < param->blockSize0Log2 || WVorbisBlockSize(param->blockSize1Log2) > VORBIS_BLOCK_SIZE_MAX) + return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->setupPacket == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->setupPacketSize < WVORBIS_SETUP_MIN_SIZE) return AJM_WVORBIS_ERROR_INVALID_SETUP; + if (param->output == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + constexpr std::size_t headers = WVORBIS_LENGTH_TABLE_SIZE + VORBIS_IDENTIFICATION_SIZE + VORBIS_COMMENT_SIZE; + if (param->outputSize < headers || param->setupPacketSize > param->outputSize - headers) return AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL; + + const std::uint8_t* setup = param->setupPacket; + const auto part1 = static_cast(setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET] | setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET + 1] << 8); + std::uint8_t* out = param->output; + out = PutLittle(out, 0, 2); + out = PutLittle(out, VORBIS_IDENTIFICATION_SIZE, 2); + out = PutLittle(out, VORBIS_COMMENT_SIZE, 2); + out = PutLittle(out, part1, 2); + out = PutLittle(out, static_cast(param->setupPacketSize) - part1, 2); + + out = PutVorbisHeaderStart(out, 1); + out = PutLittle(out, 0, 4); + out = PutLittle(out, param->channels, 1); + out = PutLittle(out, param->sampleRate, 4); + out = PutLittle(out, 0, 4); + out = PutLittle(out, static_cast(param->bytesPerSecond) * 8u, 4); + out = PutLittle(out, 0, 4); + *out++ = static_cast((param->blockSize0Log2 & 0xF) | (param->blockSize1Log2 & 0xF) << 4); + *out++ = 1; + + out = PutVorbisHeaderStart(out, 3); + out = PutLittle(out, 0, 4); + out = PutLittle(out, 0, 4); + *out++ = 1; + + std::memcpy(out, setup, param->setupPacketSize); + param->writtenSize = headers + param->setupPacketSize; return 0; } diff --git a/core/libs/tests/GuestAjmWVorbis.cpp b/core/libs/tests/GuestAjmWVorbis.cpp new file mode 100644 index 0000000000..4344e98e00 --- /dev/null +++ b/core/libs/tests/GuestAjmWVorbis.cpp @@ -0,0 +1,107 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +struct HeaderPacketParam { + std::uint64_t size; + std::uint32_t channels; + std::uint32_t sampleRate; + std::int32_t bytesPerSecond; + std::int32_t blockSize0Log2; + std::int32_t blockSize1Log2; + std::uint32_t reserved; + const std::uint8_t* setupPacket; + std::uint64_t setupPacketSize; + std::uint8_t* output; + std::uint64_t outputSize; + std::uint64_t writtenSize; +}; +static_assert(sizeof(HeaderPacketParam) == 0x48); + +extern "C" int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(HeaderPacketParam* param); + +namespace { + +constexpr int InvalidParameter = static_cast(0x8A6E0001); +constexpr int InvalidSize = static_cast(0x8A6E0002); +constexpr int OutputTooSmall = static_cast(0x8A6E0003); +constexpr int InvalidSetup = static_cast(0x8A6E0004); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +constexpr std::array Setup {5, 'v', 'o', 'r', 'b', 'i', 's', 9, 0, 0xAA, 0xBB, 0xCC}; + +HeaderPacketParam Valid(std::uint8_t* output, std::size_t outputSize) { + return {sizeof(HeaderPacketParam), 2, 48000, 16000, 8, 11, 0, Setup.data(), Setup.size(), output, outputSize, 0}; +} + +int Create(HeaderPacketParam param) { return sceAjmDecWVorbisCreateHeaderPacket(¶m); } + +void TestValidation() { + std::array output {}; + Require(sceAjmDecWVorbisCreateHeaderPacket(nullptr) == InvalidParameter, "a null parameter is refused"); + auto param = Valid(output.data(), output.size()); + param.size = 0x40; + Require(Create(param) == InvalidSize, "a wrong structure size is refused"); + param = Valid(output.data(), output.size()); + param.bytesPerSecond = -1; + Require(Create(param) == InvalidParameter, "a negative byte rate is refused"); + param = Valid(output.data(), output.size()); + param.blockSize0Log2 = 5; + Require(Create(param) == InvalidParameter, "a block size below 64 is refused"); + param.blockSize0Log2 = 38; + param.blockSize1Log2 = 38; + Require(Create(param) == 0, "block size exponents are taken modulo 32 as the console's shift does"); + param = Valid(output.data(), output.size()); + param.blockSize1Log2 = 7; + Require(Create(param) == InvalidParameter, "block size 1 below block size 0 is refused"); + param.blockSize1Log2 = 14; + Require(Create(param) == InvalidParameter, "a block size above 8192 is refused"); + param = Valid(output.data(), output.size()); + param.setupPacket = nullptr; + param.setupPacketSize = 0; + Require(Create(param) == InvalidParameter, "a null setup packet is refused before its size"); + param = Valid(output.data(), output.size()); + param.setupPacketSize = 8; + Require(Create(param) == InvalidSetup, "a setup packet below 9 bytes is refused"); + param = Valid(nullptr, 0); + Require(Create(param) == InvalidParameter, "a null output is refused before its size"); + param = Valid(output.data(), 9); + Require(Create(param) == OutputTooSmall, "an output below 10 bytes is refused"); + param = Valid(output.data(), 56 + Setup.size() - 1); + Require(Create(param) == OutputTooSmall, "an output that cannot hold the setup packet is refused"); +} + +void TestPacket() { + std::array output {}; + output.fill(0xEE); + auto param = Valid(output.data(), output.size()); + Require(sceAjmDecWVorbisCreateHeaderPacket(¶m) == 0, "a valid header packet is built"); + Require(param.writtenSize == 56 + Setup.size(), "the written size covers the table, both headers and the setup packet"); + constexpr std::uint8_t Expected[] { + 0, 0, 30, 0, 16, 0, 9, 0, 3, 0, + 1, 'v', 'o', 'r', 'b', 'i', 's', 0, 0, 0, 0, 2, 0x80, 0xBB, 0, 0, 0, 0, 0, 0, 0x00, 0xF4, 0x01, 0x00, 0, 0, 0, 0, 0xB8, 1, + 3, 'v', 'o', 'r', 'b', 'i', 's', 0, 0, 0, 0, 0, 0, 0, 0, 1, + }; + static_assert(sizeof(Expected) == 56); + Require(std::memcmp(output.data(), Expected, sizeof(Expected)) == 0, "the table, identification and comment headers match"); + Require(std::memcmp(output.data() + 56, Setup.data(), Setup.size()) == 0, "the setup packet follows unchanged"); + Require(output[56 + Setup.size()] == 0xEE, "nothing is written past the packet"); +} + +} + +int main() { + TestValidation(); + TestPacket(); + std::puts("guest ajm wvorbis header packet: PASS"); +} diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index f8982027f0..f92b3eba29 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,7 @@ - [sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ajm/ajm.h#L19-L20) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L100-L104), not measured on a console; these throwing declarations only resolve imports. - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. -- [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures +- [sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signature - [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 @@ -21,6 +21,7 @@ ### Functional +- [sceAjmDecWVorbisCreateHeaderPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - refuses an output smaller than the whole packet (56 bytes plus the setup packet) with `0x8A6E0003`. The PS5 12.00 bit writer only checks that each write starts inside the buffer, so the console can accept a buffer a few bytes short and write past it - [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. - [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. From 683fc312fa7f54bd36de3c77229f47026d92d362 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:56 +0300 Subject: [PATCH 0319/1360] feat(libSceNpCommon): compare, order and validate online IDs and NpIds --- core/libs/prx/libSceNpCommon/Export.cpp | 108 ++++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 20 ---- docs/dev/debt/libSceNpCommon.md | 6 +- 3 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceNpCommon/Export.cpp diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp new file mode 100644 index 0000000000..97a06c816c --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003); +constexpr int NP_ERROR_INVALID_PLATFORM_TYPE = static_cast(0x80550004); +constexpr int NP_UTIL_ERROR_NOT_MATCH = static_cast(0x80550609); +constexpr int NP_LW_COND_ERROR_TIMEDOUT = static_cast(0x8055800B); +constexpr int NP_LW_MUTEX_ERROR_BUSY = static_cast(0x8055800F); +constexpr int KERNEL_ERROR_EBUSY = static_cast(0x80020010); +constexpr int KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +constexpr std::size_t ONLINE_ID_MAX_LENGTH = 16; +constexpr int MUTEX_TYPE_RECURSIVE = 2; +constexpr KernelClockid CLOCK_MONOTONIC_ID = 4; + +struct NpIdFull { + NpOnlineId handle; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; +static_assert(sizeof(NpIdFull) == 36); + +struct SystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; +static_assert(sizeof(SystemSwVersion) == 0x28); + +int compareBytes(const std::uint8_t* lhs, const std::uint8_t* rhs, std::size_t count) { + for (std::size_t i = 0; i < count; ++i) { + if (lhs[i] != rhs[i]) return lhs[i] < rhs[i] ? -1 : 1; + } + return 0; +} + +} + +extern "C" int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +extern "C" int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +extern "C" int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +extern "C" int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +extern "C" int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +extern "C" int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +extern "C" int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +extern "C" int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +extern "C" int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +extern "C" int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); +extern "C" int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SystemSwVersion* version); + +extern "C" { + +int APS5_VABI sceNpCmpOnlineId(const NpOnlineId* onlineId1, const NpOnlineId* onlineId2) { + if (!onlineId1 || !onlineId2) return NP_ERROR_INVALID_ARGUMENT; + if (std::strncmp(onlineId1->data, onlineId2->data, ONLINE_ID_MAX_LENGTH) != 0) return NP_UTIL_ERROR_NOT_MATCH; + return 0; +} + +int APS5_VABI sceNpCmpNpIdInOrder(const NpIdFull* npId1, const NpIdFull* npId2, std::int32_t* order) { + if (!npId1 || !npId2 || !order) return NP_ERROR_INVALID_ARGUMENT; + int compare = std::strncmp(npId1->handle.data, npId2->handle.data, ONLINE_ID_MAX_LENGTH); + if (compare == 0) compare = compareBytes(npId1->opt, npId2->opt, sizeof(npId1->opt)); + if (compare == 0) compare = compareBytes(npId1->reserved, npId2->reserved, sizeof(npId1->reserved)); + *order = compare < 0 ? -1 : compare > 0 ? 1 : 0; + return 0; +} + +int APS5_VABI sceNpGetPlatformType(const NpIdFull* npId) { + constexpr std::uint32_t TAG_PS3 = 0x00337370; + constexpr std::uint32_t TAG_VITA = 0x32707370; + constexpr std::uint32_t TAG_PS4 = 0x00347370; + if (!npId) return NP_ERROR_INVALID_ARGUMENT; + if (npId->opt[4] == 0) return 0; + std::uint32_t tag = 0; + std::memcpy(&tag, npId->opt + 4, sizeof(tag)); + switch (tag) { + case TAG_PS3: return 1; + case TAG_VITA: return 2; + case TAG_PS4: return 3; + default: return NP_ERROR_INVALID_PLATFORM_TYPE; + } +} + +int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId) { + if (!onlineId || onlineId->data[ONLINE_ID_MAX_LENGTH] != 0) return 0; + const std::size_t length = strnlen(onlineId->data, ONLINE_ID_MAX_LENGTH + 1); + if (length < 3 || length > ONLINE_ID_MAX_LENGTH) return 0; + if (!std::isalnum(static_cast(onlineId->data[0]))) return 0; + for (std::size_t i = 1; i < length; ++i) { + const unsigned char character = static_cast(onlineId->data[i]); + if (!std::isalnum(character) && character != '_' && character != '-') return 0; + } + return 1; +} + +} diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index c9d6e53ffd..3269532314 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,16 +115,6 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } -int APS5_VABI sceNpCmpNpIdInOrder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCmpOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondDestroy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -210,11 +200,6 @@ int APS5_VABI sceNpGetNavSdkVersion(void) { return 0; } -int APS5_VABI sceNpGetPlatformType(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetProcessId(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -310,11 +295,6 @@ int APS5_VABI sceNpIntIsOnlineIdString(void) { return 0; } -int APS5_VABI sceNpIntIsValidOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 2a8900021d..521ae4d85e 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. - [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -26,8 +26,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCmpNpIdInOrder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TcwEFnakiSc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCmpOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dj+O5aD2a0Q`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -45,7 +43,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sXVQUIGmk2U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -65,7 +62,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntIsValidOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hkeX9iuCwlI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From 6fdfa33e030c6c994c8d4fdd5a9c57f9670efefb Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:56 +0300 Subject: [PATCH 0320/1360] feat(libSceNpCommon): wrap the libkernel mutex calls --- core/libs/prx/libSceNpCommon/Export.cpp | 47 +++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 50 ------------------- docs/dev/debt/libSceNpCommon.md | 10 ---- 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index 97a06c816c..5eadd6d1a6 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -105,4 +105,51 @@ int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId) { return 1; } +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags) { + PthreadMutexattr attr = nullptr; + int result = scePthreadMutexattrInit(&attr); + if (result != 0) return result; + if (flags & 1) result = scePthreadMutexattrSettype(&attr, MUTEX_TYPE_RECURSIVE); + if (result == 0) result = scePthreadMutexInit(mutex, &attr, name); + scePthreadMutexattrDestroy(&attr); + return result; +} + +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex) { + return scePthreadMutexDestroy(mutex); +} + +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex) { + return scePthreadMutexLock(mutex); +} + +int APS5_VABI sceNpMutexTryLock(PthreadMutex* mutex) { + const int result = scePthreadMutexTrylock(mutex); + return result == KERNEL_ERROR_EBUSY ? NP_LW_MUTEX_ERROR_BUSY : result; +} + +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex) { + return scePthreadMutexUnlock(mutex); +} + +int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags) { + return sceNpMutexInit(mutex, name, flags); +} + +int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex) { + return sceNpMutexDestroy(mutex); +} + +int APS5_VABI sceNpLwMutexLock(PthreadMutex* mutex) { + return sceNpMutexLock(mutex); +} + +int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex) { + return sceNpMutexTryLock(mutex); +} + +int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex) { + return sceNpMutexUnlock(mutex); +} + } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 3269532314..3956386947 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -380,31 +380,6 @@ int APS5_VABI sceNpLwCondWait(void) { return 0; } -int APS5_VABI sceNpLwMutexDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexTryLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexUnlock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpMemoryHeapDestroy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -425,31 +400,6 @@ int APS5_VABI sceNpMemoryHeapInit(void) { return 0; } -int APS5_VABI sceNpMutexDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexTryLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexUnlock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpOpenEventFlag(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 521ae4d85e..7905ee9572 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -79,20 +79,10 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4zxevggtYrQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1CiXI-MyEKs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `18j+qk6dRwk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hp0kVgu5Fxw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CQG2oyx1-nM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lQ11BpMM4LU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uEwag-0YZPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `r9Bet+s6fKc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DuslmoqQ+nk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `oZyb9ktuCpA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From affc646ae7025c4488e12af2eb660bf46b41549d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:57 +0300 Subject: [PATCH 0321/1360] feat(libSceNpCommon): wrap the condition, join, clock and version calls --- core/libs/prx/libSceNpCommon/Export.cpp | 42 +++++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 35 ---------------- docs/dev/debt/libSceNpCommon.md | 7 ---- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index 5eadd6d1a6..f10b45437e 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -152,4 +152,46 @@ int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex) { return sceNpMutexUnlock(mutex); } +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t) { + PthreadCondattr attr = nullptr; + int result = scePthreadCondattrInit(&attr); + if (result != 0) return result; + result = scePthreadCondInit(cond, &attr, name); + scePthreadCondattrDestroy(&attr); + return result; +} + +int APS5_VABI sceNpCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); +} + +int APS5_VABI sceNpCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); +} + +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec) { + if (usec == 0) return scePthreadCondWait(cond, mutex); + const int result = scePthreadCondTimedwait(cond, mutex, usec); + return result == KERNEL_ERROR_ETIMEDOUT ? NP_LW_COND_ERROR_TIMEDOUT : result; +} + +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval) { + return scePthreadJoin(thread, retval); +} + +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec) { + KernelTimespec now{}; + const int result = sceKernelClockGettime(CLOCK_MONOTONIC_ID, &now); + if (result == 0) *usec = now.tv_sec * 1000000 + now.tv_nsec / 1000; + return result; +} + +void APS5_VABI sceNpGetSdkVersion(char* version) { + SystemSwVersion swVersion{}; + swVersion.size = sizeof(swVersion); + sceKernelGetSystemSwVersion_nid_postfix(&swVersion); + const std::uint32_t hex = swVersion.version; + std::snprintf(version, 8, "%u.%02u", ((hex >> 24) & 0xF) + (hex >> 28) * 10, ((hex >> 16) & 0xF) + ((hex >> 20) & 0xF) * 10); +} + } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 3956386947..4326061d26 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,21 +115,6 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } -int APS5_VABI sceNpCondDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondSignal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondSignalAll(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -140,11 +125,6 @@ int APS5_VABI sceNpCondSignalTo(void) { return 0; } -int APS5_VABI sceNpCondTimedwait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondWait(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -210,21 +190,11 @@ int APS5_VABI sceNpGetRandom(void) { return 0; } -int APS5_VABI sceNpGetSdkVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetSdkVersionUInt(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpGetSystemClockUsec(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGlobalHeapGetAllocator(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -315,11 +285,6 @@ int APS5_VABI sceNpIpc2ClientTerm(void) { return 0; } -int APS5_VABI sceNpJoinThread(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpJsonParse(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 7905ee9572..0429e246b8 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -26,12 +26,8 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondTimedwait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ss2xO9IJxKQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -45,9 +41,7 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSystemClockUsec](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PVVsRmMkO1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -66,7 +60,6 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpJoinThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EjMsfO3GCIA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From c6fa81f8611db272fa9110bc5ee7cb473dc276a0 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:57 +0300 Subject: [PATCH 0322/1360] test(libSceNpCommon): cover the ID utilities, sync wrappers and clock --- core/libs/CMakeLists.txt | 13 +++ core/libs/tests/GuestNpCommon.cpp | 172 ++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 core/libs/tests/GuestNpCommon.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2854a5fa08..a1e0d5218d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -380,6 +380,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_common_tests tests/GuestNpCommon.cpp) + target_include_directories(guest_np_common_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_common_tests PRIVATE libSceNpCommon libkernel libc) + configure_windows_unwind(guest_np_common_tests) + foreach(testCase IN ITEMS ids sync clock) + add_test(NAME guest_np_common_${testCase} COMMAND guest_np_common_tests ${testCase}) + set_tests_properties(guest_np_common_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_common_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) diff --git a/core/libs/tests/GuestNpCommon.cpp b/core/libs/tests/GuestNpCommon.cpp new file mode 100644 index 0000000000..d605767148 --- /dev/null +++ b/core/libs/tests/GuestNpCommon.cpp @@ -0,0 +1,172 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include + +struct NpIdFull { + NpOnlineId handle; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; + +extern "C" { +int APS5_VABI sceNpCmpOnlineId(const NpOnlineId* onlineId1, const NpOnlineId* onlineId2); +int APS5_VABI sceNpCmpNpIdInOrder(const NpIdFull* npId1, const NpIdFull* npId2, std::int32_t* order); +int APS5_VABI sceNpGetPlatformType(const NpIdFull* npId); +int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId); +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexTryLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t flags); +int APS5_VABI sceNpCondDestroy(PthreadCond* cond); +int APS5_VABI sceNpCondSignal(PthreadCond* cond); +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec); +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); +void APS5_VABI sceNpGetSdkVersion(char* version); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +} + +static constexpr int INVALID_ARGUMENT = static_cast(0x80550003); +static constexpr int INVALID_PLATFORM_TYPE = static_cast(0x80550004); +static constexpr int NOT_MATCH = static_cast(0x80550609); +static constexpr int COND_TIMEDOUT = static_cast(0x8055800B); +static constexpr int MUTEX_BUSY = static_cast(0x8055800F); + +static bool expect(bool condition, const char* message) { + if (!condition) std::fprintf(stderr, "%s\n", message); + return condition; +} + +static NpOnlineId onlineId(const char* text) { + NpOnlineId id{}; + std::strncpy(id.data, text, sizeof(id.data)); + return id; +} + +static NpIdFull npIdWithTag(const char* handle, std::uint32_t tag) { + NpIdFull id{}; + id.handle = onlineId(handle); + std::memcpy(id.opt + 4, &tag, sizeof(tag)); + return id; +} + +static int testIds() { + const NpOnlineId kestrel = onlineId("kestrel_77"); + const NpOnlineId kestrelCopy = onlineId("kestrel_77"); + const NpOnlineId heron = onlineId("heron-4"); + bool ok = expect(sceNpCmpOnlineId(nullptr, &kestrel) == INVALID_ARGUMENT, "a null online ID was compared"); + ok &= expect(sceNpCmpOnlineId(&kestrel, &kestrelCopy) == 0, "equal online IDs did not match"); + ok &= expect(sceNpCmpOnlineId(&kestrel, &heron) == NOT_MATCH, "different online IDs matched"); + + NpIdFull low = npIdWithTag("kestrel_77", 0x00347370); + NpIdFull high = low; + high.reserved[7] = 1; + std::int32_t order = 5; + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, nullptr) == INVALID_ARGUMENT, "a null order was accepted"); + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, &order) == 0 && order == -1, "reserved bytes did not order the NpIds"); + ok &= expect(sceNpCmpNpIdInOrder(&high, &low, &order) == 0 && order == 1, "the reverse order is not 1"); + ok &= expect(sceNpCmpNpIdInOrder(&low, &low, &order) == 0 && order == 0, "an NpId is not equal to itself"); + high = low; + high.opt[0] = 9; + high.reserved[0] = 0; + low.reserved[0] = 200; + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, &order) == 0 && order == -1, "opt did not take precedence over reserved"); + const NpIdFull heronId = npIdWithTag("heron-4", 0); + ok &= expect(sceNpCmpNpIdInOrder(&heronId, &low, &order) == 0 && order == -1, "the handle did not take precedence"); + + const NpIdFull none = npIdWithTag("heron-4", 0); + const NpIdFull ps3 = npIdWithTag("heron-4", 0x00337370); + const NpIdFull vita = npIdWithTag("heron-4", 0x32707370); + const NpIdFull ps4 = npIdWithTag("heron-4", 0x00347370); + const NpIdFull other = npIdWithTag("heron-4", 0x00357370); + ok &= expect(sceNpGetPlatformType(nullptr) == INVALID_ARGUMENT, "a null NpId has a platform"); + ok &= expect(sceNpGetPlatformType(&none) == 0, "an untagged NpId is not platform 0"); + ok &= expect(sceNpGetPlatformType(&ps3) == 1, "the ps3 tag is not platform 1"); + ok &= expect(sceNpGetPlatformType(&vita) == 2, "the psp2 tag is not platform 2"); + ok &= expect(sceNpGetPlatformType(&ps4) == 3, "the ps4 tag is not platform 3"); + ok &= expect(sceNpGetPlatformType(&other) == INVALID_PLATFORM_TYPE, "an unknown tag was accepted"); + + NpOnlineId unterminated = onlineId("abcdefghijklmnop"); + unterminated.data[16] = 'q'; + ok &= expect(sceNpIntIsValidOnlineId(nullptr) == 0, "a null online ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&kestrel) == 1, "kestrel_77 is not valid"); + ok &= expect(sceNpIntIsValidOnlineId(&heron) == 1, "heron-4 is not valid"); + const NpOnlineId sixteen = onlineId("abcdefghijklmnop"); + const NpOnlineId shortId = onlineId("ab"); + const NpOnlineId leadingDash = onlineId("-heron"); + const NpOnlineId dotted = onlineId("her.on"); + ok &= expect(sceNpIntIsValidOnlineId(&sixteen) == 1, "a 16 character ID is not valid"); + ok &= expect(sceNpIntIsValidOnlineId(&unterminated) == 0, "an unterminated ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&shortId) == 0, "a 2 character ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&leadingDash) == 0, "an ID starting with '-' is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&dotted) == 0, "an ID with '.' is valid"); + return ok ? 0 : 1; +} + +static void* APS5_VABI returnTicket(void* arg) { + return reinterpret_cast(reinterpret_cast(arg) + 1); +} + +static int testSync() { + PthreadMutex plain = nullptr; + PthreadMutex recursive = nullptr; + bool ok = expect(sceNpMutexInit(&plain, "np-plain", 0) == 0 && plain, "a plain mutex was not created"); + ok &= expect(sceNpLwMutexInit(&recursive, "np-recursive", 1) == 0 && recursive, "a recursive mutex was not created"); + if (!ok) return 1; + ok &= expect(sceNpMutexLock(&plain) == 0, "the plain mutex did not lock"); + ok &= expect(sceNpMutexTryLock(&plain) == MUTEX_BUSY, "a held plain mutex did not report BUSY"); + ok &= expect(sceNpMutexUnlock(&plain) == 0, "the plain mutex did not unlock"); + ok &= expect(sceNpLwMutexTryLock(&recursive) == 0 && sceNpLwMutexTryLock(&recursive) == 0, "flag 1 did not make the mutex recursive"); + ok &= expect(sceNpLwMutexUnlock(&recursive) == 0 && sceNpLwMutexUnlock(&recursive) == 0, "the recursive mutex did not unlock twice"); + ok &= expect(sceNpLwMutexDestroy(&recursive) == 0, "the recursive mutex was not destroyed"); + + PthreadCond cond = nullptr; + ok &= expect(sceNpCondInit(&cond, "np-cond", 0) == 0 && cond, "a condition was not created"); + ok &= expect(sceNpMutexLock(&plain) == 0, "the plain mutex did not lock for the wait"); + ok &= expect(sceNpCondTimedwait(&cond, &plain, 2000) == COND_TIMEDOUT, "a timed wait did not report TIMEDOUT"); + ok &= expect(sceNpMutexUnlock(&plain) == 0, "the mutex was not held after the wait"); + ok &= expect(sceNpCondSignal(&cond) == 0, "signalling without waiters failed"); + ok &= expect(sceNpCondDestroy(&cond) == 0, "the condition was not destroyed"); + ok &= expect(sceNpMutexDestroy(&plain) == 0, "the plain mutex was not destroyed"); + + Pthread thread = nullptr; + void* retval = nullptr; + ok &= expect(scePthreadCreate(&thread, nullptr, returnTicket, reinterpret_cast(0x4140), "np-join") == 0, "the thread was not created"); + ok &= expect(sceNpJoinThread(thread, &retval) == 0 && retval == reinterpret_cast(0x4141), "the join did not return the thread result"); + return ok ? 0 : 1; +} + +static int testClock() { + KernelTimespec before{}; + KernelTimespec after{}; + std::int64_t usec = -1; + sceKernelClockGettime(4, &before); + bool ok = expect(sceNpGetSystemClockUsec(&usec) == 0, "the clock was not read"); + sceKernelClockGettime(4, &after); + ok &= expect(usec >= before.tv_sec * 1000000 + before.tv_nsec / 1000 && usec <= after.tv_sec * 1000000 + after.tv_nsec / 1000, "the clock is not the monotonic kernel clock in microseconds"); + char version[12]; + std::memset(version, 'x', sizeof(version)); + sceNpGetSdkVersion(version); + ok &= expect(std::strcmp(version, "1.00") == 0, "the version is not 1.00"); + ok &= expect(version[8] == 'x', "the version was written past 8 bytes"); + return ok ? 0 : 1; +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + int status = 2; + if (std::strcmp(argv[1], "ids") == 0) status = testIds(); + else if (std::strcmp(argv[1], "sync") == 0) status = testSync(); + else if (std::strcmp(argv[1], "clock") == 0) status = testClock(); + if (status == 0) std::printf("NpCommon %s tests passed\n", argv[1]); + return status; +} From e0df782d2c993ec07bd11ea0b425ddffee203d10 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:19:04 +0300 Subject: [PATCH 0323/1360] docs(debt): note where the NpCommon utilities and wrappers come from --- docs/dev/debt/libSceNpCommon.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 0429e246b8..e6e51903bb 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. - [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -133,3 +133,9 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. +- [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. +- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. From baa0ca3dfc200ff88f00d2034c4b262557af6e8d Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Wed, 21 Oct 2026 12:46:19 +0200 Subject: [PATCH 0324/1360] fix(agc): store a small label behind copied write-backs at reap instead of draining The first commit drained the device for every label recorded while any copied writer was listed, including labels nowhere near it. CPU-copied writers also occur in default runs, so that could cost a drain per label in normal play. While a copied writer is listed, a label of 8 bytes or less (every RELEASE_MEM, the fence WRITE_DATAs) is now not recorded on the GPU at all. A completion action of the open (or newest in-flight) batch stores it when that batch is reaped (AfterCompletions with storedOnGpu false, reason 5). Completions run in push order and batches finish front-first, so it lands after every earlier write-back without a drain. The existing GPU store plus completion re-store path would not keep this order: its GPU store is visible to a polling title before the write-back runs, and its re-store runs only over a write-back that overlapped the label itself. Recorder::PendingCompletionStores counts the guest stores completions still have to make (not end-of-pipe deliveries or write-backs). While it is not zero, later small labels follow the same route even after every writer was written back, so a later GPU label cannot become visible before an earlier completion store or land under it at one address. Stores over 8 bytes keep the GPU store and go behind the completions only over a copied writer's range or a pending completion store of their own range. Queue 0 reaps while it spins on REWIND, so a title polling such a label there cannot wait forever. The debt entry lists what this still leaves out of order (larger stores and GPU-made DMA_DATA copies away from those ranges, and a nested sync from inside a write-back). APS5_DRAIN_COMPLETION_LABELS=1 brings back the drain (reason 2); a second ctest entry runs the test with it. CopiedWriterLabelOrder now skips with a line naming minStorageBufferOffsetAlignment when the device binds the record at +0x1004 in place (alignment 4, AMD's driver and RADV) and nothing is written back. Above 4 the write-back count still fails the test. A new pass checks the order across batches: a later label to the address of a pending completion store is not visible before it and is not overwritten by it at the reap. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Execution/include/VulkanDevice.hpp | 8 +- .../src/Driver/Queues/Submission.cpp | 1 + .../Execution/src/VulkanDevice.cpp | 36 +++++- .../Graphics/include/Recorder.hpp | 2 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 12 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 10 +- .../execution/CopiedWriterLabelOrder.cpp | 117 ++++++++++++++++-- docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 10 files changed, 175 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index adcd1d85a0..521390800f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2065,6 +2065,11 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_copied_writer_label_order_drain COMMAND agc_driver_copied_writer_label_order_tests) + set_tests_properties(agc_driver_copied_writer_label_order_drain PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAIN_COMPLETION_LABELS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_copied_writer_label_order_drain PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 1b5e098a87..3779854cbb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -92,10 +92,10 @@ public: // Records a store of `bytes` at a guest address behind the recorded work, so the value appears // once that work completed; the batch is submitted by the queue worker's flush rules (or at once // with APS5_LABEL_SUBMIT_NOW=1). `stamp` is the record-order stamp and `queue` the recording - // queue for the recorder's pending-label table. Returns 0 when recorded on the GPU, 5 when kept - // as a completion action behind pending write-backs, else why the CPU must write it: - // 1 nothing recorded (the write is already ordered), 2 write-backs pending and 3 memory not - // imported, 4 unsuitable size or alignment. + // queue for the recorder's pending-label table. Returns 0 when recorded on the GPU, 5 when stored + // by a completion behind pending CPU write-backs or completion stores, else why the CPU must + // write it: 1 nothing recorded (the write is already ordered), 2 write-backs pending (only with + // APS5_DRAIN_COMPLETION_LABELS=1) and 3 memory not imported, 4 unsuitable size or alignment. // `reapFirst` retires finished batches before the checks; a caller recording a group of labels // under one lock passes it for the first label only. int WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst = true); 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 13d45c0f36..5249610775 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -282,6 +282,7 @@ void Driver::executeRewindTail(const Submission& stalled) { checkStopping(); noteAwaitingTitle(stalled.queue, reinterpret_cast(stalled.rewindTail - 1)); stall.Check("REWIND at", reinterpret_cast(stalled.rewindTail - 1)); + if (stalled.queue == 0 && completionsPending()) reapCompletionLabels(); PollSleep(); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 76a12cd929..78380622d4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2264,12 +2264,38 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spanbuffer, Graphics::HostImportOffset(*import, address), bytes, address); }; - // Only a write-back that can land over these bytes (a listed copied writer of the range, as - // FillBuffer tests) puts the label behind the completions; any other pending completion - // stores elsewhere. Debug aid: APS5_LABEL_GATE_ALL=1 gates on any completion, as before (a - // completion label there registered its own completion, so every later label followed). + // On the console every store a queue made before a label is in memory once the label is + // visible. A listed copied writer (a dispatch or draw whose written buffer the device holds as a + // CPU copy) is written back by the CPU when its batch is reaped, after the GPU already ran any + // store recorded here, so a title polling the label could rewrite the written region and have + // the late write-back land over its bytes (a title that rebuilds its command buffer there sees + // a stale packet). While one is listed, a label of 8 bytes or less (every RELEASE_MEM, the fence + // WRITE_DATAs) is therefore not recorded on the GPU at all: a completion action of the open (or + // newest in-flight) batch stores it when that batch is reaped. Completions run in push order and + // batches finish front-first, so it lands after every earlier write-back, whatever range that + // one wrote. The GPU store plus completion re-store below would not do: its GPU store is + // visible before the write-back, and its re-store runs only over a write-back that overlapped + // the label. While such a completion store is pending (PendingCompletionStores: no end-of-pipe + // delivery and no write-back counts there), later small labels follow it even after every + // writer was written back, or a GPU store would become visible before the earlier completion + // store lands (#1458), or land under it at the reap when both are at one address. Larger stores + // (DUMP_CONST_RAM, COPY_DATA and DMA_DATA payloads that later GPU work reads through the import) + // stay on the GPU and follow only a pending completion store over their own range. Readers of a + // range stored this way sync through its batch first (the label is a pending write of its batch + // and counted in PendingCompletionLabels). Debug aid: APS5_DRAIN_COMPLETION_LABELS=1 drains + // (reason 2) instead and stores the label on the CPU. + const bool listed = !state->copiedWriters->empty() || !Graphics::DrawCopiedWriters()->empty(); + if (drain) { + if (listed) return 2; + } else if (const bool storesPending = Graphics::Recorder::PendingCompletionStores() != 0; bytes.size() <= 8 ? (listed || storesPending) : (storesPending && recorder.CompletionLabelIn(address, bytes.size()))) { + recorder.AfterCompletions(address, bytes, stamp, queue, false); + return 5; + } + // A larger store over a listed copied writer's range (as FillBuffer tests it) goes behind the + // completions too, below; any other one stores on the GPU. Debug aid: APS5_LABEL_GATE_ALL=1 + // gates on any completion, as before (a completion label there registered its own completion, + // so every later label followed). static const bool gateAll = std::getenv("APS5_LABEL_GATE_ALL") != nullptr; - if (!state->copiedWriters->empty() || !Graphics::DrawCopiedWriters()->empty()) return 2; if (gateAll ? recorder.HasCompletions() : state->CopiedWriterOverlaps(address, bytes.size())) { // A copied buffer's write-back is still to run: the store must land after it. It is stored // by a completion action of the batch (a CPU memcpy when the batch is reaped) instead of diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f4d473d7a5..41c6e67032 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -399,6 +399,7 @@ public: static std::uint64_t PublishGeneration(); // labels waiting in completion actions (the CPU must reap their batch for them to land); static std::uint64_t PendingCompletionLabels(); + static std::uint64_t PendingCompletionStores(); // write-back completions (OnComplete) of unfinished batches: a reaper retires them when every // worker sleeps, as for the labels above; static std::uint64_t PendingWriteBackCompletions(); @@ -596,6 +597,7 @@ private: // Labels stored by this batch's completion actions (see AfterCompletions); subtracted from // the lock-free pending count when the batch finishes, whether or not its completions ran. std::uint32_t completionLabelCount = 0; + std::uint32_t completionStoreCount = 0; // Write-back completions (OnComplete), likewise. std::uint32_t writeBackCompletionCount = 0; // The ranges of the completion labels also stored on the GPU, and whether each entered the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 6655a45e82..83c8381e36 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -513,6 +513,7 @@ std::atomic pendingLabelSince{NoPendingLabel}; std::atomic writeGeneration{0}; std::atomic publishGeneration{0}; std::atomic completionLabels{0}; +std::atomic completionStores{0}; std::atomic writeBackCompletions{0}; std::atomic completionStoresSkipped{0}; std::atomic completionStoresRun{0}; @@ -1135,6 +1136,7 @@ Recorder::~Recorder() { publishGeneration.fetch_add(1, std::memory_order_release); pendingLabelSince.store(NoPendingLabel, std::memory_order_release); completionLabels.store(0, std::memory_order_release); + completionStores.store(0, std::memory_order_release); writeBackCompletions.store(0, std::memory_order_release); workSinceSubmit.store(0, std::memory_order_release); } @@ -1210,6 +1212,10 @@ std::uint64_t Recorder::PendingCompletionLabels() { return completionLabels.load(std::memory_order_acquire); } +std::uint64_t Recorder::PendingCompletionStores() { + return completionStores.load(std::memory_order_acquire); +} + std::uint64_t Recorder::PendingWriteBackCompletions() { return writeBackCompletions.load(std::memory_order_acquire); } @@ -3335,6 +3341,8 @@ void Recorder::AfterCompletions(std::uint64_t address, std::spanmarkBehindCompletion(batch, label.begin, label.end); } }; @@ -3861,6 +3871,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool list.erase(std::remove(list.begin(), list.end(), batch), list.end()); // The batch's completion labels have landed, or never will (fence failure below). if (batch->completionLabelCount != 0) completionLabels.fetch_sub(batch->completionLabelCount, std::memory_order_acq_rel); + if (batch->completionStoreCount != 0) completionStores.fetch_sub(batch->completionStoreCount, std::memory_order_acq_rel); if (batch->writeBackCompletionCount != 0) writeBackCompletions.fetch_sub(batch->writeBackCompletionCount, std::memory_order_acq_rel); // This also runs while the fence-failure throw unwinds: an allocation failure in the // rebuild keeps the previous snapshot, which is a superset (a stale entry only causes a @@ -3994,6 +4005,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool void Recorder::discard(std::unique_ptr batch) noexcept { removeLabels(*batch); if (batch->completionLabelCount != 0) completionLabels.fetch_sub(batch->completionLabelCount, std::memory_order_acq_rel); + if (batch->completionStoreCount != 0) completionStores.fetch_sub(batch->completionStoreCount, std::memory_order_acq_rel); if (batch->writeBackCompletionCount != 0) writeBackCompletions.fetch_sub(batch->writeBackCompletionCount, std::memory_order_acq_rel); if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); release(*batch); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a5322a1921..168e4d958d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1109,8 +1109,10 @@ void completionCountTests(const Device& device, Recorder& recorder) { recorder.AfterCompletions(base, value, 3, 0, true); recorder.AfterCompletions(base + 8, value, 4, 0, true); Require(Recorder::PendingCompletionLabels() == registered, "a GPU-stored completion label counts before any write-back"); + Require(Recorder::PendingCompletionStores() == registered, "a GPU-stored completion label counts as a completion store before any write-back"); recorder.AfterCompletions(base + 0x40, value, 5, 0, false); Require(Recorder::PendingCompletionLabels() == registered + 1, "a label the GPU has no view of is not pending"); + Require(Recorder::PendingCompletionStores() == registered + 1, "a label the GPU has no view of is not a pending completion store"); // A late lookup (stamp <= afterStamp) of a label whose value reaches memory only through the // batch's completion action is refused as BehindCompletion (a poller reaps for it); a GPU-stored // one keeps the ordinary reason (unclosed group, or trust off) until a write-back overlaps it. @@ -1126,16 +1128,20 @@ void completionCountTests(const Device& device, Recorder& recorder) { Recorder::NoteWrittenBack(base + 2, 4); const std::uint64_t counted = countAll ? 3 : 2; Require(Recorder::PendingCompletionLabels() == counted, "a write-back over a GPU-stored label did not count it exactly once"); + Require(Recorder::PendingCompletionStores() == counted, "a write-back over a GPU-stored label did not make it a completion store exactly once"); + int delivered = 0; + Require(recorder.AfterRecordedWork([&] { ++delivered; }), "an action after recorded work was refused"); + Require(Recorder::PendingCompletionLabels() == counted + 1 && Recorder::PendingCompletionStores() == counted, "an action after recorded work counted as a completion store"); Require(!Recorder::LookupLabel(base, 4, 3, &refusal).has_value() && refusal == Refusal::BehindCompletion, "a written-back GPU-stored label is not refused as behind a completion"); Require(!Recorder::LookupLabel(base + 8, 4, 4, &refusal).has_value() && refusal == (countAll ? Refusal::BehindCompletion : ordinary), "an untouched GPU-stored label became behind a completion"); recorder.Submit(); device.WaitQueue(); - Require(Recorder::PendingCompletionLabels() == counted && Recorder::PendingWriteBackCompletions() == 1, "counts dropped before the batch finished"); + Require(Recorder::PendingCompletionLabels() == counted + 1 && Recorder::PendingCompletionStores() == counted && Recorder::PendingWriteBackCompletions() == 1, "counts dropped before the batch finished"); memory[0] = 7; memory[2] = 7; memory[16] = 7; recorder.Sync(); - Require(ran == 1 && Recorder::PendingCompletionLabels() == 0 && Recorder::PendingWriteBackCompletions() == 0 && recorder.Idle(), "counts did not return to 0 at finish"); + Require(ran == 1 && delivered == 1 && Recorder::PendingCompletionLabels() == 0 && Recorder::PendingCompletionStores() == 0 && Recorder::PendingWriteBackCompletions() == 0 && recorder.Idle(), "counts did not return to 0 at finish"); const bool storeAlways = std::getenv("APS5_LABEL_STORE_ALWAYS") != nullptr; Require(memory[0] == 1 && memory[2] == (storeAlways ? 1u : 7u) && memory[16] == 1, "completion stores ran for the wrong labels (overlapped and not-imported ones store, the untouched GPU-stored one skips)"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp index 3f7a722c88..f143769c82 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp @@ -100,31 +100,73 @@ void RecordDispatch(AgcDriver::VulkanDevice& device, std::uint64_t record) { device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); } -bool LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std::size_t recordOffset, const GuestBlock& labels, std::size_t labelOffset, std::uint64_t stamp, std::uint64_t writeBacks, int expectedReason, const std::string& what) { +bool Drains() { + return std::getenv("APS5_DRAIN_COMPLETION_LABELS") != nullptr; +} + +VkDeviceSize StorageAlignment(AgcDriver::VulkanDevice& device) { + return device.graphicsContext().limits.minStorageBufferOffsetAlignment; +} + +std::array LabelBytes(std::uint32_t value) { + std::array bytes{}; + std::memcpy(bytes.data(), &value, sizeof(value)); + return bytes; +} + +void WaitForGpu(AgcDriver::VulkanDevice& device, std::uint64_t epoch) { + if (device.CanWaitUnlocked()) device.WaitRecorded(epoch); + else std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +std::uint64_t SubmitLocked(AgcDriver::VulkanDevice& device) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + return device.SubmitAndEpoch(); +} + +int LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std::size_t recordOffset, const GuestBlock& labels, std::size_t labelOffset, std::uint64_t stamp, std::uint64_t writeBacks, int expectedReason, const std::string& what) { volatile std::uint32_t* stored = records.Word(recordOffset + StoredOffset); volatile std::uint32_t* label = labels.Word(labelOffset); *stored = CpuValue; *label = 0; RecordDispatch(device, records.Address() + recordOffset); const auto pending = Recorder::PendingWriteBackCompletions(); - Require(pending == writeBacks, what + ": " + std::to_string(pending) + " write-back completions are pending after the dispatch, expected " + std::to_string(writeBacks) + " (the test needs APS5_CPU_COPIES=1 and a storage buffer offset alignment above 4)"); - const std::array bytes{std::byte{LabelValue}, std::byte{0}, std::byte{0}, std::byte{0}}; + if (writeBacks != 0 && pending == 0 && recordOffset % StorageAlignment(device) == 0) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + return VulkanTestSkipped; + } + Require(pending == writeBacks, what + ": " + std::to_string(pending) + " write-back completions are pending after the dispatch, expected " + std::to_string(writeBacks)); + const auto bytes = LabelBytes(LabelValue); int reason = 0; + std::uint64_t epoch = 0; { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); reason = device.WriteLabelOnGpu(labels.Address() + labelOffset, bytes, stamp, 0, false); if (reason == 3) { device.WaitIdle(); - return false; + return 1; } Require(reason == expectedReason, what + ": the label after the dispatch was handled with reason " + std::to_string(reason) + ", expected " + std::to_string(expectedReason)); if (reason >= 1 && reason <= 4) { if (reason != 1) device.WaitIdle(); AgcDriver::GuestMemory::Write(labels.Address() + labelOffset, bytes, 4); + } else if (reason == 5) { + Require(Recorder::PendingWriteBackCompletions() == 1 && Recorder::PendingCompletionStores() == 1, what + ": the label behind the write-back drained the device or is not a pending completion store"); + const std::array wide{}; + Require(device.WriteLabelOnGpu(labels.Address() + labelOffset + 0x100, wide, stamp, 0, false) == 0, what + ": a 16-byte store away from the copied writer and the pending completion store was not recorded on the GPU"); + epoch = device.SubmitAndEpoch(); } else { device.SubmitRecorded(true); } } + if (reason == 5) { + WaitForGpu(device, epoch); + Require(*label == 0, what + ": the label is visible before the batch that writes back the dispatch's record was reaped"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.ReapRecorded(epoch); + Require(Recorder::PendingCompletionStores() == 0 && Recorder::PendingWriteBackCompletions() == 0, what + ": completions outlived the reap of their batch"); + } const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (*label != LabelValue) { Require(std::chrono::steady_clock::now() < deadline, what + ": the label never landed"); @@ -133,11 +175,63 @@ bool LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std:: const std::uint32_t seen = *stored; Require(seen == GpuValue, what + ": the label is visible but the record holds " + Hex(seen) + ", the dispatch stored " + Hex(GpuValue)); *stored = CpuValue; - device.WaitIdle(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + } const std::uint32_t kept = *stored; Require(kept == CpuValue, what + ": the record holds " + Hex(kept) + " after the drain, a write-back landed over the value the CPU wrote after the label (" + Hex(CpuValue) + ")"); Require(Recorder::PendingWriteBackCompletions() == 0, what + ": write-back completions outlived the drain"); - return true; + return 0; +} + +void StreamOrderPass(AgcDriver::VulkanDevice& device, const GuestBlock& copied, const GuestBlock& direct, const GuestBlock& labels, std::uint64_t stamp) { + constexpr std::uint32_t First = 0x11u; + constexpr std::uint32_t Second = 0x22u; + const std::uint64_t address = labels.Address() + 0x4000; + volatile std::uint32_t* label = labels.Word(0x4000); + *label = 0; + *copied.Word(CopiedRecordOffset + StoredOffset) = CpuValue; + RecordDispatch(device, copied.Address() + CopiedRecordOffset); + Require(Recorder::PendingWriteBackCompletions() == 1, "stream order: the copied dispatch has no pending write-back"); + const auto copiedEpoch = SubmitLocked(device); + RecordDispatch(device, direct.Address() + DirectRecordOffset); + std::uint64_t firstEpoch = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(Recorder::PendingWriteBackCompletions() == 1, "stream order: the copied writer was written back before the first label"); + const int reason = device.WriteLabelOnGpu(address, LabelBytes(First), stamp, 0, false); + Require(reason == 5, "stream order: the first label behind the copied writer was handled with reason " + std::to_string(reason) + ", expected 5"); + firstEpoch = device.SubmitAndEpoch(); + } + WaitForGpu(device, firstEpoch); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.ReapRecorded(copiedEpoch); + Require(Recorder::PendingWriteBackCompletions() == 0, "stream order: the copied writer was not written back by the reap of its batch"); + Require(Recorder::PendingCompletionStores() == 1, "stream order: the first label's completion store is not pending after the copied writer's reap"); + Require(*label == 0, "stream order: the first label landed before its batch was reaped"); + } + RecordDispatch(device, direct.Address() + DirectRecordOffset); + std::uint64_t secondEpoch = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const int reason = device.WriteLabelOnGpu(address, LabelBytes(Second), stamp + 1, 0, false); + Require(reason == 5, "stream order: a later label to the address of a pending completion store was handled with reason " + std::to_string(reason) + ", expected 5"); + secondEpoch = device.SubmitAndEpoch(); + Require(secondEpoch > firstEpoch, "stream order: the second label went into the first label's batch"); + } + WaitForGpu(device, secondEpoch); + const std::uint32_t early = *label; + Require(early != Second, "stream order: the later label is visible while the earlier completion store to its address has not landed"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.ReapRecorded(firstEpoch); + Require(*label == First, "stream order: the label holds " + Hex(*label) + " after the first label's batch was reaped, expected " + Hex(First)); + device.ReapRecorded(secondEpoch); + const std::uint32_t last = *label; + Require(last == Second, "stream order: the label holds " + Hex(last) + " after the reap, the later label stored " + Hex(Second)); + Require(Recorder::PendingCompletionStores() == 0 && Recorder::PendingCompletionLabels() == 0, "stream order: completion stores outlived the reap of their batches"); + device.WaitIdle(); } } @@ -146,18 +240,27 @@ int main() { try { #ifdef _WIN32 _putenv_s("APS5_CPU_COPIES", "1"); + _putenv_s("APS5_NO_OPPORTUNISTIC_REAP", "1"); #else setenv("APS5_CPU_COPIES", "1", 1); + setenv("APS5_NO_OPPORTUNISTIC_REAP", "1", 1); #endif const GuestBlock copied; const GuestBlock direct; const GuestBlock labels; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!LabelPass(*device, copied, CopiedRecordOffset, labels, 0x2000, 1, 1, 2, "copied writer") || !LabelPass(*device, direct, DirectRecordOffset, labels, 0x3000, 2, 0, 0, "direct writer")) { + const int copiedPass = LabelPass(*device, copied, CopiedRecordOffset, labels, 0x2000, 1, 1, Drains() ? 2 : 5, "copied writer"); + if (copiedPass == VulkanTestSkipped) { + std::printf("skipped, the device reports a minStorageBufferOffsetAlignment of %llu, so the dispatch binds its record at +0x%zx in place and nothing is written back; the test needs an alignment above 4\n", + static_cast(StorageAlignment(*device)), CopiedRecordOffset); + return VulkanTestSkipped; + } + if (copiedPass != 0 || LabelPass(*device, direct, DirectRecordOffset, labels, 0x3000, 2, 0, 0, "direct writer") != 0) { std::puts("skipped, the device imports no host memory"); return VulkanTestSkipped; } + if (!Drains()) StreamOrderPass(*device, copied, direct, labels, 3); std::puts("copied writer label order tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a309403888..8e8fc62713 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -111,7 +111,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DISPATCH_VARIANTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 4 | number of dispatch variants kept per cache entry, clamped to the maximum | | `APS5_DISPMOVE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1024 | upper bound in KiB on the read-only buffers a dispatch-cache hit moves into its template, above which the dispatch is prepared again | | `APS5_DRAIN_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | drains every packet, disabling the batched recording path | -| `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch | +| `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch, and drains a label recorded while a copied write-back is pending instead of storing it behind the write-backs | | `APS5_DRAW_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 4096 | number of entries in the draw cache; 0 keeps the default 4096 | | `APS5_DRAW_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | treats draw packets as drains, waiting for them instead of recording ahead | | `APS5_DRAW_INPUTS_IN_PLACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | binds draw vertex and index ranges in place from host imports also with NVIDIA's Windows driver, which copies them by default | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 79655753f4..610937c25a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -113,7 +113,7 @@ - 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. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. -- A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) recorded while a copied writer is listed (a dispatch or draw whose written buffer the device holds as a CPU copy, written back to guest memory when its batch is reaped) waits for all recorded work, which runs the write-back, and is then written by the CPU: on the console every earlier store is in memory before a later label of the same queue is visible, and a title that rewrites the written region after polling the label would otherwise have the bytes overwritten by the late write-back (Gang Beasts, CUSA04865 through #6977, rebuilt its command buffer over such a region). The stall is paid once per label while such a write-back is pending; written buffers the device imports in place bind directly and pay nothing. +- A [label](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) of 8 bytes or less recorded while a copied writer is listed, or while an earlier completion store is pending, is stored by the CPU when its batch is reaped, after the write-backs, so it becomes visible up to one reap later than on the console, also after GPU writes recorded behind it. Larger stores (DUMP_CONST_RAM, COPY_DATA, DMA_DATA) and DMA_DATA copies the GPU makes stay on the GPU unless they cover a copied writer's range or a pending completion store, so they can be visible before an unrelated earlier write-back. The order also breaks when a write-back's guest store syncs a pending storage image from inside the completion: that nested sync finishes later batches, whose completion stores land first. `APS5_DRAIN_COMPLETION_LABELS=1` drains instead (reason 2). - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. From b9b9c07713532f28e3b3eba56c49b554a7f5c4f4 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 08:05:23 -0300 Subject: [PATCH 0325/1360] fix(libSceHttp): parse, build and merge URIs as measured on a PS5 A payload process on a PS5 (firmware 13.60) parsed, built and merged several hundred URIs. sceHttpUriParse cuts the hostname at the first byte that cannot be in it and starts the path there instead of failing, takes the userinfo up to the first '@', accepts a port up to 65536 that ends at '/' or the end, and gives https... schemes 443 and http... schemes 80. It sweeps the path as sceHttpUriSweepPath does but sizes the pool for the unswept path, and leaves require alone when the pool is too small. sceHttpUriBuild writes IPv6 hostnames without brackets. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHttp/src/Uri.cpp | 108 ++++++++-------- core/libs/tests/GuestHttp.cpp | 184 +++++++++++++++++++++++++-- docs/dev/debt/libSceHttp.md | 7 +- 3 files changed, 229 insertions(+), 70 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 670a29e7eb..943bce8709 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -9,6 +9,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" +extern "C" int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize); + namespace { constexpr uint32_t URI_BUILD_WITH_SCHEME = 0x01; @@ -28,6 +30,7 @@ struct UriParts { std::string_view password; std::string_view hostname; std::string path; + size_t pathSpace = 0; std::string_view query; std::string_view fragment; uint16_t port = 0; @@ -54,8 +57,8 @@ bool equalsIgnoreCase(std::string_view left, std::string_view right) { } uint16_t defaultPort(std::string_view scheme) { - if (equalsIgnoreCase(scheme, "http")) return 80; - if (equalsIgnoreCase(scheme, "https")) return 443; + if (scheme.size() >= 5 && equalsIgnoreCase(scheme.substr(0, 5), "https")) return 443; + if (scheme.size() >= 4 && equalsIgnoreCase(scheme.substr(0, 4), "http")) return 80; return 0; } @@ -63,31 +66,21 @@ bool isHostCharacter(char c) { return isAsciiAlpha(c) || isAsciiDigit(c) || c == '-' || c == '.' || c == '_'; } -std::string removeDotSegments(std::string_view input) { - std::string output; - while (!input.empty()) { - if (input.starts_with("../")) input.remove_prefix(3); - else if (input.starts_with("./")) input.remove_prefix(2); - else if (input.starts_with("/./")) input.remove_prefix(2); - else if (input == "/.") input = "/"; - else if (input.starts_with("/../") || input == "/..") { - input = input.size() == 3 ? std::string_view("/") : input.substr(3); - const size_t slash = output.rfind('/'); - output.erase(slash == std::string::npos ? 0 : slash); - } else if (input == "." || input == "..") input = {}; - else { - const size_t end = input.find('/', 1); - const size_t length = end == std::string_view::npos ? input.size() : end; - output.append(input.substr(0, length)); - input.remove_prefix(length); - } - } - return output; +bool isUserinfoCharacter(char c) { + return isAsciiAlpha(c) || isAsciiDigit(c) || std::string_view("-._~!$&'()*+,;=%:").find(c) != std::string_view::npos; +} + +std::string sweptPath(std::string_view path) { + const std::string source(path); + std::string swept(source.size() + 1, '\0'); + sceHttpUriSweepPath(swept.data(), source.c_str(), source.size() + 1); + swept.resize(std::strlen(swept.c_str())); + return swept; } size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; - if (!uri.empty() && isAsciiAlpha(uri[0])) { + if (!uri.empty() && (isAsciiAlpha(uri[0]) || uri[0] == '+' || uri[0] == '-' || uri[0] == '.')) { schemeEnd = 1; while (schemeEnd < uri.size() && (isAsciiAlpha(uri[schemeEnd]) || isAsciiDigit(uri[schemeEnd]) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; @@ -109,49 +102,51 @@ int parseUri(std::string_view uri, UriParts& parts) { parts.opaque = !uri.starts_with("//"); if (!parts.opaque) uri.remove_prefix(2); - std::string_view authority = uri.substr(0, uri.find_first_of("/?#")); - uri.remove_prefix(authority.size()); - - const size_t at = authority.rfind('@'); - if (at != std::string_view::npos) { - const std::string_view userinfo = authority.substr(0, at); + size_t userinfoEnd = 0; + while (userinfoEnd < uri.size() && isUserinfoCharacter(uri[userinfoEnd])) ++userinfoEnd; + if (userinfoEnd < uri.size() && uri[userinfoEnd] == '@') { + const std::string_view userinfo = uri.substr(0, userinfoEnd); const size_t colon = userinfo.find(':'); parts.username = userinfo.substr(0, colon); if (colon != std::string_view::npos) parts.password = userinfo.substr(colon + 1); - authority.remove_prefix(at + 1); + uri.remove_prefix(userinfoEnd + 1); } - std::string_view port; - if (authority.starts_with('[')) { - const size_t close = authority.find(']'); + if (uri.starts_with('[')) { + const size_t close = uri.find(']'); if (close == std::string_view::npos) return ERROR_INVALID_URL; - parts.hostname = authority.substr(1, close - 1); - authority.remove_prefix(close + 1); - if (!authority.empty() && authority[0] != ':') return ERROR_INVALID_URL; - if (!authority.empty()) port = authority.substr(1); + parts.hostname = uri.substr(1, close - 1); for (const char c : parts.hostname) { if (!isHostCharacter(c) && c != ':') return ERROR_INVALID_URL; } + uri.remove_prefix(close + 1); } else { - const size_t colon = authority.find(':'); - parts.hostname = authority.substr(0, colon); - if (colon != std::string_view::npos) port = authority.substr(colon + 1); - for (const char c : parts.hostname) { - if (!isHostCharacter(c)) return ERROR_INVALID_URL; + size_t hostEnd = 0; + if (!uri.starts_with('.')) { + while (hostEnd < uri.size() && isHostCharacter(uri[hostEnd])) ++hostEnd; } + parts.hostname = uri.substr(0, hostEnd); + uri.remove_prefix(hostEnd); } - if (port.size() > 5) return ERROR_INVALID_URL; - uint32_t portValue = 0; - for (const char c : port) { - if (!isAsciiDigit(c)) return ERROR_INVALID_URL; - portValue = portValue * 10 + static_cast(c - '0'); + parts.port = defaultPort(parts.scheme); + if (uri.starts_with(':')) { + uri.remove_prefix(1); + const std::string_view port = uri.substr(0, uri.find('/')); + uri.remove_prefix(port.size()); + if (port.size() > 5) return ERROR_INVALID_URL; + uint32_t portValue = 0; + for (const char c : port) { + if (!isAsciiDigit(c)) return ERROR_INVALID_URL; + portValue = portValue * 10 + static_cast(c - '0'); + } + if (portValue > 0x10000) return ERROR_INVALID_URL; + if (!port.empty()) parts.port = static_cast(portValue); } - if (portValue > 0xFFFF) return ERROR_INVALID_URL; - parts.port = port.empty() ? defaultPort(parts.scheme) : static_cast(portValue); const std::string_view path = uri.substr(0, uri.find_first_of("?#")); - parts.path = removeDotSegments(path); + parts.path = sweptPath(path); + parts.pathSpace = path.size(); uri.remove_prefix(path.size()); if (uri.starts_with('?')) { @@ -163,8 +158,8 @@ int parseUri(std::string_view uri, UriParts& parts) { } 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; + const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.query, parts.fragment}; + size_t size = parts.pathSpace + 1; for (const std::string_view field : fields) size += field.size() + 1; return size; } @@ -197,8 +192,7 @@ int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const uri.append(username); if (!password.empty()) uri.append(":").append(password); if (!username.empty() || !password.empty()) uri.push_back('@'); - if (hostname.find(':') != std::string_view::npos) uri.append("[").append(hostname).append("]"); - else uri.append(hostname); + uri.append(hostname); if (option & URI_BUILD_WITH_PORT && src_element->port != 0 && src_element->port != defaultPort(scheme)) { uri.append(":").append(std::to_string(src_element->port)); } @@ -311,10 +305,12 @@ 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}; + const size_t spaces[] = {parts.scheme.size(), parts.username.size(), parts.password.size(), parts.hostname.size(), parts.pathSpace, parts.query.size(), + parts.fragment.size()}; const size_t size = poolSize(parts); + if (write && prepare < size) return ERROR_OUT_OF_MEMORY; if (require) *require = size; if (!write) return 0; - if (prepare < size) return ERROR_OUT_OF_MEMORY; char* next = static_cast(pool); char* strings[7]; @@ -322,7 +318,7 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* strings[i] = next; if (!fields[i].empty()) std::memcpy(next, fields[i].data(), fields[i].size()); next[fields[i].size()] = '\0'; - next += fields[i].size() + 1; + next += spaces[i] + 1; } *out = {}; out->opaque = parts.opaque; diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 93eb0119d3..50d665a652 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -59,15 +59,174 @@ static void CheckUriParseCharacters() { Require(Equal(element.scheme, "aZ09+.-") && element.port == 0); for (unsigned value = 128; value <= 255; ++value) { const std::string byte(1, static_cast(value)); - const std::string urls[] = { - "http://" + byte + "/", "http://[" + byte + "]/", byte + "://host/", - "a" + byte + "://host/", "http://host:" + byte + "/" - }; - for (const auto& url : urls) { + for (const auto& url : {"http://[" + byte + "]/", "http://host:" + byte + "/"}) { std::size_t required = 0; Require(sceHttpUriParse(nullptr, url.c_str(), nullptr, &required, 0) == static_cast(0x80433060)); Require(sceHttpUriParse(&element, url.c_str(), pool, nullptr, sizeof(pool)) == static_cast(0x80433060)); } + Require(sceHttpUriParse(&element, ("http://" + byte + "/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.hostname, "") && element.path == byte + "/"); + Require(sceHttpUriParse(&element, (byte + "://host/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(element.opaque && Equal(element.scheme, "") && Equal(element.hostname, "") && element.path == byte + "://host/"); + Require(sceHttpUriParse(&element, ("a" + byte + "://host/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "") && Equal(element.hostname, "a") && element.path == byte + "://host/"); + } +} + +struct UriParseCase { + const char* url; + int opaque; + std::uint16_t port; + const char* scheme; + const char* username; + const char* password; + const char* hostname; + const char* path; + const char* query; + const char* fragment; + std::size_t pathSpace = 0; +}; + +static void CheckUriParseAsMeasured() { + constexpr UriParseCase cases[] = { + {"http://bad host/", 0, 80, "http", "", "", "bad", " host/", "", ""}, + {"http://ex%41mple.com/", 0, 80, "http", "", "", "ex", "%41mple.com/", "", ""}, + {"http://h\t/", 0, 80, "http", "", "", "h", "\t/", "", ""}, + {"not a url", 1, 0, "", "", "", "not", " a url", "", ""}, + {"example.com/a", 1, 0, "", "", "", "example.com", "/a", "", ""}, + {"mailto:x", 1, 0, "mailto", "", "", "x", "", "", ""}, + {"abc:def", 1, 0, "abc", "", "", "def", "", "", ""}, + {"http:", 1, 80, "http", "", "", "", "", "", ""}, + {"https:h", 1, 443, "https", "", "", "h", "", "", ""}, + {"//host/x", 0, 0, "", "", "", "host", "/x", "", ""}, + {"", 1, 0, "", "", "", "", "", "", ""}, + {"?q", 1, 0, "", "", "", "", "", "?q", ""}, + {"http://host:65536/", 0, 0, "http", "", "", "host", "/", "", ""}, + {"http://h:65535", 0, 65535, "http", "", "", "h", "", "", ""}, + {"http://h:0080/", 0, 80, "http", "", "", "h", "/", "", ""}, + {"http://h:00000/", 0, 0, "http", "", "", "h", "/", "", ""}, + {"http://host:/", 0, 80, "http", "", "", "host", "/", "", ""}, + {"http://[::1]x/", 0, 80, "http", "", "", "::1", "x/", "", ""}, + {"http://[::1]]/", 0, 80, "http", "", "", "::1", "]/", "", ""}, + {"http://[::1]:/", 0, 80, "http", "", "", "::1", "/", "", ""}, + {"http://[]/", 0, 80, "http", "", "", "", "/", "", ""}, + {"http://[.]/", 0, 80, "http", "", "", ".", "/", "", ""}, + {"http://u@h@i/", 0, 80, "http", "u", "", "h", "@i/", "", ""}, + {"http://u s@h/", 0, 80, "http", "", "", "u", " s@h/", "", ""}, + {"http://u:p:q@h/", 0, 80, "http", "u", "p:q", "h", "/", "", ""}, + {"http://u%41@h/", 0, 80, "http", "u%41", "", "h", "/", "", ""}, + {"http://h@/", 0, 80, "http", "h", "", "", "/", "", ""}, + {"http://[::1]@h/", 0, 80, "http", "", "", "::1", "@h/", "", ""}, + {"http://u@[::1]:8/", 0, 8, "http", "u", "", "::1", "/", "", ""}, + {".@h/", 1, 0, "", ".", "", "h", "/", "", ""}, + {"http://.a/", 0, 80, "http", "", "", "", ".a/", "", ""}, + {"http://-a/", 0, 80, "http", "", "", "-a", "/", "", ""}, + {"http://a..b/", 0, 80, "http", "", "", "a..b", "/", "", ""}, + {"a.", 1, 0, "", "", "", "a.", "", "", ""}, + {"http://h/a/..", 0, 80, "http", "", "", "h", "/a/..", "", ""}, + {"http://h/a/.", 0, 80, "http", "", "", "h", "/a/.", "", ""}, + {"http://h/a/b/../../..", 0, 80, "http", "", "", "h", "/..", "", "", 13}, + {"http://h/../../a", 0, 80, "http", "", "", "h", "/a", "", "", 8}, + {"http://h/a//b/../c", 0, 80, "http", "", "", "h", "/a//c", "", "", 10}, + {"http://h/a/.../../b", 0, 80, "http", "", "", "h", "/a/b", "", "", 11}, + {"http://h/./a/../b/", 0, 80, "http", "", "", "h", "/b/", "", "", 10}, + {"./a", 1, 0, "", "", "", "", "./a", "", ""}, + {"a/../b", 1, 0, "", "", "", "a", "/b", "", "", 5}, + {"1x://h/", 1, 0, "", "", "", "1x", "//h/", "", ""}, + {"-x://h/", 0, 0, "-x", "", "", "h", "/", "", ""}, + {"s_x://h/p", 1, 0, "", "", "", "s_x", "//h/p", "", ""}, + {"s:x://h/p", 1, 0, "s", "", "", "x", "//h/p", "", ""}, + {"s@x://h/p", 1, 0, "", "s", "", "x", "//h/p", "", ""}, + {"httpx://h/", 0, 80, "httpx", "", "", "h", "/", "", ""}, + {"httpsx://h/", 0, 443, "httpsx", "", "", "h", "/", "", ""}, + {"HTTPx://h/", 0, 80, "HTTPx", "", "", "h", "/", "", ""}, + {"htt://h/", 0, 0, "htt", "", "", "h", "/", "", ""}, + {"ws://h/", 0, 0, "ws", "", "", "h", "/", "", ""}, + {"https://h:80/", 0, 80, "https", "", "", "h", "/", "", ""}, + {"http://h/p?q#f#g", 0, 80, "http", "", "", "h", "/p", "?q", "#f#g"}, + {"http://h/p#f?q", 0, 80, "http", "", "", "h", "/p", "", "#f?q"}, + {"http://h?", 0, 80, "http", "", "", "h", "", "?", ""}, + }; + char pool[256]; + for (const auto& row : cases) { + SceHttpUriElement element{}; + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, row.url, nullptr, &required, 0) == 0); + const std::size_t size = std::strlen(row.scheme) + std::strlen(row.username) + std::strlen(row.password) + std::strlen(row.hostname) + + (row.pathSpace != 0 ? row.pathSpace : std::strlen(row.path)) + std::strlen(row.query) + std::strlen(row.fragment) + 7; + Require(required == size); + required = 77; + Require(sceHttpUriParse(&element, row.url, pool, &required, size - 1) == static_cast(0x80431022) && required == 77); + Require(sceHttpUriParse(&element, row.url, pool, &required, sizeof(pool)) == 0 && required == size); + Require(element.opaque == row.opaque && element.port == row.port); + Require(Equal(element.scheme, row.scheme) && Equal(element.username, row.username) && Equal(element.password, row.password)); + Require(Equal(element.hostname, row.hostname) && Equal(element.path, row.path)); + Require(Equal(element.query, row.query) && Equal(element.fragment, row.fragment)); + } + for (const char* url : {"http://[::1/", "http://[a b]/", "http://[a%b]/", "http://[a@b]/", "http://host:80a/", "http://h:65537/", "http://h:99999/", + "http://h:000080/", "http://h:80?q", "http://h:80#f", "http://h:80 /", "http://h: 80/", "http://h:+80/", "http://h:-1/", + "http://h:80:81/", "http://[::1]:8a/", "http://a:b/p"}) { + std::size_t required = 77; + Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == static_cast(0x80433060) && required == 77); + } +} + +struct UriMergeCase { + const char* base; + const char* relative; + std::size_t sizeOnly; + const char* merged; +}; + +static void CheckUriMergeAsMeasured() { + constexpr UriMergeCase cases[] = { + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "?x", 0x84, "http://user:pw@example.com:8080/a/b/?x"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "#y", 0x84, "http://user:pw@example.com:8080/a/b/#y"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "../../../../d", 0x9a, "http://user:pw@example.com:8080/a/b/../../../../d"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "http:d", 0x8c, "http://user:pw@example.com:8080/a/b/http:d"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "//other.com/x", 0x9a, "//other.com/x"}, + {"http://example.com", "../d", 0x44, "http://example.com/../d"}, + {"http://example.com/a/./b/../c", "d", 0x5f, "http://example.com/a/d"}, + {"not a url", "d", 0x26, "not a url/d"}, + {"example.com/a", "b", 0x32, "example.com/b"}, + {"http://[::1]:80/a/b", "c", 0x3c, "http://::1/a/c"}, + {"http://ex ample.com/a", "c", 0x47, "http://ex ample.com/c"}, + {"http://example.com/a", "//bad host/x", 0x5a, "//bad host/x"}, + {"//host/a/b", "c", 0x27, "//host/a/c"}, + {"/a/b", "c", 0x17, "/a/c"}, + {"a/b", "c", 0x14, "a/c"}, + {"", "c", 0xb, "/c"}, + {"", "", 0x9, "/"}, + {"mailto:x@y", "c", 0x27, "mailto:x@y/c"}, + {"http://h", "?q", 0x22, "http://h/?q"}, + {"https://h:443/a", "c", 0x31, "https://h/c"}, + {"http://h:0/a", "c", 0x2a, "http://h/c"}, + {"http://h:65536/a", "c", 0x32, "http://h/c"}, + {"http://:p@h/a", "c", 0x2d, "http://:p@h/c"}, + {"http://bad host/", "./x", 0x3c, "http://bad host/./x"}, + {"http://h/a", "http://bad host/", 0x44, "http://bad host/"}, + {"http://h/a", "HTTPS://Other.com/", 0x48, "HTTPS://Other.com/"}, + {"http://h/a", "//o", 0x2a, "//o"}, + {"http://h/a", "x:y", 0x2a, "http://h/x:y"}, + {"http://h/a", "d e", 0x2a, "http://h/d e"}, + {"http://h/a", "..", 0x28, "http://h/.."}, + {"http://h/a/./b/../c", "../d", 0x47, "http://h/a/../d"}, + {"http://h/a?q#f", "", 0x30, "http://h/"}, + {"not a url", "//x/y", 0x2e, "//x/y"}, + }; + char merged[512]; + for (const auto& row : cases) { + std::size_t required = 0; + Require(sceHttpUriMerge(nullptr, row.base, row.relative, &required, 0, 0) == 0 && required == row.sizeOnly); + std::memset(merged, 'Z', sizeof(merged)); + required = 77; + Require(sceHttpUriMerge(merged, row.base, row.relative, &required, row.sizeOnly - 1, 0) == static_cast(0x80431022)); + Require(merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, row.base, row.relative, &required, sizeof(merged), 0) == 0 && Equal(merged, row.merged)); + } + for (const char* base : {"http://[::1/a", "http://h:80a/a"}) { + std::size_t required = 77; + Require(sceHttpUriMerge(merged, base, "c", &required, sizeof(merged), 0) == static_cast(0x80433060) && required == 77); } } @@ -114,7 +273,7 @@ int main() { const char* url = "https://user:secret@example.com:8443/a/./b/../c?x=1&y=2#top"; std::size_t required = 0; Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == 0); - Require(required == 6 + 5 + 7 + 12 + 5 + 9 + 5); + Require(required == 6 + 5 + 7 + 12 + 12 + 9 + 5); char pool[256]; SceHttpUriElement element; @@ -124,6 +283,7 @@ int main() { Require(Equal(element.scheme, "https") && Equal(element.username, "user") && Equal(element.password, "secret")); Require(Equal(element.hostname, "example.com") && element.port == 8443); Require(Equal(element.path, "/a/c") && Equal(element.query, "?x=1&y=2") && Equal(element.fragment, "#top")); + Require(element.path == pool + 30 && element.query == pool + 42 && element.fragment == pool + 51); char built[256]; Require(sceHttpUriBuild(nullptr, &required, 0, &element, buildAll) == 0); @@ -153,7 +313,7 @@ int main() { Require(sceHttpUriParse(&element, "http://[::1]:8080/index.html", pool, nullptr, sizeof(pool)) == 0); Require(Equal(element.hostname, "::1") && element.port == 8080 && Equal(element.path, "/index.html")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); - Require(Equal(built, "http://[::1]:8080/index.html")); + Require(Equal(built, "http://::1:8080/index.html")); Require(sceHttpUriParse(&element, "mailto:someone@example.com", pool, nullptr, sizeof(pool)) == 0); Require(element.opaque != 0 && element.port == 0); @@ -170,10 +330,11 @@ int main() { Require(sceHttpUriParse(&element, nullptr, pool, nullptr, sizeof(pool)) == invalidUrl); Require(sceHttpUriParse(nullptr, url, nullptr, nullptr, 0) == invalidValue); - Require(sceHttpUriParse(nullptr, "http://host:65536/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:65537/", nullptr, &required, 0) == invalidUrl); Require(sceHttpUriParse(nullptr, "http://host:80a/", nullptr, &required, 0) == invalidUrl); Require(sceHttpUriParse(nullptr, "http://[::1/", nullptr, &required, 0) == invalidUrl); - Require(sceHttpUriParse(nullptr, "http://bad host/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:80?q", nullptr, &required, 0) == invalidUrl); + CheckUriParseAsMeasured(); Require(sceHttpUriBuild(built, nullptr, sizeof(built), nullptr, buildAll) == invalidUrl); Require(sceHttpUriBuild(nullptr, nullptr, 0, &element, buildAll) == invalidValue); @@ -285,9 +446,10 @@ int main() { 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(sceHttpUriMerge(merged, "http://[::1/", "./x", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, "http://h:80a/a", "c", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + CheckUriMergeAsMeasured(); constexpr int beforeInit = static_cast(0x80431001); constexpr int aborted = static_cast(0x80431080); diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index bc45f2c73b..6adcc33948 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -6,11 +6,12 @@ ### Unknown function info -- [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. +- [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for each single field option, all fields, opaque elements and null fields: a non-opaque URI keeps `//` whatever fields are selected (Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`), the query and fragment are appended without adding `?` or `#`, a port equal to the scheme's default is left out and an IPv6 hostname is written without brackets. The default port follows the scheme prefix measured for `sceHttpUriParse`; `sceHttpUriBuild` itself was only measured with `http` and `https`. - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end 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. 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. +- [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); results, errors and `require` (the base's `sceHttpUriParse` size plus 2 and twice the two string lengths, also for a reference copied unchanged) follow a payload process on a PS5 (firmware 13.60). A reference with an authority (`//` after an optional scheme) is copied unchanged; any other, including `mailto:x`, `http:d`, `?q` and `#f`, is appended unswept to the base path up to its last `/`, and the base query and fragment are dropped. A reference that `sceHttpUriParse` rejects returns that error, which was not measured. +- [sceHttpUriParse](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for every ASCII byte inside a scheme, userinfo and hostname, and for letters, digits, `+`, `-` and `.` at the start of a scheme, where a digit ends it; other bytes at the start of a scheme are taken as they are inside one. The userinfo ends at the first `@` after bytes from the RFC 3986 userinfo set; a hostname is letters, digits, `-`, `_` and `.` without a leading `.`, and the first byte outside it starts the path instead of failing (`http://bad host/` has the host `bad` and the path ` host/`). A port is up to 5 digits that end at `/` or the end, at most 65536, which reads as 0; an empty port takes 443 for a scheme starting with `https`, 80 for one starting with `http` (case-insensitive) and 0 otherwise. The path is swept as `sceHttpUriSweepPath` does, while the pool keeps room for the unswept path. With a pool too small the console returns `ERROR_OUT_OF_MEMORY`, leaves `require` alone and has already written part of the pool; how much is not known, so nothing is written here. - [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); decoding and error codes follow a payload process on a PS5 (firmware 13.60). A buffer one byte short of the decoded size got the decoded bytes without a terminator and left `require` alone; a shorter buffer is assumed to get the first `prepare` bytes the same way - [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before - [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 @@ -71,7 +72,7 @@ ### Functional -- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - bytes 0x80 to 0xFF were measured only as 0x80, 0xC3 and 0xFF in a hostname; the others, and any of them in a scheme or userinfo, are taken as the measured ASCII bytes outside those sets are. ASCII classification and scheme case folding are independent of the host locale. - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. From a212812dc153578fe1ce5d9c98ca0cc0511c2fb0 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 16:47:58 +0530 Subject: [PATCH 0326/1360] feat(prx): declare exports for 5 inventory-covered system libraries Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libSceRnpsAppMgr/Export.cpp | 137 ++++++++++++++++++- core/libs/prx/libSceShareUtility/Export.cpp | 110 ++++++++++++++- core/libs/prx/libSceSystemLogger2/Export.cpp | 27 +++- core/libs/prx/libSceVdecCore/Export.cpp | 100 +++++++++++++- core/libs/prx/libSceVenc/Export.cpp | 70 +++++++++- docs/dev/debt/libSceRnpsAppMgr.md | 5 + docs/dev/debt/libSceShareUtility.md | 5 + docs/dev/debt/libSceSystemLogger2.md | 5 + docs/dev/debt/libSceVdecCore.md | 5 + docs/dev/debt/libSceVenc.md | 5 + 10 files changed, 462 insertions(+), 7 deletions(-) create mode 100644 docs/dev/debt/libSceRnpsAppMgr.md create mode 100644 docs/dev/debt/libSceShareUtility.md create mode 100644 docs/dev/debt/libSceSystemLogger2.md create mode 100644 docs/dev/debt/libSceVdecCore.md create mode 100644 docs/dev/debt/libSceVenc.md diff --git a/core/libs/prx/libSceRnpsAppMgr/Export.cpp b/core/libs/prx/libSceRnpsAppMgr/Export.cpp index d81f4a5e32..a2fdcbc15d 100644 --- a/core/libs/prx/libSceRnpsAppMgr/Export.cpp +++ b/core/libs/prx/libSceRnpsAppMgr/Export.cpp @@ -1,10 +1,143 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceRnpsAppMgrBlockAppInstall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrCreateUpdateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrCreateUrlRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrDestroyBundle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrDestroyUpdateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrFinishUrlRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetAppInfoDebugString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetAppInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetUpdateProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrHasEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrPushEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRecoverUfsImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRegisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRemoveApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRemoveAppOnSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRemoveUfsImageOnSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRequestUpdateCheck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrSetCrlUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrSetEventFlagForClients() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrSetUpdateUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrStartService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrUnblockAppInstall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrUnregisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceShareUtility/Export.cpp b/core/libs/prx/libSceShareUtility/Export.cpp index fb34206c21..b8d35a3625 100644 --- a/core/libs/prx/libSceShareUtility/Export.cpp +++ b/core/libs/prx/libSceShareUtility/Export.cpp @@ -5,6 +5,114 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceShareUtilityAdvanceFromFileWithContentIdList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceFromFileWithTitleIdList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceGetServiceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceQueryServiceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceQueryServiceInfoA() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceResetPlayingContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceSetPlayingContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceSetUploadContentData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceValidateContents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenuBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenuDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenuForShareContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityInitializeEx2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenuBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenuDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenuForShareContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceSystemLogger2/Export.cpp b/core/libs/prx/libSceSystemLogger2/Export.cpp index d81f4a5e32..82a408b187 100644 --- a/core/libs/prx/libSceSystemLogger2/Export.cpp +++ b/core/libs/prx/libSceSystemLogger2/Export.cpp @@ -1,10 +1,33 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceSystemLogger2GetTimestamp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2SetEventAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2SetEventAsyncEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp index fb34206c21..343d1ec6a6 100644 --- a/core/libs/prx/libSceVdecCore/Export.cpp +++ b/core/libs/prx/libSceVdecCore/Export.cpp @@ -5,6 +5,104 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceVdecCoreAnalyzeStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreCreateDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreDeleteDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreFinalizeComputeResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreFlushDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreGetDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreInitializeComputeResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreMapMemoryBlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreQueryComputeResourceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreQueryFrameBufferInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreQueryInstanceSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreResetDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSetDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSetDecodeOutputSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSyncDecode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSyncDecodeOutputSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSyncDecodeWptr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreTrySyncDecode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreTrySyncDecodeOutputSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreTrySyncDecodeWptr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceVenc/Export.cpp b/core/libs/prx/libSceVenc/Export.cpp index fb34206c21..8d8de5aa65 100644 --- a/core/libs/prx/libSceVenc/Export.cpp +++ b/core/libs/prx/libSceVenc/Export.cpp @@ -5,6 +5,74 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceVencCreateEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencDeleteEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencGetConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencMapMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencQueryHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencQueryMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencQueryPreset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetDecodedFrame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetPictureConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetRateControlConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetReferenceFrameInvalidationConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencStartEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencStopEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/debt/libSceRnpsAppMgr.md b/docs/dev/debt/libSceRnpsAppMgr.md new file mode 100644 index 0000000000..b430381f91 --- /dev/null +++ b/docs/dev/debt/libSceRnpsAppMgr.md @@ -0,0 +1,5 @@ +# libSceRnpsAppMgr technical debt + +### Unknown function info + +- [sceRnpsAppMgrBlockAppInstall, sceRnpsAppMgrCreateUpdateRequest, sceRnpsAppMgrCreateUrlRequest, sceRnpsAppMgrDestroyBundle, sceRnpsAppMgrDestroyUpdateRequest, sceRnpsAppMgrFinishUrlRequest, sceRnpsAppMgrGetAppInfo, sceRnpsAppMgrGetAppInfoDebugString, sceRnpsAppMgrGetAppInfoList, sceRnpsAppMgrGetEventForShellUI, sceRnpsAppMgrGetUpdateProgress, sceRnpsAppMgrHasEventForShellUI, sceRnpsAppMgrInitialize, sceRnpsAppMgrPushEventForShellUI, sceRnpsAppMgrRecoverUfsImage, sceRnpsAppMgrRegisterShellUI, sceRnpsAppMgrRemoveApp, sceRnpsAppMgrRemoveAppOnSystemShutdown, sceRnpsAppMgrRemoveUfsImageOnSystemShutdown, sceRnpsAppMgrRequestUpdateCheck, sceRnpsAppMgrSetCrlUrl, sceRnpsAppMgrSetEventFlagForClients, sceRnpsAppMgrSetUpdateUrl, sceRnpsAppMgrStartService, sceRnpsAppMgrTerminate, sceRnpsAppMgrUnblockAppInstall and sceRnpsAppMgrUnregisterShellUI](../../../core/libs/prx/libSceRnpsAppMgr/Export.cpp) (libSceRnpsAppMgr) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRnpsAppMgr.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceShareUtility.md b/docs/dev/debt/libSceShareUtility.md new file mode 100644 index 0000000000..c953b5f180 --- /dev/null +++ b/docs/dev/debt/libSceShareUtility.md @@ -0,0 +1,5 @@ +# libSceShareUtility technical debt + +### Unknown function info + +- [sceShareUtilityAdvanceFromFileWithContentIdList, sceShareUtilityAdvanceFromFileWithTitleIdList, sceShareUtilityAdvanceGetServiceInfo, sceShareUtilityAdvanceInitialize, sceShareUtilityAdvanceQueryServiceInfo, sceShareUtilityAdvanceQueryServiceInfoA, sceShareUtilityAdvanceResetPlayingContent, sceShareUtilityAdvanceSetPlayingContent, sceShareUtilityAdvanceSetUploadContentData, sceShareUtilityAdvanceTerminate, sceShareUtilityAdvanceValidateContents, sceShareUtilityAdvanceWakeUpShareMenu, sceShareUtilityAdvanceWakeUpShareMenuBroadcast, sceShareUtilityAdvanceWakeUpShareMenuDefault, sceShareUtilityAdvanceWakeUpShareMenuForShareContent, sceShareUtilityInitialize, sceShareUtilityInitializeEx2, sceShareUtilityOpenShareMenu, sceShareUtilityOpenShareMenuBroadcast, sceShareUtilityOpenShareMenuDefault, sceShareUtilityOpenShareMenuForShareContent and sceShareUtilityTerminate](../../../core/libs/prx/libSceShareUtility/Export.cpp) (libSceShareUtility) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceShareUtility.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSystemLogger2.md b/docs/dev/debt/libSceSystemLogger2.md new file mode 100644 index 0000000000..3d17ac6069 --- /dev/null +++ b/docs/dev/debt/libSceSystemLogger2.md @@ -0,0 +1,5 @@ +# libSceSystemLogger2 technical debt + +### Unknown function info + +- [sceSystemLogger2GetTimestamp, sceSystemLogger2Initialize, sceSystemLogger2SetEventAsync, sceSystemLogger2SetEventAsyncEx and sceSystemLogger2Terminate](../../../core/libs/prx/libSceSystemLogger2/Export.cpp) (libSceSystemLogger2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemLogger2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVdecCore.md b/docs/dev/debt/libSceVdecCore.md new file mode 100644 index 0000000000..a6803142e3 --- /dev/null +++ b/docs/dev/debt/libSceVdecCore.md @@ -0,0 +1,5 @@ +# libSceVdecCore technical debt + +### Unknown function info + +- [sceVdecCoreAnalyzeStream, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder, sceVdecCoreFinalizeComputeResource, sceVdecCoreFlushDecodeOutput, sceVdecCoreGetDecodeOutput, sceVdecCoreInitializeComputeResource, sceVdecCoreMapMemoryBlock, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryInstanceSize, sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSetDecodeOutputSw, sceVdecCoreSyncDecode, sceVdecCoreSyncDecodeOutputSw, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode, sceVdecCoreTrySyncDecodeOutputSw and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVenc.md b/docs/dev/debt/libSceVenc.md new file mode 100644 index 0000000000..92c6460425 --- /dev/null +++ b/docs/dev/debt/libSceVenc.md @@ -0,0 +1,5 @@ +# libSceVenc technical debt + +### Unknown function info + +- [sceVencCreateEncoder, sceVencDeleteEncoder, sceVencGetConfig, sceVencMapMemory, sceVencQueryHeader, sceVencQueryMemorySize, sceVencQueryPreset, sceVencSetConfig, sceVencSetDecodedFrame, sceVencSetPictureConfig, sceVencSetRateControlConfig, sceVencSetReferenceFrameInvalidationConfig, sceVencStartEncode and sceVencStopEncode](../../../core/libs/prx/libSceVenc/Export.cpp) (libSceVenc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVenc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified From 736ee67caf96b78295e9563f6c790ffb47a4cba9 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 00:23:37 +1300 Subject: [PATCH 0327/1360] feat(libSceNpCommon): implement 94 more of the module's exports Builds on #9037. Adds threads, event flags, semaphores, atomics, heaps and kernel memory, callout contexts, time and SDK version queries, base64 and hex codecs, integer and string helpers, JSON escaping, URL encoding, online id strings, language codes, title ids and debug output. The behaviour follows libSceNpCommon.sprx from firmware 12.00, read from disassembly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/CMakeLists.txt | 11 + core/libs/prx/libSceNpCommon/CMakeLists.txt | 2 + .../prx/libSceNpCommon/PendingExports.cpp | 470 ---------- core/libs/prx/libSceNpCommon/Runtime.cpp | 836 ++++++++++++++++++ core/libs/prx/libSceNpCommon/Strings.cpp | 629 +++++++++++++ core/libs/tests/GuestNpCommonRuntime.cpp | 495 +++++++++++ docs/dev/debt/libSceNpCommon.md | 106 +-- 7 files changed, 1984 insertions(+), 565 deletions(-) create mode 100644 core/libs/prx/libSceNpCommon/Runtime.cpp create mode 100644 core/libs/prx/libSceNpCommon/Strings.cpp create mode 100644 core/libs/tests/GuestNpCommonRuntime.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index a1e0d5218d..36386a8f15 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -393,6 +393,17 @@ if(BUILD_TESTING) endif() endforeach() + add_test_executable(guest_np_common_runtime_tests tests/GuestNpCommonRuntime.cpp) + target_include_directories(guest_np_common_runtime_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_common_runtime_tests PRIVATE libSceNpCommon libkernel libc) + configure_windows_unwind(guest_np_common_runtime_tests) + add_test(NAME guest_np_common_runtime COMMAND guest_np_common_runtime_tests) + set_tests_properties(guest_np_common_runtime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_common_runtime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) diff --git a/core/libs/prx/libSceNpCommon/CMakeLists.txt b/core/libs/prx/libSceNpCommon/CMakeLists.txt index f85817c30e..bab2481853 100644 --- a/core/libs/prx/libSceNpCommon/CMakeLists.txt +++ b/core/libs/prx/libSceNpCommon/CMakeLists.txt @@ -2,5 +2,7 @@ add_library(libSceNpCommon SHARED EXCLUDE_FROM_ALL PendingExports.cpp) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp") target_sources(libSceNpCommon PRIVATE Export.cpp) endif() +target_sources(libSceNpCommon PRIVATE Runtime.cpp Strings.cpp) target_include_directories(libSceNpCommon PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNpCommon PRIVATE libc libkernel libSceRtc libSceUserService) configure_windows_unwind(libSceNpCommon) diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 4326061d26..30a117b230 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -5,106 +5,6 @@ extern "C" { -int APS5_VABI sceNpAllocateKernelMemoryNoAlignment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpArchInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpArchTerm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAtomicCas32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAtomicDec32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAtomicInc32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64Decoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64Encoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64GetDecodeSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64UrlDecoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64UrlEncoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64UrlGetDecodeSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutInitCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutStartOnCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutStartOnCtx64(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutStopOnCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutTermCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCancelEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpClearEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCloseEventFlag(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -115,176 +15,21 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } -int APS5_VABI sceNpCondSignalAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondSignalTo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondWait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCreateEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCreateSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCreateThread(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDbgAssignDebugId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDbgDumpBinary(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDbgDumpText(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDeleteEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDeleteSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpFreeKernelMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetNavSdkVersion(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpGetProcessId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGetRandom(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGetSdkVersionUInt(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocator(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocatorEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapGetAllocator(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapGetStat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapShowStat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHexToInt(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpInt32ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpInt64ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntGetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpIntIsOnlineIdString(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpIntToHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpIpc2ClientInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpIpc2ClientTerm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpJsonParse(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -315,96 +60,11 @@ int APS5_VABI sceNpJsonParseInit(void) { return 0; } -int APS5_VABI sceNpLwCondDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondSignal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondSignalAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondSignalTo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondWait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapGetAllocator(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapGetAllocatorEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpOpenEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpOpenSema(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpPanic(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpPollEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpPollSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpRtcConvertToPosixTime(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpRtcFormatRFC3339(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpRtcParseRFC3339(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpServerErrorJsonGetErrorCode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -430,91 +90,11 @@ int APS5_VABI sceNpServerErrorJsonParseMultiInit(void) { return 0; } -int APS5_VABI sceNpSetEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSignalSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrBuildHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrParseHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToInt32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToInt64(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToUInt32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToUInt64(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrcpyToBuf(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrnParseHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrncpyToBuf(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpThreadGetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUInt32ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUInt64ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUserGetUserIdList(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilBuildTitleId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilCanonicalizeNpIdForPs4(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -525,11 +105,6 @@ int APS5_VABI sceNpUtilCanonicalizeNpIdForPsp2(void) { return 0; } -int APS5_VABI sceNpUtilCmpAccountId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilGetDateSetAuto(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -565,36 +140,11 @@ int APS5_VABI sceNpUtilGetNpDebug(void) { return 0; } -int APS5_VABI sceNpUtilGetNpLanguageCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilGetNpLanguageCode2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilGetNpLanguageCode2Str(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilGetNpLanguageCodeStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilGetNpTestPatch(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpUtilGetNthChar(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilGetShareTitleCheck(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -625,29 +175,9 @@ int APS5_VABI sceNpUtilGetWebTraceSetting(void) { return 0; } -int APS5_VABI sceNpUtilHttpUrlEncode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilJidToNpId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpUtilJsonEscape(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilJsonGetOneChar(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} - -int APS5_VABI sceNpUtilJsonUnescape(void) { - NotImplemented_nid_no_patch(__func__); - return 0; } diff --git a/core/libs/prx/libSceNpCommon/Runtime.cpp b/core/libs/prx/libSceNpCommon/Runtime.cpp new file mode 100644 index 0000000000..1d87008ad5 --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -0,0 +1,836 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/MallocManagedSize.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/Pthread/include/Cond.hpp" +#include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libkernel/Semaphore/include/Semaphore.hpp" + +extern "C" { +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t flags); +int APS5_VABI sceNpCondDestroy(PthreadCond* cond); +int APS5_VABI sceNpCondSignal(PthreadCond* cond); +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec); +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, uint32_t attr, uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, uint64_t bit_pattern); +int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, uint64_t bit_pattern); +int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, int* num_wait_threads); +int APS5_VABI getpid_nid_postfix(void); +int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time); +int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes); +int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick); +int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds); +int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(void* version); +int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp); +int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); +int32_t APS5_VABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags, const char* name); +int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len); +void* APS5_VABI sceLibcMspaceCreate_nid_postfix(const char* name, void* base, std::size_t size, unsigned flags); +int APS5_VABI sceLibcMspaceDestroy_nid_postfix(void* handle); +void* APS5_VABI sceLibcMspaceMalloc_nid_postfix(void* handle, std::size_t size); +void APS5_VABI sceLibcMspaceFree_nid_postfix(void* handle, void* pointer); +void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, std::size_t size); +int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats); +} + +struct NpTitleIdParts { + char id[9]; + char reserved; + std::uint8_t index; +}; + +struct NpCallout { + NpCallout* next; + void (APS5_VABI *handler)(void* arg); + std::uint64_t deadline; + void* arg; + std::uint64_t reserved; +}; +static_assert(sizeof(NpCallout) == 0x28); + +struct NpCalloutContext { + Pthread thread; + PthreadMutex mutex; + PthreadCond cond; + int initialized; + int stopping; + NpCallout* pending; +}; +static_assert(sizeof(NpCalloutContext) == 0x28); + +struct NpHeap { + void* mspace; +}; + +struct NpHeapStat { + std::uint64_t maxSystemSize; + std::uint64_t maxInuseSize; + std::uint64_t currentInuseSize; +}; + +struct NpKernelMemory { + std::uint64_t size; + std::uint64_t reserved; + void* address; +}; +static_assert(sizeof(NpKernelMemory) == 0x18); + +struct NpMemoryHeap { + NpKernelMemory memory; + NpHeap heap; +}; +static_assert(sizeof(NpMemoryHeap) == 0x20); + +struct NpAllocator { + void* (APS5_VABI *allocate)(std::size_t size, void* context); + void* (APS5_VABI *reallocate)(void* pointer, std::size_t size, void* context); + void (APS5_VABI *release)(void* pointer, void* context); + void* context; +}; + +struct NpAllocatorEx { + NpAllocator allocator; + int outOfMemoryError; +}; + +namespace { + +constexpr int NP_ERROR_NULL_ARGUMENT = static_cast(0x80550003); +constexpr int NP_ERROR_IPC_ALREADY_INITIALIZED = static_cast(0x80559D00); +constexpr int NP_ERROR_IPC_NOT_INITIALIZED = static_cast(0x80559D01); +constexpr int NP_ERROR_ONLINE_ID_MISMATCH = static_cast(0x80550609); +constexpr std::size_t NP_ONLINE_ID_MAX = 16; +constexpr std::size_t NP_ONLINE_ID_MIN = 3; +constexpr int NP_ERROR_INVALID_ARGUMENT_UTIL = static_cast(0x80550601); +constexpr std::size_t NP_LANGUAGE_CODE_SIZE = 6; +constexpr std::size_t NP_LANGUAGE_CODE2_SIZE = 0x24; + +struct NpLanguageCodes { + const char* code; + const char* tag; +}; + +constexpr NpLanguageCodes NP_LANGUAGE_CODES[] { + {"ja", "ja-JP"}, {"en", "en-US"}, {"fr", "fr-FR"}, {"es", "es-ES"}, {"de", "de-DE"}, {"it", "it-IT"}, + {"nl", "nl-NL"}, {"pt", "pt-PT"}, {"ru", "ru-RU"}, {"ko", "ko-KR"}, {"zh-TW", "zh-Hant"}, {"zh-CN", "zh-Hans"}, + {"fi", "fi-FI"}, {"sv", "sv-SE"}, {"da", "da-DK"}, {"no", "no-NO"}, {"pl", "pl-PL"}, {"pt-BR", "pt-BR"}, + {"en-GB", "en-GB"}, {"tr", "tr-TR"}, {"es-MX", "es-419"}, {"ar", "ar-AE"}, {"fr-CA", "fr-CA"}, {"cs", "cs-CZ"}, + {"hu", "hu-HU"}, {"el", "el-GR"}, {"ro", "ro-RO"}, {"th", "th-TH"}, {"vi", "vi-VN"}, {"id", "id-ID"}, + {"uk", "uk-UA"}, +}; + +const NpLanguageCodes* FindLanguageCodes(int language) { + if (language < 0 || static_cast(language) >= std::size(NP_LANGUAGE_CODES)) return nullptr; + return &NP_LANGUAGE_CODES[language]; +} + +void CopyText(char* out, const char* text, std::size_t size) { + std::strncpy(out, text, size - 1); + out[size - 1] = '\0'; +} + +std::atomic g_debugIds {0}; + +constexpr int NP_UTIL_ERROR_BUFFER_TOO_SMALL = static_cast(0x80550602); +constexpr int NP_UTIL_ERROR_OUT_OF_RANGE = static_cast(0x8055060A); +constexpr std::size_t NP_TITLE_ID_SIZE = 13; +constexpr std::uint8_t NP_TITLE_ID_INDEX_MAX = 99; +constexpr std::size_t NP_DUMP_LINE_BYTES = 16; + +bool IsAsciiLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +bool IsOnlineIdText(const char* text) { + const std::size_t length = std::strlen(text); + if (length < NP_ONLINE_ID_MIN || length > NP_ONLINE_ID_MAX || !IsAsciiLetter(text[0])) return false; + return std::all_of(text + 1, text + length, [](char c) { + return IsAsciiLetter(c) || (c >= '0' && c <= '9') || c == '_' || c == '-'; + }); +} + +constexpr int NP_ERROR_BUFFER_TOO_SMALL = static_cast(0x80558008); +constexpr std::size_t NP_SDK_VERSION_SIZE = 8; +constexpr std::size_t NP_RFC3339_BUFFER_MIN = 0x80; +constexpr std::size_t NP_USER_ID_LIST_SIZE = 20; +constexpr int USER_ID_INVALID = -1; +constexpr KernelClockid NP_SYSTEM_CLOCK = 4; + +struct SystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; +static_assert(sizeof(SystemSwVersion) == 0x28); + +std::uint32_t SystemVersion() { + SystemSwVersion version {}; + version.size = sizeof(version); + sceKernelGetSystemSwVersion_nid_postfix(&version); + return version.version; +} + +int BcdPair(std::uint32_t value, int shift) { + return static_cast((value >> (shift + 4)) & 0xF) * 10 + static_cast((value >> shift) & 0xF); +} + + +constexpr int NP_ERROR_OUT_OF_MEMORY = static_cast(0x80550005); +constexpr int NP_ERROR_NULL_OBJECT = static_cast(0x80558005); +constexpr int NP_ERROR_HEAP_CREATE = static_cast(0x8055800A); +constexpr std::uint64_t NP_KERNEL_MEMORY_ALIGNMENT = 0x4000; +constexpr int NP_KERNEL_MEMORY_PROTECTION = 3; +constexpr std::size_t NP_GLOBAL_HEAP_SIZE = 0x80000; +constexpr char NP_GLOBAL_HEAP_NAME[] = "NpCommonGlobalHeap"; +constexpr std::size_t NP_IPC_CLIENT_HEAP_SIZE = 0x10000; +constexpr char NP_IPC_CLIENT_HEAP_NAME[] = "NpCommonIpcClientHeap"; + +void* APS5_VABI HeapAllocate(std::size_t size, void* context) { + auto* heap = static_cast(context); + return heap->mspace != nullptr ? sceLibcMspaceMalloc_nid_postfix(heap->mspace, size) : nullptr; +} + +void* APS5_VABI HeapReallocate(void* pointer, std::size_t size, void* context) { + auto* heap = static_cast(context); + return heap->mspace != nullptr ? sceLibcMspaceRealloc_nid_postfix(heap->mspace, pointer, size) : nullptr; +} + +void APS5_VABI HeapRelease(void* pointer, void* context) { + auto* heap = static_cast(context); + if (heap->mspace != nullptr && pointer != nullptr) sceLibcMspaceFree_nid_postfix(heap->mspace, pointer); +} + +void* APS5_VABI MemoryHeapAllocate(std::size_t size, void* context) { + return HeapAllocate(size, &static_cast(context)->heap); +} + +void* APS5_VABI MemoryHeapReallocate(void* pointer, std::size_t size, void* context) { + return HeapReallocate(pointer, size, &static_cast(context)->heap); +} + +void APS5_VABI MemoryHeapRelease(void* pointer, void* context) { + HeapRelease(pointer, &static_cast(context)->heap); +} + +std::uint64_t AlignKernelMemory(std::uint64_t size) { + const std::uint64_t remainder = size & (NP_KERNEL_MEMORY_ALIGNMENT - 1); + return remainder == 0 ? size : size + NP_KERNEL_MEMORY_ALIGNMENT - remainder; +} + +int MapKernelMemory(NpKernelMemory* memory, std::uint64_t size, const char* name) { + memory->reserved = 0; + memory->size = size; + return sceKernelMapNamedFlexibleMemory(&memory->address, size, NP_KERNEL_MEMORY_PROTECTION, 0, name); +} + +void UnmapKernelMemory(NpKernelMemory* memory) { + if (memory->address != nullptr) sceKernelMunmap(reinterpret_cast(memory->address), memory->size); + memory->address = nullptr; +} + +int InitHeap(NpHeap* heap, void* base, std::size_t size, const char* name) { + heap->mspace = sceLibcMspaceCreate_nid_postfix(name, base, size, 0); + return heap->mspace != nullptr ? 0 : NP_ERROR_HEAP_CREATE; +} + +void DestroyHeap(NpHeap* heap) { + if (heap->mspace == nullptr) return; + sceLibcMspaceDestroy_nid_postfix(heap->mspace); + heap->mspace = nullptr; +} + +int InitMemoryHeap(NpMemoryHeap* heap, std::uint64_t size, const char* name) { + if (heap == nullptr) return NP_ERROR_NULL_OBJECT; + int result = MapKernelMemory(&heap->memory, AlignKernelMemory(size), name); + if (result < 0) return result; + result = InitHeap(&heap->heap, heap->memory.address, heap->memory.size, name); + if (result >= 0) return result; + UnmapKernelMemory(&heap->memory); + *heap = {}; + return result; +} + +void DestroyMemoryHeap(NpMemoryHeap* heap) { + DestroyHeap(&heap->heap); + UnmapKernelMemory(&heap->memory); +} + +NpAllocator MemoryHeapAllocator(NpMemoryHeap* heap) { + return {MemoryHeapAllocate, MemoryHeapReallocate, MemoryHeapRelease, heap}; +} + +constexpr int NP_ERROR_NOT_INITIALIZED = static_cast(0x80558001); +constexpr int NP_ERROR_ALREADY_INITIALIZED = static_cast(0x80558002); +constexpr int NP_ERROR_ALREADY_QUEUED = static_cast(0x80558006); +constexpr std::uint64_t NP_CALLOUT_MAX_WAIT = 0xFFFFFFFF; + +std::mutex g_randomMutex; +std::mt19937 g_random {std::random_device {}()}; + +NpMemoryHeap g_globalHeap {}; +NpAllocator g_globalAllocator {}; +NpAllocatorEx g_globalAllocatorEx {}; +std::atomic g_archReferences {0}; +std::mutex g_ipcClientMutex; +bool g_ipcClientInitialized = false; +NpMemoryHeap g_ipcClientHeap {}; + +constexpr int KERNEL_ERROR_EBUSY = static_cast(0x80020010); +constexpr int KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast(0x80558009); +constexpr int NP_ERROR_TIMEOUT = static_cast(0x8055800B); +constexpr int NP_ERROR_EVENT_FLAG_BUSY = static_cast(0x8055800C); +constexpr int NP_ERROR_SEMA_BUSY = static_cast(0x8055800D); +constexpr int NP_ERROR_MUTEX_BUSY = static_cast(0x8055800F); +constexpr std::uint32_t NP_MUTEX_FLAG_RECURSIVE = 1; +constexpr int PTHREAD_MUTEX_TYPE_RECURSIVE = 2; +constexpr std::uint32_t NP_SEMA_FLAG_PRIORITY = 1; +constexpr std::uint32_t KERNEL_SEMA_ATTR_FIFO = 1; +constexpr std::uint32_t KERNEL_SEMA_ATTR_PRIORITY_SHIFT = 8; +constexpr int NP_THREAD_EXPLICIT_SCHED = 0; +constexpr int NP_THREAD_SCHED_POLICY = 1; + +int NpResult(int result) { + return result < 0 ? result : 0; +} + +int NpBusyResult(int result, int busy) { + return result == KERNEL_ERROR_EBUSY ? busy : NpResult(result); +} + +int InitMutex(PthreadMutex* mutex, const char* name, std::uint32_t flags) { + PthreadMutexattr attr {}; + int result = scePthreadMutexattrInit(&attr); + if (result < 0) return result; + if ((flags & NP_MUTEX_FLAG_RECURSIVE) != 0) result = scePthreadMutexattrSettype(&attr, PTHREAD_MUTEX_TYPE_RECURSIVE); + if (result >= 0) result = scePthreadMutexInit(mutex, &attr, name); + scePthreadMutexattrDestroy(&attr); + return NpResult(result); +} + +int InitCond(PthreadCond* cond, const char* name) { + PthreadCondattr attr {}; + const int result = scePthreadCondattrInit(&attr); + if (result < 0) return result; + const int created = scePthreadCondInit(cond, &attr, name); + scePthreadCondattrDestroy(&attr); + return created; +} + +} + +extern "C" { + +int APS5_VABI sceNpAtomicCas32(std::int32_t* value, std::int32_t expected, std::int32_t desired) { + std::atomic_ref(*value).compare_exchange_strong(expected, desired); + return expected; +} + +int APS5_VABI sceNpAtomicInc32(std::int32_t* value) { + return std::atomic_ref(*value).fetch_add(1); +} + +int APS5_VABI sceNpAtomicDec32(std::int32_t* value) { + return std::atomic_ref(*value).fetch_sub(1); +} + + + + + + + + + + + + + + +int APS5_VABI sceNpCondSignalAll(PthreadCond* cond) { + return scePthreadCondBroadcast(cond); +} + +int APS5_VABI sceNpCondSignalTo(PthreadCond* cond, Pthread thread) { + return scePthreadCondSignalto(cond, thread); +} + +int APS5_VABI sceNpCondWait(PthreadCond* cond, PthreadMutex* mutex) { + return scePthreadCondWait(cond, mutex); +} + + +int APS5_VABI sceNpLwCondInit(PthreadCond* cond, const char* name) { + return InitCond(cond, name); +} + +int APS5_VABI sceNpLwCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); +} + +int APS5_VABI sceNpLwCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); +} + +int APS5_VABI sceNpLwCondSignalAll(PthreadCond* cond) { + return scePthreadCondBroadcast(cond); +} + +int APS5_VABI sceNpLwCondSignalTo(PthreadCond* cond, Pthread thread) { + return scePthreadCondSignalto(cond, thread); +} + +int APS5_VABI sceNpLwCondWait(PthreadCond* cond, PthreadMutex* mutex) { + return scePthreadCondWait(cond, mutex); +} + +int APS5_VABI sceNpCreateEventFlag(KernelEventFlag* flag, const char* name, std::uint32_t attr, std::uint64_t pattern) { + return NpResult(sceKernelCreateEventFlag(flag, name, attr, pattern, nullptr)); +} + +int APS5_VABI sceNpDeleteEventFlag(KernelEventFlag flag) { + return NpResult(sceKernelDeleteEventFlag(flag)); +} + +int APS5_VABI sceNpOpenEventFlag(KernelEventFlag* flag, const char* name) { + return NpResult(sceKernelOpenEventFlag(flag, name)); +} + +int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern) { + return NpResult(sceKernelSetEventFlag(flag, pattern)); +} + +int APS5_VABI sceNpClearEventFlag(KernelEventFlag flag, std::uint64_t pattern) { + return NpResult(sceKernelClearEventFlag(flag, pattern)); +} + +int APS5_VABI sceNpPollEventFlag(KernelEventFlag flag, std::uint64_t pattern, std::uint32_t mode, std::uint64_t* result) { + return NpBusyResult(sceKernelPollEventFlag(flag, pattern, mode, result), NP_ERROR_EVENT_FLAG_BUSY); +} + +int APS5_VABI sceNpCancelEventFlag(KernelEventFlag flag, std::uint64_t pattern, int* waiting) { + return NpResult(sceKernelCancelEventFlag(flag, pattern, waiting)); +} + +int APS5_VABI sceNpCreateSema(KernelSema* sema, std::uint32_t flags, int initial, int maximum, const char* name) { + const std::uint32_t attr = KERNEL_SEMA_ATTR_FIFO + ((flags & NP_SEMA_FLAG_PRIORITY) << KERNEL_SEMA_ATTR_PRIORITY_SHIFT); + return NpResult(sceKernelCreateSema(sema, name, attr, initial, maximum, nullptr)); +} + +int APS5_VABI sceNpDeleteSema(KernelSema sema) { + return NpResult(sceKernelDeleteSema(sema)); +} + +int APS5_VABI sceNpPollSema(KernelSema sema) { + return NpBusyResult(sceKernelPollSema(sema, 1), NP_ERROR_SEMA_BUSY); +} + +int APS5_VABI sceNpSignalSema(KernelSema sema) { + return NpResult(sceKernelSignalSema(sema, 1)); +} + +int APS5_VABI sceNpCreateThread(Pthread* thread, PthreadEntry entry, void* arg, int priority, std::size_t stackSize, KernelCpumask affinity, const char* name) { + PthreadAttr attr {}; + int result = scePthreadAttrInit(&attr); + if (result < 0) return result; + result = scePthreadAttrSetstacksize(&attr, stackSize); + if (result < 0) { + scePthreadAttrDestroy(&attr); + return result; + } + if (priority != 0) { + result = scePthreadAttrSetinheritsched(&attr, NP_THREAD_EXPLICIT_SCHED); + if (result >= 0) result = scePthreadAttrSetschedpolicy(&attr, NP_THREAD_SCHED_POLICY); + if (result >= 0) { + KernelSchedParam param {}; + param.sched_priority = priority; + result = scePthreadAttrSetschedparam(&attr, ¶m); + } + } + if (result >= 0 && affinity != 0) result = scePthreadAttrSetaffinity(&attr, affinity); + if (result >= 0) result = scePthreadCreate(thread, &attr, entry, arg, name); + scePthreadAttrDestroy(&attr); + return result; +} + + +int APS5_VABI sceNpThreadGetId(Pthread* thread) { + if (thread == nullptr) return NP_ERROR_INVALID_ARGUMENT; + *thread = scePthreadSelf(); + return 0; +} + +int APS5_VABI sceNpGetProcessId(void) { + return getpid_nid_postfix(); +} + +[[noreturn]] void APS5_VABI sceNpPanic(void) { + std::abort(); +} + +int APS5_VABI sceNpDbgDumpText(const char* text, int length) { + return std::printf("%.*s", length, text); +} + +int APS5_VABI sceNpIpc2ClientInit(void) { + std::lock_guard lock(g_ipcClientMutex); + if (g_ipcClientInitialized) return NP_ERROR_IPC_ALREADY_INITIALIZED; + const int result = InitMemoryHeap(&g_ipcClientHeap, NP_IPC_CLIENT_HEAP_SIZE, NP_IPC_CLIENT_HEAP_NAME); + if (result < 0) return result; + g_ipcClientInitialized = true; + return 0; +} + +int APS5_VABI sceNpIpc2ClientTerm(void) { + std::lock_guard lock(g_ipcClientMutex); + if (!g_ipcClientInitialized) return NP_ERROR_IPC_NOT_INITIALIZED; + DestroyMemoryHeap(&g_ipcClientHeap); + g_ipcClientInitialized = false; + return 0; +} + +int APS5_VABI sceNpArchInit(void) { + if (g_archReferences.fetch_add(1) != 0) return 0; + const int result = InitMemoryHeap(&g_globalHeap, NP_GLOBAL_HEAP_SIZE, NP_GLOBAL_HEAP_NAME); + if (result < 0) g_archReferences.fetch_sub(1); + return result < 0 ? result : 0; +} + +void APS5_VABI sceNpArchTerm(void) { + if (g_archReferences.fetch_sub(1) == 1) DestroyMemoryHeap(&g_globalHeap); +} + +int APS5_VABI sceNpHeapInit(NpHeap* heap, void* base, std::size_t size, const char* name) { + return InitHeap(heap, base, size, name); +} + +void APS5_VABI sceNpHeapDestroy(NpHeap* heap) { + DestroyHeap(heap); +} + +void APS5_VABI sceNpHeapGetAllocator(NpHeap* heap, NpAllocator* allocator) { + *allocator = {HeapAllocate, HeapReallocate, HeapRelease, heap}; +} + +int APS5_VABI sceNpHeapGetStat(const NpHeap* heap, NpHeapStat* stat) { + MallocManagedSize stats {}; + stats.size = sizeof(stats); + stats.version = 1; + const int result = sceLibcMspaceMallocStats_nid_postfix(heap->mspace, &stats); + if (result == 0) *stat = {stats.maxSystemSize, stats.maxInuseSize, stats.currentInuseSize}; + return result; +} + +void APS5_VABI sceNpHeapShowStat(const NpHeap* heap) { + NpHeapStat stat {}; + if (sceNpHeapGetStat(heap, &stat) != 0) { + std::printf("[NpCommon] heap statistics unavailable\n"); + return; + } + std::printf("[NpCommon] heap: system %llu bytes, peak in use %llu bytes, in use %llu bytes\n", + static_cast(stat.maxSystemSize), static_cast(stat.maxInuseSize), + static_cast(stat.currentInuseSize)); +} + +int APS5_VABI sceNpMemoryHeapInit(NpMemoryHeap* heap, std::uint64_t size, const char* name) { + return InitMemoryHeap(heap, size, name); +} + +void APS5_VABI sceNpMemoryHeapDestroy(NpMemoryHeap* heap) { + DestroyMemoryHeap(heap); +} + +void APS5_VABI sceNpMemoryHeapGetAllocator(NpMemoryHeap* heap, NpAllocator* allocator) { + *allocator = MemoryHeapAllocator(heap); +} + +void APS5_VABI sceNpMemoryHeapGetAllocatorEx(NpMemoryHeap* heap, int outOfMemoryError, NpAllocatorEx* allocator) { + *allocator = {MemoryHeapAllocator(heap), outOfMemoryError}; +} + +void APS5_VABI sceNpGlobalHeapGetAllocator(NpAllocator* allocator) { + *allocator = MemoryHeapAllocator(&g_globalHeap); +} + +void APS5_VABI sceNpGlobalHeapGetAllocatorEx(NpAllocatorEx* allocator) { + *allocator = {MemoryHeapAllocator(&g_globalHeap), NP_ERROR_OUT_OF_MEMORY}; +} + +NpAllocator* APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void) { + g_globalAllocator = MemoryHeapAllocator(&g_globalHeap); + return &g_globalAllocator; +} + +NpAllocatorEx* APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { + g_globalAllocatorEx = {MemoryHeapAllocator(&g_globalHeap), NP_ERROR_OUT_OF_MEMORY}; + return &g_globalAllocatorEx; +} + +int APS5_VABI sceNpAllocateKernelMemoryNoAlignment(std::uint64_t size, const char* name, void** address, NpKernelMemory* memory) { + if (memory == nullptr) return NP_ERROR_NULL_OBJECT; + const int result = MapKernelMemory(memory, size, name); + if (result >= 0 && address != nullptr) *address = memory->address; + return result; +} + +int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(std::uint64_t size, const char* name, void** address, NpKernelMemory* memory) { + return sceNpAllocateKernelMemoryNoAlignment(AlignKernelMemory(size), name, address, memory); +} + +int APS5_VABI sceNpFreeKernelMemory(NpKernelMemory* memory) { + if (memory == nullptr) return NP_ERROR_NULL_OBJECT; + UnmapKernelMemory(memory); + return 0; +} + + +void APS5_VABI sceNpGetSdkVersionUInt(std::uint32_t* version) { + const std::uint32_t value = SystemVersion(); + if (version != nullptr) *version = value; +} + + +int APS5_VABI sceNpRtcConvertToPosixTime(const RtcTick* tick, std::int64_t* seconds) { + if (tick->tick == 0) return NP_ERROR_INVALID_ARGUMENT; + RtcDateTime time {}; + const int result = sceRtcSetTick(&time, tick); + if (result < 0) return result; + return NpResult(sceRtcGetTime_t(&time, seconds)); +} + +int APS5_VABI sceNpRtcFormatRFC3339(char* text, std::size_t size, const RtcTick* utc, int timeZoneMinutes) { + if (size < NP_RFC3339_BUFFER_MIN) return NP_ERROR_BUFFER_TOO_SMALL; + const int result = sceRtcFormatRFC3339(text, utc, timeZoneMinutes); + if (result < 0) return result; + text[size - 1] = 0; + return 0; +} + +int APS5_VABI sceNpUserGetUserIdList(int* users, std::size_t capacity, int* count) { + if (capacity < NP_USER_ID_LIST_SIZE) return NP_ERROR_INVALID_ARGUMENT; + int registered[NP_USER_ID_LIST_SIZE]; + std::fill(std::begin(registered), std::end(registered), USER_ID_INVALID); + static_assert(sizeof(UserServiceRegisteredUserIdList) <= sizeof(registered)); + const int result = sceUserServiceGetRegisteredUserIdList(reinterpret_cast(registered)); + if (result < 0) return result; + int found = 0; + for (const int user : registered) + if (user != USER_ID_INVALID) users[found++] = user; + *count = found; + return 0; +} + +int APS5_VABI sceNpRtcParseRFC3339(RtcTick* utc, const char* text) { + return sceRtcParseRFC3339(utc, text); +} + +static std::uint64_t ClockUsec() { + std::int64_t usec = 0; + sceNpGetSystemClockUsec(&usec); + return static_cast(usec); +} + +static void* APS5_VABI CalloutThread(void* arg) { + auto* context = static_cast(arg); + sceNpMutexLock(&context->mutex); + for (;;) { + const std::uint64_t now = ClockUsec(); + NpCallout* head = context->pending; + if (head != nullptr && head->deadline <= now) { + context->pending = head->next; + sceNpMutexUnlock(&context->mutex); + head->handler(head->arg); + sceNpMutexLock(&context->mutex); + continue; + } + if (context->stopping > 0) break; + const std::uint64_t wait = head != nullptr ? std::min(head->deadline - now, NP_CALLOUT_MAX_WAIT) : 0; + sceNpCondTimedwait(&context->cond, &context->mutex, static_cast(wait)); + if (context->stopping > 0) break; + } + sceNpMutexUnlock(&context->mutex); + return nullptr; +} + +int APS5_VABI sceNpCalloutInitCtx(NpCalloutContext* context, const char* name, std::size_t stackSize, int priority, + KernelCpumask affinity) { + if (context->initialized > 0) return NP_ERROR_ALREADY_INITIALIZED; + context->stopping = 0; + context->pending = nullptr; + int result = sceNpMutexInit(&context->mutex, name, 1); + if (result < 0) return result; + result = sceNpCondInit(&context->cond, name, 0); + if (result >= 0) { + result = sceNpCreateThread(&context->thread, CalloutThread, context, priority, stackSize, affinity, name); + if (result >= 0) { + context->initialized = 1; + return 0; + } + sceNpCondDestroy(&context->cond); + } + sceNpMutexDestroy(&context->mutex); + return result; +} + +void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context) { + if (context->initialized == 0) return; + sceNpMutexLock(&context->mutex); + context->stopping = 1; + sceNpCondSignal(&context->cond); + sceNpMutexUnlock(&context->mutex); + sceNpJoinThread(context->thread, nullptr); + sceNpCondDestroy(&context->cond); + sceNpMutexDestroy(&context->mutex); + context->initialized = 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx64(NpCalloutContext* context, NpCallout* callout, std::uint64_t usec, + void (APS5_VABI *handler)(void* arg), void* arg) { + if (context->initialized == 0) return NP_ERROR_NOT_INITIALIZED; + *callout = {}; + callout->deadline = ClockUsec() + usec; + callout->handler = handler; + callout->arg = arg; + sceNpMutexLock(&context->mutex); + for (NpCallout* entry = context->pending; entry != nullptr; entry = entry->next) { + if (entry == callout) { + sceNpMutexUnlock(&context->mutex); + return NP_ERROR_ALREADY_QUEUED; + } + } + NpCallout** link = &context->pending; + while (*link != nullptr && callout->deadline > (*link)->deadline) link = &(*link)->next; + callout->next = *link; + *link = callout; + sceNpCondSignal(&context->cond); + sceNpMutexUnlock(&context->mutex); + return 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx(NpCalloutContext* context, NpCallout* callout, std::uint32_t usec, + void (APS5_VABI *handler)(void* arg), void* arg) { + return sceNpCalloutStartOnCtx64(context, callout, usec, handler, arg); +} + +int APS5_VABI sceNpCalloutStopOnCtx(NpCalloutContext* context, NpCallout* callout, int* wasPending) { + if (context->initialized == 0) return NP_ERROR_NOT_INITIALIZED; + sceNpMutexLock(&context->mutex); + int removed = 0; + for (NpCallout** link = &context->pending; *link != nullptr; link = &(*link)->next) { + if (*link != callout) continue; + *link = callout->next; + removed = 1; + break; + } + if (wasPending != nullptr) *wasPending = removed; + sceNpMutexUnlock(&context->mutex); + return 0; +} + +std::uint32_t APS5_VABI sceNpGetRandom(void) { + std::lock_guard lock(g_randomMutex); + return static_cast(g_random()); +} + + +int APS5_VABI sceNpUtilCmpAccountId(std::uint64_t left, std::uint64_t right) { + return left != right ? 1 : 0; +} + +bool APS5_VABI sceNpIntIsOnlineIdString(const char* text) { + return text != nullptr && IsOnlineIdText(text); +} + + +int APS5_VABI sceNpUtilGetNpLanguageCode(int language, char* code) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + if (codes == nullptr || code == nullptr) return NP_ERROR_INVALID_ARGUMENT_UTIL; + CopyText(code, codes->code, NP_LANGUAGE_CODE_SIZE); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode2(int language, char* tag) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + if (codes == nullptr || tag == nullptr) return NP_ERROR_INVALID_ARGUMENT_UTIL; + CopyText(tag, codes->tag, NP_LANGUAGE_CODE2_SIZE); + return 0; +} + +const char* APS5_VABI sceNpUtilGetNpLanguageCodeStr(int language) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + return codes != nullptr ? codes->code : nullptr; +} + +const char* APS5_VABI sceNpUtilGetNpLanguageCode2Str(int language) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + return codes != nullptr ? codes->tag : nullptr; +} + +std::uint32_t APS5_VABI sceNpDbgAssignDebugId(void) { + std::uint32_t id = g_debugIds.fetch_add(1) + 1; + if (id == 0) id = g_debugIds.fetch_add(1) + 1; + return id; +} + +int APS5_VABI sceNpUtilBuildTitleId(const NpTitleIdParts* parts, char* titleId, std::size_t size) { + if (parts == nullptr || titleId == nullptr) return NP_ERROR_INVALID_ARGUMENT_UTIL; + if (size < NP_TITLE_ID_SIZE) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (parts->index > NP_TITLE_ID_INDEX_MAX) return NP_UTIL_ERROR_OUT_OF_RANGE; + std::memcpy(titleId, parts->id, sizeof(parts->id)); + titleId[9] = '_'; + titleId[10] = static_cast('0' + parts->index / 10); + titleId[11] = static_cast('0' + parts->index % 10); + titleId[12] = '\0'; + return 0; +} + +void APS5_VABI sceNpDbgDumpBinary(const void* data, std::size_t size) { + const auto* bytes = static_cast(data); + for (std::size_t line = 0; line < size; line += NP_DUMP_LINE_BYTES) { + const std::size_t count = std::min(NP_DUMP_LINE_BYTES, size - line); + for (std::size_t i = 0; i < NP_DUMP_LINE_BYTES; ++i) { + if (i < count) { + std::printf("%02x", bytes[line + i]); + } else { + std::printf(" "); + } + if ((i & 1) != 0) std::putchar(' '); + } + std::putchar(' '); + for (std::size_t i = 0; i < count; ++i) { + const std::uint8_t c = bytes[line + i]; + std::putchar(c >= 0x20 && c <= 0x7E ? c : '.'); + } + std::putchar('\n'); + } + std::putchar('\n'); +} + +} diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp new file mode 100644 index 0000000000..d5f70019e9 --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -0,0 +1,629 @@ +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" + +namespace { + +constexpr int NP_ERROR_INVALID_FORMAT = static_cast(0x80558003); +constexpr int NP_ERROR_INVALID_CHARACTER = static_cast(0x80558004); +constexpr int NP_ERROR_BUFFER_TOO_SMALL = static_cast(0x80558008); +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast(0x80558009); +constexpr int NP_ERROR_OUT_OF_RANGE = static_cast(0x8055800E); + +constexpr std::uint32_t NP_FORMAT_UPPERCASE = 1; +constexpr std::uint32_t NP_FORMAT_PREFIX = 2; +constexpr std::uint32_t NP_FORMAT_SIGN = 4; +constexpr std::uint32_t NP_RADIX_MAX = 36; + +constexpr char BASE64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +constexpr char BASE64URL_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +constexpr char BASE64_PAD = '='; + +int Base64Value(char c, const char* alphabet) { + if (c == '\0') return -1; + const char* found = std::strchr(alphabet, c); + return found != nullptr ? static_cast(found - alphabet) : -1; +} + +struct Base64Block { + std::uint32_t bits; + int bytes; +}; + +int DecodeLastBlock(const char* src, std::size_t length, const char* alphabet, Base64Block* block) { + char c[4] = {BASE64_PAD, BASE64_PAD, BASE64_PAD, BASE64_PAD}; + std::memcpy(c, src, length); + const int v0 = Base64Value(c[0], alphabet); + const int v1 = Base64Value(c[1], alphabet); + if (v0 < 0 || v1 < 0) return NP_ERROR_INVALID_CHARACTER; + block->bits = (static_cast(v0) << 18) | (static_cast(v1) << 12); + if (c[2] == BASE64_PAD) { + if (c[3] != BASE64_PAD) return NP_ERROR_INVALID_CHARACTER; + block->bytes = 1; + return 0; + } + const int v2 = Base64Value(c[2], alphabet); + if (v2 < 0) return NP_ERROR_INVALID_CHARACTER; + block->bits |= static_cast(v2) << 6; + if (c[3] == BASE64_PAD) { + block->bytes = 2; + return 0; + } + const int v3 = Base64Value(c[3], alphabet); + if (v3 < 0) return NP_ERROR_INVALID_CHARACTER; + block->bits |= static_cast(v3); + block->bytes = 3; + return 0; +} + +int Base64Decode(const char* src, std::size_t length, std::uint8_t* dst, std::size_t capacity, const char* alphabet, + bool padded) { + if (padded && (length & 3) != 0) return NP_ERROR_INVALID_FORMAT; + int decoded = 0; + while (length > 4) { + std::uint32_t bits = 0; + for (int i = 0; i < 4; ++i) { + const int value = Base64Value(src[i], alphabet); + if (value < 0) return NP_ERROR_INVALID_CHARACTER; + bits = (bits << 6) | static_cast(value); + } + if (dst != nullptr) { + if (capacity < 3) return NP_ERROR_INVALID_FORMAT; + capacity -= 3; + *dst++ = static_cast(bits >> 16); + *dst++ = static_cast(bits >> 8); + *dst++ = static_cast(bits); + } + decoded += 3; + src += 4; + length -= 4; + } + if (padded && length != 4) return NP_ERROR_INVALID_FORMAT; + Base64Block block {}; + const int result = DecodeLastBlock(src, length, alphabet, &block); + if (result < 0) return result; + if (dst != nullptr) { + if (capacity < static_cast(block.bytes)) return NP_ERROR_INVALID_FORMAT; + for (int i = 0; i < block.bytes; ++i) dst[i] = static_cast(block.bits >> (16 - 8 * i)); + } + return decoded + block.bytes; +} + +int Base64Encode(const std::uint8_t* src, char* dst, std::size_t length, const char* alphabet, bool padded) { + char* out = dst; + for (; length >= 3; length -= 3, src += 3) { + const std::uint32_t bits = (src[0] << 16) | (src[1] << 8) | src[2]; + for (int shift = 18; shift >= 0; shift -= 6) *out++ = alphabet[(bits >> shift) & 0x3F]; + } + if (length != 0) { + const std::uint32_t bits = (src[0] << 16) | (length == 2 ? src[1] << 8 : 0); + *out++ = alphabet[(bits >> 18) & 0x3F]; + *out++ = alphabet[(bits >> 12) & 0x3F]; + if (length == 2) *out++ = alphabet[(bits >> 6) & 0x3F]; + if (padded) { + if (length == 1) *out++ = BASE64_PAD; + *out++ = BASE64_PAD; + } + } + return static_cast(out - dst); +} + +int HexValue(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +char HexDigit(std::uint32_t value, bool uppercase) { + if (value < 10) return static_cast('0' + value); + return static_cast((uppercase ? 'A' : 'a') + value - 10); +} + +int ParseHex(const char* hex, std::size_t length, std::uint8_t* out, std::size_t capacity) { + if ((length & 1) != 0) return NP_ERROR_INVALID_ARGUMENT; + if (capacity < length / 2) return NP_ERROR_BUFFER_TOO_SMALL; + for (std::size_t i = 0; i < length; i += 2) { + const int high = HexValue(hex[i]); + const int low = HexValue(hex[i + 1]); + if (high < 0 || low < 0) return NP_ERROR_INVALID_ARGUMENT; + out[i / 2] = static_cast((high << 4) | low); + } + return 0; +} + +int FormatInteger(char* buf, std::size_t size, std::uint64_t magnitude, char sign, std::uint32_t radix, + std::uint32_t flags, std::size_t* length) { + if (radix < 1 || radix > NP_RADIX_MAX) return NP_ERROR_INVALID_ARGUMENT; + char prefix[3] = {}; + std::size_t prefixLength = 0; + if (sign != '\0') prefix[prefixLength++] = sign; + if ((flags & NP_FORMAT_PREFIX) != 0 && (radix == 8 || radix == 16)) { + prefix[prefixLength++] = '0'; + if (radix == 16) prefix[prefixLength++] = (flags & NP_FORMAT_UPPERCASE) != 0 ? 'X' : 'x'; + } + std::size_t digits = 1; + if (radix > 1) { + for (std::uint64_t rest = magnitude; rest >= radix; rest /= radix) ++digits; + } + if (buf != nullptr) { + if (size < prefixLength + digits + 1) return NP_ERROR_BUFFER_TOO_SMALL; + std::memcpy(buf, prefix, prefixLength); + char* end = buf + prefixLength + digits; + *end = '\0'; + std::uint64_t rest = magnitude; + for (std::size_t i = 0; i < digits; ++i) { + *--end = HexDigit(static_cast(rest % radix), (flags & NP_FORMAT_UPPERCASE) != 0); + rest /= radix; + } + } + if (length != nullptr) *length = prefixLength + digits; + return 0; +} + +std::uint64_t Magnitude(std::int64_t value) { + return value < 0 ? 0 - static_cast(value) : static_cast(value); +} + +char PlusSign(bool positive, std::uint32_t flags) { + return positive && (flags & NP_FORMAT_SIGN) != 0 ? '+' : '\0'; +} + +template +int ParseInteger(const char* str, std::size_t* parsedLength, Parse parse, bool* saturated) { + char* end = nullptr; + errno = 0; + parse(str, &end); + if (end == str) return NP_ERROR_OUT_OF_RANGE; + if (*saturated && errno == ERANGE) return NP_ERROR_OUT_OF_RANGE; + if (parsedLength != nullptr) *parsedLength = static_cast(end - str); + return 0; +} + +int CopyToBuffer(char* dst, std::size_t size, const char* src, std::size_t length) { + if (size < length + 1) return NP_ERROR_BUFFER_TOO_SMALL; + std::strncpy(dst, src, size - 1); + dst[size - 1] = '\0'; + return 0; +} + +constexpr int NP_UTIL_ERROR_INVALID_ARGUMENT = static_cast(0x80550601); +constexpr int NP_UTIL_ERROR_BUFFER_TOO_SMALL = static_cast(0x80550602); +constexpr int NP_UTIL_ERROR_INVALID_CHARACTER = static_cast(0x80550608); +constexpr std::size_t JSON_ESCAPE_MARGIN = 7; +constexpr std::size_t JSON_UNICODE_ESCAPE_LENGTH = 6; +constexpr std::size_t JSON_SURROGATE_PAIR_LENGTH = 12; +constexpr std::uint32_t SURROGATE_MASK = 0xFC00; +constexpr std::uint32_t HIGH_SURROGATE = 0xD800; +constexpr std::uint32_t LOW_SURROGATE = 0xDC00; +constexpr char URL_HEX_DIGITS[] = "0123456789ABCDEF"; + +const char* JsonShortEscape(unsigned char c) { + switch (c) { + case '\b': return "\\b"; + case '\t': return "\\t"; + case '\n': return "\\n"; + case '\f': return "\\f"; + case '\r': return "\\r"; + case '"': return "\\\""; + case '/': return "\\/"; + case '\\': return "\\\\"; + default: return nullptr; + } +} + +char JsonShortUnescape(char c) { + switch (c) { + case '"': return '"'; + case '\\': return '\\'; + case '/': return '/'; + case 'b': return '\b'; + case 'f': return '\f'; + case 'n': return '\n'; + case 'r': return '\r'; + case 't': return '\t'; + default: return '\0'; + } +} + +int ParseUtf16Unit(const char* text, std::uint32_t* unit) { + *unit = 0; + for (int i = 0; i < 4; ++i) { + const int value = HexValue(text[i]); + if (value < 0) { + *unit = 0; + return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } + *unit = (*unit << 4) | static_cast(value); + } + return 0; +} + +std::size_t EncodeUtf8(std::uint32_t codePoint, char* out) { + if (codePoint <= 0x7F) { + out[0] = static_cast(codePoint); + return 1; + } + if (codePoint <= 0x7FF) { + out[0] = static_cast(0xC0 | (codePoint >> 6)); + out[1] = static_cast(0x80 | (codePoint & 0x3F)); + return 2; + } + if (codePoint <= 0xFFFF) { + out[0] = static_cast(0xE0 | (codePoint >> 12)); + out[1] = static_cast(0x80 | ((codePoint >> 6) & 0x3F)); + out[2] = static_cast(0x80 | (codePoint & 0x3F)); + return 3; + } + out[0] = static_cast(0xF0 | (codePoint >> 18)); + out[1] = static_cast(0x80 | ((codePoint >> 12) & 0x3F)); + out[2] = static_cast(0x80 | ((codePoint >> 6) & 0x3F)); + out[3] = static_cast(0x80 | (codePoint & 0x3F)); + return 4; +} + +std::uint32_t CombineSurrogates(std::uint32_t high, std::uint32_t low) { + return 0x10000 + (((high & 0x3FF) << 10) | (low & 0x3FF)); +} + +bool IsContinuation(char c) { + return (static_cast(c) & 0xC0) == 0x80; +} + +std::size_t Utf8SequenceLength(unsigned char lead) { + if (lead >= 0xC2 && lead <= 0xDF) return 2; + if ((lead & 0xF0) == 0xE0) return 3; + if ((lead & 0xF8) == 0xF0) return 4; + return 0; +} + +bool IsValidSequence(const char* text, std::size_t length) { + for (std::size_t i = 1; i < length; ++i) { + if (!IsContinuation(text[i])) return false; + } + return true; +} + +bool IsAllowedTextByte(unsigned char c) { + return (c >= 0x20 && c != 0x7F) || c == '\t' || c == '\n' || c == '\r'; +} + +bool IsUrlSafe(unsigned char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' + || c == '*'; +} + +} // namespace + +extern "C" { + +int APS5_VABI sceNpBase64GetDecodeSize(const char* src, std::size_t length) { + return Base64Decode(src, length, nullptr, 0, BASE64_ALPHABET, true); +} + +int APS5_VABI sceNpBase64Decoder(const char* src, std::size_t length, void* dst, std::size_t capacity) { + return Base64Decode(src, length, static_cast(dst), capacity, BASE64_ALPHABET, true); +} + +int APS5_VABI sceNpBase64Encoder(const void* src, char* dst, std::size_t length) { + return Base64Encode(static_cast(src), dst, length, BASE64_ALPHABET, true); +} + +int APS5_VABI sceNpBase64UrlGetDecodeSize(const char* src, std::size_t length) { + return Base64Decode(src, length, nullptr, 0, BASE64URL_ALPHABET, false); +} + +int APS5_VABI sceNpBase64UrlDecoder(const char* src, std::size_t length, void* dst, std::size_t capacity) { + return Base64Decode(src, length, static_cast(dst), capacity, BASE64URL_ALPHABET, false); +} + +int APS5_VABI sceNpBase64UrlEncoder(const void* src, char* dst, std::size_t length) { + return Base64Encode(static_cast(src), dst, length, BASE64URL_ALPHABET, false); +} + +int APS5_VABI sceNpHexToInt(int c) { + return HexValue(static_cast(c)); +} + +int APS5_VABI sceNpIntToHex(std::uint32_t value, bool uppercase) { + return HexDigit(value, uppercase); +} + +int APS5_VABI sceNpUInt32ToStr(char* buf, std::size_t size, std::uint32_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + return FormatInteger(buf, size, value, PlusSign(value != 0, flags), radix, flags, length); +} + +int APS5_VABI sceNpUInt64ToStr(char* buf, std::size_t size, std::uint64_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + return FormatInteger(buf, size, value, PlusSign(value != 0, flags), radix, flags, length); +} + +int APS5_VABI sceNpInt32ToStr(char* buf, std::size_t size, std::int32_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + const char sign = value < 0 ? '-' : PlusSign(value > 0, flags); + return FormatInteger(buf, size, Magnitude(value), sign, radix, flags, length); +} + +int APS5_VABI sceNpInt64ToStr(char* buf, std::size_t size, std::int64_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + const char sign = (flags & NP_FORMAT_SIGN) == 0 ? '\0' : value < 0 ? '-' : PlusSign(value > 0, flags); + return FormatInteger(buf, size, Magnitude(value), sign, radix, flags, length); +} + +int APS5_VABI sceNpStrToInt32(const char* str, std::int32_t* value, int base, std::size_t* parsedLength) { + long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtol(s, end, base); + saturated = parsed == LONG_MIN || parsed == LONG_MAX; + }, &saturated); + if (result < 0) return result; + if (parsed != static_cast(parsed)) return NP_ERROR_OUT_OF_RANGE; + *value = static_cast(parsed); + return 0; +} + +int APS5_VABI sceNpStrToInt64(const char* str, std::int64_t* value, int base, std::size_t* parsedLength) { + long long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtoll(s, end, base); + saturated = parsed == LLONG_MIN || parsed == LLONG_MAX; + }, &saturated); + if (result < 0) return result; + *value = parsed; + return 0; +} + +int APS5_VABI sceNpStrToUInt32(const char* str, std::uint32_t* value, int base, std::size_t* parsedLength) { + unsigned long long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtoull(s, end, base); + saturated = parsed == ULLONG_MAX; + }, &saturated); + if (result < 0) return result; + if ((parsed >> 32) != 0) return NP_ERROR_OUT_OF_RANGE; + *value = static_cast(parsed); + return 0; +} + +int APS5_VABI sceNpStrToUInt64(const char* str, std::uint64_t* value, int base, std::size_t* parsedLength) { + unsigned long long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtoull(s, end, base); + saturated = parsed == ULLONG_MAX; + }, &saturated); + if (result < 0) return result; + *value = parsed; + return 0; +} + +int APS5_VABI sceNpStrBuildHex(char* dst, std::size_t size, const void* src, std::size_t length, bool uppercase) { + if (size <= length * 2) return NP_ERROR_BUFFER_TOO_SMALL; + const auto* bytes = static_cast(src); + for (std::size_t i = 0; i < length; ++i) { + dst[i * 2] = HexDigit(bytes[i] >> 4, uppercase); + dst[i * 2 + 1] = HexDigit(bytes[i] & 0xF, uppercase); + } + dst[length * 2] = '\0'; + return 0; +} + +int APS5_VABI sceNpStrParseHex(const char* hex, void* out, std::size_t capacity) { + return ParseHex(hex, std::strlen(hex), static_cast(out), capacity); +} + +int APS5_VABI sceNpStrnParseHex(const char* hex, std::size_t length, void* out, std::size_t capacity) { + return ParseHex(hex, length, static_cast(out), capacity); +} + +int APS5_VABI sceNpStrcpyToBuf(char* dst, std::size_t size, const char* src) { + return CopyToBuffer(dst, size, src, std::strlen(src)); +} + +int APS5_VABI sceNpStrncpyToBuf(char* dst, std::size_t size, const char* src, std::size_t maxLength) { + return CopyToBuffer(dst, size, src, strnlen(src, maxLength)); +} + +int APS5_VABI sceNpUtilJsonEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength) { + if (dst == nullptr || dstSize == 0 || src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dstSize < JSON_ESCAPE_MARGIN) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + std::size_t written = 0; + for (std::size_t i = 0; i < srcLength; ++i) { + if (written > dstSize - JSON_ESCAPE_MARGIN) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + const auto c = static_cast(src[i]); + if (c == 0) return 0; + char piece[JSON_ESCAPE_MARGIN] = {}; + if (const char* escape = JsonShortEscape(c)) { + std::strcpy(piece, escape); + } else if (c >= 0x20 && c != 0x7F) { + piece[0] = static_cast(c); + } else { + std::snprintf(piece, sizeof(piece), "\\u%04x", c); + } + std::strcat(dst, piece); + written += std::strlen(piece); + } + return 0; +} + +int APS5_VABI sceNpUtilJsonUnescape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, + std::size_t* dstLength) { + if (src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dst == nullptr && dstLength == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + const char* end = src + srcLength; + std::size_t count = 0; + int status = 0; + char* out = dst; + auto emit = [&](const char* bytes, std::size_t length, std::size_t consumed) { + if (out != nullptr) { + std::memcpy(out, bytes, length); + out += length; + } + count += length; + src += consumed; + }; + while (src < end) { + if (dst != nullptr && count + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + const std::size_t remaining = static_cast(end - src); + if (src[0] != '\\' || remaining < 2) { + emit(src, 1, 1); + continue; + } + if (const char simple = JsonShortUnescape(src[1])) { + emit(&simple, 1, 2); + continue; + } + if (src[1] != 'u' || remaining < JSON_UNICODE_ESCAPE_LENGTH) { + emit(src, 1, 1); + continue; + } + std::uint32_t unit = 0; + int result = ParseUtf16Unit(src + 2, &unit); + if (result < 0) { + status = result; + emit(src, 1, 1); + continue; + } + status = 0; + char encoded[4] = {}; + if ((unit & SURROGATE_MASK) != HIGH_SURROGATE) { + const std::size_t length = EncodeUtf8(unit, encoded); + if (dst != nullptr && count + length > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + emit(encoded, length, JSON_UNICODE_ESCAPE_LENGTH); + continue; + } + std::uint32_t low = 0; + if (remaining < JSON_SURROGATE_PAIR_LENGTH || src[6] != '\\' || src[7] != 'u') { + emit(src, 1, 1); + continue; + } + result = ParseUtf16Unit(src + 8, &low); + if (result < 0) { + status = result; + emit(src, 1, 1); + continue; + } + if ((low & SURROGATE_MASK) != LOW_SURROGATE) { + emit(src, 1, 1); + continue; + } + const std::size_t length = EncodeUtf8(CombineSurrogates(unit, low), encoded); + if (dst != nullptr && count + length > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + emit(encoded, length, JSON_SURROGATE_PAIR_LENGTH); + } + ++count; + if (count > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dstLength != nullptr) *dstLength = count; + if (out != nullptr) *out = '\0'; + return status; +} + +int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, + std::size_t* dstLength, std::size_t* consumed) { + if (src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dst == nullptr && dstLength == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + std::size_t decoded = 1; + std::size_t used = 1; + int status = 0; + char encoded[4] = {}; + const auto lead = static_cast(src[0]); + if (lead == '\\') { + used = 2; + if (srcLength < 2) { + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else if (const char simple = JsonShortUnescape(src[1])) { + encoded[0] = simple; + } else if (src[1] != 'u') { + used = 1; + encoded[0] = '\\'; + } else { + used = JSON_UNICODE_ESCAPE_LENGTH; + std::uint32_t unit = 0; + if (srcLength < JSON_UNICODE_ESCAPE_LENGTH || ParseUtf16Unit(src + 2, &unit) < 0) { + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else if ((unit & SURROGATE_MASK) != HIGH_SURROGATE) { + decoded = EncodeUtf8(unit, encoded); + } else { + used = JSON_SURROGATE_PAIR_LENGTH; + std::uint32_t low = 0; + if (srcLength < JSON_SURROGATE_PAIR_LENGTH || ParseUtf16Unit(src + 8, &low) < 0 + || (low & SURROGATE_MASK) != LOW_SURROGATE) { + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else { + decoded = EncodeUtf8(CombineSurrogates(unit, low), encoded); + } + } + } + } else if (const std::size_t length = Utf8SequenceLength(lead); length != 0 && srcLength < length) { + decoded = 0; + used = length; + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else if (length != 0 && IsValidSequence(src, length)) { + decoded = length; + used = length; + std::memcpy(encoded, src, length); + } else { + encoded[0] = src[0]; + } + if (status == 0 && dst != nullptr && dstSize < decoded) status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (status == 0 && dst != nullptr) std::memcpy(dst, encoded, decoded); + if (dstLength != nullptr) *dstLength = decoded; + if (consumed != nullptr) *consumed = used; + return status; +} + +int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const char** character) { + if (text == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + const char* position = text; + for (std::size_t i = 0; i < index && *position != '\0'; ++i) { + const auto lead = static_cast(*position); + std::size_t length = 1; + if (lead < 0x80) { + if (!IsAllowedTextByte(lead)) return NP_UTIL_ERROR_INVALID_CHARACTER; + } else { + length = Utf8SequenceLength(lead); + if (length == 0 || !IsValidSequence(position, length)) return NP_UTIL_ERROR_INVALID_CHARACTER; + } + position += length; + } + *character = position; + return 0; +} + +int APS5_VABI sceNpUtilHttpUrlEncode(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, + std::size_t* dstLength) { + if (src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dst == nullptr && dstLength == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + std::size_t count = 0; + for (std::size_t i = 0; i < srcLength; ++i) { + const auto c = static_cast(src[i]); + const bool escaped = c != ' ' && !IsUrlSafe(c); + const std::size_t length = escaped ? 3 : 1; + if (dst != nullptr && count + length > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dst != nullptr) { + if (escaped) { + dst[count] = '%'; + dst[count + 1] = URL_HEX_DIGITS[c >> 4]; + dst[count + 2] = URL_HEX_DIGITS[c & 0xF]; + } else { + dst[count] = c == ' ' ? '+' : static_cast(c); + } + } + count += length; + } + if (dst != nullptr && count + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dst != nullptr) dst[count] = '\0'; + if (dstLength != nullptr) *dstLength = count + 1; + return 0; +} + +} diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp new file mode 100644 index 0000000000..e019a7b313 --- /dev/null +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -0,0 +1,495 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpAtomicCas32(std::int32_t* value, std::int32_t expected, std::int32_t desired); +int APS5_VABI sceNpAtomicInc32(std::int32_t* value); +int APS5_VABI sceNpAtomicDec32(std::int32_t* value); +int APS5_VABI sceNpCreateSema(KernelSema* sema, std::uint32_t flags, int initial, int maximum, const char* name); +int APS5_VABI sceNpDeleteSema(KernelSema sema); +int APS5_VABI sceNpPollSema(KernelSema sema); +int APS5_VABI sceNpSignalSema(KernelSema sema); +int APS5_VABI sceNpCreateEventFlag(KernelEventFlag* flag, const char* name, std::uint32_t attr, std::uint64_t pattern); +int APS5_VABI sceNpDeleteEventFlag(KernelEventFlag flag); +int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern); +int APS5_VABI sceNpPollEventFlag(KernelEventFlag flag, std::uint64_t pattern, std::uint32_t mode, std::uint64_t* result); +int APS5_VABI sceNpCreateThread(Pthread* thread, PthreadEntry entry, void* arg, int priority, std::size_t stackSize, KernelCpumask affinity, const char* name); +int APS5_VABI sceNpJoinThread(Pthread thread, void** value); +int APS5_VABI sceNpThreadGetId(Pthread* thread); + +struct NpHeap { + void* mspace; +}; + +struct NpHeapStat { + std::uint64_t maxSystemSize; + std::uint64_t maxInuseSize; + std::uint64_t currentInuseSize; +}; + +struct NpKernelMemory { + std::uint64_t size; + std::uint64_t reserved; + void* address; +}; + +struct NpMemoryHeap { + NpKernelMemory memory; + NpHeap heap; +}; + +struct NpAllocator { + void* (APS5_VABI *allocate)(std::size_t size, void* context); + void* (APS5_VABI *reallocate)(void* pointer, std::size_t size, void* context); + void (APS5_VABI *release)(void* pointer, void* context); + void* context; +}; + +struct NpAllocatorEx { + NpAllocator allocator; + int outOfMemoryError; +}; + +int APS5_VABI sceNpHeapInit(NpHeap* heap, void* base, std::size_t size, const char* name); +void APS5_VABI sceNpHeapDestroy(NpHeap* heap); +void APS5_VABI sceNpHeapGetAllocator(NpHeap* heap, NpAllocator* allocator); +int APS5_VABI sceNpHeapGetStat(const NpHeap* heap, NpHeapStat* stat); +int APS5_VABI sceNpMemoryHeapInit(NpMemoryHeap* heap, std::uint64_t size, const char* name); +void APS5_VABI sceNpMemoryHeapDestroy(NpMemoryHeap* heap); +void APS5_VABI sceNpMemoryHeapGetAllocatorEx(NpMemoryHeap* heap, int outOfMemoryError, NpAllocatorEx* allocator); +int APS5_VABI sceNpArchInit(void); +int APS5_VABI sceNpIpc2ClientInit(void); +int APS5_VABI sceNpIpc2ClientTerm(void); +void APS5_VABI sceNpArchTerm(void); +NpAllocatorEx* APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void); +int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(std::uint64_t size, const char* name, void** address, NpKernelMemory* memory); +int APS5_VABI sceNpFreeKernelMemory(NpKernelMemory* memory); +struct NpCallout { + NpCallout* next; + void (APS5_VABI *handler)(void* arg); + std::uint64_t deadline; + void* arg; + std::uint64_t reserved; +}; + +struct NpCalloutContext { + Pthread thread; + PthreadMutex mutex; + PthreadCond cond; + int initialized; + int stopping; + NpCallout* pending; +}; + +int APS5_VABI sceNpCalloutInitCtx(NpCalloutContext* context, const char* name, std::size_t stackSize, int priority, KernelCpumask affinity); +void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context); +int APS5_VABI sceNpCalloutStartOnCtx(NpCalloutContext* context, NpCallout* callout, std::uint32_t usec, void (APS5_VABI *handler)(void* arg), void* arg); +int APS5_VABI sceNpCalloutStopOnCtx(NpCalloutContext* context, NpCallout* callout, int* wasPending); +int APS5_VABI sceNpUtilCmpAccountId(std::uint64_t left, std::uint64_t right); +bool APS5_VABI sceNpIntIsOnlineIdString(const char* text); +int APS5_VABI sceNpUtilGetNpLanguageCode(int language, char* code); +int APS5_VABI sceNpUtilGetNpLanguageCode2(int language, char* tag); +const char* APS5_VABI sceNpUtilGetNpLanguageCodeStr(int language); +const char* APS5_VABI sceNpUtilGetNpLanguageCode2Str(int language); +std::uint32_t APS5_VABI sceNpDbgAssignDebugId(void); +int APS5_VABI sceNpUtilJsonEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength); +int APS5_VABI sceNpUtilJsonUnescape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength); +int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength, std::size_t* consumed); +int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const char** character); +int APS5_VABI sceNpUtilHttpUrlEncode(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength); +struct NpTitleIdParts { + char id[9]; + char reserved; + std::uint8_t index; +}; + +int APS5_VABI sceNpUtilBuildTitleId(const NpTitleIdParts* parts, char* titleId, std::size_t size); +int APS5_VABI sceNpBase64GetDecodeSize(const char* src, std::size_t length); +int APS5_VABI sceNpBase64Decoder(const char* src, std::size_t length, void* dst, std::size_t capacity); +int APS5_VABI sceNpBase64Encoder(const void* src, char* dst, std::size_t length); +int APS5_VABI sceNpBase64UrlGetDecodeSize(const char* src, std::size_t length); +int APS5_VABI sceNpBase64UrlDecoder(const char* src, std::size_t length, void* dst, std::size_t capacity); +int APS5_VABI sceNpBase64UrlEncoder(const void* src, char* dst, std::size_t length); +int APS5_VABI sceNpHexToInt(int c); +int APS5_VABI sceNpIntToHex(std::uint32_t value, bool uppercase); +int APS5_VABI sceNpUInt32ToStr(char* buf, std::size_t size, std::uint32_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpUInt64ToStr(char* buf, std::size_t size, std::uint64_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpInt32ToStr(char* buf, std::size_t size, std::int32_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpInt64ToStr(char* buf, std::size_t size, std::int64_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpStrToInt32(const char* str, std::int32_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrToInt64(const char* str, std::int64_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrToUInt32(const char* str, std::uint32_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrToUInt64(const char* str, std::uint64_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrBuildHex(char* dst, std::size_t size, const void* src, std::size_t length, bool uppercase); +int APS5_VABI sceNpStrParseHex(const char* hex, void* out, std::size_t capacity); +int APS5_VABI sceNpStrnParseHex(const char* hex, std::size_t length, void* out, std::size_t capacity); +int APS5_VABI sceNpStrcpyToBuf(char* dst, std::size_t size, const char* src); +int APS5_VABI sceNpStrncpyToBuf(char* dst, std::size_t size, const char* src, std::size_t maxLength); +} + +namespace { + +constexpr int NpIpcAlreadyInitialized = static_cast(0x80559D00); +constexpr int NpIpcNotInitialized = static_cast(0x80559D01); +constexpr int NpOutOfMemory = static_cast(0x80550005); +constexpr int NpNullObject = static_cast(0x80558005); +constexpr int NpNotInitialized = static_cast(0x80558001); +constexpr int NpAlreadyInitialized = static_cast(0x80558002); +constexpr int NpAlreadyQueued = static_cast(0x80558006); +constexpr int NpUtilInvalid = static_cast(0x80550601); +constexpr int NpUtilBufferTooSmall = static_cast(0x80550602); +constexpr int NpUtilOutOfRange = static_cast(0x8055060A); +constexpr int NpUtilInvalidCharacter = static_cast(0x80550608); +constexpr int NpInvalidFormat = static_cast(0x80558003); +constexpr int NpInvalidCharacter = static_cast(0x80558004); +constexpr int NpBufferTooSmall = static_cast(0x80558008); +constexpr int NpOutOfRange = static_cast(0x8055800E); +constexpr int NpInvalidArgument = static_cast(0x80558009); +constexpr int NpEventFlagBusy = static_cast(0x8055800C); +constexpr int NpSemaBusy = static_cast(0x8055800D); +constexpr std::uint32_t EventFlagWaitAnd = 1; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void* APS5_VABI ReturnArgument(void* arg) { + Pthread self {}; + Require(sceNpThreadGetId(&self) == 0 && self != nullptr, "a created thread can read its id"); + return arg; +} + +void TestAtomics() { + std::int32_t value = 5; + Require(sceNpAtomicInc32(&value) == 5 && value == 6, "increment returns the previous value"); + Require(sceNpAtomicDec32(&value) == 6 && value == 5, "decrement returns the previous value"); + Require(sceNpAtomicCas32(&value, 4, 9) == 5 && value == 5, "a failed compare-and-swap returns the current value"); + Require(sceNpAtomicCas32(&value, 5, 9) == 5 && value == 9, "a successful compare-and-swap returns the previous value"); +} + +void TestSemaphoresAndFlags() { + KernelSema sema {}; + Require(sceNpCreateSema(&sema, 0, 0, 2, "NpSema") == 0, "a semaphore is created"); + Require(sceNpPollSema(sema) == NpSemaBusy, "an empty semaphore reports the busy error"); + Require(sceNpSignalSema(sema) == 0 && sceNpPollSema(sema) == 0, "a signalled semaphore can be taken once"); + Require(sceNpDeleteSema(sema) == 0, "the semaphore is deleted"); + + KernelEventFlag flag {}; + Require(sceNpCreateEventFlag(&flag, "NpFlag", 0, 0) == 0, "an event flag is created"); + std::uint64_t result = 0; + Require(sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == NpEventFlagBusy, "a clear event flag reports the busy error"); + Require(sceNpSetEventFlag(flag, 1) == 0 && sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == 0 && result == 1, "a set bit is polled"); + Require(sceNpDeleteEventFlag(flag) == 0, "the event flag is deleted"); +} + +void TestThreads() { + Require(sceNpThreadGetId(nullptr) == NpInvalidArgument, "a null thread id output is refused"); + Pthread thread {}; + int marker = 0; + Require(sceNpCreateThread(&thread, ReturnArgument, &marker, 0, 0x10000, 0, "NpThread") == 0, "a thread is created"); + void* value = nullptr; + Require(sceNpJoinThread(thread, &value) == 0 && value == &marker, "the thread is joined with its return value"); +} + +void UseAllocator(const NpAllocator& allocator) { + auto* block = static_cast(allocator.allocate(64, allocator.context)); + Require(block != nullptr, "the allocator hands out memory"); + block[63] = 0x5A; + block = static_cast(allocator.reallocate(block, 256, allocator.context)); + Require(block != nullptr && block[63] == 0x5A, "reallocation keeps the contents"); + allocator.release(block, allocator.context); + allocator.release(nullptr, allocator.context); +} + +void TestHeaps() { + NpMemoryHeap memoryHeap {}; + Require(sceNpMemoryHeapInit(nullptr, 0x1000, "np-test") == NpNullObject, "a null memory heap is refused"); + Require(sceNpMemoryHeapInit(&memoryHeap, 0x1001, "np-test") == 0, "a memory heap is created"); + Require(memoryHeap.memory.size == 0x4000 && memoryHeap.memory.address != nullptr && memoryHeap.heap.mspace != nullptr, + "the memory heap size is rounded up to 16 KiB"); + NpAllocatorEx allocatorEx {}; + sceNpMemoryHeapGetAllocatorEx(&memoryHeap, 0x1234, &allocatorEx); + Require(allocatorEx.allocator.context == &memoryHeap && allocatorEx.outOfMemoryError == 0x1234, + "the extended allocator carries the caller's error code"); + UseAllocator(allocatorEx.allocator); + sceNpMemoryHeapDestroy(&memoryHeap); + Require(memoryHeap.memory.address == nullptr && memoryHeap.heap.mspace == nullptr, "destroying clears the memory heap"); + + NpKernelMemory memory {}; + void* address = nullptr; + Require(sceNpAllocateKernelMemoryWithAlignment(0x10, "np-test", &address, nullptr) == NpNullObject, + "kernel memory needs an object"); + Require(sceNpAllocateKernelMemoryWithAlignment(0x10, "np-test", &address, &memory) == 0 + && address == memory.address && memory.size == 0x4000, + "kernel memory is mapped in 16 KiB units"); + NpHeap heap {}; + Require(sceNpHeapInit(&heap, memory.address, memory.size, "np-test") == 0 && heap.mspace != nullptr, "a heap is created"); + NpAllocator allocator {}; + sceNpHeapGetAllocator(&heap, &allocator); + Require(allocator.context == &heap, "the heap allocator uses the heap as its context"); + UseAllocator(allocator); + NpHeapStat stat {}; + Require(sceNpHeapGetStat(&heap, &stat) == 0 && stat.maxInuseSize >= stat.currentInuseSize, "heap statistics are reported"); + sceNpHeapDestroy(&heap); + Require(heap.mspace == nullptr, "destroying clears the heap"); + Require(sceNpFreeKernelMemory(&memory) == 0 && memory.address == nullptr, "kernel memory is released"); + Require(sceNpFreeKernelMemory(nullptr) == NpNullObject, "freeing needs an object"); + + Require(sceNpArchInit() == 0 && sceNpArchInit() == 0, "the library initialises more than once"); + NpAllocatorEx* global = sceNpGlobalHeapGetAllocatorExPtr(); + Require(global->outOfMemoryError == NpOutOfMemory, "the global allocator reports the library's out-of-memory error"); + UseAllocator(global->allocator); + sceNpArchTerm(); + UseAllocator(sceNpGlobalHeapGetAllocatorExPtr()->allocator); + sceNpArchTerm(); + + Require(sceNpIpc2ClientTerm() == NpIpcNotInitialized, "the ipc client must be started before it stops"); + Require(sceNpIpc2ClientInit() == 0 && sceNpIpc2ClientInit() == NpIpcAlreadyInitialized, "the ipc client starts once"); + Require(sceNpIpc2ClientTerm() == 0 && sceNpIpc2ClientInit() == 0 && sceNpIpc2ClientTerm() == 0, "the ipc client restarts"); +} + +struct CalloutRecord { + std::atomic order {0}; + std::atomic first {0}; + std::atomic second {0}; +}; + +CalloutRecord g_callouts; + +void APS5_VABI RecordFirst(void*) { + g_callouts.first = ++g_callouts.order; +} + +void APS5_VABI RecordSecond(void*) { + g_callouts.second = ++g_callouts.order; +} + +void APS5_VABI RecordUnexpected(void*) { + Require(false, "a stopped callout does not run"); +} + +void TestCallouts() { + NpCalloutContext context {}; + NpCallout callout {}; + Require(sceNpCalloutStartOnCtx(&context, &callout, 0, RecordFirst, nullptr) == NpNotInitialized, "a context must be initialised first"); + Require(sceNpCalloutInitCtx(&context, "np-callout", 0x4000, 0, 0) == 0, "a callout context starts"); + Require(sceNpCalloutInitCtx(&context, "np-callout", 0x4000, 0, 0) == NpAlreadyInitialized, "a context starts once"); + + NpCallout stopped {}; + int wasPending = 0; + Require(sceNpCalloutStartOnCtx(&context, &stopped, 60000000, RecordUnexpected, nullptr) == 0, "a distant callout is queued"); + Require(sceNpCalloutStartOnCtx(&context, &stopped, 60000000, RecordUnexpected, nullptr) == NpAlreadyQueued, + "a queued callout cannot be queued again"); + Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 1, "a queued callout is removed"); + Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 0, "removing twice reports nothing pending"); + + NpCallout later {}; + Require(sceNpCalloutStartOnCtx(&context, &later, 20000, RecordSecond, nullptr) == 0, "a later callout is queued"); + Require(sceNpCalloutStartOnCtx(&context, &callout, 1000, RecordFirst, nullptr) == 0, "an earlier callout is queued"); + for (int i = 0; i < 2000 && g_callouts.second == 0; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(g_callouts.first == 1 && g_callouts.second == 2, "callouts run in deadline order"); + sceNpCalloutTermCtx(&context); + Require(context.initialized == 0, "terminating clears the context"); + sceNpCalloutTermCtx(&context); +} + +bool Equals(const char* actual, const char* expected) { + return std::strcmp(actual, expected) == 0; +} + +void TestIds() { + Require(sceNpUtilCmpAccountId(7, 7) == 0 && sceNpUtilCmpAccountId(7, 8) == 1, "account ids compare for equality"); + + Require(sceNpIntIsOnlineIdString("Alice_01") && sceNpIntIsOnlineIdString("Bob-2"), "letters, digits, '_' and '-' are allowed"); + Require(!sceNpIntIsOnlineIdString("ab") && !sceNpIntIsOnlineIdString("abcdefghijklmnopq"), "online ids are 3 to 16 characters"); + Require(!sceNpIntIsOnlineIdString("1abc") && !sceNpIntIsOnlineIdString("ab.c") && !sceNpIntIsOnlineIdString(nullptr), + "online ids start with a letter and use no other symbols"); +} + +void TestJsonText() { + char escaped[64] = {}; + Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "a\"b/\n\x01", 7) == 0 && Equals(escaped, "a\\\"b\\/\\n\\u0001"), + "json escaping covers quotes, solidus, newlines and controls"); + std::strcpy(escaped, "x"); + Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "yz", 2) == 0 && Equals(escaped, "xyz"), "escaping appends to the buffer"); + Require(sceNpUtilJsonEscape(escaped, 6, "a", 1) == NpUtilBufferTooSmall, "escaping needs room for one escape"); + Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), nullptr, 1) == NpUtilInvalid, "escaping needs a source"); + + char text[32] = {}; + std::size_t length = 0; + Require(sceNpUtilJsonUnescape(text, sizeof(text), "a\\nb\\u0041", 10, &length) == 0 && Equals(text, "a\nbA") && length == 5, + "json unescaping decodes escapes and counts the terminator"); + Require(sceNpUtilJsonUnescape(text, sizeof(text), "\\uD83D\\uDE00", 12, &length) == 0 && Equals(text, "\xF0\x9F\x98\x80") + && length == 5, + "surrogate pairs become one UTF-8 character"); + Require(sceNpUtilJsonUnescape(text, sizeof(text), "\\u00G1", 6, &length) == NpUtilBufferTooSmall && Equals(text, "\\u00G1"), + "a bad unicode escape is copied and reported"); + Require(sceNpUtilJsonUnescape(nullptr, sizeof(text), "abc", 3, &length) == 0 && length == 4, "a null buffer reports the size"); + Require(sceNpUtilJsonUnescape(text, 3, "abc", 3, &length) == NpUtilBufferTooSmall, "unescaping needs room for the terminator"); + + char one[4] = {}; + std::size_t consumed = 0; + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\\u00e9x", 7, &length, &consumed) == 0 && length == 2 && consumed == 6 + && std::memcmp(one, "\xC3\xA9", 2) == 0, + "one escaped character is decoded"); + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\xE6\x97\xA5", 3, &length, &consumed) == 0 && length == 3 && consumed == 3, + "one UTF-8 character is copied"); + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\xE6\x97", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 0 + && consumed == 3, + "a cut UTF-8 character reports its full length"); + Require(sceNpUtilJsonGetOneChar(one, 1, "\xC3\xA9", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 2, + "a short buffer still reports the decoded size"); + + const char* character = nullptr; + const char* japanese = "\xE6\x97\xA5" "a"; + Require(sceNpUtilGetNthChar(1, japanese, &character) == 0 && character == japanese + 3, "characters are counted, not bytes"); + Require(sceNpUtilGetNthChar(10, "abc", &character) == 0 && *character == '\0', "counting stops at the end"); + Require(sceNpUtilGetNthChar(2, "a\x01b", &character) == NpUtilInvalidCharacter, "control characters are refused"); + + char url[32] = {}; + Require(sceNpUtilHttpUrlEncode(url, sizeof(url), "a b=c*", 6, &length) == 0 && Equals(url, "a+b%3Dc*") && length == 9, + "form encoding keeps the safe set and escapes the rest"); + Require(sceNpUtilHttpUrlEncode(nullptr, 0, "@", 1, &length) == 0 && length == 4, "a null buffer reports the encoded size"); + Require(sceNpUtilHttpUrlEncode(url, 3, "@", 1, &length) == NpUtilBufferTooSmall, "encoding needs room for the terminator"); +} + +void TestTitleIds() { + NpTitleIdParts parts {{'A', 'B', 'C', 'D', '1', '2', '3', '4', '5'}, 0, 7}; + char titleId[13] = {}; + Require(sceNpUtilBuildTitleId(&parts, titleId, sizeof(titleId)) == 0 && Equals(titleId, "ABCD12345_07"), "the index is two digits"); + Require(sceNpUtilBuildTitleId(&parts, titleId, 12) == NpUtilBufferTooSmall, "a title id needs 13 bytes"); + parts.index = 100; + Require(sceNpUtilBuildTitleId(&parts, titleId, sizeof(titleId)) == NpUtilOutOfRange, "the index is at most 99"); +} + +void TestLanguagesAndDebugIds() { + char code[6] = {}; + char tag[36] = {}; + Require(sceNpUtilGetNpLanguageCode(1, code) == 0 && Equals(code, "en"), "language 1 is English"); + Require(sceNpUtilGetNpLanguageCode2(20, tag) == 0 && Equals(tag, "es-419"), "language 20 is Latin American Spanish"); + Require(Equals(sceNpUtilGetNpLanguageCodeStr(11), "zh-CN") && Equals(sceNpUtilGetNpLanguageCode2Str(30), "uk-UA"), + "language strings are returned directly"); + Require(sceNpUtilGetNpLanguageCode(31, code) == NpUtilInvalid && sceNpUtilGetNpLanguageCodeStr(31) == nullptr, + "unknown languages are refused"); + Require(sceNpUtilGetNpLanguageCode2(0, nullptr) == NpUtilInvalid, "a language code needs a buffer"); + const std::uint32_t first = sceNpDbgAssignDebugId(); + Require(first != 0 && sceNpDbgAssignDebugId() == first + 1, "debug ids count up"); +} + +void TestBase64() { + const std::uint8_t payload[] = {1, 2, 3, 4, 5}; + char text[16] = {}; + Require(sceNpBase64Encoder(payload, text, 5) == 8 && Equals(text, "AQIDBAU="), "base64 pads the last group"); + Require(sceNpBase64Encoder(payload, text, 0) == 0, "an empty payload encodes to nothing"); + Require(sceNpBase64GetDecodeSize("AQIDBAU=", 8) == 5 && sceNpBase64GetDecodeSize("AQ==", 4) == 1, + "the decoded size follows the padding"); + Require(sceNpBase64GetDecodeSize("", 0) == NpInvalidFormat && sceNpBase64GetDecodeSize("AQI", 3) == NpInvalidFormat, + "base64 needs whole groups"); + Require(sceNpBase64GetDecodeSize("AQ=A", 4) == NpInvalidCharacter && sceNpBase64GetDecodeSize("A=AAAQID", 8) == NpInvalidCharacter, + "padding is only allowed at the end"); + std::uint8_t bytes[8] = {}; + Require(sceNpBase64Decoder("AQIDBAU=", 8, bytes, sizeof(bytes)) == 5 && std::memcmp(bytes, payload, 5) == 0, + "base64 decodes back to the payload"); + Require(sceNpBase64Decoder("AQID", 4, bytes, 2) == NpInvalidFormat, "decoding refuses a short buffer"); + Require(sceNpBase64Decoder("AQ?D", 4, bytes, 3) == NpInvalidCharacter, "decoding refuses characters outside the alphabet"); + + const std::uint8_t url[] = {0xFB, 0x00}; + std::memset(text, 0, sizeof(text)); + Require(sceNpBase64UrlEncoder(url, text, 1) == 2 && Equals(text, "-w"), "base64url leaves out the padding"); + Require(sceNpBase64UrlEncoder(url, text, 2) == 3 && Equals(text, "-wA"), "base64url writes three characters for two bytes"); + Require(sceNpBase64UrlGetDecodeSize("-wA", 3) == 2 && sceNpBase64UrlGetDecodeSize("-w==", 4) == 1, + "base64url accepts padded and unpadded tails"); + Require(sceNpBase64UrlGetDecodeSize("", 0) == NpInvalidCharacter && sceNpBase64UrlGetDecodeSize("AAAAA", 5) == NpInvalidCharacter, + "a single trailing character cannot be decoded"); + Require(sceNpBase64UrlGetDecodeSize("+w==", 4) == NpInvalidCharacter, "base64url refuses the standard alphabet"); + Require(sceNpBase64UrlDecoder("-wA", 3, bytes, 2) == 2 && bytes[0] == 0xFB && bytes[1] == 0, "base64url decodes a short tail"); + Require(sceNpBase64UrlDecoder("-w", 2, bytes, 0) == NpInvalidFormat, "base64url decoding refuses a short buffer"); +} + +void TestNumbers() { + Require(sceNpHexToInt('a') == 10 && sceNpHexToInt('F') == 15 && sceNpHexToInt('g') == -1, "hex digits are read"); + Require(sceNpIntToHex(10, false) == 'a' && sceNpIntToHex(15, true) == 'F' && sceNpIntToHex(9, true) == '9', "hex digits are written"); + + char text[32] = {}; + std::size_t length = 0; + Require(sceNpUInt32ToStr(text, sizeof(text), 255, 16, 3, &length) == 0 && Equals(text, "0XFF") && length == 4, + "the hex prefix follows the case flag"); + Require(sceNpUInt32ToStr(text, sizeof(text), 8, 8, 2, &length) == 0 && Equals(text, "010") && length == 3, "octal gets a zero prefix"); + Require(sceNpUInt32ToStr(text, sizeof(text), 0, 10, 4, &length) == 0 && Equals(text, "0") && length == 1, "zero has no sign"); + Require(sceNpUInt32ToStr(text, 4, 1234, 10, 0, &length) == NpBufferTooSmall, "the terminator must fit"); + Require(sceNpUInt32ToStr(text, sizeof(text), 1, 37, 0, &length) == NpInvalidArgument, "the radix is limited to 36"); + Require(sceNpUInt32ToStr(nullptr, 0, 42, 10, 4, &length) == 0 && length == 3, "a null buffer reports the length"); + Require(sceNpInt32ToStr(text, sizeof(text), -2147483647 - 1, 10, 0, &length) == 0 && Equals(text, "-2147483648"), + "a negative 32-bit value always has a sign"); + Require(sceNpInt32ToStr(text, 3, -42, 10, 0, &length) == NpBufferTooSmall, "the sign takes buffer space"); + Require(sceNpInt64ToStr(text, sizeof(text), -42, 10, 0, &length) == 0 && Equals(text, "42") && length == 2, + "a 64-bit value only gets a sign with the sign flag"); + Require(sceNpInt64ToStr(text, sizeof(text), -42, 10, 4, &length) == 0 && Equals(text, "-42"), "the sign flag writes the minus"); + Require(sceNpUInt64ToStr(text, sizeof(text), UINT64_MAX, 16, 3, &length) == 0 && Equals(text, "0XFFFFFFFFFFFFFFFF") && length == 18, + "the full 64-bit range is formatted"); + + std::int32_t value32 = 0; + std::size_t parsed = 0; + Require(sceNpStrToInt32("-567abc", &value32, 10, &parsed) == 0 && value32 == -567 && parsed == 4, "parsing stops at the first non-digit"); + Require(sceNpStrToInt32("0x1A", &value32, 0, &parsed) == 0 && value32 == 26, "base zero follows the prefix"); + Require(sceNpStrToInt32("invalid", &value32, 10, &parsed) == NpOutOfRange, "no digits is an error"); + parsed = 0; + Require(sceNpStrToInt32("3000000000", &value32, 10, &parsed) == NpOutOfRange && parsed == 10, + "the length is reported before the 32-bit range check"); + std::int64_t value64 = 0; + Require(sceNpStrToInt64("-9223372036854775808", &value64, 10, nullptr) == 0 && value64 == INT64_MIN, "the 64-bit minimum parses"); + Require(sceNpStrToInt64("99999999999999999999", &value64, 10, nullptr) == NpOutOfRange, "64-bit overflow is an error"); + std::uint32_t unsigned32 = 0; + Require(sceNpStrToUInt32("4294967295", &unsigned32, 10, nullptr) == 0 && unsigned32 == UINT32_MAX, "the 32-bit maximum parses"); + Require(sceNpStrToUInt32("4294967296", &unsigned32, 10, nullptr) == NpOutOfRange, "32-bit unsigned overflow is an error"); + std::uint64_t unsigned64 = 0; + Require(sceNpStrToUInt64("0xFFFFFFFFFFFFFFFF", &unsigned64, 0, nullptr) == 0 && unsigned64 == UINT64_MAX, "the 64-bit maximum parses"); + Require(sceNpStrToUInt64("18446744073709551616", &unsigned64, 10, nullptr) == NpOutOfRange, "64-bit unsigned overflow is an error"); +} + +void TestHexAndCopies() { + const std::uint8_t data[] = {0x0A, 0xFE}; + char text[8] = {}; + Require(sceNpStrBuildHex(text, 5, data, 2, false) == 0 && Equals(text, "0afe"), "bytes are written as lowercase hex"); + Require(sceNpStrBuildHex(text, 5, data, 2, true) == 0 && Equals(text, "0AFE"), "bytes are written as uppercase hex"); + Require(sceNpStrBuildHex(text, 4, data, 2, false) == NpBufferTooSmall, "hex output needs room for the terminator"); + std::uint8_t bytes[4] = {}; + Require(sceNpStrParseHex("0aFE", bytes, 2) == 0 && bytes[0] == 0x0A && bytes[1] == 0xFE, "hex is read in either case"); + Require(sceNpStrParseHex("0AF", bytes, 2) == NpInvalidArgument, "odd hex is refused"); + Require(sceNpStrParseHex("0AFE", bytes, 1) == NpBufferTooSmall, "hex parsing checks the output size"); + Require(sceNpStrParseHex("0AG1", bytes, 2) == NpInvalidArgument, "non-hex characters are refused"); + Require(sceNpStrnParseHex("DEADBEEF1234", 8, bytes, 4) == 0 && bytes[0] == 0xDE && bytes[3] == 0xEF, "a hex prefix is read"); + + char copy[8] = {}; + Require(sceNpStrcpyToBuf(copy, 6, "Hello") == 0 && Equals(copy, "Hello"), "a string that fits is copied"); + Require(sceNpStrcpyToBuf(copy, 5, "Hello") == NpBufferTooSmall, "a string without room for its terminator is refused"); + Require(sceNpStrncpyToBuf(copy, 6, "Hello World", 5) == 0 && Equals(copy, "Hello"), "the length limit sizes the copy"); + Require(sceNpStrncpyToBuf(copy, 5, "Hello World", 5) == NpBufferTooSmall, "the limited string still needs a terminator"); +} + +} + +int main() { + TestAtomics(); + TestSemaphoresAndFlags(); + TestThreads(); + TestHeaps(); + TestBase64(); + TestNumbers(); + TestHexAndCopies(); + TestCallouts(); + TestIds(); + TestLanguagesAndDebugIds(); + TestJsonText(); + TestTitleIds(); + std::puts("guest np common runtime: PASS"); +} diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index e6e51903bb..32efbb235e 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,113 +2,28 @@ ### Unknown function info -The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. -- [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpArchInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `jGF+MaB4b-M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UskWpVWxSvg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAtomicCas32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+9+kKMY9YIw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAtomicDec32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Yohe0MMDfj0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAtomicInc32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pfJgSA4jO3M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64Decoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `l67qBmMmKP4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64Encoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pu39pU8UgCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64GetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `a5IfPlpchXI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64UrlDecoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `moGcgMNTHvQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64UrlEncoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `IeNj+OcWgU8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64UrlGetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `7BjZKcN+oZ4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutInitCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9+m5nRdJ-wQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutStartOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fClnlkZmA6k`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lpr66Gby8dQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutStopOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `in19gH7G040`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutTermCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AqJ4xkWsV+I`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCancelEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kb2thTuS8t8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fhJ5uKzcn0w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDbgAssignDebugId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `90pmGqDK4BI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Etq15-l9yko`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDbgDumpText](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZaKa5x61hGA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDeleteEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sjnIeFCuTD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocatorPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DI5n4aOdxmk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `wVdn78HKc30`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lvek8w7yqyE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapGetStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `2jdHoPpS+W0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `B+yGIX1+BTI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `evz0-93ucJc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHexToInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Hvpr+otU4bo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5y0wMPQkaeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `p5ZkSMRR7AU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseExInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `nhgjiwPUIzI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `teVnFAL6GNY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `zNb6IxegrCE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `++eqYdzB8Go`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Xkn6VoN-wuQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpPollEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9YmBJ8KF9eI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpPollSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xmF0yIF4iXc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpRtcConvertToPosixTime](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VMjIo2Z-aW0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpRtcFormatRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `W0YWLVDndx0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpRtcParseRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LtkeQwMIEWY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `0lZHbA-HRD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonMultiGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `cRabutqUG7c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `WSQxnAVLKgw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UbStlMKTBeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonParseMultiInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hbe+DdooIi4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpSetEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `29ftOGIrUCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `m9JzZSoDVFY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpSignalSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-W28+9p1CKI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrBuildHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `i5TP5NLmkoQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DUHzVPNlugg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fElyBSn-l24`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CwqYdG4TrjA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToUInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uj86YxCYid0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToUInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Ted2YU9lv94`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrcpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ivnnssCwjGI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrnParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `h1SWCcBdImo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrncpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PHrpHMSU8Cs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpThreadGetId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `yvaNTRiKXmo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `rRN89jBArEM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QjNUYQbGoHA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUserGetUserIdList](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Gh74vNl06sg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilBuildTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3tAHlBnowE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilCanonicalizeNpIdForPs4](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4mEAk-UKVNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilCanonicalizeNpIdForPsp2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3FB4r8JoRE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilCmpAccountId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPRHNaD3kTc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetDateSetAuto](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `owm52JoZ8uc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetDbgCommerce](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1Gfhi+tZ9IE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetEnv](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kBON3bAtfGs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -116,26 +31,27 @@ The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpUtilGetFakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `O86rgZ2azfg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetIgnoreNpTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FrxliFYAO8Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetNpDebug](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `GRvK1ZE+FEQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OFiFmfsADas`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCode2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X9CqyP164Hc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCode2Str](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Fxux7Ob+Ynk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCodeStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RfiA17kV+xs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetNpTestPatch](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OA8f3KF9JsM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNthChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `KCk4OGu8+sc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fB5hE65pzbU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetSystemLanguage](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SXUNKr9Zkv0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetTrcNotify](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AjzLvR0g5Zs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetWebApi2FakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pmHBFJyju9E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetWebApi2FakeRateLimitTarget](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZRxKp9vjcNc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetWebTraceSetting](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4CqfNm3pisU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilHttpUrlEncode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ajoqGz0D9Dw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJidToNpId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `458yjI+OECI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. ### Functional - [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. - [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. +- [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 12.00, read from disassembly and not measured on a console. +- [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. +- [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. +- [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. +- [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. +- [The integer-to-string exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) return `0x80558008` when the hexadecimal prefix does not fit; the firmware writes the prefix without checking the size. [The base64 decoders](../../../core/libs/prx/libSceNpCommon/Strings.cpp) treat bytes above 0x7F as invalid; the firmware indexes its table with them as signed values. [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/Strings.cpp) checks that a `\u` escape and a surrogate pair fit in the input; the firmware reads past it. +- [sceNpUtilGetNpDebug, sceNpUtilGetNpTestPatch, sceNpUtilGetTrcNotify, sceNpUtilGetIgnoreNpTitleId, sceNpUtilGetFakeDisplayNameMode, sceNpUtilGetWebTraceSetting, sceNpUtilGetWebApi2FakeRateLimit, sceNpUtilGetWebApi2FakeRateLimitTarget, sceNpUtilGetFakeRateLimit, sceNpUtilGetDbgCommerce, sceNpUtilGetSystemLanguage, sceNpUtilGetEnv, sceNpUtilGetDateSetAuto and sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) read system and debug registry keys through libSceRegMgr, whose `sceRegMgrNonSysGetInt` and string reads still throw; they throw until those exist. +- [sceNpOpenSema, sceNpCloseSema and sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores and event flags, which still throw. +- [sceNpSetPlatformType, sceNpIntGetPlatformType, sceNpIntSetPlatformType, sceNpUtilCanonicalizeNpIdForPs4, sceNpUtilCanonicalizeNpIdForPsp2, sceNpUtilJidToNpId and sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) depend on console platform tags, network domain names or a version string of the console; they throw. +- [sceNpJsonParse, sceNpJsonParseInit, sceNpJsonParseBuf, sceNpJsonParseBufInit, sceNpJsonParseEx, sceNpJsonParseExInit and the sceNpServerErrorJson exports](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) are a streaming, schema-checked JSON parser whose event and error rules are not yet specified; they throw. From 77a05cbfe9504eb3f7fb64a810c5c47ec7b20774 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 08:24:19 -0300 Subject: [PATCH 0328/1360] fix(libSceHttp): parse status lines past lineLen as measured on a PS5 A payload process on a PS5 (firmware 13.60) parsed 124 status lines and lengths. It checks lineLen only for its minimum of 8 and for the three code digits, reads the version and its separators past it, and finds the line feed up to and including the byte at lineLen, so a length that stops right before the line feed still parses. When the version ends past lineLen the unsigned subtractions wrap and the search continues until a line feed. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHttp/src/Parse.cpp | 21 ++-- core/libs/tests/GuestHttp.cpp | 160 ++++++++++++++++++++++++- docs/dev/debt/libSceHttp.md | 2 +- 3 files changed, 171 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Parse.cpp b/core/libs/prx/libSceHttp/src/Parse.cpp index 0732f62407..409e18cf1b 100644 --- a/core/libs/prx/libSceHttp/src/Parse.cpp +++ b/core/libs/prx/libSceHttp/src/Parse.cpp @@ -11,10 +11,10 @@ bool isAsciiDigit(char c) { return c >= '0' && c <= '9'; } -bool parseVersionNumber(const char* line, size_t lineLen, size_t& index, int32_t* value) { - if (index >= lineLen || !isAsciiDigit(line[index])) return false; +bool parseVersionNumber(const char* line, size_t& index, int32_t* value) { + if (!isAsciiDigit(line[index])) return false; uint32_t number = 0; - while (index < lineLen && isAsciiDigit(line[index])) { + while (isAsciiDigit(line[index])) { number = number * 10 + static_cast(line[index] - '0'); ++index; } @@ -36,11 +36,11 @@ int APS5_VABI sceHttpParseStatusLine(const char* statusLine, size_t lineLen, int if (lineLen < 8 || std::memcmp(statusLine, "HTTP/", 5) != 0) return ERROR_PARSE_HTTP_INVALID_RESPONSE; size_t index = 5; - if (!parseVersionNumber(statusLine, lineLen, index, httpMajorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; - if (index >= lineLen || statusLine[index] != '.') return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (!parseVersionNumber(statusLine, index, httpMajorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (statusLine[index] != '.') return ERROR_PARSE_HTTP_INVALID_RESPONSE; ++index; - if (!parseVersionNumber(statusLine, lineLen, index, httpMinorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; - if (index >= lineLen || statusLine[index] != ' ') return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (!parseVersionNumber(statusLine, index, httpMinorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (statusLine[index] != ' ') return ERROR_PARSE_HTTP_INVALID_RESPONSE; ++index; if (lineLen - index < 3) return ERROR_PARSE_HTTP_INVALID_RESPONSE; @@ -51,10 +51,11 @@ int APS5_VABI sceHttpParseStatusLine(const char* statusLine, size_t lineLen, int index += 3; const char* phrase = statusLine + index; - const void* lineFeed = std::memchr(phrase, '\n', lineLen - index); - if (!lineFeed) return ERROR_PARSE_HTTP_INVALID_RESPONSE; + const size_t searched = lineLen - index + 1; + size_t length = 0; + while (length != searched && phrase[length] != '\n') ++length; + if (length == searched) return ERROR_PARSE_HTTP_INVALID_RESPONSE; - size_t length = static_cast(static_cast(lineFeed) - phrase); const size_t consumed = index + length + 1; if (consumed > static_cast(INT32_MAX)) { throw std::out_of_range("sceHttpParseStatusLine: consumed byte count exceeds int"); diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 93eb0119d3..01e409a2ce 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -92,6 +92,163 @@ static void CheckUriEscapeBytes() { } } +struct StatusLineCase { + bool tildeFill; + const char* line; + std::size_t length; + int result; + std::int32_t major; + std::int32_t minor; + std::int32_t code; + long reasonOffset; + std::size_t phraseLength; +}; + +static void CheckStatusLineAsMeasured() { + constexpr int parseInvalidResponse = static_cast(0x80432060); + constexpr StatusLineCase cases[] = { + {false, "HTTP/1.1 200 OK\r\n", 17, 17, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.0 404 Not Found\n", 23, 23, 1, 0, 404, 12, 0xa}, + {false, "HTTP/1.1 200 OK", 15, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {false, "HTTP/1.1 200\r\n", 14, 14, 1, 1, 200, 12, 0x0}, + {false, "HTTP/1.1 200 \r\n", 15, 15, 1, 1, 200, 12, 0x1}, + {false, "HTTP/2 200 OK\r\n", 15, parseInvalidResponse, 2, 0, -7, -1, 0xabcd}, + {false, "HTTP/10.20 999 X\r\n", 18, 18, 10, 20, 999, 14, 0x2}, + {false, "HTTP/1.1 20 OK\r\n", 16, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1 2000 OK\r\n", 18, 18, 1, 1, 200, 12, 0x4}, + {false, "HTTP/1.1 abc OK\r\n", 17, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "http/1.1 200 OK\r\n", 17, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\r\n", 18, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1\t200 OK\r\n", 17, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200OK\r\n", 16, 16, 1, 1, 200, 12, 0x2}, + {false, "ICY 200 OK\r\n", 12, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\r", 16, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\r\nX: y", 21, 17, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.1 200 OK\r\r\n", 18, 18, 1, 1, 200, 12, 0x4}, + {false, "HTTP/.1 200 OK\r\n", 16, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1. 200 OK\r\n", 16, parseInvalidResponse, 1, 0, -7, -1, 0xabcd}, + {false, "HTTP/4294967297.1 200 OK\n", 25, 25, 1, 1, 200, 21, 0x3}, + {false, "HTTP/1.1 -12 OK\n", 16, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, " HTTP/1.1 200 OK\n", 17, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 A\nB\n", 17, 15, 1, 1, 200, 12, 0x2}, + {false, "HTTP/1.1 200\n", 13, 13, 1, 1, 200, 12, 0x0}, + {false, "HTTP/1.1 200 OK\n", 15, 16, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.1 200 OK\n", 12, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\n", 11, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\n", 8, 16, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.1 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 8, 16, 1, 1, 200, 12, 0x3}, + {true, "HTTP/1.1 200 OK\n", 9, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 10, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 11, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 12, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 13, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 14, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 15, 16, 1, 1, 200, 12, 0x3}, + {true, "HTTP/1.1 200 OK\n", 16, 16, 1, 1, 200, 12, 0x3}, + {true, "HTTP/10.20 999 X\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 8, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 9, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 10, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 11, parseInvalidResponse, 10, 20, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 12, parseInvalidResponse, 10, 20, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 13, parseInvalidResponse, 10, 20, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 14, parseInvalidResponse, 10, 20, 999, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 15, parseInvalidResponse, 10, 20, 999, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 16, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 17, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/1.12 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 8, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/1.12 200 OK\n", 9, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/1.12 200 OK\n", 10, parseInvalidResponse, 1, 12, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 11, parseInvalidResponse, 1, 12, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 12, parseInvalidResponse, 1, 12, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 13, parseInvalidResponse, 1, 12, 200, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 14, parseInvalidResponse, 1, 12, 200, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 15, parseInvalidResponse, 1, 12, 200, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 16, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/1.12 200 OK\n", 17, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 8, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 9, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 10, parseInvalidResponse, 12, 1, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 11, parseInvalidResponse, 12, 1, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 12, parseInvalidResponse, 12, 1, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 13, parseInvalidResponse, 12, 1, 200, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 14, parseInvalidResponse, 12, 1, 200, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 15, parseInvalidResponse, 12, 1, 200, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 16, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 17, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/1.1 200\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 8, 13, 1, 1, 200, 12, 0x0}, + {true, "HTTP/1.1 200\n", 9, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 10, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 11, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 12, 13, 1, 1, 200, 12, 0x0}, + {true, "HTTP/1.1 200\n", 13, 13, 1, 1, 200, 12, 0x0}, + {true, "HTTP/1.1 200 OK", 15, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK", 8, 255, 1, 1, 200, 12, 0xf2}, + {true, "HTTP/1.1 200 OK", 9, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK", 12, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK", 13, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 2", 8, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 20", 8, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1", 8, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + }; + static char buffer[256]; + for (const auto& row : cases) { + std::memset(buffer, row.tildeFill ? '~' : '\0', sizeof(buffer)); + std::memcpy(buffer, row.line, std::strlen(row.line)); + if (row.tildeFill) buffer[sizeof(buffer) - 2] = '\n'; + buffer[sizeof(buffer) - 1] = '\0'; + std::int32_t major = -7; + std::int32_t minor = -7; + std::int32_t code = -7; + const char* reason = nullptr; + std::size_t phraseLength = 0xabcd; + Require(sceHttpParseStatusLine(buffer, row.length, &major, &minor, &code, &reason, &phraseLength) == row.result); + Require(major == row.major && minor == row.minor && code == row.code && phraseLength == row.phraseLength); + Require(row.reasonOffset < 0 ? reason == nullptr : reason == buffer + row.reasonOffset); + } +} + static int AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } static void APS5_VABI RequestStatusCallback(int, int, void* argument) { ++*static_cast(argument); @@ -448,8 +605,9 @@ int main() { Require(parse("HTTP/1.1 2x0 OK\n", 16) == parseInvalidResponse); Require(parse("HTTP/1.1 20", 11) == parseInvalidResponse); Require(parse(response, 15) == parseInvalidResponse); - Require(parse(response, 16) == parseInvalidResponse); Require(phrase == nullptr && phraseLength == 0); + Require(parse(response, 16) == 17 && phrase == response + 12 && phraseLength == 3); + CheckStatusLineAsMeasured(); auto sweep = [](const char* src, const char* expected) { char swept[128]; std::memset(swept, 'x', sizeof(swept)); diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index bc45f2c73b..10e74b4f2d 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -9,7 +9,7 @@ - [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end 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 +- [sceHttpParseStatusLine](../../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for 124 lines and lengths. As on the console, `lineLen` is only checked to be at least 8 and to leave the 3 code digits (`lineLen - index < 3` in unsigned arithmetic): the version digits and the `.` and space after them are read past `lineLen`, and the line feed is searched up to and including the byte at `lineLen`, so a length that stops right before the line feed still parses. When the version ends past `lineLen` both subtractions wrap and the search goes on until a line feed; what the console does when none follows in readable memory was not measured - [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); decoding and error codes follow a payload process on a PS5 (firmware 13.60). A buffer one byte short of the decoded size got the decoded bytes without a terminator and left `require` alone; a shorter buffer is assumed to get the first `prepare` bytes the same way - [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before From 19d7e29b3a8bcb215783f4480910fa2601237bd4 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 08:32:59 -0300 Subject: [PATCH 0329/1360] fix(libSceHttp): copy URIs as measured on a PS5 A payload on a PS5 (firmware 13.60) shows sceHttpUriCopy clears the whole output and all prepare bytes of the pool before it reads the source, so a source that is the output or lies in the pool copies as empty. Read the source after clearing, clear the output with its padding, and test the measured layout, failures and aliasing. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHttp/src/Uri.cpp | 47 +++++++++------- core/libs/tests/GuestHttp.cpp | 84 ++++++++++++++++++++++++++++ docs/dev/debt/libSceHttp.md | 2 +- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 670a29e7eb..e400bd2724 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -342,31 +342,38 @@ int APS5_VABI sceHttpUriCopy(SceHttpUriElement* out, const SceHttpUriElement* sr const bool write = out && pool; if (!write && !require) return ERROR_INVALID_VALUE; + if (write) { + std::memset(out, 0, sizeof(*out)); + std::memset(pool, 0, prepare); + } const char* const fields[] = {src->scheme, src->hostname, src->path, src->username, src->password}; - SceHttpUriElement copy{}; - char** const slots[] = {©.scheme, ©.hostname, ©.path, ©.username, ©.password}; - if (write) std::memset(pool, 0, prepare); + const uint16_t port = src->port; + size_t lengths[5]; size_t used = 0; for (size_t i = 0; i < 5; ++i) { - const size_t length = fields[i] ? std::strlen(fields[i]) : 0; - const size_t needed = length + 1; - if (write) { - if (needed > prepare - used) { - *out = {}; - std::memset(pool, 0, prepare); - return ERROR_OUT_OF_MEMORY; - } - char* next = static_cast(pool) + used; - if (length != 0) std::memcpy(next, fields[i], length); - next[length] = '\0'; - *slots[i] = next; - } - used += needed; + lengths[i] = fields[i] ? std::strlen(fields[i]) : 0; + used += lengths[i] + 1; } - if (write) { - copy.port = src->port; - *out = copy; + if (!write) { + *require = used; + return 0; } + if (used > prepare) return ERROR_OUT_OF_MEMORY; + + char* next = static_cast(pool); + char* strings[5]; + for (size_t i = 0; i < 5; ++i) { + strings[i] = next; + if (lengths[i] != 0) std::memcpy(next, fields[i], lengths[i]); + next[lengths[i]] = '\0'; + next += lengths[i] + 1; + } + out->scheme = strings[0]; + out->hostname = strings[1]; + out->path = strings[2]; + out->username = strings[3]; + out->password = strings[4]; + out->port = port; if (require) *require = used; return 0; } diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 93eb0119d3..d39b7e8d3d 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -501,5 +501,89 @@ int main() { Require(sceHttpUriCopy(nullptr, &source, nullptr, 0, nullptr) == static_cast(0x804311FE)); SceHttpUriElement empty{}; Require(sceHttpUriCopy(©, &empty, pool, sizeof(pool), &needed) == 0 && needed == 5 && copy.scheme != nullptr && copy.scheme[0] == 0); + Require(copy.scheme == pool && copy.hostname == pool + 1 && copy.path == pool + 2 && copy.username == pool + 3 && copy.password == pool + 4); + + auto allBytes = [](const void* data, std::size_t size, unsigned char value) { + const auto* bytes = static_cast(data); + for (std::size_t i = 0; i < size; ++i) { + if (bytes[i] != value) return false; + } + return true; + }; + std::memset(pool, '~', sizeof(pool)); + std::memset(©, 0x5a, sizeof(copy)); + Require(sceHttpUriCopy(©, &source, pool, 64, &needed) == 0 && needed == 30); + Require(copy.scheme == pool && copy.hostname == pool + 5 && copy.path == pool + 17 && copy.username == pool + 22 && copy.password == pool + 27); + Require(allBytes(pool + 30, 34, 0) && pool[64] == '~' && copy.query == nullptr && copy.fragment == nullptr); + Require(allBytes(reinterpret_cast(©) + 4, 4, 0) && allBytes(copy.reserved, sizeof(copy.reserved), 0)); + + std::memset(pool, '~', sizeof(pool)); + std::memset(©, 0x5a, sizeof(copy)); + needed = 0xabcd; + Require(sceHttpUriCopy(©, &source, pool, 29, &needed) == static_cast(0x80431022) && needed == 0xabcd); + Require(allBytes(©, sizeof(copy), 0) && allBytes(pool, 29, 0) && pool[29] == '~'); + std::memset(pool, '~', sizeof(pool)); + Require(sceHttpUriCopy(©, &empty, pool, 0, &needed) == static_cast(0x80431022) && pool[0] == '~' && needed == 0xabcd); + Require(sceHttpUriCopy(©, &source, pool, 4, nullptr) == static_cast(0x80431022)); + + std::memset(pool, '~', sizeof(pool)); + Require(sceHttpUriCopy(nullptr, &source, pool, sizeof(pool), &needed) == 0 && needed == 30 && pool[0] == '~'); + Require(sceHttpUriCopy(nullptr, &source, pool, sizeof(pool), nullptr) == static_cast(0x804311FE)); + std::memset(©, 0x5a, sizeof(copy)); + Require(sceHttpUriCopy(©, &source, nullptr, 64, &needed) == 0 && needed == 30 && allBytes(©, sizeof(copy), 0x5a)); + Require(sceHttpUriCopy(©, &source, nullptr, 64, nullptr) == static_cast(0x804311FE) && allBytes(©, sizeof(copy), 0x5a)); + needed = 0xabcd; + Require(sceHttpUriCopy(©, nullptr, pool, sizeof(pool), &needed) == static_cast(0x80433060) && needed == 0xabcd && allBytes(©, sizeof(copy), 0x5a)); + Require(sceHttpUriCopy(nullptr, nullptr, nullptr, 0, nullptr) == static_cast(0x80433060)); + + char scheme[] = "SCH"; + char user[] = "usr"; + char pass[] = "pwd"; + char host[] = "hostname"; + char path[] = "/p/q"; + char query[] = "?query"; + char fragment[] = "#frag"; + char blank[] = ""; + SceHttpUriElement manual{}; + manual.opaque = 7; + manual.scheme = scheme; + manual.username = user; + manual.password = pass; + manual.hostname = host; + manual.path = path; + manual.query = query; + manual.fragment = fragment; + manual.port = 1234; + std::memset(manual.reserved, 0x11, sizeof(manual.reserved)); + Require(sceHttpUriCopy(©, &manual, pool, sizeof(pool), &needed) == 0 && needed == 26 && copy.opaque == 0 && copy.port == 1234); + Require(Equal(copy.scheme, "SCH") && copy.hostname == pool + 4 && Equal(copy.hostname, "hostname") && copy.path == pool + 13 && Equal(copy.path, "/p/q")); + Require(copy.username == pool + 18 && Equal(copy.username, "usr") && copy.password == pool + 22 && Equal(copy.password, "pwd")); + Require(copy.query == nullptr && copy.fragment == nullptr && allBytes(copy.reserved, sizeof(copy.reserved), 0)); + + SceHttpUriElement mixed{}; + mixed.hostname = host; + mixed.username = blank; + mixed.query = query; + mixed.port = 80; + Require(sceHttpUriCopy(©, &mixed, pool, sizeof(pool), &needed) == 0 && needed == 13 && copy.port == 80); + Require(copy.scheme == pool && copy.hostname == pool + 1 && copy.path == pool + 10 && copy.username == pool + 11 && copy.password == pool + 12); + + SceHttpUriElement inPlace = manual; + Require(sceHttpUriCopy(&inPlace, &inPlace, pool, sizeof(pool), &needed) == 0 && needed == 5 && inPlace.port == 0); + Require(inPlace.scheme == pool && inPlace.hostname == pool + 1 && inPlace.password == pool + 4 && inPlace.password[0] == 0); + inPlace = manual; + Require(sceHttpUriCopy(&inPlace, &inPlace, nullptr, 0, &needed) == 0 && needed == 26 && inPlace.scheme == scheme); + + alignas(SceHttpUriElement) char hostPool[256]; + auto* inside = reinterpret_cast(hostPool + 160); + std::memcpy(static_cast(inside), &manual, sizeof(manual)); + Require(sceHttpUriCopy(©, inside, hostPool, sizeof(hostPool), &needed) == 0 && needed == 5 && copy.port == 0 && copy.hostname[0] == 0); + + std::memcpy(hostPool + 200, "HOSTINPOOL", 11); + SceHttpUriElement pooled = manual; + pooled.hostname = hostPool + 200; + Require(sceHttpUriCopy(nullptr, &pooled, hostPool, sizeof(hostPool), &needed) == 0 && needed == 28); + Require(sceHttpUriCopy(©, &pooled, hostPool, sizeof(hostPool), &needed) == 0 && needed == 18 && copy.hostname == hostPool + 4 && copy.hostname[0] == 0); + Require(Equal(copy.path, "/p/q") && copy.path == hostPool + 5 && copy.port == 1234); } } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index bc45f2c73b..dbd8d31613 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -18,7 +18,7 @@ - [sceHttpSetRedirectCallback, sceHttpSetCookieRecvCallback, sceHttpsSetSslVersion, sceHttpsGetSslError](../../../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), which only logs and returns 0; a null `errNum` or `detail` in `sceHttpsGetSslError` is assumed to return `SCE_HTTP_ERROR_INVALID_VALUE` like `sceHttpGetLastErrno` - [sceHttpSetAuthInfoCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes -- [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - read from the disassembly of libSceHttp.sprx on firmware 13.60, not measured on a console and not compared with other emulators. The arguments are (out, src, pool, prepare, require), `prepare` before `require`, the reverse of `sceHttpUriParse`. A null source returns `ERROR_INVALID_URL`; without an output or a pool, a null `require` returns `ERROR_INVALID_VALUE`, otherwise the size is only reported. Only the scheme, hostname, path, username and password are copied, in that order, each with its terminator (a missing one becomes an empty string and takes one byte), and the port; the query, the fragment and the opaque flag stay 0. A pool that is too small clears the output and the pool and returns `ERROR_OUT_OF_MEMORY` (0x80431022, the console's out-of-size error) +- [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - unverified: whether the strings are written before the size check, which a short pool hides by ending cleared. The rest follows a payload process on a PS5 (firmware 13.60): the arguments are (out, src, pool, prepare, require) as in the disassembly; the whole output and all `prepare` pool bytes are cleared before the source is read, so a source that is the output or lies in the pool copies as empty; the scheme, hostname, path, username and password are copied in that order with terminators, a null one as empty, and the port, while the query, fragment and opaque flag stay 0; a short pool returns 0x80431022 and leaves `require` alone; a null source returns 0x80433060, and no output or pool with a null `require` 0x804311FE - [sceHttpAuthCacheImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpAuthCacheExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature From 0e2252f638036cd2c70a3e6842c4dedab3c13ce8 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:35:39 +0200 Subject: [PATCH 0330/1360] fix(libkernel): accept flock on a directory descriptor on Windows FreeBSD 11.4's sys_flock only requires a vnode descriptor, so flock on a directory descriptor succeeds. On Windows the call reached LockFileEx, which rejects a directory handle with ERROR_INVALID_PARAMETER, and flock_nid_postfix turned that into a runtime_error that aborted the process. A directory descriptor now returns 0 without taking a lock; the new guest_file_lock_directory case covers it and the debt entry records that no lock is held. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 3 +++ core/libs/tests/GuestFileLock.cpp | 18 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..a983d37c1b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1826,7 +1826,7 @@ if(BUILD_TESTING) target_include_directories(guest_file_lock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_file_lock_tests PRIVATE libkernel libc) configure_windows_unwind(guest_file_lock_tests) - foreach(fileLockCase IN ITEMS duplicate lifetime conversion threads streams process) + foreach(fileLockCase IN ITEMS duplicate lifetime conversion threads streams process directory) add_test(NAME guest_file_lock_${fileLockCase} COMMAND guest_file_lock_tests ${fileLockCase}) set_tests_properties(guest_file_lock_${fileLockCase} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2146bbe2d6..528d6e4524 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -843,6 +843,9 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { #endif const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); +#ifdef _WIN32 + if (File::IsDirectoryDescriptor(d)) return 0; +#endif if (NativeFlock(d, type | (operation & 4)) != 0) { #ifdef _WIN32 const auto error = ::GetLastError(); diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 3fb7ac7b71..4bb5835820 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -296,6 +296,23 @@ static void Processes() { Require(ChildTry(fixtures, 2) == 0); } +static void Directories() { + const std::filesystem::path directory = std::filesystem::temp_directory_path() / ("anyps5-flock-dir-" + std::to_string(::GetCurrentProcessId())); + std::filesystem::create_directories(directory); + const std::string path = directory.string(); + const int descriptor = open_nid_postfix(path.c_str(), 0x20000, 0); + Require(descriptor >= 0); + Require(flock_nid_postfix(descriptor, 1 | 4) == 0); + Require(flock_nid_postfix(descriptor, 2 | 4) == 0); + Require(flock_nid_postfix(descriptor, 2) == 0); + Require(flock_nid_postfix(descriptor, 8) == 0); + Require(flock_nid_postfix(descriptor, 0) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(descriptor, 4) == -1 && *__error_nid_postfix() == 9); + Require(close_nid_postfix(descriptor) == 0); + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); +} + int main(int argc, char** argv) { if (argc >= 4 && std::string(argv[1]) == "child") { int busy = 0; @@ -320,5 +337,6 @@ int main(int argc, char** argv) { else if (mode == "threads") Threads(); else if (mode == "streams") Streams(); else if (mode == "process") Processes(); + else if (mode == "directory") Directories(); else std::abort(); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 7028b833ac..550dc6281a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -238,3 +238,4 @@ - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured +- [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console From 546d62d6b4edfa643b4f9b7788616b559e344299 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 13:46:12 +0200 Subject: [PATCH 0331/1360] feat(libSceNgs2): report the ATRAC9 frame layout in sceNgs2GetWaveformFrameInfo --- core/libs/prx/libSceNgs2.native/src/Atrac9.cpp | 6 +++++- core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp | 4 +++- docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 8985bc004c..2bd6b4de5e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -439,7 +439,11 @@ int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat* format, uint if (format->waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: the frame info of waveform type " + Ngs2Hex(format->waveform_type) + " is not implemented"); Atrac9CodecInfo codec{}; if (!MatchingCodecInfo(*format, codec)) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; - throw std::runtime_error("NGS2: the decoder delay of an ATRAC9 waveform format is not implemented"); + *out_frame_size = static_cast(codec.superframeSize); + *out_num_frame_samples = static_cast(codec.frameSamples * codec.framesInSuperframe); + *out_units_per_frame = static_cast(codec.framesInSuperframe); + *out_num_delay_samples = static_cast(codec.frameSamples); + return SCE_NGS2_OK; } } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 065724161b..87ebee6ab4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -212,7 +212,9 @@ static void TestFrameInfo() { for (const auto& format : supported) { Ngs2WaveformBlock block{}; Require(sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes); - Require(FrameInfoThrows(format)); + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_OK); + Require(values[0] == SuperframeBytes && values[1] == SuperframeSamples && values[2] == 4 && values[3] == SuperframeSamples / 4); } for (const auto& format : rejected) { Ngs2WaveformBlock block{}; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 266d5c1ac9..e56621e83a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,7 +24,7 @@ - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before -- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - no source gives its behaviour; shadPS4 only stubs it. The outputs are taken from their names to be the frame size, frame samples, units per frame and delay samples of [`OrbisNgs2WaveformInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.h#L54-L75). Only `PCM_I16L` answers (channels * 2 bytes, 1 sample, 1 unit, 0 delay, as for parsed 16-bit PCM, itself assumed). ATRAC9 throws after the format check: its delay is the per-file encoder delay of the `fact` chunk ([vgmstream](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c#L1129-L1137)), so no codec value is known. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad channel count or rate `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified on a console +- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console, and shadPS4 only stubs it. The outputs are taken to be the frame size, frame samples, units per frame and delay of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes, the superframe samples, the frames per superframe and a delay of one frame, whatever the `fact` chunk says ([info layout](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L432-L440)); prosper does not cover this call itself. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad format `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [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 From df0a74e7c64849e2ecf698f37de9227b606a807a Mon Sep 17 00:00:00 2001 From: Gabrieljr42 Date: Wed, 21 Oct 2026 08:48:05 -0300 Subject: [PATCH 0332/1360] fix(ajm): parse protected MP3 frame metadata correctly --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 61 ++++++++++------------ core/libs/tests/GuestAjm.cpp | 26 ++++++++- docs/dev/debt/libSceAjm.native.md | 2 +- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index ea2955bc5a..1a27be97b1 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -853,12 +853,13 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame, int parseOfl) { const std::size_t end = std::min(streamSize, frame->frame_size); - if (end <= 4) return; - const std::size_t available = end - 4; + const std::size_t sideInfoStart = 4 + (((header >> 16u) & 1u) == 0 ? 2u : 0u); + if (end <= sideInfoStart) return; + const std::size_t available = end - sideInfoStart; std::size_t bit = 0; const auto readBits = [&](std::uint32_t count) { std::uint32_t value = 0; - for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u); + for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[sideInfoStart + bit / 8] >> (7 - bit % 8)) & 1u : 0u); return value; }; const bool mpeg1 = ((header >> 19u) & 3u) == 3u; @@ -872,13 +873,14 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint } } bit += mainDataBits; - std::size_t at = 4 + std::min((bit + 7) / 8, available); + std::size_t at = sideInfoStart + std::min((bit + 7) / 8, available); - std::size_t tagAt = 0; - for (const std::size_t candidate : std::initializer_list{at + 2, at, (mpeg1 ? (mono ? 23u : 38u) : (mono ? 15u : 23u)), (mpeg1 ? (mono ? 21u : 36u) : (mono ? 13u : 21u))}) { - if (candidate + 8 <= end && (std::memcmp(stream + candidate, "Xing", 4) == 0 || std::memcmp(stream + candidate, "Info", 4) == 0)) { - tagAt = candidate; - break; + std::size_t tagAt = at; + if (tagAt + 8 > end || (std::memcmp(stream + tagAt, "Xing", 4) != 0 && std::memcmp(stream + tagAt, "Info", 4) != 0)) { + const std::size_t fixedOffset = mpeg1 ? (mono ? 21u : 36u) : (mono ? 13u : 21u); + tagAt = 0; + if (fixedOffset + 8 <= end && (std::memcmp(stream + fixedOffset, "Xing", 4) == 0 || std::memcmp(stream + fixedOffset, "Info", 4) == 0)) { + tagAt = fixedOffset; } } @@ -914,15 +916,8 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint return; } - std::size_t vbriAt = 0; - for (const std::size_t candidate : std::initializer_list{36u, at, at + 2, 38u}) { - if (candidate + 26 <= end && std::memcmp(stream + candidate, "VBRI", 4) == 0) { - vbriAt = candidate; - break; - } - } - - if (vbriAt != 0) { + const std::size_t vbriAt = end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0 ? 36 : at; + if (vbriAt + 26 <= end && std::memcmp(stream + vbriAt, "VBRI", 4) == 0) { frame->encoder_delay = ReadBigEndian(stream + vbriAt + 6, 2); frame->ofl_type = MP3_OFL_VBRI; if (parseOfl == 1 && frame->frame_size <= streamSize) { @@ -936,23 +931,20 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint return; } - for (std::size_t fghAt = std::min(at, std::size_t{4}); fghAt + 10 <= end; ++fghAt) { - if (stream[fghAt] == 0xB4u) { - std::uint8_t crc = 0xFF; - for (std::size_t i = 0; i < 9; ++i) { - for (int shift = 7; shift >= 0; --shift) { - const bool same = ((crc >> 7u) & 1u) == ((stream[fghAt + i] >> shift) & 1u); - crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); - } - } - if (stream[fghAt + 9] == crc) { - frame->encoder_delay = ReadBigEndian(stream + fghAt + 1, 2); - frame->total_samples = ReadBigEndian(stream + fghAt + 3, 4); - frame->ofl_type = MP3_OFL_FGH; - break; - } + while (at + 9 < end && stream[at] != 0xB4u) ++at; + if (at + 9 >= end) return; + std::uint8_t crc = 0xFF; + for (std::size_t i = 0; i < 9; ++i) { + for (int shift = 7; shift >= 0; --shift) { + const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); + crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); } } + if (stream[at + 9] == crc) { + frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); + frame->total_samples = ReadBigEndian(stream + at + 3, 4); + frame->ofl_type = MP3_OFL_FGH; + } } int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { @@ -969,9 +961,10 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par const std::uint32_t layer = (header >> 17u) & 3u; const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; - const bool valid = (header >> 21u) == 0x7FFu && layer == 1u && sampleRate != 0 && bitrate != 0; + const bool valid = (header >> 21u) == 0x7FFu && layer != 0 && sampleRate != 0 && bitrate != 0; AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (parseOfl && layer != 1u) return SCE_AJM_ERROR_INVALID_PARAMETER; const bool mpeg1 = version == 3; frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 5c726cd1a1..4fca98867a 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -393,12 +393,18 @@ void TestMp3ParseFrame() { Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); for (const std::uint8_t layer : {0u, 2u, 3u}) { const std::uint8_t header[4] = {0xFF, static_cast(0xF9u | (layer << 1u)), 0x90, 0x04}; - for (const int parseOfl : {0, 1}) { + if (layer == 0) { std::memset(&frame, 0xA5, sizeof(frame)); const auto previous = frame; - Require(sceAjmDecMp3ParseFrame(header, sizeof(header), parseOfl, &frame) == invalidParameter); + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), 0, &frame) == invalidParameter); Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); + } else { + Require(ParsesTo(header, sizeof(header), 417, 2, 1152, 128000, 44100)); } + std::memset(&frame, 0xA5, sizeof(frame)); + const auto previous = frame; + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), 1, &frame) == invalidParameter); + Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); } } @@ -493,6 +499,15 @@ void TestMp3ParseOfl() { const auto fgh = Mp3Frame(FGH_FRAME, sizeof(FGH_FRAME), 417); Require(OflParsesTo(fgh, 417, 0, 1105, 441000, 3)); Require(OflParsesTo(Mp3Frame(FGH_BAD_CRC_FRAME, sizeof(FGH_BAD_CRC_FRAME), 417), 417, 0, 0, 0, 0)); + const auto marker = std::find(fgh.begin() + 36, fgh.end(), 0xB4u); + Require(marker != fgh.end() && static_cast(fgh.end() - marker) >= 10); + std::vector misleadingFgh(417, 0); + std::copy_n(fgh.begin(), 4, misleadingFgh.begin()); + const auto markerOffset = static_cast(marker - fgh.begin()); + std::copy_n(fgh.begin() + static_cast(markerOffset), 10, misleadingFgh.begin() + 36); + misleadingFgh[45] ^= 0x01u; + std::copy_n(fgh.begin() + static_cast(markerOffset), 10, misleadingFgh.begin() + 48); + Require(OflParsesTo(misleadingFgh, 417, 0, 0, 0, 0)); auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); Require(OflParsesTo(vbri, 417, 0, 576, 0, 2)); vbri.insert(vbri.end(), fgh.begin(), fgh.end()); @@ -507,6 +522,13 @@ void TestMp3ParseOfl() { const auto protectedMpeg2 = ProtectedMp3Frame(mpeg2, tagAfterCrc); Require(OflParsesTo(protectedMpeg2, 208, 20, 576 + 576 + 529, 10000, 1)); } + auto protectedNonzeroSideInfo = ProtectedMp3Frame(stereo, true); + protectedNonzeroSideInfo[8] = 0x00; + protectedNonzeroSideInfo[9] = 0x10; + std::move_backward(protectedNonzeroSideInfo.begin() + 38, protectedNonzeroSideInfo.end() - 2, protectedNonzeroSideInfo.end()); + protectedNonzeroSideInfo[38] = 0; + protectedNonzeroSideInfo[39] = 0; + Require(OflParsesTo(protectedNonzeroSideInfo, 417, 10, 1152 + 576 + 529, 10000, 1)); const auto protectedFgh = ProtectedMp3Frame(fgh, true); Require(OflParsesTo(protectedFgh, 417, 0, 1105, 441000, 3)); auto monoVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index a0c7cff06d..1f3f674bc2 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -8,7 +8,7 @@ - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. - [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder -- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: only Layer III is accepted and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag with or without CRC offset, VBRI at byte 36 or after side information, 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 +- [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: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description. - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` From c5df2f4ce045a07eba7cc29bec1e286b442a50c2 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 08:43:54 -0300 Subject: [PATCH 0333/1360] fix(libSceNet): accept every sceNetMemoryAllocate flag as measured on a PS5 A PS5 (firmware 13.60) allocates with any flag value, zero-fills only for flag 2 and returns 64-byte aligned blocks. Record the measured behaviour of sceNetGetIfName, sceNetGetRandom, sceNetGetSystemTime, sceNetMemoryFree and sceNetUsleep, which already match the console, and keep only their open gaps. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 5 ++--- core/libs/tests/GuestSceNet.cpp | 32 +++++++++++++++++++++--------- docs/dev/debt/libSceNet.md | 9 +++------ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d6a9bd0b15..8723f16ddb 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2250,12 +2250,11 @@ int APS5_VABI sceNetIoctl(void) { void* APS5_VABI sceNetMemoryAllocate(std::int64_t size, int flags) { constexpr int zeroFill = 2; - if ((flags & ~zeroFill) != 0) - throw std::runtime_error("sceNetMemoryAllocate: flags " + std::to_string(flags) + " are not supported"); + constexpr std::size_t blockAlignment = 64; if (size <= 0) return nullptr; void* memory = nullptr; try { - memory = GuestHeap::GuestHeapAllocate_nid_postfix(static_cast(size)); + memory = GuestHeap::GuestHeapAlign_nid_postfix(blockAlignment, static_cast(size)); } catch (const std::bad_alloc&) { return nullptr; } catch (const std::length_error&) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3f00cfe6d4..4af7e936ae 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -297,12 +297,18 @@ static void CheckRandom() { Require(Failed(sceNetGetRandom(nullptr), 22)); std::array values{}; for (auto& value : values) Require(sceNetGetRandom(&value) == 0); + std::array guarded{0x5a5a5a5au, 0x5a5a5a5au}; + Require(sceNetGetRandom(guarded.data()) == 0 && guarded[1] == 0x5a5a5a5au && *sceNetErrnoLoc() == 22); Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); } static void CheckNetMemory() { - Require(sceNetMemoryAllocate(0, 0) == nullptr); - Require(sceNetMemoryAllocate(-1, 2) == nullptr); + *sceNetErrnoLoc() = 33; + for (const std::int64_t size : {std::int64_t{0}, std::int64_t{-1}, INT64_MIN}) { + Require(sceNetMemoryAllocate(size, 0) == nullptr); + Require(sceNetMemoryAllocate(size, 2) == nullptr); + } + Require(*sceNetErrnoLoc() == 33); auto* zeroed = static_cast(sceNetMemoryAllocate(4096, 2)); Require(zeroed != nullptr); Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); @@ -313,14 +319,20 @@ static void CheckNetMemory() { Require(zeroed[4095] == 0xa5); sceNetMemoryFree(plain); sceNetMemoryFree(zeroed); - sceNetMemoryFree(nullptr); - bool threw = false; - try { - sceNetMemoryAllocate(16, 1); - } catch (const std::runtime_error&) { - threw = true; + for (const int flags : {1, 3, 4, 8, 0x10, 0x100, INT32_MIN}) { + auto* block = static_cast(sceNetMemoryAllocate(16, flags)); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + if ((flags & 2) != 0) Require(std::all_of(block, block + 16, [](std::uint8_t byte) { return byte == 0; })); + std::memset(block, 0x5a, 16); + sceNetMemoryFree(block); } - Require(threw); + for (const std::int64_t size : {1, 7, 256, 65536}) { + void* block = sceNetMemoryAllocate(size, 0); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + sceNetMemoryFree(block); + } + sceNetMemoryFree(nullptr); + Require(*sceNetErrnoLoc() == 33); } static void CheckUnboundSocketName() { @@ -896,6 +908,8 @@ static void CheckUsleep() { Require(std::chrono::steady_clock::now() - start >= std::chrono::microseconds(2000)); *sceNetErrnoLoc() = 0; Require(Failed(sceNetUsleep(-1), 22)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(INT32_MIN), 22)); } static void CheckEpollIdent() { diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fa35cc1780..76917cc204 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -38,19 +38,14 @@ - [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware - [sceNetGetIfnameNumList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetNameToIndex](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` - [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware -- [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSocketInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -63,7 +58,6 @@ - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware - [sceNetShowIfconfigForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRouteForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowNetstatForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -178,6 +172,9 @@ ### Functional +- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) takes its value from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does; the console's generator is unknown. The `(uint32_t* output)` signature, the four-byte write and the `EINVAL` failure for a null output were measured on a PS5 (firmware 13.60) +- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) takes its blocks from the guest heap; the console's pool size and what it returns once the pool is exhausted are unmeasured, and AnyPS5 returns null without setting an errno, as the console does for a size of 0 or less. A reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents in AnyPS5. Measured on a PS5 (firmware 13.60): a size of 0, -1 or `INT64_MIN` returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 have no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned +- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) prints nothing for a negative count, where the console prints `[LIBNET] usleep ` to standard output before failing with `EINVAL`. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. From 36226741f56ebb3a1d8902ebdc6e612a93975ff6 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 19:09:51 +0300 Subject: [PATCH 0334/1360] fix(libc): open a directory with fopen in a read mode on Windows FreeBSD 11 fopen opens the path with _open, and vn_open_vnode refuses a directory only for FWRITE or O_TRUNC, so fopen of a directory in a read mode succeeds; Linux hosts already open it. On Windows CreateFileW without FILE_FLAG_BACKUP_SEMANTICS refused it with EACCES. fopen now opens such a path as a libkernel directory descriptor, which libkernel registers in libc when it loads, and fclose and freopen remove it from libkernel's table. Reads on the stream fail with EISDIR and set the error indicator, as on a Linux host and FreeBSD's tmpfs: the Windows CRT fails them already but reports EINVAL. Writes fail with EBADF, and fseek, ftell and ungetc work, as on any read-only stream. --- core/libs/prx/libc/include/FileStream.hpp | 17 ++++ core/libs/prx/libc/src/FileAndHeap.cpp | 78 ++++++++++++++++--- core/libs/prx/libc/src/Formatting.cpp | 2 + core/libs/prx/libc/src/StandardStreams.cpp | 3 + .../File/src/DirectoryDescriptor.cpp | 14 ++++ core/libs/tests/FileStreams.cpp | 11 +-- core/libs/tests/GuestFilesystem.cpp | 46 +++++++++++ docs/dev/debt/libc.md | 2 +- 8 files changed, 151 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 1b9954d873..7a81d29f07 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -23,6 +23,8 @@ struct UcrtStreamState { int count; long flags; }; +extern "C" void LibcRegisterDirectoryStreams_nid_no_patch(int (*open)(const wchar_t* path), void (*forget)(int descriptor)); +extern "C" void ForgetDirectoryStream_nid_no_patch(int descriptor); #else #include #endif @@ -66,6 +68,7 @@ class FileStream { bool _dynamic; bool guestError = false; bool randomDevice = false; + bool directory = false; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { @@ -130,6 +133,14 @@ public: return randomDevice; } + void MarkDirectory() { + directory = true; + } + + bool IsDirectory() const { + return directory; + } + bool Reopen(const std::filesystem::path& filename, const char* mode, [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); @@ -140,6 +151,7 @@ public: std::wstring wideMode; for (const char character : std::string_view(mode)) wideMode.push_back(static_cast(character)); const int replaced = previous ? _fileno(previous) : -1; + if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(replaced); auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); if (open && !filename.empty()) { @@ -260,6 +272,10 @@ public: #endif } + void KeepDirectoryReadError(bool failed) { + if (failed && directory && (_guest.flags & 0x40) != 0) errno = 21; + } + void SetError() { guestError = true; SyncStatus(); @@ -276,6 +292,7 @@ public: GetHandle(); #ifdef _WIN32 GuestFileLockForget_nid_no_patch(::_fileno(_handle)); + if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); #endif const int result = std::fclose(std::exchange(_handle, nullptr)); _guest.flags = 0; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 410fa6f309..0b6e86284c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -65,6 +65,19 @@ static std::optional ParseFileMode(const char* mode) { #ifdef _WIN32 static constexpr DWORD AppendOnlyAccess = FILE_GENERIC_WRITE & ~FILE_WRITE_DATA; + +static std::optional HostOpenName(const std::filesystem::path& path) { + auto filename = path.native(); + if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { + std::error_code pathError; + auto absolutePath = std::filesystem::absolute(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return std::nullopt; } + const auto absolute = absolutePath.lexically_normal().make_preferred().native(); + if (absolute.size() >= MAX_PATH) + filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + } + return filename; +} #endif static std::FILE* OpenNative(const std::filesystem::path& path, const std::string& nativeMode) { @@ -76,16 +89,9 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin const DWORD disposition = access == 'r' ? OPEN_EXISTING : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; - auto filename = path.native(); - if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { - std::error_code pathError; - auto absolutePath = std::filesystem::absolute(path, pathError); - if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } - const auto absolute = absolutePath.lexically_normal().make_preferred().native(); - if (absolute.size() >= MAX_PATH) - filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; - } - const auto handle = CreateFileW(filename.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + const auto filename = HostOpenName(path); + if (!filename) return nullptr; + const auto handle = CreateFileW(filename->c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { errno = WindowsOpenError_nid_no_patch(GetLastError()); @@ -307,6 +313,47 @@ static int OpenFailure(const std::filesystem::path& path, bool writes, int hostE return writes && error == 13 && std::filesystem::is_directory(path, status) ? 21 : error; } +#ifdef _WIN32 +static int (*g_openDirectoryDescriptor)(const wchar_t*) = nullptr; +static void (*g_forgetDirectoryDescriptor)(int) = nullptr; + +void LibcRegisterDirectoryStreams_nid_no_patch(int (*open)(const wchar_t*), void (*forget)(int)) { + g_openDirectoryDescriptor = open; + g_forgetDirectoryDescriptor = forget; +} + +void ForgetDirectoryStream_nid_no_patch(int descriptor) { + if (!g_forgetDirectoryDescriptor) throw std::runtime_error("ForgetDirectoryStream: libkernel has not registered its directory descriptors"); + g_forgetDirectoryDescriptor(descriptor); +} + +static FileStream* OpenDirectoryStream(const std::filesystem::path& path) { + if (!g_openDirectoryDescriptor) throw std::runtime_error("fopen: libkernel has not registered its directory descriptors"); + const auto filename = HostOpenName(path); + if (!filename) { errno = GuestErrnoFromHost_nid_no_patch(errno); return nullptr; } + const auto release = [](void* pointer) { ::operator delete(pointer); }; + std::unique_ptr storage(::operator new(sizeof(FileStream)), release); + const int descriptor = g_openDirectoryDescriptor(filename->c_str()); + if (descriptor < 0) { + const int error = GuestErrnoFromHost_nid_no_patch(errno); + errno = error == 2 ? NotFoundError_nid_no_patch(path) : error; + return nullptr; + } + auto* native = ::_fdopen(descriptor, "rb"); + if (!native) { + const int error = GuestErrnoFromHost_nid_no_patch(errno); + ForgetDirectoryStream_nid_no_patch(descriptor); + ::_close(descriptor); + errno = error; + return nullptr; + } + auto* stream = new (storage.get()) FileStream(native, true); + storage.release(); + stream->MarkDirectory(); + return stream; +} +#endif + FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { if (!stream || !mode) { errno = 22; return nullptr; } if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. @@ -370,7 +417,13 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { - errno = OpenFailure(fpath, parsed->writes, errno); + const int hostError = errno; +#ifdef _WIN32 + std::error_code status; + if (!parsed->writes && GuestErrnoFromHost_nid_no_patch(hostError) == 13 && std::filesystem::is_directory(fpath, status)) + return OpenDirectoryStream(fpath); +#endif + errno = OpenFailure(fpath, parsed->writes, hostError); if (errno == 2) errno = NotFoundError_nid_no_patch(fpath); return nullptr; } @@ -445,6 +498,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File if (!stream->SwitchToReading()) return 0; const auto result = std::fread(buffer, size, count, handle); stream->SyncStatus(); + stream->KeepDirectoryReadError(result < count); return result; } @@ -529,7 +583,7 @@ int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { int APS5_VABI fflush_nid_postfix(FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } std::FILE* native = stream ? GetStreamHandle(stream) : nullptr; - if (stream && stream->IsRandomDevice()) return 0; + if (stream && (stream->IsRandomDevice() || stream->IsDirectory())) return 0; #ifdef __linux__ if (native && !__fwriting(native)) return 0; #endif diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 07fc5468a8..1ab1877529 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -378,6 +378,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { va_end(args); #endif stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } @@ -656,6 +657,7 @@ int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList va_end(copy); #endif stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 03cdaed4cd..2cd656f01f 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -39,6 +39,7 @@ int APS5_VABI fgetc_nid_postfix(FileStream* stream) { if (!stream->SwitchToReading()) return -1; const int result = std::fgetc(native); stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { @@ -94,6 +95,7 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { const int result = ::getc_unlocked(native); #endif stream->SyncUnlockedStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } @@ -140,6 +142,7 @@ char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { if (!stream->SwitchToReading()) return nullptr; auto* result = std::fgets(buffer, size, native); stream->SyncStatus(); + stream->KeepDirectoryReadError(result == nullptr); return result; } int APS5_VABI feof_nid_postfix(FileStream* stream) { diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 7727bb9000..e140a6b0f4 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -8,6 +8,7 @@ #ifdef _WIN32 +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include @@ -63,6 +64,19 @@ void Load(DirectoryState& state, int fd) { } } +int OpenStreamDirectory(const wchar_t* path) { + return File::OpenDirectoryDescriptor(path); +} + +struct StreamRegistration { + StreamRegistration() { LibcRegisterDirectoryStreams_nid_no_patch(&OpenStreamDirectory, &File::ForgetDirectoryDescriptor); } + ~StreamRegistration() { LibcRegisterDirectoryStreams_nid_no_patch(nullptr, nullptr); } + StreamRegistration(const StreamRegistration&) = delete; + StreamRegistration& operator=(const StreamRegistration&) = delete; +}; + +const StreamRegistration g_streamRegistration; + } namespace File { diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index f53419748c..55445e2707 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -234,18 +234,11 @@ int main(int argc, char** argv) { errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); Require(errno == ENOENT); -#ifdef _WIN32 - const std::string directoryMode = "rb"; - constexpr int directoryError = EACCES; -#else - const std::string directoryMode = "wb"; - constexpr int directoryError = EISDIR; -#endif const std::string directoryName = std::string(argv[1]) + ".dir"; Require(std::filesystem::create_directory(directoryName)); errno = 0; - Require(fopen_nid_postfix(directoryName.c_str(), directoryMode.c_str()) == nullptr); - Require(errno == directoryError); + Require(fopen_nid_postfix(directoryName.c_str(), "wb") == nullptr); + Require(errno == EISDIR); Require(std::filesystem::remove(directoryName)); const std::u8string unicodeName = u8"セーブé.tmp"; const std::string guestName(unicodeName.begin(), unicodeName.end()); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index c1a1a863bb..5faa99a95b 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -85,6 +85,13 @@ FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*) std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); int APS5_VABI fclose_nid_postfix(FileStream*); int APS5_VABI fgetc_nid_postfix(FileStream*); +int APS5_VABI fileno_nid_postfix(FileStream*); +char* APS5_VABI fgets_nid_postfix(char*, int, FileStream*); +int APS5_VABI fscanf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fputc_nid_postfix(int, FileStream*); +int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftell_nid_postfix(FileStream*); +void APS5_VABI clearerr_nid_postfix(FileStream*); int APS5_VABI feof_nid_postfix(FileStream*); int APS5_VABI ferror_nid_postfix(FileStream*); int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); @@ -604,6 +611,45 @@ int main() { for (const char* openMode : {"w", "wb", "r+", "a"}) { Require(fopen_nid_postfix(root.string().c_str(), openMode) == nullptr && *__error_nid_postfix() == 21); } + { + const auto listed = root / "listed.txt"; + std::ofstream(listed, std::ios::binary) << "listed"; + for (const char* openMode : {"r", "rb", "re"}) { + auto* directoryStream = fopen_nid_postfix(root.string().c_str(), openMode); + Require(directoryStream != nullptr); + const int directoryDescriptor = fileno_nid_postfix(directoryStream); + FileStat directoryStatus{}; + Require(directoryDescriptor >= 0 && sceKernelFstat(directoryDescriptor, &directoryStatus) == 0 && (directoryStatus.st_mode & 0170000) == 0040000); + Require(feof_nid_postfix(directoryStream) == 0 && ferror_nid_postfix(directoryStream) == 0 && fflush_nid_postfix(directoryStream) == 0); + char unread[8] = {}; + int scanned = 0; + *__error_nid_postfix() = 0; + Require(fread_nid_postfix(unread, 1, sizeof(unread), directoryStream) == 0 && *__error_nid_postfix() == 21); + Require(ferror_nid_postfix(directoryStream) != 0 && feof_nid_postfix(directoryStream) == 0); + clearerr_nid_postfix(directoryStream); + Require(ferror_nid_postfix(directoryStream) == 0); + *__error_nid_postfix() = 0; + Require(fgetc_nid_postfix(directoryStream) == -1 && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + *__error_nid_postfix() = 0; + Require(fgets_nid_postfix(unread, sizeof(unread), directoryStream) == nullptr && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + *__error_nid_postfix() = 0; + Require(fscanf_nid_postfix(directoryStream, "%d", &scanned) == -1 && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + Require(fseek_nid_postfix(directoryStream, 0, 0) == 0 && ftell_nid_postfix(directoryStream) == 0); + *__error_nid_postfix() = 0; + Require(fputc_nid_postfix('x', directoryStream) == -1 && *__error_nid_postfix() == 9 && ferror_nid_postfix(directoryStream) != 0); + Require(fclose_nid_postfix(directoryStream) == 0); +#ifdef _WIN32 + const int reused = open_nid_postfix(listed.string().c_str(), 0, 0); + char text[6]{}; + Require(reused == directoryDescriptor && read_nid_postfix(reused, text, sizeof(text)) == 6 && std::memcmp(text, "listed", 6) == 0); + Require(close_nid_postfix(reused) == 0); +#endif + } + Require(std::filesystem::remove(listed)); + } Require(mkdir_nid_postfix((root / "missing" / "child").string().c_str(), 0700) == -1 && *__error_nid_postfix() == 2); Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..0805d05317 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -95,7 +95,7 @@ - On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows fail to open a directory in a read mode with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows: `freopen` onto a directory in a read mode fails with `EACCES`, where FreeBSD opens it. Reading a directory stream from `fopen` fails with `EISDIR` and sets the error indicator on every host, as FreeBSD 11 tmpfs does; FreeBSD 11 FFS returns the directory's raw blocks instead, and what the console's filesystems return is unverified - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way From 99c195e79a7763aea3a376afb2d3f58b2c401947 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 19:00:50 +0300 Subject: [PATCH 0335/1360] fix(libc): leave tzname[1] as three spaces for a zone without daylight saving time FreeBSD 11 localtime.c initializes tzname to WILDABBR (" ") and settzname() resets both entries to it before filling them from the zone's types, so a zone with no daylight saving type keeps " " in tzname[1]. Windows left it empty and Linux repeated tzname[0]. --- core/libs/prx/libc/src/Time.cpp | 24 +++++++++++++++++------- core/libs/tests/GuestTimeConversion.cpp | 8 +++++--- docs/dev/debt/libc.md | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index c3499f8047..afb22bd316 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -183,6 +183,7 @@ std::recursive_mutex& zoneMutex() { } char** HostZoneNames(); +char* GuestZoneName(int index); void applyGuestZone() { static bool initialized = false; @@ -205,9 +206,8 @@ void applyGuestZone() { #else ::tzset(); #endif - char** names = HostZoneNames(); - tzname_nid_postfix[0] = names[0]; - tzname_nid_postfix[1] = names[1]; + tzname_nid_postfix[0] = GuestZoneName(0); + tzname_nid_postfix[1] = GuestZoneName(1); } int& zoneDepth() { @@ -515,11 +515,22 @@ char** HostZoneNames() { return ::tzname; #endif } + +char* GuestZoneName(int index) { + static char unnamed[] = " "; + char** names = HostZoneNames(); +#ifdef _WIN32 + const bool daylightSaving = _daylight != 0; +#else + const bool daylightSaving = ::daylight != 0; +#endif + return index == 1 && !daylightSaving ? unnamed : names[index]; +} } extern "C" { -char* tzname_nid_postfix[2] = {HostZoneNames()[0], HostZoneNames()[1]}; +char* tzname_nid_postfix[2] = {GuestZoneName(0), GuestZoneName(1)}; int64_t timezone_nid_postfix = 0; int daylight_nid_postfix = 0; @@ -533,9 +544,8 @@ int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { const ZoneScope zone; - char** names = HostZoneNames(); - tzname_nid_postfix[0] = names[0]; - tzname_nid_postfix[1] = names[1]; + tzname_nid_postfix[0] = GuestZoneName(0); + tzname_nid_postfix[1] = GuestZoneName(1); const int64_t now = libc_time_nid_postfix(nullptr); GuestTm universal{}; GuestTm local{}; diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 7b11ebe2ef..f3fca8ad13 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -359,7 +359,8 @@ static void CheckGuestZone() { Require(setenv_nid_postfix("TZ", "EST5", 1) == 0, "Could not set the guest time zone without tzset"); Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 19 && result.tm_gmtoff == -18000, "localtime ignored a guest TZ change made without tzset"); - Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0, "Implicit guest TZ update did not refresh tzname"); + Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], " ") == 0, + "Implicit guest TZ update did not refresh tzname"); Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); tzset_nid_postfix(); } @@ -527,8 +528,9 @@ int main() { GuestTm shifted{}; Require(localtime_s_nid_postfix(&epoch, &shifted) == &shifted && shifted.tm_hour == 5 && shifted.tm_gmtoff == 18000, "tzset did not pick up the new TZ"); - Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr, - "tzname does not name the zone tzset set"); + Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr && + std::strcmp(tzname_nid_postfix[1], " ") == 0, + "tzname does not name the zone tzset set and leave the daylight name blank"); Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set the guest time zone"); tzset_nid_postfix(); Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], "EDT") == 0, diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..bfc4a0c442 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -117,7 +117,7 @@ - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount -- [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known +- [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. The console's names are not known - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written From c46ff25ee7f8e1df1d846f3ed7355cc7665a1f7b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:52:11 +0300 Subject: [PATCH 0336/1360] feat(libSceImeDialog): report the star state of an open dialog --- core/libs/prx/libSceImeDialog/Export.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index c9b181b025..37bce0671f 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -190,7 +190,10 @@ int APS5_VABI sceImeDialogForceClose(void) { } int APS5_VABI sceImeDialogGetCurrentStarState(int64_t state) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotInUse; + if (state == 0) return ErrorInvalidAddress; + *reinterpret_cast(state) = 0; return 0; } From 77f439cf285f6102ac6ada6cc826f1631bc45e28 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:52:11 +0300 Subject: [PATCH 0337/1360] feat(libSceImeDialog): open the dialog through sceImeDialogInitInternal --- core/libs/prx/libSceImeDialog/Export.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 37bce0671f..fb194af1f5 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -198,8 +198,7 @@ int APS5_VABI sceImeDialogGetCurrentStarState(int64_t state) { } int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceImeDialogInit(param, extended); } int APS5_VABI sceImeDialogSetPanelPosition(int32_t posx, int32_t posy) { From 2c0f778b933e5274ee46ac42f542b0be9b69406e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:52:11 +0300 Subject: [PATCH 0338/1360] feat(libSceImeDialog): refuse panel moves without a movable panel --- core/libs/prx/libSceImeDialog/Export.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index fb194af1f5..c19acdd36a 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -17,6 +17,7 @@ constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; constexpr std::uint32_t MaxTextLength = 2048; constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorSuspending = static_cast(0x80bc0009u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorNotInUse = static_cast(0x80bc0107u); constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); @@ -201,9 +202,9 @@ int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { return sceImeDialogInit(param, extended); } -int APS5_VABI sceImeDialogSetPanelPosition(int32_t posx, int32_t posy) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceImeDialogSetPanelPosition(int32_t, int32_t) { + std::lock_guard lock(g_mutex); + return g_dialog.status == StatusNone ? ErrorNotInUse : ErrorSuspending; } From 0e82cc6484e769d3a7da3f547fbcf928be908de3 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:52:11 +0300 Subject: [PATCH 0339/1360] test(libSceImeDialog): cover the star state, internal init and panel position --- core/libs/tests/GuestImeDialog.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/tests/GuestImeDialog.cpp b/core/libs/tests/GuestImeDialog.cpp index 574398c192..756a809fd7 100644 --- a/core/libs/tests/GuestImeDialog.cpp +++ b/core/libs/tests/GuestImeDialog.cpp @@ -14,6 +14,9 @@ int APS5_VABI sceImeDialogGetResult(Result*); int APS5_VABI sceImeDialogAbort(void); int APS5_VABI sceImeDialogTerm(void); int APS5_VABI sceImeDialogForceClose(void); +int APS5_VABI sceImeDialogGetCurrentStarState(std::int64_t); +int APS5_VABI sceImeDialogInitInternal(const Param*, const ExtendedParam*); +int APS5_VABI sceImeDialogSetPanelPosition(std::int32_t, std::int32_t); } namespace { @@ -25,6 +28,7 @@ constexpr std::uint32_t EndStatusOk = 0; constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorSuspending = static_cast(0x80bc0009u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorNotInUse = static_cast(0x80bc0107u); constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); @@ -186,4 +190,21 @@ int main() { Require(sceImeDialogInit(&noBuffer, nullptr) == ErrorInvalidInputTextBuffer); Require(sceImeDialogGetStatus() == StatusNone); Require(!ImeCapturesKeyboard_nid_postfix()); + + std::uint32_t star = 0xa5a5a5a5u; + const auto starAddress = static_cast(reinterpret_cast(&star)); + Require(sceImeDialogGetCurrentStarState(starAddress) == ErrorNotInUse && star == 0xa5a5a5a5u); + Require(sceImeDialogSetPanelPosition(640, 360) == ErrorNotInUse); + Require(sceImeDialogInitInternal(nullptr, nullptr) == ErrorInvalidAddress); + Require(sceImeDialogInitInternal(&empty, nullptr) == ErrorInvalidMaxTextLength); + Require(sceImeDialogGetStatus() == StatusNone); + Require(sceImeDialogInitInternal(¶m, nullptr) == 0); + Require(sceImeDialogGetStatus() == StatusRunning && ImeCapturesKeyboard_nid_postfix()); + Require(sceImeDialogInitInternal(¶m, nullptr) == ErrorBusy); + Require(sceImeDialogGetCurrentStarState(0) == ErrorInvalidAddress); + Require(sceImeDialogGetCurrentStarState(starAddress) == 0 && star == 0); + Require(sceImeDialogSetPanelPosition(640, 360) == ErrorSuspending); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogSetPanelPosition(0, 0) == ErrorSuspending); + Close(); } From 3c5924518f4f23364cd7695281b033abd08a1a3d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:52:11 +0300 Subject: [PATCH 0340/1360] docs(debt): note where the internal ImeDialog calls come from --- docs/dev/debt/libSceImeDialog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md index 58f32f8d68..84726f50bc 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -6,7 +6,8 @@ ### Unknown function info -- [sceImeDialogForTestFunction, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console. They are declared solely to resolve known imports, `sceImeDialogForTestFunction` without parameters and the others with shadPS4's parameters; this is not an ABI claim, and every call throws. +- [sceImeDialogForTestFunction](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - declared without parameters to resolve a known import, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51); not measured on a console and not an ABI claim. +- [sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a console; signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/ime/ime_dialog.cpp). `sceImeDialogInitInternal` runs `sceImeDialogInit`, as shadPS4's two share one path apart from user flags this library does not keep. There is no movable panel, so `sceImeDialogSetPanelPosition` never succeeds: `DIALOG_NOT_IN_USE` without a dialog and `IME_SUSPENDING` while one is running or finished, the states in which shadPS4 refuses it too. The star state is always 0 - [sceImeDialogForceClose](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a PS5: its `int (void)` signature and behaviour follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.cpp#L571-L601) (PS4). It closes a running or finished dialog, as `sceImeDialogTerm` does, without writing the typed text to the title's buffer, which shadPS4 updates while typing and this library only on Enter, and returns `0x80bc0107` with no dialog open, the `NotInUse` code of [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Dialog/ImeDialog.cs#L224-L225), while this library's `sceImeDialogTerm` returns `0x80bc0002` in the same state. Closes when: measured on a PS5 - [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it From 505943c4ef9eb59d7f8ebe3411e060f6aa6e0873 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 07:55:36 -0400 Subject: [PATCH 0341/1360] feat(tools): press several buttons together from pad_input.py Some titles wait for a chord on their title screen: RollerCoaster Tycoon Adventures Deluxe (PPSA17330) asks for L1 + R1. The command file already carries a button bit field, but the helper accepted one name. Names joined by '+' are now combined into one command; unknown names, duplicates and release in a chord are rejected without touching the command file. Co-Authored-By: Claude Opus 5.5 --- docs/user/USAGE.md | 3 ++- tools/pad_input.py | 18 ++++++++++++++++-- tools/tests/test_pad_input.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 42270f520c..1b06bc7bc0 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -216,11 +216,12 @@ python3 tools/pad_input.py /tmp/anyps5-pad release python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 python3 tools/pad_input.py /tmp/anyps5-pad touch-right +python3 tools/pad_input.py /tmp/anyps5-pad l1+r1 ``` For timed button sequences, see [Controller timelines](INPUT_MAPPING.md#controller-timelines). -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Button names joined by `+`, such as `l1+r1`, are pressed together in one command; `release` cannot be joined. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### Video-out and pad telemetry diff --git a/tools/pad_input.py b/tools/pad_input.py index ab93227072..465957651c 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -52,14 +52,28 @@ def next_sequence(path): return sequence +def button_mask(value): + names = value.split("+") + if any(name not in BUTTONS for name in names) or ("release" in names and len(names) > 1) or len(set(names)) != len(names): + raise argparse.ArgumentTypeError(f"invalid button {value!r}: use one of {', '.join(BUTTONS)}, or several joined by '+'") + mask = 0 + for name in names: + mask |= BUTTONS[name] + return mask + + def main(): parser = argparse.ArgumentParser(description="Queue a controller pulse for APS5_PAD_INPUT_FILE.") parser.add_argument("path", type=Path) - parser.add_argument("button", choices=BUTTONS) + parser.add_argument("button", help="a button name, or several joined by '+' to press them together (for example l1+r1)") parser.add_argument("--hold-ms", type=int, default=300) parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() + try: + mask = button_mask(args.button) + except argparse.ArgumentTypeError as error: + parser.error(str(error)) if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") sticks = "" @@ -86,7 +100,7 @@ def main(): if os.geteuid() == 0: os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) - output.write(f"{sequence} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") + output.write(f"{sequence} {mask} {args.hold_ms}{sticks}\n") os.replace(temporary, args.path) finally: if temporary is not None: diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 76e820c325..bb557866d4 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -85,6 +85,22 @@ class PadInputTests(unittest.TestCase): self.assertEqual(self.queue(1000, "touch-left"), [1000, 0x200000, 300]) self.assertEqual(self.queue(1000, "touch-right"), [1001, 0x400000, 300]) + def test_joined_buttons_are_pressed_together(self): + self.assertEqual(self.queue(1000, "l1+r1"), [1000, 0x400 | 0x800, 300]) + self.assertEqual(self.queue(1000, "cross+square+up", "--hold-ms", "40"), [1001, 0x4000 | 0x8000 | 0x10, 40]) + + def test_invalid_joined_buttons_report_error_and_preserve_command(self): + original = "2000 16384 300\n" + for button in ["l1+nothing", "l1+release", "l1+l1", "l1+", "+r1"]: + with self.subTest(button=button): + self.path.write_text(original, encoding="utf-8") + errors = io.StringIO() + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + self.queue(1000, button) + self.assertEqual(error.exception.code, 2) + self.assertIn("invalid button", errors.getvalue()) + self.assertEqual(self.path.read_text(encoding="utf-8"), original) + def test_valid_stick_command_and_unsigned_tokens_preserve_sequence(self): self.path.write_text("+2000 +16384 +300 +0 +255 +128 +128\n", encoding="utf-8") self.assertEqual(self.queue(1000, "release"), [2001, 0, 300]) From 9ab433f250cb3cdb7023c7d41ef61995cf634d3c Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 02:54:58 +0700 Subject: [PATCH 0342/1360] fix(agc): start DB_Z_INFO and DB_STENCIL_INFO at their clear-state value A context that never wrote DB_Z_INFO or DB_STENCIL_INFO had neither register, so a draw testing depth or stencil without a depth target was refused (PPSA24040, DB_DEPTH_CONTROL 0x777), and a depth or stencil plane whose other register was never written stopped on a missing register. Both now start at 0, FORMAT INVALID, initially and after CLEAR_STATE. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/QueueState.hpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 52 +++++++++++++++++++ core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 7 +-- docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 3954c462a3..8a178ac8fd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -221,7 +221,7 @@ inline Registers BuildInitialContextRegisters() { Registers result{ {0x200, 0}, {0x201, 0}, {0x202, 0xcc0010}, {0x203, 0}, {0x204, 0}, {0x205, 0}, {0x206, 1087}, {0x207, 0}, - {0x0, 0}, {0x2, 0}, {0x3, 0}, {0x4, 0}, {0x8, 0}, {0x9, 0x3f800000}, {0xa, 0}, {0xb, 0}, + {0x0, 0}, {0x2, 0}, {0x3, 0}, {0x4, 0}, {0x8, 0}, {0x9, 0x3f800000}, {0xa, 0}, {0xb, 0}, {0x10, 0}, {0x11, 0}, {0x80, 0}, {0x83, 0xffff}, {0x8c, 0xaa99aaaa}, {0x8d, 0}, {0x8e, 0}, {0x8f, 0}, {0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000}, {0x90, 0x80000000}, {0x91, 0x40004000}, diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b435118f3d..6ab933b8a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -863,6 +863,57 @@ void msaaEnableClearOneSampleTargetTests() { expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); } +void UnwrittenDepthPlaneTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + const auto fresh = [](std::uint32_t depthControl) { + auto written = makeState(); + written.context[0x200] = depthControl; + written.context[0x1b3] = 2; + written.context[0x1b4] = 2; + AgcDriver::QueueState queue; + queue.shader = written.shader; + queue.userConfig = written.userConfig; + written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; }); + return queue; + }; + auto queue = fresh(0x777); + auto state = DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest, "depth and stencil tests on unwritten depth and stencil planes were kept (PPSA24040's DB_DEPTH_CONTROL)"); + auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused depth and stencil tests on unwritten depth and stencil planes: " + rejection); + queue = fresh(0x77f); + state = DecodeState(queue); + Require(!state.depth && !state.depthBoundsTest, "a depth bounds test on unwritten depth and stencil planes was kept"); + rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused a depth bounds test on unwritten depth and stencil planes: " + rejection); + queue = fresh(0x007007b3); + queue.context[0x010] = 0x80000183; + queue.context[0x012] = 0x00001000; + queue.context[0x014] = 0x00001000; + queue.context[0x007] = 0x0001001f; + queue.context[0x00b] = 0x3f800000; + state = DecodeState(queue); + Require(state.depth && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth plane beside an unwritten DB_STENCIL_INFO did not decode as depth only"); + rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused a depth plane beside an unwritten DB_STENCIL_INFO: " + rejection); + queue = fresh(0x007007b3); + queue.context[0x011] = 0x20000181; + queue.context[0x013] = 0x00002000; + queue.context[0x015] = 0x00002000; + queue.context[0x007] = 0x003f003f; + queue.context[0x00b] = 0x3f800000; + queue.context[0x10c] = 0x01ffff00; + queue.context[0x10d] = 0x01ffff00; + state = DecodeState(queue); + Require(!state.depthTest && state.stencilTest, "a stencil plane beside an unwritten DB_Z_INFO kept the depth test or lost the stencil test"); + rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused a stencil plane beside an unwritten DB_Z_INFO: " + rejection); + queue.context[0x010] = 0x80000183; + queue.ClearContext(); + for (const auto offset : {0x010u, 0x011u}) Require(queue.context.contains(offset) && queue.context.at(offset) == 0, "CLEAR_STATE did not reset DB_Z_INFO and DB_STENCIL_INFO to their clear-state value 0"); +} + void MsaaEnableSingleSampleTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; @@ -11030,6 +11081,7 @@ int main(int argc, char** argv) { UnwrittenSampleLocationTests(); sampleLocationGridFallbackTests(); sampleOrderTests(); + UnwrittenDepthPlaneTests(); depthCopyFormatTests(); quadPixelMaskTests(); ShaderStageTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 6900032797..a7caea8b53 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -192,9 +192,9 @@ void testContextAndBases() { check(state.context.contains(0x280) && state.context.at(0x280) == 0x00080008u && state.context.contains(0x281) && state.context.at(0x281) == 0xffff0000u, "initial point context differs from the SDK defaults"); execute(state, makePacket(0x69, {0x280, 0x00180018u, 0x00400008u})); check(state.context.at(0x280) == 0x00180018u && state.context.at(0x281) == 0x00400008u, "explicit point size and limits were not stored"); - for (const auto offset : {0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu})); + for (const auto offset : {0x010u, 0x011u, 0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu})); execute(state, makePacket(0x12, {0})); - for (const auto offset : {0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore stencil defaults"); + for (const auto offset : {0x010u, 0x011u, 0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore depth and stencil defaults"); check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "CLEAR_STATE did not restore point size and limits"); execute(state, makePacket(0x69, {0x10, 17})); execute(state, makePacket(0x76, {0x20c, 2})); @@ -1021,8 +1021,9 @@ void testInvalidRegisterListEntries() { check(!AgcDriver::Pm4::IndirectRegistersValid(std::array{0xffffffffu, 0}), "the sentinel was taken as a register offset"); check(AgcDriver::Pm4::IndirectRegistersValid(std::array{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected"); AgcDriver::QueueState state; + const auto initial = state.context; expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register"); - check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state"); + check(state.context == initial, "a rejected register list partially changed state"); AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true); check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one"); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e708b868c4..ceaa4b9d15 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -67,6 +67,7 @@ - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color. +- [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. From 2d50c084739aa1897cc09268396f75b1287ae399 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Wed, 21 Oct 2026 15:25:41 +0400 Subject: [PATCH 0343/1360] test(agc): exercise heap mirror regressions on Windows --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++-- .../tests/execution/ResidentHeapMirror.cpp | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0132d5b0a8..edec83ebae 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2203,9 +2203,9 @@ if(BUILD_TESTING) add_test(NAME agc_heap_mirror_run_unwatch COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch) add_test(NAME agc_heap_mirror_run_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch) set_tests_properties(agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_SPACE_CACHE=1") - set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES TIMEOUT 30) + set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache agc_sparse_heap_mirror_budget PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache agc_sparse_heap_mirror_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") else() add_test(NAME agc_heap_mirror_unwatched_import COMMAND agc_driver_graphics_tests --heap-mirror-unwatched-import) set_tests_properties(agc_heap_mirror_unwatched_import PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp index 7657a227d6..1ea57a498b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" @@ -80,8 +81,10 @@ public: explicit GuestHeap(bool writable, std::uint64_t ranges = 1, std::uint64_t rangeBytes = HeapBytes, void* at = nullptr) : ranges(ranges), rangeBytes(rangeBytes) { const auto bytes = rangeBytes * ranges; #ifdef _WIN32 - raw = VirtualAlloc(at, bytes, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE); + raw = at == nullptr ? GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block) + : GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(reinterpret_cast(at), bytes, Block); Require(raw != nullptr && (at == nullptr || raw == at), "resident heap mirror: cannot allocate the guest heap"); + GuestArena::GuestArenaCommit_nid_postfix(raw, bytes, PAGE_READWRITE, bytes); base = static_cast(raw); #else raw = mmap(at, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | (at != nullptr ? MAP_FIXED_NOREPLACE : 0), -1, 0); @@ -100,7 +103,8 @@ public: } GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(base, rangeBytes * ranges); #ifdef _WIN32 - VirtualFree(raw, 0, MEM_RELEASE); + GuestArena::GuestArenaReset_nid_postfix(raw, rangeBytes * ranges); + GuestArena::GuestArenaRelease_nid_postfix(raw, rangeBytes * ranges); #else munmap(raw, rangeBytes * ranges + Block); #endif @@ -325,7 +329,12 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { +#ifdef _WIN32 + const bool watched = GuestArena::GuestArenaWriteWatched_nid_postfix(); +#else + const bool watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); +#endif + if (!watched) { std::puts("skipped, no guest write watch: heap mirrors need one"); return VulkanTestSkipped; } From 99bdd60a6ed0c0b5e094d948924bfec1f1a2104e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:59:31 +0300 Subject: [PATCH 0344/1360] feat(libSceNpManager): deliver pending state callbacks from sceNpCheckCallbackForLib --- core/libs/prx/libSceNpManager/Export.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index f1f3403abb..34b2683095 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -919,8 +919,7 @@ int APS5_VABI sceNpUnknown_WxLcN76q19o(void) { int APS5_VABI sceNpCheckCallbackForLib() { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNpCheckCallback(); } } From 38e3a0e1f6912bba105ed93136f4fa8de1ce85fb Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:59:31 +0300 Subject: [PATCH 0345/1360] test(libSceNpManager): deliver the state callbacks through sceNpCheckCallbackForLib --- core/libs/tests/GuestNpManager.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index ca4daa5da4..5846ba7869 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -26,6 +26,7 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_ extern "C" { int APS5_VABI sceNpCheckCallback(void); +int APS5_VABI sceNpCheckCallbackForLib(void); int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata); int APS5_VABI sceNpUnregisterStateCallback(void); int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); @@ -145,10 +146,14 @@ void CheckStateCallbacksA() { "unregistering one id removed another callback"); } Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot"); - Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed after registering again"); + Require(sceNpCheckCallbackForLib() == 0, "sceNpCheckCallbackForLib failed after registering again"); for (int index = 0; index < 8; ++index) { Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT"); } + Require(sceNpCheckCallbackForLib() == 0 && sceNpCheckCallback() == 0, "checking again failed"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "sceNpCheckCallbackForLib left the state pending"); + } for (int index = 0; index < 8; ++index) { Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback"); } From fcc7f2aba92f3e467b1b3a530abe782c8ce3ded4 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:59:31 +0300 Subject: [PATCH 0346/1360] docs(debt): note where sceNpCheckCallbackForLib comes from --- docs/dev/debt/libSceNpManager.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 9aedd80b56..dfa57df230 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -17,4 +17,4 @@ - [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured - [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpCheckCallbackForLib`](../../../core/libs/prx/libSceNpManager/Export.cpp). +- [sceNpCheckCallbackForLib](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a PS5: it delivers the pending state callbacks like `sceNpCheckCallback`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp) does with its pending state events; shadPS4's `sceNpCheckCallback` also runs other registered NP callbacks, which this library does not keep From d1b7722630572a7dad3895f0e7ab208110784724 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:33:27 +0530 Subject: [PATCH 0347/1360] feat(libSceAppContent): implement temporary data mount --- core/libs/prx/libSceAppContent/Export.cpp | 5 ++--- core/libs/tests/GuestAppContent.cpp | 12 ++++++++++++ docs/dev/debt/libSceAppContent.md | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index bb0eba21ea..86b1ae35d7 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -270,9 +270,8 @@ int APS5_VABI sceAppContentRequestPatchInstall(void) { return 0; } -int APS5_VABI sceAppContentTemporaryDataMount(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint) { + return sceAppContentTemporaryDataMount2(TEMPORARY_DATA_OPTION_FORMAT, mountPoint); } } diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 6d3745cd70..714877d345 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t, const NpUnifiedEntitlementLabel*, void*); +int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -148,6 +149,17 @@ int main() { Require(sceAppContentTemporaryDataMount2(1, &mountPoint) == 0); Require(std::filesystem::is_empty(backing)); Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount(nullptr) == ErrorParameter); + { std::ofstream file(backing / "remove.txt"); file << "format"; } + std::memset(&mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceAppContentTemporaryDataMount(&mountPoint) == 0); + Require(std::strcmp(mountPoint.data, "/temp0") == 0); + Require(std::filesystem::is_empty(backing)); + Require(sceAppContentTemporaryDataMount(&second) == ErrorBusy); + Require(sceAppContentTemporaryDataMount2(0, &second) == ErrorBusy); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); unsigned char info[24]; std::memset(info, 0x5a, sizeof(info)); unsigned char untouchedInfo[24]; diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 62bdd6066f..9f9cc0981e 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,8 +6,8 @@ - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified -- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 17 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 16 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified -- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. +- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with format option 1 (`TEMPORARY_DATA_OPTION_FORMAT`), formatting the directory on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does; console behavior and hardware return values remain unverified. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. From 1bf9429135b13e0597e8a52565dba93830bd5b93 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 21 Oct 2026 15:43:17 +0530 Subject: [PATCH 0348/1360] feat(libSceFreeType): expose font and glyph names --- core/libs/prx/libSceFreeType/CMakeLists.txt | 8 ++ core/libs/prx/libSceFreeType/src/Names.cpp | 33 +++++ core/libs/tests/GuestFreeTypeNames.cpp | 128 ++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 +- 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceFreeType/src/Names.cpp create mode 100644 core/libs/tests/GuestFreeTypeNames.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e0a3b3e116..c00397a353 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL Export.cpp src/GuestObjects.cpp + src/Names.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceFreeType PRIVATE freetype) @@ -20,7 +21,14 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_charmap_tests) add_test(NAME guest_freetype_charmap COMMAND guest_freetype_charmap_tests) + add_test_executable(guest_freetype_names_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeNames.cpp) + target_include_directories(guest_freetype_names_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_names_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_names_tests) + add_test(NAME guest_freetype_names COMMAND guest_freetype_names_tests) if(WIN32) + set_tests_properties(guest_freetype_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_freetype PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_charmap PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceFreeType/src/Names.cpp b/core/libs/prx/libSceFreeType/src/Names.cpp new file mode 100644 index 0000000000..c420522762 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Names.cpp @@ -0,0 +1,33 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include + +using namespace FreeType; + +extern "C" { + +int APS5_VABI FT_Get_Glyph_Name_nid_postfix(GuestFaceRec* face, std::uint32_t glyphIndex, void* buffer, std::uint32_t bufferSize) { + const GuestFace* owner = FaceOf(face); + const FT_Face host = owner ? owner->host : nullptr; + if (!host || !buffer || bufferSize == 0) return FT_Get_Glyph_Name(host, glyphIndex, buffer, bufferSize); + GuestArena::HostWrite write(buffer, bufferSize); + if (static_cast(glyphIndex) >= host->num_glyphs) { + static_cast(buffer)[0] = '\0'; + return FT_Err_Invalid_Glyph_Index; + } + return FT_Get_Glyph_Name(host, glyphIndex, buffer, bufferSize); +} + +std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(GuestFaceRec* face, const char* glyphName) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Name_Index(owner ? owner->host : nullptr, glyphName); +} + +const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Postscript_Name(owner ? owner->host : nullptr); +} + +} diff --git a/core/libs/tests/GuestFreeTypeNames.cpp b/core/libs/tests/GuestFreeTypeNames.cpp new file mode 100644 index 0000000000..a6aa3b7673 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeNames.cpp @@ -0,0 +1,128 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Get_Glyph_Name_nid_postfix(GuestFaceRec*, std::uint32_t, void*, std::uint32_t); +std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(GuestFaceRec*, const char*); +const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType names check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::vector NamedFont(const std::string& postscript, const std::string& first, const std::string& second) { + std::vector post(32, 0); + post[1] = 2; + for (const int value : {3, 0, 258, 259}) FontFixture::Put16(post, value); + for (const auto* name : {&first, &second}) { + post.push_back(static_cast(name->size())); + post.insert(post.end(), name->begin(), name->end()); + } + auto names = FontFixture::Words({0, 1, 18, 3, 1, 0x0409, 6, static_cast(postscript.size() * 2), 0}); + for (const unsigned char ch : postscript) FontFixture::Put16(names, ch); + return FontFixture::BuildFont(3, {{"post", post}, {"name", names}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static void CompareGlyph(GuestFaceRec* guest, FT_Face native, std::uint32_t index, std::uint32_t size) { + std::array expected; + expected.fill('?'); + auto actual = expected; + const FT_Error error = FT_Get_Glyph_Name(native, index, expected.data() + 3, size); + Require(FT_Get_Glyph_Name_nid_postfix(guest, index, actual.data() + 3, size) == error); + Require(actual == expected); +} + +static void CheckFace(FacePair& face, bool names) { + Require((FT_HAS_GLYPH_NAMES(face.oracle) != 0) == names); + for (const std::uint32_t index : {0u, 1u, 2u, 3u, 0x7FFFFFFFu}) { + for (const std::uint32_t size : {0u, 1u, 2u, 5u, 24u}) CompareGlyph(&face.guest.rec, face.oracle, index, size); + Require(FT_Get_Glyph_Name_nid_postfix(&face.guest.rec, index, nullptr, 24) == FT_Get_Glyph_Name(face.oracle, index, nullptr, 24)); + } + for (const std::uint32_t index : {0x80000000u, 0xFFFFFFFFu}) { + for (const std::uint32_t size : {0u, 1u, 24u}) { + std::array expected; + expected.fill('?'); + auto actual = expected; + if (size) expected[3] = '\0'; + Require(FT_Get_Glyph_Name_nid_postfix(&face.guest.rec, index, actual.data() + 3, size) == + (size ? FT_Err_Invalid_Glyph_Index : FT_Err_Invalid_Argument)); + Require(actual == expected); + } + } + for (const char* name : {".notdef", "alpha.custom", "beta", "gamma.custom", "delta", "ALPHA.CUSTOM", "missing", ""}) { + Require(FT_Get_Name_Index_nid_postfix(&face.guest.rec, name) == FT_Get_Name_Index(face.oracle, name)); + } + Require(FT_Get_Name_Index_nid_postfix(&face.guest.rec, nullptr) == 0); + const char* actual = FT_Get_Postscript_Name_nid_postfix(&face.guest.rec); + const char* expected = FT_Get_Postscript_Name(face.oracle); + Require((actual == nullptr) == (expected == nullptr)); + Require(!actual || std::strcmp(actual, expected) == 0); + Require(actual == FT_Get_Postscript_Name(face.guest.host)); +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + FacePair first(library, nativeLibrary, NamedFont("NameFixture-A", "alpha.custom", "beta")); + FacePair second(library, nativeLibrary, NamedFont("NameFixture-B", "gamma.custom", "delta")); + std::vector post(32, 0); + post[1] = 3; + FacePair unnamed(library, nativeLibrary, FontFixture::BuildFont(3, {{"post", post}})); + CheckFace(first, true); + Require(FT_Get_Name_Index_nid_postfix(&first.guest.rec, "alpha.custom") == 1); + Require(FT_Get_Name_Index_nid_postfix(&first.guest.rec, "beta") == 2); + const char* retained = FT_Get_Postscript_Name_nid_postfix(&first.guest.rec); + Require(retained && std::strcmp(retained, "NameFixture-A") == 0); + CheckFace(second, true); + Require(FT_Get_Name_Index_nid_postfix(&second.guest.rec, "gamma.custom") == 1); + Require(FT_Get_Name_Index_nid_postfix(&second.guest.rec, "delta") == 2); + CheckFace(unnamed, false); + Require(FT_Get_Postscript_Name_nid_postfix(&unnamed.guest.rec) == nullptr); + Require(FT_Get_Postscript_Name_nid_postfix(&first.guest.rec) == retained && std::strcmp(retained, "NameFixture-A") == 0); + for (const std::uint32_t size : {0u, 1u, 24u}) CompareGlyph(nullptr, nullptr, 0, size); + Require(FT_Get_Glyph_Name_nid_postfix(nullptr, 0, nullptr, 0) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Name_Index_nid_postfix(nullptr, "alpha.custom") == 0); + Require(FT_Get_Name_Index_nid_postfix(nullptr, nullptr) == 0); + Require(FT_Get_Postscript_Name_nid_postfix(nullptr) == nullptr); + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..682c368d3c 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,7 +2,7 @@ ### Unknown function info -- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. +- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions From 091dc1d1b52635aa289fe26ef6f04699cf3f71ad Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 21 Oct 2026 16:29:51 +0530 Subject: [PATCH 0349/1360] feat(libSceFreeType): expose raw SFNT name records --- core/libs/prx/libSceFreeType/CMakeLists.txt | 7 + .../libSceFreeType/include/GuestFreeType.hpp | 10 ++ core/libs/prx/libSceFreeType/src/Names.cpp | 22 +++ core/libs/tests/GuestFreeTypeSfntNames.cpp | 144 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 185 insertions(+) create mode 100644 core/libs/tests/GuestFreeTypeSfntNames.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index c00397a353..3dd0cd443c 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -26,7 +26,14 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_names_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_names_tests) add_test(NAME guest_freetype_names COMMAND guest_freetype_names_tests) + add_test_executable(guest_freetype_sfnt_names_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeSfntNames.cpp) + target_include_directories(guest_freetype_sfnt_names_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_sfnt_names_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_sfnt_names_tests) + add_test(NAME guest_freetype_sfnt_names COMMAND guest_freetype_sfnt_names_tests) if(WIN32) + set_tests_properties(guest_freetype_sfnt_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_names PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(guest_freetype PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 1a2a074378..28fead1ee8 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -40,6 +40,15 @@ struct GuestCharMapRec { std::uint16_t encoding_id; }; +struct GuestSfntName { + std::uint16_t platform_id; + std::uint16_t encoding_id; + std::uint16_t language_id; + std::uint16_t name_id; + unsigned char* string; + std::uint32_t string_len; +}; + struct GuestSizeMetrics { std::uint16_t x_ppem; std::uint16_t y_ppem; @@ -152,6 +161,7 @@ struct GuestFaceRec { static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); +static_assert(sizeof(GuestSfntName) == 24 && offsetof(GuestSfntName, string) == 8 && offsetof(GuestSfntName, string_len) == 16); static_assert(sizeof(GuestSizeMetrics) == 56 && offsetof(GuestSizeMetrics, x_scale) == 8); static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 24); static_assert(sizeof(GuestBitmap) == 40 && offsetof(GuestBitmap, buffer) == 16 && offsetof(GuestBitmap, palette) == 32); diff --git a/core/libs/prx/libSceFreeType/src/Names.cpp b/core/libs/prx/libSceFreeType/src/Names.cpp index c420522762..23d3b8ad8f 100644 --- a/core/libs/prx/libSceFreeType/src/Names.cpp +++ b/core/libs/prx/libSceFreeType/src/Names.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include FT_SFNT_NAMES_H + #include using namespace FreeType; @@ -30,4 +32,24 @@ const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec* face) { return FT_Get_Postscript_Name(owner ? owner->host : nullptr); } +std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Sfnt_Name_Count(owner ? owner->host : nullptr); +} + +int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(GuestFaceRec* face, std::uint32_t index, GuestSfntName* name) { + const GuestFace* owner = FaceOf(face); + FT_SfntName native{}; + const FT_Error error = FT_Get_Sfnt_Name(owner ? owner->host : nullptr, index, name ? &native : nullptr); + if (!error) { + name->platform_id = native.platform_id; + name->encoding_id = native.encoding_id; + name->language_id = native.language_id; + name->name_id = native.name_id; + name->string = native.string; + name->string_len = native.string_len; + } + return error; +} + } diff --git a/core/libs/tests/GuestFreeTypeSfntNames.cpp b/core/libs/tests/GuestFreeTypeSfntNames.cpp new file mode 100644 index 0000000000..12affd6cbd --- /dev/null +++ b/core/libs/tests/GuestFreeTypeSfntNames.cpp @@ -0,0 +1,144 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_SFNT_NAMES_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(GuestFaceRec*); +int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(GuestFaceRec*, std::uint32_t, GuestSfntName*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType SFNT names check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct NameRecord { + std::uint16_t platform; + std::uint16_t encoding; + std::uint16_t language; + std::uint16_t name; + std::vector bytes; +}; + +static std::vector NamedFont(const std::vector& records) { + auto table = FontFixture::Words({0, static_cast(records.size()), 6 + 12 * static_cast(records.size())}); + int offset = 0; + for (const auto& record : records) { + for (const int value : {static_cast(record.platform), static_cast(record.encoding), static_cast(record.language), + static_cast(record.name), static_cast(record.bytes.size()), offset}) FontFixture::Put16(table, value); + offset += static_cast(record.bytes.size()); + } + for (const auto& record : records) table.insert(table.end(), record.bytes.begin(), record.bytes.end()); + return FontFixture::BuildFont(3, {{"name", table}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static GuestSfntName Query(FacePair& face, std::uint32_t index, const NameRecord& record) { + struct GuardedName { + std::array before; + GuestSfntName value; + std::array after; + } actual; + std::memset(&actual, 0xA5, sizeof(actual)); + FT_SfntName expected{}; + Require(FT_Get_Sfnt_Name(face.oracle, index, &expected) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(&face.guest.rec, index, &actual.value) == 0); + for (const auto byte : actual.before) Require(byte == 0xA5); + for (const auto byte : actual.after) Require(byte == 0xA5); + const auto& name = actual.value; + Require(name.platform_id == expected.platform_id && name.platform_id == record.platform); + Require(name.encoding_id == expected.encoding_id && name.encoding_id == record.encoding); + Require(name.language_id == expected.language_id && name.language_id == record.language); + Require(name.name_id == expected.name_id && name.name_id == record.name); + Require(name.string_len == expected.string_len && name.string_len == record.bytes.size()); + Require(std::memcmp(name.string, expected.string, name.string_len) == 0); + Require(std::memcmp(name.string, record.bytes.data(), name.string_len) == 0); + FT_SfntName native{}; + Require(FT_Get_Sfnt_Name(face.guest.host, index, &native) == 0 && name.string == native.string); + return name; +} + +static void InvalidQuery(GuestFaceRec* guest, FT_Face native, std::uint32_t index) { + GuestSfntName actual; + std::memset(&actual, 0xA5, sizeof(actual)); + std::array before; + std::memcpy(before.data(), &actual, sizeof(actual)); + FT_SfntName expected{}; + const FT_Error error = FT_Get_Sfnt_Name(native, index, &expected); + Require(error != 0); + Require(FT_Get_Sfnt_Name_nid_postfix(guest, index, &actual) == error); + Require(std::memcmp(&actual, before.data(), sizeof(actual)) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(guest, index, nullptr) == FT_Get_Sfnt_Name(native, index, nullptr)); +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + const std::vector records = { + {1, 0, 0, 1, {'F', 'a', 'm', 'i', 'l', 'y'}}, + {3, 1, 0x0409, 6, {0, 'S', 0, 'f', 0, 'n', 0, 't'}}, + {3, 10, 0x0411, 256, {0, 'A', 0, 0, 0, 'B'}} + }; + FacePair first(library, nativeLibrary, NamedFont(records)); + FacePair empty(library, nativeLibrary, FontFixture::BuildFont(3, {})); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&first.guest.rec) == records.size()); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&first.guest.rec) == FT_Get_Sfnt_Name_Count(first.oracle)); + std::vector retained; + for (std::uint32_t index = 0; index < records.size(); ++index) retained.push_back(Query(first, index, records[index])); + { + const std::vector other = {{3, 1, 0x0409, 6, {0, 'O', 0, 't', 0, 'h', 0, 'e', 0, 'r'}}}; + FacePair second(library, nativeLibrary, NamedFont(other)); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&second.guest.rec) == other.size()); + Query(second, 0, other[0]); + } + for (std::uint32_t index = 0; index < records.size(); ++index) { + Require(Query(first, index, records[index]).string == retained[index].string); + Require(std::memcmp(retained[index].string, records[index].bytes.data(), retained[index].string_len) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(&first.guest.rec, index, nullptr) == FT_Get_Sfnt_Name(first.oracle, index, nullptr)); + } + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&empty.guest.rec) == 0); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(nullptr) == 0); + for (const std::uint32_t index : {0u, 3u, 0x80000000u, 0xFFFFFFFFu}) { + if (index >= records.size()) InvalidQuery(&first.guest.rec, first.oracle, index); + InvalidQuery(&empty.guest.rec, empty.oracle, index); + InvalidQuery(nullptr, nullptr, index); + } + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 682c368d3c..cb7f83a4fe 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,6 +2,8 @@ ### Unknown function info +- [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive + - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions From 9544a3e76682352c7ae161f48db903dc75a82047 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 21 Oct 2026 16:46:19 +0530 Subject: [PATCH 0350/1360] feat(libSceFreeType): check outlines and query control bounds --- core/libs/prx/libSceFreeType/CMakeLists.txt | 8 ++ core/libs/prx/libSceFreeType/src/Outline.cpp | 37 +++++++ core/libs/tests/GuestFreeTypeOutline.cpp | 105 +++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 152 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/Outline.cpp create mode 100644 core/libs/tests/GuestFreeTypeOutline.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e0a3b3e116..a0c0c765f1 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL Export.cpp + src/Outline.cpp src/GuestObjects.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) @@ -10,6 +11,11 @@ endif() configure_windows_unwind(libSceFreeType) if(BUILD_TESTING) + add_test_executable(guest_freetype_outline_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOutline.cpp) + target_include_directories(guest_freetype_outline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_outline_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_outline_tests) + add_test(NAME guest_freetype_outline COMMAND guest_freetype_outline_tests) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_tests PRIVATE libSceFreeType freetype libc) @@ -25,5 +31,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_charmap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_outline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFreeType/src/Outline.cpp b/core/libs/prx/libSceFreeType/src/Outline.cpp new file mode 100644 index 0000000000..6c68b2aee5 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Outline.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_OUTLINE_H + +#include +#include + +using namespace FreeType; + +extern "C" { + +int APS5_VABI FT_Outline_Check_nid_postfix(GuestOutline* outline) { + if (!outline) return FT_Outline_Check(nullptr); + FT_Outline native{outline->n_contours, outline->n_points, nullptr, nullptr, outline->contours, outline->flags}; + return FT_Outline_Check(&native); +} + +void APS5_VABI FT_Outline_Get_CBox_nid_postfix(const GuestOutline* outline, GuestBBox* box) { + if (!outline || !box) return; + GuestBBox bounds{}; + if (outline->n_points) { + const GuestVector& first = outline->points[0]; + bounds = {first.x, first.y, first.x, first.y}; + for (std::size_t index = 1; index < outline->n_points; ++index) { + const GuestVector& point = outline->points[index]; + bounds.xMin = std::min(bounds.xMin, point.x); + bounds.yMin = std::min(bounds.yMin, point.y); + bounds.xMax = std::max(bounds.xMax, point.x); + bounds.yMax = std::max(bounds.yMax, point.y); + } + } + *box = bounds; +} + +} \ No newline at end of file diff --git a/core/libs/tests/GuestFreeTypeOutline.cpp b/core/libs/tests/GuestFreeTypeOutline.cpp new file mode 100644 index 0000000000..234090218f --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOutline.cpp @@ -0,0 +1,105 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_OUTLINE_H + +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Outline_Check_nid_postfix(GuestOutline*); +void APS5_VABI FT_Outline_Get_CBox_nid_postfix(const GuestOutline*, GuestBBox*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType outline check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void CompareCheck(std::uint16_t pointCount, std::vector& contours) { + GuestOutline guest{static_cast(contours.size()), pointCount, nullptr, nullptr, contours.data(), 0x71}; + FT_Outline native{guest.n_contours, pointCount, nullptr, nullptr, contours.data(), guest.flags}; + const auto before = contours; + Require(FT_Outline_Check_nid_postfix(&guest) == FT_Outline_Check(&native)); + Require(contours == before && guest.flags == 0x71 && guest.points == nullptr && guest.tags == nullptr); +} + +static void EnumerateContours(std::uint16_t pointCount, std::vector& contours, std::size_t index) { + if (index == contours.size()) { + CompareCheck(pointCount, contours); + return; + } + for (const std::uint16_t end : {0, 1, 2, 3, 4, 5, 6, 65535}) { + contours[index] = end; + EnumerateContours(pointCount, contours, index + 1); + } +} + +static bool Equal(const GuestBBox& actual, const GuestBBox& expected) { + return actual.xMin == expected.xMin && actual.yMin == expected.yMin && actual.xMax == expected.xMax && actual.yMax == expected.yMax; +} + +static void CompareBounds(std::vector points) { + std::vector nativePoints; + for (const auto& point : points) nativePoints.push_back({static_cast(point.x), static_cast(point.y)}); + GuestOutline guest{65535, static_cast(points.size()), points.data(), nullptr, nullptr, 0x71}; + FT_Outline native{guest.n_contours, guest.n_points, nativePoints.data(), nullptr, nullptr, guest.flags}; + struct GuardedBox { + std::uint64_t before; + GuestBBox value; + std::uint64_t after; + } actual{0x1122334455667788, {91, 92, 93, 94}, 0x8877665544332211}; + FT_BBox expected{91, 92, 93, 94}; + FT_Outline_Get_CBox(&native, &expected); + FT_Outline_Get_CBox_nid_postfix(&guest, &actual.value); + Require(Equal(actual.value, {expected.xMin, expected.yMin, expected.xMax, expected.yMax})); + Require(actual.before == 0x1122334455667788 && actual.after == 0x8877665544332211); + FT_Outline_Get_CBox_nid_postfix(&guest, nullptr); + Require(guest.n_contours == 65535 && guest.flags == 0x71 && guest.tags == nullptr && guest.contours == nullptr); +} + +int main() { + Require(FT_Outline_Check_nid_postfix(nullptr) == FT_Outline_Check(nullptr)); + for (std::uint16_t pointCount = 0; pointCount <= 6; ++pointCount) { + for (std::size_t contourCount = 0; contourCount <= 3; ++contourCount) { + std::vector contours(contourCount); + EnumerateContours(pointCount, contours, 0); + } + } + std::vector maximum = {65534}; + CompareCheck(65535, maximum); + GuestOutline high{1, 65535, nullptr, nullptr, maximum.data(), 0}; + Require(FT_Outline_Check_nid_postfix(&high) == 0); + std::vector maximumContours(65535); + for (std::size_t index = 0; index < maximumContours.size(); ++index) maximumContours[index] = static_cast(index); + CompareCheck(65535, maximumContours); + CompareBounds({}); + CompareBounds({{17, -29}}); + CompareBounds({{-10, 20}, {5000, -7000}, {30, 40}, {-200, 9000}}); + CompareBounds({{std::numeric_limits::min(), 0}, {0, std::numeric_limits::max()}}); + std::vector maximumPoints(65535, {3, 7}); + maximumPoints.back() = {-12345, 54321}; + CompareBounds(maximumPoints); + std::array wide = {{{std::numeric_limits::min(), 1}, + {std::int64_t{1} << 40, std::numeric_limits::max()}, + {std::numeric_limits::max(), std::numeric_limits::min()}}}; + GuestOutline outline{0, static_cast(wide.size()), wide.data(), nullptr, nullptr, 0}; + GuestBBox box{1, 2, 3, 4}; + FT_Outline_Get_CBox_nid_postfix(&outline, &box); + Require(Equal(box, {std::numeric_limits::min(), std::numeric_limits::min(), + std::numeric_limits::max(), std::numeric_limits::max()})); + const GuestBBox retained = box; + FT_Outline_Get_CBox_nid_postfix(nullptr, &box); + Require(Equal(box, retained)); + FT_Outline_Get_CBox_nid_postfix(nullptr, nullptr); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..1dbcf13cc1 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -8,6 +8,8 @@ - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. +- [FT_Outline_Check, FT_Outline_Get_CBox](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c), including its unsigned 16-bit counts and contour endpoints. Control-box coordinates use the existing guest LP64 layout and retain their full 64-bit range on Windows; contour checking does not validate point tags + ### Functional - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. From 0ed04b266a04f5bd27c66420986f2fd090cbaf68 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 21 Oct 2026 16:59:29 +0530 Subject: [PATCH 0351/1360] feat(libSceFreeType): expose charmap metadata --- core/libs/prx/libSceFreeType/CMakeLists.txt | 8 + core/libs/prx/libSceFreeType/src/Charmap.cpp | 41 +++++ core/libs/tests/GuestFreeTypeCharmapInfo.cpp | 150 +++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 201 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/Charmap.cpp create mode 100644 core/libs/tests/GuestFreeTypeCharmapInfo.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index a0c0c765f1..a9b8e79355 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL Export.cpp src/Outline.cpp + src/Charmap.cpp src/GuestObjects.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) @@ -16,6 +17,11 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_outline_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_outline_tests) add_test(NAME guest_freetype_outline COMMAND guest_freetype_outline_tests) + add_test_executable(guest_freetype_charmap_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmapInfo.cpp) + target_include_directories(guest_freetype_charmap_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_charmap_info_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_charmap_info_tests) + add_test(NAME guest_freetype_charmap_info COMMAND guest_freetype_charmap_info_tests) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_tests PRIVATE libSceFreeType freetype libc) @@ -33,5 +39,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_outline PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_charmap_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFreeType/src/Charmap.cpp b/core/libs/prx/libSceFreeType/src/Charmap.cpp new file mode 100644 index 0000000000..c6a776664e --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Charmap.cpp @@ -0,0 +1,41 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include + +using namespace FreeType; + +namespace { + +FT_CharMap HostCharmap(const GuestCharMapRec* charmap) { + if (!charmap || !charmap->face) return nullptr; + const GuestFace* owner = FaceOf(static_cast(charmap->face)); + if (owner && owner->host) { + for (std::size_t index = 0; index < owner->charmapPointers.size(); ++index) { + if (owner->charmapPointers[index] == charmap) return owner->host->charmaps[index]; + } + } + throw std::runtime_error("FreeType: a charmap outside the guest face's read-only array is not implemented"); +} + +} + +extern "C" { + +int APS5_VABI FT_Get_Charmap_Index_nid_postfix(GuestCharMapRec* charmap) { + return FT_Get_Charmap_Index(HostCharmap(charmap)); +} + +std::uint64_t APS5_VABI FT_Get_CMap_Language_ID_nid_postfix(GuestCharMapRec* charmap) { + return FT_Get_CMap_Language_ID(HostCharmap(charmap)); +} + +std::int64_t APS5_VABI FT_Get_CMap_Format_nid_postfix(GuestCharMapRec* charmap) { + return FT_Get_CMap_Format(HostCharmap(charmap)); +} + +} \ No newline at end of file diff --git a/core/libs/tests/GuestFreeTypeCharmapInfo.cpp b/core/libs/tests/GuestFreeTypeCharmapInfo.cpp new file mode 100644 index 0000000000..acc33ac305 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeCharmapInfo.cpp @@ -0,0 +1,150 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Get_Charmap_Index_nid_postfix(GuestCharMapRec*); +std::uint64_t APS5_VABI FT_Get_CMap_Language_ID_nid_postfix(GuestCharMapRec*); +std::int64_t APS5_VABI FT_Get_CMap_Format_nid_postfix(GuestCharMapRec*); +int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec*, GuestCharMapRec*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType charmap info check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::vector Font(std::uint16_t language4, std::uint32_t language12) { + auto cmap = FontFixture::Words({0, 2, 3, 1, 0, 20, 3, 10, 0, 52}); + const auto format4 = FontFixture::Words({4, 32, language4, 4, 4, 1, 0, 'A', 0xFFFF, 0, 'A', 0xFFFF, 1 - 'A', 1, 0, 0}); + const auto format12 = FontFixture::Words({12, 0, 0, 28, static_cast(language12 >> 16), static_cast(language12 & 0xFFFF), + 0, 1, 1, 0xF600, 1, 0xF600, 0, 2}); + cmap.insert(cmap.end(), format4.begin(), format4.end()); + cmap.insert(cmap.end(), format12.begin(), format12.end()); + return FontFixture::BuildFont(3, {{"cmap", cmap}}); +} + +static std::vector BitmapFont() { + const std::string_view font = + "STARTFONT 2.1\n" + "FONT -anyps5-fixture-medium-r-normal--1-10-75-75-c-10-iso10646-1\n" + "SIZE 1 75 75\n" + "FONTBOUNDINGBOX 1 1 0 0\n" + "STARTPROPERTIES 4\n" + "FONT_ASCENT 1\n" + "FONT_DESCENT 0\n" + "CHARSET_REGISTRY \"ISO10646\"\n" + "CHARSET_ENCODING \"1\"\n" + "ENDPROPERTIES\n" + "CHARS 1\n" + "STARTCHAR A\n" + "ENCODING 65\n" + "SWIDTH 1000 0\n" + "DWIDTH 1 0\n" + "BBX 1 1 0 0\n" + "BITMAP\n" + "80\n" + "ENDCHAR\n" + "ENDFONT\n"; + return {font.begin(), font.end()}; +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static void Compare(FacePair& face, std::uint16_t language4, std::uint32_t language12) { + Require(face.guest.rec.num_charmaps == 2 && face.oracle->num_charmaps == 2); + const auto selected = face.guest.rec.charmap; + const auto nativeSelected = face.oracle->charmap; + for (int index = 0; index < face.guest.rec.num_charmaps; ++index) { + GuestCharMapRec* guest = face.guest.rec.charmaps[index]; + FT_CharMap native = face.oracle->charmaps[index]; + Require(FT_Get_Charmap_Index_nid_postfix(guest) == index); + Require(FT_Get_Charmap_Index_nid_postfix(guest) == FT_Get_Charmap_Index(native)); + const auto format = FT_Get_CMap_Format_nid_postfix(guest); + Require(format == FT_Get_CMap_Format(native) && (format == 4 || format == 12)); + const auto language = FT_Get_CMap_Language_ID_nid_postfix(guest); + Require(language == FT_Get_CMap_Language_ID(native)); + Require(language == (format == 4 ? language4 : language12)); + } + Require(face.guest.rec.charmap == selected && face.oracle->charmap == nativeSelected); +} + +template static bool Unsupported(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string_view(error.what()).starts_with("FreeType: a charmap outside"); + } + return false; +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + FacePair first(library, nativeLibrary, Font(0x0411, 0x80000001u)); + FacePair second(library, nativeLibrary, Font(0x0412, 0xFFFFFFFFu)); + Compare(first, 0x0411, 0x80000001u); + Compare(second, 0x0412, 0xFFFFFFFFu); + for (int index = 0; index < first.guest.rec.num_charmaps; ++index) { + Require(FT_Set_Charmap_nid_postfix(&first.guest.rec, first.guest.rec.charmaps[index]) == 0); + Require(FT_Set_Charmap(first.oracle, first.oracle->charmaps[index]) == 0); + Compare(first, 0x0411, 0x80000001u); + Compare(second, 0x0412, 0xFFFFFFFFu); + } + GuestCharMapRec copied = *first.guest.rec.charmaps[0]; + Require(Unsupported([&] { FT_Get_Charmap_Index_nid_postfix(&copied); })); + Require(Unsupported([&] { FT_Get_CMap_Language_ID_nid_postfix(&copied); })); + Require(Unsupported([&] { FT_Get_CMap_Format_nid_postfix(&copied); })); + FacePair bitmap(library, nativeLibrary, BitmapFont()); + Require(!FT_IS_SFNT(bitmap.oracle) && bitmap.guest.rec.num_charmaps == 1); + GuestCharMapRec* bitmapMap = bitmap.guest.rec.charmaps[0]; + Require(FT_Get_Charmap_Index_nid_postfix(bitmapMap) == FT_Get_Charmap_Index(bitmap.oracle->charmaps[0])); + Require(FT_Get_CMap_Language_ID_nid_postfix(bitmapMap) == FT_Get_CMap_Language_ID(bitmap.oracle->charmaps[0])); + Require(FT_Get_CMap_Language_ID_nid_postfix(bitmapMap) == 0); + Require(FT_Get_CMap_Format_nid_postfix(bitmapMap) == FT_Get_CMap_Format(bitmap.oracle->charmaps[0])); + Require(FT_Get_CMap_Format_nid_postfix(bitmapMap) == -1); + GuestCharMapRec empty{}; + FT_CharMapRec nativeEmpty{}; + for (GuestCharMapRec* guest : {static_cast(nullptr), &empty}) { + Require(FT_Get_Charmap_Index_nid_postfix(guest) == FT_Get_Charmap_Index(guest ? &nativeEmpty : nullptr)); + Require(FT_Get_CMap_Language_ID_nid_postfix(guest) == FT_Get_CMap_Language_ID(guest ? &nativeEmpty : nullptr)); + Require(FT_Get_CMap_Format_nid_postfix(guest) == FT_Get_CMap_Format(guest ? &nativeEmpty : nullptr)); + } + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} \ No newline at end of file diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 1dbcf13cc1..ef8249e4e0 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -10,6 +10,8 @@ - [FT_Outline_Check, FT_Outline_Get_CBox](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c), including its unsigned 16-bit counts and contour endpoints. Control-box coordinates use the existing guest LP64 layout and retain their full 64-bit range on Windows; contour checking does not validate point tags +- [FT_Get_Charmap_Index, FT_Get_CMap_Language_ID, FT_Get_CMap_Format](../../../core/libs/prx/libSceFreeType/src/Charmap.cpp) (libSceFreeType) - console signatures and return values are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c) and the existing guest LP64 model. Charmaps must be the face's original read-only guest records; copied or otherwise unregistered records with a non-null face throw instead of reading private native cmap fields from guest memory + ### Functional - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. From a5b7e4d268ab78c1e705db1db6f48735f617690b Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 21 Oct 2026 17:13:44 +0530 Subject: [PATCH 0352/1360] feat(libSceFreeType): reverse and translate guest outlines --- core/libs/prx/libSceFreeType/CMakeLists.txt | 7 + core/libs/prx/libSceFreeType/src/Outline.cpp | 29 ++- .../tests/GuestFreeTypeOutlineMutations.cpp | 169 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestFreeTypeOutlineMutations.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index a9b8e79355..89ed1f1576 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -27,6 +27,11 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_tests) add_test(NAME guest_freetype COMMAND guest_freetype_tests) + add_test_executable(guest_freetype_outline_mutations_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOutlineMutations.cpp) + target_include_directories(guest_freetype_outline_mutations_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_outline_mutations_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_outline_mutations_tests) + add_test(NAME guest_freetype_outline_mutations COMMAND guest_freetype_outline_mutations_tests) add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) @@ -41,5 +46,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_charmap_info PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_outline_mutations PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFreeType/src/Outline.cpp b/core/libs/prx/libSceFreeType/src/Outline.cpp index 6c68b2aee5..1725531f3a 100644 --- a/core/libs/prx/libSceFreeType/src/Outline.cpp +++ b/core/libs/prx/libSceFreeType/src/Outline.cpp @@ -5,7 +5,9 @@ #include FT_OUTLINE_H #include +#include #include +#include using namespace FreeType; @@ -34,4 +36,29 @@ void APS5_VABI FT_Outline_Get_CBox_nid_postfix(const GuestOutline* outline, Gues *box = bounds; } -} \ No newline at end of file +void APS5_VABI FT_Outline_Translate_nid_postfix(const GuestOutline* outline, std::int64_t xOffset, std::int64_t yOffset) { + if (!outline) return; + for (std::size_t index = 0; index < outline->n_points; ++index) { + GuestVector& point = outline->points[index]; + point.x = std::bit_cast(static_cast(point.x) + static_cast(xOffset)); + point.y = std::bit_cast(static_cast(point.y) + static_cast(yOffset)); + } +} + +void APS5_VABI FT_Outline_Reverse_nid_postfix(GuestOutline* outline) { + if (!outline) return; + std::size_t first = 1; + for (std::size_t contour = 0; contour < outline->n_contours; ++contour) { + const std::size_t last = outline->contours[contour]; + for (std::size_t left = first, right = last; left < right; ++left, --right) { + std::swap(outline->points[left], outline->points[right]); + } + for (std::size_t left = first, right = last; left < right; ++left, --right) { + std::swap(outline->tags[left], outline->tags[right]); + } + first = last + 2; + } + outline->flags ^= FT_OUTLINE_REVERSE_FILL; +} + +} diff --git a/core/libs/tests/GuestFreeTypeOutlineMutations.cpp b/core/libs/tests/GuestFreeTypeOutlineMutations.cpp new file mode 100644 index 0000000000..67144147f3 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOutlineMutations.cpp @@ -0,0 +1,169 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_OUTLINE_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +void APS5_VABI FT_Outline_Translate_nid_postfix(const GuestOutline*, std::int64_t, std::int64_t); +void APS5_VABI FT_Outline_Reverse_nid_postfix(GuestOutline*); +int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType outline mutation check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static bool Equal(const GuestVector& left, const GuestVector& right) { + return left.x == right.x && left.y == right.y; +} + +static void ReverseOracle(std::size_t count, std::vector contours, std::int32_t flags) { + std::vector storage(count + 2); + storage.front() = {991, 992}; + storage.back() = {-993, -994}; + std::vector tags(count + 2, 0xA5); + std::vector nativePoints; + std::vector nativeTags; + for (std::size_t index = 0; index < count; ++index) { + storage[index + 1] = {(std::int64_t{1} << 40) + static_cast(index) * 3, + -(std::int64_t{1} << 41) + static_cast(index) * 7}; + tags[index + 1] = static_cast(index * 73); + nativePoints.push_back({static_cast(index), -static_cast(index)}); + nativeTags.push_back(tags[index + 1]); + } + const auto original = storage; + const auto originalTags = tags; + const auto originalContours = contours; + GuestOutline guest{static_cast(contours.size()), static_cast(count), storage.data() + 1, + tags.data() + 1, contours.data(), flags}; + FT_Outline native{guest.n_contours, guest.n_points, nativePoints.data(), nativeTags.data(), contours.data(), flags}; + FT_Outline_Reverse(&native); + FT_Outline_Reverse_nid_postfix(&guest); + Require(guest.flags == native.flags && guest.flags == (flags ^ FT_OUTLINE_REVERSE_FILL)); + Require(guest.n_points == count && guest.n_contours == contours.size() && contours == originalContours); + for (std::size_t index = 0; index < count; ++index) { + const auto source = static_cast(nativePoints[index].x); + Require(source < count && Equal(storage[index + 1], original[source + 1])); + Require(tags[index + 1] == nativeTags[index]); + } + Require(Equal(storage.front(), original.front()) && Equal(storage.back(), original.back())); + Require(tags.front() == 0xA5 && tags.back() == 0xA5); + FT_Outline_Reverse_nid_postfix(&guest); + Require(guest.flags == flags && tags == originalTags); + for (std::size_t index = 0; index < storage.size(); ++index) Require(Equal(storage[index], original[index])); +} + +static void TranslateOracle(std::size_t count, FT_Pos xOffset, FT_Pos yOffset) { + std::vector storage(count + 2, {991, -992}); + std::vector nativePoints; + for (std::size_t index = 0; index < count; ++index) { + const auto value = static_cast(index % 101); + storage[index + 1] = {value - 50, 25 - value}; + nativePoints.push_back({value - 50, 25 - value}); + } + GuestOutline guest{65535, static_cast(count), storage.data() + 1, nullptr, nullptr, 0x71}; + FT_Outline native{guest.n_contours, guest.n_points, nativePoints.data(), nullptr, nullptr, guest.flags}; + FT_Outline_Translate(&native, xOffset, yOffset); + FT_Outline_Translate_nid_postfix(&guest, xOffset, yOffset); + for (std::size_t index = 0; index < count; ++index) { + Require(storage[index + 1].x == nativePoints[index].x && storage[index + 1].y == nativePoints[index].y); + } + Require(Equal(storage.front(), {991, -992}) && Equal(storage.back(), {991, -992})); + Require(guest.n_contours == 65535 && guest.n_points == count && guest.flags == 0x71); + Require(guest.tags == nullptr && guest.contours == nullptr); +} + +static void WideTranslation() { + const auto low = std::numeric_limits::min(); + const auto high = std::numeric_limits::max(); + const auto wide = std::int64_t{1} << 40; + std::array points = {{{high, low}, {low, high}, {-1, 0}, {1, -1}, {wide, -wide}, {high - 5, low + 5}}}; + const auto original = points; + const std::array expected = {{{low, high}, {low + 1, high - 1}, {0, -1}, {2, -2}, {wide + 1, -wide - 1}, {high - 4, low + 4}}}; + GuestOutline guest{0, static_cast(points.size()), points.data(), nullptr, nullptr, 0}; + FT_Outline_Translate_nid_postfix(&guest, 1, -1); + for (std::size_t index = 0; index < points.size(); ++index) Require(Equal(points[index], expected[index])); + FT_Outline_Translate_nid_postfix(&guest, -1, 1); + for (std::size_t index = 0; index < points.size(); ++index) Require(Equal(points[index], original[index])); + GuestVector point{7, -9}; + guest.n_points = 1; + guest.points = &point; + FT_Outline_Translate_nid_postfix(&guest, wide, -wide); + Require(Equal(point, {wide + 7, -wide - 9})); + point = {low, 1}; + FT_Outline_Translate_nid_postfix(&guest, low, high); + Require(Equal(point, {0, low})); +} + +static void RenderModifiedOutline() { + const auto bytes = SquareGlyphFont('A', 700); + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + { + GuestFace guest; + guest.library = &library; + FT_Face native = nullptr; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &native) == 0); + Require(FT_Set_Pixel_Sizes(guest.host, 18, 18) == 0 && FT_Set_Pixel_Sizes(native, 18, 18) == 0); + Require(FT_Load_Glyph(guest.host, 1, FT_LOAD_DEFAULT) == 0 && FT_Load_Glyph(native, 1, FT_LOAD_DEFAULT) == 0); + SyncFace(guest); + FT_Outline_Reverse_nid_postfix(&guest.slot.outline); + FT_Outline_Translate_nid_postfix(&guest.slot.outline, 77, -129); + FT_Outline_Reverse(&native->glyph->outline); + FT_Outline_Translate(&native->glyph->outline, 77, -129); + Require(FT_Render_Glyph_nid_postfix(&guest.slot, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(native->glyph, FT_RENDER_MODE_NORMAL)); + const auto& actual = guest.slot.bitmap; + const auto& expected = native->glyph->bitmap; + Require(actual.width > 0 && actual.rows > 0 && actual.width == expected.width && actual.rows == expected.rows); + Require(actual.pitch == expected.pitch && actual.pixel_mode == expected.pixel_mode && actual.num_grays == expected.num_grays); + Require(guest.slot.bitmap_left == native->glyph->bitmap_left && guest.slot.bitmap_top == native->glyph->bitmap_top); + const auto pitch = static_cast(expected.pitch < 0 ? -expected.pitch : expected.pitch); + Require(std::memcmp(actual.buffer, expected.buffer, pitch * expected.rows) == 0); + Require(FT_Done_Face(guest.host) == 0 && FT_Done_Face(native) == 0); + } + Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} + +int main() { + FT_Outline_Reverse_nid_postfix(nullptr); + FT_Outline_Translate_nid_postfix(nullptr, std::numeric_limits::min(), std::numeric_limits::max()); + ReverseOracle(0, {}, FT_OUTLINE_REVERSE_FILL); + for (std::size_t count = 1; count <= 8; ++count) { + for (unsigned mask = 0; mask < (1u << (count - 1)); ++mask) { + std::vector contours; + for (std::size_t index = 0; index < count; ++index) { + if (index + 1 == count || (mask & (1u << index))) contours.push_back(static_cast(index)); + } + ReverseOracle(count, contours, static_cast(mask) | 0x31); + } + } + ReverseOracle(65535, {65534}, 0); + std::vector maximumContours(65535); + for (std::size_t index = 0; index < maximumContours.size(); ++index) maximumContours[index] = static_cast(index); + ReverseOracle(65535, maximumContours, FT_OUTLINE_REVERSE_FILL); + for (std::size_t count = 0; count <= 6; ++count) { + for (const FT_Pos xOffset : {-65, 0, 64, 127}) { + for (const FT_Pos yOffset : {-99, 0, 101}) TranslateOracle(count, xOffset, yOffset); + } + } + TranslateOracle(65535, 99, -17); + WideTranslation(); + RenderModifiedOutline(); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index ef8249e4e0..7b338d154d 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -12,6 +12,8 @@ - [FT_Get_Charmap_Index, FT_Get_CMap_Language_ID, FT_Get_CMap_Format](../../../core/libs/prx/libSceFreeType/src/Charmap.cpp) (libSceFreeType) - console signatures and return values are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c) and the existing guest LP64 model. Charmaps must be the face's original read-only guest records; copied or otherwise unregistered records with a non-null face throw instead of reading private native cmap fields from guest memory +- [FT_Outline_Reverse, FT_Outline_Translate](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and overflow behavior are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with LP64 coordinates. Translation uses 64-bit modular addition to match that build's `ADD_LONG`; reversal preserves each contour's first point and toggles the reverse-fill flag. Coordinates outside the host `FT_Pos` range still throw if the modified outline is later rendered on Windows + ### Functional - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. From 114df6965f229fcdc0cbd42265a4837320de7d82 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 21 Oct 2026 17:21:06 +0530 Subject: [PATCH 0353/1360] feat(libSceFreeType): expose font format and embedding flags --- core/libs/prx/libSceFreeType/CMakeLists.txt | 8 ++ core/libs/prx/libSceFreeType/src/FaceInfo.cpp | 22 +++++ core/libs/tests/FontFixture.hpp | 26 ++++++ core/libs/tests/GuestFreeTypeCharmapInfo.cpp | 27 +----- core/libs/tests/GuestFreeTypeFaceInfo.cpp | 89 +++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 6 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 core/libs/prx/libSceFreeType/src/FaceInfo.cpp create mode 100644 core/libs/tests/GuestFreeTypeFaceInfo.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 89ed1f1576..a8ee678982 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL Export.cpp src/Outline.cpp src/Charmap.cpp + src/FaceInfo.cpp src/GuestObjects.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) @@ -32,6 +33,11 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_outline_mutations_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_outline_mutations_tests) add_test(NAME guest_freetype_outline_mutations COMMAND guest_freetype_outline_mutations_tests) + add_test_executable(guest_freetype_face_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeFaceInfo.cpp) + target_include_directories(guest_freetype_face_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_face_info_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_face_info_tests) + add_test(NAME guest_freetype_face_info COMMAND guest_freetype_face_info_tests) add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) @@ -48,5 +54,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_outline_mutations PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_face_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFreeType/src/FaceInfo.cpp b/core/libs/prx/libSceFreeType/src/FaceInfo.cpp new file mode 100644 index 0000000000..6810256751 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/FaceInfo.cpp @@ -0,0 +1,22 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_FONT_FORMATS_H + +#include + +using namespace FreeType; + +extern "C" { + +const char* APS5_VABI FT_Get_X11_Font_Format_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_X11_Font_Format(owner ? owner->host : nullptr); +} + +std::uint16_t APS5_VABI FT_Get_FSType_Flags_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_FSType_Flags(owner ? owner->host : nullptr); +} + +} diff --git a/core/libs/tests/FontFixture.hpp b/core/libs/tests/FontFixture.hpp index 601512cd98..e6e7c11e2b 100644 --- a/core/libs/tests/FontFixture.hpp +++ b/core/libs/tests/FontFixture.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace FontFixture { @@ -20,6 +21,31 @@ inline std::vector Words(std::initializer_list values) { return out; } +inline std::vector BitmapFont() { + const std::string_view font = + "STARTFONT 2.1\n" + "FONT -anyps5-fixture-medium-r-normal--1-10-75-75-c-10-iso10646-1\n" + "SIZE 1 75 75\n" + "FONTBOUNDINGBOX 1 1 0 0\n" + "STARTPROPERTIES 4\n" + "FONT_ASCENT 1\n" + "FONT_DESCENT 0\n" + "CHARSET_REGISTRY \"ISO10646\"\n" + "CHARSET_ENCODING \"1\"\n" + "ENDPROPERTIES\n" + "CHARS 1\n" + "STARTCHAR A\n" + "ENCODING 65\n" + "SWIDTH 1000 0\n" + "DWIDTH 1 0\n" + "BBX 1 1 0 0\n" + "BITMAP\n" + "80\n" + "ENDCHAR\n" + "ENDFONT\n"; + return {font.begin(), font.end()}; +} + inline std::vector BuildFont(int glyphCount, std::map> tables) { tables["glyf"] = Words({0, 0}); tables["head"] = Words({1, 0, 1, 0, 0, 0, 0x5F0F, 0x3CF5, 0, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0}); diff --git a/core/libs/tests/GuestFreeTypeCharmapInfo.cpp b/core/libs/tests/GuestFreeTypeCharmapInfo.cpp index acc33ac305..1c8ec9dd5a 100644 --- a/core/libs/tests/GuestFreeTypeCharmapInfo.cpp +++ b/core/libs/tests/GuestFreeTypeCharmapInfo.cpp @@ -39,31 +39,6 @@ static std::vector Font(std::uint16_t language4, std::uint32_t la return FontFixture::BuildFont(3, {{"cmap", cmap}}); } -static std::vector BitmapFont() { - const std::string_view font = - "STARTFONT 2.1\n" - "FONT -anyps5-fixture-medium-r-normal--1-10-75-75-c-10-iso10646-1\n" - "SIZE 1 75 75\n" - "FONTBOUNDINGBOX 1 1 0 0\n" - "STARTPROPERTIES 4\n" - "FONT_ASCENT 1\n" - "FONT_DESCENT 0\n" - "CHARSET_REGISTRY \"ISO10646\"\n" - "CHARSET_ENCODING \"1\"\n" - "ENDPROPERTIES\n" - "CHARS 1\n" - "STARTCHAR A\n" - "ENCODING 65\n" - "SWIDTH 1000 0\n" - "DWIDTH 1 0\n" - "BBX 1 1 0 0\n" - "BITMAP\n" - "80\n" - "ENDCHAR\n" - "ENDFONT\n"; - return {font.begin(), font.end()}; -} - struct FacePair { std::vector bytes; GuestFace guest; @@ -129,7 +104,7 @@ int main() { Require(Unsupported([&] { FT_Get_Charmap_Index_nid_postfix(&copied); })); Require(Unsupported([&] { FT_Get_CMap_Language_ID_nid_postfix(&copied); })); Require(Unsupported([&] { FT_Get_CMap_Format_nid_postfix(&copied); })); - FacePair bitmap(library, nativeLibrary, BitmapFont()); + FacePair bitmap(library, nativeLibrary, FontFixture::BitmapFont()); Require(!FT_IS_SFNT(bitmap.oracle) && bitmap.guest.rec.num_charmaps == 1); GuestCharMapRec* bitmapMap = bitmap.guest.rec.charmaps[0]; Require(FT_Get_Charmap_Index_nid_postfix(bitmapMap) == FT_Get_Charmap_Index(bitmap.oracle->charmaps[0])); diff --git a/core/libs/tests/GuestFreeTypeFaceInfo.cpp b/core/libs/tests/GuestFreeTypeFaceInfo.cpp new file mode 100644 index 0000000000..f30e8e8a78 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeFaceInfo.cpp @@ -0,0 +1,89 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_FONT_FORMATS_H + +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +const char* APS5_VABI FT_Get_X11_Font_Format_nid_postfix(GuestFaceRec*); +std::uint16_t APS5_VABI FT_Get_FSType_Flags_nid_postfix(GuestFaceRec*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType face info check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::vector Font(std::uint16_t flags) { + std::vector os2(78, 0); + os2[8] = static_cast(flags >> 8); + os2[9] = static_cast(flags); + return FontFixture::BuildFont(3, {{"OS/2", os2}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static const char* Query(FacePair& face, const char* format, std::uint16_t flags) { + const char* actual = FT_Get_X11_Font_Format_nid_postfix(&face.guest.rec); + const char* expected = FT_Get_X11_Font_Format(face.oracle); + Require(actual && expected && std::strcmp(actual, expected) == 0 && std::strcmp(actual, format) == 0); + Require(actual == FT_Get_X11_Font_Format(face.guest.host)); + Require(FT_Get_FSType_Flags_nid_postfix(&face.guest.rec) == FT_Get_FSType_Flags(face.oracle)); + Require(FT_Get_FSType_Flags_nid_postfix(&face.guest.rec) == flags); + return actual; +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + std::vector retained; + const char* bitmapFormat = nullptr; + { + FacePair bitmap(library, nativeLibrary, FontFixture::BitmapFont()); + bitmapFormat = Query(bitmap, "BDF", 0); + for (const std::uint16_t flags : {0, 2, 4, 8, 0x0100, 0x0200, 0x0308, 0x8000, 0xFFFF}) { + FacePair face(library, nativeLibrary, Font(flags)); + retained.push_back(Query(face, "TrueType", flags)); + Require(Query(bitmap, "BDF", 0) == bitmapFormat); + } + FacePair missing(library, nativeLibrary, FontFixture::BuildFont(3, {})); + Query(missing, "TrueType", 0); + GuestFaceRec empty{}; + for (GuestFaceRec* face : {static_cast(nullptr), &empty}) { + Require(FT_Get_X11_Font_Format_nid_postfix(face) == FT_Get_X11_Font_Format(nullptr)); + Require(FT_Get_FSType_Flags_nid_postfix(face) == FT_Get_FSType_Flags(nullptr)); + } + } + Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(nativeLibrary) == 0); + for (const char* format : retained) Require(std::strcmp(format, "TrueType") == 0); + Require(std::strcmp(bitmapFormat, "BDF") == 0); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 7b338d154d..83a7fdfcf9 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -14,6 +14,8 @@ - [FT_Outline_Reverse, FT_Outline_Translate](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and overflow behavior are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with LP64 coordinates. Translation uses 64-bit modular addition to match that build's `ADD_LONG`; reversal preserves each contour's first point and toggles the reverse-fill flag. Coordinates outside the host `FT_Pos` range still throw if the modified outline is later rendered on Windows +- [FT_Get_X11_Font_Format, FT_Get_FSType_Flags](../../../core/libs/prx/libSceFreeType/src/FaceInfo.cpp) (libSceFreeType) - console signatures, format labels and embedding-flag behavior are unverified; the adapter follows bundled FreeType 2.13.3's [format](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfntfmt.c) and [FSType](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfstype.c) services. Format strings borrow native driver storage; the console's string lifetime has not been measured + ### Functional - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. From 9863f8b64120395eb6137fd1f247fb783fe9cac3 Mon Sep 17 00:00:00 2001 From: capmare Date: Wed, 21 Oct 2026 14:13:06 +0200 Subject: [PATCH 0354/1360] fix(shader): give the frontend's call analysis the dispatch's wave size AcquireFrontend built the frontend's SwappcInfo with a wave size of 0, while the driver's LinkUserDataCallees passes the dispatch's wave size. A wave64 v_cmp_e64 that reuses a call's link pair after the return, followed by a v_cndmask_b32_e64 that reads it, was refused as escaping the call/return model. The frontend now receives the dispatch's wave size, and a frontend built for one wave size is not reused for another. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 27 +++++++++++++++++-- core/shader/recompiler/Recompiler.cpp | 4 +-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a754f42271..9a4bf7b90c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -168,7 +168,6 @@ void verifyFrontendIdentity() { require((preparation.AcquireFrontend(partial) == first) == reuse, "frontend pair: partial-thread state changed the frontend identity"); auto target = request; target.target.subgroupSize = 64u; - target.context.waveSize = 32u; target.context.compute->numThreads = {32u, 2u, 1u}; require((preparation.AcquireFrontend(target) == first) == reuse, "frontend pair: translation-only state changed the frontend identity"); const std::array changedUserData{1u, 2u}; @@ -197,6 +196,10 @@ void verifyFrontendIdentity() { changed.context.userDataBaseRegister = 4u; verifyMiss(changed, "frontend pair: changed user-data base reused the frontend"); changed = request; + changed.context.waveSize = 32u; + changed.context.compute->numThreads = {32u, 2u, 1u}; + verifyMiss(changed, "frontend pair: a different wave size reused the frontend"); + changed = request; changed.context.userData = std::span(userData).first(1u); verifyMiss(changed, "frontend pair: changed user-data count reused the frontend"); changed = request; @@ -222,6 +225,25 @@ void verifyFrontendIdentity() { require((preparation.AcquireFrontend(request) == first) == reuse, "frontend pair: a failed miss replaced the prior frontend"); } +void verifyFrontendWaveSizeCalls() { + using namespace ShaderRecompiler; + const std::vector code{ + 0xbb0e0005u, + 0xd4c2000eu, 0x00020300u, + 0xd5010002u, 0x003a0300u, + 0xbf820001u, + 0xbe80200eu, + 0xbf810000u}; + const std::array userData{}; + for (const std::uint32_t waveSize : {32u, 64u}) { + auto request = FrontendRequest(code, userData); + request.context.waveSize = waveSize; + request.context.compute->numThreads = {waveSize, 1u, 1u}; + ShaderPreparationContext preparation; + require(preparation.AcquireFrontend(request) != nullptr, "frontend wave size: a mask write that reuses the link pair and its v_cndmask read were refused"); + } +} + void verifyFrontendProgram(const ShaderRecompiler::IrProgram& reused, const ShaderRecompiler::IrProgram& fresh) { using namespace ShaderRecompiler; require(ProgramToString(reused) == ProgramToString(fresh), "frontend pair: reused frontend changed the IR"); @@ -256,6 +278,7 @@ void verifyFrontendProgram(const ShaderRecompiler::IrProgram& reused, const Shad void verifyFrontendPair() { using namespace ShaderRecompiler; verifyFrontendIdentity(); + verifyFrontendWaveSizeCalls(); const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; const std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; const std::array buffer{0x10000000u, 0u, 0x40u, 0x00027facu}; @@ -318,7 +341,7 @@ void verifyFrontendPair() { const auto freshChanged = PrepareResourceProgram(translated); verifyFrontendProgram(changed, freshChanged); require(changed.WaveSize() != freshFull.WaveSize(), "frontend pair: changed translation inputs were ignored"); - require((preparation.AcquireFrontend(request) == retained) == reuse, "frontend pair: translating a variant replaced the frontend"); + require(preparation.AcquireFrontend(request) != retained, "frontend pair: a variant with another wave size reused the frontend"); } for (const bool partialFirst : {false, true}) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..c1a26afc7c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -169,7 +169,7 @@ struct PreparedControlFlow { bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { return stage == request.shader.stage && mergedHull == MergedHullStage(request) && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && - swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && + swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && swappc.waveSize == swappcInfo.waveSize && std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code); } @@ -189,7 +189,7 @@ struct PreparedControlFlow { std::shared_ptr ShaderPreparationContext::AcquireFrontend(const RecompileRequest& request) { const auto inputInfo = RequestInputInfo(request); - const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), 0u, request.context.linkedCallFallbacks}; + const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize, request.context.linkedCallFallbacks}; static const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; if (reuse && frontend != nullptr && frontend->Matches(request, swappc)) { return frontend; From 83ae329b44b72da5754ba8b0fa25868dc559ff40 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Tue, 20 Oct 2026 19:03:39 -0500 Subject: [PATCH 0355/1360] test(libSceMove): assert unknown handle rejection for zero and negative handles --- core/libs/tests/GuestMove.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp index b114030e35..1750bc75f6 100644 --- a/core/libs/tests/GuestMove.cpp +++ b/core/libs/tests/GuestMove.cpp @@ -99,6 +99,8 @@ int main() { Require(sceMoveInit() == kOk); Require(sceMoveInit() == kAlreadyInit); + RequireUnknownHandle(0); + RequireUnknownHandle(-1); RequireUnknownHandle(kFirstHandle); const std::int32_t first = sceMoveOpen(kUserId, 0, 0); From 9bd2bb06cc0d9815a9b7b50d882fb7fa2dc23458 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Wed, 21 Oct 2026 15:16:35 +0300 Subject: [PATCH 0356/1360] fix(libkernel): return ENOTSOCK from sendfile on a descriptor that is not a socket FreeBSD 11 sendfile checks the file descriptor first, then looks up the socket with getsock_cap, which answers ENOTSOCK for an open descriptor that is not a socket and EBADF for one that is not open (sys/kern/kern_sendfile.c, sys/kern/uipc_syscalls.c). sendfile replaced the errno that Lookup had already set with EBADF. It now reports the errno Lookup chose, so an open non-socket gives ENOTSOCK and a closed descriptor still gives EBADF. The sentence about this gap is cut from the sendfile debt entry and guest_sendfile covers both cases. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- core/libs/tests/GuestSendfile.cpp | 7 +++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fbf308a2e4..b2289e23e2 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1617,7 +1617,7 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset }; if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast(-received)); const auto socket = Lookup(descriptor); - if (!socket) return finish(9); + if (!socket) return finish(*__error_nid_postfix()); if (socket->type != 1) return finish(22); sockaddr_storage peer{}; socklen_t size = sizeof(peer); diff --git a/core/libs/tests/GuestSendfile.cpp b/core/libs/tests/GuestSendfile.cpp index d8aa5f363e..9759531b34 100644 --- a/core/libs/tests/GuestSendfile.cpp +++ b/core/libs/tests/GuestSendfile.cpp @@ -89,7 +89,7 @@ int main() { Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &negative, &sent, 0), 22) && sent == 77); Require(Fails(sendfile_nid_postfix(pair[1], pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); sent = 77; - Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 9) && sent == 0); + Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 38) && sent == 0); const int datagram = socket_nid_postfix(2, 2, 0); Require(datagram >= 0); sent = 77; @@ -108,10 +108,13 @@ int main() { Require(sent > 0 && sent < static_cast(large.size())); Require(Receive(pair[1], static_cast(sent)) == large.substr(0, static_cast(sent))); + Require(close_nid_postfix(unconnected) == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, unconnected, 0, 0, nullptr, &sent, 0), 9) && sent == 0); Require(close_nid_postfix(largeFile) == 0 && close_nid_postfix(file) == 0); sent = 77; Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, nullptr, &sent, 0), 9) && sent == 77); - Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(unconnected) == 0); + Require(close_nid_postfix(datagram) == 0); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); std::filesystem::remove("sendfile_probe.bin"); std::filesystem::remove("sendfile_large.bin"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..943c083f2f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -216,7 +216,7 @@ - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. -- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it +- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same From 4a446319efc1581eeb373a57b0ba6417f4a3c27b Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 14:20:22 +0200 Subject: [PATCH 0357/1360] docs(libSceNgs2.native): shorten the sceNgs2GetWaveformFrameInfo debt entry --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1809b53cfc..5c50506012 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,7 +24,7 @@ - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before -- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console, and shadPS4 only stubs it. The outputs are taken to be the frame size, frame samples, units per frame and delay of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes, the superframe samples, the frames per superframe and a delay of one frame, whatever the `fact` chunk says ([info layout](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L432-L440)); prosper does not cover this call itself. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad format `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified +- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console; shadPS4 only stubs it, and prosper does not cover the call. The outputs are taken to be the frame fields of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes and samples, the frames per superframe and a one-frame delay, whatever the `fact` chunk says. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad format `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [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 From 3509bb528a093bd93e7882b8a5372aa8e86f09a7 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Wed, 21 Oct 2026 15:26:46 +0300 Subject: [PATCH 0358/1360] fix(libSceNet): report EAGAIN when a stream receive times out on Windows FreeBSD 11.4's soreceive_generic waits in sbwait (sys/kern/uipc_socket.c, uipc_sockbuf.c), whose msleep_sbt on so_rcv.sb_timeo returns EWOULDBLOCK (35) for every socket type when SO_RCVTIMEO expires. On Windows the host socket is non-blocking and native_transfer enforces the timeout itself, but it reported WSAETIMEDOUT, which only datagram receives turned into EAGAIN; a stream receive failed with ETIMEDOUT (60). A receive that reaches the stored timeout now reports WSAEWOULDBLOCK, so every receive returns EAGAIN, and a host WSAETIMEDOUT still maps to ETIMEDOUT. --- core/libs/prx/libSceNet/Export.cpp | 17 ++++------------ core/libs/tests/GuestSceNet.cpp | 31 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d6a9bd0b15..403ad000f2 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -438,15 +438,6 @@ std::int64_t message_length(const NetMsghdr* message) { return static_cast(total); } -int receive_error(const Sock& socket) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.native->receiveTimeout > 0) return NET_EAGAIN; -#else - (void)socket; -#endif - return native_error(); -} - bool message_flags_supported(int flags, int accepted) { return (flags & ~accepted) == 0; } @@ -474,7 +465,7 @@ auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOp if (timeout > 0) { const auto remaining = deadline - std::chrono::steady_clock::now(); if (remaining <= std::chrono::steady_clock::duration::zero()) { - WSASetLastError(completeLength > 0 ? WSAEWOULDBLOCK : WSAETIMEDOUT); + WSASetLastError(send && completeLength == 0 ? WSAETIMEDOUT : WSAEWOULDBLOCK); return transferred > 0 ? static_cast(transferred) : static_cast(-1); } milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); @@ -1080,7 +1071,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif - return result >= 0 ? result : fail(receive_error(socket)); + return result >= 0 ? result : fail(native_error()); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { @@ -1121,7 +1112,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif - if (result < 0) return fail(receive_error(socket)); + if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } @@ -1239,7 +1230,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { }); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); received = static_cast(buffer.size()); #else return fail(native_error()); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3f00cfe6d4..098821d987 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -580,6 +580,36 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + const int client = sceNetSocket("stream-timeout-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + const KernelTimeval timeout{0, 50000}; + Require(sceNetSetsockopt(client, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[8]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(client, buffer, sizeof(buffer), 0), 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(Failed(sceNetRecvfrom(client, buffer, sizeof(buffer), 0, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(client, &receive, 0), 35)); + const char payload[] = "late"; + Require(sceNetSend(accepted, payload, sizeof(payload), 0) == sizeof(payload)); + Require(sceNetRecv(client, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + Require(sceNetSend(client, payload, sizeof(payload), 0) == sizeof(payload)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(accepted, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckMicrosecondTimeouts(int receiver) { const int receiveTimeout = 50000; const int sendTimeout = 70000; @@ -1648,6 +1678,7 @@ int main() { stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); CheckReceiveTimeout(udp_receiver, udp_sender, address); + CheckStreamReceiveTimeout(); CheckMicrosecondTimeouts(udp_receiver); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fa35cc1780..a73d228e1d 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -182,7 +182,7 @@ - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. From f1c8ae0ccb303f879868ec2c27071ec6c07d7a63 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:24:41 +0200 Subject: [PATCH 0359/1360] fix(libkernel): report a stream whose peer closed as readable in poll on Windows WSAPoll reports POLLHUP alone for a stream socket whose peer has closed, so poll never returned POLLIN for it and a caller waiting for POLLIN did not see the end of the stream. sopoll_generic reports a socket that can receive no more as readable and adds POLLHUP only once it can send no more either. poll now adds the requested read bit and keeps POLLHUP only after the socket shut its own sending side. A reset or refused connection keeps WSAPoll's flags. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 13 +++++++++ core/libs/tests/GuestPoll.cpp | 34 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 48 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fbf308a2e4..4aaabd8324 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -533,6 +533,16 @@ constexpr short GuestPollWritable = 0x4; constexpr short GuestPollVnodeReady = GuestPollReadable | GuestPollWritable; constexpr short GuestPollHangUp = 0x10; constexpr short GuestPollError = 0x8; +#ifdef _WIN32 +short ClosedStreamEvents(int descriptor, short revents, short requested) { + if ((revents & (POLLHUP | POLLERR)) != POLLHUP) return revents; + const auto socket = Find(descriptor); + if (!socket || socket->type != 1) return revents; + revents = static_cast(revents | (requested & POLLRDNORM)); + std::lock_guard lock(socket->modeMutex); + return socket->sendShut ? revents : static_cast(revents & ~POLLHUP); +} +#endif [[noreturn]] void ThrowReadEndWritable(int descriptor) { throw std::runtime_error("poll: POLLOUT on the read end of pipe descriptor " + std::to_string(descriptor) + " is not supported"); } @@ -1714,6 +1724,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c } const auto collect = [&](std::size_t i, bool pipe) { auto& entry = descriptors[owners[i]]; +#ifdef _WIN32 + if (!pipe) native[i].revents = ClosedStreamEvents(entry.descriptor, native[i].revents, native[i].events); +#endif for (const auto& flag : RequestFlags) if ((entry.events & flag.guest) && (native[i].revents & flag.native)) entry.revents = static_cast(entry.revents | flag.guest); diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 131e75eb9d..d81f0760f6 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -31,6 +31,7 @@ int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI shutdown_nid_postfix(int, int); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI rmdir_nid_postfix(const char*); @@ -212,8 +213,41 @@ static void PollsUrgentData() { Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); } +static void PollsAStreamWhosePeerClosed() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0 && listen_nid_postfix(listener, 4) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + + Require(shutdown_nid_postfix(client, 1) == 0); + PollDescriptor wait{server, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == In); + wait = {server, static_cast(In | Out), 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Out)); + wait = {server, ReadNormal, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == ReadNormal); + wait = {server, Out, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == Out); + char byte; + Require(recv_nid_postfix(server, &byte, 1, 0) == 0); + + Require(shutdown_nid_postfix(server, 1) == 0); + wait = {server, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup)); + wait = {client, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup)); + for (const int descriptor : {client, server, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { PollsUrgentData(); + PollsAStreamWhosePeerClosed(); const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0 && listener < 1024); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..69dfd47c9d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -194,6 +194,7 @@ - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on Windows reports a guest stream socket whose peer closed as readable and adds `POLLHUP` only once the socket has shut its own sending side, as FreeBSD 11.4's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3080-L3086) does; `WSAPoll` reports `POLLHUP` alone in both cases. A reset or refused connection (`POLLHUP` with `POLLERR`) keeps `WSAPoll`'s flags, a libSceNet socket keeps them too, and a call that asks for no event still returns early on the peer's close with nothing reported. Not measured on a console. - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 From 8af8f1d54f51b5201116d975e31d63609c262aca Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:59:48 +0530 Subject: [PATCH 0360/1360] docs(libSceAppContent): clarify temporary storage debt --- docs/dev/debt/libSceAppContent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 9f9cc0981e..f212f6e04c 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -10,4 +10,4 @@ ### Functional - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified -- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with format option 1 (`TEMPORARY_DATA_OPTION_FORMAT`), formatting the directory on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does; console behavior and hardware return values remain unverified. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. +- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). From b50e5c7a4d02ec85d90edb3474319f9ebfb71b1e Mon Sep 17 00:00:00 2001 From: jacob6769 Date: Wed, 21 Oct 2026 21:29:23 +0900 Subject: [PATCH 0361/1360] feat(libSceCesCs): implement the EUC-KR, UHC and Big5 scalar conversions Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/Export.cpp | 291 ++++++++++++++++++- core/libs/prx/libSceCesCs/PendingExports.cpp | 225 -------------- core/libs/tests/GuestCesDbcs.cpp | 288 ++++++++++++++++++ docs/dev/debt/libSceCesCs.md | 4 +- 4 files changed, 578 insertions(+), 230 deletions(-) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 25dcb94514..929f6427cd 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -501,14 +502,49 @@ int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_ return 0; } -int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8Max, uint32_t* utf8Len) { +constexpr bool NATIVE_BIG_ENDIAN = std::endian::native == std::endian::big; + +template +uint32_t loadUnit(const Unit* source) { + const auto* bytes = reinterpret_cast(source); + uint32_t code = 0; + for (uint32_t i = 0; i < sizeof(Unit); ++i) code |= uint32_t(bytes[i]) << (8 * (BigEndian ? sizeof(Unit) - 1 - i : i)); + return code; +} + +template +void storeUnit(Unit* target, uint32_t code) { + auto* bytes = reinterpret_cast(target); + for (uint32_t i = 0; i < sizeof(Unit); ++i) bytes[i] = static_cast(code >> (8 * (BigEndian ? sizeof(Unit) - 1 - i : i))); +} + +template +int decodeUtf16(const uint16_t* utf16, uint32_t utf16Max, uint32_t& length, uint32_t& code) { + code = loadUnit(utf16); + length = 1; + if (code >= 0xDC00 && code <= 0xDFFF) return CES_ERROR_INVALID_ENCODE; + if (code >= 0xD800 && code <= 0xDBFF) { + if (utf16Max < 2) return CES_ERROR_SRC_BUFFER_END; + const uint32_t low = loadUnit(utf16 + 1); + if (low < 0xDC00 || low > 0xDFFF) return CES_ERROR_INVALID_ENCODE; + code = 0x10000 + ((code - 0xD800) << 10) + low - 0xDC00; + length = 2; + } + return 0; +} + +int decodeScalar(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, const void* target, uint32_t& consumed, uint32_t& code) { if (!accepted) return CES_ERROR_INVALID_PARAMETER; if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; if (mbcsMax == 0) return CES_ERROR_SRC_BUFFER_END; - if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; + if (!target) return CES_ERROR_INVALID_DST_BUFFER; + return decodeMbcs(profile, mbcs, mbcsMax, consumed, code); +} + +int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8Max, uint32_t* utf8Len) { uint32_t consumed = 0; uint32_t code = 0; - if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return result; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, utf8, consumed, code); result != 0) return result; uint8_t encoded[3]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; @@ -518,6 +554,75 @@ int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t return 0; } +int mbcsToUcs2(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* ucs2) { + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, ucs2, consumed, code); result != 0) return result; + *ucs2 = static_cast(code); + if (mbcsLen) *mbcsLen = consumed; + return 0; +} + +template +int mbcsToUtf16(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* utf16, uint32_t utf16Max, uint32_t* utf16Len) { + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, utf16, consumed, code); result != 0) return result; + if (utf16Max < 1) return CES_ERROR_DST_BUFFER_END; + storeUnit(utf16, code); + if (mbcsLen) *mbcsLen = consumed; + if (utf16Len) *utf16Len = 1; + return 0; +} + +template +int mbcsToUtf32(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf32) { + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, utf32, consumed, code); result != 0) return result; + storeUnit(utf32, code); + if (mbcsLen) *mbcsLen = consumed; + return 0; +} + +int storeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + uint8_t encoded[3]; + uint32_t produced = 0; + if (const int result = encodeMbcs(profile, code, encoded, produced); result != 0) return result; + if (mbcsMax < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(mbcs, encoded, produced); + if (mbcsLen) *mbcsLen = produced; + return 0; +} + +int scalarToMbcs(bool accepted, uint32_t code, const void* profile, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + if (code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)) return CES_ERROR_INVALID_ENCODE; + return storeMbcs(profile, code, mbcs, mbcsMax, mbcsLen); +} + +template +int utf32ToMbcs(bool accepted, const uint32_t* utf32, const void* profile, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!utf32) return CES_ERROR_INVALID_SRC_BUFFER; + return scalarToMbcs(accepted, loadUnit(utf32), profile, mbcs, mbcsMax, mbcsLen); +} + +template +int unitsToMbcs(bool accepted, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, const void* profile, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!source) return CES_ERROR_INVALID_SRC_BUFFER; + if (sourceMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decode(source, sourceMax, consumed, code); result != 0) return result; + if (const int result = storeMbcs(profile, code, mbcs, mbcsMax, mbcsLen); result != 0) return result; + if (sourceLen) *sourceLen = consumed; + return 0; +} + } extern "C" { @@ -674,4 +779,184 @@ int APS5_VABI sceCesUhcToUtf8(void* profile, uint8_t* uhcBuffer, uint32_t uhcMax return mbcsToUtf8(isUhc(profile), profile, uhcBuffer, uhcMax, uhcLength, utf8Buffer, utf8Max, utf8Length); } +int APS5_VABI sceCesEucKrToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { + return mbcsToUcs2(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, ucs2); +} + +int APS5_VABI sceCesEucKrToUtf16(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucKrToUtf16be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucKrToUtf16le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucKrToUtf32(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return mbcsToUtf32(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucKrToUtf32be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return mbcsToUtf32(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucKrToUtf32le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return mbcsToUtf32(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesUhcToUcs2(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* ucs2) { + return mbcsToUcs2(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, ucs2); +} + +int APS5_VABI sceCesUhcToUtf16(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesUhcToUtf16be(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesUhcToUtf16le(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesUhcToUtf32(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint32_t* utf32) { + return mbcsToUtf32(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf32); +} + +int APS5_VABI sceCesUhcToUtf32be(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint32_t* utf32) { + return mbcsToUtf32(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf32); +} + +int APS5_VABI sceCesUhcToUtf32le(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint32_t* utf32) { + return mbcsToUtf32(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf32); +} + +int APS5_VABI sceCesBig5ToUcs2(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* ucs2) { + return mbcsToUcs2(isBig5(profile), profile, big5Buffer, big5Max, big5Size, ucs2); +} + +int APS5_VABI sceCesBig5ToUtf16(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesBig5ToUtf16be(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesBig5ToUtf16le(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesBig5ToUtf32(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint32_t* utf32) { + return mbcsToUtf32(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf32); +} + +int APS5_VABI sceCesBig5ToUtf32be(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint32_t* utf32) { + return mbcsToUtf32(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf32); +} + +int APS5_VABI sceCesBig5ToUtf32le(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint32_t* utf32) { + return mbcsToUtf32(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf32); +} + +int APS5_VABI sceCesUcs2ToEucKr(uint16_t ucs2, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToMbcs(isEucKr(profile), ucs2, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf8ToEucKr(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToEucKr(uint32_t utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToMbcs(isEucKr(profile), utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32beToEucKr(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToMbcs(isEucKr(profile), utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32leToEucKr(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToMbcs(isEucKr(profile), utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUcs2ToUhc(uint16_t ucs2, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return scalarToMbcs(isUhc(profile), ucs2, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUtf8ToUhc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf8Buffer, utf8Max, utf8Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf16Buffer, utf16Max, utf16Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf16Buffer, utf16Max, utf16Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf16Buffer, utf16Max, utf16Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToUhc(uint32_t utf32, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return scalarToMbcs(isUhc(profile), utf32, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUtf32beToUhc(const uint32_t* utf32, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return utf32ToMbcs(isUhc(profile), utf32, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUtf32leToUhc(const uint32_t* utf32, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return utf32ToMbcs(isUhc(profile), utf32, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUcs2ToBig5(uint16_t ucs2, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return scalarToMbcs(isBig5(profile), ucs2, profile, big5Buffer, big5Max, big5Size); +} + +int APS5_VABI sceCesUtf8ToBig5(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf8Buffer, utf8Max, utf8Length, profile, big5Buffer, big5Max, big5Size, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf16Buffer, utf16Max, utf16Length, profile, big5Buffer, big5Max, big5Size, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf16Buffer, utf16Max, utf16Length, profile, big5Buffer, big5Max, big5Size, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf16Buffer, utf16Max, utf16Length, profile, big5Buffer, big5Max, big5Size, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToBig5(uint32_t utf32, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return scalarToMbcs(isBig5(profile), utf32, profile, big5Buffer, big5Max, big5Size); +} + +int APS5_VABI sceCesUtf32beToBig5(const uint32_t* utf32, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return utf32ToMbcs(isBig5(profile), utf32, profile, big5Buffer, big5Max, big5Size); +} + +int APS5_VABI sceCesUtf32leToBig5(const uint32_t* utf32, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return utf32ToMbcs(isBig5(profile), utf32, profile, big5Buffer, big5Max, big5Size); +} + } diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 165fe278e8..88adbc635b 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -2,41 +2,6 @@ extern "C" { -int APS5_VABI sceCesBig5ToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesEucCnToUcs2(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -117,41 +82,6 @@ int APS5_VABI sceCesEucJpUcsProfileSetSbcs(void) { return 0; } -int APS5_VABI sceCesEucKrToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesEucKrUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -732,11 +662,6 @@ int APS5_VABI sceCesUcs2StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUcs2ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -747,11 +672,6 @@ int APS5_VABI sceCesUcs2ToEucJp(void) { return 0; } -int APS5_VABI sceCesUcs2ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -772,11 +692,6 @@ int APS5_VABI sceCesUcs2ToSbc(void) { return 0; } -int APS5_VABI sceCesUcs2ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitEucCnGb2312(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -847,41 +762,6 @@ int APS5_VABI sceCesUcsToJiscs(void) { return 0; } -int APS5_VABI sceCesUhcToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUhcUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -907,11 +787,6 @@ int APS5_VABI sceCesUtf16StrToMbcsStr(void) { return 0; } -int APS5_VABI sceCesUtf16ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -922,11 +797,6 @@ int APS5_VABI sceCesUtf16ToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf16ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -947,16 +817,6 @@ int APS5_VABI sceCesUtf16ToSbc(void) { return 0; } -int APS5_VABI sceCesUtf16ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16beToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -967,11 +827,6 @@ int APS5_VABI sceCesUtf16beToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf16beToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -992,16 +847,6 @@ int APS5_VABI sceCesUtf16beToSbc(void) { return 0; } -int APS5_VABI sceCesUtf16beToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16leToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1012,11 +857,6 @@ int APS5_VABI sceCesUtf16leToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf16leToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1037,11 +877,6 @@ int APS5_VABI sceCesUtf16leToSbc(void) { return 0; } -int APS5_VABI sceCesUtf16leToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1062,11 +897,6 @@ int APS5_VABI sceCesUtf32StrToMbcsStr(void) { return 0; } -int APS5_VABI sceCesUtf32ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1077,11 +907,6 @@ int APS5_VABI sceCesUtf32ToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf32ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1102,16 +927,6 @@ int APS5_VABI sceCesUtf32ToSbc(void) { return 0; } -int APS5_VABI sceCesUtf32ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32beToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1122,11 +937,6 @@ int APS5_VABI sceCesUtf32beToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf32beToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1147,16 +957,6 @@ int APS5_VABI sceCesUtf32beToSbc(void) { return 0; } -int APS5_VABI sceCesUtf32beToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32leToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1167,11 +967,6 @@ int APS5_VABI sceCesUtf32leToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf32leToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1192,11 +987,6 @@ int APS5_VABI sceCesUtf32leToSbc(void) { return 0; } -int APS5_VABI sceCesUtf32leToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1207,11 +997,6 @@ int APS5_VABI sceCesUtf8StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf8ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1222,11 +1007,6 @@ int APS5_VABI sceCesUtf8ToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf8ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1242,9 +1022,4 @@ int APS5_VABI sceCesUtf8ToSJis(void) { return 0; } -int APS5_VABI sceCesUtf8ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index 552f5193e9..753c8f5c3e 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -16,9 +16,84 @@ int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUcs2StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesEucKrToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUhcToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesUhcToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesBig5ToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesBig5ToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUcs2ToEucKr(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToEucKr(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToEucKr(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToEucKr(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToEucKr(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToEucKr(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToEucKr(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToEucKr(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToUhc(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToUhc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToUhc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToUhc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToUhc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToUhc(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToUhc(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToUhc(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToBig5(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToBig5(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToBig5(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToBig5(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToBig5(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToBig5(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToBig5(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToBig5(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); } using ToUtf8 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using ToUcs2 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +using ToUtf16 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +using ToUtf32 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +using FromUcs2 = int (APS5_VABI*)(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf8 = int (APS5_VABI*)(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf16 = int (APS5_VABI*)(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf32 = int (APS5_VABI*)(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf32Unit = int (APS5_VABI*)(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); + +struct Decoders { + ToUtf8 utf8; + ToUcs2 ucs2; + ToUtf16 utf16[3]; + ToUtf32 utf32[3]; +}; + +struct Encoders { + FromUcs2 ucs2; + FromUtf8 utf8; + FromUtf16 utf16[3]; + FromUtf32 utf32; + FromUtf32Unit utf32Unit[3]; +}; + +constexpr Decoders eucKrDecoders = {sceCesEucKrToUtf8, sceCesEucKrToUcs2, {sceCesEucKrToUtf16, sceCesEucKrToUtf16be, sceCesEucKrToUtf16le}, {sceCesEucKrToUtf32, sceCesEucKrToUtf32be, sceCesEucKrToUtf32le}}; +constexpr Decoders uhcDecoders = {sceCesUhcToUtf8, sceCesUhcToUcs2, {sceCesUhcToUtf16, sceCesUhcToUtf16be, sceCesUhcToUtf16le}, {sceCesUhcToUtf32, sceCesUhcToUtf32be, sceCesUhcToUtf32le}}; +constexpr Decoders big5Decoders = {sceCesBig5ToUtf8, sceCesBig5ToUcs2, {sceCesBig5ToUtf16, sceCesBig5ToUtf16be, sceCesBig5ToUtf16le}, {sceCesBig5ToUtf32, sceCesBig5ToUtf32be, sceCesBig5ToUtf32le}}; +constexpr Encoders eucKrEncoders = {sceCesUcs2ToEucKr, sceCesUtf8ToEucKr, {sceCesUtf16ToEucKr, sceCesUtf16beToEucKr, sceCesUtf16leToEucKr}, sceCesUtf32ToEucKr, {nullptr, sceCesUtf32beToEucKr, sceCesUtf32leToEucKr}}; +constexpr Encoders uhcEncoders = {sceCesUcs2ToUhc, sceCesUtf8ToUhc, {sceCesUtf16ToUhc, sceCesUtf16beToUhc, sceCesUtf16leToUhc}, sceCesUtf32ToUhc, {nullptr, sceCesUtf32beToUhc, sceCesUtf32leToUhc}}; +constexpr Encoders big5Encoders = {sceCesUcs2ToBig5, sceCesUtf8ToBig5, {sceCesUtf16ToBig5, sceCesUtf16beToBig5, sceCesUtf16leToBig5}, sceCesUtf32ToBig5, {nullptr, sceCesUtf32beToBig5, sceCesUtf32leToBig5}}; constexpr int invalidParameter = static_cast(0x805C0001); constexpr int invalidSrcBuffer = static_cast(0x805C0010); @@ -110,6 +185,137 @@ static std::uint32_t RoundTripMismatches(void* context) { return mismatches; } +static std::uint32_t Load(const void* data, std::uint32_t size, int order) { + if (order == 0) { + std::uint16_t unit = 0; + std::uint32_t wide = 0; + if (size == 2) std::memcpy(&unit, data, 2); + else std::memcpy(&wide, data, 4); + return size == 2 ? unit : wide; + } + const auto* bytes = static_cast(data); + std::uint32_t value = 0; + for (std::uint32_t i = 0; i < size; ++i) value |= static_cast(bytes[i]) << (8 * (order == 1 ? size - 1 - i : i)); + return value; +} + +static void Store(void* data, std::uint32_t size, int order, std::uint32_t value) { + if (order == 0) { + const auto unit = static_cast(value); + if (size == 2) std::memcpy(data, &unit, 2); + else std::memcpy(data, &value, 4); + return; + } + auto* bytes = static_cast(data); + for (std::uint32_t i = 0; i < size; ++i) bytes[i] = static_cast(value >> (8 * (order == 1 ? size - 1 - i : i))); +} + +static std::uint32_t Utf8Code(const std::uint8_t* utf8, std::uint32_t length) { + if (length == 1) return utf8[0]; + if (length == 2) return (utf8[0] & 0x1Fu) << 6 | (utf8[1] & 0x3Fu); + return (utf8[0] & 0x0Fu) << 12 | (utf8[1] & 0x3Fu) << 6 | (utf8[2] & 0x3Fu); +} + +static std::uint32_t Utf8Bytes(std::uint32_t code, std::uint8_t* utf8) { + if (code < 0x80) { + utf8[0] = static_cast(code); + return 1; + } + const std::uint32_t length = code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + for (std::uint32_t i = length - 1; i > 0; --i) { + utf8[i] = static_cast(0x80 | (code & 0x3F)); + code >>= 6; + } + utf8[0] = static_cast((length == 2 ? 0xC0 : length == 3 ? 0xE0 : 0xF0) | code); + return length; +} + +static void RequireDecoders(const Decoders& decoders, const void* profile) { + for (std::uint32_t lead = 0; lead < 256; ++lead) { + for (std::uint32_t trail = 0; trail < 256; ++trail) { + const std::uint8_t source[2] = {static_cast(lead), static_cast(trail)}; + std::uint8_t utf8[4] = {}; + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + const int expected = decoders.utf8(profile, source, sizeof(source), &consumed, utf8, sizeof(utf8), &produced); + const std::uint32_t code = expected == 0 ? Utf8Code(utf8, produced) : 0; + std::uint16_t ucs2 = 0x5555; + std::uint32_t read = 77; + Require(decoders.ucs2(profile, source, sizeof(source), &read, &ucs2) == expected); + Require(expected == 0 ? read == consumed && ucs2 == code : read == 77 && ucs2 == 0x5555); + for (int order = 0; order < 3; ++order) { + std::uint16_t utf16 = 0x5555; + std::uint32_t units = 77; + read = 77; + Require(decoders.utf16[order](profile, source, sizeof(source), &read, &utf16, 1, &units) == expected); + Require(expected == 0 ? read == consumed && units == 1 && Load(&utf16, 2, order) == code : read == 77 && units == 77 && utf16 == 0x5555); + std::uint32_t utf32 = 0x55555555; + read = 77; + Require(decoders.utf32[order](profile, source, sizeof(source), &read, &utf32) == expected); + Require(expected == 0 ? read == consumed && Load(&utf32, 4, order) == code : read == 77 && utf32 == 0x55555555); + } + } + } +} + +struct Encoded { + std::uint8_t bytes[2] = {0x55, 0x55}; + std::uint32_t size = 77; + std::uint32_t read = 77; +}; + +static int Encode(const Encoders& encoders, int form, std::uint32_t code, const void* profile, Encoded& out) { + std::uint8_t utf8[4] = {}; + std::uint16_t utf16[2] = {}; + std::uint32_t utf32 = 0; + if (form == 0) return encoders.ucs2(static_cast(code), profile, out.bytes, sizeof(out.bytes), &out.size); + if (form == 1) return encoders.utf8(utf8, Utf8Bytes(code, utf8), &out.read, profile, out.bytes, sizeof(out.bytes), &out.size); + if (form <= 4) { + const int order = form - 2; + if (code < 0x10000) Store(utf16, 2, order, code); + else { + Store(utf16, 2, order, 0xD800 | (code - 0x10000) >> 10); + Store(utf16 + 1, 2, order, 0xDC00 | (code & 0x3FF)); + } + return encoders.utf16[order](utf16, code < 0x10000 ? 1 : 2, &out.read, profile, out.bytes, sizeof(out.bytes), &out.size); + } + if (form == 5) return encoders.utf32(code, profile, out.bytes, sizeof(out.bytes), &out.size); + Store(&utf32, 4, form - 5, code); + return encoders.utf32Unit[form - 5](&utf32, profile, out.bytes, sizeof(out.bytes), &out.size); +} + +static std::uint32_t SourceUnits(int form, std::uint32_t code) { + if (form == 1) return code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + if (form >= 2 && form <= 4) return code < 0x10000 ? 1 : 2; + return 77; +} + +static void RequireEncode(const Encoders& encoders, const void* profile, std::uint32_t code, int expected, const std::uint8_t* bytes, std::uint32_t size) { + for (int form = 0; form < 8; ++form) { + if (form == 0 && code > 0xFFFF) continue; + Encoded out; + Require(Encode(encoders, form, code, profile, out) == expected); + if (expected == 0) Require(out.read == SourceUnits(form, code) && out.size == size && std::memcmp(out.bytes, bytes, size) == 0); + else Require(out.read == 77 && out.size == 77 && out.bytes[0] == 0x55 && out.bytes[1] == 0x55); + } +} + +static void RequireEncoders(const Encoders& encoders, const void* profile, void* context) { + Require(sceCesMbcsUcsContextInit(context, profile) == 0); + for (std::uint32_t code = 1; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + const auto ucs2 = static_cast(code); + std::uint8_t bytes[2] = {}; + std::uint32_t read = 0; + std::uint32_t written = 0; + const int expected = sceCesUcs2StrToMbcsStr(context, &ucs2, 1, &read, bytes, sizeof(bytes), &written); + RequireEncode(encoders, profile, code, expected, bytes, written); + } + RequireEncode(encoders, profile, 0, 0, Bytes("\0"), 1); + RequireEncode(encoders, profile, 0x20000, unassignedCode, nullptr, 0); + RequireEncode(encoders, profile, 0x10FFFF, unassignedCode, nullptr, 0); +} + int main() { alignas(8) std::uint8_t eucKrSheet[64] = {}; alignas(8) std::uint8_t uhcSheet[64] = {}; @@ -175,5 +381,87 @@ int main() { RequireString(context, "\xF9\xE9\xF9\xEB\xF9\xEA\xA2\xA1\xA2\xA3\xA2\xA2", "\xE2\x95\x9E\xE2\x95\xA1\xE2\x95\xAA\xE2\x95\xAE\xE2\x95\xAF\xE2\x95\xB0"); Require(Utf8ToMbcs(context, "\xEA\xB0\x80") == unassignedCode); Require(RoundTripMismatches(context) == 10); + + RequireDecoders(eucKrDecoders, eucKr); + RequireDecoders(uhcDecoders, uhc); + RequireDecoders(big5Decoders, big5); + RequireEncoders(eucKrEncoders, eucKr, context); + RequireEncoders(uhcEncoders, uhc, context); + RequireEncoders(big5Encoders, big5, context); + + const std::uint8_t* ga = Bytes("\xB0\xA1"); + std::uint16_t unit = 0; + std::uint32_t wide = 0; + std::uint32_t read = 0; + std::uint32_t units = 0; + Require(sceCesEucKrToUtf16be(eucKr, ga, 2, &read, &unit, 1, &units) == 0 && read == 2 && units == 1 && std::memcmp(&unit, "\xAC\x00", 2) == 0); + Require(sceCesEucKrToUtf16le(eucKr, ga, 2, &read, &unit, 1, &units) == 0 && std::memcmp(&unit, "\x00\xAC", 2) == 0); + Require(sceCesEucKrToUtf16(eucKr, ga, 2, nullptr, &unit, 1, nullptr) == 0 && unit == 0xAC00); + Require(sceCesEucKrToUtf32be(eucKr, ga, 2, &read, &wide) == 0 && read == 2 && std::memcmp(&wide, "\x00\x00\xAC\x00", 4) == 0); + Require(sceCesEucKrToUtf32le(eucKr, ga, 2, &read, &wide) == 0 && std::memcmp(&wide, "\x00\xAC\x00\x00", 4) == 0); + Require(sceCesEucKrToUtf32(eucKr, ga, 2, nullptr, &wide) == 0 && wide == 0xAC00); + Require(sceCesEucKrToUcs2(eucKr, Bytes("\0"), 1, &read, &unit) == 0 && read == 1 && unit == 0); + Require(sceCesUhcToUcs2(uhc, Bytes("\x8C\x63"), 2, nullptr, &unit) == 0 && unit == 0xB620); + Require(sceCesBig5ToUtf32le(big5, Bytes("\xA4\xA4"), 2, &read, &wide) == 0 && std::memcmp(&wide, "\x2D\x4E\x00\x00", 4) == 0); + + Require(sceCesEucKrToUtf16(uhc, nullptr, 0, nullptr, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesEucKrToUtf16(eucKr, nullptr, 0, nullptr, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesEucKrToUtf16(eucKr, ga, 0, nullptr, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesEucKrToUtf16(eucKr, ga, 2, nullptr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesEucKrToUtf16(eucKr, Bytes("\x8C\x63"), 2, nullptr, &unit, 0, nullptr) == invalidEncode); + unit = 0x5555; + read = 77; + units = 77; + Require(sceCesEucKrToUtf16(eucKr, ga, 2, &read, &unit, 0, &units) == dstBufferEnd && unit == 0x5555 && read == 77 && units == 77); + Require(sceCesUhcToUcs2(uhc, ga, 2, nullptr, nullptr) == invalidDstBuffer); + Require(sceCesBig5ToUtf32(big5, Bytes("\xA4\xA4"), 1, nullptr, &wide) == srcBufferEnd); + Require(sceCesBig5ToUtf32be(eucJp, Bytes("\xA4\xA4"), 2, nullptr, &wide) == invalidParameter); + Require(sceCesBig5ToUtf32le(big5, Bytes("\xA4\xA4"), 2, nullptr, nullptr) == invalidDstBuffer); + + RequireEncode(eucKrEncoders, eucKr, 0x20AC, 0, Bytes("\xA2\xE6"), 2); + RequireEncode(eucKrEncoders, eucKr, 0xB620, unassignedCode, nullptr, 0); + RequireEncode(uhcEncoders, uhc, 0xB620, 0, Bytes("\x8C\x63"), 2); + RequireEncode(big5Encoders, big5, 0x2550, 0, Bytes("\xF9\xF9"), 2); + RequireEncode(big5Encoders, big5, 0x5341, 0, Bytes("\xA4\x51"), 2); + RequireEncode(big5Encoders, big5, 0xAC00, unassignedCode, nullptr, 0); + RequireEncode(eucKrEncoders, uhc, 0xAC00, invalidParameter, nullptr, 0); + RequireEncode(uhcEncoders, nullptr, 0xAC00, invalidParameter, nullptr, 0); + RequireEncode(big5Encoders, eucJp, 0x4E2D, invalidParameter, nullptr, 0); + + std::uint8_t out[2] = {0x55, 0x55}; + std::uint32_t size = 77; + read = 77; + const std::uint32_t big = 0x0000AC00; + Require(sceCesUcs2ToEucKr(0xAC00, uhc, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUcs2ToEucKr(0xD800, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUcs2ToEucKr(0xD800, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUtf32ToUhc(0x110000, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf32ToUhc(0xDFFF, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf32beToBig5(nullptr, eucKr, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf32beToBig5(nullptr, big5, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf32leToBig5(&big, big5, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUtf8ToEucKr(nullptr, 0, nullptr, uhc, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf8ToEucKr(nullptr, 0, nullptr, eucKr, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 0, nullptr, eucKr, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 3, nullptr, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 2, &read, eucKr, out, 2, &size) == srcBufferEnd); + Require(sceCesUtf8ToEucKr(Bytes("\xC0\x80"), 2, &read, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 3, &read, eucKr, out, 1, &size) == dstBufferEnd); + const std::uint16_t lowSurrogate[1] = {0xDC00}; + const std::uint16_t highSurrogate[2] = {0xD800, 0x0041}; + Require(sceCesUtf16ToUhc(lowSurrogate, 1, &read, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf16ToUhc(highSurrogate, 1, &read, uhc, out, 2, &size) == srcBufferEnd); + Require(sceCesUtf16ToUhc(highSurrogate, 2, &read, uhc, out, 2, &size) == invalidEncode); + Require(out[0] == 0x55 && out[1] == 0x55 && size == 77 && read == 77); + + std::memcpy(&unit, "\xAC\x00", 2); + Require(sceCesUtf16beToEucKr(&unit, 1, &read, eucKr, out, 2, &size) == 0 && read == 1 && size == 2 && std::memcmp(out, "\xB0\xA1", 2) == 0); + std::memcpy(&unit, "\x00\xAC", 2); + Require(sceCesUtf16leToUhc(&unit, 1, nullptr, uhc, out, 2, nullptr) == 0 && std::memcmp(out, "\xB0\xA1", 2) == 0); + std::memcpy(&wide, "\x00\x00\x4E\x2D", 4); + Require(sceCesUtf32beToBig5(&wide, big5, out, 2, &size) == 0 && size == 2 && std::memcmp(out, "\xA4\xA4", 2) == 0); + std::memcpy(&wide, "\x2D\x4E\x00\x00", 4); + Require(sceCesUtf32leToBig5(&wide, big5, out, 2, nullptr) == 0 && std::memcmp(out, "\xA4\xA4", 2) == 0); + Require(sceCesUcs2ToEucKr(0, eucKr, out, 1, &size) == 0 && size == 1 && out[0] == 0); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 14cef5e67e..0cc0674e2e 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,5 +11,5 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 249 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. -- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 204 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Unicode scalar conversions do From f2bc1195f863f148ada0822a3d841a9f9c481f5b Mon Sep 17 00:00:00 2001 From: Retr0 Date: Wed, 21 Oct 2026 15:22:38 +0300 Subject: [PATCH 0362/1360] fix(libkernel): reject socket addresses longer than sockaddr_in or sockaddr_in6 with EINVAL FreeBSD 11.4 getsockaddr sets sa_len to the length argument, and tcp_usr_bind, tcp_usr_connect, in_pcbbind_setup, in_pcbconnect_setup, in6_pcbbind and in6_pcbladdr answer EINVAL when it is not the size of the socket family's address. udp6_bind and udp6_connect pass an IPv4-mapped address to the IPv4 code without that check when IPV6_V6ONLY is clear, and udp6_send always checks it. bind and connect on AF_INET and AF_INET6 sockets, and sendto on AF_INET6 datagram sockets, now fail a longer address with EINVAL. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 ++++ core/libs/tests/GuestSocketAddressLength.cpp | 64 ++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fbf308a2e4..66f967656d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -290,6 +290,15 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, Fail(47); return false; } +int LongAddressError(const Socket& socket, const void* address, std::uint32_t length) { + if ((socket.family != 2 && socket.family != 28) || length <= (socket.family == 2 ? 16u : 28u)) return 0; + constexpr unsigned char mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + if (socket.family != 28 || socket.type != 2 || std::memcmp(static_cast(address) + 8, mapped, sizeof(mapped)) != 0) return 22; + int ipv6Only = 0; + socklen_t size = sizeof(ipv6Only); + if (::getsockopt(socket.value, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), &size)) return NativeError(); + return ipv6Only ? 22 : 0; +} std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } @@ -1190,6 +1199,7 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); #endif if (socket->type == 1 && Listening(*socket)) return Fail(45); + if (const int error = LongAddressError(*socket, address, length)) return Fail(error); if (ZeroPortConnect(*socket, address, length)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { @@ -1268,6 +1278,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; + if (const int error = LongAddressError(*socket, address, length)) return Fail(error); return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { @@ -1351,6 +1362,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st sockaddr_storage native{}; socklen_t size; if (!Address(address, addressLength, native, size)) return -1; + if (socket->family == 28 && socket->type == 2 && addressLength > 28) return Fail(22); if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 930a8626d6..e0e4833d0d 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -9,6 +9,9 @@ extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); @@ -64,10 +67,71 @@ void FullAddressesStillWork() { Require(close_nid_postfix(stream) == 0, "close failed"); } +std::array PaddedLoopback(int family) { + std::array address{}; + if (family == 2) { + const auto v4 = Loopback(); + std::memcpy(address.data(), v4.data(), v4.size()); + } else { + address[0] = 28; + address[1] = 28; + address[23] = 1; + } + return address; +} + +void LongAddressesAreInvalid(int family, int type) { + const std::uint32_t size = family == 2 ? 16 : 28; + auto address = PaddedLoopback(family); + const int descriptor = socket_nid_postfix(family, type, 0); + const int listener = type == Stream ? socket_nid_postfix(family, Stream, 0) : descriptor; + Require(descriptor >= 0 && listener >= 0, "socket failed"); + for (std::uint32_t length = size + 1; length <= address.size(); ++length) { + Require(Fails(bind_nid_postfix(descriptor, address.data(), length), ErrorInvalid), "bind with an address longer than sockaddr_in or sockaddr_in6 must fail with EINVAL (22)"); + } + Require(bind_nid_postfix(listener, address.data(), size) == 0, "bind with an address of exactly the size of sockaddr_in or sockaddr_in6 failed"); + if (type == Stream) Require(listen_nid_postfix(listener, 1) == 0, "listen failed"); + std::uint32_t named = size; + Require(getsockname_nid_postfix(listener, address.data(), &named) == 0 && named == size, "getsockname failed"); + for (std::uint32_t length = size + 1; length <= address.size(); ++length) { + Require(Fails(connect_nid_postfix(descriptor, address.data(), length), ErrorInvalid), "connect with an address longer than sockaddr_in or sockaddr_in6 must fail with EINVAL (22)"); + } + if (family == 28 && type == Datagram) { + for (std::uint32_t length = size + 1; length <= address.size(); ++length) { + Require(Fails(sendto_nid_postfix(descriptor, "x", 1, 0, address.data(), length), ErrorInvalid), "sendto on an AF_INET6 datagram socket with an address longer than sockaddr_in6 must fail with EINVAL (22)"); + } + Require(sendto_nid_postfix(descriptor, "x", 1, 0, address.data(), size) == 1, "sendto with an address of exactly the size of sockaddr_in6 failed"); + } + Require(connect_nid_postfix(descriptor, address.data(), size) == 0, "connect with an address of exactly the size of sockaddr_in or sockaddr_in6 failed"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + if (listener != descriptor) Require(close_nid_postfix(listener) == 0, "close failed"); +} + +void LongMappedAddressesBindOnlyDualStackDatagramSockets() { + const int datagram = socket_nid_postfix(28, Datagram, 0); + Require(datagram >= 0, "socket failed"); + std::array mapped{29, 28}; + mapped[18] = 0xff; + mapped[19] = 0xff; + mapped[20] = 127; + mapped[23] = 1; + Require(Fails(bind_nid_postfix(datagram, mapped.data(), mapped.size()), ErrorInvalid), "bind of a long IPv4-mapped address with IPV6_V6ONLY set must fail with EINVAL (22)"); + const int off = 0; + Require(setsockopt_nid_postfix(datagram, 41, 27, &off, sizeof(off)) == 0, "clearing IPV6_V6ONLY failed"); + Require(bind_nid_postfix(datagram, mapped.data(), mapped.size()) == 0, "udp6_bind passes a long IPv4-mapped address to in_pcbbind without IPV6_V6ONLY, so bind must succeed"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, mapped.data(), mapped.size()), ErrorInvalid), "udp6_send checks the length before the IPv4-mapped case, so sendto must fail with EINVAL (22)"); + Require(close_nid_postfix(datagram) == 0, "close failed"); +} + } int main() { ShortAddressesAreInvalid(); FullAddressesStillWork(); + LongAddressesAreInvalid(2, Stream); + LongAddressesAreInvalid(2, Datagram); + LongAddressesAreInvalid(28, Stream); + LongAddressesAreInvalid(28, Datagram); + LongMappedAddressesBindOnlyDualStackDatagramSockets(); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..b66c342ff4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -189,7 +189,7 @@ - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws - [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. An address longer than `sockaddr_in` or `sockaddr_in6` is still accepted where FreeBSD answers `EINVAL`, and an address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for these is not known. None of this was measured on a console. +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. `sendto` does not check an address longer than `sockaddr_in` on an `AF_INET` datagram socket, where `udp_output` answers `EINVAL` unless the socket has a local address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, nor on an unconnected stream socket, which FreeBSD connects implicitly through `tcp_connect` and its length test. An address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for it is not known. None of this was measured on a console. - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number From d72c5dcd0d99b256dfa19008ba8785be803df170 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 19 Oct 2026 13:51:27 +0530 Subject: [PATCH 0363/1360] fix(shader): keep host interpolation across entry EXEC copies --- .../libSceAgcDriver/tests/ConstantBitTest.cpp | 77 +++++++++ .../execution/PixelExecInterpolation.cpp | 159 ++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 24 +-- 3 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index 06d73e6578..d7bac76983 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -1,6 +1,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/ConstantFolder.hpp" #include "Optimization/DeadCodeEliminator.hpp" +#include #include #include #include @@ -203,6 +204,71 @@ bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32 return retainedLane; } +enum class QuadMask { + Expanded, + Intersected, + Nested, + SeparateHalves, + RealOnly, + Partial, + WrongHalf, + Divergent, +}; + +bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderStage stage = IrShaderStage::Pixel) { + IrProgram program; + program.Resources().stage = stage; + program.SetWaveSize(wave); + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + if (shape == QuadMask::Divergent) { + auto& taken = program.CreateBlock(); + auto& other = program.CreateBlock(); + program.BlockOrder().push_back(&taken); + program.BlockOrder().push_back(&other); + builder.BranchConditional(builder.ULessThan(lane, builder.Constant(16u)), taken, other); + builder.SetInsertionPoint(other); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + builder.SetInsertionPoint(taken); + } + const auto ballot = [&](IrValue& condition) { + auto& mask = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&condition}); + return std::array{&builder.CompositeExtract(mask, 0u), wave == 64u ? &builder.CompositeExtract(mask, 1u) : &builder.Constant(0u)}; + }; + const auto expand = [&](const std::array& words) { + auto& wide = builder.ConstructU64(*words[0], *words[1]); + auto& mask = builder.Emit(IrOpcode::WqmU64, IrType::U64, {&wide}); + return std::array{&builder.CompositeExtract(mask, 0u), &builder.CompositeExtract(mask, 1u)}; + }; + auto& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + const auto real = ballot(builder.IEqual(helper, builder.Constant(0u))); + const auto quads = expand(real); + const auto host = expand(ballot(builder.ConstantBool(true))); + auto words = quads; + if (shape == QuadMask::Intersected || shape == QuadMask::Nested || shape == QuadMask::SeparateHalves || shape == QuadMask::RealOnly) { + const auto highHost = shape == QuadMask::SeparateHalves ? expand(ballot(builder.ConstantBool(true))) : host; + const auto source = shape == QuadMask::Nested || shape == QuadMask::RealOnly ? real : quads; + words = {&builder.BitwiseAnd(*source[0], *host[0]), &builder.BitwiseAnd(*source[1], *highHost[1])}; + if (shape == QuadMask::Nested) words = expand(words); + } + if (shape == QuadMask::Partial) { + words = {&builder.BitwiseAnd(*words[0], builder.Constant(0x0000ffffu)), &builder.Constant(0u)}; + } + if (shape == QuadMask::WrongHalf) words[1] = words[0]; + IrValue& word = wave == 64u ? builder.Select(builder.ULessThan(lane, builder.Constant(32u)), *words[0], *words[1]) : *words[0]; + auto& shifted = builder.ShiftRightLogical(word, builder.BitwiseAnd(lane, builder.Constant(31u))); + auto& test = builder.INotEqual(builder.BitwiseAnd(shifted, builder.Constant(1u)), builder.Constant(0u)); + auto& user = builder.Emit(IrOpcode::Reference, IrType::Void, {&test}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder{}.Fold(program); + const auto* result = user.Argument(0)->Resolve(); + return expected ? result->HasImmediate() && result->ImmediateBool() : !result->HasImmediate(); +} + bool run(const char* name, bool passed) { if (!passed) std::fprintf(stderr, "%s\n", name); return passed; @@ -213,6 +279,17 @@ bool run(const char* name, bool passed) { int main() { try { bool passed = true; + for (const auto wave : {32u, 64u}) { + passed &= run("a whole-quad entry mask covers pixel helpers", quadProjection(wave, QuadMask::Expanded, true)); + passed &= run("intersecting whole-quad entry masks preserves pixel helpers", quadProjection(wave, QuadMask::Intersected, true)); + passed &= run("a nested whole-quad entry mask covers pixel helpers", quadProjection(wave, QuadMask::Nested, true)); + passed &= run("independent low and high whole-quad masks cover pixel helpers", quadProjection(wave, QuadMask::SeparateHalves, true)); + passed &= run("an unexpanded entry mask must keep the helper bit test", quadProjection(wave, QuadMask::RealOnly, false)); + passed &= run("a partial whole-quad mask must keep the lane bit test", quadProjection(wave, QuadMask::Partial, false)); + passed &= run("a divergent whole-quad ballot must keep the lane bit test", quadProjection(wave, QuadMask::Divergent, false)); + passed &= run("whole-quad entry proofs do not apply to other stages", quadProjection(wave, QuadMask::Intersected, false, IrShaderStage::Vertex)); + } + passed &= run("a low word cannot prove the high-half helper bits", quadProjection(64u, QuadMask::WrongHalf, false)); passed &= run("an all-zero word must fold the lane bit test to false", foldsTo(0u, false, true)); passed &= run("an all-one word must fold the lane bit test to true", foldsTo(0xffffffffu, true, true)); passed &= run("a mixed word must keep the lane bit test", foldsTo(0x0000ffffu, false, true)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp new file mode 100644 index 0000000000..f9d09c28f4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp @@ -0,0 +1,159 @@ +#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 Width = 64; +constexpr std::uint32_t Height = 48; +alignas(256) std::array Pixels{}; +constexpr std::array, 3> Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf800020f, 0x03020100, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array ExecCopyPixelCode{ + 0xbefe0a7e, 0xc8080000, 0xbe80047e, 0xbefe0400, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, +}; + +alignas(256) constexpr std::array RepeatedWqmPixelCode{ + 0xbefe0a7e, 0xc8080000, 0xbefe0a7e, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, +}; + +alignas(256) constexpr std::array ExpandedExecPixelCode{ + 0xc8080000, 0xbefe0a7e, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, +}; + +std::array, 3> ClipPositions() { + std::array, 3> positions{}; + for (std::size_t i = 0; i < Corners.size(); ++i) positions[i] = {Corners[i][0] * Corners[i][2], Corners[i][1] * Corners[i][2], 0.5f * Corners[i][2], Corners[i][2]}; + return positions; +} + +struct Expected { + bool inside; + float value; +}; + +Expected Interpolated(std::uint32_t x, std::uint32_t y, bool snapped) { + std::array, 3> window{}; + for (std::size_t i = 0; i < Corners.size(); ++i) window[i] = {(Corners[i][0] + 1.0) * Width / 2.0, (1.0 - Corners[i][1]) * Height / 2.0}; + if (snapped) { + for (auto& corner : window) for (auto& coordinate : corner) coordinate = std::round(coordinate * 256.0) / 256.0; + } + const double px = x + 0.5; + const double py = y + 0.5; + const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; + const double area = edge(window[0], window[1], window[2][0], window[2][1]); + const std::array weights{edge(window[1], window[2], px, py) / area, edge(window[2], window[0], px, py) / area, edge(window[0], window[1], px, py) / area}; + double denominator = 0.0; + for (std::size_t i = 0; i < Corners.size(); ++i) denominator += weights[i] / Corners[i][2]; + const auto positions = ClipPositions(); + double value = 0.0; + for (std::size_t i = 0; i < Corners.size(); ++i) value += weights[i] / Corners[i][2] / denominator * positions[i][0]; + return {std::min({weights[0], weights[1], weights[2]}) > 0.02, static_cast(value)}; +} + +std::uint32_t CheckPixels() { + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = Interpolated(x, y, false); + if (!expected.inside) continue; + const auto snapped = Interpolated(x, y, true); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto value = Pixels[(y * Width + x) * 4u + channel]; + Require(std::fabs(value - expected.value) <= 1e-4f || std::fabs(value - snapped.value) <= 1e-4f, "EXEC interpolation: incorrect pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel)); + } + ++checked; + } + } + Require(checked > 200u, "EXEC interpolation: too few interior pixels were checked"); + return checked; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + auto target = device.Target(); + target.fragmentShaderBarycentricEnabled = false; + static const auto positions = ClipPositions(); + const auto address = reinterpret_cast(positions.data()); + const std::array vertexUserData{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), 3u, 0x01016facu}; + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.interpolatorCount = 1; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.executeOnNoop = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const auto compile = [&](std::span code) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, {}, std::nullopt, pixel, std::nullopt, memory}, target, PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + return ShaderRecompiler::Recompile(fragment); + }; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32G32B32A32_SFLOAT, sizeof(Pixels), 0xe4u, AgcDriver::Graphics::ColorTileMode::Linear, 16u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + for (const auto code : {std::span(ExecCopyPixelCode), std::span(RepeatedWqmPixelCode)}) { + const auto result = compile(code); + Require(!result.barycentricEmulation.active, "changing EXEC between v_interp_p1/p2 needed a generated stage"); + Pixels.fill(-7.0f); + const std::array shaders{{{ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, &result, PixelPushOffset(vertexPush, target)}}}; + device.Draw(state, {0, 3, 0, 1, 0, false}, shaders); + device.WaitIdle(); + std::printf("wave%u %zu words: host interpolation matched %u pixels\n", waveSize, code.size(), CheckPixels()); + } + Require(compile(ExpandedExecPixelCode).barycentricEmulation.active, "a partial interpolation written before WQM incorrectly used host interpolation"); + std::printf("wave%u: expanded EXEC keeps barycentric emulation\n", waveSize); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto wave : {32u, 64u}) Run(*device, wave); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 1eb0dd3650..a7419f9c3a 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -245,14 +245,23 @@ bool runsAllStartingLanes(const IrProgram& program, const IrBlock* target) { return false; } -bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint32_t index, std::uint32_t depth = 0) { +enum class LaneCoverage { Real, Quads }; + +bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint32_t index, LaneCoverage coverage, std::uint32_t depth = 0) { if (depth > 8u) return false; if (word.Opcode() == IrOpcode::BitwiseAnd32) { - return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) && holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + return holdsStartingLanes(program, resolveArg(word, 0), index, coverage, depth + 1u) && holdsStartingLanes(program, resolveArg(word, 1), index, coverage, depth + 1u); } if (word.Opcode() == IrOpcode::BitwiseOr32) { - return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) || holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + return holdsStartingLanes(program, resolveArg(word, 0), index, coverage, depth + 1u) || holdsStartingLanes(program, resolveArg(word, 1), index, coverage, depth + 1u); } + if (word.Opcode() == IrOpcode::CompositeExtractU64 && isImmediateU32(resolveArg(word, 1), index)) { + const auto& mask = resolveArg(word, 0); + if (mask.Opcode() != IrOpcode::WqmU64) return false; + const auto& lanes = resolveArg(mask, 0); + return lanes.Opcode() == IrOpcode::CompositeConstructU64 && holdsStartingLanes(program, resolveArg(lanes, index), index, LaneCoverage::Real, depth + 1u); + } + if (coverage == LaneCoverage::Quads) return false; if (word.Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediateU32(resolveArg(word, 1), index)) return false; const auto& ballot = resolveArg(word, 0); if (ballot.Opcode() != IrOpcode::Ballot || !runsAllStartingLanes(program, ballot.Parent())) return false; @@ -291,13 +300,8 @@ bool foldStartingQuadBitTest(const IrProgram& program, IrBuilder& builder, IrVal } else if (program.WaveSize() != 32u) { return false; } - if (low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediateU32(resolveArg(*low, 1), 0u)) return false; - const auto& wqm = resolveArg(*low, 0); - if (high != nullptr && (high->Opcode() != IrOpcode::CompositeExtractU64 || &resolveArg(*high, 0) != &wqm || !isImmediateU32(resolveArg(*high, 1), 1u))) return false; - if (wqm.Opcode() != IrOpcode::WqmU64) return false; - const auto& lanes = resolveArg(wqm, 0); - if (lanes.Opcode() != IrOpcode::CompositeConstructU64 || !holdsStartingLanes(program, resolveArg(lanes, 0), 0u)) return false; - if (high != nullptr && !holdsStartingLanes(program, resolveArg(lanes, 1), 1u)) return false; + if (!holdsStartingLanes(program, *low, 0u, LaneCoverage::Quads)) return false; + if (high != nullptr && !holdsStartingLanes(program, *high, 1u, LaneCoverage::Quads)) return false; replaceWith(inst, builder.ConstantBool(true)); return true; } From 4895ab1a4cd803c60013bccfba1e51478aad356e Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 21 Oct 2026 07:37:15 -0500 Subject: [PATCH 0364/1360] test(libSceUsbd): cover sceUsbdFillControlSetup buffer formatting --- core/libs/tests/GuestUsbd.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 59e59542b0..358e369eea 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -50,6 +50,7 @@ void APS5_VABI sceUsbdFillInterruptTransfer(void* transfer, void* deviceHandle, std::int32_t APS5_VABI sceUsbdCancelTransfer(void* transfer); std::int32_t APS5_VABI sceUsbdEventHandlingOk(); void APS5_VABI sceUsbdFreeConfigDescriptor(void* config); +void APS5_VABI sceUsbdFillControlSetup(std::uint8_t* buffer, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint16_t length); } namespace { @@ -293,6 +294,13 @@ int main(int argc, char** argv) { sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); sceUsbdFreeConfigDescriptor(nullptr); + Require(Throws([] { sceUsbdFillControlSetup(nullptr, 0x80, 6, 0x0100, 0, 18); }), "null control setup buffer accepted"); + std::uint8_t setupBuffer[8] = {}; + sceUsbdFillControlSetup(setupBuffer, 0x80, 6, 0x0100, 0x0409, 18); + Require(setupBuffer[0] == 0x80 && setupBuffer[1] == 6, "request type or request not filled"); + Require(setupBuffer[2] == 0x00 && setupBuffer[3] == 0x01, "value not filled little-endian"); + Require(setupBuffer[4] == 0x09 && setupBuffer[5] == 0x04, "index not filled little-endian"); + Require(setupBuffer[6] == 18 && setupBuffer[7] == 0, "length not filled little-endian"); if (argc == 2 && std::strcmp(argv[1], "device") == 0) Device(); sceUsbdExit(); std::puts("USBD tests passed"); From 9abca4b9f91183b28d4d112981276ce8b0aa9090 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:58:10 +0200 Subject: [PATCH 0365/1360] docs(debt): say what the socketpair poll note still lacks after the peer closes On Windows a pair end now reports POLLIN after the peer's shutdown or close. The console also reports POLLHUP after close; the loopback connection cannot tell a close from a shutdown. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 69dfd47c9d..abeca9682c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -183,7 +183,7 @@ - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; `RLIMIT_NPROC` always throws; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443, as FreeBSD 11 would if the console's `maxproc` were 1604: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` lowers an `RLIMIT_NPROC` limit above `maxprocperuid` to it ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)); 1604 × 9 / 10 gives 1443. Neither value is measured: `kern.maxproc` fails with `ENOENT` in the payload process. Here that call throws, because the limit is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. -- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` +- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported - Guest sockets on macOS ([the equeue wait, kevent, fstat and ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp)): macOS has no `ppoll`, so the wait uses `poll` with the timeout rounded up to a millisecond; it has no `POLLRDHUP`, so a stream that the peer closed is found by peeking one byte when it is readable; `FIONREAD` on a datagram socket, and the byte count that `fstat` and `kevent` report for it, is `SO_NREAD`, the size of the next datagram, because macOS adds the source address to `FIONREAD`; and `gethostbyname` replaces `gethostbyname_r`, which macOS lacks. - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws From 6d464e94951e9f18ae33d8627d1ad6ac3be1f872 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:50:12 +0200 Subject: [PATCH 0366/1360] test(recompiler): give the shader cache source fixture room under load shader_cache_runtime_sources configures a CMake fixture several times with a 15 s limit per configure and 180 s for the whole test. On a loaded machine a configure takes longer than that: under a parallel build (load average ~12 on WSL2 Ubuntu) the test timed out in 2 of 2 runs, and on an idle Windows machine it needs about 115 s of the 180 s. Raise the limits to 120 s per configure, 60 s per version query and 600 s for the test. Co-Authored-By: Claude Sonnet 5.5 --- CMakeLists.txt | 2 +- tools/tests/test_shader_cache_runtime_sources.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eaa66c8db..7dd610b9f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,7 +135,7 @@ if(BUILD_TESTING) add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND}) - set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180) + set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 600) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index 0c462bb7ea..cae5368f02 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -70,7 +70,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource def configure(self, flags=''): result = subprocess.run([CMAKE, '-S', str(self.root), '-B', str(self.build), '-G', 'Ninja', f'-DCMAKE_CXX_FLAGS={flags}'], - capture_output=True, encoding='utf-8', errors='replace', timeout=15) + capture_output=True, encoding='utf-8', errors='replace', timeout=120) if result.returncode: raise AssertionError(result.stdout + result.stderr) if (self.root / 'compiler-invoked').exists(): @@ -93,7 +93,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource f'-DRECOMPILER_DIR={self.recompiler.as_posix()}', f'-DLIST_FILE={self.listing.as_posix()}', f'-DOUTPUT={output.as_posix()}', f'-DEXTRA={self.extra}', '-P', str(self.recompiler / 'ShaderCacheVersion.cmake')], - capture_output=True, encoding='utf-8', errors='replace', timeout=10) + capture_output=True, encoding='utf-8', errors='replace', timeout=60) if not success: if result.returncode == 0 or output.exists(): raise AssertionError('The generator accepted an unkeyed dependency') @@ -138,7 +138,7 @@ class ShaderCacheRuntimeSourceTests(unittest.TestCase): 'if(NOT result EQUAL 99)', 'message(FATAL_ERROR "Unexpected compiler arguments were accepted")', 'endif()'] script.write_text('\n'.join(lines) + '\n', encoding='utf-8') result = subprocess.run([CMAKE, '-P', str(script)], capture_output=True, - encoding='utf-8', errors='replace', timeout=10) + encoding='utf-8', errors='replace', timeout=60) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(len(marker.read_text(encoding='utf-8').splitlines()), len(rejected)) From b7794963150cbd894234aa2d1ad22133f3c60318 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 09:56:48 -0300 Subject: [PATCH 0367/1360] fix(libkernel): match event flag attributes and error results to PS5 Measured on firmware 13.60: create accepts attribute bits 0x1, 0x2, 0x10, 0x20 and 0x100 and rejects 0x1|0x2; wait and poll ignore mode bits outside the match and clear fields; EPERM, ECANCELED and EACCES write 0 to the result pattern. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/EventFlag/src/EventFlag.cpp | 21 +++++++--- core/libs/tests/GuestKernelErrors.cpp | 39 +++++++++++++++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index a6403ce990..e9d51f67c1 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -27,8 +27,11 @@ static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; static constexpr int SCE_KERNEL_ERROR_ECANCELED = 0x80020055; +static constexpr uint32_t EVF_ATTR_TH_FIFO = 0x01; +static constexpr uint32_t EVF_ATTR_TH_PRIO = 0x02; static constexpr uint32_t EVF_ATTR_SINGLE = 0x10; static constexpr uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr uint32_t EVF_ATTR_UNKNOWN_0x100 = 0x100; static constexpr uint32_t EVF_WAITMODE_AND = 0x01; static constexpr uint32_t EVF_WAITMODE_OR = 0x02; static constexpr uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; @@ -110,7 +113,6 @@ static bool TraceSync() { static bool IsValidWaitMode(uint32_t waitMode) { const uint32_t match = waitMode & (EVF_WAITMODE_AND | EVF_WAITMODE_OR); const uint32_t clear = waitMode & (EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT); - if ((waitMode & ~(EVF_WAITMODE_AND | EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT)) != 0) return false; return (match == EVF_WAITMODE_AND || match == EVF_WAITMODE_OR) && clear != (EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT); } @@ -130,7 +132,9 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui (void)param; if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; - if ((attr & ~0x33u) != 0) return SCE_KERNEL_ERROR_EINVAL; + if ((attr & EVF_ATTR_TH_FIFO) && (attr & EVF_ATTR_TH_PRIO)) return SCE_KERNEL_ERROR_EINVAL; + if ((attr & ~(EVF_ATTR_TH_FIFO | EVF_ATTR_TH_PRIO | EVF_ATTR_SINGLE | EVF_ATTR_MULTI | EVF_ATTR_UNKNOWN_0x100)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (attr & EVF_ATTR_UNKNOWN_0x100) NotImplemented_nid_no_patch("sceKernelCreateEventFlag attribute 0x100"); std::shared_ptr flag; try { flag = std::make_shared(); @@ -226,7 +230,10 @@ int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; std::lock_guard lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) return SCE_KERNEL_ERROR_EPERM; + if (!ef->_multipleWaiters && ef->_waiters != 0) { + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EPERM; + } if (!IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { if (result_pat) *result_pat = ef->_pattern; return SCE_KERNEL_ERROR_EBUSY; @@ -242,7 +249,10 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; std::unique_lock lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) return SCE_KERNEL_ERROR_EPERM; + if (!ef->_multipleWaiters && ef->_waiters != 0) { + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EPERM; + } if (IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { Consume(ef, bit_pattern, wait_mode, result_pat); return SCE_OK; @@ -275,10 +285,11 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u } KernelTraceWait_nid_postfix("evf", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); if (ef->_deleted) { + if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EACCES; } if (ef->_cancelGeneration != generation) { - if (result_pat) *result_pat = ef->_pattern; + if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_ECANCELED; } if (timedOut && !waiter.satisfied) { diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 1932625356..61a1d7ae0c 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -198,6 +198,13 @@ int main() { Require(sceKernelDeleteEventFlag(ef) == SCE_OK); Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_SINGLE | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_MULTI | 0x1000u, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + for (const std::uint32_t attr : {0x03u, 0x13u, 0x23u, 0x103u, 0x130u, 0x04u, 0x08u, 0x40u, 0x80u, 0x200u, 0x80000000u, 0xffffffffu}) { + Require(sceKernelCreateEventFlag(&ef, "test_bad_attr", attr, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + } + for (const std::uint32_t attr : {0x02u, 0x11u, 0x12u, 0x21u, 0x22u}) { + Require(sceKernelCreateEventFlag(&ef, "test_attr", attr, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + } Require(sceKernelDeleteEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelSetEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelClearEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); @@ -224,6 +231,16 @@ int main() { Require(sceKernelPollEventFlag(ef, 1, 0, &pattern) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_KERNEL_ERROR_EINVAL); + for (const std::uint32_t mode : {0x04u, 0x40u, 0x100u, 0x80000000u, 0x103u}) { + pattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(ef, 1, mode, &pattern) == SCE_KERNEL_ERROR_EINVAL && pattern == 0xdeadbeef); + } + for (const std::uint32_t mode : {EVF_WAITMODE_OR | 0x04u, EVF_WAITMODE_OR | 0x08u, EVF_WAITMODE_AND | 0x40u, EVF_WAITMODE_AND | 0x100u, EVF_WAITMODE_OR | 0x80000000u, + EVF_WAITMODE_OR | 0xffffff00u}) { + Require(sceKernelPollEventFlag(ef, 0x10, mode, &pattern) == SCE_OK && pattern == 0x15); + } + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_PAT | 0x100u, &pattern) == SCE_OK && pattern == 0x15); + Require(sceKernelSetEventFlag(ef, 0x10) == SCE_OK); Require(sceKernelPollEventFlag(ef, 0x20, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); Require(pattern == 0x15); @@ -261,7 +278,22 @@ int main() { Require(sceKernelDeleteEventFlag(ef) == SCE_OK); - Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0, nullptr) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0x04, nullptr) == SCE_OK); + std::atomic singleRes{-1}; + std::uint64_t singlePat = 0xdeadbeef; + std::thread singleWaiter([&] { + singleRes = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, &singlePat, nullptr); + }); + WaitForEventFlagWaiters(ef, 1); + pattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(ef, 0x04, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EPERM && pattern == 0); + pattern = 0xdeadbeef; + Require(sceKernelWaitEventFlag(ef, 0x04, EVF_WAITMODE_AND, &pattern, nullptr) == SCE_KERNEL_ERROR_EPERM && pattern == 0); + waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x08, &waitThreads) == SCE_OK && waitThreads == 1); + singleWaiter.join(); + Require(singleRes == SCE_KERNEL_ERROR_ECANCELED && singlePat == 0); + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_AND, &pattern) == SCE_OK && pattern == 0x08); Require(sceKernelDeleteEventFlag(ef) == SCE_OK); Require(sceKernelCreateEventFlag(&ef, "imm_sat", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); @@ -323,8 +355,9 @@ int main() { Require(sceKernelCreateEventFlag(&ef, "del_waiters", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); std::atomic delRes1{-1}; std::atomic delRes2{-1}; + std::uint64_t delPat = 0xdeadbeef; std::thread delT1([&] { - delRes1 = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, nullptr, nullptr); + delRes1 = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, &delPat, nullptr); }); std::thread delT2([&] { delRes2 = sceKernelWaitEventFlag(ef, 0x02, EVF_WAITMODE_AND, nullptr, nullptr); @@ -333,7 +366,7 @@ int main() { Require(sceKernelDeleteEventFlag(ef) == SCE_OK); delT1.join(); delT2.join(); - Require(delRes1 == SCE_KERNEL_ERROR_EACCES); + Require(delRes1 == SCE_KERNEL_ERROR_EACCES && delPat == 0); Require(delRes2 == SCE_KERNEL_ERROR_EACCES); for (int iter = 0; iter < 50; ++iter) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..a8dc3a9fc1 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,7 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware +- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - attribute bit 0x100 is accepted by firmware 13.60 alongside 0x1, 0x2, 0x10 and 0x20, but what it changes is unknown, so a flag created with it calls `NotImplemented_nid_no_patch`. - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked From 3e6c6bd71f167ca87e663139b05f5a7a54db931d Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 09:57:32 -0300 Subject: [PATCH 0368/1360] test(libkernel): cover cancelling two event flag waiters Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestKernelErrors.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 61a1d7ae0c..efb2403c29 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -296,6 +296,27 @@ int main() { Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_AND, &pattern) == SCE_OK && pattern == 0x08); Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "multi_cancel", EVF_ATTR_MULTI, 0x01, nullptr) == SCE_OK); + std::atomic cancelRes1{-1}; + std::atomic cancelRes2{-1}; + std::uint64_t cancelPat1 = 0xdeadbeef; + std::uint64_t cancelPat2 = 0xdeadbeef; + std::thread cancelT1([&] { + cancelRes1 = sceKernelWaitEventFlag(ef, 0x02, EVF_WAITMODE_AND, &cancelPat1, nullptr); + }); + std::thread cancelT2([&] { + cancelRes2 = sceKernelWaitEventFlag(ef, 0x04, EVF_WAITMODE_OR, &cancelPat2, nullptr); + }); + WaitForEventFlagWaiters(ef, 2); + waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x05, &waitThreads) == SCE_OK && waitThreads == 2); + cancelT1.join(); + cancelT2.join(); + Require(cancelRes1 == SCE_KERNEL_ERROR_ECANCELED && cancelPat1 == 0); + Require(cancelRes2 == SCE_KERNEL_ERROR_ECANCELED && cancelPat2 == 0); + Require(sceKernelPollEventFlag(ef, 0x05, EVF_WAITMODE_AND, &pattern) == SCE_OK && pattern == 0x05); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "imm_sat", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); pattern = 0; Require(sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_PAT, &pattern, nullptr) == SCE_OK); From e7ea4903255e217bc08d64d3db8b61fc706d6bc3 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 00:20:39 +0000 Subject: [PATCH 0369/1360] feat(agc): draw point lists on the vertex path with their point size A point list routed through the vertex path was refused, and so was PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE on it. OliSe Player (PPSA01153) stops on the latter with two of its MilkDrop presets. The vertex path now draws point lists as Vulkan points. The vertex stage writes PointSize: the misc export's X clamped to PA_SU_POINT_MINMAX when USE_VTX_POINT_SIZE is set, and the PA_SU_POINT_SIZE height otherwise. The point state travels in the recompile request (version 31) and the cache and draw keys. Non-square register sizes, inverted clamps, point sprite texture coordinates and, without largePoints, sizes other than one pixel throw. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/Draw.cpp | 3 +- .../Execution/src/Driver/Draw/DrawLookup.cpp | 5 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 5 +- .../Execution/src/VulkanDevice.cpp | 4 + .../Graphics/include/Context.hpp | 1 + .../Graphics/include/State.hpp | 5 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 1 + .../Graphics/src/ShaderValidation.cpp | 7 + .../libSceAgcDriver/Graphics/src/State.cpp | 46 ++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 71 +++++++- .../tests/execution/VertexPointSize.cpp | 157 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 5 + .../ControlFlow/src/RequestSerializer.cpp | 15 ++ .../Optimization/ShaderStageInputInfo.hpp | 4 + .../Optimization/src/ShaderInfoCollector.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 4 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 9 +- .../src/ShaderInputInfoBuilder.cpp | 4 + .../tests/RequestSerializerTests.cpp | 38 ++++- docs/dev/debt/libSceAgcDriver.md | 3 +- 22 files changed, 375 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index dd0231145e..88d1e170b1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -250,9 +250,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); - if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; if (vertexInfos[i]) { + vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode; + Graphics::ApplyPointSize(graphics, *vertexInfos[i]); } }; if (!registerKey) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 6bfa9b1fb4..5cb9982751 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include namespace AgcDriver::DriverDetail { @@ -51,6 +52,10 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(vertex.pointSize)); + mix(std::bit_cast(vertex.pointSizeMin)); + mix(std::bit_cast(vertex.pointSizeMax)); for (std::uint32_t r = 0; r < vertex.resourcesNum; ++r) { for (const auto field : vertex.resources[r].fields) mix(field); const auto& destination = vertex.resourcesDst[r]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index dd66e54ebf..27de50b2bd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -213,7 +213,7 @@ void Driver::traceDrawKeyLookup(const std::array& components, } bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) { - if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; + if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl || a.pointList != b.pointList || a.pointSize != b.pointSize || a.pointSizeMin != b.pointSizeMin || a.pointSizeMax != b.pointSizeMax) return false; if (a.depthRemapMode != b.depthRemapMode) return false; for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) { if (a.resources[i].fields != b.resources[i].fields) return false; 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 b69bccd8fe..cd890ae617 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1435,7 +1435,10 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr}); continue; } - if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; + if (vertex) { + vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; + Graphics::ApplyPointSize(decoded.state, *vertex); + } ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}}; std::vector key; ShaderRecompiler::BuildPreparedShaderKey(request, key); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 286da2ec1f..77c0d421a9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -314,6 +314,7 @@ struct VulkanDevice::State { bool fragmentShaderBarycentric = false; bool shaderTerminateInvocation = false; bool geometryShader = false; + bool largePoints = false; bool sampleRateShading = false; bool storageImageMultisample = false; bool shaderClipDistance = false; @@ -1702,6 +1703,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.sampleRateShading = available.sampleRateShading; enabled.geometryShader = available.geometryShader; state->geometryShader = enabled.geometryShader == VK_TRUE; + enabled.largePoints = available.largePoints; + state->largePoints = enabled.largePoints == VK_TRUE; state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; if (enabled.sampleRateShading) state->capabilities.push_back(spv::CapabilityInterpolationFunction); if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); @@ -3829,6 +3832,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageTableUpdateAfterBind = ImageTableUpdateAfterBind(context); context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context); context.geometryShader = state->geometryShader; + context.largePoints = state->largePoints; context.sampleRateShading = state->sampleRateShading; context.fillModeNonSolid = state->fillModeNonSolid; context.maintenance5 = state->maintenance5; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index afaa005b1d..bcf3fa3ab3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -237,6 +237,7 @@ struct Context { bool imageTableUpdateAfterBind = false; bool samplerTableUpdateAfterBind = false; bool geometryShader = false; + bool largePoints = false; bool sampleRateShading = false; bool fillModeNonSolid = false; bool maintenance5 = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index f8b9d16afe..399fd2c1f8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -153,6 +153,10 @@ struct State { bool customSampleLocations = false; bool sampleLocationGrid = false; std::uint32_t paClVsOutCntl = 0; + bool vertexPoints = false; + float pointSize = 1.0f; + float pointSizeMin = 0.0f; + float pointSizeMax = 0.0f; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; bool pixelEarlyZ = false; }; @@ -162,6 +166,7 @@ struct State { inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u); ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); +void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); void ApplyDepthRemap(const Context& context, State& state); void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 8e0ecd6ea6..0b280b7532 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -666,6 +666,7 @@ bool ValidationKey(const Context& context, std::span shade add(context.viewportIndexLayer); add(context.shaderTerminateInvocation); add(state.paClVsOutCntl); + add(state.vertexPoints); add(state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 20ce2e4e8b..789bee596e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -341,6 +341,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + Require(!state.vertexPoints || context.largePoints || (state.pointSize == 1.0f && ((state.paClVsOutCntl & (1u << 16u)) == 0 || (state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f))), "points of a size other than one pixel require the largePoints device feature"); Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index f9912af9f9..2033a2af61 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -53,6 +53,8 @@ struct Module { bool layerOutput = false; bool meshCapability = false; std::uint32_t primitiveInputs = 0; + bool pointSizeOutput = false; + bool pointSize = false; std::map> modes; const std::vector& Type(std::uint32_t id) const { @@ -149,6 +151,10 @@ struct Module { Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex || value == spv::BuiltInBaseInstance) && signature == "i32", "unsupported vertex built-in input"); } else if (vertex && storage == spv::StorageClassOutput) { if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return; + if (value == spv::BuiltInPointSize && pointSizeOutput && signature == "f32" && !pointSize) { + pointSize = true; + return; + } Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { @@ -186,6 +192,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(words[4] == 0, "unsupported SPIR-V schema: " + std::to_string(words[4])); Module module; + module.pointSizeOutput = state.vertexPoints && stage == Stage::Vertex; std::uint32_t entries = 0; std::uint32_t memoryModels = 0; std::uint32_t entryPoint = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 507c286b67..8c59d6f89b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -190,6 +190,32 @@ std::uint32_t inactiveVertexPointSize(const QueueState& queue) { } } +bool vertexPointList(const QueueState& queue) { + const auto stages = find(queue.context, 0x2d5); + const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig); + return stages != queue.context.end() && primitive != queue.userConfig.end() && (stages->second & 0x24u) == 0 && primitive->second == 1u; +} + +void decodePointSize(const QueueState& queue, State& result) { + const auto& cx = queue.context; + const auto size = read(cx, 0x280); + const auto limits = read(cx, 0x281); + const bool fromVertex = (result.paClVsOutCntl & (1u << 16u)) != 0; + Require(!fromVertex || (result.paClVsOutCntl & (1u << 21u)) != 0, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported"); + Require(fromVertex || (size & 0xffffu) == size >> 16u, "points whose PA_SU_POINT_SIZE width and height differ are unsupported"); + Require((limits & 0xffffu) <= limits >> 16u, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported"); + result.pointSize = static_cast(size & 0xffffu) / 8.0f; + result.pointSizeMin = static_cast(limits & 0xffffu) / 8.0f; + result.pointSizeMax = static_cast(limits >> 16u) / 8.0f; + const auto interpolation = find(cx, 0x1b5); + const auto inputs = find(cx, 0x1b6); + if (interpolation == cx.end() || (interpolation->second & 2u) == 0 || inputs == cx.end()) return; + for (std::uint32_t input = 0; input < (inputs->second & 0x3fu); ++input) { + const auto control = find(cx, 0x191 + input); + Require(control == cx.end() || (control->second & 0x820000u) == 0, "point sprite texture coordinates (SPI_PS_INPUT_CNTL PT_SPRITE_TEX) are not implemented"); + } +} + constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. @@ -1044,6 +1070,15 @@ std::array PixelSampleLocations(const State& state, const Shad return locations; } +void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex) { + if (!state.vertexPoints) return; + vertex.paClVsOutCntl |= state.paClVsOutCntl & (1u << 16u); + vertex.pointList = true; + vertex.pointSize = state.pointSize; + vertex.pointSizeMin = state.pointSizeMin; + vertex.pointSizeMax = state.pointSizeMax; +} + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { @@ -1218,7 +1253,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast(result.stages.path), result.stages.vertexWaveSize); switch (primitive) { - case 1: Require(result.stages.mesh.has_value(), "point-list vertex rendering requires point-size output support"); result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break; + case 1: + Require(result.stages.mesh.has_value() || result.stages.path == ShaderPath::Vertex, "point lists are only drawn through the vertex and geometry paths"); + result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + result.vertexPoints = result.stages.path == ShaderPath::Vertex; + break; case 2: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break; case 3: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; break; case 7: @@ -1243,9 +1282,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } APS5_LOG_OUT_DEBUG("Topology=%u", static_cast(result.topology)); result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && !result.quadList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; - zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); + zero(cx, 0x207, ~(LayerExports | ClipCullExports | (result.vertexPoints ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); result.paClVsOutCntl = read(cx, 0x207); + if (result.vertexPoints) decodePointSize(queue, result); result.samples = decodeSamples(cx); Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"); decodeSampleLocations(cx, result); @@ -2006,7 +2046,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6 && (primitive & 0x3fu) != 21) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } - if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | (vertexPointList(queue) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 52f66dab74..d2cbe7f9b7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -162,9 +162,15 @@ void inactiveVertexPointSizeTests() { } throw std::runtime_error("potentially active point size passed the state decoder"); }; - for (const auto primitive : {1u, 9u}) { + { auto queue = pointSizeState(); - queue.userConfig[0x242] = primitive; + queue.userConfig[0x242] = 9; + rejected(queue); + } + { + auto queue = pointSizeState(); + queue.userConfig[0x242] = 1; + queue.context[0x2d5] = 0x2020; rejected(queue); } for (const auto stages : {0x2004u, 0x2020u}) { @@ -221,6 +227,66 @@ void PolygonPrimitiveTests() { Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN && indexedState.primitiveRestart, "indexed polygon decoding did not keep primitive restart"); } +void vertexPointListTests() { + const auto pointState = [](std::uint32_t control, std::uint32_t size, std::uint32_t limits) { + auto queue = makeState(); + queue.userConfig[0x242] = 1; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = control; + queue.context[0x280] = size; + queue.context[0x281] = limits; + return queue; + }; + { + const auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "a vertex-path point list with a vertex point size failed the draw precheck: " + rejection); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.paClVsOutCntl == 0x01210000u, "a vertex-path point list with a vertex point size did not decode as a point list"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a vertex-path point list is not rasterized as points"); + Require(state.vertexPoints && state.pointSize == 1.0f && state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f, "PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX of 8 did not decode as one-pixel points"); + ShaderRecompiler::ShaderVertexStageInfo vertex{}; + vertex.paClVsOutCntl = 1u << 21u; + AgcDriver::Graphics::ApplyPointSize(state, vertex); + Require(vertex.pointList && vertex.paClVsOutCntl == ((1u << 16u) | (1u << 21u)) && vertex.pointSize == 1.0f && vertex.pointSizeMin == 1.0f && vertex.pointSizeMax == 1.0f, "the vertex stage did not receive the vertex point size"); + } + { + const auto state = AgcDriver::Graphics::DecodeState(pointState(0, 0x00200020u, 0x00100004u)); + Require(state.vertexPoints && state.pointSize == 4.0f && state.pointSizeMin == 0.5f && state.pointSizeMax == 2.0f, "a register point size did not decode as diameters"); + ShaderRecompiler::ShaderVertexStageInfo vertex{}; + vertex.paClVsOutCntl = 1u << 21u; + AgcDriver::Graphics::ApplyPointSize(state, vertex); + Require(vertex.pointList && vertex.paClVsOutCntl == (1u << 21u) && vertex.pointSize == 4.0f, "a register point size turned on the vertex point size"); + } + { + const auto state = AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0x00200010u, 0x00400000u)); + Require(state.pointSizeMin == 0.0f && state.pointSizeMax == 8.0f, "a vertex point size refused a register size whose width and height differ"); + } + { + auto queue = makeState(); + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = 0x01210000u; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.vertexPoints, "a triangle draw decoded as vertex points"); + ShaderRecompiler::ShaderVertexStageInfo vertex{}; + AgcDriver::Graphics::ApplyPointSize(state, vertex); + Require(!vertex.pointList && vertex.paClVsOutCntl == 0, "a triangle draw received a point size"); + } + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00200010u, 0x00080008u)); }, "points whose PA_SU_POINT_SIZE width and height differ are unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00080008u, 0x00080010u)); }, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(1u << 16u, 0x00080008u, 0x00080008u)); }, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported"); + for (const auto sprite : {1u << 17u, 1u << 23u}) { + auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u); + queue.context[0x1b5] = 0x486bu; + queue.context[0x1b6] = 2; + queue.context[0x191] = 0; + queue.context[0x192] = sprite; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "point sprite texture coordinates (SPI_PS_INPUT_CNTL PT_SPRITE_TEX) are not implemented"); + queue.context[0x1b5] = 0x4869u; + Require(AgcDriver::Graphics::DecodeState(queue).vertexPoints, "PT_SPRITE_TEX was refused with point sprites off"); + } +} + void retainedPixelStageTests() { using namespace AgcDriver::Graphics; auto queue = makeState(); @@ -11104,6 +11170,7 @@ int main(int argc, char** argv) { NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); PolygonPrimitiveTests(); + vertexPointListTests(); TuningFieldTests(); PixelInputLayoutTests(); UnexportedPixelInputTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp new file mode 100644 index 0000000000..03302a8f6f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp @@ -0,0 +1,157 @@ +#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 +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +constexpr std::uint32_t MiscVector = 1u << 21u; +constexpr std::uint32_t VertexPointSize = 1u << 16u; +alignas(256) std::array pixels{}; +alignas(256) std::array vertices{}; +constexpr std::array positionCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +constexpr std::array pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; + +std::vector SizeExportCode(float size) { + std::vector code(positionCode.begin(), positionCode.begin() + 8); + code.insert(code.end(), {0x7e0802ffu, std::bit_cast(size), 0xf80000cfu, 0x03020100u, 0xf80008d1u, 0x00000004u, 0xbf810000u}); + return code; +} + +struct PointSize { + float size = 1.0f; + float minimum = 0.0f; + float maximum = 0.0f; + bool fromVertex = false; +}; + +struct Square { + unsigned pixels = 0; + std::uint32_t left = Extent, top = Extent, right = 0, bottom = 0; +}; + +std::vector RunCase(AgcDriver::VulkanDevice& device, const char* name, std::span vertexCode, const PointSize& points, float offset) { + const std::array, 4> centres{{{16.0f, 16.0f}, {48.0f, 16.0f}, {16.0f, 48.0f}, {48.0f, 48.0f}}}; + vertices.fill(std::byte{0}); + constexpr std::uint32_t stride = 16u; + for (std::size_t index = 0; index < centres.size(); ++index) { + const std::array position{(centres[index][0] + offset) / Extent * 2.0f - 1.0f, (centres[index][1] + offset) / Extent * 2.0f - 1.0f, 0.0f, 1.0f}; + std::memcpy(vertices.data() + index * stride, position.data(), sizeof(position)); + } + std::array descriptor{}; + const auto tableAddress = reinterpret_cast(descriptor.data()); + std::array userData{static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u}; + const std::array memory{{{tableAddress, std::as_bytes(std::span(descriptor))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.fetchEmbedded = true; + vertexInfo.resourcesNum = 1u; + vertexInfo.resourcesDst[0] = {0, 4, 0, 0}; + vertexInfo.paClVsOutCntl = points.fromVertex ? MiscVector | VertexPointSize : 0u; + vertexInfo.pointList = true; + vertexInfo.pointSize = points.size; + vertexInfo.pointSizeMin = points.minimum; + vertexInfo.pointSizeMax = points.maximum; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}}; + const auto address = reinterpret_cast(vertices.data()); + descriptor = {static_cast(address), static_cast(address >> 32u) | (stride << 16u), static_cast(centres.size()), 0x01000facu | (77u << 12u)}; + request.context.vertex->resources[0].fields = descriptor; + const auto vertex = ShaderRecompiler::Recompile(request); + const auto pushBytes = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + pixelInfo.rasterPrimitive = ShaderRecompiler::RasterPrimitive::Point; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + state.paClVsOutCntl = vertexInfo.paClVsOutCntl; + state.vertexPoints = true; + state.pointSize = points.size; + state.pointSizeMin = points.minimum; + state.pointSizeMax = points.maximum; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, static_cast(centres.size()), 0u, 1u, 0u, false}; + draw.firstInstance = 0u; + pixels.fill(std::byte{0}); + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::vector squares(centres.size()); + for (std::uint32_t y = 0; y < Extent; ++y) { + for (std::uint32_t x = 0; x < Extent; ++x) { + const auto index = (y * Extent + x) * 4u; + if (pixels[index] == std::byte{0} && pixels[index + 3u] == std::byte{0}) continue; + auto& square = squares[(y >= Extent / 2u ? 2u : 0u) + (x >= Extent / 2u ? 1u : 0u)]; + ++square.pixels; + square.left = std::min(square.left, x); + square.top = std::min(square.top, y); + square.right = std::max(square.right, x + 1u); + square.bottom = std::max(square.bottom, y + 1u); + } + } + std::cout << "case " << name << ":"; + for (const auto& square : squares) std::cout << " " << square.pixels << " [" << square.left << "," << square.top << ")-(" << square.right << "," << square.bottom << ")"; + std::cout << "\n"; + return squares; +} + +void RequireSquares(const std::vector& squares, std::uint32_t side, const char* name) { + for (const auto& square : squares) { + Require(square.pixels == side * side && square.right - square.left == side && square.bottom - square.top == side, std::string(name) + ": a point is not a " + std::to_string(side) + "x" + std::to_string(side) + " square"); + } +} + +void Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + RequireSquares(RunCase(device, "register size 1", positionCode, {1.0f, 1.0f, 1.0f, false}, 0.5f), 1u, "register size 1"); + RequireSquares(RunCase(device, "register size 4", positionCode, {4.0f, 1.0f, 1.0f, false}, 0.0f), 4u, "register size 4"); + RequireSquares(RunCase(device, "register size 3 ignores the clamp", positionCode, {3.0f, 6.0f, 8.0f, false}, 0.5f), 3u, "register size 3 ignores the clamp"); + const auto two = SizeExportCode(2.0f); + RequireSquares(RunCase(device, "vertex size 2 within [1, 8]", two, {1.0f, 1.0f, 8.0f, true}, 0.0f), 2u, "vertex size 2 within [1, 8]"); + const auto six = SizeExportCode(6.0f); + RequireSquares(RunCase(device, "vertex size 6 clamped to 4", six, {1.0f, 0.5f, 4.0f, true}, 0.0f), 4u, "vertex size 6 clamped to 4"); + const auto one = SizeExportCode(1.0f); + RequireSquares(RunCase(device, "vertex size 1 clamped to 5", one, {1.0f, 5.0f, 8.0f, true}, 0.5f), 5u, "vertex size 1 clamped to 5"); + RequireSquares(RunCase(device, "vertex size over a register size", six, {1.0f, 1.0f, 8.0f, true}, 0.0f), 6u, "vertex size over a register size"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index d95621f9a1..cf6d2f2fe5 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -4,6 +4,7 @@ #include "Recompiler.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "prx/libc/include/HostThreadLocal.hpp" +#include #include #include #include @@ -233,6 +234,10 @@ private: append(key, value.fetchEmbedded); append(key, value.paClVsOutCntl); append(key, value.depthRemapMode); + append(key, value.pointList); + append(key, std::bit_cast(value.pointSize)); + append(key, std::bit_cast(value.pointSizeMin)); + append(key, std::bit_cast(value.pointSizeMax)); if (value.resourcesNum > value.resources.size()) throw std::runtime_error("Shader cache: invalid vertex resource count"); append(key, value.resourcesNum); for (std::uint32_t i = 0; i < value.resourcesNum; ++i) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 11a9d92195..33000ec8d1 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -1,4 +1,5 @@ #include "ControlFlow/RequestSerializer.hpp" +#include #include namespace ShaderRecompiler { @@ -863,6 +864,12 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const } writer.WriteBool(request.target.float32FlushToZero); writer.WriteBool(request.target.float32DenormPreserve); + if (request.context.vertex.has_value()) { + writer.WriteBool(request.context.vertex->pointList); + writer.WriteU32(std::bit_cast(request.context.vertex->pointSize)); + writer.WriteU32(std::bit_cast(request.context.vertex->pointSizeMin)); + writer.WriteU32(std::bit_cast(request.context.vertex->pointSizeMax)); + } return base64Encode(buffer); } @@ -917,6 +924,14 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); + if (version >= 33u && result.request.context.vertex.has_value()) { + auto& vertex = *result.request.context.vertex; + vertex.pointList = reader.ReadBool(); + vertex.pointSize = std::bit_cast(reader.ReadU32()); + vertex.pointSizeMin = std::bit_cast(reader.ReadU32()); + vertex.pointSizeMax = std::bit_cast(reader.ReadU32()); + result.vertex = vertex; + } if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 84034d4306..3a9286f578 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -141,6 +141,10 @@ struct ShaderVertexInputInfo { std::uint32_t scratchSizeDwords = 0; std::uint32_t hostSubgroupSize = 64; std::uint32_t paClVsOutCntl = 0; + bool pointList = false; + float pointSize = 1.0f; + float pointSizeMin = 0.0f; + float pointSizeMax = 0.0f; ShaderClipSpaceTransform clipSpace; ShaderDepthRemap depthRemap; ShaderMeshInputInfo mesh; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 135e1cd331..c5ebc46690 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -588,6 +588,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + if (program.Resources().stage == IrShaderStage::Vertex && inputInfo.vertex != nullptr && inputInfo.vertex->pointList) AddOutput(next, StageOutputKind::PointSize, 0, 0, "gl_PointSize"); 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 127ed88d5b..6eda873842 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -214,6 +214,10 @@ struct ShaderVertexStageInfo { bool fetchEmbedded; std::uint32_t paClVsOutCntl = 0; std::uint32_t depthRemapMode = 0; + bool pointList = false; + float pointSize = 1.0f; + float pointSizeMin = 0.0f; + float pointSizeMax = 0.0f; }; struct ShaderFloatMode { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 4884f65f29..f7b8e9697b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -9,6 +9,7 @@ #include "PipelineSpecialization.hpp" #include #include +#include #include #include @@ -611,7 +612,12 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const const auto f32 = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); if (output.pointSize) { - state.module.AddFunction(spv::OpStore, state.pointSizeVariable, f32); + auto size = f32; + if (state.inputInfo.vertex->pointList) { + size = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), size, GlslStd450(state), GLSLstd450FClamp, f32, ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMin)), ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMax))); + } + state.module.AddFunction(spv::OpStore, state.pointSizeVariable, size); continue; } const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) { @@ -1556,6 +1562,7 @@ void EmitProgram(SpirvEmitterState& state) { } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); EmitBdaAnchors(state); + if (state.pointSizeVariable != 0u && state.inputInfo.vertex != nullptr && state.inputInfo.vertex->pointList) state.module.AddFunction(spv::OpStore, state.pointSizeVariable, ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSize))); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index ecffac9de7..aecd8393c1 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -190,6 +190,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.fetchEmbedded = vertex.fetchEmbedded; vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; vertexStorage.depthRemap = {vertex.depthRemapMode}; + vertexStorage.pointList = vertex.pointList; + vertexStorage.pointSize = vertex.pointSize; + vertexStorage.pointSizeMin = vertex.pointSizeMin; + vertexStorage.pointSizeMax = vertex.pointSizeMax; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 2696063735..96060e5b6f 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -57,6 +57,11 @@ void InsertPixelDefaultsAfterFrontFace(std::vector& bytes, bool sa bytes.insert(bytes.begin() + 282u, sampleOrder ? 51u : 35u, 0u); } +void AppendVertexTail(std::vector& bytes) { + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + bytes.insert(bytes.end(), {0u, 0u, 0u, 0x80u, 0x3fu, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}); +} + std::vector DecodeBase64(std::string_view text) { constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::vector bytes; @@ -219,7 +224,7 @@ int main() { if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + AppendVertexTail(upgraded); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -259,7 +264,7 @@ int main() { layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + AppendVertexTail(layered); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -284,7 +289,7 @@ int main() { InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + AppendVertexTail(bytes); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -413,8 +418,8 @@ int main() { upgraded[4] = 42u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); - main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + AppendVertexTail(main38); + AppendVertexTail(upgraded); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -437,7 +442,7 @@ int main() { InsertPixelDefaultsAfterFrontFace(main25); } if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + AppendVertexTail(main25); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -526,6 +531,27 @@ int main() { const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); Require(replay35.request.target.spannedWaves == spanned && !replay35.request.target.float32FlushToZero); } + ShaderRecompiler::RecompileRequest pointRequest{}; + pointRequest.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + pointRequest.context.vertex->paClVsOutCntl = 0x01210000u; + pointRequest.context.vertex->pointList = true; + pointRequest.context.vertex->pointSize = 4.0f; + pointRequest.context.vertex->pointSizeMin = 0.5f; + pointRequest.context.vertex->pointSizeMax = 8.0f; + const auto pointEncoded = serializer.Serialize(pointRequest); + const auto pointReplay = serializer.Deserialize(pointEncoded); + Require(pointReplay.request.context.vertex.has_value() && pointReplay.request.context.vertex->paClVsOutCntl == 0x01210000u && pointReplay.request.context.vertex->pointList); + Require(pointReplay.request.context.vertex->pointSize == 4.0f && pointReplay.request.context.vertex->pointSizeMin == 0.5f && pointReplay.request.context.vertex->pointSizeMax == 8.0f); + Require(serializer.Serialize(pointReplay.request) == pointEncoded); + auto pointBytes = DecodeBase64(pointEncoded); + pointBytes.pop_back(); + bool pointRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(pointBytes))); + } catch (const std::runtime_error& error) { + pointRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(pointRejected); return 0; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 52e16ee07a..39c5a314dd 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -16,7 +16,8 @@ - [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured. -- [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. +- [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Patches and absent routing or rasterization registers still throw; point lists on that routing use it (next entry). Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. +- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X component clamped to `PA_SU_POINT_MINMAX` when `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is set, and the `PA_SU_POINT_SIZE` height otherwise, which the export replaces when it runs. Both registers are read as half sizes in 12.4 fixed point, so a field of 8 is a one-pixel point, as [radeonsi programs them](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064). That the clamp applies to the exported size and not to the register size is inferred from radeonsi, which sets both clamps to the register size when the size is not per vertex; neither is measured on console hardware. Sizes outside the device's `pointSizeRange` are clamped by the device. A register size whose width and height differ, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite texture coordinates (`SPI_PS_INPUT_CNTL` PT_SPRITE_TEX or PT_SPRITE_TEX_ATTR1 with `SPI_INTERP_CONTROL_0.PNT_SPRITE_ENA`) throw, and points of any size other than one pixel need the `largePoints` device feature. OliSe Player (PPSA01153) draws such points with `PA_CL_VS_OUT_CNTL = 0x01210000` for its Che - Escape and Stahlregen + Geiss + Martin - Ouboros (Metal Finish 2) presets; Che - Escape sets both size registers to `0x00080008`. - [RELEASE_MEM of the system counter](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - a `RELEASE_MEM` with `DATA_SEL = 4` throws: the frequency and epoch of the console's 64-bit system counter are not known. [amdgpu's GFX10 packet header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/nvd.h) names the value "send 64bit sys counter value", beside 3, the GPU counter, which the driver writes as a 100 MHz count of the host's steady clock. An older KFD header names 4 a CP performance counter instead. A PS4 title through #6977 writes it at the end of each frame. - [Point and line polygon modes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - front and back polygon types that differ while both faces are drawn throw, as Vulkan has one `polygonMode` per draw. Points are drawn only one pixel wide (`PA_SU_POINT_SIZE = 0x00080008` inside `PA_SU_POINT_MINMAX`, the `VK_KHR_maintenance5` size of a point whose shader does not write `PointSize`); other sizes, a per-vertex size and devices without `maintenance5` throw. Lines must be one pixel wide (`PA_SU_LINE_CNTL`) and use Vulkan's default line rasterization. Depth bias with a point or line polygon mode, and such a mode on a rectangle list, throw. The fields follow the [AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf); the console's rasterization of these points and lines is not measured. - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. From f3433b0a11325e755b83ef9258b111ba84ef03e3 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 00:41:18 +0000 Subject: [PATCH 0370/1360] fix(agc): keep the written point size above zero Vulkan needs PointSize above zero. A PA_SU_POINT_SIZE of 0 without USE_VTX_POINT_SIZE, and a PA_SU_POINT_MINMAX maximum of 0 with it, now throw. With USE_VTX_POINT_SIZE the clamp's lower bound is at least the smallest positive float, so an exported size of 0 or less, which a clamp minimum of 0 lets through, is written as a positive value. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/State.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 7 +++++++ .../tests/execution/VertexPointSize.cpp | 8 ++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 15 +++++++++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8c59d6f89b..8da8aec557 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -203,7 +203,9 @@ void decodePointSize(const QueueState& queue, State& result) { const bool fromVertex = (result.paClVsOutCntl & (1u << 16u)) != 0; Require(!fromVertex || (result.paClVsOutCntl & (1u << 21u)) != 0, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported"); Require(fromVertex || (size & 0xffffu) == size >> 16u, "points whose PA_SU_POINT_SIZE width and height differ are unsupported"); + Require(fromVertex || (size & 0xffffu) != 0, "points of size 0 (PA_SU_POINT_SIZE 0) are unsupported"); Require((limits & 0xffffu) <= limits >> 16u, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported"); + Require(!fromVertex || (limits >> 16u) != 0, "a vertex point size clamped to 0 (PA_SU_POINT_MINMAX maximum 0) is unsupported"); result.pointSize = static_cast(size & 0xffffu) / 8.0f; result.pointSizeMin = static_cast(limits & 0xffffu) / 8.0f; result.pointSizeMax = static_cast(limits >> 16u) / 8.0f; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d2cbe7f9b7..db77650bc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -275,6 +275,13 @@ void vertexPointListTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00200010u, 0x00080008u)); }, "points whose PA_SU_POINT_SIZE width and height differ are unsupported"); expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00080008u, 0x00080010u)); }, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported"); expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(1u << 16u, 0x00080008u, 0x00080008u)); }, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0, 0x00080008u)); }, "points of size 0 (PA_SU_POINT_SIZE 0) are unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0x00080008u, 0)); }, "a vertex point size clamped to 0 (PA_SU_POINT_MINMAX maximum 0) is unsupported"); + { + const auto state = AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0, 0x00200000u)); + Require(state.pointSize == 0.0f && state.pointSizeMin == 0.0f && state.pointSizeMax == 4.0f, "a vertex point size refused a zero register size or a zero clamp minimum"); + static_cast(AgcDriver::Graphics::DecodeState(pointState(0, 0x00080008u, 0))); + } for (const auto sprite : {1u << 17u, 1u << 23u}) { auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u); queue.context[0x1b5] = 0x486bu; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp index 03302a8f6f..253201b452 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp @@ -128,6 +128,10 @@ void RequireSquares(const std::vector& squares, std::uint32_t side, cons } } +void RequireAtMostOnePixel(const std::vector& squares, const char* name) { + for (const auto& square : squares) Require(square.pixels <= 1u, std::string(name) + ": a point with no positive size covers more than one pixel"); +} + void Run(AgcDriver::VulkanDevice& device) { std::cout << "device: " << device.DeviceName() << "\n"; RequireSquares(RunCase(device, "register size 1", positionCode, {1.0f, 1.0f, 1.0f, false}, 0.5f), 1u, "register size 1"); @@ -140,6 +144,10 @@ void Run(AgcDriver::VulkanDevice& device) { const auto one = SizeExportCode(1.0f); RequireSquares(RunCase(device, "vertex size 1 clamped to 5", one, {1.0f, 5.0f, 8.0f, true}, 0.5f), 5u, "vertex size 1 clamped to 5"); RequireSquares(RunCase(device, "vertex size over a register size", six, {1.0f, 1.0f, 8.0f, true}, 0.0f), 6u, "vertex size over a register size"); + const auto zero = SizeExportCode(0.0f); + RequireAtMostOnePixel(RunCase(device, "vertex size 0 within [0, 4]", zero, {0.0f, 0.0f, 4.0f, true}, 0.5f), "vertex size 0 within [0, 4]"); + const auto negative = SizeExportCode(-3.0f); + RequireAtMostOnePixel(RunCase(device, "vertex size -3 within [0, 4]", negative, {1.0f, 0.0f, 4.0f, true}, 0.5f), "vertex size -3 within [0, 4]"); } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f7b8e9697b..a76bb5f199 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -580,6 +580,10 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const return vector; } +float PointSizeFloor(const ShaderVertexInputInfo& vertex) { + return std::max(vertex.pointSizeMin, std::numeric_limits::min()); +} + void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const ExportInfo& exp) { auto& state = ctx.state; if (state.inputInfo.vertex == nullptr) { @@ -615,7 +619,7 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const auto size = f32; if (state.inputInfo.vertex->pointList) { size = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), size, GlslStd450(state), GLSLstd450FClamp, f32, ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMin)), ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMax))); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), size, GlslStd450(state), GLSLstd450FClamp, f32, ConstantF32(state, std::bit_cast(PointSizeFloor(*state.inputInfo.vertex))), ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMax))); } state.module.AddFunction(spv::OpStore, state.pointSizeVariable, size); continue; @@ -1562,7 +1566,14 @@ void EmitProgram(SpirvEmitterState& state) { } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); EmitBdaAnchors(state); - if (state.pointSizeVariable != 0u && state.inputInfo.vertex != nullptr && state.inputInfo.vertex->pointList) state.module.AddFunction(spv::OpStore, state.pointSizeVariable, ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSize))); + if (state.pointSizeVariable != 0u && state.inputInfo.vertex != nullptr && state.inputInfo.vertex->pointList) { + const auto& vertex = *state.inputInfo.vertex; + const bool fromVertex = (vertex.paClVsOutCntl & (1u << 16u)) != 0u; + if (!fromVertex && !(vertex.pointSize > 0.0f)) FailEmit("a point list without a vertex point size needs a positive PA_SU_POINT_SIZE"); + if (fromVertex && !(vertex.pointSizeMax > 0.0f)) FailEmit("a vertex point size needs a positive PA_SU_POINT_MINMAX maximum"); + const auto size = fromVertex ? std::clamp(vertex.pointSize, PointSizeFloor(vertex), vertex.pointSizeMax) : vertex.pointSize; + state.module.AddFunction(spv::OpStore, state.pointSizeVariable, ConstantF32(state, std::bit_cast(size))); + } if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 39c5a314dd..61794b6d33 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -17,7 +17,7 @@ - [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured. - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Patches and absent routing or rasterization registers still throw; point lists on that routing use it (next entry). Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. -- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X component clamped to `PA_SU_POINT_MINMAX` when `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is set, and the `PA_SU_POINT_SIZE` height otherwise, which the export replaces when it runs. Both registers are read as half sizes in 12.4 fixed point, so a field of 8 is a one-pixel point, as [radeonsi programs them](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064). That the clamp applies to the exported size and not to the register size is inferred from radeonsi, which sets both clamps to the register size when the size is not per vertex; neither is measured on console hardware. Sizes outside the device's `pointSizeRange` are clamped by the device. A register size whose width and height differ, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite texture coordinates (`SPI_PS_INPUT_CNTL` PT_SPRITE_TEX or PT_SPRITE_TEX_ATTR1 with `SPI_INTERP_CONTROL_0.PNT_SPRITE_ENA`) throw, and points of any size other than one pixel need the `largePoints` device feature. OliSe Player (PPSA01153) draws such points with `PA_CL_VS_OUT_CNTL = 0x01210000` for its Che - Escape and Stahlregen + Geiss + Martin - Ouboros (Metal Finish 2) presets; Che - Escape sets both size registers to `0x00080008`. +- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X component clamped to `PA_SU_POINT_MINMAX` when `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is set, and the `PA_SU_POINT_SIZE` height otherwise, which the export replaces when it runs. Both registers are read as half sizes in 12.4 fixed point, so a field of 8 is a one-pixel point, as [radeonsi programs them](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064). That the clamp applies to the exported size and not to the register size is inferred from radeonsi, which sets both clamps to the register size when the size is not per vertex; neither is measured on console hardware. Sizes outside the device's `pointSizeRange` are clamped by the device. Vulkan needs a `PointSize` above zero, so the clamp's lower bound is at least the smallest positive float: an exported size of 0 or less, which a clamp minimum of 0 lets through (radeonsi programs that minimum for multisampled and sprite points), draws as the device's smallest point (one pixel on NVIDIA), while on the console it probably covers nothing (inferred). A register size of 0, a clamp maximum of 0 with USE_VTX_POINT_SIZE, a register size whose width and height differ, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite texture coordinates (`SPI_PS_INPUT_CNTL` PT_SPRITE_TEX or PT_SPRITE_TEX_ATTR1 with `SPI_INTERP_CONTROL_0.PNT_SPRITE_ENA`) throw, and points of any size other than one pixel need the `largePoints` device feature. OliSe Player (PPSA01153) draws such points with `PA_CL_VS_OUT_CNTL = 0x01210000` for its Che - Escape and Stahlregen + Geiss + Martin - Ouboros (Metal Finish 2) presets; Che - Escape sets both size registers to `0x00080008`. - [RELEASE_MEM of the system counter](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - a `RELEASE_MEM` with `DATA_SEL = 4` throws: the frequency and epoch of the console's 64-bit system counter are not known. [amdgpu's GFX10 packet header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/nvd.h) names the value "send 64bit sys counter value", beside 3, the GPU counter, which the driver writes as a 100 MHz count of the host's steady clock. An older KFD header names 4 a CP performance counter instead. A PS4 title through #6977 writes it at the end of each frame. - [Point and line polygon modes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - front and back polygon types that differ while both faces are drawn throw, as Vulkan has one `polygonMode` per draw. Points are drawn only one pixel wide (`PA_SU_POINT_SIZE = 0x00080008` inside `PA_SU_POINT_MINMAX`, the `VK_KHR_maintenance5` size of a point whose shader does not write `PointSize`); other sizes, a per-vertex size and devices without `maintenance5` throw. Lines must be one pixel wide (`PA_SU_LINE_CNTL`) and use Vulkan's default line rasterization. Depth bias with a point or line polygon mode, and such a mode on a rectangle list, throw. The fields follow the [AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf); the console's rasterization of these points and lines is not measured. - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. From 11bc7b16866a3dd551accfae8c5e12e027cd9642 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 10:06:03 +0000 Subject: [PATCH 0371/1360] fix(shader): move the vertex point fields to request version 43 main took request versions 33 to 42 for other fields, the last of them the pixel sample order, so requests of those versions from main have no point-list tail. The point fields now come with version 43, after them, and the tests expect 43 and reject 44. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 13 +++++++------ .../ControlFlow/src/RequestSerializer.cpp | 6 +++--- .../recompiler/tests/RequestSerializerTests.cpp | 16 ++++++++-------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a754f42271..9087424e31 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4849,7 +4849,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSoAAAA=", "new requests did not use serialization version 42"); + require(requestPrefix(encoded, 8u) == "NVNQQSsAAAA=", "new requests did not use serialization version 43"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4967,7 +4967,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSsAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSwAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5259,12 +5259,13 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 7, remappedBytes.begin()), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags of a vertex request"); + constexpr std::size_t pointTailBytes = 13u; + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 7u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 7 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - pointTailBytes, plainBytes.end(), remappedBytes.end() - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags and the point tail of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; - legacyBytes.resize(legacyBytes.size() - 6u); + legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str()); require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); @@ -5272,10 +5273,10 @@ void verifyDepthRemapRequests() { auto version35 = remappedBytes; version35.pop_back(); version35[4] = 35u; - version35.pop_back(); + version35.resize(version35.size() - 1u - pointTailBytes); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); require(replay35.request.target.depthRangeRemap && replay35.request.context.vertex->depthRemapMode == 2u && !replay35.request.target.float32FlushToZero && serializer.Serialize(replay35.request) == remapped, "a version 35 request misread the depth remap fields or read an f32 flush flag"); - auto truncated = remappedBytes; + auto truncated = version35; truncated.resize(truncated.size() - 2u); expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap mode was accepted"); auto trailing = remappedBytes; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 33000ec8d1..cf3880a037 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -832,7 +832,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(42u); + writer.WriteU32(43u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -878,7 +878,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 > 42u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 43u) 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); @@ -924,7 +924,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); - if (version >= 33u && result.request.context.vertex.has_value()) { + if (version >= 43u && result.request.context.vertex.has_value()) { auto& vertex = *result.request.context.vertex; vertex.pointList = reader.ReadBool(); vertex.pointSize = std::bit_cast(reader.ReadU32()); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 96060e5b6f..425183cf80 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -219,7 +219,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 42u; + upgraded[4] = 43u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -259,7 +259,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 42u; + layered[4] = 43u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -285,7 +285,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 42u; + bytes[4] = 43u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -415,10 +415,10 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); auto upgraded = main38; - upgraded[4] = 42u; + upgraded[4] = 43u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); - AppendVertexTail(main38); + main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); AppendVertexTail(upgraded); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); @@ -435,7 +435,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 42u; + main25[4] = 43u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -458,14 +458,14 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 42u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 43u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.pop_back(); version40[4] = 40u; const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 43u; + future[4] = 44u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); From 891bb20161a799d68b16e420da9bcb2a3daa4e8b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 11:52:50 +0000 Subject: [PATCH 0372/1360] fix(agc): journal a vertex point list and its point sizes Pipeline creation and SPIR-V validation read State::vertexPoints and the three point sizes, so the pipeline journal's recipe codec and canonical state now carry them, the State layout guard names its new size and the journal format becomes 6 (main took 5 for the recipe codec change of #5656). Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/PipelineJournal.cpp | 14 +++++++++++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index d0669ed238..1347ef9127 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 800, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); @@ -219,6 +219,10 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.mergedCoverage); for (const auto mask : state.quadSampleMask) writer.Raw(mask); writer.U32(state.depthRemap.mode); + writer.Bool(state.vertexPoints); + writer.Raw(state.pointSize); + writer.Raw(state.pointSizeMin); + writer.Raw(state.pointSizeMax); } void decodeState(Reader& reader, State& state) { @@ -280,6 +284,10 @@ void decodeState(Reader& reader, State& state) { state.mergedCoverage = reader.Bool(); for (auto& mask : state.quadSampleMask) mask = reader.Raw(); state.depthRemap.mode = reader.U32(); + state.vertexPoints = reader.Bool(); + state.pointSize = reader.Raw(); + state.pointSizeMin = reader.Raw(); + state.pointSizeMax = reader.Raw(); } void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { @@ -1053,6 +1061,10 @@ State CanonicalPipelineState(const State& state) { canonical.mergedCoverage = state.mergedCoverage; canonical.quadSampleMask = state.quadSampleMask; canonical.depthRemap.mode = state.depthRemap.mode; + canonical.vertexPoints = state.vertexPoints; + canonical.pointSize = state.pointSize; + canonical.pointSizeMin = state.pointSizeMin; + canonical.pointSizeMax = state.pointSizeMax; return canonical; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index db77650bc4..c9bb0717d7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -10696,6 +10696,14 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { GraphicsPipelineRecipe lineDecoded; Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(lineState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), lineDecoded), "the line strip mesh recipe does not decode"); Require(lineDecoded.state.stages.mesh && lineDecoded.state.stages.mesh->outputPrimitiveSize == 2u && lineDecoded.state.stages.mesh->esgsItemSize == 4u, "the recipe dropped the mesh output primitive size"); + auto pointState = state; + pointState.vertexPoints = true; + pointState.pointSize = 4.0f; + pointState.pointSizeMin = 0.5f; + pointState.pointSizeMax = 8.0f; + GraphicsPipelineRecipe pointDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(pointState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), pointDecoded), "the point list recipe does not decode"); + Require(pointDecoded.state.vertexPoints && pointDecoded.state.pointSize == 4.0f && pointDecoded.state.pointSizeMin == 0.5f && pointDecoded.state.pointSizeMax == 8.0f, "the recipe dropped the vertex point list or its point sizes"); auto fourSampleState = state; fourSampleState.samples = 4; auto maskState = fourSampleState; From 89be76cb4cde128ca1390689596e1c5e43675f12 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 15:31:44 +0000 Subject: [PATCH 0373/1360] test(shader): check version 39, 41, 42 and 43 requests with merged hull and point state A tessellated vertex request with the merged hull flag and a point list serializes as version 43 with both. Without its point tail it reads as main's version 42, without the pixel sample order as version 41, and without the f32 denormal preserve flag as version 39, each keeping the merged hull flag and taking the default point state. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 5 +++- .../tests/RequestSerializerTests.cpp | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9087424e31..797326cd71 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5317,9 +5317,12 @@ void verifyMergedHullConfiguration() { const auto replay = serializer.Deserialize(merged); require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization"); require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); + constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); + require(legacy.size() > 53u + pointTailBytes && legacy[4] == 43u, "a merged hull request did not use serialization version 43"); + legacy.resize(legacy.size() - pointTailBytes); legacy.pop_back(); - require(legacy.size() > 52u && legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); + require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) { auto older = legacy; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 425183cf80..9ff48f6bcb 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -427,6 +427,35 @@ int main() { Require(replay38.request.graphics->draw.indexAddress == 0xabcdef3400u && replay38.request.graphics->draw.indexCount == 57u && replay38.request.graphics->draw.indexElementBytes == 4u && replay38.request.graphics->draw.instanceCount == 9u); Require(replay38.request.target.float16DenormPreserve == preserve); Require(serializer.Serialize(replay38.request) == EncodeBase64(upgraded)); + if (fixture.tessellation) { + for (const bool merged : {false, true}) { + auto pointed = request; + pointed.graphics->tessellation->mergedHull = merged; + pointed.context.vertex->pointList = true; + pointed.context.vertex->pointSize = 3.0f; + pointed.context.vertex->pointSizeMin = 1.0f; + pointed.context.vertex->pointSizeMax = 8.0f; + auto version43 = DecodeBase64(serializer.Serialize(pointed)); + Require(version43.size() > 14u && version43[4] == 43u); + const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); + Require(replay43.request.graphics->tessellation->mergedHull == merged && replay43.request.context.vertex->pointList && replay43.request.context.vertex->pointSize == 3.0f && replay43.request.context.vertex->pointSizeMin == 1.0f && replay43.request.context.vertex->pointSizeMax == 8.0f); + auto version42 = version43; + version42.resize(version42.size() - 13u); + version42[4] = 42u; + const auto replay42 = serializer.Deserialize(EncodeBase64(version42)); + Require(replay42.request.graphics->tessellation->mergedHull == merged && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f && replay42.request.context.vertex->pointSizeMin == 0.0f && replay42.request.context.vertex->pointSizeMax == 0.0f); + auto version41 = version42; + if (fixture.pixel) version41.erase(version41.begin() + 317u, version41.begin() + 333u); + version41[4] = 41u; + const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); + Require(replay41.request.graphics->tessellation->mergedHull == merged && !replay41.request.context.vertex->pointList && replay41.request.context.vertex->pointSize == 1.0f && replay41.request.context.vertex->pointSizeMin == 0.0f && replay41.request.context.vertex->pointSizeMax == 0.0f); + auto version39 = version41; + version39.pop_back(); + version39[4] = 39u; + const auto replay39 = serializer.Deserialize(EncodeBase64(version39)); + Require(replay39.request.graphics->tessellation->mergedHull == merged && !replay39.request.context.vertex->pointList && replay39.request.context.vertex->pointSize == 1.0f && replay39.request.context.vertex->pointSizeMin == 0.0f && replay39.request.context.vertex->pointSizeMax == 0.0f); + } + } for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; auto main25 = bytes; From f71ead79e96a1000ee7542c79a14a3eb197b40fd Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 15:41:56 +0000 Subject: [PATCH 0374/1360] docs(agc): put the vertex point list entry's gaps first and shorten it The entry was twice the 1000-character limit, led with the behaviour rather than the gaps, and named a title. It now lists what throws and what is inferred first, and names no title. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 61794b6d33..986eefd76c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -17,7 +17,7 @@ - [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured. - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Patches and absent routing or rasterization registers still throw; point lists on that routing use it (next entry). Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. -- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X component clamped to `PA_SU_POINT_MINMAX` when `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is set, and the `PA_SU_POINT_SIZE` height otherwise, which the export replaces when it runs. Both registers are read as half sizes in 12.4 fixed point, so a field of 8 is a one-pixel point, as [radeonsi programs them](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064). That the clamp applies to the exported size and not to the register size is inferred from radeonsi, which sets both clamps to the register size when the size is not per vertex; neither is measured on console hardware. Sizes outside the device's `pointSizeRange` are clamped by the device. Vulkan needs a `PointSize` above zero, so the clamp's lower bound is at least the smallest positive float: an exported size of 0 or less, which a clamp minimum of 0 lets through (radeonsi programs that minimum for multisampled and sprite points), draws as the device's smallest point (one pixel on NVIDIA), while on the console it probably covers nothing (inferred). A register size of 0, a clamp maximum of 0 with USE_VTX_POINT_SIZE, a register size whose width and height differ, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite texture coordinates (`SPI_PS_INPUT_CNTL` PT_SPRITE_TEX or PT_SPRITE_TEX_ATTR1 with `SPI_INTERP_CONTROL_0.PNT_SPRITE_ENA`) throw, and points of any size other than one pixel need the `largePoints` device feature. OliSe Player (PPSA01153) draws such points with `PA_CL_VS_OUT_CNTL = 0x01210000` for its Che - Escape and Stahlregen + Geiss + Martin - Ouboros (Metal Finish 2) presets; Che - Escape sets both size registers to `0x00080008`. +- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - throws for a register size of 0, a clamp maximum of 0 with `USE_VTX_POINT_SIZE`, unequal register width and height, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite coordinates; sizes above one pixel need `largePoints`. Inferred, not measured: that `PA_SU_POINT_MINMAX` clamps the exported size and not the register size ([radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064)), and that an exported size of 0 or less covers nothing (here: the smallest point). A point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X, clamped, under `USE_VTX_POINT_SIZE`, else the `PA_SU_POINT_SIZE` height (12.4 fixed-point half sizes). - [RELEASE_MEM of the system counter](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - a `RELEASE_MEM` with `DATA_SEL = 4` throws: the frequency and epoch of the console's 64-bit system counter are not known. [amdgpu's GFX10 packet header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/nvd.h) names the value "send 64bit sys counter value", beside 3, the GPU counter, which the driver writes as a 100 MHz count of the host's steady clock. An older KFD header names 4 a CP performance counter instead. A PS4 title through #6977 writes it at the end of each frame. - [Point and line polygon modes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - front and back polygon types that differ while both faces are drawn throw, as Vulkan has one `polygonMode` per draw. Points are drawn only one pixel wide (`PA_SU_POINT_SIZE = 0x00080008` inside `PA_SU_POINT_MINMAX`, the `VK_KHR_maintenance5` size of a point whose shader does not write `PointSize`); other sizes, a per-vertex size and devices without `maintenance5` throw. Lines must be one pixel wide (`PA_SU_LINE_CNTL`) and use Vulkan's default line rasterization. Depth bias with a point or line polygon mode, and such a mode on a rectangle list, throw. The fields follow the [AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf); the console's rasterization of these points and lines is not measured. - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. From e751dabd7b7260711d862a8aa2e113de83009299 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 21:47:43 -0400 Subject: [PATCH 0375/1360] feat(agc): rasterize geometry point outputs as mesh points --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 15 +++++++++++++++ .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 16 +++++++++------- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c9bb0717d7..7315c36c0e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1149,6 +1149,15 @@ void ShaderStageTests() { Require(mesh.outputPrimitiveSize == 1 && mesh.maxPrimitives == 255 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a point-list subgroup did not decode as points"); Require(mesh.provokingVertex == 0, "a point draw has provoking vertex 0"); lines.context[0x205] = raster; + auto points = lines; + points.userConfig[0x242] = 6; + points.userConfig[0x25b] = 0x5c2e; + points.context[0x1ff] = 0x8a; + points.context[0x2ce] = 3; + points.context[0x29b] = 0; + const auto pointState = AgcDriver::Graphics::DecodeState(points); + const auto& pointMesh = *pointState.stages.mesh; + Require(pointMesh.outputPrimitiveSize == 1u && pointMesh.primitivesPerGroup == 44u && pointMesh.verticesPerGroup == 46u && pointMesh.maxVertices == 138u && pointMesh.maxPrimitives == 138u && pointMesh.threadsPerGroup == 192u && AgcDriver::Graphics::RasterPrimitiveOf(pointState) == ShaderRecompiler::RasterPrimitive::Point, "triangle-strip input with point output did not decode the reported subgroup"); lines.context[0x29b] = 1; lines.context[0x2ce] = 1; expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index a3c2a13217..0a370358d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -395,6 +395,21 @@ int main() { } ClearPixels(); + auto pointSubgroup = SmallSubgroup; + pointSubgroup.outputPrimitiveSize = 1u; + for (const auto pointWave : {32u, 64u}) { + ClearPixels(); + DrawMesh(device, {pointSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, pointWave}); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const auto& position = Ordered[3u * triangle].position; + const auto x = static_cast((position[0] + 1.0f) * 0.5f * Width); + const auto y = static_cast((1.0f - position[1]) * 0.5f * Height); + const auto offset = (static_cast(y) * Width + x) * 4u; + Require(PixelIs(offset, TriangleColor(triangle)), "mesh point missing at first exported index for primitive " + std::to_string(triangle)); + Require(PixelIs(CellPixel(triangle), Background), "mesh point output filled the interior of its packed triangle"); + } + } + auto lineSubgroup = FastLaunchSubgroup; lineSubgroup.maxPrimitives = 2u; lineSubgroup.outputPrimitiveSize = 2u; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 797326cd71..a7d80f6b93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4466,13 +4466,15 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(other, key); require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration"); require(mesh.outputPrimitiveSize == 3u && replay.request.graphics->mesh->outputPrimitiveSize == 3u, "a mesh configuration does not output triangles by default"); - otherMesh = mesh; - otherMesh.outputPrimitiveSize = 2u; - other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; - const auto lineReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(other)); - require(lineReplay.request.graphics.has_value() && lineReplay.request.graphics->mesh.has_value() && lineReplay.request.graphics->mesh->outputPrimitiveSize == 2u, "the mesh output primitive size was lost in serialization"); - RecompileCacheKey::Build(other, key); - require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh output primitive size"); + for (const auto primitiveSize : {1u, 2u}) { + otherMesh = mesh; + otherMesh.outputPrimitiveSize = primitiveSize; + other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; + const auto primitiveReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(other)); + require(primitiveReplay.request.graphics.has_value() && primitiveReplay.request.graphics->mesh.has_value() && primitiveReplay.request.graphics->mesh->outputPrimitiveSize == primitiveSize, "the mesh output primitive size was lost in serialization"); + RecompileCacheKey::Build(other, key); + require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh output primitive size"); + } RecompileRequest hull{}; hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; hull.context.waveSize = 64; From 4f76eb75ce1b25242470282a1b98b5401c37c026 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 16 Oct 2026 08:28:29 -0400 Subject: [PATCH 0376/1360] feat(agc): apply register and exported sizes to mesh points --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 +- .../Graphics/src/ShaderValidation.cpp | 6 ++- .../libSceAgcDriver/Graphics/src/State.cpp | 15 +++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++ core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 50 ++++++++++++++++++- .../Optimization/src/ShaderInfoCollector.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 18 +++++-- .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/debt/libSceAgcDriver.md | 2 +- 9 files changed, 94 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 789bee596e..d8406cebeb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -341,7 +341,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); - Require(!state.vertexPoints || context.largePoints || (state.pointSize == 1.0f && ((state.paClVsOutCntl & (1u << 16u)) == 0 || (state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f))), "points of a size other than one pixel require the largePoints device feature"); + Require(!(state.vertexPoints || (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u)) || context.largePoints || (state.pointSize == 1.0f && ((state.paClVsOutCntl & (1u << 16u)) == 0 || (state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f))), "points of a size other than one pixel require the largePoints device feature"); Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 2033a2af61..abfe99cf36 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -140,6 +140,10 @@ struct Module { Require(input && (clipInput || (value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in"); return; } + if (!input && value == spv::BuiltInPointSize && pointSizeOutput && signature == "f32" && !pointSize) { + pointSize = true; + return; + } if (!input && value == spv::BuiltInPrimitivePointIndicesEXT) primitiveIndices = 1u; if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u; if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = 3u; @@ -192,7 +196,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(words[4] == 0, "unsupported SPIR-V schema: " + std::to_string(words[4])); Module module; - module.pointSizeOutput = state.vertexPoints && stage == Stage::Vertex; + module.pointSizeOutput = (state.vertexPoints && stage == Stage::Vertex) || (mesh && state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u); std::uint32_t entries = 0; std::uint32_t memoryModels = 0; std::uint32_t entryPoint = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8da8aec557..0e3edf77b1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -190,10 +190,13 @@ std::uint32_t inactiveVertexPointSize(const QueueState& queue) { } } -bool vertexPointList(const QueueState& queue) { +bool pointOutput(const QueueState& queue) { const auto stages = find(queue.context, 0x2d5); const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig); - return stages != queue.context.end() && primitive != queue.userConfig.end() && (stages->second & 0x24u) == 0 && primitive->second == 1u; + if (stages == queue.context.end()) return false; + if ((stages->second & 0x24u) == 0) return primitive != queue.userConfig.end() && primitive->second == 1u; + const auto output = find(queue.context, 0x29b); + return (stages->second & 0x24u) == 0x20u && output != queue.context.end() && output->second == 0u; } void decodePointSize(const QueueState& queue, State& result) { @@ -1073,7 +1076,7 @@ std::array PixelSampleLocations(const State& state, const Shad } void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex) { - if (!state.vertexPoints) return; + if (!state.vertexPoints && !(state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u)) return; vertex.paClVsOutCntl |= state.paClVsOutCntl & (1u << 16u); vertex.pointList = true; vertex.pointSize = state.pointSize; @@ -1284,10 +1287,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } APS5_LOG_OUT_DEBUG("Topology=%u", static_cast(result.topology)); result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && !result.quadList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; - zero(cx, 0x207, ~(LayerExports | ClipCullExports | (result.vertexPoints ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); + zero(cx, 0x207, ~(LayerExports | ClipCullExports | ((result.vertexPoints || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 1u)) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); result.paClVsOutCntl = read(cx, 0x207); - if (result.vertexPoints) decodePointSize(queue, result); + if (result.vertexPoints || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 1u)) decodePointSize(queue, result); result.samples = decodeSamples(cx); Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"); decodeSampleLocations(cx, result); @@ -2048,7 +2051,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6 && (primitive & 0x3fu) != 21) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } - if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | (vertexPointList(queue) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | (pointOutput(queue) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7315c36c0e..ff6d85226a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1155,8 +1155,13 @@ void ShaderStageTests() { points.context[0x1ff] = 0x8a; points.context[0x2ce] = 3; points.context[0x29b] = 0; + points.context[0x280] = 0x00180018u; + points.context[0x281] = 0x00400008u; const auto pointState = AgcDriver::Graphics::DecodeState(points); const auto& pointMesh = *pointState.stages.mesh; + ShaderRecompiler::ShaderVertexStageInfo pointInfo{}; + AgcDriver::Graphics::ApplyPointSize(pointState, pointInfo); + Require(pointInfo.pointList && pointInfo.pointSize == 3.0f && pointInfo.pointSizeMin == 1.0f && pointInfo.pointSizeMax == 8.0f, "geometry points lost their register sizes"); Require(pointMesh.outputPrimitiveSize == 1u && pointMesh.primitivesPerGroup == 44u && pointMesh.verticesPerGroup == 46u && pointMesh.maxVertices == 138u && pointMesh.maxPrimitives == 138u && pointMesh.threadsPerGroup == 192u && AgcDriver::Graphics::RasterPrimitiveOf(pointState) == ShaderRecompiler::RasterPrimitive::Point, "triangle-strip input with point output did not decode the reported subgroup"); lines.context[0x29b] = 1; lines.context[0x2ce] = 1; @@ -10713,6 +10718,14 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { GraphicsPipelineRecipe pointDecoded; Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(pointState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), pointDecoded), "the point list recipe does not decode"); Require(pointDecoded.state.vertexPoints && pointDecoded.state.pointSize == 4.0f && pointDecoded.state.pointSizeMin == 0.5f && pointDecoded.state.pointSizeMax == 8.0f, "the recipe dropped the vertex point list or its point sizes"); + auto meshPointState = pointState; + meshPointState.vertexPoints = false; + meshPointState.stages.mesh = *lineState.stages.mesh; + meshPointState.stages.mesh->outputPrimitiveSize = 1u; + meshPointState.stages.mesh->provokingVertex = 0u; + GraphicsPipelineRecipe meshPointDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(meshPointState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), meshPointDecoded), "the mesh point recipe does not decode"); + Require(!meshPointDecoded.state.vertexPoints && meshPointDecoded.state.stages.mesh && meshPointDecoded.state.stages.mesh->outputPrimitiveSize == 1u && meshPointDecoded.state.pointSize == 4.0f && meshPointDecoded.state.pointSizeMin == 0.5f && meshPointDecoded.state.pointSizeMax == 8.0f, "the recipe dropped mesh points or their sizes without the vertex point flag"); auto fourSampleState = state; fourSampleState.samples = 4; auto maskState = fourSampleState; diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 0a370358d5..ce6f2d65f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include #include +#include #include #include #include @@ -144,6 +145,7 @@ struct MeshDraw { VkCullModeFlags cull = VK_CULL_MODE_NONE; std::span code = {}; std::uint32_t stagesRegister = 0x20u; + ShaderRecompiler::ShaderVertexStageInfo vertex{}; }; bool ReadsNumWorkgroups(std::span words) { @@ -176,7 +178,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; ShaderRecompiler::RecompileRequest geometry{ {ShaderStage::Mesh, reinterpret_cast(code.data()), code, 0, {}}, - {setup.waveSize, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory}, + {setup.waveSize, 0, userData, std::nullopt, std::nullopt, setup.vertex, geometryMemory}, target, {0, 0, 0, setup.geometryPushBytes}, ShaderRecompiler::GraphicsCompileContext{0, {}, setup.mesh, std::nullopt, {setup.draw.indexAddress, setup.draw.indexCount, setup.draw.indexSize, setup.draw.instanceCount}} @@ -207,6 +209,10 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt}; + state.pointSize = setup.vertex.pointSize; + state.pointSizeMin = setup.vertex.pointSizeMin; + state.pointSizeMax = setup.vertex.pointSizeMax; + state.paClVsOutCntl = setup.vertex.paClVsOutCntl; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -410,6 +416,48 @@ int main() { } } + for (const auto pointWave : {32u, 64u}) { + for (const auto size : {3.0f, 4.0f}) { + ClearPixels(); + MeshDraw points{pointSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size()))}; + points.waveSize = pointWave; + points.vertex.pointSize = size; + DrawMesh(device, points); + for (std::uint32_t primitive = 0; primitive < Triangles; ++primitive) { + std::uint32_t covered = 0; + for (std::size_t pixel = 0; pixel < Pixels.size(); pixel += 4u) covered += PixelIs(pixel, TriangleColor(primitive)); + Require(covered == static_cast(size * size), "mesh point did not cover its register-sized square"); + } + } + for (const auto exportedSize : {6.0f, -3.0f}) { + ClearPixels(); + const auto& baseCode = pointWave == 32u ? Wave32GeometryCode : GeometryCode; + std::array sizedCode{}; + std::copy_n(baseCode.begin(), 101u, sizedCode.begin()); + sizedCode[92] = 0xbf88000eu; + sizedCode[99] = 0xf80000cfu; + sizedCode[101] = 0x7e6802ffu; + sizedCode[102] = std::bit_cast(exportedSize); + sizedCode[103] = 0xf80008d1u; + sizedCode[104] = 0x00000034u; + std::copy(baseCode.begin() + 101u, baseCode.end(), sizedCode.begin() + 105u); + MeshDraw exported{pointSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size()))}; + exported.waveSize = pointWave; + exported.code = sizedCode; + exported.vertex.paClVsOutCntl = (1u << 16u) | (1u << 21u); + exported.vertex.pointSizeMin = 3.0f; + exported.vertex.pointSizeMax = 4.0f; + DrawMesh(device, exported); + for (std::uint32_t primitive = 0; primitive < Triangles; ++primitive) { + std::uint32_t covered = 0; + for (std::size_t pixel = 0; pixel < Pixels.size(); pixel += 4u) covered += PixelIs(pixel, TriangleColor(primitive)); + const auto side = exportedSize < 3.0f ? 3u : 4u; + Require(covered == side * side, "mesh point ignored its exported size or clamp"); + } + } + } + + ClearPixels(); auto lineSubgroup = FastLaunchSubgroup; lineSubgroup.maxPrimitives = 2u; lineSubgroup.outputPrimitiveSize = 2u; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index c5ebc46690..78674062f5 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -588,7 +588,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); - if (program.Resources().stage == IrShaderStage::Vertex && inputInfo.vertex != nullptr && inputInfo.vertex->pointList) AddOutput(next, StageOutputKind::PointSize, 0, 0, "gl_PointSize"); + if ((program.Resources().stage == IrShaderStage::Vertex || program.Resources().stage == IrShaderStage::Mesh) && inputInfo.vertex != nullptr && inputInfo.vertex->pointList) AddOutput(next, StageOutputKind::PointSize, 0, 0, "gl_PointSize"); CollectSamplerUses(program, next); program.Info() = std::move(next); program.Metadata().shaderInfoComplete = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index a76bb5f199..2b9a966a60 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -621,7 +621,8 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const size = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32(state), size, GlslStd450(state), GLSLstd450FClamp, f32, ConstantF32(state, std::bit_cast(PointSizeFloor(*state.inputInfo.vertex))), ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMax))); } - state.module.AddFunction(spv::OpStore, state.pointSizeVariable, size); + const auto pointer = state.program.Resources().stage == IrShaderStage::Mesh ? MeshOutputPointer(state, StageOutputKind::PointSize) : state.pointSizeVariable; + state.module.AddFunction(spv::OpStore, pointer, size); continue; } const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) { @@ -779,7 +780,7 @@ std::uint32_t MeshOutputType(SpirvEmitterState& state, StageOutputKind kind) { if (kind == StageOutputKind::Layer) { return TypeU32(state); } - return IsMeshDistance(kind) ? TypeF32(state) : TypeF32Vector(state, 4u); + return IsMeshDistance(kind) || kind == StageOutputKind::PointSize ? TypeF32(state) : TypeF32Vector(state, 4u); } std::uint32_t MeshDistancePointer(SpirvEmitterState& state, const SpirvOutputBinding& output, std::uint32_t vertex) { @@ -969,7 +970,7 @@ void DefineMeshOutputs(SpirvEmitterState& state) { output.meshDataVariable = MeshArray(state, spv::StorageClassPrivate, TypeF32(state), state.laneCount); continue; } - if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer) { + if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer && output.kind != StageOutputKind::PointSize) { throw std::runtime_error("unsupported mesh output kind"); } const auto type = MeshOutputType(state, output.kind); @@ -985,7 +986,7 @@ void DefineMeshOutputs(SpirvEmitterState& state) { if (output.kind == StageOutputKind::Parameter) { state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationLocation, output.location); } else if (output.kind != StageOutputKind::Layer) { - state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationBuiltIn, spv::BuiltInPosition); + state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationBuiltIn, output.kind == StageOutputKind::PointSize ? spv::BuiltInPointSize : spv::BuiltInPosition); } if (output.kind == StageOutputKind::Position) { state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationInvariant); @@ -1076,6 +1077,15 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { EmitLabel(state, state.module.AllocateId()); for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshPrimitiveData, spv::StorageClassPrivate, TypeU32(state), ConstantU32(state, half)), ConstantU32(state, 0x80000000u)); + if (state.inputInfo.vertex->pointList) { + const auto& vertex = *state.inputInfo.vertex; + const bool fromVertex = (vertex.paClVsOutCntl & (1u << 16u)) != 0u; + if (!fromVertex && !(vertex.pointSize > 0.0f)) FailEmit("mesh points need a positive PA_SU_POINT_SIZE"); + if (fromVertex && !(vertex.pointSizeMax > 0.0f)) FailEmit("mesh point sizes need a positive PA_SU_POINT_MINMAX maximum"); + const auto size = fromVertex ? std::clamp(vertex.pointSize, PointSizeFloor(vertex), vertex.pointSizeMax) : vertex.pointSize; + state.laneHalf = half; + state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, StageOutputKind::PointSize), ConstantF32(state, std::bit_cast(size))); + } } const auto firstInvocation = state.module.AllocateId(); state.laneHalf = 0; diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index aecd8393c1..f1f7de5a92 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -239,6 +239,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.provokingVertex = mesh->provokingVertex; target.esgsItemSize = mesh->esgsItemSize; target.outputPrimitiveSize = mesh->outputPrimitiveSize; + if (target.outputPrimitiveSize == 1u) vertexStorage.pointList = true; } ShaderStageInputInfo result; result.vertex = &vertexStorage; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 986eefd76c..db05c500fa 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -29,7 +29,7 @@ - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. -- [Point-list geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` (point list) runs as a mesh shader with `OutputPoints` and scalar `PrimitivePointIndicesEXT`. Each primitive export emits one point whose vertex index is extracted from bits [8:0] of the packed word. A subgroup gets up to `maxVertices` points, with provoking vertex 0 and point raster primitive topology. Rectangle lists, per-stream types and point or line strips from a vertex program on the mesh path still throw. +- [Point geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` emits mesh points with `OutputPoints` and scalar `PrimitivePointIndicesEXT` as required by [SPV_EXT_mesh_shader](https://github.khronos.org/SPIRV-Registry/extensions/EXT/SPV_EXT_mesh_shader.html). Each packed primitive export contributes its first 9-bit vertex index and its null bit, using the same NGG primitive packing as line and triangle outputs. The subgroup reserves at least `GE_MAX_OUTPUT_PER_SUBGROUP` point slots, as the line path does; the console budget is not measured. Points use the register size or the misc-X point-size export and clamp interpreted by the vertex-point path; those register, clamp and rasterization inferences remain unmeasured on the console. Point sprites remain unsupported. Point output from a vertex workgroup still throws. Cocoon (PPSA08766) reaches this path with triangle-strip input. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. `PS_ITER_SAMPLE`, an `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. From 95a2a45fe33a12b068e0adc2b94acd14593cebfd Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sat, 17 Oct 2026 23:19:21 -0400 Subject: [PATCH 0377/1360] test(shader): preserve version 32 requests with spanned waves --- .../recompiler/tests/RequestSerializerTests.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 9ff48f6bcb..1b3305f5f4 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -582,5 +582,20 @@ int main() { } Require(pointRejected); + for (const bool spanned : {false, true}) { + pointRequest.target.spannedWaves = spanned; + auto main32 = DecodeBase64(serializer.Serialize(pointRequest)); + main32.resize(main32.size() - 13u); + main32[4] = 32u; + const auto replay32 = serializer.Deserialize(EncodeBase64(main32)); + Require(replay32.request.target.spannedWaves == spanned); + Require(replay32.request.context.vertex.has_value()); + Require(replay32.request.context.vertex->paClVsOutCntl == pointRequest.context.vertex->paClVsOutCntl); + Require(!replay32.request.context.vertex->pointList && replay32.request.context.vertex->pointSize == 1.0f); + Require(replay32.request.context.vertex->pointSizeMin == 0.0f && replay32.request.context.vertex->pointSizeMax == 0.0f); + const auto upgraded32 = DecodeBase64(serializer.Serialize(replay32.request)); + Require(upgraded32[4] == 33u && upgraded32.size() == main32.size() + 13u); + } + return 0; } From 8409360d409e1fd5ffb9cd28d20279d6005b0559 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 21:39:16 -0400 Subject: [PATCH 0378/1360] test(shader): retain request versions through 34 for point replay --- .../tests/RequestSerializerTests.cpp | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 1b3305f5f4..cfb2b19c70 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -583,19 +583,29 @@ int main() { Require(pointRejected); for (const bool spanned : {false, true}) { - pointRequest.target.spannedWaves = spanned; - auto main32 = DecodeBase64(serializer.Serialize(pointRequest)); - main32.resize(main32.size() - 13u); - main32[4] = 32u; - const auto replay32 = serializer.Deserialize(EncodeBase64(main32)); - Require(replay32.request.target.spannedWaves == spanned); - Require(replay32.request.context.vertex.has_value()); - Require(replay32.request.context.vertex->paClVsOutCntl == pointRequest.context.vertex->paClVsOutCntl); - Require(!replay32.request.context.vertex->pointList && replay32.request.context.vertex->pointSize == 1.0f); - Require(replay32.request.context.vertex->pointSizeMin == 0.0f && replay32.request.context.vertex->pointSizeMax == 0.0f); - const auto upgraded32 = DecodeBase64(serializer.Serialize(replay32.request)); - Require(upgraded32[4] == 33u && upgraded32.size() == main32.size() + 13u); + for (const std::uint8_t version : {32u, 33u, 34u}) { + pointRequest.target.spannedWaves = spanned; + auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); + legacy.resize(legacy.size() - 13u); + legacy[4] = version; + const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); + Require(replayLegacy.request.target.spannedWaves == spanned); + Require(replayLegacy.request.context.vertex.has_value()); + Require(replayLegacy.request.context.vertex->paClVsOutCntl == pointRequest.context.vertex->paClVsOutCntl); + Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); + Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); + const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); + Require(upgradedLegacy[4] == 35u && upgradedLegacy.size() == legacy.size() + 13u); + } } + pointRequest.context.pixel = pixel; + auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); + legacy34.resize(legacy34.size() - 13u); + legacy34[4] = 34u; + const auto replay34 = serializer.Deserialize(EncodeBase64(legacy34)); + Require(replay34.request.context.pixel.has_value() && replay34.request.context.pixel->iteratesSamples && replay34.request.context.pixel->positionLocation == pixel.positionLocation && replay34.request.context.pixel->sampleLocations == pixel.sampleLocations); + Require(replay34.request.context.vertex.has_value() && !replay34.request.context.vertex->pointList && replay34.request.context.vertex->pointSize == 1.0f); + return 0; } From c750708c8d0ac2355f25d1fc56ec0711954d65cb Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 23:59:41 -0400 Subject: [PATCH 0379/1360] test(shader): preserve depth remap requests through point replay --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/RequestSerializerTests.cpp | 37 +++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a7d80f6b93..2d1ade81b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5258,7 +5258,7 @@ void verifyDepthRemapRequests() { const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); - require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization"); + require(serializer.Serialize(replay.request) == remapped, "a version 37 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index cfb2b19c70..5c77f7aab0 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -582,26 +582,55 @@ int main() { } Require(pointRejected); + ShaderRecompiler::RecompileRequest pointRequest{}; + pointRequest.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + pointRequest.context.vertex->paClVsOutCntl = 0x01210000u; + pointRequest.target.depthRangeRemap = true; + pointRequest.target.float32FlushToZero = true; + pointRequest.context.vertex->depthRemapMode = 2u; + pointRequest.context.vertex->pointList = true; + pointRequest.context.vertex->pointSize = 4.0f; + pointRequest.context.vertex->pointSizeMin = 0.5f; + pointRequest.context.vertex->pointSizeMax = 8.0f; + const auto pointEncoded = serializer.Serialize(pointRequest); + const auto pointReplay = serializer.Deserialize(pointEncoded); + Require(pointReplay.request.context.vertex.has_value() && pointReplay.request.context.vertex->paClVsOutCntl == 0x01210000u && pointReplay.request.context.vertex->pointList); + Require(pointReplay.request.context.vertex->pointSize == 4.0f && pointReplay.request.context.vertex->pointSizeMin == 0.5f && pointReplay.request.context.vertex->pointSizeMax == 8.0f); + Require(pointReplay.request.target.depthRangeRemap && pointReplay.request.context.vertex->depthRemapMode == 2u); + Require(serializer.Serialize(pointReplay.request) == pointEncoded); + auto pointBytes = DecodeBase64(pointEncoded); + pointBytes.pop_back(); + bool pointRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(pointBytes))); + } catch (const std::runtime_error& error) { + pointRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(pointRejected); + for (const bool spanned : {false, true}) { - for (const std::uint8_t version : {32u, 33u, 34u}) { + for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - 13u); + legacy.resize(legacy.size() - (version >= 36u ? 13u : version >= 35u ? 14u : 19u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); + Require(replayLegacy.request.target.float32FlushToZero == (version >= 36u)); + Require(replayLegacy.request.target.depthRangeRemap == (version >= 35u)); + Require(replayLegacy.request.context.vertex->depthRemapMode == (version >= 35u ? 2u : 0u)); Require(replayLegacy.request.context.vertex.has_value()); Require(replayLegacy.request.context.vertex->paClVsOutCntl == pointRequest.context.vertex->paClVsOutCntl); Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 35u && upgradedLegacy.size() == legacy.size() + 13u); + Require(upgradedLegacy[4] == 39u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 13u : version >= 35u ? 14u : 19u)); } } pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 13u); + legacy34.resize(legacy34.size() - 19u); legacy34[4] = 34u; const auto replay34 = serializer.Deserialize(EncodeBase64(legacy34)); Require(replay34.request.context.pixel.has_value() && replay34.request.context.pixel->iteratesSamples && replay34.request.context.pixel->positionLocation == pixel.positionLocation && replay34.request.context.pixel->sampleLocations == pixel.sampleLocations); From 70c8476c7ca0277ce35c5af1eacc759f326cd2c2 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 01:50:41 -0400 Subject: [PATCH 0380/1360] test(shader): align request version guards after float flush merge --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/RequestSerializerTests.cpp | 30 +++++-------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2d1ade81b1..9c41238ecc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5258,7 +5258,7 @@ void verifyDepthRemapRequests() { const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); - require(serializer.Serialize(replay.request) == remapped, "a version 37 depth remap request changed on re-serialization"); + require(serializer.Serialize(replay.request) == remapped, "the depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 5c77f7aab0..d5d009cb6a 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -582,31 +582,12 @@ int main() { } Require(pointRejected); - ShaderRecompiler::RecompileRequest pointRequest{}; - pointRequest.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{}; - pointRequest.context.vertex->paClVsOutCntl = 0x01210000u; pointRequest.target.depthRangeRemap = true; pointRequest.target.float32FlushToZero = true; pointRequest.context.vertex->depthRemapMode = 2u; - pointRequest.context.vertex->pointList = true; - pointRequest.context.vertex->pointSize = 4.0f; - pointRequest.context.vertex->pointSizeMin = 0.5f; - pointRequest.context.vertex->pointSizeMax = 8.0f; - const auto pointEncoded = serializer.Serialize(pointRequest); - const auto pointReplay = serializer.Deserialize(pointEncoded); - Require(pointReplay.request.context.vertex.has_value() && pointReplay.request.context.vertex->paClVsOutCntl == 0x01210000u && pointReplay.request.context.vertex->pointList); - Require(pointReplay.request.context.vertex->pointSize == 4.0f && pointReplay.request.context.vertex->pointSizeMin == 0.5f && pointReplay.request.context.vertex->pointSizeMax == 8.0f); - Require(pointReplay.request.target.depthRangeRemap && pointReplay.request.context.vertex->depthRemapMode == 2u); - Require(serializer.Serialize(pointReplay.request) == pointEncoded); - auto pointBytes = DecodeBase64(pointEncoded); - pointBytes.pop_back(); - bool pointRejected = false; - try { - static_cast(serializer.Deserialize(EncodeBase64(pointBytes))); - } catch (const std::runtime_error& error) { - pointRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; - } - Require(pointRejected); + const auto remappedPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); + Require(remappedPoint.request.target.depthRangeRemap && remappedPoint.request.context.vertex->depthRemapMode == 2u); + Require(serializer.Serialize(remappedPoint.request) == serializer.Serialize(pointRequest)); for (const bool spanned : {false, true}) { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { @@ -618,8 +599,8 @@ int main() { Require(replayLegacy.request.target.spannedWaves == spanned); Require(replayLegacy.request.target.float32FlushToZero == (version >= 36u)); Require(replayLegacy.request.target.depthRangeRemap == (version >= 35u)); - Require(replayLegacy.request.context.vertex->depthRemapMode == (version >= 35u ? 2u : 0u)); Require(replayLegacy.request.context.vertex.has_value()); + Require(replayLegacy.request.context.vertex->depthRemapMode == (version >= 35u ? 2u : 0u)); Require(replayLegacy.request.context.vertex->paClVsOutCntl == pointRequest.context.vertex->paClVsOutCntl); Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); @@ -631,6 +612,9 @@ int main() { pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); legacy34.resize(legacy34.size() - 19u); + const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); + Require(stencil34 < legacy34.end() && *stencil34 == 0u); + legacy34.erase(stencil34); legacy34[4] = 34u; const auto replay34 = serializer.Deserialize(EncodeBase64(legacy34)); Require(replay34.request.context.pixel.has_value() && replay34.request.context.pixel->iteratesSamples && replay34.request.context.pixel->positionLocation == pixel.positionLocation && replay34.request.context.pixel->sampleLocations == pixel.sampleLocations); From ee9b1c824951501631659afba44c1c4f9aa8293c Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 12:32:35 -0400 Subject: [PATCH 0381/1360] test(shader): upgrade legacy point requests to version 40 --- core/shader/recompiler/tests/RequestSerializerTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index d5d009cb6a..aaac818699 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -605,7 +605,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 39u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 13u : version >= 35u ? 14u : 19u)); + Require(upgradedLegacy[4] == 40u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 13u : version >= 35u ? 14u : 19u)); } } From 19be2835f2396f3e9033ce4da5bf4b5aa53355e4 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 21:04:21 -0400 Subject: [PATCH 0382/1360] test(shader): preserve legacy point requests after denorm serialization --- core/shader/recompiler/tests/RequestSerializerTests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index aaac818699..8d453160fc 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -593,7 +593,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 13u : version >= 35u ? 14u : 19u)); + legacy.resize(legacy.size() - (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -605,13 +605,13 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 40u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 13u : version >= 35u ? 14u : 19u)); + Require(upgradedLegacy[4] == 42u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); } } pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 19u); + legacy34.resize(legacy34.size() - 20u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencil34 < legacy34.end() && *stencil34 == 0u); legacy34.erase(stencil34); From b779c90159a631f3ace02cf9525553c49388d5c7 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 06:22:30 -0400 Subject: [PATCH 0383/1360] test(shader): retain sample order through legacy point replay --- .../recompiler/tests/RequestSerializerTests.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 8d453160fc..b1db645fe1 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -605,16 +605,27 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 42u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); + Require(upgradedLegacy[4] == 43u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); } } + pixel.sampleOrder = {0x76543210u, 0x01234567u, 0x10325476u, 0x67452301u}; + pointRequest.context.pixel = pixel; + const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); + Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); + auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); + legacy42.resize(legacy42.size() - 13u); + legacy42[4] = 42u; + const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); + Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); + pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); legacy34.resize(legacy34.size() - 20u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); - Require(stencil34 < legacy34.end() && *stencil34 == 0u); - legacy34.erase(stencil34); + const auto legacyPixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); + Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); + legacy34.erase(stencil34, stencil34 + legacyPixelTail); legacy34[4] = 34u; const auto replay34 = serializer.Deserialize(EncodeBase64(legacy34)); Require(replay34.request.context.pixel.has_value() && replay34.request.context.pixel->iteratesSamples && replay34.request.context.pixel->positionLocation == pixel.positionLocation && replay34.request.context.pixel->sampleLocations == pixel.sampleLocations); From a914d58c0c195b9fc5f1a3766f0647b18d92fa47 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 07:03:02 -0400 Subject: [PATCH 0384/1360] fix(agc): version point journal after polygon mode --- core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 1347ef9127..c71a557efc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 6u; +constexpr std::uint32_t FileFormat = 7u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; From 1a74a6cdd9d3525115ccfe12dccb9185ad70fb5e Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 10:57:29 +0700 Subject: [PATCH 0385/1360] fix(shader): keep a v_writelane lane known across computed-index window stores fdfawef324 notes that current LLVM spills SGPRs into VGPRs it allocates like any other register, so one window register can be an ordinary register early in a program and hold spill lanes later. ReadLaneEliminator only followed a spilled lane across M0-indexed window stores for slots never used as ordinary registers, so RESIDENT EVIL 4 (PPSA07411) and LEGO Horizon Adventures (PPSA14632) kept their reloaded descriptors as run-time values and stopped. A known lane can only come from a v_writelane chain, so keep it across a computed-index store instead; a constant-index store of the slot still ends the spill. This is fdfawef324's suggestion on #6277. That such a store misses the spill cannot be proven for these programs, so it is checked at run time where a replaced read depends on it: a backward liveness over each slot and lane whose known value a replaced read used puts a WindowLaneGuard before every computed-index store at which that lane is live, and the guard records a WindowLaneSpill fault when the store changes that lane of the slot. The driver reads the fault buffer back for every program with such a guard and reports the dispatch or draw as failed, so a broken assumption is no longer silent. This also covers the pure spill slots main already folds. Co-authored-by: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Co-Authored-By: Claude --- .../Graphics/src/BdaResources.cpp | 4 + .../Graphics/src/ShaderAddressResources.cpp | 2 +- .../libSceAgcDriver/tests/VectorWindow.cpp | 36 +++++ .../tests/execution/WindowLaneSpill.cpp | 133 ++++++++++++++-- core/shader/recompiler/BdaAbi.hpp | 2 +- .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 2 + .../Optimization/ReadLaneEliminator.hpp | 1 + .../Optimization/src/ReadLaneEliminator.cpp | 145 +++++++++++++----- core/shader/recompiler/Recompiler.cpp | 2 + core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 10 ++ docs/dev/debt/shader.md | 1 + 16 files changed, 291 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 1da043f87f..66fbaeeb29 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -282,6 +282,10 @@ void BdaResources::CheckFault() const { message << "shader stage " << report.stage << " called address 0x" << std::hex << report.address << " at program counter 0x" << report.instruction << ", which no linked callee of that call matches"; throw std::runtime_error(message.str()); } + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::WindowLaneSpill) { + message << "shader stage " << report.stage << ": a relative write stored " << report.bytes << " in lane " << (report.address >> 32u) << " of vector window slot " << (report.address & 0xffffffffu) << ", which holds a scalar spilled with v_writelane_b32 that a later v_readlane_b32 was replaced with"; + throw std::runtime_error(message.str()); + } message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { const auto bytes = table->Bytes(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 2915b76a5a..f542724dc8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -134,7 +134,7 @@ void ShaderResources::prepareAddressBindings(std::span sha if (tables != 0 && storesThroughAddressesCached(shader.program->spirv)) bdaReadOnly = false; usesFaultBuffer = usesFaultBuffer || faults != 0; const bool tableFaultsOnly = std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTableMap; }); - faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || loopGuarded)); + faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || shader.program->windowLaneGuards != 0 || loopGuarded)); } if (usesBda) { Require(context.bufferDeviceAddress, "buffer device address is not enabled"); diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index a69e325898..6e07c138b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -40,6 +40,13 @@ constexpr std::uint32_t SampleV8V2S0S8 = 0x00400802u; constexpr std::uint32_t AddV8V1V8 = 0x4a101101u; constexpr std::uint32_t BufferStoreDword = 0xe0700000u; constexpr std::uint32_t StoreV8WithS12 = 0x80030800u; +constexpr std::uint32_t AndM0GroupXOne = 0x877c8104u; +constexpr std::uint32_t MovreldV2V0 = 0x7e048500u; +constexpr std::uint32_t MoveS16Two = 0xbe900382u; +constexpr std::uint32_t BranchForwardTwo = 0xbf820002u; +constexpr std::uint32_t SubtractS16One = 0x80908110u; +constexpr std::uint32_t CompareS16NotZero = 0xbf078010u; +constexpr std::uint32_t BranchBackFourOnScc = 0xbf85fffcu; struct Shape { std::size_t functionArrays = 0; @@ -306,6 +313,35 @@ int main(int argc, char** argv) { return shape.functionArrays == 0 && shape.accessChains == 0; }); + passed &= run("a sampler dword spilled into a window register with ordinary writes keeps its definition past a relative write", [] { + const std::array userData{}; + const std::array spilled{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, MoveV4One, WritelaneV4, FromS8Lane0, MovreldV2V0, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + return Scan(RecompileProgram(spilled, userData)).functionArrays == 1; + }); + + passed &= run("a sampler dword spilled into a window register and overwritten by an ordinary write before its v_readlane stays a run-time value", [] { + const std::array userData{}; + const std::array overwritten{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MovreldV2V0, MoveV4One, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + try { + RecompileProgram(overwritten, userData); + } catch (const std::exception& error) { + return std::string_view(error.what()).find("GetSamplerResource dword 0 is not a valid runtime value; chain: ReadLane(LoadVectorWindow(Immediate), Immediate)") != std::string_view::npos; + } + return false; + }); + + passed &= run("a sampler dword spilled into a window register before a loop entered at its condition keeps its definition past the loop's relative write", [] { + const std::array userData{}; + const std::array code{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MoveS16Two, BranchForwardTwo, MovreldV2V0, SubtractS16One, CompareS16NotZero, BranchBackFourOnScc, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + return Scan(RecompileProgram(code, userData)).functionArrays == 1; + }); + + passed &= run("a sampler dword spilled into a window register with ordinary writes before a loop entered at its condition keeps its definition past the loop's relative write", [] { + const std::array userData{}; + const std::array code{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, MoveV4One, WritelaneV4, FromS8Lane0, MoveS16Two, BranchForwardTwo, MovreldV2V0, SubtractS16One, CompareS16NotZero, BranchBackFourOnScc, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + return Scan(RecompileProgram(code, userData)).functionArrays == 1; + }); + passed &= run("a sampler dword spilled into a relatively read register keeps its definition through v_readlane", [] { const std::array code{MoveM0GroupX, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MovrelsV1V4, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, AddV8V1V8, BufferStoreDword, StoreV8WithS12, EndProgram}; const std::array userData{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp index f597fc79ee..a18422c711 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp @@ -7,7 +7,13 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#else +#include +#endif namespace { @@ -15,7 +21,8 @@ using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; -alignas(256) std::array Output{}; +constexpr std::uint32_t WideThreads = 64; +alignas(256) std::array Output{}; alignas(256) constexpr std::array Code{ 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x00010004, 0xd7610003, 0x00010205, 0xd7610003, 0x00010406, @@ -29,28 +36,111 @@ alignas(256) constexpr std::array IndexedStoreCode{ 0xd7600009, 0x00010303, 0xd760000a, 0x00010503, 0xd760000b, 0x00010703, 0xe0702000, 0x80020001, 0xbf810000, }; +alignas(256) constexpr std::array ReusedRegisterCode{ + 0x7e060300, 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x00010004, 0xd7610003, 0x00010205, 0xd7610003, + 0x00010406, 0xd7610003, 0x00010607, 0x7e048500, 0xbe840380, 0xbe850380, 0xbe860380, 0xbe870380, 0xd7600008, + 0x00010103, 0xd7600009, 0x00010303, 0xd760000a, 0x00010503, 0xd760000b, 0x00010703, 0xe0702000, 0x80020001, + 0xbf810000, +}; + +alignas(256) constexpr std::array OverwrittenSpillCode{ + 0x877c8104, 0x7e040280, 0x7e060280, 0xd7610004, 0x00010008, 0x7e048500, 0x7e0802f2, 0xd7600008, 0x00010104, + 0xf09c0f08, 0x00400802, 0xe0700000, 0x80030800, 0xbf810000, +}; + +alignas(256) constexpr std::array AliasingStoreCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x0001000d, 0x7e040287, 0x7e048500, + 0xd7600008, 0x00010103, 0x7e040208, 0xe0702000, 0x80010201, 0xbf810000, +}; + +alignas(256) constexpr std::array WideAliasingStoreCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x0001500d, 0x7e040287, 0x7e048500, + 0xd7600008, 0x00015103, 0x7e040208, 0xe0702000, 0x80010201, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreAfterReadCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x0001000d, 0xd7600008, + 0x00010103, 0x7e048500, 0x7e040208, 0xe0702000, 0x80010201, 0xbf810000, +}; + +constexpr std::uint32_t Spilled = 0x1234u; + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + std::array BufferDescriptor(const void* data, std::uint32_t count) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::span code) { - Output.fill(0xdeadbeefu); - std::vector userData(16, 0u); - const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); - std::copy(output.begin(), output.end(), userData.begin() + 4); +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData, std::uint32_t waveSize = Threads) { const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, - {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + return ShaderRecompiler::Recompile(request); +} + +std::string Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t relativeIndex = 0, std::uint32_t waveSize = Threads) { + Output.fill(0xdeadbeefu); + std::vector userData(16, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[12] = relativeIndex; + userData[13] = Spilled; + const auto result = Compile(device, code, userData, waveSize); + StderrCapture log; device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); + return log.Finish(); +} + +constexpr std::string_view Overwritten = "which holds a scalar spilled with v_writelane_b32 that a later v_readlane_b32 was replaced with"; + +void CheckSpilled(const std::string& log, const std::string& name, std::uint32_t lanes = Threads) { + Require(log.find(Overwritten) == std::string::npos, name + ": reported an overwritten spill: " + log); + for (std::uint32_t tid = 0; tid < lanes; ++tid) { + Require(Output[tid] == Spilled, name + ": lane " + std::to_string(tid) + " stored " + std::to_string(Output[tid]) + ", expected the spilled " + std::to_string(Spilled)); + } } void Check(const std::string& name) { @@ -59,6 +149,17 @@ void Check(const std::string& name) { } } +void CheckRefused(AgcDriver::VulkanDevice& device, std::span code, const std::string& name) { + const std::vector userData(16, 0u); + std::string error; + try { + static_cast(Compile(device, code, userData)); + } catch (const std::exception& thrown) { + error = thrown.what(); + } + Require(error.find("GetSamplerResource dword 0 is not a valid runtime value") != std::string::npos, name + ": " + (error.empty() ? std::string("recompiled") : error)); +} + } int main() { @@ -73,6 +174,20 @@ int main() { Check("window lane spill"); Run(*device, IndexedStoreCode); Check("window lane spill past an indexed store"); + CheckRefused(*device, OverwrittenSpillCode, "window lane spill overwritten by an ordinary write before its v_readlane"); + Run(*device, ReusedRegisterCode); + Check("window lane spill into a register with ordinary writes past an indexed store"); + CheckSpilled(Run(*device, AliasingStoreCode, 0), "window lane spill past an indexed store with m0 = 0"); + const auto aliased = Run(*device, AliasingStoreCode, 1); + Require(aliased.find(Overwritten) != std::string::npos, "window lane spill overwritten by an indexed store with m0 = 1 was not reported: " + aliased); + CheckSpilled(Run(*device, StoreAfterReadCode, 1), "window lane spill overwritten by an indexed store with m0 = 1 after its v_readlane"); + if (device->Target().subgroupSize >= WideThreads) { + CheckSpilled(Run(*device, WideAliasingStoreCode, 0, WideThreads), "wave64 window lane spill in lane 40 past an indexed store with m0 = 0", WideThreads); + const auto wide = Run(*device, WideAliasingStoreCode, 1, WideThreads); + Require(wide.find("stored 40 in lane 40 of vector window slot 1") != std::string::npos, "wave64 window lane spill in lane 40 overwritten by an indexed store with m0 = 1 was not reported: " + wide); + } else { + std::printf("wave64 cases skipped, subgroup size %u\n", device->Target().subgroupSize); + } std::puts("window lane spill tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 0c84230b80..f5df5fcbcd 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -35,7 +35,7 @@ inline bool RangesOrdered(const Range* ranges, std::size_t count) { } enum class FaultState : std::uint32_t { Empty, Writing, Ready }; -enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable, UnlinkedCall }; +enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable, UnlinkedCall, WindowLaneSpill }; struct Fault { FaultState state; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index c1ed5e72ed..c6b34841ba 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -74,6 +74,7 @@ enum class IrOpcode : std::uint16_t { Reference, ReferenceU32, UnlinkedCall, + WindowLaneGuard, GetUserData, GetShaderBase, ShaderClock, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index b3622f4c6a..22ec648a27 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -115,6 +115,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("Reference", Void, U1), makeMeta("ReferenceU32", Void, U32), makeMeta("UnlinkedCall", Void, U32, U32, U32), + makeMeta("WindowLaneGuard", Void, U32, U32, U32, U32, U32), makeMeta("GetUserData", U32, ScalarReg), makeMeta("GetShaderBase", U64), makeMeta("ShaderClock", U64), @@ -894,6 +895,7 @@ bool IrOpcodeHasSideEffects(IrOpcode opcode) { case IrOpcode::Reference: case IrOpcode::ReferenceU32: case IrOpcode::UnlinkedCall: + case IrOpcode::WindowLaneGuard: case IrOpcode::SetTessellationAttribute: case IrOpcode::InterpolateHostP2: case IrOpcode::SetThreadBitScalarRegister: diff --git a/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp index a52432123a..8fdbe75a07 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { struct ReadLaneEliminationStats { std::uint32_t rewrittenReads = 0; + std::uint32_t guardedStores = 0; }; class ReadLaneEliminator { diff --git a/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp b/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp index 97fbe47cb2..d2ebf74e9b 100644 --- a/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp @@ -1,6 +1,8 @@ #include "Optimization/ReadLaneEliminator.hpp" #include #include +#include +#include #include #include #include @@ -52,44 +54,13 @@ struct LaneFlow { struct WindowFlows { const IrProgram* program = nullptr; - std::unordered_set spillSlots; std::unordered_map flows; std::unordered_set computing; + std::vector>* folded = nullptr; }; thread_local WindowFlows* windowFlows = nullptr; -std::unordered_set collectLaneSpillSlots(const IrProgram& program) { - std::unordered_set candidates; - std::unordered_set rejected; - const auto immediateSlot = [](const IrValue& inst, std::uint32_t& slot) { - const IrValue* index = inst.Argument(0)->Resolve(); - if (!isImmediateU32(*index)) return false; - slot = index->ImmediateU32(); - return true; - }; - for (const auto& block : program.Blocks()) { - for (const IrValue* inst : block->Instructions()) { - std::uint32_t slot = 0; - if (inst->Opcode() == IrOpcode::StoreVectorWindow) { - if (!immediateSlot(*inst, slot)) continue; - candidates.insert(slot); - if (inst->Argument(1)->Resolve()->Opcode() != IrOpcode::WriteLane) rejected.insert(slot); - } else if (inst->Opcode() == IrOpcode::LoadVectorWindow) { - if (!immediateSlot(*inst, slot)) continue; - candidates.insert(slot); - for (const IrUse& use : inst->OperandUses()) { - const bool laneRead = use.user->Opcode() == IrOpcode::ReadLane && use.operand == 0u; - const bool chained = use.user->Opcode() == IrOpcode::WriteLane && use.operand == 2u; - if (!laneRead && !chained) rejected.insert(slot); - } - } - } - } - for (const auto slot : rejected) candidates.erase(slot); - return candidates; -} - LaneState windowLoadLane(const IrValue* load, std::uint32_t lane, std::uint32_t waveSize); LaneState storedLane(IrValue* value, std::uint32_t slot, std::uint32_t lane, std::uint32_t waveSize, const LaneFlow& flow) { @@ -117,7 +88,6 @@ const LaneFlow* laneFlow(std::uint32_t slot, std::uint32_t lane, std::uint32_t w if (const auto found = flows.flows.find(key); found != flows.flows.end()) return &found->second; if (!flows.computing.insert(key).second) return nullptr; LaneFlow flow; - const bool spill = flows.spillSlots.contains(slot); const auto& blocks = flows.program->Blocks(); constexpr int MaxPasses = 64; bool settled = false; @@ -144,11 +114,7 @@ const LaneFlow* laneFlow(std::uint32_t slot, std::uint32_t lane, std::uint32_t w } } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { const IrValue* index = inst->Argument(0)->Resolve(); - if (!isImmediateU32(*index)) { - if (!spill) running = {LaneState::Kind::Unknown, nullptr}; - continue; - } - if (index->ImmediateU32() != slot) continue; + if (!isImmediateU32(*index) || index->ImmediateU32() != slot) continue; running = storedLane(inst->Argument(1), slot, lane, waveSize, flow); } } @@ -177,8 +143,98 @@ LaneState windowLoadLane(const IrValue* load, std::uint32_t lane, std::uint32_t WindowLane windowLane(IrValue* load, std::uint32_t lane, std::uint32_t waveSize) { const LaneState state = windowLoadLane(load, lane, waveSize); - if (state.kind == LaneState::Kind::Known) return {state.value, true}; - return {}; + if (state.kind != LaneState::Kind::Known) return {}; + if (windowFlows != nullptr && windowFlows->folded != nullptr) windowFlows->folded->emplace_back(load, lane); + return {state.value, true}; +} + +IrValue* laneSource(IrValue* value, std::uint32_t lane, std::uint32_t waveSize) { + for (;;) { + value = value->Resolve(); + if (value->Opcode() == IrOpcode::WriteLane) { + const IrValue* selector = value->Argument(1)->Resolve(); + if (!isImmediateU32(*selector) || selector->ImmediateU32() % waveSize == lane) return nullptr; + value = value->Argument(2); + continue; + } + if (value->Opcode() != IrOpcode::LoadVectorWindow || !isImmediateU32(*value->Argument(0)->Resolve())) return nullptr; + return value; + } +} + +IrValue& immediate(IrProgram& program, std::uint32_t value) { + IrValue& constant = program.CreateValue(IrOpcode::Void, IrType::U32); + constant.SetImmediateU32(value); + return constant; +} + +std::size_t guardSpilledLanes(IrProgram& program, const std::vector>& folded, std::uint32_t waveSize) { + const auto keyOf = [](std::uint32_t slot, std::uint32_t lane) { return (static_cast(slot) << 32u) | lane; }; + std::map> used; + std::vector work; + const auto use = [&](IrValue* load, std::uint32_t lane) { + const auto key = keyOf(load->Argument(0)->Resolve()->ImmediateU32(), lane); + if (!used[key].insert(load).second) return false; + work.push_back(key); + return true; + }; + for (const auto& [load, lane] : folded) use(load, lane); + std::unordered_map> guarded; + while (!work.empty()) { + const auto key = work.back(); + work.pop_back(); + const auto slot = static_cast(key >> 32u); + const auto lane = static_cast(key); + const auto& usedLoads = used[key]; + std::unordered_map liveIn; + for (bool changed = true; changed;) { + changed = false; + for (auto block = program.Blocks().rbegin(); block != program.Blocks().rend(); ++block) { + bool live = false; + for (const IrBlock* successor : (*block)->Successors()) live = live || liveIn[successor]; + for (auto inst = (*block)->Instructions().rbegin(); inst != (*block)->Instructions().rend(); ++inst) { + IrValue* value = *inst; + if (value->Opcode() == IrOpcode::LoadVectorWindow) { + live = live || usedLoads.contains(value); + continue; + } + if (value->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = value->Argument(0)->Resolve(); + if (!isImmediateU32(*index)) { + if (live) guarded[value].insert(key); + continue; + } + if (index->ImmediateU32() != slot) continue; + if (live) { + if (IrValue* source = laneSource(value->Argument(1), lane, waveSize); source != nullptr && use(source, lane)) changed = true; + } + live = false; + } + if (const auto found = liveIn.find(block->get()); found == liveIn.end() || found->second != live) { + changed = changed || live || found != liveIn.end(); + liveIn[block->get()] = live; + } + } + } + } + std::size_t guards = 0; + for (const auto& block : program.Blocks()) { + for (IrValue* store : std::vector(block->Instructions().begin(), block->Instructions().end())) { + const auto found = guarded.find(store); + if (found == guarded.end()) continue; + IrValue& current = program.CreateValue(IrOpcode::LoadVectorWindow, IrType::U32); + current.AddArgument(store->Argument(0)); + block->InsertInstructionBefore(store, ¤t); + for (const auto key : found->second) { + IrValue& guard = program.CreateValue(IrOpcode::WindowLaneGuard, IrType::Void); + for (IrValue* argument : {store->Argument(0), store->Argument(1), ¤t, &immediate(program, static_cast(key >> 32u)), &immediate(program, static_cast(key))}) guard.AddArgument(argument); + block->InsertInstructionBefore(store, &guard); + ++guards; + } + } + } + if (guards != 0u) program.Info().usesFaultBuffer = true; + return guards; } ChainResult searchChain(IrValue* value, std::uint32_t lane, std::uint32_t waveSize) { @@ -362,7 +418,7 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: } WindowFlows flows; flows.program = &program; - flows.spillSlots = collectLaneSpillSlots(program); + std::vector> folded; struct FlowScope { ~FlowScope() { windowFlows = nullptr; } } flowScope; @@ -381,6 +437,7 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: const std::uint32_t lane = selector->ImmediateU32() % waveSize; const ChainResult chain = searchChain(inst->Argument(0), lane, waveSize); if (chain.write != nullptr) { + if (chain.value->Opcode() == IrOpcode::LoadVectorWindow) folded.emplace_back(chain.value, lane); inst->ReplaceUsesWith(chain.scalar, true); stats.rewrittenReads++; continue; @@ -397,10 +454,14 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: } PhiMap phiMap; - inst->ReplaceUsesWith(getRealValue(program, phiMap, chain.value, lane, waveSize), true); + flows.folded = &folded; + IrValue* real = getRealValue(program, phiMap, chain.value, lane, waveSize); + flows.folded = nullptr; + inst->ReplaceUsesWith(real, true); stats.rewrittenReads++; } } + stats.guardedStores = static_cast(guardSpilledLanes(program, folded, waveSize)); return stats; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..763f88f3ed 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -666,6 +666,8 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) } result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; + result.windowLaneGuards = 0; + for (const auto& block : program.Blocks()) result.windowLaneGuards += static_cast(std::ranges::count_if(block->Instructions(), [](const IrValue* value) { return value->Opcode() == IrOpcode::WindowLaneGuard; })); result.memoryOffsetDword = bindings.layout.memoryOffsetDword; result.shaderDataDwords = bindings.layout.ShaderDataDwords(); result.imageMetadataDword = bindings.layout.ImageMetadataDword(); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 127ed88d5b..db07fdc684 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -653,6 +653,7 @@ struct CompiledShaderArtifact { std::vector runtimeImageResources; bool registrationModule = false; std::uint32_t bdaAbiVersion = 0; + std::uint32_t windowLaneGuards = 0; std::uint32_t runtimeAbiVersion = RuntimeAbi::Version; std::vector vertexInputs; std::int32_t vertexOffsetSgpr = -1; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bcda9f0c40..406136a9b6 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -469,6 +469,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.runtimeAbiVersion); writer.Values(std::span(result.spirv.Words())); writer.Value(result.bdaAbiVersion); + writer.Value(result.windowLaneGuards); writer.Value(result.memoryOffsetDword); writer.Value(result.shaderDataDwords); writer.Value(result.imageMetadataDword); @@ -527,6 +528,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.runtimeAbiVersion); result.spirv = reader.Spirv(); reader.Value(result.bdaAbiVersion); + reader.Value(result.windowLaneGuards); reader.Value(result.memoryOffsetDword); reader.Value(result.shaderDataDwords); reader.Value(result.imageMetadataDword); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index e5acfa71f9..abd799b205 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 44; +inline constexpr std::uint32_t FormatVersion = 45; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index a8f03e192a..c442047480 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -221,6 +221,7 @@ std::uint32_t EmitFPCmpClass32(SpirvEmitterState& state, std::uint32_t value, st void EmitReference(SpirvValueEmitContext& context); void EmitReferenceU32(SpirvValueEmitContext& context); void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t low, std::uint32_t high, std::uint32_t programCounter); +void EmitWindowLaneGuard(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t index, std::uint32_t value, std::uint32_t current, std::uint32_t slot, std::uint32_t lane); void EmitControlNop(SpirvValueEmitContext& context); void EmitWaitcnt(SpirvValueEmitContext& context); void EmitSendmsg(SpirvValueEmitContext& context); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 4cd661b16a..9c34192ed8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -308,6 +308,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::Reference: return Invoke(EmitReference, ctx, inst); case IrOpcode::ReferenceU32: return Invoke(EmitReferenceU32, ctx, inst); case IrOpcode::UnlinkedCall: return Invoke(EmitUnlinkedCall, ctx, inst); + case IrOpcode::WindowLaneGuard: return Invoke(EmitWindowLaneGuard, ctx, inst); case IrOpcode::GetUserData: return Invoke(EmitGetUserData, ctx, inst); case IrOpcode::GetShaderBase: return Invoke(EmitGetShaderBase, ctx, inst); case IrOpcode::ShaderClock: return Invoke(EmitShaderClock, ctx, inst); @@ -920,6 +921,15 @@ void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint StopBdaInvocationIf(state, ConstantBool(state, true)); } +void EmitWindowLaneGuard(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t index, std::uint32_t value, std::uint32_t current, std::uint32_t slot, std::uint32_t lane) { + auto& state = ctx.state; + if (state.faultBufferVariable == 0) ctx.Fail(inst, "checks a spilled lane without a fault buffer"); + const auto boolean = TypeBool(state); + const auto ownLane = Binary(state, spv::OpIEqual, boolean, EmitSubgroupLocalInvocationId(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), lane, ConstantU32(state, state.program.WaveSize() - 1u))); + const auto overwritten = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, index, slot), Binary(state, spv::OpINotEqual, boolean, value, current)); + EmitIfCondition(state, Binary(state, spv::OpLogicalAnd, boolean, ownLane, overwritten), [&] { RecordBdaFaultWords(state, slot, lane, value, ConstantU32(state, 0u), BdaAbi::FaultReason::WindowLaneSpill); }); +} + void EmitControlNop(SpirvValueEmitContext& context) { EmitVoid(context); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d76932d773..aa27f24aae 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -166,6 +166,7 @@ - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it, and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. A VALU mask covers one SGPR in wave32 and a pair in wave64. When the graph is built without the wave size, the dataflow runs for both: a program that escapes under both throws there, and one that escapes under only one throws when it is translated at that size. - [Static scalar call link pairs](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) each serve one call, or several calls whose regions are disjoint, each call closed by the first return reading the pair after its target. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in zero-initialised function-scope storage per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. +- [Lane spills read back with `v_readlane_b32`](../../../core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp) (shader recompiler) - not verified for the guest compiler: a relative write (`v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) with a computed index is taken to miss a register between a `v_writelane_b32` of a lane and a later `v_readlane_b32` of it, so the read becomes the spilled scalar. That holds when the compiler keeps spill registers out of its relatively indexed arrays. Each such write that a replaced read depends on is checked at run time: one that changes that lane of that register records a fault, and the dispatch or draw fails with `which holds a scalar spilled with v_writelane_b32`, where the console would read the new value ([m0 = 1 counterexample](https://github.com/boykopovar/AnyPS5/pull/6277#issuecomment-6215244790)). An inactive or out-of-range lane writes nothing. A constant-index write of the register still ends the spill. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. From c416e3aca3cfc8f18f27aad07e316ac47640a746 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 10:17:32 -0300 Subject: [PATCH 0386/1360] feat(libkernel): implement shared event flags and sceKernelCloseEventFlag Measured on firmware 13.60: attribute 0x100 makes a flag openable by name, and only such flags can be opened. Each open returns a new handle on the same flag, which sceKernelCloseEventFlag releases. Closing a creator handle and deleting through an opened one return EPERM. Two live shared flags cannot share a name, names of 32 or more bytes return ENAMETOOLONG, and a missing name returns ESRCH. After the creator deletes the flag, an opened handle polls and waits with EACCES and sets, clears and cancels with no effect. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/EventFlag/src/EventFlag.cpp | 232 +++++++++++------- .../prx/libkernel_sys/Export.Discovery.cpp | 5 - core/libs/tests/GuestKernelErrors.cpp | 83 ++++++- docs/dev/debt/libkernel.md | 3 +- 4 files changed, 221 insertions(+), 102 deletions(-) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index e9d51f67c1..4d2e1f7d1d 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -14,24 +14,26 @@ #include #include #include +#include #include static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; -static constexpr int SCE_KERNEL_ERROR_ENOENT = 0x80020002; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = 0x8002003F; static constexpr int SCE_KERNEL_ERROR_ECANCELED = 0x80020055; static constexpr uint32_t EVF_ATTR_TH_FIFO = 0x01; static constexpr uint32_t EVF_ATTR_TH_PRIO = 0x02; static constexpr uint32_t EVF_ATTR_SINGLE = 0x10; static constexpr uint32_t EVF_ATTR_MULTI = 0x20; -static constexpr uint32_t EVF_ATTR_UNKNOWN_0x100 = 0x100; +static constexpr uint32_t EVF_ATTR_SHARED = 0x100; +static constexpr size_t EVF_NAME_CAPACITY = 32; static constexpr uint32_t EVF_WAITMODE_AND = 0x01; static constexpr uint32_t EVF_WAITMODE_OR = 0x02; static constexpr uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; @@ -52,6 +54,7 @@ struct KernelEventFlagPrivate { std::string _name; uint64_t _pattern = 0; bool _multipleWaiters = false; + bool _shared = false; bool _deleted = false; int _waiters = 0; uint64_t _cancelGeneration = 0; @@ -87,20 +90,33 @@ struct KernelEventFlagPrivate { namespace { -std::shared_mutex eventFlagRegistryMutex; -std::unordered_map> eventFlagRegistry; +struct EventFlagHandle { + std::shared_ptr flag; + bool opened = false; +}; -std::shared_ptr ResolveEventFlag(KernelEventFlag handle) { +std::shared_mutex eventFlagRegistryMutex; +std::unordered_map> eventFlagRegistry; + +std::shared_ptr ResolveEventFlag(KernelEventFlag handle) { if (!handle) return {}; std::shared_lock lock(eventFlagRegistryMutex); const auto it = eventFlagRegistry.find(handle); - return it == eventFlagRegistry.end() ? std::shared_ptr{} : it->second; + return it == eventFlagRegistry.end() ? std::shared_ptr{} : it->second; } -void RetireEventFlag(KernelEventFlag handle, const std::shared_ptr& owner) { +KernelEventFlag RegisterEventFlag(const std::shared_ptr& entry) { + const auto handle = reinterpret_cast(entry.get()); + eventFlagRegistry.emplace(handle, entry); + return handle; +} + +bool RetireEventFlag(KernelEventFlag handle, const std::shared_ptr& owner) { std::unique_lock lock(eventFlagRegistryMutex); const auto it = eventFlagRegistry.find(handle); - if (it != eventFlagRegistry.end() && it->second == owner) eventFlagRegistry.erase(it); + if (it == eventFlagRegistry.end() || it->second != owner) return false; + eventFlagRegistry.erase(it); + return true; } } @@ -133,137 +149,175 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_TH_FIFO) && (attr & EVF_ATTR_TH_PRIO)) return SCE_KERNEL_ERROR_EINVAL; - if ((attr & ~(EVF_ATTR_TH_FIFO | EVF_ATTR_TH_PRIO | EVF_ATTR_SINGLE | EVF_ATTR_MULTI | EVF_ATTR_UNKNOWN_0x100)) != 0) return SCE_KERNEL_ERROR_EINVAL; - if (attr & EVF_ATTR_UNKNOWN_0x100) NotImplemented_nid_no_patch("sceKernelCreateEventFlag attribute 0x100"); + if ((attr & ~(EVF_ATTR_TH_FIFO | EVF_ATTR_TH_PRIO | EVF_ATTR_SINGLE | EVF_ATTR_MULTI | EVF_ATTR_SHARED)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (std::strlen(name) >= EVF_NAME_CAPACITY) return SCE_KERNEL_ERROR_ENAMETOOLONG; + KernelEventFlag handle = nullptr; std::shared_ptr flag; try { flag = std::make_shared(); flag->_name = name; flag->_pattern = init_pattern; flag->_multipleWaiters = (attr & EVF_ATTR_MULTI) != 0; + flag->_shared = (attr & EVF_ATTR_SHARED) != 0; + auto entry = std::make_shared(EventFlagHandle{flag, false}); std::unique_lock lock(eventFlagRegistryMutex); - eventFlagRegistry.emplace(flag.get(), flag); + if (flag->_shared) { + for (const auto& [other, otherEntry] : eventFlagRegistry) { + if (otherEntry->opened || !otherEntry->flag->_shared) continue; + std::lock_guard flagLock(otherEntry->flag->_mutex); + if (!otherEntry->flag->_deleted && otherEntry->flag->_name == flag->_name) return SCE_KERNEL_ERROR_EPERM; + } + } + handle = RegisterEventFlag(entry); } catch (const std::bad_alloc&) { return SCE_KERNEL_ERROR_ENOMEM; } - *ef = flag.get(); - if (TraceSync()) std::fprintf(stderr, "[evf] create %p '%s' attr=0x%x init=0x%llx\n", static_cast(flag.get()), name, attr, static_cast(init_pattern)); + *ef = handle; + if (TraceSync()) std::fprintf(stderr, "[evf] create %p '%s' attr=0x%x init=0x%llx\n", static_cast(handle), name, attr, static_cast(init_pattern)); return SCE_OK; } int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; - std::shared_lock lock(eventFlagRegistryMutex); - for (const auto& [handle, flag] : eventFlagRegistry) { - std::lock_guard flagLock(flag->_mutex); - if (flag->_deleted || flag->_name != name) continue; - *ef = handle; - return SCE_OK; + try { + std::unique_lock lock(eventFlagRegistryMutex); + std::shared_ptr target; + for (const auto& [handle, entry] : eventFlagRegistry) { + if (entry->opened || !entry->flag->_shared) continue; + std::lock_guard flagLock(entry->flag->_mutex); + if (entry->flag->_deleted || entry->flag->_name != name) continue; + target = entry->flag; + break; + } + if (!target) return SCE_KERNEL_ERROR_ESRCH; + *ef = RegisterEventFlag(std::make_shared(EventFlagHandle{std::move(target), true})); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; } - return SCE_KERNEL_ERROR_ENOENT; + return SCE_OK; +} + +int APS5_VABI sceKernelCloseEventFlag(KernelEventFlag ef) { + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + if (!entry->opened) return SCE_KERNEL_ERROR_EPERM; + return RetireEventFlag(ef, entry) ? SCE_OK : SCE_KERNEL_ERROR_ESRCH; } int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::unique_lock lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_deleted = true; - ef->_changed.NotifyAll(); - ef->_changed.Wait(lock, [&] { return ef->_waiters == 0; }); + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + if (entry->opened) return SCE_KERNEL_ERROR_EPERM; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::unique_lock lock(flag->_mutex); + if (flag->_deleted) return SCE_KERNEL_ERROR_ESRCH; + flag->_deleted = true; + flag->_changed.NotifyAll(); + flag->_changed.Wait(lock, [&] { return flag->_waiters == 0; }); lock.unlock(); - RetireEventFlag(ef, keepAlive); + RetireEventFlag(ef, entry); return SCE_OK; } int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_pattern |= bit_pattern; - if (TraceSync()) std::fprintf(stderr, "[evf] set %p '%s' |=0x%llx -> 0x%llx\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), static_cast(ef->_pattern)); - for (auto* waiter = ef->_waiterHead; waiter != nullptr; waiter = waiter->next) { - if (!waiter->satisfied && IsSatisfied(ef->_pattern, waiter->bitPattern, waiter->waitMode)) { + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) return entry->opened ? SCE_OK : SCE_KERNEL_ERROR_ESRCH; + flag->_pattern |= bit_pattern; + if (TraceSync()) std::fprintf(stderr, "[evf] set %p '%s' |=0x%llx -> 0x%llx\n", static_cast(ef), flag->_name.c_str(), static_cast(bit_pattern), static_cast(flag->_pattern)); + for (auto* waiter = flag->_waiterHead; waiter != nullptr; waiter = waiter->next) { + if (!waiter->satisfied && IsSatisfied(flag->_pattern, waiter->bitPattern, waiter->waitMode)) { waiter->satisfied = true; - waiter->resultPattern = ef->_pattern; + waiter->resultPattern = flag->_pattern; if (waiter->waitMode & EVF_WAITMODE_CLEAR_ALL) { - ef->_pattern = 0; + flag->_pattern = 0; } else if (waiter->waitMode & EVF_WAITMODE_CLEAR_PAT) { - ef->_pattern &= ~waiter->bitPattern; + flag->_pattern &= ~waiter->bitPattern; } } } - ef->_changed.NotifyAll(); + flag->_changed.NotifyAll(); return SCE_OK; } int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_pattern &= bit_pattern; + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) return entry->opened ? SCE_OK : SCE_KERNEL_ERROR_ESRCH; + flag->_pattern &= bit_pattern; return SCE_OK; } int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, int* num_wait_threads) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (num_wait_threads) *num_wait_threads = ef->_waiters; - ef->_pattern = set_pattern; - ++ef->_cancelGeneration; - ef->_changed.NotifyAll(); + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) { + if (!entry->opened) return SCE_KERNEL_ERROR_ESRCH; + if (num_wait_threads) *num_wait_threads = 0; + return SCE_OK; + } + if (num_wait_threads) *num_wait_threads = flag->_waiters; + flag->_pattern = set_pattern; + ++flag->_cancelGeneration; + flag->_changed.NotifyAll(); return SCE_OK; } int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) { + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) { + if (!entry->opened) return SCE_KERNEL_ERROR_ESRCH; + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EACCES; + } + if (!flag->_multipleWaiters && flag->_waiters != 0) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EPERM; } - if (!IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { - if (result_pat) *result_pat = ef->_pattern; + if (!IsSatisfied(flag->_pattern, bit_pattern, wait_mode)) { + if (result_pat) *result_pat = flag->_pattern; return SCE_KERNEL_ERROR_EBUSY; } - Consume(ef, bit_pattern, wait_mode, result_pat); + Consume(flag, bit_pattern, wait_mode, result_pat); return SCE_OK; } int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat, KernelUseconds* timeout) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; - std::unique_lock lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) { + KernelEventFlagPrivate* flag = entry->flag.get(); + std::unique_lock lock(flag->_mutex); + if (flag->_deleted) { + if (!entry->opened) return SCE_KERNEL_ERROR_ESRCH; + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EACCES; + } + if (!flag->_multipleWaiters && flag->_waiters != 0) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EPERM; } - if (IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { - Consume(ef, bit_pattern, wait_mode, result_pat); + if (IsSatisfied(flag->_pattern, bit_pattern, wait_mode)) { + Consume(flag, bit_pattern, wait_mode, result_pat); return SCE_OK; } - const uint64_t generation = ef->_cancelGeneration; - if (TraceSync()) std::fprintf(stderr, "[evf] wait %p '%s' want=0x%llx mode=0x%x have=0x%llx timeout=%d caller=%p outer=%p\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), wait_mode, static_cast(ef->_pattern), timeout ? static_cast(*timeout) : -1, __builtin_return_address(0), static_cast(static_cast(__builtin_frame_address(0))[0])[1]); - ++ef->_waiters; + const uint64_t generation = flag->_cancelGeneration; + if (TraceSync()) std::fprintf(stderr, "[evf] wait %p '%s' want=0x%llx mode=0x%x have=0x%llx timeout=%d caller=%p outer=%p\n", static_cast(ef), flag->_name.c_str(), static_cast(bit_pattern), wait_mode, static_cast(flag->_pattern), timeout ? static_cast(*timeout) : -1, __builtin_return_address(0), static_cast(static_cast(__builtin_frame_address(0))[0])[1]); + ++flag->_waiters; WaiterNode waiter; waiter.bitPattern = bit_pattern; waiter.waitMode = wait_mode; - ef->pushWaiter(&waiter); + flag->pushWaiter(&waiter); struct WaiterGuard { KernelEventFlagPrivate* flag; WaiterNode* node; @@ -272,28 +326,28 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u --flag->_waiters; flag->_changed.NotifyAll(); } - } waiterGuard{ef, &waiter}; - const auto released = [&] { return ef->_deleted || ef->_cancelGeneration != generation || waiter.satisfied; }; + } waiterGuard{flag, &waiter}; + const auto released = [&] { return flag->_deleted || flag->_cancelGeneration != generation || waiter.satisfied; }; bool timedOut = false; const auto waitStart = std::chrono::steady_clock::now(); if (timeout) { const std::uint64_t deadline = TimedWait::DeadlineNanos(*timeout); - timedOut = !ef->_changed.WaitUntil(lock, deadline, released); + timedOut = !flag->_changed.WaitUntil(lock, deadline, released); *timeout = static_cast(TimedWait::RemainingMicros(deadline)); } else { - ef->_changed.Wait(lock, released); + flag->_changed.Wait(lock, released); } KernelTraceWait_nid_postfix("evf", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); - if (ef->_deleted) { + if (flag->_deleted) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EACCES; } - if (ef->_cancelGeneration != generation) { + if (flag->_cancelGeneration != generation) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_ECANCELED; } if (timedOut && !waiter.satisfied) { - if (result_pat) *result_pat = ef->_pattern; + if (result_pat) *result_pat = flag->_pattern; return SCE_KERNEL_ERROR_ETIMEDOUT; } if (result_pat) *result_pat = waiter.resultPattern; @@ -303,8 +357,8 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u } extern "C" int APS5_Test_GetEventFlagWaiterCount(KernelEventFlag handle) { - const auto keepAlive = ResolveEventFlag(handle); - if (!keepAlive) return -1; - std::lock_guard lock(keepAlive->_mutex); - return keepAlive->_waiters; + const auto entry = ResolveEventFlag(handle); + if (!entry) return -1; + std::lock_guard lock(entry->flag->_mutex); + return entry->flag->_waiters; } diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..08a5b55ada 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -688,11 +688,6 @@ int APS5_VABI sceKernelCloseEport(void) { return 0; } -int APS5_VABI sceKernelCloseEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelCloseSema(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index efb2403c29..a635a36c7f 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -35,6 +35,7 @@ int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); +int APS5_VABI sceKernelCloseEventFlag(KernelEventFlag ef); int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); @@ -54,9 +55,11 @@ static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000D); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = static_cast(0x8002003F); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); static constexpr std::uint32_t EVF_ATTR_SINGLE = 0x10; static constexpr std::uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr std::uint32_t EVF_ATTR_SHARED = 0x100; static constexpr std::uint32_t EVF_WAITMODE_AND = 0x01; static constexpr std::uint32_t EVF_WAITMODE_OR = 0x02; static constexpr std::uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; @@ -212,18 +215,86 @@ int main() { Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCreateEventFlag(&ef, "", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + for (const std::uint32_t attr : {EVF_ATTR_MULTI, EVF_ATTR_SHARED | EVF_ATTR_MULTI}) { + Require(sceKernelCreateEventFlag(&ef, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", attr, 0, nullptr) == SCE_KERNEL_ERROR_ENAMETOOLONG); + } + KernelEventFlag opened = nullptr; Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); - Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); - Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened == ef); + Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); + Require(sceKernelCloseEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); + + KernelEventFlag plain = nullptr; + Require(sceKernelCreateEventFlag(&plain, "test_open", EVF_ATTR_MULTI, 0x01, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); + Require(sceKernelCloseEventFlag(plain) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelSetEventFlag(plain, 0x02) == SCE_OK); + KernelEventFlag plainTwin = nullptr; + Require(sceKernelCreateEventFlag(&plainTwin, "test_open", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(plainTwin) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + KernelEventFlag sharedTwin = nullptr; + Require(sceKernelCreateEventFlag(&sharedTwin, "test_open", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EPERM && sharedTwin == nullptr); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened != nullptr && opened != ef && opened != plain); + KernelEventFlag openedTwice = nullptr; + Require(sceKernelOpenEventFlag(&openedTwice, "test_open") == SCE_OK && openedTwice != opened && openedTwice != ef); Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); + Require(sceKernelClearEventFlag(openedTwice, ~std::uint64_t{0x04}) == SCE_OK); std::uint64_t openedPattern = 0; - Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x0d); + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x09); + Require(sceKernelDeleteEventFlag(opened) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelCloseEventFlag(ef) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelCloseEventFlag(openedTwice) == SCE_OK); + Require(sceKernelCloseEventFlag(openedTwice) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSetEventFlag(openedTwice, 0x01) == SCE_KERNEL_ERROR_ESRCH); + + std::atomic orphanWaitRes{-1}; + std::uint64_t orphanWaitPat = 0xdeadbeef; + std::thread orphanWaiter([&] { + orphanWaitRes = sceKernelWaitEventFlag(opened, 0x80, EVF_WAITMODE_OR, &orphanWaitPat, nullptr); + }); + WaitForEventFlagWaiters(opened, 1); Require(sceKernelDeleteEventFlag(ef) == SCE_OK); - opened = nullptr; - Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + orphanWaiter.join(); + Require(orphanWaitRes == SCE_KERNEL_ERROR_EACCES && orphanWaitPat == 0); + KernelEventFlag reopened = nullptr; + Require(sceKernelOpenEventFlag(&reopened, "test_open") == SCE_KERNEL_ERROR_ESRCH && reopened == nullptr); + std::uint64_t orphanPattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(opened, 0, EVF_WAITMODE_OR, &orphanPattern) == SCE_KERNEL_ERROR_EINVAL && orphanPattern == 0xdeadbeef); + Require(sceKernelPollEventFlag(opened, 0x01, EVF_WAITMODE_OR, &orphanPattern) == SCE_KERNEL_ERROR_EACCES && orphanPattern == 0); + orphanPattern = 0xdeadbeef; + KernelUseconds orphanTimeout = 1000; + Require(sceKernelWaitEventFlag(opened, 0x01, EVF_WAITMODE_OR, &orphanPattern, &orphanTimeout) == SCE_KERNEL_ERROR_EACCES && orphanPattern == 0 && orphanTimeout == 1000); + Require(sceKernelSetEventFlag(opened, 0x02) == SCE_OK); + Require(sceKernelClearEventFlag(opened, 0) == SCE_OK); + int orphanCount = -1; + Require(sceKernelCancelEventFlag(opened, 0x04, &orphanCount) == SCE_OK && orphanCount == 0); + Require(sceKernelDeleteEventFlag(opened) == SCE_KERNEL_ERROR_EPERM); + + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_SHARED | EVF_ATTR_SINGLE, 0x01, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&reopened, "test_open") == SCE_OK && reopened != opened); + Require(sceKernelCloseEventFlag(opened) == SCE_OK); + Require(sceKernelCloseEventFlag(opened) == SCE_KERNEL_ERROR_ESRCH); + std::atomic sharedSingleRes{-1}; + std::uint64_t sharedSinglePat = 0; + std::thread sharedSingleWaiter([&] { + sharedSingleRes = sceKernelWaitEventFlag(ef, 0x80, EVF_WAITMODE_OR, &sharedSinglePat, nullptr); + }); + WaitForEventFlagWaiters(ef, 1); + openedPattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(reopened, 0x01, EVF_WAITMODE_OR, &openedPattern) == SCE_KERNEL_ERROR_EPERM && openedPattern == 0); + Require(sceKernelSetEventFlag(reopened, 0x80) == SCE_OK); + sharedSingleWaiter.join(); + Require(sharedSingleRes == SCE_OK && sharedSinglePat == 0x81); + Require(sceKernelCloseEventFlag(reopened) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelDeleteEventFlag(plain) == SCE_OK); Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); std::uint64_t pattern = 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a8dc3a9fc1..6cdba1cc80 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,8 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - attribute bit 0x100 is accepted by firmware 13.60 alongside 0x1, 0x2, 0x10 and 0x20, but what it changes is unknown, so a flag created with it calls `NotImplemented_nid_no_patch`. -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) From d6b384688af7a6bbc7afa56f20a9d7a505f184bb Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 15:18:19 +0200 Subject: [PATCH 0387/1360] feat(libkernel_sys): implement the scePthread scope, atfork and setconcurrency forms --- core/libs/CMakeLists.txt | 7 +- .../prx/libkernel_sys/Export.Discovery.cpp | 24 +++---- core/libs/tests/GuestKernelSysPthread.cpp | 66 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 4 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysPthread.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index e5ae4a8cdb..11721afa82 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -306,12 +306,17 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_sys_barrier_tests) add_test(NAME guest_kernel_sys_barrier COMMAND guest_kernel_sys_barrier_tests) set_tests_properties(guest_kernel_sys_barrier PROPERTIES TIMEOUT 20) + add_test_executable(guest_kernel_sys_pthread_tests tests/GuestKernelSysPthread.cpp) + target_include_directories(guest_kernel_sys_pthread_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_pthread_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_pthread_tests) + add_test(NAME guest_kernel_sys_pthread COMMAND guest_kernel_sys_pthread_tests) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) endif() if(WIN32) - set_tests_properties(guest_kernel_sys guest_kernel_sys_barrier PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_barrier guest_kernel_sys_pthread PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(APPLE) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index f56381b92b..b431738db3 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -14,6 +14,10 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(void* const* attr, int* int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(void** attr, int pshared); int APS5_VABI pthread_getcpuclockid_nid_postfix(void* thread, int* clockId); int APS5_VABI pthread_getconcurrency_nid_postfix(); +int APS5_VABI pthread_setconcurrency_nid_postfix(int level); +int APS5_VABI pthread_attr_getscope_nid_postfix(void* const* attr, int* scope); +int APS5_VABI pthread_attr_setscope_nid_postfix(void** attr, int scope); +int APS5_VABI pthread_atfork_nid_postfix(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)()); int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags); } @@ -2311,14 +2315,12 @@ int APS5_VABI sceLibcMspaceCreateForMonoMutex(void) { return 0; } -int APS5_VABI scePthreadAtfork(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAtfork(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)()) { + return SceResult(pthread_atfork_nid_postfix(prepare, parent, child)); } -int APS5_VABI scePthreadAttrGetscope(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAttrGetscope(void* const* attr, int* scope) { + return SceResult(pthread_attr_getscope_nid_postfix(attr, scope)); } int APS5_VABI scePthreadAttrSetcreatesuspend(void) { @@ -2326,9 +2328,8 @@ int APS5_VABI scePthreadAttrSetcreatesuspend(void) { return 0; } -int APS5_VABI scePthreadAttrSetscope(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAttrSetscope(void** attr, int scope) { + return SceResult(pthread_attr_setscope_nid_postfix(attr, scope)); } int APS5_VABI scePthreadBarrierDestroy(void** barrier) { @@ -2454,9 +2455,8 @@ int APS5_VABI scePthreadSetName(void) { return 0; } -int APS5_VABI scePthreadSetconcurrency(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadSetconcurrency(int level) { + return SceResult(pthread_setconcurrency_nid_postfix(level)); } int APS5_VABI scePthreadSingle(void) { diff --git a/core/libs/tests/GuestKernelSysPthread.cpp b/core/libs/tests/GuestKernelSysPthread.cpp new file mode 100644 index 0000000000..9d352aa66c --- /dev/null +++ b/core/libs/tests/GuestKernelSysPthread.cpp @@ -0,0 +1,66 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Atfork.hpp" +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope); +int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope); +int APS5_VABI scePthreadAtfork(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child); +int APS5_VABI scePthreadSetconcurrency(int level); +int APS5_VABI scePthreadGetconcurrency(); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCOPE_PROCESS = 0; +static constexpr int SCOPE_SYSTEM = 2; + +static void Require(bool value) { if (!value) std::abort(); } + +static void APS5_VABI Prepare() {} +static void APS5_VABI Parent() {} +static void APS5_VABI Child() {} + +static void TestScope() { + PthreadAttr attr{}; + Require(scePthreadAttrInit(&attr) == 0); + int scope = -1; + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrSetscope(&attr, SCOPE_PROCESS) == 0); + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + for (const int invalid : {1, 3, -1}) { + Require(scePthreadAttrSetscope(&attr, invalid) == SCE_KERNEL_ERROR_EINVAL); + scope = -1; + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + } + Require(scePthreadAttrSetscope(&attr, SCOPE_SYSTEM) == 0); + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrGetscope(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetscope(nullptr, &scope) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetscope(nullptr, SCOPE_PROCESS) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrDestroy(&attr) == 0); +} + +static void TestConcurrency() { + Require(scePthreadSetconcurrency(3) == 0 && scePthreadGetconcurrency() == 3); + Require(scePthreadSetconcurrency(-1) == SCE_KERNEL_ERROR_EINVAL && scePthreadGetconcurrency() == 3); + Require(scePthreadSetconcurrency(0) == 0 && scePthreadGetconcurrency() == 0); +} + +static void TestAtfork() { + const auto before = PthreadAtforkHandlers_nid_no_patch().size(); + Require(scePthreadAtfork(Prepare, Parent, Child) == 0); + Require(scePthreadAtfork(nullptr, nullptr, nullptr) == 0); + const auto handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == before + 2); + Require(handlers[before].prepare == Prepare && handlers[before].parent == Parent && handlers[before].child == Child); + Require(handlers[before + 1].prepare == nullptr && handlers[before + 1].parent == nullptr && handlers[before + 1].child == nullptr); +} + +int main() { + TestScope(); + TestConcurrency(); + TestAtfork(); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 57f8251ae5..4fad705eef 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -22,3 +22,4 @@ ### Unknown function info - [scePthreadBarrier*, scePthreadBarrierattr*, scePthreadGetcpuclockid, scePthreadGetconcurrency and sceKernelMsync](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_barrier*`, `pthread_getcpuclockid`, `pthread_getconcurrency` and `msync` and turn a POSIX error into `0x80020000 | errno`; that the console's `sce` forms do the same is not measured. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); `scePthreadBarrierInit` ignores its name, and `scePthreadBarrierWait` returns -1 to the serial thread, both as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/barrier.cpp) (PS4). `scePthreadGetconcurrency` returns the level unchanged +- [scePthreadAttrGetscope, scePthreadAttrSetscope, scePthreadAtfork and scePthreadSetconcurrency](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_attr_getscope`, `pthread_attr_setscope`, `pthread_atfork` and `pthread_setconcurrency` and turn a POSIX error into `0x80020000 | errno`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/kernel/threads/pthread_attr.cpp#L392) exports `scePthreadAttrGetscope` as `ORBIS(posix_pthread_attr_getscope)`; shadPS4 has no export for the other three, whose names and NIDs are in the [symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and whose signatures follow the same POSIX forms. Not measured on a console. As in libkernel, the scope is 0 (process) or 2 (system), and `scePthreadAtfork` handlers are only recorded, as no process forks From 3990593a198fe5544e3abf19706f16ba19ce3f28 Mon Sep 17 00:00:00 2001 From: simonbronk Date: Wed, 21 Oct 2026 09:45:23 +0200 Subject: [PATCH 0388/1360] feat(libSceCesCs): implement Shift-JIS and EUC-JP scalar conversions Implement the single-character conversions between the CP932 and CP51932 profiles and UTF-8, UTF-16, UTF-32 and UCS-2 in both directions, and the Shift-JIS to JIS X 0208 row/cell arithmetic. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/CMakeLists.txt | 12 +- core/libs/prx/libSceCesCs/Export.cpp | 308 ++++++++++++++++++- core/libs/prx/libSceCesCs/PendingExports.cpp | 160 ---------- core/libs/tests/GuestCesJapanese.cpp | 170 ++++++++++ docs/dev/debt/libSceCesCs.md | 3 +- 5 files changed, 472 insertions(+), 181 deletions(-) create mode 100644 core/libs/tests/GuestCesJapanese.cpp diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index c9d9076534..c1f3d0b0a7 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -25,10 +25,14 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) - foreach(variant IN ITEMS unicode unicode_module) - add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) + foreach(variant IN ITEMS unicode unicode_module japanese japanese_module) + if(variant MATCHES "^japanese") + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesJapanese.cpp) + else() + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) + endif() target_include_directories(guest_ces_${variant}_tests PRIVATE ${LIBS_INCLUDE_DIR}) - if(variant STREQUAL "unicode_module") + if(variant MATCHES "_module$") target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs-module) else() target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs) @@ -36,7 +40,7 @@ if(BUILD_TESTING) add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) endforeach() if(WIN32) - set_tests_properties(guest_ces_unicode guest_ces_unicode_module PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 25dcb94514..7d7d1593d5 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,29 @@ bool isSbc(const void* profile) { return profile == CP1252_PROFILE || profile == CP1251_PROFILE || profile == CP1250_PROFILE; } +bool sjisToJis(uint8_t lead, uint8_t trail, int& row, int& cell) { + if (!((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC))) return false; + if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return false; + row = lead <= 0x9F ? (lead - 0x81) * 2 + 1 : (lead - 0xE0) * 2 + 63; + if (trail >= 0x9F) { + row += 1; + cell = trail - 0x9E; + } else { + cell = trail > 0x7E ? trail - 0x40 : trail - 0x3F; + } + return true; +} + +void jisToSjis(int row, int cell, uint8_t& lead, uint8_t& trail) { + if (row % 2 != 0) { + lead = static_cast(row < 63 ? 0x81 + (row - 1) / 2 : 0xE0 + (row - 63) / 2); + trail = static_cast(cell <= 63 ? cell + 0x3F : cell + 0x40); + } else { + lead = static_cast(row < 63 ? 0x81 + (row - 2) / 2 : 0xE0 + (row - 64) / 2); + trail = static_cast(cell + 0x9E); + } +} + const void* contextProfile(const void* context) { const void* profile = nullptr; std::memcpy(&profile, context, sizeof(profile)); @@ -307,14 +331,9 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 } return CES_ERROR_UNASSIGNED_CODE; } - int row = (lead <= 0x9F) ? (lead - 0x81) * 2 + 1 : (lead - 0xE0) * 2 + 63; + int row = 0; int cell = 0; - if (trail >= 0x9F) { - row += 1; - cell = trail - 0x9E; - } else { - cell = (trail > 0x7E) ? trail - 0x40 : trail - 0x3F; - } + sjisToJis(lead, trail, row, cell); if (row < 1 || row > 94 || cell < 1 || cell > 94) return CES_ERROR_UNASSIGNED_CODE; code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; if (code == 0) return CES_ERROR_UNASSIGNED_CODE; @@ -397,15 +416,7 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng const auto& reverse = jisReverse(); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); if (found == reverse.end() || found->code != code || found->supplementary) return CES_ERROR_UNASSIGNED_CODE; - const int row = found->cell / 94 + 1; - const int cell = found->cell % 94 + 1; - if (row % 2 != 0) { - mbcs[0] = static_cast(row < 63 ? 0x81 + (row - 1) / 2 : 0xE0 + (row - 63) / 2); - mbcs[1] = static_cast(cell <= 63 ? cell + 0x3F : cell + 0x40); - } else { - mbcs[0] = static_cast(row < 63 ? 0x81 + (row - 2) / 2 : 0xE0 + (row - 64) / 2); - mbcs[1] = static_cast(cell + 0x9E); - } + jisToSjis(found->cell / 94 + 1, found->cell % 94 + 1, mbcs[0], mbcs[1]); length = 2; return 0; } @@ -518,6 +529,131 @@ int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t return 0; } +constexpr bool NATIVE_BIG_ENDIAN = std::endian::native == std::endian::big; + +template +uint32_t loadUnit(const Unit* source) { + const auto* bytes = reinterpret_cast(source); + uint32_t code = 0; + for (uint32_t i = 0; i < sizeof(Unit); ++i) code |= uint32_t(bytes[i]) << (8 * (BigEndian ? sizeof(Unit) - 1 - i : i)); + return code; +} + +template +void storeUnit(Unit* target, uint32_t code) { + auto* bytes = reinterpret_cast(target); + for (uint32_t i = 0; i < sizeof(Unit); ++i) bytes[i] = static_cast(code >> (8 * (BigEndian ? sizeof(Unit) - 1 - i : i))); +} + +bool isScalar(uint32_t code) { + return code <= 0x10FFFF && (code < 0xD800 || code > 0xDFFF); +} + +template +int decodeUtf16(const uint16_t* utf16, uint32_t utf16max, uint32_t& length, uint32_t& code) { + code = loadUnit(utf16); + length = 1; + if (code >= 0xDC00 && code <= 0xDFFF) return CES_ERROR_INVALID_ENCODE; + if (code >= 0xD800 && code <= 0xDBFF) { + if (utf16max < 2) return CES_ERROR_SRC_BUFFER_END; + const uint32_t low = loadUnit(utf16 + 1); + if (low < 0xDC00 || low > 0xDFFF) return CES_ERROR_INVALID_ENCODE; + code = 0x10000 + ((code - 0xD800) << 10) + low - 0xDC00; + length = 2; + } + return 0; +} + +int decodeJapanese(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, const void* target, uint32_t& length, uint32_t& code) { + if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!source) return CES_ERROR_INVALID_SRC_BUFFER; + if (sourceMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!target) return CES_ERROR_INVALID_DST_BUFFER; + return decodeMbcs(profile, source, sourceMax, length, code); +} + +int japaneseToUcs2(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* ucs2) { + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeJapanese(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return result; + *ucs2 = static_cast(code); + if (sourceSize) *sourceSize = length; + return 0; +} + +int japaneseToUtf8(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len) { + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeJapanese(matches, profile, source, sourceMax, utf8, length, code); result != 0) return result; + uint8_t encoded[3]; + const uint32_t produced = encodeUtf8(code, encoded); + if (utf8max < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(utf8, encoded, produced); + if (sourceSize) *sourceSize = length; + if (utf8Len) *utf8Len = produced; + return 0; +} + +template +int japaneseToUtf16(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeJapanese(matches, profile, source, sourceMax, utf16, length, code); result != 0) return result; + if (utf16max < 1) return CES_ERROR_DST_BUFFER_END; + storeUnit(utf16, code); + if (sourceSize) *sourceSize = length; + if (utf16Len) *utf16Len = 1; + return 0; +} + +template +int japaneseToUtf32(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint32_t* utf32) { + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeJapanese(matches, profile, source, sourceMax, utf32, length, code); result != 0) return result; + storeUnit(utf32, code); + if (sourceSize) *sourceSize = length; + return 0; +} + +int encodeJapanese(const void* profile, uint32_t code, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { + uint8_t encoded[3]; + uint32_t size = 0; + if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; + if (targetMax < size) return CES_ERROR_DST_BUFFER_END; + std::memcpy(target, encoded, size); + if (targetSize) *targetSize = size; + return 0; +} + +int scalarToJapanese(bool (*matches)(const void*), uint32_t code, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { + if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!target) return CES_ERROR_INVALID_DST_BUFFER; + if (!isScalar(code)) return CES_ERROR_INVALID_ENCODE; + return encodeJapanese(profile, code, target, targetMax, targetSize); +} + +template +int utf32ToJapanese(bool (*matches)(const void*), const uint32_t* utf32, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { + if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!utf32) return CES_ERROR_INVALID_SRC_BUFFER; + return scalarToJapanese(matches, loadUnit(utf32), profile, target, targetMax, targetSize); +} + +template +int bufferToJapanese(bool (*matches)(const void*), const Source* source, uint32_t sourceMax, uint32_t* sourceLen, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize, Decode decode) { + if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!source) return CES_ERROR_INVALID_SRC_BUFFER; + if (sourceMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!target) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decode(source, sourceMax, length, code); result != 0) return result; + if (const int result = encodeJapanese(profile, code, target, targetMax, targetSize); result != 0) return result; + if (sourceLen) *sourceLen = length; + return 0; +} + } extern "C" { @@ -674,4 +810,144 @@ int APS5_VABI sceCesUhcToUtf8(void* profile, uint8_t* uhcBuffer, uint32_t uhcMax return mbcsToUtf8(isUhc(profile), profile, uhcBuffer, uhcMax, uhcLength, utf8Buffer, utf8Max, utf8Length); } +int APS5_VABI sceCesSJisToUcs2(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* ucs2) { + return japaneseToUcs2(isSjis, profile, sjisBuffer, sjisMax, sjisSize, ucs2); +} + +int APS5_VABI sceCesSJisToUtf8(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + return japaneseToUtf8(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf8Buffer, utf8Max, utf8Length); +} + +int APS5_VABI sceCesSJisToUtf16(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return japaneseToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesSJisToUtf16be(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return japaneseToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesSJisToUtf16le(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return japaneseToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesSJisToUtf32(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint32_t* utf32) { + return japaneseToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); +} + +int APS5_VABI sceCesSJisToUtf32be(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint32_t* utf32) { + return japaneseToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); +} + +int APS5_VABI sceCesSJisToUtf32le(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint32_t* utf32) { + return japaneseToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); +} + +int APS5_VABI sceCesEucJpToUtf16(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return japaneseToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucJpToUtf16be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return japaneseToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucJpToUtf16le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return japaneseToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucJpToUtf32(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return japaneseToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucJpToUtf32be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return japaneseToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucJpToUtf32le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return japaneseToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesUcs2ToSJis(uint16_t ucs2, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return scalarToJapanese(isSjis, ucs2, profile, sjisBuffer, sjisMax, sjisSize); +} + +int APS5_VABI sceCesUtf8ToSJis(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return bufferToJapanese(isSjis, utf8Buffer, utf8Max, utf8Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return bufferToJapanese(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return bufferToJapanese(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return bufferToJapanese(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToSJis(uint32_t utf32, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return scalarToJapanese(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); +} + +int APS5_VABI sceCesUtf32beToSJis(const uint32_t* utf32, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return utf32ToJapanese(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); +} + +int APS5_VABI sceCesUtf32leToSJis(const uint32_t* utf32, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + return utf32ToJapanese(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); +} + +int APS5_VABI sceCesUcs2ToEucJp(uint16_t ucs2, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToJapanese(isEucJp, ucs2, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf8ToEucJp(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToJapanese(isEucJp, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToJapanese(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToJapanese(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToJapanese(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToEucJp(uint32_t utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToJapanese(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32beToEucJp(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToJapanese(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32leToEucJp(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToJapanese(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesSJisCodeToJisX0208(uint16_t sjisCode, uint8_t* ku, uint8_t* ten) { + if (!ku || !ten) return CES_ERROR_INVALID_DST_BUFFER; + int row = 0; + int cell = 0; + if (!sjisToJis(static_cast(sjisCode >> 8), static_cast(sjisCode), row, cell) || row > 94) return CES_ERROR_INVALID_ENCODE; + *ku = static_cast(row); + *ten = static_cast(cell); + return 0; +} + +int APS5_VABI sceCesJisX0208ToSJisCode(uint8_t ku, uint8_t ten, uint16_t* sjisCode) { + if (!sjisCode) return CES_ERROR_INVALID_DST_BUFFER; + if (ku < 1 || ku > 94 || ten < 1 || ten > 94) return CES_ERROR_INVALID_ENCODE; + uint8_t lead = 0; + uint8_t trail = 0; + jisToSjis(ku, ten, lead, trail); + *sjisCode = static_cast(lead << 8 | trail); + return 0; +} + } diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 165fe278e8..22641831e7 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -82,36 +82,6 @@ int APS5_VABI sceCesEucCnUcsProfileSetSbcs(void) { return 0; } -int APS5_VABI sceCesEucJpToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucJpToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucJpToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucJpToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucJpToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucJpToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesEucJpUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -272,11 +242,6 @@ int APS5_VABI sceCesJisGetLevel(void) { return 0; } -int APS5_VABI sceCesJisX0208ToSJisCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesJisX0213ToSJisCode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -622,11 +587,6 @@ int APS5_VABI sceCesRefersUcsProfileKsX1003Tilde0x7e(void) { return 0; } -int APS5_VABI sceCesSJisCodeToJisX0208(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesSJisCodeToJisX0213(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -642,46 +602,6 @@ int APS5_VABI sceCesSJisPutCode(void) { return 0; } -int APS5_VABI sceCesSJisToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisToUtf8(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesSJisUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -742,11 +662,6 @@ int APS5_VABI sceCesUcs2ToEucCn(void) { return 0; } -int APS5_VABI sceCesUcs2ToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -762,11 +677,6 @@ int APS5_VABI sceCesUcs2ToMbc(void) { return 0; } -int APS5_VABI sceCesUcs2ToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -917,11 +827,6 @@ int APS5_VABI sceCesUtf16ToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf16ToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -937,11 +842,6 @@ int APS5_VABI sceCesUtf16ToMbc(void) { return 0; } -int APS5_VABI sceCesUtf16ToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -962,11 +862,6 @@ int APS5_VABI sceCesUtf16beToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf16beToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -982,11 +877,6 @@ int APS5_VABI sceCesUtf16beToMbc(void) { return 0; } -int APS5_VABI sceCesUtf16beToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1007,11 +897,6 @@ int APS5_VABI sceCesUtf16leToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf16leToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1027,11 +912,6 @@ int APS5_VABI sceCesUtf16leToMbc(void) { return 0; } -int APS5_VABI sceCesUtf16leToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1072,11 +952,6 @@ int APS5_VABI sceCesUtf32ToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf32ToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1092,11 +967,6 @@ int APS5_VABI sceCesUtf32ToMbc(void) { return 0; } -int APS5_VABI sceCesUtf32ToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1117,11 +987,6 @@ int APS5_VABI sceCesUtf32beToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf32beToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1137,11 +1002,6 @@ int APS5_VABI sceCesUtf32beToMbc(void) { return 0; } -int APS5_VABI sceCesUtf32beToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1162,11 +1022,6 @@ int APS5_VABI sceCesUtf32leToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf32leToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1182,11 +1037,6 @@ int APS5_VABI sceCesUtf32leToMbc(void) { return 0; } -int APS5_VABI sceCesUtf32leToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToSbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1217,11 +1067,6 @@ int APS5_VABI sceCesUtf8ToEucCn(void) { return 0; } -int APS5_VABI sceCesUtf8ToEucJp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1237,11 +1082,6 @@ int APS5_VABI sceCesUtf8ToMbc(void) { return 0; } -int APS5_VABI sceCesUtf8ToSJis(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp new file mode 100644 index 0000000000..2103b87187 --- /dev/null +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -0,0 +1,170 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); +int APS5_VABI sceCesSJisToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesSJisToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSJisToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSJisToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSJisToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSJisToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesSJisToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesSJisToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucJpToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucJpToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucJpToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucJpToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUcs2ToSJis(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToEucJp(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToSJis(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToEucJp(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToSJis(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToEucJp(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToSJis(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToSJis(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToEucJp(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSJisCodeToJisX0208(std::uint16_t, std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesJisX0208ToSJisCode(std::uint8_t, std::uint8_t, std::uint16_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } + +int main() { + constexpr int invalidParameter = static_cast(0x805C0001); + constexpr int invalidSrcBuffer = static_cast(0x805C0010); + constexpr int srcBufferEnd = static_cast(0x805C0011); + constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int unassignedCode = static_cast(0x805C0020); + constexpr int invalidDstBuffer = static_cast(0x805C0030); + constexpr int dstBufferEnd = static_cast(0x805C0031); + + alignas(8) std::uint8_t sjisSheet[64] = {}; + alignas(8) std::uint8_t eucSheet[64] = {}; + const void* sjis = sceCesUcsProfileInitSJis1997Cp932(sjisSheet); + const void* euc = sceCesUcsProfileInitEucJpCp51932(eucSheet); + Require(sjis != nullptr && euc != nullptr); + + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + std::uint16_t ucs2 = 0; + Require(sceCesSJisToUcs2(sjis, Bytes("\x82\xA0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x3042); + Require(sceCesSJisToUcs2(sjis, Bytes("\xB1"), 1, &consumed, &ucs2) == 0 && consumed == 1 && ucs2 == 0xFF71); + Require(sceCesSJisToUcs2(sjis, Bytes("A"), 1, &consumed, &ucs2) == 0 && consumed == 1 && ucs2 == 'A'); + Require(sceCesSJisToUcs2(sjis, Bytes("\x81\x60"), 2, &consumed, &ucs2) == 0 && ucs2 == 0xFF5E); + Require(sceCesSJisToUcs2(sjis, Bytes("\xFA\x40"), 2, &consumed, &ucs2) == 0 && ucs2 == 0x2170); + Require(sceCesSJisToUcs2(euc, Bytes("\x82\xA0"), 2, &consumed, &ucs2) == invalidParameter); + Require(sceCesSJisToUcs2(sjis, nullptr, 2, &consumed, &ucs2) == invalidSrcBuffer); + Require(sceCesSJisToUcs2(sjis, Bytes("\x82\xA0"), 0, &consumed, &ucs2) == srcBufferEnd); + Require(sceCesSJisToUcs2(sjis, Bytes("\x82\xA0"), 1, &consumed, &ucs2) == srcBufferEnd); + Require(sceCesSJisToUcs2(sjis, Bytes("\x82\xA0"), 2, &consumed, nullptr) == invalidDstBuffer); + Require(sceCesSJisToUcs2(sjis, Bytes("\x82\x7F"), 2, &consumed, &ucs2) == invalidEncode); + Require(sceCesSJisToUcs2(sjis, Bytes("\xF0\x40"), 2, &consumed, &ucs2) == unassignedCode); + + std::uint8_t utf8[4] = {}; + Require(sceCesSJisToUtf8(sjis, Bytes("\x93\xFA"), 2, &consumed, utf8, sizeof(utf8), &produced) == 0); + Require(consumed == 2 && produced == 3 && std::memcmp(utf8, "\xE6\x97\xA5", 3) == 0); + Require(sceCesSJisToUtf8(sjis, Bytes("\x93\xFA"), 2, &consumed, utf8, 2, &produced) == dstBufferEnd); + + std::uint16_t utf16 = 0; + Require(sceCesSJisToUtf16(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf16, 1, &produced) == 0 && utf16 == 0x3042 && produced == 1); + Require(sceCesSJisToUtf16(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf16, 0, &produced) == dstBufferEnd); + Require(sceCesSJisToUtf16be(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf16, 1, &produced) == 0); + Require(std::memcmp(&utf16, "\x30\x42", 2) == 0); + Require(sceCesSJisToUtf16le(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf16, 1, &produced) == 0); + Require(std::memcmp(&utf16, "\x42\x30", 2) == 0); + + std::uint32_t utf32 = 0; + Require(sceCesSJisToUtf32(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf32) == 0 && utf32 == 0x3042); + Require(sceCesSJisToUtf32be(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf32) == 0); + Require(std::memcmp(&utf32, "\x00\x00\x30\x42", 4) == 0); + Require(sceCesSJisToUtf32le(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf32) == 0); + Require(std::memcmp(&utf32, "\x42\x30\x00\x00", 4) == 0); + + Require(sceCesEucJpToUtf16(euc, Bytes("\xA4\xA2"), 2, &consumed, &utf16, 1, &produced) == 0 && consumed == 2 && utf16 == 0x3042); + Require(sceCesEucJpToUtf16be(euc, Bytes("\x8E\xB1"), 2, &consumed, &utf16, 1, &produced) == 0); + Require(std::memcmp(&utf16, "\xFF\x71", 2) == 0); + Require(sceCesEucJpToUtf32(euc, Bytes("\x8F\xB0\xA1"), 3, &consumed, &utf32) == 0 && consumed == 3 && utf32 == 0x4E02); + Require(sceCesEucJpToUtf32le(euc, Bytes("\xA4\xA2"), 2, &consumed, &utf32) == 0); + Require(std::memcmp(&utf32, "\x42\x30\x00\x00", 4) == 0); + Require(sceCesEucJpToUtf32(sjis, Bytes("\xA4\xA2"), 2, &consumed, &utf32) == invalidParameter); + + std::uint8_t mbcs[4] = {}; + Require(sceCesUcs2ToSJis(0x3042, sjis, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x82\xA0", 2) == 0); + Require(sceCesUcs2ToSJis(0xFF71, sjis, mbcs, sizeof(mbcs), &produced) == 0 && produced == 1 && mbcs[0] == 0xB1); + Require(sceCesUcs2ToSJis(0x3042, sjis, mbcs, 1, &produced) == dstBufferEnd); + Require(sceCesUcs2ToSJis(0xD800, sjis, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToSJis(0x4E02, sjis, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesUcs2ToSJis(0x3042, euc, mbcs, sizeof(mbcs), &produced) == invalidParameter); + Require(sceCesUcs2ToSJis(0x3042, sceCesRefersUcsProfileCp1252(), mbcs, sizeof(mbcs), &produced) == invalidParameter); + Require(sceCesUcs2ToSJis(0x3042, sjis, nullptr, sizeof(mbcs), &produced) == invalidDstBuffer); + Require(sceCesUcs2ToEucJp(0x4E02, euc, mbcs, sizeof(mbcs), &produced) == 0 && produced == 3 && std::memcmp(mbcs, "\x8F\xB0\xA1", 3) == 0); + Require(sceCesUcs2ToEucJp(0xFF71, euc, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x8E\xB1", 2) == 0); + + Require(sceCesUtf8ToSJis(Bytes("\xE6\x97\xA5"), 3, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == 0); + Require(consumed == 3 && produced == 2 && std::memcmp(mbcs, "\x93\xFA", 2) == 0); + Require(sceCesUtf8ToSJis(Bytes("\xE6\x97\xA5"), 2, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == srcBufferEnd); + Require(sceCesUtf8ToSJis(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesUtf8ToSJis(nullptr, 3, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == invalidSrcBuffer); + Require(sceCesUtf8ToEucJp(Bytes("\xE6\x97\xA5"), 3, &consumed, euc, mbcs, sizeof(mbcs), &produced) == 0); + Require(produced == 2 && std::memcmp(mbcs, "\xC6\xFC", 2) == 0); + + const std::uint16_t hiragana[] = {0x3042}; + const std::uint16_t pair[] = {0xD83D, 0xDE00}; + const std::uint16_t lone[] = {0xDC00}; + Require(sceCesUtf16ToSJis(hiragana, 1, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 1 && std::memcmp(mbcs, "\x82\xA0", 2) == 0); + Require(sceCesUtf16ToSJis(pair, 2, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesUtf16ToSJis(pair, 1, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == srcBufferEnd); + Require(sceCesUtf16ToSJis(lone, 1, &consumed, sjis, mbcs, sizeof(mbcs), &produced) == invalidEncode); + std::uint16_t hiraganaBe = 0; + std::memcpy(&hiraganaBe, "\x30\x42", 2); + Require(sceCesUtf16beToEucJp(&hiraganaBe, 1, &consumed, euc, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\xA4\xA2", 2) == 0); + + Require(sceCesUtf32ToSJis(0x65E5, sjis, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\x93\xFA", 2) == 0); + Require(sceCesUtf32ToSJis(0x110000, sjis, mbcs, sizeof(mbcs), &produced) == invalidEncode); + std::uint32_t utf32Be = 0; + std::memcpy(&utf32Be, "\x00\x00\x65\xE5", 4); + Require(sceCesUtf32beToSJis(&utf32Be, sjis, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\x93\xFA", 2) == 0); + Require(sceCesUtf32beToSJis(nullptr, sjis, mbcs, sizeof(mbcs), &produced) == invalidSrcBuffer); + std::uint32_t utf32Le = 0; + std::memcpy(&utf32Le, "\xE5\x65\x00\x00", 4); + Require(sceCesUtf32leToEucJp(&utf32Le, euc, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\xC6\xFC", 2) == 0); + + for (int code = 1; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::uint8_t encoded[4] = {}; + std::uint32_t size = 0; + if (sceCesUcs2ToSJis(static_cast(code), sjis, encoded, sizeof(encoded), &size) == 0) { + std::uint16_t back = 0; + Require(sceCesSJisToUcs2(sjis, encoded, size, &consumed, &back) == 0 && consumed == size && back == code); + } + } + + std::uint8_t ku = 0; + std::uint8_t ten = 0; + std::uint16_t sjisCode = 0; + Require(sceCesSJisCodeToJisX0208(0x82A0, &ku, &ten) == 0 && ku == 4 && ten == 2); + Require(sceCesSJisCodeToJisX0208(0x93FA, &ku, &ten) == 0 && ku == 38 && ten == 92); + Require(sceCesSJisCodeToJisX0208(0xEFFC, &ku, &ten) == 0 && ku == 94 && ten == 94); + Require(sceCesSJisCodeToJisX0208(0xF040, &ku, &ten) == invalidEncode); + Require(sceCesSJisCodeToJisX0208(0x0041, &ku, &ten) == invalidEncode); + Require(sceCesSJisCodeToJisX0208(0x827F, &ku, &ten) == invalidEncode); + Require(sceCesSJisCodeToJisX0208(0x82A0, nullptr, &ten) == invalidDstBuffer); + Require(sceCesJisX0208ToSJisCode(4, 2, &sjisCode) == 0 && sjisCode == 0x82A0); + Require(sceCesJisX0208ToSJisCode(0, 2, &sjisCode) == invalidEncode); + Require(sceCesJisX0208ToSJisCode(4, 95, &sjisCode) == invalidEncode); + Require(sceCesJisX0208ToSJisCode(4, 2, nullptr) == invalidDstBuffer); + for (int row = 1; row <= 94; ++row) { + for (int cell = 1; cell <= 94; ++cell) { + Require(sceCesJisX0208ToSJisCode(static_cast(row), static_cast(cell), &sjisCode) == 0); + Require(sceCesSJisCodeToJisX0208(sjisCode, &ku, &ten) == 0 && ku == row && ten == cell); + } + } + return 0; +} diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 14cef5e67e..47475ea60d 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -10,6 +10,7 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged +- [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 249 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 217 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure From 79fe0b903c38bab9acb8825e084f34de4381988f Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 09:54:38 +0200 Subject: [PATCH 0389/1360] feat(libSceCesCs): add UTF-16 and UTF-32 forms of the SBC and MBCS string conversions Implement sceCesSbcTo{Ucs2,Utf16,Utf32}* and their inverses, and the UTF-16 and UTF-32 MBCS string conversions and length queries, on the existing single-byte, CP932 and CP51932 profiles. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/CMakeLists.txt | 8 +- core/libs/prx/libSceCesCs/Export.cpp | 159 +++++++++++++++++++ core/libs/prx/libSceCesCs/PendingExports.cpp | 110 ------------- core/libs/tests/GuestCesWide.cpp | 140 ++++++++++++++++ docs/dev/debt/libSceCesCs.md | 3 +- 5 files changed, 306 insertions(+), 114 deletions(-) create mode 100644 core/libs/tests/GuestCesWide.cpp diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index c1f3d0b0a7..16f1eeb233 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -25,8 +25,10 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) - foreach(variant IN ITEMS unicode unicode_module japanese japanese_module) - if(variant MATCHES "^japanese") + foreach(variant IN ITEMS unicode unicode_module japanese japanese_module wide wide_module) + if(variant MATCHES "^wide") + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesWide.cpp) + elseif(variant MATCHES "^japanese") add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesJapanese.cpp) else() add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) @@ -40,7 +42,7 @@ if(BUILD_TESTING) add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) endforeach() if(WIN32) - set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module guest_ces_wide guest_ces_wide_module PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 7d7d1593d5..63b2ac4ec3 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -654,6 +654,70 @@ int bufferToJapanese(bool (*matches)(const void*), const Source* source, uint32_ return 0; } +int decodeUtf32(const uint32_t* utf32, uint32_t, uint32_t& length, uint32_t& code) { + length = 1; + code = utf32[0]; + return isScalar(code) ? 0 : CES_ERROR_INVALID_ENCODE; +} + +uint32_t encodeUtf32(uint32_t code, uint32_t* utf32) { + utf32[0] = code; + return 1; +} + +int sbcToUnit(const uint8_t* profile, uint8_t sbc, const void* target, uint32_t& code) { + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!target) return CES_ERROR_INVALID_DST_BUFFER; + code = sbcToUnicode(profile, sbc); + return 0; +} + +template +int sbcToUtf16(const uint8_t* profile, uint8_t sbc, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { + uint32_t code = 0; + if (const int result = sbcToUnit(profile, sbc, utf16, code); result != 0) return result; + if (utf16max < 1) return CES_ERROR_DST_BUFFER_END; + storeUnit(utf16, code); + if (utf16Len) *utf16Len = 1; + return 0; +} + +template +int sbcToUtf32(const uint8_t* profile, uint8_t sbc, uint32_t* utf32) { + uint32_t code = 0; + if (const int result = sbcToUnit(profile, sbc, utf32, code); result != 0) return result; + storeUnit(utf32, code); + return 0; +} + +int scalarToSbc(uint32_t code, const uint8_t* profile, uint8_t* sbc) { + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; + if (!isScalar(code)) return CES_ERROR_INVALID_ENCODE; + return unicodeToSbc(profile, code, *sbc) ? 0 : CES_ERROR_UNASSIGNED_CODE; +} + +template +int utf16ToSbc(const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, const uint8_t* profile, uint8_t* sbc) { + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!utf16) return CES_ERROR_INVALID_SRC_BUFFER; + if (utf16max == 0) return CES_ERROR_SRC_BUFFER_END; + if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeUtf16(utf16, utf16max, length, code); result != 0) return result; + if (!unicodeToSbc(profile, code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; + if (utf16Len) *utf16Len = length; + return 0; +} + +template +int utf32ToSbc(const uint32_t* utf32, const uint8_t* profile, uint8_t* sbc) { + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!utf32) return CES_ERROR_INVALID_SRC_BUFFER; + return scalarToSbc(loadUnit(utf32), profile, sbc); +} + } extern "C" { @@ -950,4 +1014,99 @@ int APS5_VABI sceCesJisX0208ToSJisCode(uint8_t ku, uint8_t ten, uint16_t* sjisCo return 0; } +int APS5_VABI sceCesSbcToUcs2(const uint8_t* profile, uint8_t sbc, uint16_t* ucs2) { + uint32_t code = 0; + if (const int result = sbcToUnit(profile, sbc, ucs2, code); result != 0) return result; + *ucs2 = static_cast(code); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16(const uint8_t* profile, uint8_t sbc, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { + return sbcToUtf16(profile, sbc, utf16, utf16max, utf16Len); +} + +int APS5_VABI sceCesSbcToUtf16be(const uint8_t* profile, uint8_t sbc, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { + return sbcToUtf16(profile, sbc, utf16, utf16max, utf16Len); +} + +int APS5_VABI sceCesSbcToUtf16le(const uint8_t* profile, uint8_t sbc, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { + return sbcToUtf16(profile, sbc, utf16, utf16max, utf16Len); +} + +int APS5_VABI sceCesSbcToUtf32(const uint8_t* profile, uint8_t sbc, uint32_t* utf32) { + return sbcToUtf32(profile, sbc, utf32); +} + +int APS5_VABI sceCesSbcToUtf32be(const uint8_t* profile, uint8_t sbc, uint32_t* utf32) { + return sbcToUtf32(profile, sbc, utf32); +} + +int APS5_VABI sceCesSbcToUtf32le(const uint8_t* profile, uint8_t sbc, uint32_t* utf32) { + return sbcToUtf32(profile, sbc, utf32); +} + +int APS5_VABI sceCesUcs2ToSbc(uint16_t ucs2, const uint8_t* profile, uint8_t* sbc) { + return scalarToSbc(ucs2, profile, sbc); +} + +int APS5_VABI sceCesUtf16ToSbc(const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, const uint8_t* profile, uint8_t* sbc) { + return utf16ToSbc(utf16, utf16max, utf16Len, profile, sbc); +} + +int APS5_VABI sceCesUtf16beToSbc(const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, const uint8_t* profile, uint8_t* sbc) { + return utf16ToSbc(utf16, utf16max, utf16Len, profile, sbc); +} + +int APS5_VABI sceCesUtf16leToSbc(const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, const uint8_t* profile, uint8_t* sbc) { + return utf16ToSbc(utf16, utf16max, utf16Len, profile, sbc); +} + +int APS5_VABI sceCesUtf32ToSbc(uint32_t utf32, const uint8_t* profile, uint8_t* sbc) { + return scalarToSbc(utf32, profile, sbc); +} + +int APS5_VABI sceCesUtf32beToSbc(const uint32_t* utf32, const uint8_t* profile, uint8_t* sbc) { + return utf32ToSbc(utf32, profile, sbc); +} + +int APS5_VABI sceCesUtf32leToSbc(const uint32_t* utf32, const uint8_t* profile, uint8_t* sbc) { + return utf32ToSbc(utf32, profile, sbc); +} + +int APS5_VABI sceCesUtf16StrToMbcsStr(void* context, const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + return toMbcs(context, utf16, utf16max, utf16Len, mbcs, mbcsMax, mbcsLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf16StrGetMbcsLen(void* context, const uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len, uint32_t* mbcsLen) { + return toMbcs(context, utf16, utf16max, utf16Len, static_cast(nullptr), 0, mbcsLen, decodeUtf16); +} + +int APS5_VABI sceCesMbcsStrToUtf16Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { + if (!utf16) return CES_ERROR_INVALID_DST_BUFFER; + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, encodeUcs2); +} + +int APS5_VABI sceCesMbcsStrGetUtf16Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf16Len) { + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf16Len, encodeUcs2); +} + +int APS5_VABI sceCesUtf32StrToMbcsStr(void* context, const uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + return toMbcs(context, utf32, utf32max, utf32Len, mbcs, mbcsMax, mbcsLen, decodeUtf32); +} + +int APS5_VABI sceCesUtf32StrGetMbcsLen(void* context, const uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len, uint32_t* mbcsLen) { + return toMbcs(context, utf32, utf32max, utf32Len, static_cast(nullptr), 0, mbcsLen, decodeUtf32); +} + +int APS5_VABI sceCesMbcsStrToUtf32Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len) { + if (!utf32) return CES_ERROR_INVALID_DST_BUFFER; + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf32, utf32max, utf32Len, encodeUtf32); +} + +int APS5_VABI sceCesMbcsStrGetUtf32Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf32Len) { + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf32Len, encodeUtf32); +} + } diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 22641831e7..a4aa4d1291 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -292,26 +292,6 @@ int APS5_VABI sceCesMbcToUtf8(void) { return 0; } -int APS5_VABI sceCesMbcsStrGetUtf16Len(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcsStrGetUtf32Len(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcsStrToUtf16Str(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcsStrToUtf32Str(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesMbcsUcsContextInitCopy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -607,41 +587,6 @@ int APS5_VABI sceCesSJisUcsProfileSetSbcs(void) { return 0; } -int APS5_VABI sceCesSbcToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSbcToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSbcToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSbcToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSbcToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSbcToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSbcToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -677,11 +622,6 @@ int APS5_VABI sceCesUcs2ToMbc(void) { return 0; } -int APS5_VABI sceCesUcs2ToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -802,21 +742,11 @@ int APS5_VABI sceCesUtf16StrGetIso2022Len(void) { return 0; } -int APS5_VABI sceCesUtf16StrGetMbcsLen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16StrToIso2022Str(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf16StrToMbcsStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToBig5(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -842,11 +772,6 @@ int APS5_VABI sceCesUtf16ToMbc(void) { return 0; } -int APS5_VABI sceCesUtf16ToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -877,11 +802,6 @@ int APS5_VABI sceCesUtf16beToMbc(void) { return 0; } -int APS5_VABI sceCesUtf16beToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -912,11 +832,6 @@ int APS5_VABI sceCesUtf16leToMbc(void) { return 0; } -int APS5_VABI sceCesUtf16leToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -927,21 +842,11 @@ int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { return 0; } -int APS5_VABI sceCesUtf32StrGetMbcsLen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32StrToIso2022Str(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf32StrToMbcsStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToBig5(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -967,11 +872,6 @@ int APS5_VABI sceCesUtf32ToMbc(void) { return 0; } -int APS5_VABI sceCesUtf32ToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1002,11 +902,6 @@ int APS5_VABI sceCesUtf32beToMbc(void) { return 0; } -int APS5_VABI sceCesUtf32beToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1037,11 +932,6 @@ int APS5_VABI sceCesUtf32leToMbc(void) { return 0; } -int APS5_VABI sceCesUtf32leToSbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToUhc(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesWide.cpp b/core/libs/tests/GuestCesWide.cpp new file mode 100644 index 0000000000..5d840cbdfe --- /dev/null +++ b/core/libs/tests/GuestCesWide.cpp @@ -0,0 +1,140 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +int APS5_VABI sceCesSbcToUcs2(const std::uint8_t*, std::uint8_t, std::uint16_t*); +int APS5_VABI sceCesSbcToUtf16(const std::uint8_t*, std::uint8_t, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSbcToUtf16be(const std::uint8_t*, std::uint8_t, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSbcToUtf16le(const std::uint8_t*, std::uint8_t, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSbcToUtf32(const std::uint8_t*, std::uint8_t, std::uint32_t*); +int APS5_VABI sceCesSbcToUtf32be(const std::uint8_t*, std::uint8_t, std::uint32_t*); +int APS5_VABI sceCesSbcToUtf32le(const std::uint8_t*, std::uint8_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToSbc(std::uint16_t, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf16ToSbc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf16beToSbc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf16leToSbc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf32ToSbc(std::uint32_t, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf32beToSbc(const std::uint32_t*, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf32leToSbc(const std::uint32_t*, const std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesUtf16StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16StrGetMbcsLen(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUtf32StrToMbcsStr(void*, const std::uint32_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32StrGetMbcsLen(void*, const std::uint32_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf16Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrGetUtf16Len(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf32Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrGetUtf32Len(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } + +int main() { + constexpr int invalidParameter = static_cast(0x805C0001); + constexpr int invalidSrcBuffer = static_cast(0x805C0010); + constexpr int srcBufferEnd = static_cast(0x805C0011); + constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int unassignedCode = static_cast(0x805C0020); + constexpr int invalidDstBuffer = static_cast(0x805C0030); + constexpr int dstBufferEnd = static_cast(0x805C0031); + + const std::uint8_t* cp1252 = sceCesRefersUcsProfileCp1252(); + const std::uint8_t* cp1251 = sceCesRefersUcsProfileCp1251(); + alignas(8) std::uint8_t sjisSheet[64] = {}; + const void* sjis = sceCesUcsProfileInitSJis1997Cp932(sjisSheet); + + std::uint16_t ucs2 = 0; + std::uint16_t utf16 = 0; + std::uint32_t utf32 = 0; + std::uint32_t length = 0; + Require(sceCesSbcToUcs2(cp1252, 0x80, &ucs2) == 0 && ucs2 == 0x20AC); + Require(sceCesSbcToUcs2(cp1251, 0xC0, &ucs2) == 0 && ucs2 == 0x0410); + Require(sceCesSbcToUcs2(static_cast(sjis), 0x41, &ucs2) == invalidParameter); + Require(sceCesSbcToUcs2(cp1252, 0x41, nullptr) == invalidDstBuffer); + Require(sceCesSbcToUtf16(cp1252, 0x80, &utf16, 1, &length) == 0 && utf16 == 0x20AC && length == 1); + Require(sceCesSbcToUtf16(cp1252, 0x80, &utf16, 0, &length) == dstBufferEnd); + Require(sceCesSbcToUtf16be(cp1252, 0x80, &utf16, 1, &length) == 0 && std::memcmp(&utf16, "\x20\xAC", 2) == 0); + Require(sceCesSbcToUtf16le(cp1252, 0x80, &utf16, 1, &length) == 0 && std::memcmp(&utf16, "\xAC\x20", 2) == 0); + Require(sceCesSbcToUtf32(cp1252, 0x9F, &utf32) == 0 && utf32 == 0x0178); + Require(sceCesSbcToUtf32be(cp1252, 0x9F, &utf32) == 0 && std::memcmp(&utf32, "\x00\x00\x01\x78", 4) == 0); + Require(sceCesSbcToUtf32le(cp1252, 0x9F, &utf32) == 0 && std::memcmp(&utf32, "\x78\x01\x00\x00", 4) == 0); + + std::uint8_t sbc = 0; + Require(sceCesUcs2ToSbc(0x20AC, cp1252, &sbc) == 0 && sbc == 0x80); + Require(sceCesUcs2ToSbc(0x0410, cp1251, &sbc) == 0 && sbc == 0xC0); + Require(sceCesUcs2ToSbc(0x3042, cp1252, &sbc) == unassignedCode); + Require(sceCesUcs2ToSbc(0xD800, cp1252, &sbc) == invalidEncode); + Require(sceCesUcs2ToSbc(0x41, cp1252, nullptr) == invalidDstBuffer); + Require(sceCesUcs2ToSbc(0x41, static_cast(sjis), &sbc) == invalidParameter); + const std::uint16_t euro[] = {0x20AC}; + const std::uint16_t pair[] = {0xD83D, 0xDE00}; + Require(sceCesUtf16ToSbc(euro, 1, &length, cp1252, &sbc) == 0 && sbc == 0x80 && length == 1); + Require(sceCesUtf16ToSbc(pair, 2, &length, cp1252, &sbc) == unassignedCode); + Require(sceCesUtf16ToSbc(pair, 1, &length, cp1252, &sbc) == srcBufferEnd); + Require(sceCesUtf16ToSbc(euro, 0, &length, cp1252, &sbc) == srcBufferEnd); + Require(sceCesUtf16ToSbc(nullptr, 1, &length, cp1252, &sbc) == invalidSrcBuffer); + std::uint16_t euroBe = 0; + std::memcpy(&euroBe, "\x20\xAC", 2); + Require(sceCesUtf16beToSbc(&euroBe, 1, &length, cp1252, &sbc) == 0 && sbc == 0x80); + std::uint16_t euroLe = 0; + std::memcpy(&euroLe, "\xAC\x20", 2); + Require(sceCesUtf16leToSbc(&euroLe, 1, &length, cp1252, &sbc) == 0 && sbc == 0x80); + Require(sceCesUtf32ToSbc(0x0178, cp1252, &sbc) == 0 && sbc == 0x9F); + Require(sceCesUtf32ToSbc(0x110000, cp1252, &sbc) == invalidEncode); + std::uint32_t yBe = 0; + std::memcpy(&yBe, "\x00\x00\x01\x78", 4); + Require(sceCesUtf32beToSbc(&yBe, cp1252, &sbc) == 0 && sbc == 0x9F); + std::uint32_t yLe = 0; + std::memcpy(&yLe, "\x78\x01\x00\x00", 4); + Require(sceCesUtf32leToSbc(&yLe, cp1252, &sbc) == 0 && sbc == 0x9F); + Require(sceCesUtf32leToSbc(nullptr, cp1252, &sbc) == invalidSrcBuffer); + for (int value = 0; value < 256; ++value) { + Require(sceCesSbcToUtf32(cp1251, static_cast(value), &utf32) == 0); + Require(sceCesUtf32ToSbc(utf32, cp1251, &sbc) == 0 && sbc == value); + } + + alignas(8) std::uint8_t context[64] = {}; + Require(sceCesMbcsUcsContextInit(context, sjis) == 0); + const std::uint16_t wide[] = {0x3042, 'A', 0xFF71, 0}; + std::uint8_t mbcs[8] = {}; + std::uint32_t mbcsLength = 0; + Require(sceCesUtf16StrToMbcsStr(context, wide, 4, &length, mbcs, sizeof(mbcs), &mbcsLength) == 0); + Require(length == 3 && mbcsLength == 4 && std::memcmp(mbcs, "\x82\xA0" "A\xB1", 4) == 0); + Require(sceCesUtf16StrGetMbcsLen(context, wide, 4, &length, &mbcsLength) == 0 && length == 3 && mbcsLength == 4); + Require(sceCesUtf16StrToMbcsStr(context, wide, 4, &length, mbcs, 3, &mbcsLength) == dstBufferEnd); + Require(sceCesUtf16StrToMbcsStr(context, wide, 4, &length, nullptr, 3, &mbcsLength) == invalidDstBuffer); + Require(sceCesUtf16StrToMbcsStr(context, pair, 2, &length, mbcs, sizeof(mbcs), &mbcsLength) == unassignedCode); + const std::uint32_t wide32[] = {0x65E5, 'B', 0}; + Require(sceCesUtf32StrToMbcsStr(context, wide32, 3, &length, mbcs, sizeof(mbcs), &mbcsLength) == 0); + Require(length == 2 && mbcsLength == 3 && std::memcmp(mbcs, "\x93\xFA" "B", 3) == 0); + Require(sceCesUtf32StrGetMbcsLen(context, wide32, 1, &length, &mbcsLength) == 0 && length == 1 && mbcsLength == 2); + const std::uint32_t bad32[] = {0xDC00}; + Require(sceCesUtf32StrGetMbcsLen(context, bad32, 1, &length, &mbcsLength) == invalidEncode); + + std::uint16_t utf16Out[4] = {}; + std::uint32_t outLength = 0; + Require(sceCesMbcsStrToUtf16Str(context, Bytes("\x82\xA0" "A\xB1"), 8, &length, utf16Out, 4, &outLength) == 0); + Require(length == 4 && outLength == 3 && utf16Out[0] == 0x3042 && utf16Out[1] == 'A' && utf16Out[2] == 0xFF71); + Require(sceCesMbcsStrGetUtf16Len(context, Bytes("\x82\xA0" "A"), 3, &length, &outLength) == 0 && length == 3 && outLength == 2); + Require(sceCesMbcsStrToUtf16Str(context, Bytes("\x82\xA0" "A"), 3, &length, utf16Out, 1, &outLength) == dstBufferEnd); + Require(sceCesMbcsStrToUtf16Str(context, Bytes("\x82"), 1, &length, utf16Out, 4, &outLength) == srcBufferEnd); + std::uint32_t utf32Out[4] = {}; + Require(sceCesMbcsStrToUtf32Str(context, Bytes("\x93\xFA" "B"), 3, &length, utf32Out, 4, &outLength) == 0); + Require(length == 3 && outLength == 2 && utf32Out[0] == 0x65E5 && utf32Out[1] == 'B'); + Require(sceCesMbcsStrToUtf32Str(context, Bytes("A"), 1, &length, nullptr, 4, &outLength) == invalidDstBuffer); + Require(sceCesMbcsStrGetUtf32Len(context, Bytes("AB"), 2, &length, &outLength) == 0 && outLength == 2); + Require(sceCesMbcsStrGetUtf32Len(nullptr, Bytes("AB"), 2, &length, &outLength) == invalidParameter); + + Require(sceCesMbcsUcsContextInit(context, cp1252) == 0); + Require(sceCesMbcsStrToUtf16Str(context, Bytes("\x80\xE9"), 2, &length, utf16Out, 4, &outLength) == 0); + Require(outLength == 2 && utf16Out[0] == 0x20AC && utf16Out[1] == 0x00E9); + return 0; +} diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 47475ea60d..7384c2ad94 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,6 +11,7 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements +- [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 217 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 195 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure From f974e91f08f70cdbceaeae32c4dd8650b3358b49 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 10:11:28 +0200 Subject: [PATCH 0390/1360] feat(libSceCesCs): convert GBK and EUC-CN through the CP936 table Add the GBK (CP936) and EUC-CN profiles, their scalar conversions to and from UTF-8, UTF-16, UTF-32 and UCS-2, and accept both profiles in the MBCS string conversions. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/CMakeLists.txt | 8 +- core/libs/prx/libSceCesCs/Cp936.hpp | 262 ++++++++++++++++ core/libs/prx/libSceCesCs/Export.cpp | 311 ++++++++++++++++--- core/libs/prx/libSceCesCs/PendingExports.cpp | 170 ---------- core/libs/tests/GuestCesChinese.cpp | 138 ++++++++ docs/dev/debt/libSceCesCs.md | 3 +- 6 files changed, 671 insertions(+), 221 deletions(-) create mode 100644 core/libs/prx/libSceCesCs/Cp936.hpp create mode 100644 core/libs/tests/GuestCesChinese.cpp diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 16f1eeb233..04182062c0 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -25,8 +25,10 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) - foreach(variant IN ITEMS unicode unicode_module japanese japanese_module wide wide_module) - if(variant MATCHES "^wide") + foreach(variant IN ITEMS unicode unicode_module japanese japanese_module wide wide_module chinese chinese_module) + if(variant MATCHES "^chinese") + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesChinese.cpp) + elseif(variant MATCHES "^wide") add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesWide.cpp) elseif(variant MATCHES "^japanese") add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesJapanese.cpp) @@ -42,7 +44,7 @@ if(BUILD_TESTING) add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) endforeach() if(WIN32) - set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module guest_ces_wide guest_ces_wide_module PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module guest_ces_wide guest_ces_wide_module guest_ces_chinese guest_ces_chinese_module PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceCesCs/Cp936.hpp b/core/libs/prx/libSceCesCs/Cp936.hpp new file mode 100644 index 0000000000..09e143d1d0 --- /dev/null +++ b/core/libs/prx/libSceCesCs/Cp936.hpp @@ -0,0 +1,262 @@ +#pragma once + +#include + +namespace Cp936 { + +inline constexpr uint16_t TO_UCS2[126 * 190] = { + 0x4E02, 0x4E04, 0x4E05, 0x4E06, 0x4E0F, 0x4E12, 0x4E17, 0x4E1F, 0x4E20, 0x4E21, 0x4E23, 0x4E26, 0x4E29, 0x4E2E, 0x4E2F, 0x4E31, 0x4E33, 0x4E35, 0x4E37, 0x4E3C, 0x4E40, 0x4E41, 0x4E42, 0x4E44, 0x4E46, 0x4E4A, 0x4E51, 0x4E55, 0x4E57, 0x4E5A, 0x4E5B, 0x4E62, 0x4E63, 0x4E64, 0x4E65, 0x4E67, 0x4E68, 0x4E6A, 0x4E6B, 0x4E6C, 0x4E6D, 0x4E6E, 0x4E6F, 0x4E72, 0x4E74, 0x4E75, 0x4E76, 0x4E77, 0x4E78, 0x4E79, 0x4E7A, 0x4E7B, 0x4E7C, 0x4E7D, 0x4E7F, 0x4E80, 0x4E81, 0x4E82, 0x4E83, 0x4E84, 0x4E85, 0x4E87, 0x4E8A, 0x4E90, 0x4E96, 0x4E97, 0x4E99, 0x4E9C, 0x4E9D, 0x4E9E, 0x4EA3, 0x4EAA, 0x4EAF, 0x4EB0, 0x4EB1, 0x4EB4, 0x4EB6, 0x4EB7, 0x4EB8, 0x4EB9, 0x4EBC, 0x4EBD, 0x4EBE, 0x4EC8, 0x4ECC, 0x4ECF, 0x4ED0, 0x4ED2, 0x4EDA, 0x4EDB, 0x4EDC, 0x4EE0, 0x4EE2, 0x4EE6, 0x4EE7, + 0x4EE9, 0x4EED, 0x4EEE, 0x4EEF, 0x4EF1, 0x4EF4, 0x4EF8, 0x4EF9, 0x4EFA, 0x4EFC, 0x4EFE, 0x4F00, 0x4F02, 0x4F03, 0x4F04, 0x4F05, 0x4F06, 0x4F07, 0x4F08, 0x4F0B, 0x4F0C, 0x4F12, 0x4F13, 0x4F14, 0x4F15, 0x4F16, 0x4F1C, 0x4F1D, 0x4F21, 0x4F23, 0x4F28, 0x4F29, 0x4F2C, 0x4F2D, 0x4F2E, 0x4F31, 0x4F33, 0x4F35, 0x4F37, 0x4F39, 0x4F3B, 0x4F3E, 0x4F3F, 0x4F40, 0x4F41, 0x4F42, 0x4F44, 0x4F45, 0x4F47, 0x4F48, 0x4F49, 0x4F4A, 0x4F4B, 0x4F4C, 0x4F52, 0x4F54, 0x4F56, 0x4F61, 0x4F62, 0x4F66, 0x4F68, 0x4F6A, 0x4F6B, 0x4F6D, 0x4F6E, 0x4F71, 0x4F72, 0x4F75, 0x4F77, 0x4F78, 0x4F79, 0x4F7A, 0x4F7D, 0x4F80, 0x4F81, 0x4F82, 0x4F85, 0x4F86, 0x4F87, 0x4F8A, 0x4F8C, 0x4F8E, 0x4F90, 0x4F92, 0x4F93, 0x4F95, 0x4F96, 0x4F98, 0x4F99, 0x4F9A, 0x4F9C, 0x4F9E, 0x4F9F, 0x4FA1, 0x4FA2, + 0x4FA4, 0x4FAB, 0x4FAD, 0x4FB0, 0x4FB1, 0x4FB2, 0x4FB3, 0x4FB4, 0x4FB6, 0x4FB7, 0x4FB8, 0x4FB9, 0x4FBA, 0x4FBB, 0x4FBC, 0x4FBD, 0x4FBE, 0x4FC0, 0x4FC1, 0x4FC2, 0x4FC6, 0x4FC7, 0x4FC8, 0x4FC9, 0x4FCB, 0x4FCC, 0x4FCD, 0x4FD2, 0x4FD3, 0x4FD4, 0x4FD5, 0x4FD6, 0x4FD9, 0x4FDB, 0x4FE0, 0x4FE2, 0x4FE4, 0x4FE5, 0x4FE7, 0x4FEB, 0x4FEC, 0x4FF0, 0x4FF2, 0x4FF4, 0x4FF5, 0x4FF6, 0x4FF7, 0x4FF9, 0x4FFB, 0x4FFC, 0x4FFD, 0x4FFF, 0x5000, 0x5001, 0x5002, 0x5003, 0x5004, 0x5005, 0x5006, 0x5007, 0x5008, 0x5009, 0x500A, 0x500B, 0x500E, 0x5010, 0x5011, 0x5013, 0x5015, 0x5016, 0x5017, 0x501B, 0x501D, 0x501E, 0x5020, 0x5022, 0x5023, 0x5024, 0x5027, 0x502B, 0x502F, 0x5030, 0x5031, 0x5032, 0x5033, 0x5034, 0x5035, 0x5036, 0x5037, 0x5038, 0x5039, 0x503B, 0x503D, 0x503F, 0x5040, + 0x5041, 0x5042, 0x5044, 0x5045, 0x5046, 0x5049, 0x504A, 0x504B, 0x504D, 0x5050, 0x5051, 0x5052, 0x5053, 0x5054, 0x5056, 0x5057, 0x5058, 0x5059, 0x505B, 0x505D, 0x505E, 0x505F, 0x5060, 0x5061, 0x5062, 0x5063, 0x5064, 0x5066, 0x5067, 0x5068, 0x5069, 0x506A, 0x506B, 0x506D, 0x506E, 0x506F, 0x5070, 0x5071, 0x5072, 0x5073, 0x5074, 0x5075, 0x5078, 0x5079, 0x507A, 0x507C, 0x507D, 0x5081, 0x5082, 0x5083, 0x5084, 0x5086, 0x5087, 0x5089, 0x508A, 0x508B, 0x508C, 0x508E, 0x508F, 0x5090, 0x5091, 0x5092, 0x5093, 0x5094, 0x5095, 0x5096, 0x5097, 0x5098, 0x5099, 0x509A, 0x509B, 0x509C, 0x509D, 0x509E, 0x509F, 0x50A0, 0x50A1, 0x50A2, 0x50A4, 0x50A6, 0x50AA, 0x50AB, 0x50AD, 0x50AE, 0x50AF, 0x50B0, 0x50B1, 0x50B3, 0x50B4, 0x50B5, 0x50B6, 0x50B7, 0x50B8, 0x50B9, 0x50BC, + 0x50BD, 0x50BE, 0x50BF, 0x50C0, 0x50C1, 0x50C2, 0x50C3, 0x50C4, 0x50C5, 0x50C6, 0x50C7, 0x50C8, 0x50C9, 0x50CA, 0x50CB, 0x50CC, 0x50CD, 0x50CE, 0x50D0, 0x50D1, 0x50D2, 0x50D3, 0x50D4, 0x50D5, 0x50D7, 0x50D8, 0x50D9, 0x50DB, 0x50DC, 0x50DD, 0x50DE, 0x50DF, 0x50E0, 0x50E1, 0x50E2, 0x50E3, 0x50E4, 0x50E5, 0x50E8, 0x50E9, 0x50EA, 0x50EB, 0x50EF, 0x50F0, 0x50F1, 0x50F2, 0x50F4, 0x50F6, 0x50F7, 0x50F8, 0x50F9, 0x50FA, 0x50FC, 0x50FD, 0x50FE, 0x50FF, 0x5100, 0x5101, 0x5102, 0x5103, 0x5104, 0x5105, 0x5108, 0x5109, 0x510A, 0x510C, 0x510D, 0x510E, 0x510F, 0x5110, 0x5111, 0x5113, 0x5114, 0x5115, 0x5116, 0x5117, 0x5118, 0x5119, 0x511A, 0x511B, 0x511C, 0x511D, 0x511E, 0x511F, 0x5120, 0x5122, 0x5123, 0x5124, 0x5125, 0x5126, 0x5127, 0x5128, 0x5129, 0x512A, 0x512B, + 0x512C, 0x512D, 0x512E, 0x512F, 0x5130, 0x5131, 0x5132, 0x5133, 0x5134, 0x5135, 0x5136, 0x5137, 0x5138, 0x5139, 0x513A, 0x513B, 0x513C, 0x513D, 0x513E, 0x5142, 0x5147, 0x514A, 0x514C, 0x514E, 0x514F, 0x5150, 0x5152, 0x5153, 0x5157, 0x5158, 0x5159, 0x515B, 0x515D, 0x515E, 0x515F, 0x5160, 0x5161, 0x5163, 0x5164, 0x5166, 0x5167, 0x5169, 0x516A, 0x516F, 0x5172, 0x517A, 0x517E, 0x517F, 0x5183, 0x5184, 0x5186, 0x5187, 0x518A, 0x518B, 0x518E, 0x518F, 0x5190, 0x5191, 0x5193, 0x5194, 0x5198, 0x519A, 0x519D, 0x519E, 0x519F, 0x51A1, 0x51A3, 0x51A6, 0x51A7, 0x51A8, 0x51A9, 0x51AA, 0x51AD, 0x51AE, 0x51B4, 0x51B8, 0x51B9, 0x51BA, 0x51BE, 0x51BF, 0x51C1, 0x51C2, 0x51C3, 0x51C5, 0x51C8, 0x51CA, 0x51CD, 0x51CE, 0x51D0, 0x51D2, 0x51D3, 0x51D4, 0x51D5, 0x51D6, 0x51D7, + 0x51D8, 0x51D9, 0x51DA, 0x51DC, 0x51DE, 0x51DF, 0x51E2, 0x51E3, 0x51E5, 0x51E6, 0x51E7, 0x51E8, 0x51E9, 0x51EA, 0x51EC, 0x51EE, 0x51F1, 0x51F2, 0x51F4, 0x51F7, 0x51FE, 0x5204, 0x5205, 0x5209, 0x520B, 0x520C, 0x520F, 0x5210, 0x5213, 0x5214, 0x5215, 0x521C, 0x521E, 0x521F, 0x5221, 0x5222, 0x5223, 0x5225, 0x5226, 0x5227, 0x522A, 0x522C, 0x522F, 0x5231, 0x5232, 0x5234, 0x5235, 0x523C, 0x523E, 0x5244, 0x5245, 0x5246, 0x5247, 0x5248, 0x5249, 0x524B, 0x524E, 0x524F, 0x5252, 0x5253, 0x5255, 0x5257, 0x5258, 0x5259, 0x525A, 0x525B, 0x525D, 0x525F, 0x5260, 0x5262, 0x5263, 0x5264, 0x5266, 0x5268, 0x526B, 0x526C, 0x526D, 0x526E, 0x5270, 0x5271, 0x5273, 0x5274, 0x5275, 0x5276, 0x5277, 0x5278, 0x5279, 0x527A, 0x527B, 0x527C, 0x527E, 0x5280, 0x5283, 0x5284, 0x5285, + 0x5286, 0x5287, 0x5289, 0x528A, 0x528B, 0x528C, 0x528D, 0x528E, 0x528F, 0x5291, 0x5292, 0x5294, 0x5295, 0x5296, 0x5297, 0x5298, 0x5299, 0x529A, 0x529C, 0x52A4, 0x52A5, 0x52A6, 0x52A7, 0x52AE, 0x52AF, 0x52B0, 0x52B4, 0x52B5, 0x52B6, 0x52B7, 0x52B8, 0x52B9, 0x52BA, 0x52BB, 0x52BC, 0x52BD, 0x52C0, 0x52C1, 0x52C2, 0x52C4, 0x52C5, 0x52C6, 0x52C8, 0x52CA, 0x52CC, 0x52CD, 0x52CE, 0x52CF, 0x52D1, 0x52D3, 0x52D4, 0x52D5, 0x52D7, 0x52D9, 0x52DA, 0x52DB, 0x52DC, 0x52DD, 0x52DE, 0x52E0, 0x52E1, 0x52E2, 0x52E3, 0x52E5, 0x52E6, 0x52E7, 0x52E8, 0x52E9, 0x52EA, 0x52EB, 0x52EC, 0x52ED, 0x52EE, 0x52EF, 0x52F1, 0x52F2, 0x52F3, 0x52F4, 0x52F5, 0x52F6, 0x52F7, 0x52F8, 0x52FB, 0x52FC, 0x52FD, 0x5301, 0x5302, 0x5303, 0x5304, 0x5307, 0x5309, 0x530A, 0x530B, 0x530C, 0x530E, + 0x5311, 0x5312, 0x5313, 0x5314, 0x5318, 0x531B, 0x531C, 0x531E, 0x531F, 0x5322, 0x5324, 0x5325, 0x5327, 0x5328, 0x5329, 0x532B, 0x532C, 0x532D, 0x532F, 0x5330, 0x5331, 0x5332, 0x5333, 0x5334, 0x5335, 0x5336, 0x5337, 0x5338, 0x533C, 0x533D, 0x5340, 0x5342, 0x5344, 0x5346, 0x534B, 0x534C, 0x534D, 0x5350, 0x5354, 0x5358, 0x5359, 0x535B, 0x535D, 0x5365, 0x5368, 0x536A, 0x536C, 0x536D, 0x5372, 0x5376, 0x5379, 0x537B, 0x537C, 0x537D, 0x537E, 0x5380, 0x5381, 0x5383, 0x5387, 0x5388, 0x538A, 0x538E, 0x538F, 0x5390, 0x5391, 0x5392, 0x5393, 0x5394, 0x5396, 0x5397, 0x5399, 0x539B, 0x539C, 0x539E, 0x53A0, 0x53A1, 0x53A4, 0x53A7, 0x53AA, 0x53AB, 0x53AC, 0x53AD, 0x53AF, 0x53B0, 0x53B1, 0x53B2, 0x53B3, 0x53B4, 0x53B5, 0x53B7, 0x53B8, 0x53B9, 0x53BA, 0x53BC, 0x53BD, + 0x53BE, 0x53C0, 0x53C3, 0x53C4, 0x53C5, 0x53C6, 0x53C7, 0x53CE, 0x53CF, 0x53D0, 0x53D2, 0x53D3, 0x53D5, 0x53DA, 0x53DC, 0x53DD, 0x53DE, 0x53E1, 0x53E2, 0x53E7, 0x53F4, 0x53FA, 0x53FE, 0x53FF, 0x5400, 0x5402, 0x5405, 0x5407, 0x540B, 0x5414, 0x5418, 0x5419, 0x541A, 0x541C, 0x5422, 0x5424, 0x5425, 0x542A, 0x5430, 0x5433, 0x5436, 0x5437, 0x543A, 0x543D, 0x543F, 0x5441, 0x5442, 0x5444, 0x5445, 0x5447, 0x5449, 0x544C, 0x544D, 0x544E, 0x544F, 0x5451, 0x545A, 0x545D, 0x545E, 0x545F, 0x5460, 0x5461, 0x5463, 0x5465, 0x5467, 0x5469, 0x546A, 0x546B, 0x546C, 0x546D, 0x546E, 0x546F, 0x5470, 0x5474, 0x5479, 0x547A, 0x547E, 0x547F, 0x5481, 0x5483, 0x5485, 0x5487, 0x5488, 0x5489, 0x548A, 0x548D, 0x5491, 0x5493, 0x5497, 0x5498, 0x549C, 0x549E, 0x549F, 0x54A0, 0x54A1, + 0x54A2, 0x54A5, 0x54AE, 0x54B0, 0x54B2, 0x54B5, 0x54B6, 0x54B7, 0x54B9, 0x54BA, 0x54BC, 0x54BE, 0x54C3, 0x54C5, 0x54CA, 0x54CB, 0x54D6, 0x54D8, 0x54DB, 0x54E0, 0x54E1, 0x54E2, 0x54E3, 0x54E4, 0x54EB, 0x54EC, 0x54EF, 0x54F0, 0x54F1, 0x54F4, 0x54F5, 0x54F6, 0x54F7, 0x54F8, 0x54F9, 0x54FB, 0x54FE, 0x5500, 0x5502, 0x5503, 0x5504, 0x5505, 0x5508, 0x550A, 0x550B, 0x550C, 0x550D, 0x550E, 0x5512, 0x5513, 0x5515, 0x5516, 0x5517, 0x5518, 0x5519, 0x551A, 0x551C, 0x551D, 0x551E, 0x551F, 0x5521, 0x5525, 0x5526, 0x5528, 0x5529, 0x552B, 0x552D, 0x5532, 0x5534, 0x5535, 0x5536, 0x5538, 0x5539, 0x553A, 0x553B, 0x553D, 0x5540, 0x5542, 0x5545, 0x5547, 0x5548, 0x554B, 0x554C, 0x554D, 0x554E, 0x554F, 0x5551, 0x5552, 0x5553, 0x5554, 0x5557, 0x5558, 0x5559, 0x555A, 0x555B, + 0x555D, 0x555E, 0x555F, 0x5560, 0x5562, 0x5563, 0x5568, 0x5569, 0x556B, 0x556F, 0x5570, 0x5571, 0x5572, 0x5573, 0x5574, 0x5579, 0x557A, 0x557D, 0x557F, 0x5585, 0x5586, 0x558C, 0x558D, 0x558E, 0x5590, 0x5592, 0x5593, 0x5595, 0x5596, 0x5597, 0x559A, 0x559B, 0x559E, 0x55A0, 0x55A1, 0x55A2, 0x55A3, 0x55A4, 0x55A5, 0x55A6, 0x55A8, 0x55A9, 0x55AA, 0x55AB, 0x55AC, 0x55AD, 0x55AE, 0x55AF, 0x55B0, 0x55B2, 0x55B4, 0x55B6, 0x55B8, 0x55BA, 0x55BC, 0x55BF, 0x55C0, 0x55C1, 0x55C2, 0x55C3, 0x55C6, 0x55C7, 0x55C8, 0x55CA, 0x55CB, 0x55CE, 0x55CF, 0x55D0, 0x55D5, 0x55D7, 0x55D8, 0x55D9, 0x55DA, 0x55DB, 0x55DE, 0x55E0, 0x55E2, 0x55E7, 0x55E9, 0x55ED, 0x55EE, 0x55F0, 0x55F1, 0x55F4, 0x55F6, 0x55F8, 0x55F9, 0x55FA, 0x55FB, 0x55FC, 0x55FF, 0x5602, 0x5603, 0x5604, 0x5605, + 0x5606, 0x5607, 0x560A, 0x560B, 0x560D, 0x5610, 0x5611, 0x5612, 0x5613, 0x5614, 0x5615, 0x5616, 0x5617, 0x5619, 0x561A, 0x561C, 0x561D, 0x5620, 0x5621, 0x5622, 0x5625, 0x5626, 0x5628, 0x5629, 0x562A, 0x562B, 0x562E, 0x562F, 0x5630, 0x5633, 0x5635, 0x5637, 0x5638, 0x563A, 0x563C, 0x563D, 0x563E, 0x5640, 0x5641, 0x5642, 0x5643, 0x5644, 0x5645, 0x5646, 0x5647, 0x5648, 0x5649, 0x564A, 0x564B, 0x564F, 0x5650, 0x5651, 0x5652, 0x5653, 0x5655, 0x5656, 0x565A, 0x565B, 0x565D, 0x565E, 0x565F, 0x5660, 0x5661, 0x5663, 0x5665, 0x5666, 0x5667, 0x566D, 0x566E, 0x566F, 0x5670, 0x5672, 0x5673, 0x5674, 0x5675, 0x5677, 0x5678, 0x5679, 0x567A, 0x567D, 0x567E, 0x567F, 0x5680, 0x5681, 0x5682, 0x5683, 0x5684, 0x5687, 0x5688, 0x5689, 0x568A, 0x568B, 0x568C, 0x568D, 0x5690, + 0x5691, 0x5692, 0x5694, 0x5695, 0x5696, 0x5697, 0x5698, 0x5699, 0x569A, 0x569B, 0x569C, 0x569D, 0x569E, 0x569F, 0x56A0, 0x56A1, 0x56A2, 0x56A4, 0x56A5, 0x56A6, 0x56A7, 0x56A8, 0x56A9, 0x56AA, 0x56AB, 0x56AC, 0x56AD, 0x56AE, 0x56B0, 0x56B1, 0x56B2, 0x56B3, 0x56B4, 0x56B5, 0x56B6, 0x56B8, 0x56B9, 0x56BA, 0x56BB, 0x56BD, 0x56BE, 0x56BF, 0x56C0, 0x56C1, 0x56C2, 0x56C3, 0x56C4, 0x56C5, 0x56C6, 0x56C7, 0x56C8, 0x56C9, 0x56CB, 0x56CC, 0x56CD, 0x56CE, 0x56CF, 0x56D0, 0x56D1, 0x56D2, 0x56D3, 0x56D5, 0x56D6, 0x56D8, 0x56D9, 0x56DC, 0x56E3, 0x56E5, 0x56E6, 0x56E7, 0x56E8, 0x56E9, 0x56EA, 0x56EC, 0x56EE, 0x56EF, 0x56F2, 0x56F3, 0x56F6, 0x56F7, 0x56F8, 0x56FB, 0x56FC, 0x5700, 0x5701, 0x5702, 0x5705, 0x5707, 0x570B, 0x570C, 0x570D, 0x570E, 0x570F, 0x5710, 0x5711, + 0x5712, 0x5713, 0x5714, 0x5715, 0x5716, 0x5717, 0x5718, 0x5719, 0x571A, 0x571B, 0x571D, 0x571E, 0x5720, 0x5721, 0x5722, 0x5724, 0x5725, 0x5726, 0x5727, 0x572B, 0x5731, 0x5732, 0x5734, 0x5735, 0x5736, 0x5737, 0x5738, 0x573C, 0x573D, 0x573F, 0x5741, 0x5743, 0x5744, 0x5745, 0x5746, 0x5748, 0x5749, 0x574B, 0x5752, 0x5753, 0x5754, 0x5755, 0x5756, 0x5758, 0x5759, 0x5762, 0x5763, 0x5765, 0x5767, 0x576C, 0x576E, 0x5770, 0x5771, 0x5772, 0x5774, 0x5775, 0x5778, 0x5779, 0x577A, 0x577D, 0x577E, 0x577F, 0x5780, 0x5781, 0x5787, 0x5788, 0x5789, 0x578A, 0x578D, 0x578E, 0x578F, 0x5790, 0x5791, 0x5794, 0x5795, 0x5796, 0x5797, 0x5798, 0x5799, 0x579A, 0x579C, 0x579D, 0x579E, 0x579F, 0x57A5, 0x57A8, 0x57AA, 0x57AC, 0x57AF, 0x57B0, 0x57B1, 0x57B3, 0x57B5, 0x57B6, 0x57B7, + 0x57B9, 0x57BA, 0x57BB, 0x57BC, 0x57BD, 0x57BE, 0x57BF, 0x57C0, 0x57C1, 0x57C4, 0x57C5, 0x57C6, 0x57C7, 0x57C8, 0x57C9, 0x57CA, 0x57CC, 0x57CD, 0x57D0, 0x57D1, 0x57D3, 0x57D6, 0x57D7, 0x57DB, 0x57DC, 0x57DE, 0x57E1, 0x57E2, 0x57E3, 0x57E5, 0x57E6, 0x57E7, 0x57E8, 0x57E9, 0x57EA, 0x57EB, 0x57EC, 0x57EE, 0x57F0, 0x57F1, 0x57F2, 0x57F3, 0x57F5, 0x57F6, 0x57F7, 0x57FB, 0x57FC, 0x57FE, 0x57FF, 0x5801, 0x5803, 0x5804, 0x5805, 0x5808, 0x5809, 0x580A, 0x580C, 0x580E, 0x580F, 0x5810, 0x5812, 0x5813, 0x5814, 0x5816, 0x5817, 0x5818, 0x581A, 0x581B, 0x581C, 0x581D, 0x581F, 0x5822, 0x5823, 0x5825, 0x5826, 0x5827, 0x5828, 0x5829, 0x582B, 0x582C, 0x582D, 0x582E, 0x582F, 0x5831, 0x5832, 0x5833, 0x5834, 0x5836, 0x5837, 0x5838, 0x5839, 0x583A, 0x583B, 0x583C, 0x583D, + 0x583E, 0x583F, 0x5840, 0x5841, 0x5842, 0x5843, 0x5845, 0x5846, 0x5847, 0x5848, 0x5849, 0x584A, 0x584B, 0x584E, 0x584F, 0x5850, 0x5852, 0x5853, 0x5855, 0x5856, 0x5857, 0x5859, 0x585A, 0x585B, 0x585C, 0x585D, 0x585F, 0x5860, 0x5861, 0x5862, 0x5863, 0x5864, 0x5866, 0x5867, 0x5868, 0x5869, 0x586A, 0x586D, 0x586E, 0x586F, 0x5870, 0x5871, 0x5872, 0x5873, 0x5874, 0x5875, 0x5876, 0x5877, 0x5878, 0x5879, 0x587A, 0x587B, 0x587C, 0x587D, 0x587F, 0x5882, 0x5884, 0x5886, 0x5887, 0x5888, 0x588A, 0x588B, 0x588C, 0x588D, 0x588E, 0x588F, 0x5890, 0x5891, 0x5894, 0x5895, 0x5896, 0x5897, 0x5898, 0x589B, 0x589C, 0x589D, 0x58A0, 0x58A1, 0x58A2, 0x58A3, 0x58A4, 0x58A5, 0x58A6, 0x58A7, 0x58AA, 0x58AB, 0x58AC, 0x58AD, 0x58AE, 0x58AF, 0x58B0, 0x58B1, 0x58B2, 0x58B3, 0x58B4, + 0x58B5, 0x58B6, 0x58B7, 0x58B8, 0x58B9, 0x58BA, 0x58BB, 0x58BD, 0x58BE, 0x58BF, 0x58C0, 0x58C2, 0x58C3, 0x58C4, 0x58C6, 0x58C7, 0x58C8, 0x58C9, 0x58CA, 0x58CB, 0x58CC, 0x58CD, 0x58CE, 0x58CF, 0x58D0, 0x58D2, 0x58D3, 0x58D4, 0x58D6, 0x58D7, 0x58D8, 0x58D9, 0x58DA, 0x58DB, 0x58DC, 0x58DD, 0x58DE, 0x58DF, 0x58E0, 0x58E1, 0x58E2, 0x58E3, 0x58E5, 0x58E6, 0x58E7, 0x58E8, 0x58E9, 0x58EA, 0x58ED, 0x58EF, 0x58F1, 0x58F2, 0x58F4, 0x58F5, 0x58F7, 0x58F8, 0x58FA, 0x58FB, 0x58FC, 0x58FD, 0x58FE, 0x58FF, 0x5900, 0x5901, 0x5903, 0x5905, 0x5906, 0x5908, 0x5909, 0x590A, 0x590B, 0x590C, 0x590E, 0x5910, 0x5911, 0x5912, 0x5913, 0x5917, 0x5918, 0x591B, 0x591D, 0x591E, 0x5920, 0x5921, 0x5922, 0x5923, 0x5926, 0x5928, 0x592C, 0x5930, 0x5932, 0x5933, 0x5935, 0x5936, 0x593B, + 0x593D, 0x593E, 0x593F, 0x5940, 0x5943, 0x5945, 0x5946, 0x594A, 0x594C, 0x594D, 0x5950, 0x5952, 0x5953, 0x5959, 0x595B, 0x595C, 0x595D, 0x595E, 0x595F, 0x5961, 0x5963, 0x5964, 0x5966, 0x5967, 0x5968, 0x5969, 0x596A, 0x596B, 0x596C, 0x596D, 0x596E, 0x596F, 0x5970, 0x5971, 0x5972, 0x5975, 0x5977, 0x597A, 0x597B, 0x597C, 0x597E, 0x597F, 0x5980, 0x5985, 0x5989, 0x598B, 0x598C, 0x598E, 0x598F, 0x5990, 0x5991, 0x5994, 0x5995, 0x5998, 0x599A, 0x599B, 0x599C, 0x599D, 0x599F, 0x59A0, 0x59A1, 0x59A2, 0x59A6, 0x59A7, 0x59AC, 0x59AD, 0x59B0, 0x59B1, 0x59B3, 0x59B4, 0x59B5, 0x59B6, 0x59B7, 0x59B8, 0x59BA, 0x59BC, 0x59BD, 0x59BF, 0x59C0, 0x59C1, 0x59C2, 0x59C3, 0x59C4, 0x59C5, 0x59C7, 0x59C8, 0x59C9, 0x59CC, 0x59CD, 0x59CE, 0x59CF, 0x59D5, 0x59D6, 0x59D9, 0x59DB, + 0x59DE, 0x59DF, 0x59E0, 0x59E1, 0x59E2, 0x59E4, 0x59E6, 0x59E7, 0x59E9, 0x59EA, 0x59EB, 0x59ED, 0x59EE, 0x59EF, 0x59F0, 0x59F1, 0x59F2, 0x59F3, 0x59F4, 0x59F5, 0x59F6, 0x59F7, 0x59F8, 0x59FA, 0x59FC, 0x59FD, 0x59FE, 0x5A00, 0x5A02, 0x5A0A, 0x5A0B, 0x5A0D, 0x5A0E, 0x5A0F, 0x5A10, 0x5A12, 0x5A14, 0x5A15, 0x5A16, 0x5A17, 0x5A19, 0x5A1A, 0x5A1B, 0x5A1D, 0x5A1E, 0x5A21, 0x5A22, 0x5A24, 0x5A26, 0x5A27, 0x5A28, 0x5A2A, 0x5A2B, 0x5A2C, 0x5A2D, 0x5A2E, 0x5A2F, 0x5A30, 0x5A33, 0x5A35, 0x5A37, 0x5A38, 0x5A39, 0x5A3A, 0x5A3B, 0x5A3D, 0x5A3E, 0x5A3F, 0x5A41, 0x5A42, 0x5A43, 0x5A44, 0x5A45, 0x5A47, 0x5A48, 0x5A4B, 0x5A4C, 0x5A4D, 0x5A4E, 0x5A4F, 0x5A50, 0x5A51, 0x5A52, 0x5A53, 0x5A54, 0x5A56, 0x5A57, 0x5A58, 0x5A59, 0x5A5B, 0x5A5C, 0x5A5D, 0x5A5E, 0x5A5F, 0x5A60, + 0x5A61, 0x5A63, 0x5A64, 0x5A65, 0x5A66, 0x5A68, 0x5A69, 0x5A6B, 0x5A6C, 0x5A6D, 0x5A6E, 0x5A6F, 0x5A70, 0x5A71, 0x5A72, 0x5A73, 0x5A78, 0x5A79, 0x5A7B, 0x5A7C, 0x5A7D, 0x5A7E, 0x5A80, 0x5A81, 0x5A82, 0x5A83, 0x5A84, 0x5A85, 0x5A86, 0x5A87, 0x5A88, 0x5A89, 0x5A8A, 0x5A8B, 0x5A8C, 0x5A8D, 0x5A8E, 0x5A8F, 0x5A90, 0x5A91, 0x5A93, 0x5A94, 0x5A95, 0x5A96, 0x5A97, 0x5A98, 0x5A99, 0x5A9C, 0x5A9D, 0x5A9E, 0x5A9F, 0x5AA0, 0x5AA1, 0x5AA2, 0x5AA3, 0x5AA4, 0x5AA5, 0x5AA6, 0x5AA7, 0x5AA8, 0x5AA9, 0x5AAB, 0x5AAC, 0x5AAD, 0x5AAE, 0x5AAF, 0x5AB0, 0x5AB1, 0x5AB4, 0x5AB6, 0x5AB7, 0x5AB9, 0x5ABA, 0x5ABB, 0x5ABC, 0x5ABD, 0x5ABF, 0x5AC0, 0x5AC3, 0x5AC4, 0x5AC5, 0x5AC6, 0x5AC7, 0x5AC8, 0x5ACA, 0x5ACB, 0x5ACD, 0x5ACE, 0x5ACF, 0x5AD0, 0x5AD1, 0x5AD3, 0x5AD5, 0x5AD7, 0x5AD9, + 0x5ADA, 0x5ADB, 0x5ADD, 0x5ADE, 0x5ADF, 0x5AE2, 0x5AE4, 0x5AE5, 0x5AE7, 0x5AE8, 0x5AEA, 0x5AEC, 0x5AED, 0x5AEE, 0x5AEF, 0x5AF0, 0x5AF2, 0x5AF3, 0x5AF4, 0x5AF5, 0x5AF6, 0x5AF7, 0x5AF8, 0x5AF9, 0x5AFA, 0x5AFB, 0x5AFC, 0x5AFD, 0x5AFE, 0x5AFF, 0x5B00, 0x5B01, 0x5B02, 0x5B03, 0x5B04, 0x5B05, 0x5B06, 0x5B07, 0x5B08, 0x5B0A, 0x5B0B, 0x5B0C, 0x5B0D, 0x5B0E, 0x5B0F, 0x5B10, 0x5B11, 0x5B12, 0x5B13, 0x5B14, 0x5B15, 0x5B18, 0x5B19, 0x5B1A, 0x5B1B, 0x5B1C, 0x5B1D, 0x5B1E, 0x5B1F, 0x5B20, 0x5B21, 0x5B22, 0x5B23, 0x5B24, 0x5B25, 0x5B26, 0x5B27, 0x5B28, 0x5B29, 0x5B2A, 0x5B2B, 0x5B2C, 0x5B2D, 0x5B2E, 0x5B2F, 0x5B30, 0x5B31, 0x5B33, 0x5B35, 0x5B36, 0x5B38, 0x5B39, 0x5B3A, 0x5B3B, 0x5B3C, 0x5B3D, 0x5B3E, 0x5B3F, 0x5B41, 0x5B42, 0x5B43, 0x5B44, 0x5B45, 0x5B46, 0x5B47, + 0x5B48, 0x5B49, 0x5B4A, 0x5B4B, 0x5B4C, 0x5B4D, 0x5B4E, 0x5B4F, 0x5B52, 0x5B56, 0x5B5E, 0x5B60, 0x5B61, 0x5B67, 0x5B68, 0x5B6B, 0x5B6D, 0x5B6E, 0x5B6F, 0x5B72, 0x5B74, 0x5B76, 0x5B77, 0x5B78, 0x5B79, 0x5B7B, 0x5B7C, 0x5B7E, 0x5B7F, 0x5B82, 0x5B86, 0x5B8A, 0x5B8D, 0x5B8E, 0x5B90, 0x5B91, 0x5B92, 0x5B94, 0x5B96, 0x5B9F, 0x5BA7, 0x5BA8, 0x5BA9, 0x5BAC, 0x5BAD, 0x5BAE, 0x5BAF, 0x5BB1, 0x5BB2, 0x5BB7, 0x5BBA, 0x5BBB, 0x5BBC, 0x5BC0, 0x5BC1, 0x5BC3, 0x5BC8, 0x5BC9, 0x5BCA, 0x5BCB, 0x5BCD, 0x5BCE, 0x5BCF, 0x5BD1, 0x5BD4, 0x5BD5, 0x5BD6, 0x5BD7, 0x5BD8, 0x5BD9, 0x5BDA, 0x5BDB, 0x5BDC, 0x5BE0, 0x5BE2, 0x5BE3, 0x5BE6, 0x5BE7, 0x5BE9, 0x5BEA, 0x5BEB, 0x5BEC, 0x5BED, 0x5BEF, 0x5BF1, 0x5BF2, 0x5BF3, 0x5BF4, 0x5BF5, 0x5BF6, 0x5BF7, 0x5BFD, 0x5BFE, 0x5C00, 0x5C02, + 0x5C03, 0x5C05, 0x5C07, 0x5C08, 0x5C0B, 0x5C0C, 0x5C0D, 0x5C0E, 0x5C10, 0x5C12, 0x5C13, 0x5C17, 0x5C19, 0x5C1B, 0x5C1E, 0x5C1F, 0x5C20, 0x5C21, 0x5C23, 0x5C26, 0x5C28, 0x5C29, 0x5C2A, 0x5C2B, 0x5C2D, 0x5C2E, 0x5C2F, 0x5C30, 0x5C32, 0x5C33, 0x5C35, 0x5C36, 0x5C37, 0x5C43, 0x5C44, 0x5C46, 0x5C47, 0x5C4C, 0x5C4D, 0x5C52, 0x5C53, 0x5C54, 0x5C56, 0x5C57, 0x5C58, 0x5C5A, 0x5C5B, 0x5C5C, 0x5C5D, 0x5C5F, 0x5C62, 0x5C64, 0x5C67, 0x5C68, 0x5C69, 0x5C6A, 0x5C6B, 0x5C6C, 0x5C6D, 0x5C70, 0x5C72, 0x5C73, 0x5C74, 0x5C75, 0x5C76, 0x5C77, 0x5C78, 0x5C7B, 0x5C7C, 0x5C7D, 0x5C7E, 0x5C80, 0x5C83, 0x5C84, 0x5C85, 0x5C86, 0x5C87, 0x5C89, 0x5C8A, 0x5C8B, 0x5C8E, 0x5C8F, 0x5C92, 0x5C93, 0x5C95, 0x5C9D, 0x5C9E, 0x5C9F, 0x5CA0, 0x5CA1, 0x5CA4, 0x5CA5, 0x5CA6, 0x5CA7, 0x5CA8, + 0x5CAA, 0x5CAE, 0x5CAF, 0x5CB0, 0x5CB2, 0x5CB4, 0x5CB6, 0x5CB9, 0x5CBA, 0x5CBB, 0x5CBC, 0x5CBE, 0x5CC0, 0x5CC2, 0x5CC3, 0x5CC5, 0x5CC6, 0x5CC7, 0x5CC8, 0x5CC9, 0x5CCA, 0x5CCC, 0x5CCD, 0x5CCE, 0x5CCF, 0x5CD0, 0x5CD1, 0x5CD3, 0x5CD4, 0x5CD5, 0x5CD6, 0x5CD7, 0x5CD8, 0x5CDA, 0x5CDB, 0x5CDC, 0x5CDD, 0x5CDE, 0x5CDF, 0x5CE0, 0x5CE2, 0x5CE3, 0x5CE7, 0x5CE9, 0x5CEB, 0x5CEC, 0x5CEE, 0x5CEF, 0x5CF1, 0x5CF2, 0x5CF3, 0x5CF4, 0x5CF5, 0x5CF6, 0x5CF7, 0x5CF8, 0x5CF9, 0x5CFA, 0x5CFC, 0x5CFD, 0x5CFE, 0x5CFF, 0x5D00, 0x5D01, 0x5D04, 0x5D05, 0x5D08, 0x5D09, 0x5D0A, 0x5D0B, 0x5D0C, 0x5D0D, 0x5D0F, 0x5D10, 0x5D11, 0x5D12, 0x5D13, 0x5D15, 0x5D17, 0x5D18, 0x5D19, 0x5D1A, 0x5D1C, 0x5D1D, 0x5D1F, 0x5D20, 0x5D21, 0x5D22, 0x5D23, 0x5D25, 0x5D28, 0x5D2A, 0x5D2B, 0x5D2C, 0x5D2F, + 0x5D30, 0x5D31, 0x5D32, 0x5D33, 0x5D35, 0x5D36, 0x5D37, 0x5D38, 0x5D39, 0x5D3A, 0x5D3B, 0x5D3C, 0x5D3F, 0x5D40, 0x5D41, 0x5D42, 0x5D43, 0x5D44, 0x5D45, 0x5D46, 0x5D48, 0x5D49, 0x5D4D, 0x5D4E, 0x5D4F, 0x5D50, 0x5D51, 0x5D52, 0x5D53, 0x5D54, 0x5D55, 0x5D56, 0x5D57, 0x5D59, 0x5D5A, 0x5D5C, 0x5D5E, 0x5D5F, 0x5D60, 0x5D61, 0x5D62, 0x5D63, 0x5D64, 0x5D65, 0x5D66, 0x5D67, 0x5D68, 0x5D6A, 0x5D6D, 0x5D6E, 0x5D70, 0x5D71, 0x5D72, 0x5D73, 0x5D75, 0x5D76, 0x5D77, 0x5D78, 0x5D79, 0x5D7A, 0x5D7B, 0x5D7C, 0x5D7D, 0x5D7E, 0x5D7F, 0x5D80, 0x5D81, 0x5D83, 0x5D84, 0x5D85, 0x5D86, 0x5D87, 0x5D88, 0x5D89, 0x5D8A, 0x5D8B, 0x5D8C, 0x5D8D, 0x5D8E, 0x5D8F, 0x5D90, 0x5D91, 0x5D92, 0x5D93, 0x5D94, 0x5D95, 0x5D96, 0x5D97, 0x5D98, 0x5D9A, 0x5D9B, 0x5D9C, 0x5D9E, 0x5D9F, 0x5DA0, + 0x5DA1, 0x5DA2, 0x5DA3, 0x5DA4, 0x5DA5, 0x5DA6, 0x5DA7, 0x5DA8, 0x5DA9, 0x5DAA, 0x5DAB, 0x5DAC, 0x5DAD, 0x5DAE, 0x5DAF, 0x5DB0, 0x5DB1, 0x5DB2, 0x5DB3, 0x5DB4, 0x5DB5, 0x5DB6, 0x5DB8, 0x5DB9, 0x5DBA, 0x5DBB, 0x5DBC, 0x5DBD, 0x5DBE, 0x5DBF, 0x5DC0, 0x5DC1, 0x5DC2, 0x5DC3, 0x5DC4, 0x5DC6, 0x5DC7, 0x5DC8, 0x5DC9, 0x5DCA, 0x5DCB, 0x5DCC, 0x5DCE, 0x5DCF, 0x5DD0, 0x5DD1, 0x5DD2, 0x5DD3, 0x5DD4, 0x5DD5, 0x5DD6, 0x5DD7, 0x5DD8, 0x5DD9, 0x5DDA, 0x5DDC, 0x5DDF, 0x5DE0, 0x5DE3, 0x5DE4, 0x5DEA, 0x5DEC, 0x5DED, 0x5DF0, 0x5DF5, 0x5DF6, 0x5DF8, 0x5DF9, 0x5DFA, 0x5DFB, 0x5DFC, 0x5DFF, 0x5E00, 0x5E04, 0x5E07, 0x5E09, 0x5E0A, 0x5E0B, 0x5E0D, 0x5E0E, 0x5E12, 0x5E13, 0x5E17, 0x5E1E, 0x5E1F, 0x5E20, 0x5E21, 0x5E22, 0x5E23, 0x5E24, 0x5E25, 0x5E28, 0x5E29, 0x5E2A, 0x5E2B, + 0x5E2C, 0x5E2F, 0x5E30, 0x5E32, 0x5E33, 0x5E34, 0x5E35, 0x5E36, 0x5E39, 0x5E3A, 0x5E3E, 0x5E3F, 0x5E40, 0x5E41, 0x5E43, 0x5E46, 0x5E47, 0x5E48, 0x5E49, 0x5E4A, 0x5E4B, 0x5E4D, 0x5E4E, 0x5E4F, 0x5E50, 0x5E51, 0x5E52, 0x5E53, 0x5E56, 0x5E57, 0x5E58, 0x5E59, 0x5E5A, 0x5E5C, 0x5E5D, 0x5E5F, 0x5E60, 0x5E63, 0x5E64, 0x5E65, 0x5E66, 0x5E67, 0x5E68, 0x5E69, 0x5E6A, 0x5E6B, 0x5E6C, 0x5E6D, 0x5E6E, 0x5E6F, 0x5E70, 0x5E71, 0x5E75, 0x5E77, 0x5E79, 0x5E7E, 0x5E81, 0x5E82, 0x5E83, 0x5E85, 0x5E88, 0x5E89, 0x5E8C, 0x5E8D, 0x5E8E, 0x5E92, 0x5E98, 0x5E9B, 0x5E9D, 0x5EA1, 0x5EA2, 0x5EA3, 0x5EA4, 0x5EA8, 0x5EA9, 0x5EAA, 0x5EAB, 0x5EAC, 0x5EAE, 0x5EAF, 0x5EB0, 0x5EB1, 0x5EB2, 0x5EB4, 0x5EBA, 0x5EBB, 0x5EBC, 0x5EBD, 0x5EBF, 0x5EC0, 0x5EC1, 0x5EC2, 0x5EC3, 0x5EC4, 0x5EC5, + 0x5EC6, 0x5EC7, 0x5EC8, 0x5ECB, 0x5ECC, 0x5ECD, 0x5ECE, 0x5ECF, 0x5ED0, 0x5ED4, 0x5ED5, 0x5ED7, 0x5ED8, 0x5ED9, 0x5EDA, 0x5EDC, 0x5EDD, 0x5EDE, 0x5EDF, 0x5EE0, 0x5EE1, 0x5EE2, 0x5EE3, 0x5EE4, 0x5EE5, 0x5EE6, 0x5EE7, 0x5EE9, 0x5EEB, 0x5EEC, 0x5EED, 0x5EEE, 0x5EEF, 0x5EF0, 0x5EF1, 0x5EF2, 0x5EF3, 0x5EF5, 0x5EF8, 0x5EF9, 0x5EFB, 0x5EFC, 0x5EFD, 0x5F05, 0x5F06, 0x5F07, 0x5F09, 0x5F0C, 0x5F0D, 0x5F0E, 0x5F10, 0x5F12, 0x5F14, 0x5F16, 0x5F19, 0x5F1A, 0x5F1C, 0x5F1D, 0x5F1E, 0x5F21, 0x5F22, 0x5F23, 0x5F24, 0x5F28, 0x5F2B, 0x5F2C, 0x5F2E, 0x5F30, 0x5F32, 0x5F33, 0x5F34, 0x5F35, 0x5F36, 0x5F37, 0x5F38, 0x5F3B, 0x5F3D, 0x5F3E, 0x5F3F, 0x5F41, 0x5F42, 0x5F43, 0x5F44, 0x5F45, 0x5F46, 0x5F47, 0x5F48, 0x5F49, 0x5F4A, 0x5F4B, 0x5F4C, 0x5F4D, 0x5F4E, 0x5F4F, 0x5F51, + 0x5F54, 0x5F59, 0x5F5A, 0x5F5B, 0x5F5C, 0x5F5E, 0x5F5F, 0x5F60, 0x5F63, 0x5F65, 0x5F67, 0x5F68, 0x5F6B, 0x5F6E, 0x5F6F, 0x5F72, 0x5F74, 0x5F75, 0x5F76, 0x5F78, 0x5F7A, 0x5F7D, 0x5F7E, 0x5F7F, 0x5F83, 0x5F86, 0x5F8D, 0x5F8E, 0x5F8F, 0x5F91, 0x5F93, 0x5F94, 0x5F96, 0x5F9A, 0x5F9B, 0x5F9D, 0x5F9E, 0x5F9F, 0x5FA0, 0x5FA2, 0x5FA3, 0x5FA4, 0x5FA5, 0x5FA6, 0x5FA7, 0x5FA9, 0x5FAB, 0x5FAC, 0x5FAF, 0x5FB0, 0x5FB1, 0x5FB2, 0x5FB3, 0x5FB4, 0x5FB6, 0x5FB8, 0x5FB9, 0x5FBA, 0x5FBB, 0x5FBE, 0x5FBF, 0x5FC0, 0x5FC1, 0x5FC2, 0x5FC7, 0x5FC8, 0x5FCA, 0x5FCB, 0x5FCE, 0x5FD3, 0x5FD4, 0x5FD5, 0x5FDA, 0x5FDB, 0x5FDC, 0x5FDE, 0x5FDF, 0x5FE2, 0x5FE3, 0x5FE5, 0x5FE6, 0x5FE8, 0x5FE9, 0x5FEC, 0x5FEF, 0x5FF0, 0x5FF2, 0x5FF3, 0x5FF4, 0x5FF6, 0x5FF7, 0x5FF9, 0x5FFA, 0x5FFC, 0x6007, + 0x6008, 0x6009, 0x600B, 0x600C, 0x6010, 0x6011, 0x6013, 0x6017, 0x6018, 0x601A, 0x601E, 0x601F, 0x6022, 0x6023, 0x6024, 0x602C, 0x602D, 0x602E, 0x6030, 0x6031, 0x6032, 0x6033, 0x6034, 0x6036, 0x6037, 0x6038, 0x6039, 0x603A, 0x603D, 0x603E, 0x6040, 0x6044, 0x6045, 0x6046, 0x6047, 0x6048, 0x6049, 0x604A, 0x604C, 0x604E, 0x604F, 0x6051, 0x6053, 0x6054, 0x6056, 0x6057, 0x6058, 0x605B, 0x605C, 0x605E, 0x605F, 0x6060, 0x6061, 0x6065, 0x6066, 0x606E, 0x6071, 0x6072, 0x6074, 0x6075, 0x6077, 0x607E, 0x6080, 0x6081, 0x6082, 0x6085, 0x6086, 0x6087, 0x6088, 0x608A, 0x608B, 0x608E, 0x608F, 0x6090, 0x6091, 0x6093, 0x6095, 0x6097, 0x6098, 0x6099, 0x609C, 0x609E, 0x60A1, 0x60A2, 0x60A4, 0x60A5, 0x60A7, 0x60A9, 0x60AA, 0x60AE, 0x60B0, 0x60B3, 0x60B5, 0x60B6, 0x60B7, + 0x60B9, 0x60BA, 0x60BD, 0x60BE, 0x60BF, 0x60C0, 0x60C1, 0x60C2, 0x60C3, 0x60C4, 0x60C7, 0x60C8, 0x60C9, 0x60CC, 0x60CD, 0x60CE, 0x60CF, 0x60D0, 0x60D2, 0x60D3, 0x60D4, 0x60D6, 0x60D7, 0x60D9, 0x60DB, 0x60DE, 0x60E1, 0x60E2, 0x60E3, 0x60E4, 0x60E5, 0x60EA, 0x60F1, 0x60F2, 0x60F5, 0x60F7, 0x60F8, 0x60FB, 0x60FC, 0x60FD, 0x60FE, 0x60FF, 0x6102, 0x6103, 0x6104, 0x6105, 0x6107, 0x610A, 0x610B, 0x610C, 0x6110, 0x6111, 0x6112, 0x6113, 0x6114, 0x6116, 0x6117, 0x6118, 0x6119, 0x611B, 0x611C, 0x611D, 0x611E, 0x6121, 0x6122, 0x6125, 0x6128, 0x6129, 0x612A, 0x612C, 0x612D, 0x612E, 0x612F, 0x6130, 0x6131, 0x6132, 0x6133, 0x6134, 0x6135, 0x6136, 0x6137, 0x6138, 0x6139, 0x613A, 0x613B, 0x613C, 0x613D, 0x613E, 0x6140, 0x6141, 0x6142, 0x6143, 0x6144, 0x6145, 0x6146, + 0x6147, 0x6149, 0x614B, 0x614D, 0x614F, 0x6150, 0x6152, 0x6153, 0x6154, 0x6156, 0x6157, 0x6158, 0x6159, 0x615A, 0x615B, 0x615C, 0x615E, 0x615F, 0x6160, 0x6161, 0x6163, 0x6164, 0x6165, 0x6166, 0x6169, 0x616A, 0x616B, 0x616C, 0x616D, 0x616E, 0x616F, 0x6171, 0x6172, 0x6173, 0x6174, 0x6176, 0x6178, 0x6179, 0x617A, 0x617B, 0x617C, 0x617D, 0x617E, 0x617F, 0x6180, 0x6181, 0x6182, 0x6183, 0x6184, 0x6185, 0x6186, 0x6187, 0x6188, 0x6189, 0x618A, 0x618C, 0x618D, 0x618F, 0x6190, 0x6191, 0x6192, 0x6193, 0x6195, 0x6196, 0x6197, 0x6198, 0x6199, 0x619A, 0x619B, 0x619C, 0x619E, 0x619F, 0x61A0, 0x61A1, 0x61A2, 0x61A3, 0x61A4, 0x61A5, 0x61A6, 0x61AA, 0x61AB, 0x61AD, 0x61AE, 0x61AF, 0x61B0, 0x61B1, 0x61B2, 0x61B3, 0x61B4, 0x61B5, 0x61B6, 0x61B8, 0x61B9, 0x61BA, 0x61BB, + 0x61BC, 0x61BD, 0x61BF, 0x61C0, 0x61C1, 0x61C3, 0x61C4, 0x61C5, 0x61C6, 0x61C7, 0x61C9, 0x61CC, 0x61CD, 0x61CE, 0x61CF, 0x61D0, 0x61D3, 0x61D5, 0x61D6, 0x61D7, 0x61D8, 0x61D9, 0x61DA, 0x61DB, 0x61DC, 0x61DD, 0x61DE, 0x61DF, 0x61E0, 0x61E1, 0x61E2, 0x61E3, 0x61E4, 0x61E5, 0x61E7, 0x61E8, 0x61E9, 0x61EA, 0x61EB, 0x61EC, 0x61ED, 0x61EE, 0x61EF, 0x61F0, 0x61F1, 0x61F2, 0x61F3, 0x61F4, 0x61F6, 0x61F7, 0x61F8, 0x61F9, 0x61FA, 0x61FB, 0x61FC, 0x61FD, 0x61FE, 0x6200, 0x6201, 0x6202, 0x6203, 0x6204, 0x6205, 0x6207, 0x6209, 0x6213, 0x6214, 0x6219, 0x621C, 0x621D, 0x621E, 0x6220, 0x6223, 0x6226, 0x6227, 0x6228, 0x6229, 0x622B, 0x622D, 0x622F, 0x6230, 0x6231, 0x6232, 0x6235, 0x6236, 0x6238, 0x6239, 0x623A, 0x623B, 0x623C, 0x6242, 0x6244, 0x6245, 0x6246, 0x624A, + 0x624F, 0x6250, 0x6255, 0x6256, 0x6257, 0x6259, 0x625A, 0x625C, 0x625D, 0x625E, 0x625F, 0x6260, 0x6261, 0x6262, 0x6264, 0x6265, 0x6268, 0x6271, 0x6272, 0x6274, 0x6275, 0x6277, 0x6278, 0x627A, 0x627B, 0x627D, 0x6281, 0x6282, 0x6283, 0x6285, 0x6286, 0x6287, 0x6288, 0x628B, 0x628C, 0x628D, 0x628E, 0x628F, 0x6290, 0x6294, 0x6299, 0x629C, 0x629D, 0x629E, 0x62A3, 0x62A6, 0x62A7, 0x62A9, 0x62AA, 0x62AD, 0x62AE, 0x62AF, 0x62B0, 0x62B2, 0x62B3, 0x62B4, 0x62B6, 0x62B7, 0x62B8, 0x62BA, 0x62BE, 0x62C0, 0x62C1, 0x62C3, 0x62CB, 0x62CF, 0x62D1, 0x62D5, 0x62DD, 0x62DE, 0x62E0, 0x62E1, 0x62E4, 0x62EA, 0x62EB, 0x62F0, 0x62F2, 0x62F5, 0x62F8, 0x62F9, 0x62FA, 0x62FB, 0x6300, 0x6303, 0x6304, 0x6305, 0x6306, 0x630A, 0x630B, 0x630C, 0x630D, 0x630F, 0x6310, 0x6312, 0x6313, + 0x6314, 0x6315, 0x6317, 0x6318, 0x6319, 0x631C, 0x6326, 0x6327, 0x6329, 0x632C, 0x632D, 0x632E, 0x6330, 0x6331, 0x6333, 0x6334, 0x6335, 0x6336, 0x6337, 0x6338, 0x633B, 0x633C, 0x633E, 0x633F, 0x6340, 0x6341, 0x6344, 0x6347, 0x6348, 0x634A, 0x6351, 0x6352, 0x6353, 0x6354, 0x6356, 0x6357, 0x6358, 0x6359, 0x635A, 0x635B, 0x635C, 0x635D, 0x6360, 0x6364, 0x6365, 0x6366, 0x6368, 0x636A, 0x636B, 0x636C, 0x636F, 0x6370, 0x6372, 0x6373, 0x6374, 0x6375, 0x6378, 0x6379, 0x637C, 0x637D, 0x637E, 0x637F, 0x6381, 0x6383, 0x6384, 0x6385, 0x6386, 0x638B, 0x638D, 0x6391, 0x6393, 0x6394, 0x6395, 0x6397, 0x6399, 0x639A, 0x639B, 0x639C, 0x639D, 0x639E, 0x639F, 0x63A1, 0x63A4, 0x63A6, 0x63AB, 0x63AF, 0x63B1, 0x63B2, 0x63B5, 0x63B6, 0x63B9, 0x63BB, 0x63BD, 0x63BF, 0x63C0, + 0x63C1, 0x63C2, 0x63C3, 0x63C5, 0x63C7, 0x63C8, 0x63CA, 0x63CB, 0x63CC, 0x63D1, 0x63D3, 0x63D4, 0x63D5, 0x63D7, 0x63D8, 0x63D9, 0x63DA, 0x63DB, 0x63DC, 0x63DD, 0x63DF, 0x63E2, 0x63E4, 0x63E5, 0x63E6, 0x63E7, 0x63E8, 0x63EB, 0x63EC, 0x63EE, 0x63EF, 0x63F0, 0x63F1, 0x63F3, 0x63F5, 0x63F7, 0x63F9, 0x63FA, 0x63FB, 0x63FC, 0x63FE, 0x6403, 0x6404, 0x6406, 0x6407, 0x6408, 0x6409, 0x640A, 0x640D, 0x640E, 0x6411, 0x6412, 0x6415, 0x6416, 0x6417, 0x6418, 0x6419, 0x641A, 0x641D, 0x641F, 0x6422, 0x6423, 0x6424, 0x6425, 0x6427, 0x6428, 0x6429, 0x642B, 0x642E, 0x642F, 0x6430, 0x6431, 0x6432, 0x6433, 0x6435, 0x6436, 0x6437, 0x6438, 0x6439, 0x643B, 0x643C, 0x643E, 0x6440, 0x6442, 0x6443, 0x6449, 0x644B, 0x644C, 0x644D, 0x644E, 0x644F, 0x6450, 0x6451, 0x6453, 0x6455, + 0x6456, 0x6457, 0x6459, 0x645A, 0x645B, 0x645C, 0x645D, 0x645F, 0x6460, 0x6461, 0x6462, 0x6463, 0x6464, 0x6465, 0x6466, 0x6468, 0x646A, 0x646B, 0x646C, 0x646E, 0x646F, 0x6470, 0x6471, 0x6472, 0x6473, 0x6474, 0x6475, 0x6476, 0x6477, 0x647B, 0x647C, 0x647D, 0x647E, 0x647F, 0x6480, 0x6481, 0x6483, 0x6486, 0x6488, 0x6489, 0x648A, 0x648B, 0x648C, 0x648D, 0x648E, 0x648F, 0x6490, 0x6493, 0x6494, 0x6497, 0x6498, 0x649A, 0x649B, 0x649C, 0x649D, 0x649F, 0x64A0, 0x64A1, 0x64A2, 0x64A3, 0x64A5, 0x64A6, 0x64A7, 0x64A8, 0x64AA, 0x64AB, 0x64AF, 0x64B1, 0x64B2, 0x64B3, 0x64B4, 0x64B6, 0x64B9, 0x64BB, 0x64BD, 0x64BE, 0x64BF, 0x64C1, 0x64C3, 0x64C4, 0x64C6, 0x64C7, 0x64C8, 0x64C9, 0x64CA, 0x64CB, 0x64CC, 0x64CF, 0x64D1, 0x64D3, 0x64D4, 0x64D5, 0x64D6, 0x64D9, 0x64DA, + 0x64DB, 0x64DC, 0x64DD, 0x64DF, 0x64E0, 0x64E1, 0x64E3, 0x64E5, 0x64E7, 0x64E8, 0x64E9, 0x64EA, 0x64EB, 0x64EC, 0x64ED, 0x64EE, 0x64EF, 0x64F0, 0x64F1, 0x64F2, 0x64F3, 0x64F4, 0x64F5, 0x64F6, 0x64F7, 0x64F8, 0x64F9, 0x64FA, 0x64FB, 0x64FC, 0x64FD, 0x64FE, 0x64FF, 0x6501, 0x6502, 0x6503, 0x6504, 0x6505, 0x6506, 0x6507, 0x6508, 0x650A, 0x650B, 0x650C, 0x650D, 0x650E, 0x650F, 0x6510, 0x6511, 0x6513, 0x6514, 0x6515, 0x6516, 0x6517, 0x6519, 0x651A, 0x651B, 0x651C, 0x651D, 0x651E, 0x651F, 0x6520, 0x6521, 0x6522, 0x6523, 0x6524, 0x6526, 0x6527, 0x6528, 0x6529, 0x652A, 0x652C, 0x652D, 0x6530, 0x6531, 0x6532, 0x6533, 0x6537, 0x653A, 0x653C, 0x653D, 0x6540, 0x6541, 0x6542, 0x6543, 0x6544, 0x6546, 0x6547, 0x654A, 0x654B, 0x654D, 0x654E, 0x6550, 0x6552, 0x6553, + 0x6554, 0x6557, 0x6558, 0x655A, 0x655C, 0x655F, 0x6560, 0x6561, 0x6564, 0x6565, 0x6567, 0x6568, 0x6569, 0x656A, 0x656D, 0x656E, 0x656F, 0x6571, 0x6573, 0x6575, 0x6576, 0x6578, 0x6579, 0x657A, 0x657B, 0x657C, 0x657D, 0x657E, 0x657F, 0x6580, 0x6581, 0x6582, 0x6583, 0x6584, 0x6585, 0x6586, 0x6588, 0x6589, 0x658A, 0x658D, 0x658E, 0x658F, 0x6592, 0x6594, 0x6595, 0x6596, 0x6598, 0x659A, 0x659D, 0x659E, 0x65A0, 0x65A2, 0x65A3, 0x65A6, 0x65A8, 0x65AA, 0x65AC, 0x65AE, 0x65B1, 0x65B2, 0x65B3, 0x65B4, 0x65B5, 0x65B6, 0x65B7, 0x65B8, 0x65BA, 0x65BB, 0x65BE, 0x65BF, 0x65C0, 0x65C2, 0x65C7, 0x65C8, 0x65C9, 0x65CA, 0x65CD, 0x65D0, 0x65D1, 0x65D3, 0x65D4, 0x65D5, 0x65D8, 0x65D9, 0x65DA, 0x65DB, 0x65DC, 0x65DD, 0x65DE, 0x65DF, 0x65E1, 0x65E3, 0x65E4, 0x65EA, 0x65EB, + 0x65F2, 0x65F3, 0x65F4, 0x65F5, 0x65F8, 0x65F9, 0x65FB, 0x65FC, 0x65FD, 0x65FE, 0x65FF, 0x6601, 0x6604, 0x6605, 0x6607, 0x6608, 0x6609, 0x660B, 0x660D, 0x6610, 0x6611, 0x6612, 0x6616, 0x6617, 0x6618, 0x661A, 0x661B, 0x661C, 0x661E, 0x6621, 0x6622, 0x6623, 0x6624, 0x6626, 0x6629, 0x662A, 0x662B, 0x662C, 0x662E, 0x6630, 0x6632, 0x6633, 0x6637, 0x6638, 0x6639, 0x663A, 0x663B, 0x663D, 0x663F, 0x6640, 0x6642, 0x6644, 0x6645, 0x6646, 0x6647, 0x6648, 0x6649, 0x664A, 0x664D, 0x664E, 0x6650, 0x6651, 0x6658, 0x6659, 0x665B, 0x665C, 0x665D, 0x665E, 0x6660, 0x6662, 0x6663, 0x6665, 0x6667, 0x6669, 0x666A, 0x666B, 0x666C, 0x666D, 0x6671, 0x6672, 0x6673, 0x6675, 0x6678, 0x6679, 0x667B, 0x667C, 0x667D, 0x667F, 0x6680, 0x6681, 0x6683, 0x6685, 0x6686, 0x6688, 0x6689, + 0x668A, 0x668B, 0x668D, 0x668E, 0x668F, 0x6690, 0x6692, 0x6693, 0x6694, 0x6695, 0x6698, 0x6699, 0x669A, 0x669B, 0x669C, 0x669E, 0x669F, 0x66A0, 0x66A1, 0x66A2, 0x66A3, 0x66A4, 0x66A5, 0x66A6, 0x66A9, 0x66AA, 0x66AB, 0x66AC, 0x66AD, 0x66AF, 0x66B0, 0x66B1, 0x66B2, 0x66B3, 0x66B5, 0x66B6, 0x66B7, 0x66B8, 0x66BA, 0x66BB, 0x66BC, 0x66BD, 0x66BF, 0x66C0, 0x66C1, 0x66C2, 0x66C3, 0x66C4, 0x66C5, 0x66C6, 0x66C7, 0x66C8, 0x66C9, 0x66CA, 0x66CB, 0x66CC, 0x66CD, 0x66CE, 0x66CF, 0x66D0, 0x66D1, 0x66D2, 0x66D3, 0x66D4, 0x66D5, 0x66D6, 0x66D7, 0x66D8, 0x66DA, 0x66DE, 0x66DF, 0x66E0, 0x66E1, 0x66E2, 0x66E3, 0x66E4, 0x66E5, 0x66E7, 0x66E8, 0x66EA, 0x66EB, 0x66EC, 0x66ED, 0x66EE, 0x66EF, 0x66F1, 0x66F5, 0x66F6, 0x66F8, 0x66FA, 0x66FB, 0x66FD, 0x6701, 0x6702, 0x6703, + 0x6704, 0x6705, 0x6706, 0x6707, 0x670C, 0x670E, 0x670F, 0x6711, 0x6712, 0x6713, 0x6716, 0x6718, 0x6719, 0x671A, 0x671C, 0x671E, 0x6720, 0x6721, 0x6722, 0x6723, 0x6724, 0x6725, 0x6727, 0x6729, 0x672E, 0x6730, 0x6732, 0x6733, 0x6736, 0x6737, 0x6738, 0x6739, 0x673B, 0x673C, 0x673E, 0x673F, 0x6741, 0x6744, 0x6745, 0x6747, 0x674A, 0x674B, 0x674D, 0x6752, 0x6754, 0x6755, 0x6757, 0x6758, 0x6759, 0x675A, 0x675B, 0x675D, 0x6762, 0x6763, 0x6764, 0x6766, 0x6767, 0x676B, 0x676C, 0x676E, 0x6771, 0x6774, 0x6776, 0x6778, 0x6779, 0x677A, 0x677B, 0x677D, 0x6780, 0x6782, 0x6783, 0x6785, 0x6786, 0x6788, 0x678A, 0x678C, 0x678D, 0x678E, 0x678F, 0x6791, 0x6792, 0x6793, 0x6794, 0x6796, 0x6799, 0x679B, 0x679F, 0x67A0, 0x67A1, 0x67A4, 0x67A6, 0x67A9, 0x67AC, 0x67AE, 0x67B1, + 0x67B2, 0x67B4, 0x67B9, 0x67BA, 0x67BB, 0x67BC, 0x67BD, 0x67BE, 0x67BF, 0x67C0, 0x67C2, 0x67C5, 0x67C6, 0x67C7, 0x67C8, 0x67C9, 0x67CA, 0x67CB, 0x67CC, 0x67CD, 0x67CE, 0x67D5, 0x67D6, 0x67D7, 0x67DB, 0x67DF, 0x67E1, 0x67E3, 0x67E4, 0x67E6, 0x67E7, 0x67E8, 0x67EA, 0x67EB, 0x67ED, 0x67EE, 0x67F2, 0x67F5, 0x67F6, 0x67F7, 0x67F8, 0x67F9, 0x67FA, 0x67FB, 0x67FC, 0x67FE, 0x6801, 0x6802, 0x6803, 0x6804, 0x6806, 0x680D, 0x6810, 0x6812, 0x6814, 0x6815, 0x6818, 0x6819, 0x681A, 0x681B, 0x681C, 0x681E, 0x681F, 0x6820, 0x6822, 0x6823, 0x6824, 0x6825, 0x6826, 0x6827, 0x6828, 0x682B, 0x682C, 0x682D, 0x682E, 0x682F, 0x6830, 0x6831, 0x6834, 0x6835, 0x6836, 0x683A, 0x683B, 0x683F, 0x6847, 0x684B, 0x684D, 0x684F, 0x6852, 0x6856, 0x6857, 0x6858, 0x6859, 0x685A, 0x685B, + 0x685C, 0x685D, 0x685E, 0x685F, 0x686A, 0x686C, 0x686D, 0x686E, 0x686F, 0x6870, 0x6871, 0x6872, 0x6873, 0x6875, 0x6878, 0x6879, 0x687A, 0x687B, 0x687C, 0x687D, 0x687E, 0x687F, 0x6880, 0x6882, 0x6884, 0x6887, 0x6888, 0x6889, 0x688A, 0x688B, 0x688C, 0x688D, 0x688E, 0x6890, 0x6891, 0x6892, 0x6894, 0x6895, 0x6896, 0x6898, 0x6899, 0x689A, 0x689B, 0x689C, 0x689D, 0x689E, 0x689F, 0x68A0, 0x68A1, 0x68A3, 0x68A4, 0x68A5, 0x68A9, 0x68AA, 0x68AB, 0x68AC, 0x68AE, 0x68B1, 0x68B2, 0x68B4, 0x68B6, 0x68B7, 0x68B8, 0x68B9, 0x68BA, 0x68BB, 0x68BC, 0x68BD, 0x68BE, 0x68BF, 0x68C1, 0x68C3, 0x68C4, 0x68C5, 0x68C6, 0x68C7, 0x68C8, 0x68CA, 0x68CC, 0x68CE, 0x68CF, 0x68D0, 0x68D1, 0x68D3, 0x68D4, 0x68D6, 0x68D7, 0x68D9, 0x68DB, 0x68DC, 0x68DD, 0x68DE, 0x68DF, 0x68E1, 0x68E2, + 0x68E4, 0x68E5, 0x68E6, 0x68E7, 0x68E8, 0x68E9, 0x68EA, 0x68EB, 0x68EC, 0x68ED, 0x68EF, 0x68F2, 0x68F3, 0x68F4, 0x68F6, 0x68F7, 0x68F8, 0x68FB, 0x68FD, 0x68FE, 0x68FF, 0x6900, 0x6902, 0x6903, 0x6904, 0x6906, 0x6907, 0x6908, 0x6909, 0x690A, 0x690C, 0x690F, 0x6911, 0x6913, 0x6914, 0x6915, 0x6916, 0x6917, 0x6918, 0x6919, 0x691A, 0x691B, 0x691C, 0x691D, 0x691E, 0x6921, 0x6922, 0x6923, 0x6925, 0x6926, 0x6927, 0x6928, 0x6929, 0x692A, 0x692B, 0x692C, 0x692E, 0x692F, 0x6931, 0x6932, 0x6933, 0x6935, 0x6936, 0x6937, 0x6938, 0x693A, 0x693B, 0x693C, 0x693E, 0x6940, 0x6941, 0x6943, 0x6944, 0x6945, 0x6946, 0x6947, 0x6948, 0x6949, 0x694A, 0x694B, 0x694C, 0x694D, 0x694E, 0x694F, 0x6950, 0x6951, 0x6952, 0x6953, 0x6955, 0x6956, 0x6958, 0x6959, 0x695B, 0x695C, 0x695F, + 0x6961, 0x6962, 0x6964, 0x6965, 0x6967, 0x6968, 0x6969, 0x696A, 0x696C, 0x696D, 0x696F, 0x6970, 0x6972, 0x6973, 0x6974, 0x6975, 0x6976, 0x697A, 0x697B, 0x697D, 0x697E, 0x697F, 0x6981, 0x6983, 0x6985, 0x698A, 0x698B, 0x698C, 0x698E, 0x698F, 0x6990, 0x6991, 0x6992, 0x6993, 0x6996, 0x6997, 0x6999, 0x699A, 0x699D, 0x699E, 0x699F, 0x69A0, 0x69A1, 0x69A2, 0x69A3, 0x69A4, 0x69A5, 0x69A6, 0x69A9, 0x69AA, 0x69AC, 0x69AE, 0x69AF, 0x69B0, 0x69B2, 0x69B3, 0x69B5, 0x69B6, 0x69B8, 0x69B9, 0x69BA, 0x69BC, 0x69BD, 0x69BE, 0x69BF, 0x69C0, 0x69C2, 0x69C3, 0x69C4, 0x69C5, 0x69C6, 0x69C7, 0x69C8, 0x69C9, 0x69CB, 0x69CD, 0x69CF, 0x69D1, 0x69D2, 0x69D3, 0x69D5, 0x69D6, 0x69D7, 0x69D8, 0x69D9, 0x69DA, 0x69DC, 0x69DD, 0x69DE, 0x69E1, 0x69E2, 0x69E3, 0x69E4, 0x69E5, 0x69E6, + 0x69E7, 0x69E8, 0x69E9, 0x69EA, 0x69EB, 0x69EC, 0x69EE, 0x69EF, 0x69F0, 0x69F1, 0x69F3, 0x69F4, 0x69F5, 0x69F6, 0x69F7, 0x69F8, 0x69F9, 0x69FA, 0x69FB, 0x69FC, 0x69FE, 0x6A00, 0x6A01, 0x6A02, 0x6A03, 0x6A04, 0x6A05, 0x6A06, 0x6A07, 0x6A08, 0x6A09, 0x6A0B, 0x6A0C, 0x6A0D, 0x6A0E, 0x6A0F, 0x6A10, 0x6A11, 0x6A12, 0x6A13, 0x6A14, 0x6A15, 0x6A16, 0x6A19, 0x6A1A, 0x6A1B, 0x6A1C, 0x6A1D, 0x6A1E, 0x6A20, 0x6A22, 0x6A23, 0x6A24, 0x6A25, 0x6A26, 0x6A27, 0x6A29, 0x6A2B, 0x6A2C, 0x6A2D, 0x6A2E, 0x6A30, 0x6A32, 0x6A33, 0x6A34, 0x6A36, 0x6A37, 0x6A38, 0x6A39, 0x6A3A, 0x6A3B, 0x6A3C, 0x6A3F, 0x6A40, 0x6A41, 0x6A42, 0x6A43, 0x6A45, 0x6A46, 0x6A48, 0x6A49, 0x6A4A, 0x6A4B, 0x6A4C, 0x6A4D, 0x6A4E, 0x6A4F, 0x6A51, 0x6A52, 0x6A53, 0x6A54, 0x6A55, 0x6A56, 0x6A57, 0x6A5A, + 0x6A5C, 0x6A5D, 0x6A5E, 0x6A5F, 0x6A60, 0x6A62, 0x6A63, 0x6A64, 0x6A66, 0x6A67, 0x6A68, 0x6A69, 0x6A6A, 0x6A6B, 0x6A6C, 0x6A6D, 0x6A6E, 0x6A6F, 0x6A70, 0x6A72, 0x6A73, 0x6A74, 0x6A75, 0x6A76, 0x6A77, 0x6A78, 0x6A7A, 0x6A7B, 0x6A7D, 0x6A7E, 0x6A7F, 0x6A81, 0x6A82, 0x6A83, 0x6A85, 0x6A86, 0x6A87, 0x6A88, 0x6A89, 0x6A8A, 0x6A8B, 0x6A8C, 0x6A8D, 0x6A8F, 0x6A92, 0x6A93, 0x6A94, 0x6A95, 0x6A96, 0x6A98, 0x6A99, 0x6A9A, 0x6A9B, 0x6A9C, 0x6A9D, 0x6A9E, 0x6A9F, 0x6AA1, 0x6AA2, 0x6AA3, 0x6AA4, 0x6AA5, 0x6AA6, 0x6AA7, 0x6AA8, 0x6AAA, 0x6AAD, 0x6AAE, 0x6AAF, 0x6AB0, 0x6AB1, 0x6AB2, 0x6AB3, 0x6AB4, 0x6AB5, 0x6AB6, 0x6AB7, 0x6AB8, 0x6AB9, 0x6ABA, 0x6ABB, 0x6ABC, 0x6ABD, 0x6ABE, 0x6ABF, 0x6AC0, 0x6AC1, 0x6AC2, 0x6AC3, 0x6AC4, 0x6AC5, 0x6AC6, 0x6AC7, 0x6AC8, 0x6AC9, + 0x6ACA, 0x6ACB, 0x6ACC, 0x6ACD, 0x6ACE, 0x6ACF, 0x6AD0, 0x6AD1, 0x6AD2, 0x6AD3, 0x6AD4, 0x6AD5, 0x6AD6, 0x6AD7, 0x6AD8, 0x6AD9, 0x6ADA, 0x6ADB, 0x6ADC, 0x6ADD, 0x6ADE, 0x6ADF, 0x6AE0, 0x6AE1, 0x6AE2, 0x6AE3, 0x6AE4, 0x6AE5, 0x6AE6, 0x6AE7, 0x6AE8, 0x6AE9, 0x6AEA, 0x6AEB, 0x6AEC, 0x6AED, 0x6AEE, 0x6AEF, 0x6AF0, 0x6AF1, 0x6AF2, 0x6AF3, 0x6AF4, 0x6AF5, 0x6AF6, 0x6AF7, 0x6AF8, 0x6AF9, 0x6AFA, 0x6AFB, 0x6AFC, 0x6AFD, 0x6AFE, 0x6AFF, 0x6B00, 0x6B01, 0x6B02, 0x6B03, 0x6B04, 0x6B05, 0x6B06, 0x6B07, 0x6B08, 0x6B09, 0x6B0A, 0x6B0B, 0x6B0C, 0x6B0D, 0x6B0E, 0x6B0F, 0x6B10, 0x6B11, 0x6B12, 0x6B13, 0x6B14, 0x6B15, 0x6B16, 0x6B17, 0x6B18, 0x6B19, 0x6B1A, 0x6B1B, 0x6B1C, 0x6B1D, 0x6B1E, 0x6B1F, 0x6B25, 0x6B26, 0x6B28, 0x6B29, 0x6B2A, 0x6B2B, 0x6B2C, 0x6B2D, 0x6B2E, + 0x6B2F, 0x6B30, 0x6B31, 0x6B33, 0x6B34, 0x6B35, 0x6B36, 0x6B38, 0x6B3B, 0x6B3C, 0x6B3D, 0x6B3F, 0x6B40, 0x6B41, 0x6B42, 0x6B44, 0x6B45, 0x6B48, 0x6B4A, 0x6B4B, 0x6B4D, 0x6B4E, 0x6B4F, 0x6B50, 0x6B51, 0x6B52, 0x6B53, 0x6B54, 0x6B55, 0x6B56, 0x6B57, 0x6B58, 0x6B5A, 0x6B5B, 0x6B5C, 0x6B5D, 0x6B5E, 0x6B5F, 0x6B60, 0x6B61, 0x6B68, 0x6B69, 0x6B6B, 0x6B6C, 0x6B6D, 0x6B6E, 0x6B6F, 0x6B70, 0x6B71, 0x6B72, 0x6B73, 0x6B74, 0x6B75, 0x6B76, 0x6B77, 0x6B78, 0x6B7A, 0x6B7D, 0x6B7E, 0x6B7F, 0x6B80, 0x6B85, 0x6B88, 0x6B8C, 0x6B8E, 0x6B8F, 0x6B90, 0x6B91, 0x6B94, 0x6B95, 0x6B97, 0x6B98, 0x6B99, 0x6B9C, 0x6B9D, 0x6B9E, 0x6B9F, 0x6BA0, 0x6BA2, 0x6BA3, 0x6BA4, 0x6BA5, 0x6BA6, 0x6BA7, 0x6BA8, 0x6BA9, 0x6BAB, 0x6BAC, 0x6BAD, 0x6BAE, 0x6BAF, 0x6BB0, 0x6BB1, 0x6BB2, 0x6BB6, + 0x6BB8, 0x6BB9, 0x6BBA, 0x6BBB, 0x6BBC, 0x6BBD, 0x6BBE, 0x6BC0, 0x6BC3, 0x6BC4, 0x6BC6, 0x6BC7, 0x6BC8, 0x6BC9, 0x6BCA, 0x6BCC, 0x6BCE, 0x6BD0, 0x6BD1, 0x6BD8, 0x6BDA, 0x6BDC, 0x6BDD, 0x6BDE, 0x6BDF, 0x6BE0, 0x6BE2, 0x6BE3, 0x6BE4, 0x6BE5, 0x6BE6, 0x6BE7, 0x6BE8, 0x6BE9, 0x6BEC, 0x6BED, 0x6BEE, 0x6BF0, 0x6BF1, 0x6BF2, 0x6BF4, 0x6BF6, 0x6BF7, 0x6BF8, 0x6BFA, 0x6BFB, 0x6BFC, 0x6BFE, 0x6BFF, 0x6C00, 0x6C01, 0x6C02, 0x6C03, 0x6C04, 0x6C08, 0x6C09, 0x6C0A, 0x6C0B, 0x6C0C, 0x6C0E, 0x6C12, 0x6C17, 0x6C1C, 0x6C1D, 0x6C1E, 0x6C20, 0x6C23, 0x6C25, 0x6C2B, 0x6C2C, 0x6C2D, 0x6C31, 0x6C33, 0x6C36, 0x6C37, 0x6C39, 0x6C3A, 0x6C3B, 0x6C3C, 0x6C3E, 0x6C3F, 0x6C43, 0x6C44, 0x6C45, 0x6C48, 0x6C4B, 0x6C4C, 0x6C4D, 0x6C4E, 0x6C4F, 0x6C51, 0x6C52, 0x6C53, 0x6C56, 0x6C58, + 0x6C59, 0x6C5A, 0x6C62, 0x6C63, 0x6C65, 0x6C66, 0x6C67, 0x6C6B, 0x6C6C, 0x6C6D, 0x6C6E, 0x6C6F, 0x6C71, 0x6C73, 0x6C75, 0x6C77, 0x6C78, 0x6C7A, 0x6C7B, 0x6C7C, 0x6C7F, 0x6C80, 0x6C84, 0x6C87, 0x6C8A, 0x6C8B, 0x6C8D, 0x6C8E, 0x6C91, 0x6C92, 0x6C95, 0x6C96, 0x6C97, 0x6C98, 0x6C9A, 0x6C9C, 0x6C9D, 0x6C9E, 0x6CA0, 0x6CA2, 0x6CA8, 0x6CAC, 0x6CAF, 0x6CB0, 0x6CB4, 0x6CB5, 0x6CB6, 0x6CB7, 0x6CBA, 0x6CC0, 0x6CC1, 0x6CC2, 0x6CC3, 0x6CC6, 0x6CC7, 0x6CC8, 0x6CCB, 0x6CCD, 0x6CCE, 0x6CCF, 0x6CD1, 0x6CD2, 0x6CD8, 0x6CD9, 0x6CDA, 0x6CDC, 0x6CDD, 0x6CDF, 0x6CE4, 0x6CE6, 0x6CE7, 0x6CE9, 0x6CEC, 0x6CED, 0x6CF2, 0x6CF4, 0x6CF9, 0x6CFF, 0x6D00, 0x6D02, 0x6D03, 0x6D05, 0x6D06, 0x6D08, 0x6D09, 0x6D0A, 0x6D0D, 0x6D0F, 0x6D10, 0x6D11, 0x6D13, 0x6D14, 0x6D15, 0x6D16, 0x6D18, + 0x6D1C, 0x6D1D, 0x6D1F, 0x6D20, 0x6D21, 0x6D22, 0x6D23, 0x6D24, 0x6D26, 0x6D28, 0x6D29, 0x6D2C, 0x6D2D, 0x6D2F, 0x6D30, 0x6D34, 0x6D36, 0x6D37, 0x6D38, 0x6D3A, 0x6D3F, 0x6D40, 0x6D42, 0x6D44, 0x6D49, 0x6D4C, 0x6D50, 0x6D55, 0x6D56, 0x6D57, 0x6D58, 0x6D5B, 0x6D5D, 0x6D5F, 0x6D61, 0x6D62, 0x6D64, 0x6D65, 0x6D67, 0x6D68, 0x6D6B, 0x6D6C, 0x6D6D, 0x6D70, 0x6D71, 0x6D72, 0x6D73, 0x6D75, 0x6D76, 0x6D79, 0x6D7A, 0x6D7B, 0x6D7D, 0x6D7E, 0x6D7F, 0x6D80, 0x6D81, 0x6D83, 0x6D84, 0x6D86, 0x6D87, 0x6D8A, 0x6D8B, 0x6D8D, 0x6D8F, 0x6D90, 0x6D92, 0x6D96, 0x6D97, 0x6D98, 0x6D99, 0x6D9A, 0x6D9C, 0x6DA2, 0x6DA5, 0x6DAC, 0x6DAD, 0x6DB0, 0x6DB1, 0x6DB3, 0x6DB4, 0x6DB6, 0x6DB7, 0x6DB9, 0x6DBA, 0x6DBB, 0x6DBC, 0x6DBD, 0x6DBE, 0x6DC1, 0x6DC2, 0x6DC3, 0x6DC8, 0x6DC9, 0x6DCA, + 0x6DCD, 0x6DCE, 0x6DCF, 0x6DD0, 0x6DD2, 0x6DD3, 0x6DD4, 0x6DD5, 0x6DD7, 0x6DDA, 0x6DDB, 0x6DDC, 0x6DDF, 0x6DE2, 0x6DE3, 0x6DE5, 0x6DE7, 0x6DE8, 0x6DE9, 0x6DEA, 0x6DED, 0x6DEF, 0x6DF0, 0x6DF2, 0x6DF4, 0x6DF5, 0x6DF6, 0x6DF8, 0x6DFA, 0x6DFD, 0x6DFE, 0x6DFF, 0x6E00, 0x6E01, 0x6E02, 0x6E03, 0x6E04, 0x6E06, 0x6E07, 0x6E08, 0x6E09, 0x6E0B, 0x6E0F, 0x6E12, 0x6E13, 0x6E15, 0x6E18, 0x6E19, 0x6E1B, 0x6E1C, 0x6E1E, 0x6E1F, 0x6E22, 0x6E26, 0x6E27, 0x6E28, 0x6E2A, 0x6E2C, 0x6E2E, 0x6E30, 0x6E31, 0x6E33, 0x6E35, 0x6E36, 0x6E37, 0x6E39, 0x6E3B, 0x6E3C, 0x6E3D, 0x6E3E, 0x6E3F, 0x6E40, 0x6E41, 0x6E42, 0x6E45, 0x6E46, 0x6E47, 0x6E48, 0x6E49, 0x6E4A, 0x6E4B, 0x6E4C, 0x6E4F, 0x6E50, 0x6E51, 0x6E52, 0x6E55, 0x6E57, 0x6E59, 0x6E5A, 0x6E5C, 0x6E5D, 0x6E5E, 0x6E60, 0x6E61, + 0x6E62, 0x6E63, 0x6E64, 0x6E65, 0x6E66, 0x6E67, 0x6E68, 0x6E69, 0x6E6A, 0x6E6C, 0x6E6D, 0x6E6F, 0x6E70, 0x6E71, 0x6E72, 0x6E73, 0x6E74, 0x6E75, 0x6E76, 0x6E77, 0x6E78, 0x6E79, 0x6E7A, 0x6E7B, 0x6E7C, 0x6E7D, 0x6E80, 0x6E81, 0x6E82, 0x6E84, 0x6E87, 0x6E88, 0x6E8A, 0x6E8B, 0x6E8C, 0x6E8D, 0x6E8E, 0x6E91, 0x6E92, 0x6E93, 0x6E94, 0x6E95, 0x6E96, 0x6E97, 0x6E99, 0x6E9A, 0x6E9B, 0x6E9D, 0x6E9E, 0x6EA0, 0x6EA1, 0x6EA3, 0x6EA4, 0x6EA6, 0x6EA8, 0x6EA9, 0x6EAB, 0x6EAC, 0x6EAD, 0x6EAE, 0x6EB0, 0x6EB3, 0x6EB5, 0x6EB8, 0x6EB9, 0x6EBC, 0x6EBE, 0x6EBF, 0x6EC0, 0x6EC3, 0x6EC4, 0x6EC5, 0x6EC6, 0x6EC8, 0x6EC9, 0x6ECA, 0x6ECC, 0x6ECD, 0x6ECE, 0x6ED0, 0x6ED2, 0x6ED6, 0x6ED8, 0x6ED9, 0x6EDB, 0x6EDC, 0x6EDD, 0x6EE3, 0x6EE7, 0x6EEA, 0x6EEB, 0x6EEC, 0x6EED, 0x6EEE, 0x6EEF, + 0x6EF0, 0x6EF1, 0x6EF2, 0x6EF3, 0x6EF5, 0x6EF6, 0x6EF7, 0x6EF8, 0x6EFA, 0x6EFB, 0x6EFC, 0x6EFD, 0x6EFE, 0x6EFF, 0x6F00, 0x6F01, 0x6F03, 0x6F04, 0x6F05, 0x6F07, 0x6F08, 0x6F0A, 0x6F0B, 0x6F0C, 0x6F0D, 0x6F0E, 0x6F10, 0x6F11, 0x6F12, 0x6F16, 0x6F17, 0x6F18, 0x6F19, 0x6F1A, 0x6F1B, 0x6F1C, 0x6F1D, 0x6F1E, 0x6F1F, 0x6F21, 0x6F22, 0x6F23, 0x6F25, 0x6F26, 0x6F27, 0x6F28, 0x6F2C, 0x6F2E, 0x6F30, 0x6F32, 0x6F34, 0x6F35, 0x6F37, 0x6F38, 0x6F39, 0x6F3A, 0x6F3B, 0x6F3C, 0x6F3D, 0x6F3F, 0x6F40, 0x6F41, 0x6F42, 0x6F43, 0x6F44, 0x6F45, 0x6F48, 0x6F49, 0x6F4A, 0x6F4C, 0x6F4E, 0x6F4F, 0x6F50, 0x6F51, 0x6F52, 0x6F53, 0x6F54, 0x6F55, 0x6F56, 0x6F57, 0x6F59, 0x6F5A, 0x6F5B, 0x6F5D, 0x6F5F, 0x6F60, 0x6F61, 0x6F63, 0x6F64, 0x6F65, 0x6F67, 0x6F68, 0x6F69, 0x6F6A, 0x6F6B, + 0x6F6C, 0x6F6F, 0x6F70, 0x6F71, 0x6F73, 0x6F75, 0x6F76, 0x6F77, 0x6F79, 0x6F7B, 0x6F7D, 0x6F7E, 0x6F7F, 0x6F80, 0x6F81, 0x6F82, 0x6F83, 0x6F85, 0x6F86, 0x6F87, 0x6F8A, 0x6F8B, 0x6F8F, 0x6F90, 0x6F91, 0x6F92, 0x6F93, 0x6F94, 0x6F95, 0x6F96, 0x6F97, 0x6F98, 0x6F99, 0x6F9A, 0x6F9B, 0x6F9D, 0x6F9E, 0x6F9F, 0x6FA0, 0x6FA2, 0x6FA3, 0x6FA4, 0x6FA5, 0x6FA6, 0x6FA8, 0x6FA9, 0x6FAA, 0x6FAB, 0x6FAC, 0x6FAD, 0x6FAE, 0x6FAF, 0x6FB0, 0x6FB1, 0x6FB2, 0x6FB4, 0x6FB5, 0x6FB7, 0x6FB8, 0x6FBA, 0x6FBB, 0x6FBC, 0x6FBD, 0x6FBE, 0x6FBF, 0x6FC1, 0x6FC3, 0x6FC4, 0x6FC5, 0x6FC6, 0x6FC7, 0x6FC8, 0x6FCA, 0x6FCB, 0x6FCC, 0x6FCD, 0x6FCE, 0x6FCF, 0x6FD0, 0x6FD3, 0x6FD4, 0x6FD5, 0x6FD6, 0x6FD7, 0x6FD8, 0x6FD9, 0x6FDA, 0x6FDB, 0x6FDC, 0x6FDD, 0x6FDF, 0x6FE2, 0x6FE3, 0x6FE4, 0x6FE5, + 0x6FE6, 0x6FE7, 0x6FE8, 0x6FE9, 0x6FEA, 0x6FEB, 0x6FEC, 0x6FED, 0x6FF0, 0x6FF1, 0x6FF2, 0x6FF3, 0x6FF4, 0x6FF5, 0x6FF6, 0x6FF7, 0x6FF8, 0x6FF9, 0x6FFA, 0x6FFB, 0x6FFC, 0x6FFD, 0x6FFE, 0x6FFF, 0x7000, 0x7001, 0x7002, 0x7003, 0x7004, 0x7005, 0x7006, 0x7007, 0x7008, 0x7009, 0x700A, 0x700B, 0x700C, 0x700D, 0x700E, 0x700F, 0x7010, 0x7012, 0x7013, 0x7014, 0x7015, 0x7016, 0x7017, 0x7018, 0x7019, 0x701C, 0x701D, 0x701E, 0x701F, 0x7020, 0x7021, 0x7022, 0x7024, 0x7025, 0x7026, 0x7027, 0x7028, 0x7029, 0x702A, 0x702B, 0x702C, 0x702D, 0x702E, 0x702F, 0x7030, 0x7031, 0x7032, 0x7033, 0x7034, 0x7036, 0x7037, 0x7038, 0x703A, 0x703B, 0x703C, 0x703D, 0x703E, 0x703F, 0x7040, 0x7041, 0x7042, 0x7043, 0x7044, 0x7045, 0x7046, 0x7047, 0x7048, 0x7049, 0x704A, 0x704B, 0x704D, + 0x704E, 0x7050, 0x7051, 0x7052, 0x7053, 0x7054, 0x7055, 0x7056, 0x7057, 0x7058, 0x7059, 0x705A, 0x705B, 0x705C, 0x705D, 0x705F, 0x7060, 0x7061, 0x7062, 0x7063, 0x7064, 0x7065, 0x7066, 0x7067, 0x7068, 0x7069, 0x706A, 0x706E, 0x7071, 0x7072, 0x7073, 0x7074, 0x7077, 0x7079, 0x707A, 0x707B, 0x707D, 0x7081, 0x7082, 0x7083, 0x7084, 0x7086, 0x7087, 0x7088, 0x708B, 0x708C, 0x708D, 0x708F, 0x7090, 0x7091, 0x7093, 0x7097, 0x7098, 0x709A, 0x709B, 0x709E, 0x709F, 0x70A0, 0x70A1, 0x70A2, 0x70A3, 0x70A4, 0x70A5, 0x70A6, 0x70A7, 0x70A8, 0x70A9, 0x70AA, 0x70B0, 0x70B2, 0x70B4, 0x70B5, 0x70B6, 0x70BA, 0x70BE, 0x70BF, 0x70C4, 0x70C5, 0x70C6, 0x70C7, 0x70C9, 0x70CB, 0x70CC, 0x70CD, 0x70CE, 0x70CF, 0x70D0, 0x70D1, 0x70D2, 0x70D3, 0x70D4, 0x70D5, 0x70D6, 0x70D7, 0x70DA, + 0x70DC, 0x70DD, 0x70DE, 0x70E0, 0x70E1, 0x70E2, 0x70E3, 0x70E5, 0x70EA, 0x70EE, 0x70F0, 0x70F1, 0x70F2, 0x70F3, 0x70F4, 0x70F5, 0x70F6, 0x70F8, 0x70FA, 0x70FB, 0x70FC, 0x70FE, 0x70FF, 0x7100, 0x7101, 0x7102, 0x7103, 0x7104, 0x7105, 0x7106, 0x7107, 0x7108, 0x710B, 0x710C, 0x710D, 0x710E, 0x710F, 0x7111, 0x7112, 0x7114, 0x7117, 0x711B, 0x711C, 0x711D, 0x711E, 0x711F, 0x7120, 0x7121, 0x7122, 0x7123, 0x7124, 0x7125, 0x7127, 0x7128, 0x7129, 0x712A, 0x712B, 0x712C, 0x712D, 0x712E, 0x7132, 0x7133, 0x7134, 0x7135, 0x7137, 0x7138, 0x7139, 0x713A, 0x713B, 0x713C, 0x713D, 0x713E, 0x713F, 0x7140, 0x7141, 0x7142, 0x7143, 0x7144, 0x7146, 0x7147, 0x7148, 0x7149, 0x714B, 0x714D, 0x714F, 0x7150, 0x7151, 0x7152, 0x7153, 0x7154, 0x7155, 0x7156, 0x7157, 0x7158, 0x7159, + 0x715A, 0x715B, 0x715D, 0x715F, 0x7160, 0x7161, 0x7162, 0x7163, 0x7165, 0x7169, 0x716A, 0x716B, 0x716C, 0x716D, 0x716F, 0x7170, 0x7171, 0x7174, 0x7175, 0x7176, 0x7177, 0x7179, 0x717B, 0x717C, 0x717E, 0x717F, 0x7180, 0x7181, 0x7182, 0x7183, 0x7185, 0x7186, 0x7187, 0x7188, 0x7189, 0x718B, 0x718C, 0x718D, 0x718E, 0x7190, 0x7191, 0x7192, 0x7193, 0x7195, 0x7196, 0x7197, 0x719A, 0x719B, 0x719C, 0x719D, 0x719E, 0x71A1, 0x71A2, 0x71A3, 0x71A4, 0x71A5, 0x71A6, 0x71A7, 0x71A9, 0x71AA, 0x71AB, 0x71AD, 0x71AE, 0x71AF, 0x71B0, 0x71B1, 0x71B2, 0x71B4, 0x71B6, 0x71B7, 0x71B8, 0x71BA, 0x71BB, 0x71BC, 0x71BD, 0x71BE, 0x71BF, 0x71C0, 0x71C1, 0x71C2, 0x71C4, 0x71C5, 0x71C6, 0x71C7, 0x71C8, 0x71C9, 0x71CA, 0x71CB, 0x71CC, 0x71CD, 0x71CF, 0x71D0, 0x71D1, 0x71D2, 0x71D3, + 0x71D6, 0x71D7, 0x71D8, 0x71D9, 0x71DA, 0x71DB, 0x71DC, 0x71DD, 0x71DE, 0x71DF, 0x71E1, 0x71E2, 0x71E3, 0x71E4, 0x71E6, 0x71E8, 0x71E9, 0x71EA, 0x71EB, 0x71EC, 0x71ED, 0x71EF, 0x71F0, 0x71F1, 0x71F2, 0x71F3, 0x71F4, 0x71F5, 0x71F6, 0x71F7, 0x71F8, 0x71FA, 0x71FB, 0x71FC, 0x71FD, 0x71FE, 0x71FF, 0x7200, 0x7201, 0x7202, 0x7203, 0x7204, 0x7205, 0x7207, 0x7208, 0x7209, 0x720A, 0x720B, 0x720C, 0x720D, 0x720E, 0x720F, 0x7210, 0x7211, 0x7212, 0x7213, 0x7214, 0x7215, 0x7216, 0x7217, 0x7218, 0x7219, 0x721A, 0x721B, 0x721C, 0x721E, 0x721F, 0x7220, 0x7221, 0x7222, 0x7223, 0x7224, 0x7225, 0x7226, 0x7227, 0x7229, 0x722B, 0x722D, 0x722E, 0x722F, 0x7232, 0x7233, 0x7234, 0x723A, 0x723C, 0x723E, 0x7240, 0x7241, 0x7242, 0x7243, 0x7244, 0x7245, 0x7246, 0x7249, 0x724A, + 0x724B, 0x724E, 0x724F, 0x7250, 0x7251, 0x7253, 0x7254, 0x7255, 0x7257, 0x7258, 0x725A, 0x725C, 0x725E, 0x7260, 0x7263, 0x7264, 0x7265, 0x7268, 0x726A, 0x726B, 0x726C, 0x726D, 0x7270, 0x7271, 0x7273, 0x7274, 0x7276, 0x7277, 0x7278, 0x727B, 0x727C, 0x727D, 0x7282, 0x7283, 0x7285, 0x7286, 0x7287, 0x7288, 0x7289, 0x728C, 0x728E, 0x7290, 0x7291, 0x7293, 0x7294, 0x7295, 0x7296, 0x7297, 0x7298, 0x7299, 0x729A, 0x729B, 0x729C, 0x729D, 0x729E, 0x72A0, 0x72A1, 0x72A2, 0x72A3, 0x72A4, 0x72A5, 0x72A6, 0x72A7, 0x72A8, 0x72A9, 0x72AA, 0x72AB, 0x72AE, 0x72B1, 0x72B2, 0x72B3, 0x72B5, 0x72BA, 0x72BB, 0x72BC, 0x72BD, 0x72BE, 0x72BF, 0x72C0, 0x72C5, 0x72C6, 0x72C7, 0x72C9, 0x72CA, 0x72CB, 0x72CC, 0x72CF, 0x72D1, 0x72D3, 0x72D4, 0x72D5, 0x72D6, 0x72D8, 0x72DA, 0x72DB, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x3000, 0x3001, 0x3002, 0x00B7, 0x02C9, 0x02C7, 0x00A8, 0x3003, 0x3005, 0x2014, 0xFF5E, 0x2016, 0x2026, 0x2018, 0x2019, 0x201C, 0x201D, 0x3014, 0x3015, 0x3008, 0x3009, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x3016, 0x3017, 0x3010, 0x3011, 0x00B1, 0x00D7, 0x00F7, 0x2236, 0x2227, 0x2228, 0x2211, 0x220F, 0x222A, 0x2229, 0x2208, 0x2237, 0x221A, 0x22A5, 0x2225, 0x2220, 0x2312, 0x2299, 0x222B, 0x222E, 0x2261, 0x224C, 0x2248, 0x223D, 0x221D, 0x2260, 0x226E, 0x226F, 0x2264, 0x2265, 0x221E, 0x2235, 0x2234, 0x2642, 0x2640, 0x00B0, 0x2032, 0x2033, 0x2103, 0xFF04, 0x00A4, 0xFFE0, 0xFFE1, 0x2030, 0x00A7, 0x2116, 0x2606, 0x2605, 0x25CB, 0x25CF, 0x25CE, 0x25C7, 0x25C6, 0x25A1, 0x25A0, 0x25B3, 0x25B2, 0x203B, 0x2192, 0x2190, 0x2191, 0x2193, 0x3013, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2488, 0x2489, 0x248A, 0x248B, 0x248C, 0x248D, 0x248E, 0x248F, 0x2490, 0x2491, 0x2492, 0x2493, 0x2494, 0x2495, 0x2496, 0x2497, 0x2498, 0x2499, 0x249A, 0x249B, 0x2474, 0x2475, 0x2476, 0x2477, 0x2478, 0x2479, 0x247A, 0x247B, 0x247C, 0x247D, 0x247E, 0x247F, 0x2480, 0x2481, 0x2482, 0x2483, 0x2484, 0x2485, 0x2486, 0x2487, 0x2460, 0x2461, 0x2462, 0x2463, 0x2464, 0x2465, 0x2466, 0x2467, 0x2468, 0x2469, 0x0000, 0x0000, 0x3220, 0x3221, 0x3222, 0x3223, 0x3224, 0x3225, 0x3226, 0x3227, 0x3228, 0x3229, 0x0000, 0x0000, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x216A, 0x216B, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0xFF01, 0xFF02, 0xFF03, 0xFFE5, 0xFF05, 0xFF06, 0xFF07, 0xFF08, 0xFF09, 0xFF0A, 0xFF0B, 0xFF0C, 0xFF0D, 0xFF0E, 0xFF0F, 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0xFF1A, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0xFF1F, 0xFF20, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F, 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF3B, 0xFF3C, 0xFF3D, 0xFF3E, 0xFF3F, 0xFF40, 0xFF41, 0xFF42, 0xFF43, 0xFF44, 0xFF45, 0xFF46, 0xFF47, 0xFF48, 0xFF49, 0xFF4A, 0xFF4B, 0xFF4C, 0xFF4D, 0xFF4E, 0xFF4F, 0xFF50, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55, 0xFF56, 0xFF57, 0xFF58, 0xFF59, 0xFF5A, 0xFF5B, 0xFF5C, 0xFF5D, 0xFFE3, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x3041, 0x3042, 0x3043, 0x3044, 0x3045, 0x3046, 0x3047, 0x3048, 0x3049, 0x304A, 0x304B, 0x304C, 0x304D, 0x304E, 0x304F, 0x3050, 0x3051, 0x3052, 0x3053, 0x3054, 0x3055, 0x3056, 0x3057, 0x3058, 0x3059, 0x305A, 0x305B, 0x305C, 0x305D, 0x305E, 0x305F, 0x3060, 0x3061, 0x3062, 0x3063, 0x3064, 0x3065, 0x3066, 0x3067, 0x3068, 0x3069, 0x306A, 0x306B, 0x306C, 0x306D, 0x306E, 0x306F, 0x3070, 0x3071, 0x3072, 0x3073, 0x3074, 0x3075, 0x3076, 0x3077, 0x3078, 0x3079, 0x307A, 0x307B, 0x307C, 0x307D, 0x307E, 0x307F, 0x3080, 0x3081, 0x3082, 0x3083, 0x3084, 0x3085, 0x3086, 0x3087, 0x3088, 0x3089, 0x308A, 0x308B, 0x308C, 0x308D, 0x308E, 0x308F, 0x3090, 0x3091, 0x3092, 0x3093, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x30A1, 0x30A2, 0x30A3, 0x30A4, 0x30A5, 0x30A6, 0x30A7, 0x30A8, 0x30A9, 0x30AA, 0x30AB, 0x30AC, 0x30AD, 0x30AE, 0x30AF, 0x30B0, 0x30B1, 0x30B2, 0x30B3, 0x30B4, 0x30B5, 0x30B6, 0x30B7, 0x30B8, 0x30B9, 0x30BA, 0x30BB, 0x30BC, 0x30BD, 0x30BE, 0x30BF, 0x30C0, 0x30C1, 0x30C2, 0x30C3, 0x30C4, 0x30C5, 0x30C6, 0x30C7, 0x30C8, 0x30C9, 0x30CA, 0x30CB, 0x30CC, 0x30CD, 0x30CE, 0x30CF, 0x30D0, 0x30D1, 0x30D2, 0x30D3, 0x30D4, 0x30D5, 0x30D6, 0x30D7, 0x30D8, 0x30D9, 0x30DA, 0x30DB, 0x30DC, 0x30DD, 0x30DE, 0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30E3, 0x30E4, 0x30E5, 0x30E6, 0x30E7, 0x30E8, 0x30E9, 0x30EA, 0x30EB, 0x30EC, 0x30ED, 0x30EE, 0x30EF, 0x30F0, 0x30F1, 0x30F2, 0x30F3, 0x30F4, 0x30F5, 0x30F6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFE35, 0xFE36, 0xFE39, 0xFE3A, 0xFE3F, 0xFE40, 0xFE3D, 0xFE3E, 0xFE41, 0xFE42, 0xFE43, 0xFE44, 0x0000, 0x0000, 0xFE3B, 0xFE3C, 0xFE37, 0xFE38, 0xFE31, 0x0000, 0xFE33, 0xFE34, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0451, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x02CA, 0x02CB, 0x02D9, 0x2013, 0x2015, 0x2025, 0x2035, 0x2105, 0x2109, 0x2196, 0x2197, 0x2198, 0x2199, 0x2215, 0x221F, 0x2223, 0x2252, 0x2266, 0x2267, 0x22BF, 0x2550, 0x2551, 0x2552, 0x2553, 0x2554, 0x2555, 0x2556, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255D, 0x255E, 0x255F, 0x2560, 0x2561, 0x2562, 0x2563, 0x2564, 0x2565, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x256D, 0x256E, 0x256F, 0x2570, 0x2571, 0x2572, 0x2573, 0x2581, 0x2582, 0x2583, 0x2584, 0x2585, 0x2586, 0x2587, 0x2588, 0x2589, 0x258A, 0x258B, 0x258C, 0x258D, 0x258E, 0x258F, 0x2593, 0x2594, 0x2595, 0x25BC, 0x25BD, 0x25E2, 0x25E3, 0x25E4, 0x25E5, 0x2609, 0x2295, 0x3012, 0x301D, 0x301E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0101, 0x00E1, 0x01CE, 0x00E0, 0x0113, 0x00E9, 0x011B, 0x00E8, 0x012B, 0x00ED, 0x01D0, 0x00EC, 0x014D, 0x00F3, 0x01D2, 0x00F2, 0x016B, 0x00FA, 0x01D4, 0x00F9, 0x01D6, 0x01D8, 0x01DA, 0x01DC, 0x00FC, 0x00EA, 0x0251, 0x0000, 0x0144, 0x0148, 0x0000, 0x0261, 0x0000, 0x0000, 0x0000, 0x0000, 0x3105, 0x3106, 0x3107, 0x3108, 0x3109, 0x310A, 0x310B, 0x310C, 0x310D, 0x310E, 0x310F, 0x3110, 0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116, 0x3117, 0x3118, 0x3119, 0x311A, 0x311B, 0x311C, 0x311D, 0x311E, 0x311F, 0x3120, 0x3121, 0x3122, 0x3123, 0x3124, 0x3125, 0x3126, 0x3127, 0x3128, 0x3129, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x3021, 0x3022, 0x3023, 0x3024, 0x3025, 0x3026, 0x3027, 0x3028, 0x3029, 0x32A3, 0x338E, 0x338F, 0x339C, 0x339D, 0x339E, 0x33A1, 0x33C4, 0x33CE, 0x33D1, 0x33D2, 0x33D5, 0xFE30, 0xFFE2, 0xFFE4, 0x0000, 0x2121, 0x3231, 0x0000, 0x2010, 0x0000, 0x0000, 0x0000, 0x30FC, 0x309B, 0x309C, 0x30FD, 0x30FE, 0x3006, 0x309D, 0x309E, 0xFE49, 0xFE4A, 0xFE4B, 0xFE4C, 0xFE4D, 0xFE4E, 0xFE4F, 0xFE50, 0xFE51, 0xFE52, 0xFE54, 0xFE55, 0xFE56, 0xFE57, 0xFE59, 0xFE5A, 0xFE5B, 0xFE5C, 0xFE5D, 0xFE5E, 0xFE5F, 0xFE60, 0xFE61, 0xFE62, 0xFE63, 0xFE64, 0xFE65, 0xFE66, 0xFE68, 0xFE69, 0xFE6A, 0xFE6B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x3007, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x2500, 0x2501, 0x2502, 0x2503, 0x2504, 0x2505, 0x2506, 0x2507, 0x2508, 0x2509, 0x250A, 0x250B, 0x250C, 0x250D, 0x250E, 0x250F, 0x2510, 0x2511, 0x2512, 0x2513, 0x2514, 0x2515, 0x2516, 0x2517, 0x2518, 0x2519, 0x251A, 0x251B, 0x251C, 0x251D, 0x251E, 0x251F, 0x2520, 0x2521, 0x2522, 0x2523, 0x2524, 0x2525, 0x2526, 0x2527, 0x2528, 0x2529, 0x252A, 0x252B, 0x252C, 0x252D, 0x252E, 0x252F, 0x2530, 0x2531, 0x2532, 0x2533, 0x2534, 0x2535, 0x2536, 0x2537, 0x2538, 0x2539, 0x253A, 0x253B, 0x253C, 0x253D, 0x253E, 0x253F, 0x2540, 0x2541, 0x2542, 0x2543, 0x2544, 0x2545, 0x2546, 0x2547, 0x2548, 0x2549, 0x254A, 0x254B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x72DC, 0x72DD, 0x72DF, 0x72E2, 0x72E3, 0x72E4, 0x72E5, 0x72E6, 0x72E7, 0x72EA, 0x72EB, 0x72F5, 0x72F6, 0x72F9, 0x72FD, 0x72FE, 0x72FF, 0x7300, 0x7302, 0x7304, 0x7305, 0x7306, 0x7307, 0x7308, 0x7309, 0x730B, 0x730C, 0x730D, 0x730F, 0x7310, 0x7311, 0x7312, 0x7314, 0x7318, 0x7319, 0x731A, 0x731F, 0x7320, 0x7323, 0x7324, 0x7326, 0x7327, 0x7328, 0x732D, 0x732F, 0x7330, 0x7332, 0x7333, 0x7335, 0x7336, 0x733A, 0x733B, 0x733C, 0x733D, 0x7340, 0x7341, 0x7342, 0x7343, 0x7344, 0x7345, 0x7346, 0x7347, 0x7348, 0x7349, 0x734A, 0x734B, 0x734C, 0x734E, 0x734F, 0x7351, 0x7353, 0x7354, 0x7355, 0x7356, 0x7358, 0x7359, 0x735A, 0x735B, 0x735C, 0x735D, 0x735E, 0x735F, 0x7361, 0x7362, 0x7363, 0x7364, 0x7365, 0x7366, 0x7367, 0x7368, 0x7369, 0x736A, 0x736B, 0x736E, 0x7370, + 0x7371, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x7372, 0x7373, 0x7374, 0x7375, 0x7376, 0x7377, 0x7378, 0x7379, 0x737A, 0x737B, 0x737C, 0x737D, 0x737F, 0x7380, 0x7381, 0x7382, 0x7383, 0x7385, 0x7386, 0x7388, 0x738A, 0x738C, 0x738D, 0x738F, 0x7390, 0x7392, 0x7393, 0x7394, 0x7395, 0x7397, 0x7398, 0x7399, 0x739A, 0x739C, 0x739D, 0x739E, 0x73A0, 0x73A1, 0x73A3, 0x73A4, 0x73A5, 0x73A6, 0x73A7, 0x73A8, 0x73AA, 0x73AC, 0x73AD, 0x73B1, 0x73B4, 0x73B5, 0x73B6, 0x73B8, 0x73B9, 0x73BC, 0x73BD, 0x73BE, 0x73BF, 0x73C1, 0x73C3, 0x73C4, 0x73C5, 0x73C6, 0x73C7, 0x73CB, 0x73CC, 0x73CE, 0x73D2, 0x73D3, 0x73D4, 0x73D5, 0x73D6, 0x73D7, 0x73D8, 0x73DA, 0x73DB, 0x73DC, 0x73DD, 0x73DF, 0x73E1, 0x73E2, 0x73E3, 0x73E4, 0x73E6, 0x73E8, 0x73EA, 0x73EB, 0x73EC, 0x73EE, 0x73EF, 0x73F0, 0x73F1, 0x73F3, 0x73F4, 0x73F5, 0x73F6, + 0x73F7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x73F8, 0x73F9, 0x73FA, 0x73FB, 0x73FC, 0x73FD, 0x73FE, 0x73FF, 0x7400, 0x7401, 0x7402, 0x7404, 0x7407, 0x7408, 0x740B, 0x740C, 0x740D, 0x740E, 0x7411, 0x7412, 0x7413, 0x7414, 0x7415, 0x7416, 0x7417, 0x7418, 0x7419, 0x741C, 0x741D, 0x741E, 0x741F, 0x7420, 0x7421, 0x7423, 0x7424, 0x7427, 0x7429, 0x742B, 0x742D, 0x742F, 0x7431, 0x7432, 0x7437, 0x7438, 0x7439, 0x743A, 0x743B, 0x743D, 0x743E, 0x743F, 0x7440, 0x7442, 0x7443, 0x7444, 0x7445, 0x7446, 0x7447, 0x7448, 0x7449, 0x744A, 0x744B, 0x744C, 0x744D, 0x744E, 0x744F, 0x7450, 0x7451, 0x7452, 0x7453, 0x7454, 0x7456, 0x7458, 0x745D, 0x7460, 0x7461, 0x7462, 0x7463, 0x7464, 0x7465, 0x7466, 0x7467, 0x7468, 0x7469, 0x746A, 0x746B, 0x746C, 0x746E, 0x746F, 0x7471, 0x7472, 0x7473, 0x7474, 0x7475, 0x7478, 0x7479, + 0x747A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x747B, 0x747C, 0x747D, 0x747F, 0x7482, 0x7484, 0x7485, 0x7486, 0x7488, 0x7489, 0x748A, 0x748C, 0x748D, 0x748F, 0x7491, 0x7492, 0x7493, 0x7494, 0x7495, 0x7496, 0x7497, 0x7498, 0x7499, 0x749A, 0x749B, 0x749D, 0x749F, 0x74A0, 0x74A1, 0x74A2, 0x74A3, 0x74A4, 0x74A5, 0x74A6, 0x74AA, 0x74AB, 0x74AC, 0x74AD, 0x74AE, 0x74AF, 0x74B0, 0x74B1, 0x74B2, 0x74B3, 0x74B4, 0x74B5, 0x74B6, 0x74B7, 0x74B8, 0x74B9, 0x74BB, 0x74BC, 0x74BD, 0x74BE, 0x74BF, 0x74C0, 0x74C1, 0x74C2, 0x74C3, 0x74C4, 0x74C5, 0x74C6, 0x74C7, 0x74C8, 0x74C9, 0x74CA, 0x74CB, 0x74CC, 0x74CD, 0x74CE, 0x74CF, 0x74D0, 0x74D1, 0x74D3, 0x74D4, 0x74D5, 0x74D6, 0x74D7, 0x74D8, 0x74D9, 0x74DA, 0x74DB, 0x74DD, 0x74DF, 0x74E1, 0x74E5, 0x74E7, 0x74E8, 0x74E9, 0x74EA, 0x74EB, 0x74EC, 0x74ED, 0x74F0, 0x74F1, + 0x74F2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x74F3, 0x74F5, 0x74F8, 0x74F9, 0x74FA, 0x74FB, 0x74FC, 0x74FD, 0x74FE, 0x7500, 0x7501, 0x7502, 0x7503, 0x7505, 0x7506, 0x7507, 0x7508, 0x7509, 0x750A, 0x750B, 0x750C, 0x750E, 0x7510, 0x7512, 0x7514, 0x7515, 0x7516, 0x7517, 0x751B, 0x751D, 0x751E, 0x7520, 0x7521, 0x7522, 0x7523, 0x7524, 0x7526, 0x7527, 0x752A, 0x752E, 0x7534, 0x7536, 0x7539, 0x753C, 0x753D, 0x753F, 0x7541, 0x7542, 0x7543, 0x7544, 0x7546, 0x7547, 0x7549, 0x754A, 0x754D, 0x7550, 0x7551, 0x7552, 0x7553, 0x7555, 0x7556, 0x7557, 0x7558, 0x755D, 0x755E, 0x755F, 0x7560, 0x7561, 0x7562, 0x7563, 0x7564, 0x7567, 0x7568, 0x7569, 0x756B, 0x756C, 0x756D, 0x756E, 0x756F, 0x7570, 0x7571, 0x7573, 0x7575, 0x7576, 0x7577, 0x757A, 0x757B, 0x757C, 0x757D, 0x757E, 0x7580, 0x7581, 0x7582, 0x7584, 0x7585, + 0x7587, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x7588, 0x7589, 0x758A, 0x758C, 0x758D, 0x758E, 0x7590, 0x7593, 0x7595, 0x7598, 0x759B, 0x759C, 0x759E, 0x75A2, 0x75A6, 0x75A7, 0x75A8, 0x75A9, 0x75AA, 0x75AD, 0x75B6, 0x75B7, 0x75BA, 0x75BB, 0x75BF, 0x75C0, 0x75C1, 0x75C6, 0x75CB, 0x75CC, 0x75CE, 0x75CF, 0x75D0, 0x75D1, 0x75D3, 0x75D7, 0x75D9, 0x75DA, 0x75DC, 0x75DD, 0x75DF, 0x75E0, 0x75E1, 0x75E5, 0x75E9, 0x75EC, 0x75ED, 0x75EE, 0x75EF, 0x75F2, 0x75F3, 0x75F5, 0x75F6, 0x75F7, 0x75F8, 0x75FA, 0x75FB, 0x75FD, 0x75FE, 0x7602, 0x7604, 0x7606, 0x7607, 0x7608, 0x7609, 0x760B, 0x760D, 0x760E, 0x760F, 0x7611, 0x7612, 0x7613, 0x7614, 0x7616, 0x761A, 0x761C, 0x761D, 0x761E, 0x7621, 0x7623, 0x7627, 0x7628, 0x762C, 0x762E, 0x762F, 0x7631, 0x7632, 0x7636, 0x7637, 0x7639, 0x763A, 0x763B, 0x763D, 0x7641, 0x7642, + 0x7644, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x7645, 0x7646, 0x7647, 0x7648, 0x7649, 0x764A, 0x764B, 0x764E, 0x764F, 0x7650, 0x7651, 0x7652, 0x7653, 0x7655, 0x7657, 0x7658, 0x7659, 0x765A, 0x765B, 0x765D, 0x765F, 0x7660, 0x7661, 0x7662, 0x7664, 0x7665, 0x7666, 0x7667, 0x7668, 0x7669, 0x766A, 0x766C, 0x766D, 0x766E, 0x7670, 0x7671, 0x7672, 0x7673, 0x7674, 0x7675, 0x7676, 0x7677, 0x7679, 0x767A, 0x767C, 0x767F, 0x7680, 0x7681, 0x7683, 0x7685, 0x7689, 0x768A, 0x768C, 0x768D, 0x768F, 0x7690, 0x7692, 0x7694, 0x7695, 0x7697, 0x7698, 0x769A, 0x769B, 0x769C, 0x769D, 0x769E, 0x769F, 0x76A0, 0x76A1, 0x76A2, 0x76A3, 0x76A5, 0x76A6, 0x76A7, 0x76A8, 0x76A9, 0x76AA, 0x76AB, 0x76AC, 0x76AD, 0x76AF, 0x76B0, 0x76B3, 0x76B5, 0x76B6, 0x76B7, 0x76B8, 0x76B9, 0x76BA, 0x76BB, 0x76BC, 0x76BD, 0x76BE, 0x76C0, 0x76C1, + 0x76C3, 0x554A, 0x963F, 0x57C3, 0x6328, 0x54CE, 0x5509, 0x54C0, 0x7691, 0x764C, 0x853C, 0x77EE, 0x827E, 0x788D, 0x7231, 0x9698, 0x978D, 0x6C28, 0x5B89, 0x4FFA, 0x6309, 0x6697, 0x5CB8, 0x80FA, 0x6848, 0x80AE, 0x6602, 0x76CE, 0x51F9, 0x6556, 0x71AC, 0x7FF1, 0x8884, 0x50B2, 0x5965, 0x61CA, 0x6FB3, 0x82AD, 0x634C, 0x6252, 0x53ED, 0x5427, 0x7B06, 0x516B, 0x75A4, 0x5DF4, 0x62D4, 0x8DCB, 0x9776, 0x628A, 0x8019, 0x575D, 0x9738, 0x7F62, 0x7238, 0x767D, 0x67CF, 0x767E, 0x6446, 0x4F70, 0x8D25, 0x62DC, 0x7A17, 0x6591, 0x73ED, 0x642C, 0x6273, 0x822C, 0x9881, 0x677F, 0x7248, 0x626E, 0x62CC, 0x4F34, 0x74E3, 0x534A, 0x529E, 0x7ECA, 0x90A6, 0x5E2E, 0x6886, 0x699C, 0x8180, 0x7ED1, 0x68D2, 0x78C5, 0x868C, 0x9551, 0x508D, 0x8C24, 0x82DE, 0x80DE, 0x5305, 0x8912, 0x5265, + 0x76C4, 0x76C7, 0x76C9, 0x76CB, 0x76CC, 0x76D3, 0x76D5, 0x76D9, 0x76DA, 0x76DC, 0x76DD, 0x76DE, 0x76E0, 0x76E1, 0x76E2, 0x76E3, 0x76E4, 0x76E6, 0x76E7, 0x76E8, 0x76E9, 0x76EA, 0x76EB, 0x76EC, 0x76ED, 0x76F0, 0x76F3, 0x76F5, 0x76F6, 0x76F7, 0x76FA, 0x76FB, 0x76FD, 0x76FF, 0x7700, 0x7702, 0x7703, 0x7705, 0x7706, 0x770A, 0x770C, 0x770E, 0x770F, 0x7710, 0x7711, 0x7712, 0x7713, 0x7714, 0x7715, 0x7716, 0x7717, 0x7718, 0x771B, 0x771C, 0x771D, 0x771E, 0x7721, 0x7723, 0x7724, 0x7725, 0x7727, 0x772A, 0x772B, 0x772C, 0x772E, 0x7730, 0x7731, 0x7732, 0x7733, 0x7734, 0x7739, 0x773B, 0x773D, 0x773E, 0x773F, 0x7742, 0x7744, 0x7745, 0x7746, 0x7748, 0x7749, 0x774A, 0x774B, 0x774C, 0x774D, 0x774E, 0x774F, 0x7752, 0x7753, 0x7754, 0x7755, 0x7756, 0x7757, 0x7758, 0x7759, + 0x775C, 0x8584, 0x96F9, 0x4FDD, 0x5821, 0x9971, 0x5B9D, 0x62B1, 0x62A5, 0x66B4, 0x8C79, 0x9C8D, 0x7206, 0x676F, 0x7891, 0x60B2, 0x5351, 0x5317, 0x8F88, 0x80CC, 0x8D1D, 0x94A1, 0x500D, 0x72C8, 0x5907, 0x60EB, 0x7119, 0x88AB, 0x5954, 0x82EF, 0x672C, 0x7B28, 0x5D29, 0x7EF7, 0x752D, 0x6CF5, 0x8E66, 0x8FF8, 0x903C, 0x9F3B, 0x6BD4, 0x9119, 0x7B14, 0x5F7C, 0x78A7, 0x84D6, 0x853D, 0x6BD5, 0x6BD9, 0x6BD6, 0x5E01, 0x5E87, 0x75F9, 0x95ED, 0x655D, 0x5F0A, 0x5FC5, 0x8F9F, 0x58C1, 0x81C2, 0x907F, 0x965B, 0x97AD, 0x8FB9, 0x7F16, 0x8D2C, 0x6241, 0x4FBF, 0x53D8, 0x535E, 0x8FA8, 0x8FA9, 0x8FAB, 0x904D, 0x6807, 0x5F6A, 0x8198, 0x8868, 0x9CD6, 0x618B, 0x522B, 0x762A, 0x5F6C, 0x658C, 0x6FD2, 0x6EE8, 0x5BBE, 0x6448, 0x5175, 0x51B0, 0x67C4, 0x4E19, 0x79C9, 0x997C, 0x70B3, + 0x775D, 0x775E, 0x775F, 0x7760, 0x7764, 0x7767, 0x7769, 0x776A, 0x776D, 0x776E, 0x776F, 0x7770, 0x7771, 0x7772, 0x7773, 0x7774, 0x7775, 0x7776, 0x7777, 0x7778, 0x777A, 0x777B, 0x777C, 0x7781, 0x7782, 0x7783, 0x7786, 0x7787, 0x7788, 0x7789, 0x778A, 0x778B, 0x778F, 0x7790, 0x7793, 0x7794, 0x7795, 0x7796, 0x7797, 0x7798, 0x7799, 0x779A, 0x779B, 0x779C, 0x779D, 0x779E, 0x77A1, 0x77A3, 0x77A4, 0x77A6, 0x77A8, 0x77AB, 0x77AD, 0x77AE, 0x77AF, 0x77B1, 0x77B2, 0x77B4, 0x77B6, 0x77B7, 0x77B8, 0x77B9, 0x77BA, 0x77BC, 0x77BE, 0x77C0, 0x77C1, 0x77C2, 0x77C3, 0x77C4, 0x77C5, 0x77C6, 0x77C7, 0x77C8, 0x77C9, 0x77CA, 0x77CB, 0x77CC, 0x77CE, 0x77CF, 0x77D0, 0x77D1, 0x77D2, 0x77D3, 0x77D4, 0x77D5, 0x77D6, 0x77D8, 0x77D9, 0x77DA, 0x77DD, 0x77DE, 0x77DF, 0x77E0, 0x77E1, + 0x77E4, 0x75C5, 0x5E76, 0x73BB, 0x83E0, 0x64AD, 0x62E8, 0x94B5, 0x6CE2, 0x535A, 0x52C3, 0x640F, 0x94C2, 0x7B94, 0x4F2F, 0x5E1B, 0x8236, 0x8116, 0x818A, 0x6E24, 0x6CCA, 0x9A73, 0x6355, 0x535C, 0x54FA, 0x8865, 0x57E0, 0x4E0D, 0x5E03, 0x6B65, 0x7C3F, 0x90E8, 0x6016, 0x64E6, 0x731C, 0x88C1, 0x6750, 0x624D, 0x8D22, 0x776C, 0x8E29, 0x91C7, 0x5F69, 0x83DC, 0x8521, 0x9910, 0x53C2, 0x8695, 0x6B8B, 0x60ED, 0x60E8, 0x707F, 0x82CD, 0x8231, 0x4ED3, 0x6CA7, 0x85CF, 0x64CD, 0x7CD9, 0x69FD, 0x66F9, 0x8349, 0x5395, 0x7B56, 0x4FA7, 0x518C, 0x6D4B, 0x5C42, 0x8E6D, 0x63D2, 0x53C9, 0x832C, 0x8336, 0x67E5, 0x78B4, 0x643D, 0x5BDF, 0x5C94, 0x5DEE, 0x8BE7, 0x62C6, 0x67F4, 0x8C7A, 0x6400, 0x63BA, 0x8749, 0x998B, 0x8C17, 0x7F20, 0x94F2, 0x4EA7, 0x9610, 0x98A4, 0x660C, 0x7316, + 0x77E6, 0x77E8, 0x77EA, 0x77EF, 0x77F0, 0x77F1, 0x77F2, 0x77F4, 0x77F5, 0x77F7, 0x77F9, 0x77FA, 0x77FB, 0x77FC, 0x7803, 0x7804, 0x7805, 0x7806, 0x7807, 0x7808, 0x780A, 0x780B, 0x780E, 0x780F, 0x7810, 0x7813, 0x7815, 0x7819, 0x781B, 0x781E, 0x7820, 0x7821, 0x7822, 0x7824, 0x7828, 0x782A, 0x782B, 0x782E, 0x782F, 0x7831, 0x7832, 0x7833, 0x7835, 0x7836, 0x783D, 0x783F, 0x7841, 0x7842, 0x7843, 0x7844, 0x7846, 0x7848, 0x7849, 0x784A, 0x784B, 0x784D, 0x784F, 0x7851, 0x7853, 0x7854, 0x7858, 0x7859, 0x785A, 0x785B, 0x785C, 0x785E, 0x785F, 0x7860, 0x7861, 0x7862, 0x7863, 0x7864, 0x7865, 0x7866, 0x7867, 0x7868, 0x7869, 0x786F, 0x7870, 0x7871, 0x7872, 0x7873, 0x7874, 0x7875, 0x7876, 0x7878, 0x7879, 0x787A, 0x787B, 0x787D, 0x787E, 0x787F, 0x7880, 0x7881, 0x7882, + 0x7883, 0x573A, 0x5C1D, 0x5E38, 0x957F, 0x507F, 0x80A0, 0x5382, 0x655E, 0x7545, 0x5531, 0x5021, 0x8D85, 0x6284, 0x949E, 0x671D, 0x5632, 0x6F6E, 0x5DE2, 0x5435, 0x7092, 0x8F66, 0x626F, 0x64A4, 0x63A3, 0x5F7B, 0x6F88, 0x90F4, 0x81E3, 0x8FB0, 0x5C18, 0x6668, 0x5FF1, 0x6C89, 0x9648, 0x8D81, 0x886C, 0x6491, 0x79F0, 0x57CE, 0x6A59, 0x6210, 0x5448, 0x4E58, 0x7A0B, 0x60E9, 0x6F84, 0x8BDA, 0x627F, 0x901E, 0x9A8B, 0x79E4, 0x5403, 0x75F4, 0x6301, 0x5319, 0x6C60, 0x8FDF, 0x5F1B, 0x9A70, 0x803B, 0x9F7F, 0x4F88, 0x5C3A, 0x8D64, 0x7FC5, 0x65A5, 0x70BD, 0x5145, 0x51B2, 0x866B, 0x5D07, 0x5BA0, 0x62BD, 0x916C, 0x7574, 0x8E0C, 0x7A20, 0x6101, 0x7B79, 0x4EC7, 0x7EF8, 0x7785, 0x4E11, 0x81ED, 0x521D, 0x51FA, 0x6A71, 0x53A8, 0x8E87, 0x9504, 0x96CF, 0x6EC1, 0x9664, 0x695A, + 0x7884, 0x7885, 0x7886, 0x7888, 0x788A, 0x788B, 0x788F, 0x7890, 0x7892, 0x7894, 0x7895, 0x7896, 0x7899, 0x789D, 0x789E, 0x78A0, 0x78A2, 0x78A4, 0x78A6, 0x78A8, 0x78A9, 0x78AA, 0x78AB, 0x78AC, 0x78AD, 0x78AE, 0x78AF, 0x78B5, 0x78B6, 0x78B7, 0x78B8, 0x78BA, 0x78BB, 0x78BC, 0x78BD, 0x78BF, 0x78C0, 0x78C2, 0x78C3, 0x78C4, 0x78C6, 0x78C7, 0x78C8, 0x78CC, 0x78CD, 0x78CE, 0x78CF, 0x78D1, 0x78D2, 0x78D3, 0x78D6, 0x78D7, 0x78D8, 0x78DA, 0x78DB, 0x78DC, 0x78DD, 0x78DE, 0x78DF, 0x78E0, 0x78E1, 0x78E2, 0x78E3, 0x78E4, 0x78E5, 0x78E6, 0x78E7, 0x78E9, 0x78EA, 0x78EB, 0x78ED, 0x78EE, 0x78EF, 0x78F0, 0x78F1, 0x78F3, 0x78F5, 0x78F6, 0x78F8, 0x78F9, 0x78FB, 0x78FC, 0x78FD, 0x78FE, 0x78FF, 0x7900, 0x7902, 0x7903, 0x7904, 0x7906, 0x7907, 0x7908, 0x7909, 0x790A, 0x790B, + 0x790C, 0x7840, 0x50A8, 0x77D7, 0x6410, 0x89E6, 0x5904, 0x63E3, 0x5DDD, 0x7A7F, 0x693D, 0x4F20, 0x8239, 0x5598, 0x4E32, 0x75AE, 0x7A97, 0x5E62, 0x5E8A, 0x95EF, 0x521B, 0x5439, 0x708A, 0x6376, 0x9524, 0x5782, 0x6625, 0x693F, 0x9187, 0x5507, 0x6DF3, 0x7EAF, 0x8822, 0x6233, 0x7EF0, 0x75B5, 0x8328, 0x78C1, 0x96CC, 0x8F9E, 0x6148, 0x74F7, 0x8BCD, 0x6B64, 0x523A, 0x8D50, 0x6B21, 0x806A, 0x8471, 0x56F1, 0x5306, 0x4ECE, 0x4E1B, 0x51D1, 0x7C97, 0x918B, 0x7C07, 0x4FC3, 0x8E7F, 0x7BE1, 0x7A9C, 0x6467, 0x5D14, 0x50AC, 0x8106, 0x7601, 0x7CB9, 0x6DEC, 0x7FE0, 0x6751, 0x5B58, 0x5BF8, 0x78CB, 0x64AE, 0x6413, 0x63AA, 0x632B, 0x9519, 0x642D, 0x8FBE, 0x7B54, 0x7629, 0x6253, 0x5927, 0x5446, 0x6B79, 0x50A3, 0x6234, 0x5E26, 0x6B86, 0x4EE3, 0x8D37, 0x888B, 0x5F85, 0x902E, + 0x790D, 0x790E, 0x790F, 0x7910, 0x7911, 0x7912, 0x7914, 0x7915, 0x7916, 0x7917, 0x7918, 0x7919, 0x791A, 0x791B, 0x791C, 0x791D, 0x791F, 0x7920, 0x7921, 0x7922, 0x7923, 0x7925, 0x7926, 0x7927, 0x7928, 0x7929, 0x792A, 0x792B, 0x792C, 0x792D, 0x792E, 0x792F, 0x7930, 0x7931, 0x7932, 0x7933, 0x7935, 0x7936, 0x7937, 0x7938, 0x7939, 0x793D, 0x793F, 0x7942, 0x7943, 0x7944, 0x7945, 0x7947, 0x794A, 0x794B, 0x794C, 0x794D, 0x794E, 0x794F, 0x7950, 0x7951, 0x7952, 0x7954, 0x7955, 0x7958, 0x7959, 0x7961, 0x7963, 0x7964, 0x7966, 0x7969, 0x796A, 0x796B, 0x796C, 0x796E, 0x7970, 0x7971, 0x7972, 0x7973, 0x7974, 0x7975, 0x7976, 0x7979, 0x797B, 0x797C, 0x797D, 0x797E, 0x797F, 0x7982, 0x7983, 0x7986, 0x7987, 0x7988, 0x7989, 0x798B, 0x798C, 0x798D, 0x798E, 0x7990, 0x7991, + 0x7992, 0x6020, 0x803D, 0x62C5, 0x4E39, 0x5355, 0x90F8, 0x63B8, 0x80C6, 0x65E6, 0x6C2E, 0x4F46, 0x60EE, 0x6DE1, 0x8BDE, 0x5F39, 0x86CB, 0x5F53, 0x6321, 0x515A, 0x8361, 0x6863, 0x5200, 0x6363, 0x8E48, 0x5012, 0x5C9B, 0x7977, 0x5BFC, 0x5230, 0x7A3B, 0x60BC, 0x9053, 0x76D7, 0x5FB7, 0x5F97, 0x7684, 0x8E6C, 0x706F, 0x767B, 0x7B49, 0x77AA, 0x51F3, 0x9093, 0x5824, 0x4F4E, 0x6EF4, 0x8FEA, 0x654C, 0x7B1B, 0x72C4, 0x6DA4, 0x7FDF, 0x5AE1, 0x62B5, 0x5E95, 0x5730, 0x8482, 0x7B2C, 0x5E1D, 0x5F1F, 0x9012, 0x7F14, 0x98A0, 0x6382, 0x6EC7, 0x7898, 0x70B9, 0x5178, 0x975B, 0x57AB, 0x7535, 0x4F43, 0x7538, 0x5E97, 0x60E6, 0x5960, 0x6DC0, 0x6BBF, 0x7889, 0x53FC, 0x96D5, 0x51CB, 0x5201, 0x6389, 0x540A, 0x9493, 0x8C03, 0x8DCC, 0x7239, 0x789F, 0x8776, 0x8FED, 0x8C0D, 0x53E0, + 0x7993, 0x7994, 0x7995, 0x7996, 0x7997, 0x7998, 0x7999, 0x799B, 0x799C, 0x799D, 0x799E, 0x799F, 0x79A0, 0x79A1, 0x79A2, 0x79A3, 0x79A4, 0x79A5, 0x79A6, 0x79A8, 0x79A9, 0x79AA, 0x79AB, 0x79AC, 0x79AD, 0x79AE, 0x79AF, 0x79B0, 0x79B1, 0x79B2, 0x79B4, 0x79B5, 0x79B6, 0x79B7, 0x79B8, 0x79BC, 0x79BF, 0x79C2, 0x79C4, 0x79C5, 0x79C7, 0x79C8, 0x79CA, 0x79CC, 0x79CE, 0x79CF, 0x79D0, 0x79D3, 0x79D4, 0x79D6, 0x79D7, 0x79D9, 0x79DA, 0x79DB, 0x79DC, 0x79DD, 0x79DE, 0x79E0, 0x79E1, 0x79E2, 0x79E5, 0x79E8, 0x79EA, 0x79EC, 0x79EE, 0x79F1, 0x79F2, 0x79F3, 0x79F4, 0x79F5, 0x79F6, 0x79F7, 0x79F9, 0x79FA, 0x79FC, 0x79FE, 0x79FF, 0x7A01, 0x7A04, 0x7A05, 0x7A07, 0x7A08, 0x7A09, 0x7A0A, 0x7A0C, 0x7A0F, 0x7A10, 0x7A11, 0x7A12, 0x7A13, 0x7A15, 0x7A16, 0x7A18, 0x7A19, 0x7A1B, + 0x7A1C, 0x4E01, 0x76EF, 0x53EE, 0x9489, 0x9876, 0x9F0E, 0x952D, 0x5B9A, 0x8BA2, 0x4E22, 0x4E1C, 0x51AC, 0x8463, 0x61C2, 0x52A8, 0x680B, 0x4F97, 0x606B, 0x51BB, 0x6D1E, 0x515C, 0x6296, 0x6597, 0x9661, 0x8C46, 0x9017, 0x75D8, 0x90FD, 0x7763, 0x6BD2, 0x728A, 0x72EC, 0x8BFB, 0x5835, 0x7779, 0x8D4C, 0x675C, 0x9540, 0x809A, 0x5EA6, 0x6E21, 0x5992, 0x7AEF, 0x77ED, 0x953B, 0x6BB5, 0x65AD, 0x7F0E, 0x5806, 0x5151, 0x961F, 0x5BF9, 0x58A9, 0x5428, 0x8E72, 0x6566, 0x987F, 0x56E4, 0x949D, 0x76FE, 0x9041, 0x6387, 0x54C6, 0x591A, 0x593A, 0x579B, 0x8EB2, 0x6735, 0x8DFA, 0x8235, 0x5241, 0x60F0, 0x5815, 0x86FE, 0x5CE8, 0x9E45, 0x4FC4, 0x989D, 0x8BB9, 0x5A25, 0x6076, 0x5384, 0x627C, 0x904F, 0x9102, 0x997F, 0x6069, 0x800C, 0x513F, 0x8033, 0x5C14, 0x9975, 0x6D31, 0x4E8C, + 0x7A1D, 0x7A1F, 0x7A21, 0x7A22, 0x7A24, 0x7A25, 0x7A26, 0x7A27, 0x7A28, 0x7A29, 0x7A2A, 0x7A2B, 0x7A2C, 0x7A2D, 0x7A2E, 0x7A2F, 0x7A30, 0x7A31, 0x7A32, 0x7A34, 0x7A35, 0x7A36, 0x7A38, 0x7A3A, 0x7A3E, 0x7A40, 0x7A41, 0x7A42, 0x7A43, 0x7A44, 0x7A45, 0x7A47, 0x7A48, 0x7A49, 0x7A4A, 0x7A4B, 0x7A4C, 0x7A4D, 0x7A4E, 0x7A4F, 0x7A50, 0x7A52, 0x7A53, 0x7A54, 0x7A55, 0x7A56, 0x7A58, 0x7A59, 0x7A5A, 0x7A5B, 0x7A5C, 0x7A5D, 0x7A5E, 0x7A5F, 0x7A60, 0x7A61, 0x7A62, 0x7A63, 0x7A64, 0x7A65, 0x7A66, 0x7A67, 0x7A68, 0x7A69, 0x7A6A, 0x7A6B, 0x7A6C, 0x7A6D, 0x7A6E, 0x7A6F, 0x7A71, 0x7A72, 0x7A73, 0x7A75, 0x7A7B, 0x7A7C, 0x7A7D, 0x7A7E, 0x7A82, 0x7A85, 0x7A87, 0x7A89, 0x7A8A, 0x7A8B, 0x7A8C, 0x7A8E, 0x7A8F, 0x7A90, 0x7A93, 0x7A94, 0x7A99, 0x7A9A, 0x7A9B, 0x7A9E, 0x7AA1, + 0x7AA2, 0x8D30, 0x53D1, 0x7F5A, 0x7B4F, 0x4F10, 0x4E4F, 0x9600, 0x6CD5, 0x73D0, 0x85E9, 0x5E06, 0x756A, 0x7FFB, 0x6A0A, 0x77FE, 0x9492, 0x7E41, 0x51E1, 0x70E6, 0x53CD, 0x8FD4, 0x8303, 0x8D29, 0x72AF, 0x996D, 0x6CDB, 0x574A, 0x82B3, 0x65B9, 0x80AA, 0x623F, 0x9632, 0x59A8, 0x4EFF, 0x8BBF, 0x7EBA, 0x653E, 0x83F2, 0x975E, 0x5561, 0x98DE, 0x80A5, 0x532A, 0x8BFD, 0x5420, 0x80BA, 0x5E9F, 0x6CB8, 0x8D39, 0x82AC, 0x915A, 0x5429, 0x6C1B, 0x5206, 0x7EB7, 0x575F, 0x711A, 0x6C7E, 0x7C89, 0x594B, 0x4EFD, 0x5FFF, 0x6124, 0x7CAA, 0x4E30, 0x5C01, 0x67AB, 0x8702, 0x5CF0, 0x950B, 0x98CE, 0x75AF, 0x70FD, 0x9022, 0x51AF, 0x7F1D, 0x8BBD, 0x5949, 0x51E4, 0x4F5B, 0x5426, 0x592B, 0x6577, 0x80A4, 0x5B75, 0x6276, 0x62C2, 0x8F90, 0x5E45, 0x6C1F, 0x7B26, 0x4F0F, 0x4FD8, 0x670D, + 0x7AA3, 0x7AA4, 0x7AA7, 0x7AA9, 0x7AAA, 0x7AAB, 0x7AAE, 0x7AAF, 0x7AB0, 0x7AB1, 0x7AB2, 0x7AB4, 0x7AB5, 0x7AB6, 0x7AB7, 0x7AB8, 0x7AB9, 0x7ABA, 0x7ABB, 0x7ABC, 0x7ABD, 0x7ABE, 0x7AC0, 0x7AC1, 0x7AC2, 0x7AC3, 0x7AC4, 0x7AC5, 0x7AC6, 0x7AC7, 0x7AC8, 0x7AC9, 0x7ACA, 0x7ACC, 0x7ACD, 0x7ACE, 0x7ACF, 0x7AD0, 0x7AD1, 0x7AD2, 0x7AD3, 0x7AD4, 0x7AD5, 0x7AD7, 0x7AD8, 0x7ADA, 0x7ADB, 0x7ADC, 0x7ADD, 0x7AE1, 0x7AE2, 0x7AE4, 0x7AE7, 0x7AE8, 0x7AE9, 0x7AEA, 0x7AEB, 0x7AEC, 0x7AEE, 0x7AF0, 0x7AF1, 0x7AF2, 0x7AF3, 0x7AF4, 0x7AF5, 0x7AF6, 0x7AF7, 0x7AF8, 0x7AFB, 0x7AFC, 0x7AFE, 0x7B00, 0x7B01, 0x7B02, 0x7B05, 0x7B07, 0x7B09, 0x7B0C, 0x7B0D, 0x7B0E, 0x7B10, 0x7B12, 0x7B13, 0x7B16, 0x7B17, 0x7B18, 0x7B1A, 0x7B1C, 0x7B1D, 0x7B1F, 0x7B21, 0x7B22, 0x7B23, 0x7B27, 0x7B29, + 0x7B2D, 0x6D6E, 0x6DAA, 0x798F, 0x88B1, 0x5F17, 0x752B, 0x629A, 0x8F85, 0x4FEF, 0x91DC, 0x65A7, 0x812F, 0x8151, 0x5E9C, 0x8150, 0x8D74, 0x526F, 0x8986, 0x8D4B, 0x590D, 0x5085, 0x4ED8, 0x961C, 0x7236, 0x8179, 0x8D1F, 0x5BCC, 0x8BA3, 0x9644, 0x5987, 0x7F1A, 0x5490, 0x5676, 0x560E, 0x8BE5, 0x6539, 0x6982, 0x9499, 0x76D6, 0x6E89, 0x5E72, 0x7518, 0x6746, 0x67D1, 0x7AFF, 0x809D, 0x8D76, 0x611F, 0x79C6, 0x6562, 0x8D63, 0x5188, 0x521A, 0x94A2, 0x7F38, 0x809B, 0x7EB2, 0x5C97, 0x6E2F, 0x6760, 0x7BD9, 0x768B, 0x9AD8, 0x818F, 0x7F94, 0x7CD5, 0x641E, 0x9550, 0x7A3F, 0x544A, 0x54E5, 0x6B4C, 0x6401, 0x6208, 0x9E3D, 0x80F3, 0x7599, 0x5272, 0x9769, 0x845B, 0x683C, 0x86E4, 0x9601, 0x9694, 0x94EC, 0x4E2A, 0x5404, 0x7ED9, 0x6839, 0x8DDF, 0x8015, 0x66F4, 0x5E9A, 0x7FB9, + 0x7B2F, 0x7B30, 0x7B32, 0x7B34, 0x7B35, 0x7B36, 0x7B37, 0x7B39, 0x7B3B, 0x7B3D, 0x7B3F, 0x7B40, 0x7B41, 0x7B42, 0x7B43, 0x7B44, 0x7B46, 0x7B48, 0x7B4A, 0x7B4D, 0x7B4E, 0x7B53, 0x7B55, 0x7B57, 0x7B59, 0x7B5C, 0x7B5E, 0x7B5F, 0x7B61, 0x7B63, 0x7B64, 0x7B65, 0x7B66, 0x7B67, 0x7B68, 0x7B69, 0x7B6A, 0x7B6B, 0x7B6C, 0x7B6D, 0x7B6F, 0x7B70, 0x7B73, 0x7B74, 0x7B76, 0x7B78, 0x7B7A, 0x7B7C, 0x7B7D, 0x7B7F, 0x7B81, 0x7B82, 0x7B83, 0x7B84, 0x7B86, 0x7B87, 0x7B88, 0x7B89, 0x7B8A, 0x7B8B, 0x7B8C, 0x7B8E, 0x7B8F, 0x7B91, 0x7B92, 0x7B93, 0x7B96, 0x7B98, 0x7B99, 0x7B9A, 0x7B9B, 0x7B9E, 0x7B9F, 0x7BA0, 0x7BA3, 0x7BA4, 0x7BA5, 0x7BAE, 0x7BAF, 0x7BB0, 0x7BB2, 0x7BB3, 0x7BB5, 0x7BB6, 0x7BB7, 0x7BB9, 0x7BBA, 0x7BBB, 0x7BBC, 0x7BBD, 0x7BBE, 0x7BBF, 0x7BC0, 0x7BC2, 0x7BC3, + 0x7BC4, 0x57C2, 0x803F, 0x6897, 0x5DE5, 0x653B, 0x529F, 0x606D, 0x9F9A, 0x4F9B, 0x8EAC, 0x516C, 0x5BAB, 0x5F13, 0x5DE9, 0x6C5E, 0x62F1, 0x8D21, 0x5171, 0x94A9, 0x52FE, 0x6C9F, 0x82DF, 0x72D7, 0x57A2, 0x6784, 0x8D2D, 0x591F, 0x8F9C, 0x83C7, 0x5495, 0x7B8D, 0x4F30, 0x6CBD, 0x5B64, 0x59D1, 0x9F13, 0x53E4, 0x86CA, 0x9AA8, 0x8C37, 0x80A1, 0x6545, 0x987E, 0x56FA, 0x96C7, 0x522E, 0x74DC, 0x5250, 0x5BE1, 0x6302, 0x8902, 0x4E56, 0x62D0, 0x602A, 0x68FA, 0x5173, 0x5B98, 0x51A0, 0x89C2, 0x7BA1, 0x9986, 0x7F50, 0x60EF, 0x704C, 0x8D2F, 0x5149, 0x5E7F, 0x901B, 0x7470, 0x89C4, 0x572D, 0x7845, 0x5F52, 0x9F9F, 0x95FA, 0x8F68, 0x9B3C, 0x8BE1, 0x7678, 0x6842, 0x67DC, 0x8DEA, 0x8D35, 0x523D, 0x8F8A, 0x6EDA, 0x68CD, 0x9505, 0x90ED, 0x56FD, 0x679C, 0x88F9, 0x8FC7, 0x54C8, + 0x7BC5, 0x7BC8, 0x7BC9, 0x7BCA, 0x7BCB, 0x7BCD, 0x7BCE, 0x7BCF, 0x7BD0, 0x7BD2, 0x7BD4, 0x7BD5, 0x7BD6, 0x7BD7, 0x7BD8, 0x7BDB, 0x7BDC, 0x7BDE, 0x7BDF, 0x7BE0, 0x7BE2, 0x7BE3, 0x7BE4, 0x7BE7, 0x7BE8, 0x7BE9, 0x7BEB, 0x7BEC, 0x7BED, 0x7BEF, 0x7BF0, 0x7BF2, 0x7BF3, 0x7BF4, 0x7BF5, 0x7BF6, 0x7BF8, 0x7BF9, 0x7BFA, 0x7BFB, 0x7BFD, 0x7BFF, 0x7C00, 0x7C01, 0x7C02, 0x7C03, 0x7C04, 0x7C05, 0x7C06, 0x7C08, 0x7C09, 0x7C0A, 0x7C0D, 0x7C0E, 0x7C10, 0x7C11, 0x7C12, 0x7C13, 0x7C14, 0x7C15, 0x7C17, 0x7C18, 0x7C19, 0x7C1A, 0x7C1B, 0x7C1C, 0x7C1D, 0x7C1E, 0x7C20, 0x7C21, 0x7C22, 0x7C23, 0x7C24, 0x7C25, 0x7C28, 0x7C29, 0x7C2B, 0x7C2C, 0x7C2D, 0x7C2E, 0x7C2F, 0x7C30, 0x7C31, 0x7C32, 0x7C33, 0x7C34, 0x7C35, 0x7C36, 0x7C37, 0x7C39, 0x7C3A, 0x7C3B, 0x7C3C, 0x7C3D, 0x7C3E, + 0x7C42, 0x9AB8, 0x5B69, 0x6D77, 0x6C26, 0x4EA5, 0x5BB3, 0x9A87, 0x9163, 0x61A8, 0x90AF, 0x97E9, 0x542B, 0x6DB5, 0x5BD2, 0x51FD, 0x558A, 0x7F55, 0x7FF0, 0x64BC, 0x634D, 0x65F1, 0x61BE, 0x608D, 0x710A, 0x6C57, 0x6C49, 0x592F, 0x676D, 0x822A, 0x58D5, 0x568E, 0x8C6A, 0x6BEB, 0x90DD, 0x597D, 0x8017, 0x53F7, 0x6D69, 0x5475, 0x559D, 0x8377, 0x83CF, 0x6838, 0x79BE, 0x548C, 0x4F55, 0x5408, 0x76D2, 0x8C89, 0x9602, 0x6CB3, 0x6DB8, 0x8D6B, 0x8910, 0x9E64, 0x8D3A, 0x563F, 0x9ED1, 0x75D5, 0x5F88, 0x72E0, 0x6068, 0x54FC, 0x4EA8, 0x6A2A, 0x8861, 0x6052, 0x8F70, 0x54C4, 0x70D8, 0x8679, 0x9E3F, 0x6D2A, 0x5B8F, 0x5F18, 0x7EA2, 0x5589, 0x4FAF, 0x7334, 0x543C, 0x539A, 0x5019, 0x540E, 0x547C, 0x4E4E, 0x5FFD, 0x745A, 0x58F6, 0x846B, 0x80E1, 0x8774, 0x72D0, 0x7CCA, 0x6E56, + 0x7C43, 0x7C44, 0x7C45, 0x7C46, 0x7C47, 0x7C48, 0x7C49, 0x7C4A, 0x7C4B, 0x7C4C, 0x7C4E, 0x7C4F, 0x7C50, 0x7C51, 0x7C52, 0x7C53, 0x7C54, 0x7C55, 0x7C56, 0x7C57, 0x7C58, 0x7C59, 0x7C5A, 0x7C5B, 0x7C5C, 0x7C5D, 0x7C5E, 0x7C5F, 0x7C60, 0x7C61, 0x7C62, 0x7C63, 0x7C64, 0x7C65, 0x7C66, 0x7C67, 0x7C68, 0x7C69, 0x7C6A, 0x7C6B, 0x7C6C, 0x7C6D, 0x7C6E, 0x7C6F, 0x7C70, 0x7C71, 0x7C72, 0x7C75, 0x7C76, 0x7C77, 0x7C78, 0x7C79, 0x7C7A, 0x7C7E, 0x7C7F, 0x7C80, 0x7C81, 0x7C82, 0x7C83, 0x7C84, 0x7C85, 0x7C86, 0x7C87, 0x7C88, 0x7C8A, 0x7C8B, 0x7C8C, 0x7C8D, 0x7C8E, 0x7C8F, 0x7C90, 0x7C93, 0x7C94, 0x7C96, 0x7C99, 0x7C9A, 0x7C9B, 0x7CA0, 0x7CA1, 0x7CA3, 0x7CA6, 0x7CA7, 0x7CA8, 0x7CA9, 0x7CAB, 0x7CAC, 0x7CAD, 0x7CAF, 0x7CB0, 0x7CB4, 0x7CB5, 0x7CB6, 0x7CB7, 0x7CB8, 0x7CBA, + 0x7CBB, 0x5F27, 0x864E, 0x552C, 0x62A4, 0x4E92, 0x6CAA, 0x6237, 0x82B1, 0x54D7, 0x534E, 0x733E, 0x6ED1, 0x753B, 0x5212, 0x5316, 0x8BDD, 0x69D0, 0x5F8A, 0x6000, 0x6DEE, 0x574F, 0x6B22, 0x73AF, 0x6853, 0x8FD8, 0x7F13, 0x6362, 0x60A3, 0x5524, 0x75EA, 0x8C62, 0x7115, 0x6DA3, 0x5BA6, 0x5E7B, 0x8352, 0x614C, 0x9EC4, 0x78FA, 0x8757, 0x7C27, 0x7687, 0x51F0, 0x60F6, 0x714C, 0x6643, 0x5E4C, 0x604D, 0x8C0E, 0x7070, 0x6325, 0x8F89, 0x5FBD, 0x6062, 0x86D4, 0x56DE, 0x6BC1, 0x6094, 0x6167, 0x5349, 0x60E0, 0x6666, 0x8D3F, 0x79FD, 0x4F1A, 0x70E9, 0x6C47, 0x8BB3, 0x8BF2, 0x7ED8, 0x8364, 0x660F, 0x5A5A, 0x9B42, 0x6D51, 0x6DF7, 0x8C41, 0x6D3B, 0x4F19, 0x706B, 0x83B7, 0x6216, 0x60D1, 0x970D, 0x8D27, 0x7978, 0x51FB, 0x573E, 0x57FA, 0x673A, 0x7578, 0x7A3D, 0x79EF, 0x7B95, + 0x7CBF, 0x7CC0, 0x7CC2, 0x7CC3, 0x7CC4, 0x7CC6, 0x7CC9, 0x7CCB, 0x7CCE, 0x7CCF, 0x7CD0, 0x7CD1, 0x7CD2, 0x7CD3, 0x7CD4, 0x7CD8, 0x7CDA, 0x7CDB, 0x7CDD, 0x7CDE, 0x7CE1, 0x7CE2, 0x7CE3, 0x7CE4, 0x7CE5, 0x7CE6, 0x7CE7, 0x7CE9, 0x7CEA, 0x7CEB, 0x7CEC, 0x7CED, 0x7CEE, 0x7CF0, 0x7CF1, 0x7CF2, 0x7CF3, 0x7CF4, 0x7CF5, 0x7CF6, 0x7CF7, 0x7CF9, 0x7CFA, 0x7CFC, 0x7CFD, 0x7CFE, 0x7CFF, 0x7D00, 0x7D01, 0x7D02, 0x7D03, 0x7D04, 0x7D05, 0x7D06, 0x7D07, 0x7D08, 0x7D09, 0x7D0B, 0x7D0C, 0x7D0D, 0x7D0E, 0x7D0F, 0x7D10, 0x7D11, 0x7D12, 0x7D13, 0x7D14, 0x7D15, 0x7D16, 0x7D17, 0x7D18, 0x7D19, 0x7D1A, 0x7D1B, 0x7D1C, 0x7D1D, 0x7D1E, 0x7D1F, 0x7D21, 0x7D23, 0x7D24, 0x7D25, 0x7D26, 0x7D28, 0x7D29, 0x7D2A, 0x7D2C, 0x7D2D, 0x7D2E, 0x7D30, 0x7D31, 0x7D32, 0x7D33, 0x7D34, 0x7D35, + 0x7D36, 0x808C, 0x9965, 0x8FF9, 0x6FC0, 0x8BA5, 0x9E21, 0x59EC, 0x7EE9, 0x7F09, 0x5409, 0x6781, 0x68D8, 0x8F91, 0x7C4D, 0x96C6, 0x53CA, 0x6025, 0x75BE, 0x6C72, 0x5373, 0x5AC9, 0x7EA7, 0x6324, 0x51E0, 0x810A, 0x5DF1, 0x84DF, 0x6280, 0x5180, 0x5B63, 0x4F0E, 0x796D, 0x5242, 0x60B8, 0x6D4E, 0x5BC4, 0x5BC2, 0x8BA1, 0x8BB0, 0x65E2, 0x5FCC, 0x9645, 0x5993, 0x7EE7, 0x7EAA, 0x5609, 0x67B7, 0x5939, 0x4F73, 0x5BB6, 0x52A0, 0x835A, 0x988A, 0x8D3E, 0x7532, 0x94BE, 0x5047, 0x7A3C, 0x4EF7, 0x67B6, 0x9A7E, 0x5AC1, 0x6B7C, 0x76D1, 0x575A, 0x5C16, 0x7B3A, 0x95F4, 0x714E, 0x517C, 0x80A9, 0x8270, 0x5978, 0x7F04, 0x8327, 0x68C0, 0x67EC, 0x78B1, 0x7877, 0x62E3, 0x6361, 0x7B80, 0x4FED, 0x526A, 0x51CF, 0x8350, 0x69DB, 0x9274, 0x8DF5, 0x8D31, 0x89C1, 0x952E, 0x7BAD, 0x4EF6, + 0x7D37, 0x7D38, 0x7D39, 0x7D3A, 0x7D3B, 0x7D3C, 0x7D3D, 0x7D3E, 0x7D3F, 0x7D40, 0x7D41, 0x7D42, 0x7D43, 0x7D44, 0x7D45, 0x7D46, 0x7D47, 0x7D48, 0x7D49, 0x7D4A, 0x7D4B, 0x7D4C, 0x7D4D, 0x7D4E, 0x7D4F, 0x7D50, 0x7D51, 0x7D52, 0x7D53, 0x7D54, 0x7D55, 0x7D56, 0x7D57, 0x7D58, 0x7D59, 0x7D5A, 0x7D5B, 0x7D5C, 0x7D5D, 0x7D5E, 0x7D5F, 0x7D60, 0x7D61, 0x7D62, 0x7D63, 0x7D64, 0x7D65, 0x7D66, 0x7D67, 0x7D68, 0x7D69, 0x7D6A, 0x7D6B, 0x7D6C, 0x7D6D, 0x7D6F, 0x7D70, 0x7D71, 0x7D72, 0x7D73, 0x7D74, 0x7D75, 0x7D76, 0x7D78, 0x7D79, 0x7D7A, 0x7D7B, 0x7D7C, 0x7D7D, 0x7D7E, 0x7D7F, 0x7D80, 0x7D81, 0x7D82, 0x7D83, 0x7D84, 0x7D85, 0x7D86, 0x7D87, 0x7D88, 0x7D89, 0x7D8A, 0x7D8B, 0x7D8C, 0x7D8D, 0x7D8E, 0x7D8F, 0x7D90, 0x7D91, 0x7D92, 0x7D93, 0x7D94, 0x7D95, 0x7D96, 0x7D97, + 0x7D98, 0x5065, 0x8230, 0x5251, 0x996F, 0x6E10, 0x6E85, 0x6DA7, 0x5EFA, 0x50F5, 0x59DC, 0x5C06, 0x6D46, 0x6C5F, 0x7586, 0x848B, 0x6868, 0x5956, 0x8BB2, 0x5320, 0x9171, 0x964D, 0x8549, 0x6912, 0x7901, 0x7126, 0x80F6, 0x4EA4, 0x90CA, 0x6D47, 0x9A84, 0x5A07, 0x56BC, 0x6405, 0x94F0, 0x77EB, 0x4FA5, 0x811A, 0x72E1, 0x89D2, 0x997A, 0x7F34, 0x7EDE, 0x527F, 0x6559, 0x9175, 0x8F7F, 0x8F83, 0x53EB, 0x7A96, 0x63ED, 0x63A5, 0x7686, 0x79F8, 0x8857, 0x9636, 0x622A, 0x52AB, 0x8282, 0x6854, 0x6770, 0x6377, 0x776B, 0x7AED, 0x6D01, 0x7ED3, 0x89E3, 0x59D0, 0x6212, 0x85C9, 0x82A5, 0x754C, 0x501F, 0x4ECB, 0x75A5, 0x8BEB, 0x5C4A, 0x5DFE, 0x7B4B, 0x65A4, 0x91D1, 0x4ECA, 0x6D25, 0x895F, 0x7D27, 0x9526, 0x4EC5, 0x8C28, 0x8FDB, 0x9773, 0x664B, 0x7981, 0x8FD1, 0x70EC, 0x6D78, + 0x7D99, 0x7D9A, 0x7D9B, 0x7D9C, 0x7D9D, 0x7D9E, 0x7D9F, 0x7DA0, 0x7DA1, 0x7DA2, 0x7DA3, 0x7DA4, 0x7DA5, 0x7DA7, 0x7DA8, 0x7DA9, 0x7DAA, 0x7DAB, 0x7DAC, 0x7DAD, 0x7DAF, 0x7DB0, 0x7DB1, 0x7DB2, 0x7DB3, 0x7DB4, 0x7DB5, 0x7DB6, 0x7DB7, 0x7DB8, 0x7DB9, 0x7DBA, 0x7DBB, 0x7DBC, 0x7DBD, 0x7DBE, 0x7DBF, 0x7DC0, 0x7DC1, 0x7DC2, 0x7DC3, 0x7DC4, 0x7DC5, 0x7DC6, 0x7DC7, 0x7DC8, 0x7DC9, 0x7DCA, 0x7DCB, 0x7DCC, 0x7DCD, 0x7DCE, 0x7DCF, 0x7DD0, 0x7DD1, 0x7DD2, 0x7DD3, 0x7DD4, 0x7DD5, 0x7DD6, 0x7DD7, 0x7DD8, 0x7DD9, 0x7DDA, 0x7DDB, 0x7DDC, 0x7DDD, 0x7DDE, 0x7DDF, 0x7DE0, 0x7DE1, 0x7DE2, 0x7DE3, 0x7DE4, 0x7DE5, 0x7DE6, 0x7DE7, 0x7DE8, 0x7DE9, 0x7DEA, 0x7DEB, 0x7DEC, 0x7DED, 0x7DEE, 0x7DEF, 0x7DF0, 0x7DF1, 0x7DF2, 0x7DF3, 0x7DF4, 0x7DF5, 0x7DF6, 0x7DF7, 0x7DF8, 0x7DF9, + 0x7DFA, 0x5C3D, 0x52B2, 0x8346, 0x5162, 0x830E, 0x775B, 0x6676, 0x9CB8, 0x4EAC, 0x60CA, 0x7CBE, 0x7CB3, 0x7ECF, 0x4E95, 0x8B66, 0x666F, 0x9888, 0x9759, 0x5883, 0x656C, 0x955C, 0x5F84, 0x75C9, 0x9756, 0x7ADF, 0x7ADE, 0x51C0, 0x70AF, 0x7A98, 0x63EA, 0x7A76, 0x7EA0, 0x7396, 0x97ED, 0x4E45, 0x7078, 0x4E5D, 0x9152, 0x53A9, 0x6551, 0x65E7, 0x81FC, 0x8205, 0x548E, 0x5C31, 0x759A, 0x97A0, 0x62D8, 0x72D9, 0x75BD, 0x5C45, 0x9A79, 0x83CA, 0x5C40, 0x5480, 0x77E9, 0x4E3E, 0x6CAE, 0x805A, 0x62D2, 0x636E, 0x5DE8, 0x5177, 0x8DDD, 0x8E1E, 0x952F, 0x4FF1, 0x53E5, 0x60E7, 0x70AC, 0x5267, 0x6350, 0x9E43, 0x5A1F, 0x5026, 0x7737, 0x5377, 0x7EE2, 0x6485, 0x652B, 0x6289, 0x6398, 0x5014, 0x7235, 0x89C9, 0x51B3, 0x8BC0, 0x7EDD, 0x5747, 0x83CC, 0x94A7, 0x519B, 0x541B, 0x5CFB, + 0x7DFB, 0x7DFC, 0x7DFD, 0x7DFE, 0x7DFF, 0x7E00, 0x7E01, 0x7E02, 0x7E03, 0x7E04, 0x7E05, 0x7E06, 0x7E07, 0x7E08, 0x7E09, 0x7E0A, 0x7E0B, 0x7E0C, 0x7E0D, 0x7E0E, 0x7E0F, 0x7E10, 0x7E11, 0x7E12, 0x7E13, 0x7E14, 0x7E15, 0x7E16, 0x7E17, 0x7E18, 0x7E19, 0x7E1A, 0x7E1B, 0x7E1C, 0x7E1D, 0x7E1E, 0x7E1F, 0x7E20, 0x7E21, 0x7E22, 0x7E23, 0x7E24, 0x7E25, 0x7E26, 0x7E27, 0x7E28, 0x7E29, 0x7E2A, 0x7E2B, 0x7E2C, 0x7E2D, 0x7E2E, 0x7E2F, 0x7E30, 0x7E31, 0x7E32, 0x7E33, 0x7E34, 0x7E35, 0x7E36, 0x7E37, 0x7E38, 0x7E39, 0x7E3A, 0x7E3C, 0x7E3D, 0x7E3E, 0x7E3F, 0x7E40, 0x7E42, 0x7E43, 0x7E44, 0x7E45, 0x7E46, 0x7E48, 0x7E49, 0x7E4A, 0x7E4B, 0x7E4C, 0x7E4D, 0x7E4E, 0x7E4F, 0x7E50, 0x7E51, 0x7E52, 0x7E53, 0x7E54, 0x7E55, 0x7E56, 0x7E57, 0x7E58, 0x7E59, 0x7E5A, 0x7E5B, 0x7E5C, + 0x7E5D, 0x4FCA, 0x7AE3, 0x6D5A, 0x90E1, 0x9A8F, 0x5580, 0x5496, 0x5361, 0x54AF, 0x5F00, 0x63E9, 0x6977, 0x51EF, 0x6168, 0x520A, 0x582A, 0x52D8, 0x574E, 0x780D, 0x770B, 0x5EB7, 0x6177, 0x7CE0, 0x625B, 0x6297, 0x4EA2, 0x7095, 0x8003, 0x62F7, 0x70E4, 0x9760, 0x5777, 0x82DB, 0x67EF, 0x68F5, 0x78D5, 0x9897, 0x79D1, 0x58F3, 0x54B3, 0x53EF, 0x6E34, 0x514B, 0x523B, 0x5BA2, 0x8BFE, 0x80AF, 0x5543, 0x57A6, 0x6073, 0x5751, 0x542D, 0x7A7A, 0x6050, 0x5B54, 0x63A7, 0x62A0, 0x53E3, 0x6263, 0x5BC7, 0x67AF, 0x54ED, 0x7A9F, 0x82E6, 0x9177, 0x5E93, 0x88E4, 0x5938, 0x57AE, 0x630E, 0x8DE8, 0x80EF, 0x5757, 0x7B77, 0x4FA9, 0x5FEB, 0x5BBD, 0x6B3E, 0x5321, 0x7B50, 0x72C2, 0x6846, 0x77FF, 0x7736, 0x65F7, 0x51B5, 0x4E8F, 0x76D4, 0x5CBF, 0x7AA5, 0x8475, 0x594E, 0x9B41, 0x5080, + 0x7E5E, 0x7E5F, 0x7E60, 0x7E61, 0x7E62, 0x7E63, 0x7E64, 0x7E65, 0x7E66, 0x7E67, 0x7E68, 0x7E69, 0x7E6A, 0x7E6B, 0x7E6C, 0x7E6D, 0x7E6E, 0x7E6F, 0x7E70, 0x7E71, 0x7E72, 0x7E73, 0x7E74, 0x7E75, 0x7E76, 0x7E77, 0x7E78, 0x7E79, 0x7E7A, 0x7E7B, 0x7E7C, 0x7E7D, 0x7E7E, 0x7E7F, 0x7E80, 0x7E81, 0x7E83, 0x7E84, 0x7E85, 0x7E86, 0x7E87, 0x7E88, 0x7E89, 0x7E8A, 0x7E8B, 0x7E8C, 0x7E8D, 0x7E8E, 0x7E8F, 0x7E90, 0x7E91, 0x7E92, 0x7E93, 0x7E94, 0x7E95, 0x7E96, 0x7E97, 0x7E98, 0x7E99, 0x7E9A, 0x7E9C, 0x7E9D, 0x7E9E, 0x7EAE, 0x7EB4, 0x7EBB, 0x7EBC, 0x7ED6, 0x7EE4, 0x7EEC, 0x7EF9, 0x7F0A, 0x7F10, 0x7F1E, 0x7F37, 0x7F39, 0x7F3B, 0x7F3C, 0x7F3D, 0x7F3E, 0x7F3F, 0x7F40, 0x7F41, 0x7F43, 0x7F46, 0x7F47, 0x7F48, 0x7F49, 0x7F4A, 0x7F4B, 0x7F4C, 0x7F4D, 0x7F4E, 0x7F4F, 0x7F52, + 0x7F53, 0x9988, 0x6127, 0x6E83, 0x5764, 0x6606, 0x6346, 0x56F0, 0x62EC, 0x6269, 0x5ED3, 0x9614, 0x5783, 0x62C9, 0x5587, 0x8721, 0x814A, 0x8FA3, 0x5566, 0x83B1, 0x6765, 0x8D56, 0x84DD, 0x5A6A, 0x680F, 0x62E6, 0x7BEE, 0x9611, 0x5170, 0x6F9C, 0x8C30, 0x63FD, 0x89C8, 0x61D2, 0x7F06, 0x70C2, 0x6EE5, 0x7405, 0x6994, 0x72FC, 0x5ECA, 0x90CE, 0x6717, 0x6D6A, 0x635E, 0x52B3, 0x7262, 0x8001, 0x4F6C, 0x59E5, 0x916A, 0x70D9, 0x6D9D, 0x52D2, 0x4E50, 0x96F7, 0x956D, 0x857E, 0x78CA, 0x7D2F, 0x5121, 0x5792, 0x64C2, 0x808B, 0x7C7B, 0x6CEA, 0x68F1, 0x695E, 0x51B7, 0x5398, 0x68A8, 0x7281, 0x9ECE, 0x7BF1, 0x72F8, 0x79BB, 0x6F13, 0x7406, 0x674E, 0x91CC, 0x9CA4, 0x793C, 0x8389, 0x8354, 0x540F, 0x6817, 0x4E3D, 0x5389, 0x52B1, 0x783E, 0x5386, 0x5229, 0x5088, 0x4F8B, 0x4FD0, + 0x7F56, 0x7F59, 0x7F5B, 0x7F5C, 0x7F5D, 0x7F5E, 0x7F60, 0x7F63, 0x7F64, 0x7F65, 0x7F66, 0x7F67, 0x7F6B, 0x7F6C, 0x7F6D, 0x7F6F, 0x7F70, 0x7F73, 0x7F75, 0x7F76, 0x7F77, 0x7F78, 0x7F7A, 0x7F7B, 0x7F7C, 0x7F7D, 0x7F7F, 0x7F80, 0x7F82, 0x7F83, 0x7F84, 0x7F85, 0x7F86, 0x7F87, 0x7F88, 0x7F89, 0x7F8B, 0x7F8D, 0x7F8F, 0x7F90, 0x7F91, 0x7F92, 0x7F93, 0x7F95, 0x7F96, 0x7F97, 0x7F98, 0x7F99, 0x7F9B, 0x7F9C, 0x7FA0, 0x7FA2, 0x7FA3, 0x7FA5, 0x7FA6, 0x7FA8, 0x7FA9, 0x7FAA, 0x7FAB, 0x7FAC, 0x7FAD, 0x7FAE, 0x7FB1, 0x7FB3, 0x7FB4, 0x7FB5, 0x7FB6, 0x7FB7, 0x7FBA, 0x7FBB, 0x7FBE, 0x7FC0, 0x7FC2, 0x7FC3, 0x7FC4, 0x7FC6, 0x7FC7, 0x7FC8, 0x7FC9, 0x7FCB, 0x7FCD, 0x7FCF, 0x7FD0, 0x7FD1, 0x7FD2, 0x7FD3, 0x7FD6, 0x7FD7, 0x7FD9, 0x7FDA, 0x7FDB, 0x7FDC, 0x7FDD, 0x7FDE, 0x7FE2, + 0x7FE3, 0x75E2, 0x7ACB, 0x7C92, 0x6CA5, 0x96B6, 0x529B, 0x7483, 0x54E9, 0x4FE9, 0x8054, 0x83B2, 0x8FDE, 0x9570, 0x5EC9, 0x601C, 0x6D9F, 0x5E18, 0x655B, 0x8138, 0x94FE, 0x604B, 0x70BC, 0x7EC3, 0x7CAE, 0x51C9, 0x6881, 0x7CB1, 0x826F, 0x4E24, 0x8F86, 0x91CF, 0x667E, 0x4EAE, 0x8C05, 0x64A9, 0x804A, 0x50DA, 0x7597, 0x71CE, 0x5BE5, 0x8FBD, 0x6F66, 0x4E86, 0x6482, 0x9563, 0x5ED6, 0x6599, 0x5217, 0x88C2, 0x70C8, 0x52A3, 0x730E, 0x7433, 0x6797, 0x78F7, 0x9716, 0x4E34, 0x90BB, 0x9CDE, 0x6DCB, 0x51DB, 0x8D41, 0x541D, 0x62CE, 0x73B2, 0x83F1, 0x96F6, 0x9F84, 0x94C3, 0x4F36, 0x7F9A, 0x51CC, 0x7075, 0x9675, 0x5CAD, 0x9886, 0x53E6, 0x4EE4, 0x6E9C, 0x7409, 0x69B4, 0x786B, 0x998F, 0x7559, 0x5218, 0x7624, 0x6D41, 0x67F3, 0x516D, 0x9F99, 0x804B, 0x5499, 0x7B3C, 0x7ABF, + 0x7FE4, 0x7FE7, 0x7FE8, 0x7FEA, 0x7FEB, 0x7FEC, 0x7FED, 0x7FEF, 0x7FF2, 0x7FF4, 0x7FF5, 0x7FF6, 0x7FF7, 0x7FF8, 0x7FF9, 0x7FFA, 0x7FFD, 0x7FFE, 0x7FFF, 0x8002, 0x8007, 0x8008, 0x8009, 0x800A, 0x800E, 0x800F, 0x8011, 0x8013, 0x801A, 0x801B, 0x801D, 0x801E, 0x801F, 0x8021, 0x8023, 0x8024, 0x802B, 0x802C, 0x802D, 0x802E, 0x802F, 0x8030, 0x8032, 0x8034, 0x8039, 0x803A, 0x803C, 0x803E, 0x8040, 0x8041, 0x8044, 0x8045, 0x8047, 0x8048, 0x8049, 0x804E, 0x804F, 0x8050, 0x8051, 0x8053, 0x8055, 0x8056, 0x8057, 0x8059, 0x805B, 0x805C, 0x805D, 0x805E, 0x805F, 0x8060, 0x8061, 0x8062, 0x8063, 0x8064, 0x8065, 0x8066, 0x8067, 0x8068, 0x806B, 0x806C, 0x806D, 0x806E, 0x806F, 0x8070, 0x8072, 0x8073, 0x8074, 0x8075, 0x8076, 0x8077, 0x8078, 0x8079, 0x807A, 0x807B, 0x807C, + 0x807D, 0x9686, 0x5784, 0x62E2, 0x9647, 0x697C, 0x5A04, 0x6402, 0x7BD3, 0x6F0F, 0x964B, 0x82A6, 0x5362, 0x9885, 0x5E90, 0x7089, 0x63B3, 0x5364, 0x864F, 0x9C81, 0x9E93, 0x788C, 0x9732, 0x8DEF, 0x8D42, 0x9E7F, 0x6F5E, 0x7984, 0x5F55, 0x9646, 0x622E, 0x9A74, 0x5415, 0x94DD, 0x4FA3, 0x65C5, 0x5C65, 0x5C61, 0x7F15, 0x8651, 0x6C2F, 0x5F8B, 0x7387, 0x6EE4, 0x7EFF, 0x5CE6, 0x631B, 0x5B6A, 0x6EE6, 0x5375, 0x4E71, 0x63A0, 0x7565, 0x62A1, 0x8F6E, 0x4F26, 0x4ED1, 0x6CA6, 0x7EB6, 0x8BBA, 0x841D, 0x87BA, 0x7F57, 0x903B, 0x9523, 0x7BA9, 0x9AA1, 0x88F8, 0x843D, 0x6D1B, 0x9A86, 0x7EDC, 0x5988, 0x9EBB, 0x739B, 0x7801, 0x8682, 0x9A6C, 0x9A82, 0x561B, 0x5417, 0x57CB, 0x4E70, 0x9EA6, 0x5356, 0x8FC8, 0x8109, 0x7792, 0x9992, 0x86EE, 0x6EE1, 0x8513, 0x66FC, 0x6162, 0x6F2B, + 0x807E, 0x8081, 0x8082, 0x8085, 0x8088, 0x808A, 0x808D, 0x808E, 0x808F, 0x8090, 0x8091, 0x8092, 0x8094, 0x8095, 0x8097, 0x8099, 0x809E, 0x80A3, 0x80A6, 0x80A7, 0x80A8, 0x80AC, 0x80B0, 0x80B3, 0x80B5, 0x80B6, 0x80B8, 0x80B9, 0x80BB, 0x80C5, 0x80C7, 0x80C8, 0x80C9, 0x80CA, 0x80CB, 0x80CF, 0x80D0, 0x80D1, 0x80D2, 0x80D3, 0x80D4, 0x80D5, 0x80D8, 0x80DF, 0x80E0, 0x80E2, 0x80E3, 0x80E6, 0x80EE, 0x80F5, 0x80F7, 0x80F9, 0x80FB, 0x80FE, 0x80FF, 0x8100, 0x8101, 0x8103, 0x8104, 0x8105, 0x8107, 0x8108, 0x810B, 0x810C, 0x8115, 0x8117, 0x8119, 0x811B, 0x811C, 0x811D, 0x811F, 0x8120, 0x8121, 0x8122, 0x8123, 0x8124, 0x8125, 0x8126, 0x8127, 0x8128, 0x8129, 0x812A, 0x812B, 0x812D, 0x812E, 0x8130, 0x8133, 0x8134, 0x8135, 0x8137, 0x8139, 0x813A, 0x813B, 0x813C, 0x813D, + 0x813F, 0x8C29, 0x8292, 0x832B, 0x76F2, 0x6C13, 0x5FD9, 0x83BD, 0x732B, 0x8305, 0x951A, 0x6BDB, 0x77DB, 0x94C6, 0x536F, 0x8302, 0x5192, 0x5E3D, 0x8C8C, 0x8D38, 0x4E48, 0x73AB, 0x679A, 0x6885, 0x9176, 0x9709, 0x7164, 0x6CA1, 0x7709, 0x5A92, 0x9541, 0x6BCF, 0x7F8E, 0x6627, 0x5BD0, 0x59B9, 0x5A9A, 0x95E8, 0x95F7, 0x4EEC, 0x840C, 0x8499, 0x6AAC, 0x76DF, 0x9530, 0x731B, 0x68A6, 0x5B5F, 0x772F, 0x919A, 0x9761, 0x7CDC, 0x8FF7, 0x8C1C, 0x5F25, 0x7C73, 0x79D8, 0x89C5, 0x6CCC, 0x871C, 0x5BC6, 0x5E42, 0x68C9, 0x7720, 0x7EF5, 0x5195, 0x514D, 0x52C9, 0x5A29, 0x7F05, 0x9762, 0x82D7, 0x63CF, 0x7784, 0x85D0, 0x79D2, 0x6E3A, 0x5E99, 0x5999, 0x8511, 0x706D, 0x6C11, 0x62BF, 0x76BF, 0x654F, 0x60AF, 0x95FD, 0x660E, 0x879F, 0x9E23, 0x94ED, 0x540D, 0x547D, 0x8C2C, 0x6478, + 0x8140, 0x8141, 0x8142, 0x8143, 0x8144, 0x8145, 0x8147, 0x8149, 0x814D, 0x814E, 0x814F, 0x8152, 0x8156, 0x8157, 0x8158, 0x815B, 0x815C, 0x815D, 0x815E, 0x815F, 0x8161, 0x8162, 0x8163, 0x8164, 0x8166, 0x8168, 0x816A, 0x816B, 0x816C, 0x816F, 0x8172, 0x8173, 0x8175, 0x8176, 0x8177, 0x8178, 0x8181, 0x8183, 0x8184, 0x8185, 0x8186, 0x8187, 0x8189, 0x818B, 0x818C, 0x818D, 0x818E, 0x8190, 0x8192, 0x8193, 0x8194, 0x8195, 0x8196, 0x8197, 0x8199, 0x819A, 0x819E, 0x819F, 0x81A0, 0x81A1, 0x81A2, 0x81A4, 0x81A5, 0x81A7, 0x81A9, 0x81AB, 0x81AC, 0x81AD, 0x81AE, 0x81AF, 0x81B0, 0x81B1, 0x81B2, 0x81B4, 0x81B5, 0x81B6, 0x81B7, 0x81B8, 0x81B9, 0x81BC, 0x81BD, 0x81BE, 0x81BF, 0x81C4, 0x81C5, 0x81C7, 0x81C8, 0x81C9, 0x81CB, 0x81CD, 0x81CE, 0x81CF, 0x81D0, 0x81D1, 0x81D2, + 0x81D3, 0x6479, 0x8611, 0x6A21, 0x819C, 0x78E8, 0x6469, 0x9B54, 0x62B9, 0x672B, 0x83AB, 0x58A8, 0x9ED8, 0x6CAB, 0x6F20, 0x5BDE, 0x964C, 0x8C0B, 0x725F, 0x67D0, 0x62C7, 0x7261, 0x4EA9, 0x59C6, 0x6BCD, 0x5893, 0x66AE, 0x5E55, 0x52DF, 0x6155, 0x6728, 0x76EE, 0x7766, 0x7267, 0x7A46, 0x62FF, 0x54EA, 0x5450, 0x94A0, 0x90A3, 0x5A1C, 0x7EB3, 0x6C16, 0x4E43, 0x5976, 0x8010, 0x5948, 0x5357, 0x7537, 0x96BE, 0x56CA, 0x6320, 0x8111, 0x607C, 0x95F9, 0x6DD6, 0x5462, 0x9981, 0x5185, 0x5AE9, 0x80FD, 0x59AE, 0x9713, 0x502A, 0x6CE5, 0x5C3C, 0x62DF, 0x4F60, 0x533F, 0x817B, 0x9006, 0x6EBA, 0x852B, 0x62C8, 0x5E74, 0x78BE, 0x64B5, 0x637B, 0x5FF5, 0x5A18, 0x917F, 0x9E1F, 0x5C3F, 0x634F, 0x8042, 0x5B7D, 0x556E, 0x954A, 0x954D, 0x6D85, 0x60A8, 0x67E0, 0x72DE, 0x51DD, 0x5B81, + 0x81D4, 0x81D5, 0x81D6, 0x81D7, 0x81D8, 0x81D9, 0x81DA, 0x81DB, 0x81DC, 0x81DD, 0x81DE, 0x81DF, 0x81E0, 0x81E1, 0x81E2, 0x81E4, 0x81E5, 0x81E6, 0x81E8, 0x81E9, 0x81EB, 0x81EE, 0x81EF, 0x81F0, 0x81F1, 0x81F2, 0x81F5, 0x81F6, 0x81F7, 0x81F8, 0x81F9, 0x81FA, 0x81FD, 0x81FF, 0x8203, 0x8207, 0x8208, 0x8209, 0x820A, 0x820B, 0x820E, 0x820F, 0x8211, 0x8213, 0x8215, 0x8216, 0x8217, 0x8218, 0x8219, 0x821A, 0x821D, 0x8220, 0x8224, 0x8225, 0x8226, 0x8227, 0x8229, 0x822E, 0x8232, 0x823A, 0x823C, 0x823D, 0x823F, 0x8240, 0x8241, 0x8242, 0x8243, 0x8245, 0x8246, 0x8248, 0x824A, 0x824C, 0x824D, 0x824E, 0x8250, 0x8251, 0x8252, 0x8253, 0x8254, 0x8255, 0x8256, 0x8257, 0x8259, 0x825B, 0x825C, 0x825D, 0x825E, 0x8260, 0x8261, 0x8262, 0x8263, 0x8264, 0x8265, 0x8266, 0x8267, + 0x8269, 0x62E7, 0x6CDE, 0x725B, 0x626D, 0x94AE, 0x7EBD, 0x8113, 0x6D53, 0x519C, 0x5F04, 0x5974, 0x52AA, 0x6012, 0x5973, 0x6696, 0x8650, 0x759F, 0x632A, 0x61E6, 0x7CEF, 0x8BFA, 0x54E6, 0x6B27, 0x9E25, 0x6BB4, 0x85D5, 0x5455, 0x5076, 0x6CA4, 0x556A, 0x8DB4, 0x722C, 0x5E15, 0x6015, 0x7436, 0x62CD, 0x6392, 0x724C, 0x5F98, 0x6E43, 0x6D3E, 0x6500, 0x6F58, 0x76D8, 0x78D0, 0x76FC, 0x7554, 0x5224, 0x53DB, 0x4E53, 0x5E9E, 0x65C1, 0x802A, 0x80D6, 0x629B, 0x5486, 0x5228, 0x70AE, 0x888D, 0x8DD1, 0x6CE1, 0x5478, 0x80DA, 0x57F9, 0x88F4, 0x8D54, 0x966A, 0x914D, 0x4F69, 0x6C9B, 0x55B7, 0x76C6, 0x7830, 0x62A8, 0x70F9, 0x6F8E, 0x5F6D, 0x84EC, 0x68DA, 0x787C, 0x7BF7, 0x81A8, 0x670B, 0x9E4F, 0x6367, 0x78B0, 0x576F, 0x7812, 0x9739, 0x6279, 0x62AB, 0x5288, 0x7435, 0x6BD7, + 0x826A, 0x826B, 0x826C, 0x826D, 0x8271, 0x8275, 0x8276, 0x8277, 0x8278, 0x827B, 0x827C, 0x8280, 0x8281, 0x8283, 0x8285, 0x8286, 0x8287, 0x8289, 0x828C, 0x8290, 0x8293, 0x8294, 0x8295, 0x8296, 0x829A, 0x829B, 0x829E, 0x82A0, 0x82A2, 0x82A3, 0x82A7, 0x82B2, 0x82B5, 0x82B6, 0x82BA, 0x82BB, 0x82BC, 0x82BF, 0x82C0, 0x82C2, 0x82C3, 0x82C5, 0x82C6, 0x82C9, 0x82D0, 0x82D6, 0x82D9, 0x82DA, 0x82DD, 0x82E2, 0x82E7, 0x82E8, 0x82E9, 0x82EA, 0x82EC, 0x82ED, 0x82EE, 0x82F0, 0x82F2, 0x82F3, 0x82F5, 0x82F6, 0x82F8, 0x82FA, 0x82FC, 0x82FD, 0x82FE, 0x82FF, 0x8300, 0x830A, 0x830B, 0x830D, 0x8310, 0x8312, 0x8313, 0x8316, 0x8318, 0x8319, 0x831D, 0x831E, 0x831F, 0x8320, 0x8321, 0x8322, 0x8323, 0x8324, 0x8325, 0x8326, 0x8329, 0x832A, 0x832E, 0x8330, 0x8332, 0x8337, 0x833B, + 0x833D, 0x5564, 0x813E, 0x75B2, 0x76AE, 0x5339, 0x75DE, 0x50FB, 0x5C41, 0x8B6C, 0x7BC7, 0x504F, 0x7247, 0x9A97, 0x98D8, 0x6F02, 0x74E2, 0x7968, 0x6487, 0x77A5, 0x62FC, 0x9891, 0x8D2B, 0x54C1, 0x8058, 0x4E52, 0x576A, 0x82F9, 0x840D, 0x5E73, 0x51ED, 0x74F6, 0x8BC4, 0x5C4F, 0x5761, 0x6CFC, 0x9887, 0x5A46, 0x7834, 0x9B44, 0x8FEB, 0x7C95, 0x5256, 0x6251, 0x94FA, 0x4EC6, 0x8386, 0x8461, 0x83E9, 0x84B2, 0x57D4, 0x6734, 0x5703, 0x666E, 0x6D66, 0x8C31, 0x66DD, 0x7011, 0x671F, 0x6B3A, 0x6816, 0x621A, 0x59BB, 0x4E03, 0x51C4, 0x6F06, 0x67D2, 0x6C8F, 0x5176, 0x68CB, 0x5947, 0x6B67, 0x7566, 0x5D0E, 0x8110, 0x9F50, 0x65D7, 0x7948, 0x7941, 0x9A91, 0x8D77, 0x5C82, 0x4E5E, 0x4F01, 0x542F, 0x5951, 0x780C, 0x5668, 0x6C14, 0x8FC4, 0x5F03, 0x6C7D, 0x6CE3, 0x8BAB, 0x6390, + 0x833E, 0x833F, 0x8341, 0x8342, 0x8344, 0x8345, 0x8348, 0x834A, 0x834B, 0x834C, 0x834D, 0x834E, 0x8353, 0x8355, 0x8356, 0x8357, 0x8358, 0x8359, 0x835D, 0x8362, 0x8370, 0x8371, 0x8372, 0x8373, 0x8374, 0x8375, 0x8376, 0x8379, 0x837A, 0x837E, 0x837F, 0x8380, 0x8381, 0x8382, 0x8383, 0x8384, 0x8387, 0x8388, 0x838A, 0x838B, 0x838C, 0x838D, 0x838F, 0x8390, 0x8391, 0x8394, 0x8395, 0x8396, 0x8397, 0x8399, 0x839A, 0x839D, 0x839F, 0x83A1, 0x83A2, 0x83A3, 0x83A4, 0x83A5, 0x83A6, 0x83A7, 0x83AC, 0x83AD, 0x83AE, 0x83AF, 0x83B5, 0x83BB, 0x83BE, 0x83BF, 0x83C2, 0x83C3, 0x83C4, 0x83C6, 0x83C8, 0x83C9, 0x83CB, 0x83CD, 0x83CE, 0x83D0, 0x83D1, 0x83D2, 0x83D3, 0x83D5, 0x83D7, 0x83D9, 0x83DA, 0x83DB, 0x83DE, 0x83E2, 0x83E3, 0x83E4, 0x83E6, 0x83E7, 0x83E8, 0x83EB, 0x83EC, + 0x83ED, 0x6070, 0x6D3D, 0x7275, 0x6266, 0x948E, 0x94C5, 0x5343, 0x8FC1, 0x7B7E, 0x4EDF, 0x8C26, 0x4E7E, 0x9ED4, 0x94B1, 0x94B3, 0x524D, 0x6F5C, 0x9063, 0x6D45, 0x8C34, 0x5811, 0x5D4C, 0x6B20, 0x6B49, 0x67AA, 0x545B, 0x8154, 0x7F8C, 0x5899, 0x8537, 0x5F3A, 0x62A2, 0x6A47, 0x9539, 0x6572, 0x6084, 0x6865, 0x77A7, 0x4E54, 0x4FA8, 0x5DE7, 0x9798, 0x64AC, 0x7FD8, 0x5CED, 0x4FCF, 0x7A8D, 0x5207, 0x8304, 0x4E14, 0x602F, 0x7A83, 0x94A6, 0x4FB5, 0x4EB2, 0x79E6, 0x7434, 0x52E4, 0x82B9, 0x64D2, 0x79BD, 0x5BDD, 0x6C81, 0x9752, 0x8F7B, 0x6C22, 0x503E, 0x537F, 0x6E05, 0x64CE, 0x6674, 0x6C30, 0x60C5, 0x9877, 0x8BF7, 0x5E86, 0x743C, 0x7A77, 0x79CB, 0x4E18, 0x90B1, 0x7403, 0x6C42, 0x56DA, 0x914B, 0x6CC5, 0x8D8B, 0x533A, 0x86C6, 0x66F2, 0x8EAF, 0x5C48, 0x9A71, 0x6E20, + 0x83EE, 0x83EF, 0x83F3, 0x83F4, 0x83F5, 0x83F6, 0x83F7, 0x83FA, 0x83FB, 0x83FC, 0x83FE, 0x83FF, 0x8400, 0x8402, 0x8405, 0x8407, 0x8408, 0x8409, 0x840A, 0x8410, 0x8412, 0x8413, 0x8414, 0x8415, 0x8416, 0x8417, 0x8419, 0x841A, 0x841B, 0x841E, 0x841F, 0x8420, 0x8421, 0x8422, 0x8423, 0x8429, 0x842A, 0x842B, 0x842C, 0x842D, 0x842E, 0x842F, 0x8430, 0x8432, 0x8433, 0x8434, 0x8435, 0x8436, 0x8437, 0x8439, 0x843A, 0x843B, 0x843E, 0x843F, 0x8440, 0x8441, 0x8442, 0x8443, 0x8444, 0x8445, 0x8447, 0x8448, 0x8449, 0x844A, 0x844B, 0x844C, 0x844D, 0x844E, 0x844F, 0x8450, 0x8452, 0x8453, 0x8454, 0x8455, 0x8456, 0x8458, 0x845D, 0x845E, 0x845F, 0x8460, 0x8462, 0x8464, 0x8465, 0x8466, 0x8467, 0x8468, 0x846A, 0x846E, 0x846F, 0x8470, 0x8472, 0x8474, 0x8477, 0x8479, 0x847B, + 0x847C, 0x53D6, 0x5A36, 0x9F8B, 0x8DA3, 0x53BB, 0x5708, 0x98A7, 0x6743, 0x919B, 0x6CC9, 0x5168, 0x75CA, 0x62F3, 0x72AC, 0x5238, 0x529D, 0x7F3A, 0x7094, 0x7638, 0x5374, 0x9E4A, 0x69B7, 0x786E, 0x96C0, 0x88D9, 0x7FA4, 0x7136, 0x71C3, 0x5189, 0x67D3, 0x74E4, 0x58E4, 0x6518, 0x56B7, 0x8BA9, 0x9976, 0x6270, 0x7ED5, 0x60F9, 0x70ED, 0x58EC, 0x4EC1, 0x4EBA, 0x5FCD, 0x97E7, 0x4EFB, 0x8BA4, 0x5203, 0x598A, 0x7EAB, 0x6254, 0x4ECD, 0x65E5, 0x620E, 0x8338, 0x84C9, 0x8363, 0x878D, 0x7194, 0x6EB6, 0x5BB9, 0x7ED2, 0x5197, 0x63C9, 0x67D4, 0x8089, 0x8339, 0x8815, 0x5112, 0x5B7A, 0x5982, 0x8FB1, 0x4E73, 0x6C5D, 0x5165, 0x8925, 0x8F6F, 0x962E, 0x854A, 0x745E, 0x9510, 0x95F0, 0x6DA6, 0x82E5, 0x5F31, 0x6492, 0x6D12, 0x8428, 0x816E, 0x9CC3, 0x585E, 0x8D5B, 0x4E09, 0x53C1, + 0x847D, 0x847E, 0x847F, 0x8480, 0x8481, 0x8483, 0x8484, 0x8485, 0x8486, 0x848A, 0x848D, 0x848F, 0x8490, 0x8491, 0x8492, 0x8493, 0x8494, 0x8495, 0x8496, 0x8498, 0x849A, 0x849B, 0x849D, 0x849E, 0x849F, 0x84A0, 0x84A2, 0x84A3, 0x84A4, 0x84A5, 0x84A6, 0x84A7, 0x84A8, 0x84A9, 0x84AA, 0x84AB, 0x84AC, 0x84AD, 0x84AE, 0x84B0, 0x84B1, 0x84B3, 0x84B5, 0x84B6, 0x84B7, 0x84BB, 0x84BC, 0x84BE, 0x84C0, 0x84C2, 0x84C3, 0x84C5, 0x84C6, 0x84C7, 0x84C8, 0x84CB, 0x84CC, 0x84CE, 0x84CF, 0x84D2, 0x84D4, 0x84D5, 0x84D7, 0x84D8, 0x84D9, 0x84DA, 0x84DB, 0x84DC, 0x84DE, 0x84E1, 0x84E2, 0x84E4, 0x84E7, 0x84E8, 0x84E9, 0x84EA, 0x84EB, 0x84ED, 0x84EE, 0x84EF, 0x84F1, 0x84F2, 0x84F3, 0x84F4, 0x84F5, 0x84F6, 0x84F7, 0x84F8, 0x84F9, 0x84FA, 0x84FB, 0x84FD, 0x84FE, 0x8500, 0x8501, + 0x8502, 0x4F1E, 0x6563, 0x6851, 0x55D3, 0x4E27, 0x6414, 0x9A9A, 0x626B, 0x5AC2, 0x745F, 0x8272, 0x6DA9, 0x68EE, 0x50E7, 0x838E, 0x7802, 0x6740, 0x5239, 0x6C99, 0x7EB1, 0x50BB, 0x5565, 0x715E, 0x7B5B, 0x6652, 0x73CA, 0x82EB, 0x6749, 0x5C71, 0x5220, 0x717D, 0x886B, 0x95EA, 0x9655, 0x64C5, 0x8D61, 0x81B3, 0x5584, 0x6C55, 0x6247, 0x7F2E, 0x5892, 0x4F24, 0x5546, 0x8D4F, 0x664C, 0x4E0A, 0x5C1A, 0x88F3, 0x68A2, 0x634E, 0x7A0D, 0x70E7, 0x828D, 0x52FA, 0x97F6, 0x5C11, 0x54E8, 0x90B5, 0x7ECD, 0x5962, 0x8D4A, 0x86C7, 0x820C, 0x820D, 0x8D66, 0x6444, 0x5C04, 0x6151, 0x6D89, 0x793E, 0x8BBE, 0x7837, 0x7533, 0x547B, 0x4F38, 0x8EAB, 0x6DF1, 0x5A20, 0x7EC5, 0x795E, 0x6C88, 0x5BA1, 0x5A76, 0x751A, 0x80BE, 0x614E, 0x6E17, 0x58F0, 0x751F, 0x7525, 0x7272, 0x5347, 0x7EF3, + 0x8503, 0x8504, 0x8505, 0x8506, 0x8507, 0x8508, 0x8509, 0x850A, 0x850B, 0x850D, 0x850E, 0x850F, 0x8510, 0x8512, 0x8514, 0x8515, 0x8516, 0x8518, 0x8519, 0x851B, 0x851C, 0x851D, 0x851E, 0x8520, 0x8522, 0x8523, 0x8524, 0x8525, 0x8526, 0x8527, 0x8528, 0x8529, 0x852A, 0x852D, 0x852E, 0x852F, 0x8530, 0x8531, 0x8532, 0x8533, 0x8534, 0x8535, 0x8536, 0x853E, 0x853F, 0x8540, 0x8541, 0x8542, 0x8544, 0x8545, 0x8546, 0x8547, 0x854B, 0x854C, 0x854D, 0x854E, 0x854F, 0x8550, 0x8551, 0x8552, 0x8553, 0x8554, 0x8555, 0x8557, 0x8558, 0x855A, 0x855B, 0x855C, 0x855D, 0x855F, 0x8560, 0x8561, 0x8562, 0x8563, 0x8565, 0x8566, 0x8567, 0x8569, 0x856A, 0x856B, 0x856C, 0x856D, 0x856E, 0x856F, 0x8570, 0x8571, 0x8573, 0x8575, 0x8576, 0x8577, 0x8578, 0x857C, 0x857D, 0x857F, 0x8580, + 0x8581, 0x7701, 0x76DB, 0x5269, 0x80DC, 0x5723, 0x5E08, 0x5931, 0x72EE, 0x65BD, 0x6E7F, 0x8BD7, 0x5C38, 0x8671, 0x5341, 0x77F3, 0x62FE, 0x65F6, 0x4EC0, 0x98DF, 0x8680, 0x5B9E, 0x8BC6, 0x53F2, 0x77E2, 0x4F7F, 0x5C4E, 0x9A76, 0x59CB, 0x5F0F, 0x793A, 0x58EB, 0x4E16, 0x67FF, 0x4E8B, 0x62ED, 0x8A93, 0x901D, 0x52BF, 0x662F, 0x55DC, 0x566C, 0x9002, 0x4ED5, 0x4F8D, 0x91CA, 0x9970, 0x6C0F, 0x5E02, 0x6043, 0x5BA4, 0x89C6, 0x8BD5, 0x6536, 0x624B, 0x9996, 0x5B88, 0x5BFF, 0x6388, 0x552E, 0x53D7, 0x7626, 0x517D, 0x852C, 0x67A2, 0x68B3, 0x6B8A, 0x6292, 0x8F93, 0x53D4, 0x8212, 0x6DD1, 0x758F, 0x4E66, 0x8D4E, 0x5B70, 0x719F, 0x85AF, 0x6691, 0x66D9, 0x7F72, 0x8700, 0x9ECD, 0x9F20, 0x5C5E, 0x672F, 0x8FF0, 0x6811, 0x675F, 0x620D, 0x7AD6, 0x5885, 0x5EB6, 0x6570, 0x6F31, + 0x8582, 0x8583, 0x8586, 0x8588, 0x8589, 0x858A, 0x858B, 0x858C, 0x858D, 0x858E, 0x8590, 0x8591, 0x8592, 0x8593, 0x8594, 0x8595, 0x8596, 0x8597, 0x8598, 0x8599, 0x859A, 0x859D, 0x859E, 0x859F, 0x85A0, 0x85A1, 0x85A2, 0x85A3, 0x85A5, 0x85A6, 0x85A7, 0x85A9, 0x85AB, 0x85AC, 0x85AD, 0x85B1, 0x85B2, 0x85B3, 0x85B4, 0x85B5, 0x85B6, 0x85B8, 0x85BA, 0x85BB, 0x85BC, 0x85BD, 0x85BE, 0x85BF, 0x85C0, 0x85C2, 0x85C3, 0x85C4, 0x85C5, 0x85C6, 0x85C7, 0x85C8, 0x85CA, 0x85CB, 0x85CC, 0x85CD, 0x85CE, 0x85D1, 0x85D2, 0x85D4, 0x85D6, 0x85D7, 0x85D8, 0x85D9, 0x85DA, 0x85DB, 0x85DD, 0x85DE, 0x85DF, 0x85E0, 0x85E1, 0x85E2, 0x85E3, 0x85E5, 0x85E6, 0x85E7, 0x85E8, 0x85EA, 0x85EB, 0x85EC, 0x85ED, 0x85EE, 0x85EF, 0x85F0, 0x85F1, 0x85F2, 0x85F3, 0x85F4, 0x85F5, 0x85F6, 0x85F7, + 0x85F8, 0x6055, 0x5237, 0x800D, 0x6454, 0x8870, 0x7529, 0x5E05, 0x6813, 0x62F4, 0x971C, 0x53CC, 0x723D, 0x8C01, 0x6C34, 0x7761, 0x7A0E, 0x542E, 0x77AC, 0x987A, 0x821C, 0x8BF4, 0x7855, 0x6714, 0x70C1, 0x65AF, 0x6495, 0x5636, 0x601D, 0x79C1, 0x53F8, 0x4E1D, 0x6B7B, 0x8086, 0x5BFA, 0x55E3, 0x56DB, 0x4F3A, 0x4F3C, 0x9972, 0x5DF3, 0x677E, 0x8038, 0x6002, 0x9882, 0x9001, 0x5B8B, 0x8BBC, 0x8BF5, 0x641C, 0x8258, 0x64DE, 0x55FD, 0x82CF, 0x9165, 0x4FD7, 0x7D20, 0x901F, 0x7C9F, 0x50F3, 0x5851, 0x6EAF, 0x5BBF, 0x8BC9, 0x8083, 0x9178, 0x849C, 0x7B97, 0x867D, 0x968B, 0x968F, 0x7EE5, 0x9AD3, 0x788E, 0x5C81, 0x7A57, 0x9042, 0x96A7, 0x795F, 0x5B59, 0x635F, 0x7B0B, 0x84D1, 0x68AD, 0x5506, 0x7F29, 0x7410, 0x7D22, 0x9501, 0x6240, 0x584C, 0x4ED6, 0x5B83, 0x5979, 0x5854, + 0x85F9, 0x85FA, 0x85FC, 0x85FD, 0x85FE, 0x8600, 0x8601, 0x8602, 0x8603, 0x8604, 0x8606, 0x8607, 0x8608, 0x8609, 0x860A, 0x860B, 0x860C, 0x860D, 0x860E, 0x860F, 0x8610, 0x8612, 0x8613, 0x8614, 0x8615, 0x8617, 0x8618, 0x8619, 0x861A, 0x861B, 0x861C, 0x861D, 0x861E, 0x861F, 0x8620, 0x8621, 0x8622, 0x8623, 0x8624, 0x8625, 0x8626, 0x8628, 0x862A, 0x862B, 0x862C, 0x862D, 0x862E, 0x862F, 0x8630, 0x8631, 0x8632, 0x8633, 0x8634, 0x8635, 0x8636, 0x8637, 0x8639, 0x863A, 0x863B, 0x863D, 0x863E, 0x863F, 0x8640, 0x8641, 0x8642, 0x8643, 0x8644, 0x8645, 0x8646, 0x8647, 0x8648, 0x8649, 0x864A, 0x864B, 0x864C, 0x8652, 0x8653, 0x8655, 0x8656, 0x8657, 0x8658, 0x8659, 0x865B, 0x865C, 0x865D, 0x865F, 0x8660, 0x8661, 0x8663, 0x8664, 0x8665, 0x8666, 0x8667, 0x8668, 0x8669, + 0x866A, 0x736D, 0x631E, 0x8E4B, 0x8E0F, 0x80CE, 0x82D4, 0x62AC, 0x53F0, 0x6CF0, 0x915E, 0x592A, 0x6001, 0x6C70, 0x574D, 0x644A, 0x8D2A, 0x762B, 0x6EE9, 0x575B, 0x6A80, 0x75F0, 0x6F6D, 0x8C2D, 0x8C08, 0x5766, 0x6BEF, 0x8892, 0x78B3, 0x63A2, 0x53F9, 0x70AD, 0x6C64, 0x5858, 0x642A, 0x5802, 0x68E0, 0x819B, 0x5510, 0x7CD6, 0x5018, 0x8EBA, 0x6DCC, 0x8D9F, 0x70EB, 0x638F, 0x6D9B, 0x6ED4, 0x7EE6, 0x8404, 0x6843, 0x9003, 0x6DD8, 0x9676, 0x8BA8, 0x5957, 0x7279, 0x85E4, 0x817E, 0x75BC, 0x8A8A, 0x68AF, 0x5254, 0x8E22, 0x9511, 0x63D0, 0x9898, 0x8E44, 0x557C, 0x4F53, 0x66FF, 0x568F, 0x60D5, 0x6D95, 0x5243, 0x5C49, 0x5929, 0x6DFB, 0x586B, 0x7530, 0x751C, 0x606C, 0x8214, 0x8146, 0x6311, 0x6761, 0x8FE2, 0x773A, 0x8DF3, 0x8D34, 0x94C1, 0x5E16, 0x5385, 0x542C, 0x70C3, + 0x866D, 0x866F, 0x8670, 0x8672, 0x8673, 0x8674, 0x8675, 0x8676, 0x8677, 0x8678, 0x8683, 0x8684, 0x8685, 0x8686, 0x8687, 0x8688, 0x8689, 0x868E, 0x868F, 0x8690, 0x8691, 0x8692, 0x8694, 0x8696, 0x8697, 0x8698, 0x8699, 0x869A, 0x869B, 0x869E, 0x869F, 0x86A0, 0x86A1, 0x86A2, 0x86A5, 0x86A6, 0x86AB, 0x86AD, 0x86AE, 0x86B2, 0x86B3, 0x86B7, 0x86B8, 0x86B9, 0x86BB, 0x86BC, 0x86BD, 0x86BE, 0x86BF, 0x86C1, 0x86C2, 0x86C3, 0x86C5, 0x86C8, 0x86CC, 0x86CD, 0x86D2, 0x86D3, 0x86D5, 0x86D6, 0x86D7, 0x86DA, 0x86DC, 0x86DD, 0x86E0, 0x86E1, 0x86E2, 0x86E3, 0x86E5, 0x86E6, 0x86E7, 0x86E8, 0x86EA, 0x86EB, 0x86EC, 0x86EF, 0x86F5, 0x86F6, 0x86F7, 0x86FA, 0x86FB, 0x86FC, 0x86FD, 0x86FF, 0x8701, 0x8704, 0x8705, 0x8706, 0x870B, 0x870C, 0x870E, 0x870F, 0x8710, 0x8711, 0x8714, + 0x8716, 0x6C40, 0x5EF7, 0x505C, 0x4EAD, 0x5EAD, 0x633A, 0x8247, 0x901A, 0x6850, 0x916E, 0x77B3, 0x540C, 0x94DC, 0x5F64, 0x7AE5, 0x6876, 0x6345, 0x7B52, 0x7EDF, 0x75DB, 0x5077, 0x6295, 0x5934, 0x900F, 0x51F8, 0x79C3, 0x7A81, 0x56FE, 0x5F92, 0x9014, 0x6D82, 0x5C60, 0x571F, 0x5410, 0x5154, 0x6E4D, 0x56E2, 0x63A8, 0x9893, 0x817F, 0x8715, 0x892A, 0x9000, 0x541E, 0x5C6F, 0x81C0, 0x62D6, 0x6258, 0x8131, 0x9E35, 0x9640, 0x9A6E, 0x9A7C, 0x692D, 0x59A5, 0x62D3, 0x553E, 0x6316, 0x54C7, 0x86D9, 0x6D3C, 0x5A03, 0x74E6, 0x889C, 0x6B6A, 0x5916, 0x8C4C, 0x5F2F, 0x6E7E, 0x73A9, 0x987D, 0x4E38, 0x70F7, 0x5B8C, 0x7897, 0x633D, 0x665A, 0x7696, 0x60CB, 0x5B9B, 0x5A49, 0x4E07, 0x8155, 0x6C6A, 0x738B, 0x4EA1, 0x6789, 0x7F51, 0x5F80, 0x65FA, 0x671B, 0x5FD8, 0x5984, 0x5A01, + 0x8719, 0x871B, 0x871D, 0x871F, 0x8720, 0x8724, 0x8726, 0x8727, 0x8728, 0x872A, 0x872B, 0x872C, 0x872D, 0x872F, 0x8730, 0x8732, 0x8733, 0x8735, 0x8736, 0x8738, 0x8739, 0x873A, 0x873C, 0x873D, 0x8740, 0x8741, 0x8742, 0x8743, 0x8744, 0x8745, 0x8746, 0x874A, 0x874B, 0x874D, 0x874F, 0x8750, 0x8751, 0x8752, 0x8754, 0x8755, 0x8756, 0x8758, 0x875A, 0x875B, 0x875C, 0x875D, 0x875E, 0x875F, 0x8761, 0x8762, 0x8766, 0x8767, 0x8768, 0x8769, 0x876A, 0x876B, 0x876C, 0x876D, 0x876F, 0x8771, 0x8772, 0x8773, 0x8775, 0x8777, 0x8778, 0x8779, 0x877A, 0x877F, 0x8780, 0x8781, 0x8784, 0x8786, 0x8787, 0x8789, 0x878A, 0x878C, 0x878E, 0x878F, 0x8790, 0x8791, 0x8792, 0x8794, 0x8795, 0x8796, 0x8798, 0x8799, 0x879A, 0x879B, 0x879C, 0x879D, 0x879E, 0x87A0, 0x87A1, 0x87A2, 0x87A3, + 0x87A4, 0x5DCD, 0x5FAE, 0x5371, 0x97E6, 0x8FDD, 0x6845, 0x56F4, 0x552F, 0x60DF, 0x4E3A, 0x6F4D, 0x7EF4, 0x82C7, 0x840E, 0x59D4, 0x4F1F, 0x4F2A, 0x5C3E, 0x7EAC, 0x672A, 0x851A, 0x5473, 0x754F, 0x80C3, 0x5582, 0x9B4F, 0x4F4D, 0x6E2D, 0x8C13, 0x5C09, 0x6170, 0x536B, 0x761F, 0x6E29, 0x868A, 0x6587, 0x95FB, 0x7EB9, 0x543B, 0x7A33, 0x7D0A, 0x95EE, 0x55E1, 0x7FC1, 0x74EE, 0x631D, 0x8717, 0x6DA1, 0x7A9D, 0x6211, 0x65A1, 0x5367, 0x63E1, 0x6C83, 0x5DEB, 0x545C, 0x94A8, 0x4E4C, 0x6C61, 0x8BEC, 0x5C4B, 0x65E0, 0x829C, 0x68A7, 0x543E, 0x5434, 0x6BCB, 0x6B66, 0x4E94, 0x6342, 0x5348, 0x821E, 0x4F0D, 0x4FAE, 0x575E, 0x620A, 0x96FE, 0x6664, 0x7269, 0x52FF, 0x52A1, 0x609F, 0x8BEF, 0x6614, 0x7199, 0x6790, 0x897F, 0x7852, 0x77FD, 0x6670, 0x563B, 0x5438, 0x9521, 0x727A, + 0x87A5, 0x87A6, 0x87A7, 0x87A9, 0x87AA, 0x87AE, 0x87B0, 0x87B1, 0x87B2, 0x87B4, 0x87B6, 0x87B7, 0x87B8, 0x87B9, 0x87BB, 0x87BC, 0x87BE, 0x87BF, 0x87C1, 0x87C2, 0x87C3, 0x87C4, 0x87C5, 0x87C7, 0x87C8, 0x87C9, 0x87CC, 0x87CD, 0x87CE, 0x87CF, 0x87D0, 0x87D4, 0x87D5, 0x87D6, 0x87D7, 0x87D8, 0x87D9, 0x87DA, 0x87DC, 0x87DD, 0x87DE, 0x87DF, 0x87E1, 0x87E2, 0x87E3, 0x87E4, 0x87E6, 0x87E7, 0x87E8, 0x87E9, 0x87EB, 0x87EC, 0x87ED, 0x87EF, 0x87F0, 0x87F1, 0x87F2, 0x87F3, 0x87F4, 0x87F5, 0x87F6, 0x87F7, 0x87F8, 0x87FA, 0x87FB, 0x87FC, 0x87FD, 0x87FF, 0x8800, 0x8801, 0x8802, 0x8804, 0x8805, 0x8806, 0x8807, 0x8808, 0x8809, 0x880B, 0x880C, 0x880D, 0x880E, 0x880F, 0x8810, 0x8811, 0x8812, 0x8814, 0x8817, 0x8818, 0x8819, 0x881A, 0x881C, 0x881D, 0x881E, 0x881F, 0x8820, + 0x8823, 0x7A00, 0x606F, 0x5E0C, 0x6089, 0x819D, 0x5915, 0x60DC, 0x7184, 0x70EF, 0x6EAA, 0x6C50, 0x7280, 0x6A84, 0x88AD, 0x5E2D, 0x4E60, 0x5AB3, 0x559C, 0x94E3, 0x6D17, 0x7CFB, 0x9699, 0x620F, 0x7EC6, 0x778E, 0x867E, 0x5323, 0x971E, 0x8F96, 0x6687, 0x5CE1, 0x4FA0, 0x72ED, 0x4E0B, 0x53A6, 0x590F, 0x5413, 0x6380, 0x9528, 0x5148, 0x4ED9, 0x9C9C, 0x7EA4, 0x54B8, 0x8D24, 0x8854, 0x8237, 0x95F2, 0x6D8E, 0x5F26, 0x5ACC, 0x663E, 0x9669, 0x73B0, 0x732E, 0x53BF, 0x817A, 0x9985, 0x7FA1, 0x5BAA, 0x9677, 0x9650, 0x7EBF, 0x76F8, 0x53A2, 0x9576, 0x9999, 0x7BB1, 0x8944, 0x6E58, 0x4E61, 0x7FD4, 0x7965, 0x8BE6, 0x60F3, 0x54CD, 0x4EAB, 0x9879, 0x5DF7, 0x6A61, 0x50CF, 0x5411, 0x8C61, 0x8427, 0x785D, 0x9704, 0x524A, 0x54EE, 0x56A3, 0x9500, 0x6D88, 0x5BB5, 0x6DC6, 0x6653, + 0x8824, 0x8825, 0x8826, 0x8827, 0x8828, 0x8829, 0x882A, 0x882B, 0x882C, 0x882D, 0x882E, 0x882F, 0x8830, 0x8831, 0x8833, 0x8834, 0x8835, 0x8836, 0x8837, 0x8838, 0x883A, 0x883B, 0x883D, 0x883E, 0x883F, 0x8841, 0x8842, 0x8843, 0x8846, 0x8847, 0x8848, 0x8849, 0x884A, 0x884B, 0x884E, 0x884F, 0x8850, 0x8851, 0x8852, 0x8853, 0x8855, 0x8856, 0x8858, 0x885A, 0x885B, 0x885C, 0x885D, 0x885E, 0x885F, 0x8860, 0x8866, 0x8867, 0x886A, 0x886D, 0x886F, 0x8871, 0x8873, 0x8874, 0x8875, 0x8876, 0x8878, 0x8879, 0x887A, 0x887B, 0x887C, 0x8880, 0x8883, 0x8886, 0x8887, 0x8889, 0x888A, 0x888C, 0x888E, 0x888F, 0x8890, 0x8891, 0x8893, 0x8894, 0x8895, 0x8897, 0x8898, 0x8899, 0x889A, 0x889B, 0x889D, 0x889E, 0x889F, 0x88A0, 0x88A1, 0x88A3, 0x88A5, 0x88A6, 0x88A7, 0x88A8, 0x88A9, + 0x88AA, 0x5C0F, 0x5B5D, 0x6821, 0x8096, 0x5578, 0x7B11, 0x6548, 0x6954, 0x4E9B, 0x6B47, 0x874E, 0x978B, 0x534F, 0x631F, 0x643A, 0x90AA, 0x659C, 0x80C1, 0x8C10, 0x5199, 0x68B0, 0x5378, 0x87F9, 0x61C8, 0x6CC4, 0x6CFB, 0x8C22, 0x5C51, 0x85AA, 0x82AF, 0x950C, 0x6B23, 0x8F9B, 0x65B0, 0x5FFB, 0x5FC3, 0x4FE1, 0x8845, 0x661F, 0x8165, 0x7329, 0x60FA, 0x5174, 0x5211, 0x578B, 0x5F62, 0x90A2, 0x884C, 0x9192, 0x5E78, 0x674F, 0x6027, 0x59D3, 0x5144, 0x51F6, 0x80F8, 0x5308, 0x6C79, 0x96C4, 0x718A, 0x4F11, 0x4FEE, 0x7F9E, 0x673D, 0x55C5, 0x9508, 0x79C0, 0x8896, 0x7EE3, 0x589F, 0x620C, 0x9700, 0x865A, 0x5618, 0x987B, 0x5F90, 0x8BB8, 0x84C4, 0x9157, 0x53D9, 0x65ED, 0x5E8F, 0x755C, 0x6064, 0x7D6E, 0x5A7F, 0x7EEA, 0x7EED, 0x8F69, 0x55A7, 0x5BA3, 0x60AC, 0x65CB, 0x7384, + 0x88AC, 0x88AE, 0x88AF, 0x88B0, 0x88B2, 0x88B3, 0x88B4, 0x88B5, 0x88B6, 0x88B8, 0x88B9, 0x88BA, 0x88BB, 0x88BD, 0x88BE, 0x88BF, 0x88C0, 0x88C3, 0x88C4, 0x88C7, 0x88C8, 0x88CA, 0x88CB, 0x88CC, 0x88CD, 0x88CF, 0x88D0, 0x88D1, 0x88D3, 0x88D6, 0x88D7, 0x88DA, 0x88DB, 0x88DC, 0x88DD, 0x88DE, 0x88E0, 0x88E1, 0x88E6, 0x88E7, 0x88E9, 0x88EA, 0x88EB, 0x88EC, 0x88ED, 0x88EE, 0x88EF, 0x88F2, 0x88F5, 0x88F6, 0x88F7, 0x88FA, 0x88FB, 0x88FD, 0x88FF, 0x8900, 0x8901, 0x8903, 0x8904, 0x8905, 0x8906, 0x8907, 0x8908, 0x8909, 0x890B, 0x890C, 0x890D, 0x890E, 0x890F, 0x8911, 0x8914, 0x8915, 0x8916, 0x8917, 0x8918, 0x891C, 0x891D, 0x891E, 0x891F, 0x8920, 0x8922, 0x8923, 0x8924, 0x8926, 0x8927, 0x8928, 0x8929, 0x892C, 0x892D, 0x892E, 0x892F, 0x8931, 0x8932, 0x8933, 0x8935, + 0x8937, 0x9009, 0x7663, 0x7729, 0x7EDA, 0x9774, 0x859B, 0x5B66, 0x7A74, 0x96EA, 0x8840, 0x52CB, 0x718F, 0x5FAA, 0x65EC, 0x8BE2, 0x5BFB, 0x9A6F, 0x5DE1, 0x6B89, 0x6C5B, 0x8BAD, 0x8BAF, 0x900A, 0x8FC5, 0x538B, 0x62BC, 0x9E26, 0x9E2D, 0x5440, 0x4E2B, 0x82BD, 0x7259, 0x869C, 0x5D16, 0x8859, 0x6DAF, 0x96C5, 0x54D1, 0x4E9A, 0x8BB6, 0x7109, 0x54BD, 0x9609, 0x70DF, 0x6DF9, 0x76D0, 0x4E25, 0x7814, 0x8712, 0x5CA9, 0x5EF6, 0x8A00, 0x989C, 0x960E, 0x708E, 0x6CBF, 0x5944, 0x63A9, 0x773C, 0x884D, 0x6F14, 0x8273, 0x5830, 0x71D5, 0x538C, 0x781A, 0x96C1, 0x5501, 0x5F66, 0x7130, 0x5BB4, 0x8C1A, 0x9A8C, 0x6B83, 0x592E, 0x9E2F, 0x79E7, 0x6768, 0x626C, 0x4F6F, 0x75A1, 0x7F8A, 0x6D0B, 0x9633, 0x6C27, 0x4EF0, 0x75D2, 0x517B, 0x6837, 0x6F3E, 0x9080, 0x8170, 0x5996, 0x7476, + 0x8938, 0x8939, 0x893A, 0x893B, 0x893C, 0x893D, 0x893E, 0x893F, 0x8940, 0x8942, 0x8943, 0x8945, 0x8946, 0x8947, 0x8948, 0x8949, 0x894A, 0x894B, 0x894C, 0x894D, 0x894E, 0x894F, 0x8950, 0x8951, 0x8952, 0x8953, 0x8954, 0x8955, 0x8956, 0x8957, 0x8958, 0x8959, 0x895A, 0x895B, 0x895C, 0x895D, 0x8960, 0x8961, 0x8962, 0x8963, 0x8964, 0x8965, 0x8967, 0x8968, 0x8969, 0x896A, 0x896B, 0x896C, 0x896D, 0x896E, 0x896F, 0x8970, 0x8971, 0x8972, 0x8973, 0x8974, 0x8975, 0x8976, 0x8977, 0x8978, 0x8979, 0x897A, 0x897C, 0x897D, 0x897E, 0x8980, 0x8982, 0x8984, 0x8985, 0x8987, 0x8988, 0x8989, 0x898A, 0x898B, 0x898C, 0x898D, 0x898E, 0x898F, 0x8990, 0x8991, 0x8992, 0x8993, 0x8994, 0x8995, 0x8996, 0x8997, 0x8998, 0x8999, 0x899A, 0x899B, 0x899C, 0x899D, 0x899E, 0x899F, 0x89A0, + 0x89A1, 0x6447, 0x5C27, 0x9065, 0x7A91, 0x8C23, 0x59DA, 0x54AC, 0x8200, 0x836F, 0x8981, 0x8000, 0x6930, 0x564E, 0x8036, 0x7237, 0x91CE, 0x51B6, 0x4E5F, 0x9875, 0x6396, 0x4E1A, 0x53F6, 0x66F3, 0x814B, 0x591C, 0x6DB2, 0x4E00, 0x58F9, 0x533B, 0x63D6, 0x94F1, 0x4F9D, 0x4F0A, 0x8863, 0x9890, 0x5937, 0x9057, 0x79FB, 0x4EEA, 0x80F0, 0x7591, 0x6C82, 0x5B9C, 0x59E8, 0x5F5D, 0x6905, 0x8681, 0x501A, 0x5DF2, 0x4E59, 0x77E3, 0x4EE5, 0x827A, 0x6291, 0x6613, 0x9091, 0x5C79, 0x4EBF, 0x5F79, 0x81C6, 0x9038, 0x8084, 0x75AB, 0x4EA6, 0x88D4, 0x610F, 0x6BC5, 0x5FC6, 0x4E49, 0x76CA, 0x6EA2, 0x8BE3, 0x8BAE, 0x8C0A, 0x8BD1, 0x5F02, 0x7FFC, 0x7FCC, 0x7ECE, 0x8335, 0x836B, 0x56E0, 0x6BB7, 0x97F3, 0x9634, 0x59FB, 0x541F, 0x94F6, 0x6DEB, 0x5BC5, 0x996E, 0x5C39, 0x5F15, 0x9690, + 0x89A2, 0x89A3, 0x89A4, 0x89A5, 0x89A6, 0x89A7, 0x89A8, 0x89A9, 0x89AA, 0x89AB, 0x89AC, 0x89AD, 0x89AE, 0x89AF, 0x89B0, 0x89B1, 0x89B2, 0x89B3, 0x89B4, 0x89B5, 0x89B6, 0x89B7, 0x89B8, 0x89B9, 0x89BA, 0x89BB, 0x89BC, 0x89BD, 0x89BE, 0x89BF, 0x89C0, 0x89C3, 0x89CD, 0x89D3, 0x89D4, 0x89D5, 0x89D7, 0x89D8, 0x89D9, 0x89DB, 0x89DD, 0x89DF, 0x89E0, 0x89E1, 0x89E2, 0x89E4, 0x89E7, 0x89E8, 0x89E9, 0x89EA, 0x89EC, 0x89ED, 0x89EE, 0x89F0, 0x89F1, 0x89F2, 0x89F4, 0x89F5, 0x89F6, 0x89F7, 0x89F8, 0x89F9, 0x89FA, 0x89FB, 0x89FC, 0x89FD, 0x89FE, 0x89FF, 0x8A01, 0x8A02, 0x8A03, 0x8A04, 0x8A05, 0x8A06, 0x8A08, 0x8A09, 0x8A0A, 0x8A0B, 0x8A0C, 0x8A0D, 0x8A0E, 0x8A0F, 0x8A10, 0x8A11, 0x8A12, 0x8A13, 0x8A14, 0x8A15, 0x8A16, 0x8A17, 0x8A18, 0x8A19, 0x8A1A, 0x8A1B, 0x8A1C, + 0x8A1D, 0x5370, 0x82F1, 0x6A31, 0x5A74, 0x9E70, 0x5E94, 0x7F28, 0x83B9, 0x8424, 0x8425, 0x8367, 0x8747, 0x8FCE, 0x8D62, 0x76C8, 0x5F71, 0x9896, 0x786C, 0x6620, 0x54DF, 0x62E5, 0x4F63, 0x81C3, 0x75C8, 0x5EB8, 0x96CD, 0x8E0A, 0x86F9, 0x548F, 0x6CF3, 0x6D8C, 0x6C38, 0x607F, 0x52C7, 0x7528, 0x5E7D, 0x4F18, 0x60A0, 0x5FE7, 0x5C24, 0x7531, 0x90AE, 0x94C0, 0x72B9, 0x6CB9, 0x6E38, 0x9149, 0x6709, 0x53CB, 0x53F3, 0x4F51, 0x91C9, 0x8BF1, 0x53C8, 0x5E7C, 0x8FC2, 0x6DE4, 0x4E8E, 0x76C2, 0x6986, 0x865E, 0x611A, 0x8206, 0x4F59, 0x4FDE, 0x903E, 0x9C7C, 0x6109, 0x6E1D, 0x6E14, 0x9685, 0x4E88, 0x5A31, 0x96E8, 0x4E0E, 0x5C7F, 0x79B9, 0x5B87, 0x8BED, 0x7FBD, 0x7389, 0x57DF, 0x828B, 0x90C1, 0x5401, 0x9047, 0x55BB, 0x5CEA, 0x5FA1, 0x6108, 0x6B32, 0x72F1, 0x80B2, 0x8A89, + 0x8A1E, 0x8A1F, 0x8A20, 0x8A21, 0x8A22, 0x8A23, 0x8A24, 0x8A25, 0x8A26, 0x8A27, 0x8A28, 0x8A29, 0x8A2A, 0x8A2B, 0x8A2C, 0x8A2D, 0x8A2E, 0x8A2F, 0x8A30, 0x8A31, 0x8A32, 0x8A33, 0x8A34, 0x8A35, 0x8A36, 0x8A37, 0x8A38, 0x8A39, 0x8A3A, 0x8A3B, 0x8A3C, 0x8A3D, 0x8A3F, 0x8A40, 0x8A41, 0x8A42, 0x8A43, 0x8A44, 0x8A45, 0x8A46, 0x8A47, 0x8A49, 0x8A4A, 0x8A4B, 0x8A4C, 0x8A4D, 0x8A4E, 0x8A4F, 0x8A50, 0x8A51, 0x8A52, 0x8A53, 0x8A54, 0x8A55, 0x8A56, 0x8A57, 0x8A58, 0x8A59, 0x8A5A, 0x8A5B, 0x8A5C, 0x8A5D, 0x8A5E, 0x8A5F, 0x8A60, 0x8A61, 0x8A62, 0x8A63, 0x8A64, 0x8A65, 0x8A66, 0x8A67, 0x8A68, 0x8A69, 0x8A6A, 0x8A6B, 0x8A6C, 0x8A6D, 0x8A6E, 0x8A6F, 0x8A70, 0x8A71, 0x8A72, 0x8A73, 0x8A74, 0x8A75, 0x8A76, 0x8A77, 0x8A78, 0x8A7A, 0x8A7B, 0x8A7C, 0x8A7D, 0x8A7E, 0x8A7F, + 0x8A80, 0x6D74, 0x5BD3, 0x88D5, 0x9884, 0x8C6B, 0x9A6D, 0x9E33, 0x6E0A, 0x51A4, 0x5143, 0x57A3, 0x8881, 0x539F, 0x63F4, 0x8F95, 0x56ED, 0x5458, 0x5706, 0x733F, 0x6E90, 0x7F18, 0x8FDC, 0x82D1, 0x613F, 0x6028, 0x9662, 0x66F0, 0x7EA6, 0x8D8A, 0x8DC3, 0x94A5, 0x5CB3, 0x7CA4, 0x6708, 0x60A6, 0x9605, 0x8018, 0x4E91, 0x90E7, 0x5300, 0x9668, 0x5141, 0x8FD0, 0x8574, 0x915D, 0x6655, 0x97F5, 0x5B55, 0x531D, 0x7838, 0x6742, 0x683D, 0x54C9, 0x707E, 0x5BB0, 0x8F7D, 0x518D, 0x5728, 0x54B1, 0x6512, 0x6682, 0x8D5E, 0x8D43, 0x810F, 0x846C, 0x906D, 0x7CDF, 0x51FF, 0x85FB, 0x67A3, 0x65E9, 0x6FA1, 0x86A4, 0x8E81, 0x566A, 0x9020, 0x7682, 0x7076, 0x71E5, 0x8D23, 0x62E9, 0x5219, 0x6CFD, 0x8D3C, 0x600E, 0x589E, 0x618E, 0x66FE, 0x8D60, 0x624E, 0x55B3, 0x6E23, 0x672D, 0x8F67, + 0x8A81, 0x8A82, 0x8A83, 0x8A84, 0x8A85, 0x8A86, 0x8A87, 0x8A88, 0x8A8B, 0x8A8C, 0x8A8D, 0x8A8E, 0x8A8F, 0x8A90, 0x8A91, 0x8A92, 0x8A94, 0x8A95, 0x8A96, 0x8A97, 0x8A98, 0x8A99, 0x8A9A, 0x8A9B, 0x8A9C, 0x8A9D, 0x8A9E, 0x8A9F, 0x8AA0, 0x8AA1, 0x8AA2, 0x8AA3, 0x8AA4, 0x8AA5, 0x8AA6, 0x8AA7, 0x8AA8, 0x8AA9, 0x8AAA, 0x8AAB, 0x8AAC, 0x8AAD, 0x8AAE, 0x8AAF, 0x8AB0, 0x8AB1, 0x8AB2, 0x8AB3, 0x8AB4, 0x8AB5, 0x8AB6, 0x8AB7, 0x8AB8, 0x8AB9, 0x8ABA, 0x8ABB, 0x8ABC, 0x8ABD, 0x8ABE, 0x8ABF, 0x8AC0, 0x8AC1, 0x8AC2, 0x8AC3, 0x8AC4, 0x8AC5, 0x8AC6, 0x8AC7, 0x8AC8, 0x8AC9, 0x8ACA, 0x8ACB, 0x8ACC, 0x8ACD, 0x8ACE, 0x8ACF, 0x8AD0, 0x8AD1, 0x8AD2, 0x8AD3, 0x8AD4, 0x8AD5, 0x8AD6, 0x8AD7, 0x8AD8, 0x8AD9, 0x8ADA, 0x8ADB, 0x8ADC, 0x8ADD, 0x8ADE, 0x8ADF, 0x8AE0, 0x8AE1, 0x8AE2, + 0x8AE3, 0x94E1, 0x95F8, 0x7728, 0x6805, 0x69A8, 0x548B, 0x4E4D, 0x70B8, 0x8BC8, 0x6458, 0x658B, 0x5B85, 0x7A84, 0x503A, 0x5BE8, 0x77BB, 0x6BE1, 0x8A79, 0x7C98, 0x6CBE, 0x76CF, 0x65A9, 0x8F97, 0x5D2D, 0x5C55, 0x8638, 0x6808, 0x5360, 0x6218, 0x7AD9, 0x6E5B, 0x7EFD, 0x6A1F, 0x7AE0, 0x5F70, 0x6F33, 0x5F20, 0x638C, 0x6DA8, 0x6756, 0x4E08, 0x5E10, 0x8D26, 0x4ED7, 0x80C0, 0x7634, 0x969C, 0x62DB, 0x662D, 0x627E, 0x6CBC, 0x8D75, 0x7167, 0x7F69, 0x5146, 0x8087, 0x53EC, 0x906E, 0x6298, 0x54F2, 0x86F0, 0x8F99, 0x8005, 0x9517, 0x8517, 0x8FD9, 0x6D59, 0x73CD, 0x659F, 0x771F, 0x7504, 0x7827, 0x81FB, 0x8D1E, 0x9488, 0x4FA6, 0x6795, 0x75B9, 0x8BCA, 0x9707, 0x632F, 0x9547, 0x9635, 0x84B8, 0x6323, 0x7741, 0x5F81, 0x72F0, 0x4E89, 0x6014, 0x6574, 0x62EF, 0x6B63, 0x653F, + 0x8AE4, 0x8AE5, 0x8AE6, 0x8AE7, 0x8AE8, 0x8AE9, 0x8AEA, 0x8AEB, 0x8AEC, 0x8AED, 0x8AEE, 0x8AEF, 0x8AF0, 0x8AF1, 0x8AF2, 0x8AF3, 0x8AF4, 0x8AF5, 0x8AF6, 0x8AF7, 0x8AF8, 0x8AF9, 0x8AFA, 0x8AFB, 0x8AFC, 0x8AFD, 0x8AFE, 0x8AFF, 0x8B00, 0x8B01, 0x8B02, 0x8B03, 0x8B04, 0x8B05, 0x8B06, 0x8B08, 0x8B09, 0x8B0A, 0x8B0B, 0x8B0C, 0x8B0D, 0x8B0E, 0x8B0F, 0x8B10, 0x8B11, 0x8B12, 0x8B13, 0x8B14, 0x8B15, 0x8B16, 0x8B17, 0x8B18, 0x8B19, 0x8B1A, 0x8B1B, 0x8B1C, 0x8B1D, 0x8B1E, 0x8B1F, 0x8B20, 0x8B21, 0x8B22, 0x8B23, 0x8B24, 0x8B25, 0x8B27, 0x8B28, 0x8B29, 0x8B2A, 0x8B2B, 0x8B2C, 0x8B2D, 0x8B2E, 0x8B2F, 0x8B30, 0x8B31, 0x8B32, 0x8B33, 0x8B34, 0x8B35, 0x8B36, 0x8B37, 0x8B38, 0x8B39, 0x8B3A, 0x8B3B, 0x8B3C, 0x8B3D, 0x8B3E, 0x8B3F, 0x8B40, 0x8B41, 0x8B42, 0x8B43, 0x8B44, + 0x8B45, 0x5E27, 0x75C7, 0x90D1, 0x8BC1, 0x829D, 0x679D, 0x652F, 0x5431, 0x8718, 0x77E5, 0x80A2, 0x8102, 0x6C41, 0x4E4B, 0x7EC7, 0x804C, 0x76F4, 0x690D, 0x6B96, 0x6267, 0x503C, 0x4F84, 0x5740, 0x6307, 0x6B62, 0x8DBE, 0x53EA, 0x65E8, 0x7EB8, 0x5FD7, 0x631A, 0x63B7, 0x81F3, 0x81F4, 0x7F6E, 0x5E1C, 0x5CD9, 0x5236, 0x667A, 0x79E9, 0x7A1A, 0x8D28, 0x7099, 0x75D4, 0x6EDE, 0x6CBB, 0x7A92, 0x4E2D, 0x76C5, 0x5FE0, 0x949F, 0x8877, 0x7EC8, 0x79CD, 0x80BF, 0x91CD, 0x4EF2, 0x4F17, 0x821F, 0x5468, 0x5DDE, 0x6D32, 0x8BCC, 0x7CA5, 0x8F74, 0x8098, 0x5E1A, 0x5492, 0x76B1, 0x5B99, 0x663C, 0x9AA4, 0x73E0, 0x682A, 0x86DB, 0x6731, 0x732A, 0x8BF8, 0x8BDB, 0x9010, 0x7AF9, 0x70DB, 0x716E, 0x62C4, 0x77A9, 0x5631, 0x4E3B, 0x8457, 0x67F1, 0x52A9, 0x86C0, 0x8D2E, 0x94F8, 0x7B51, + 0x8B46, 0x8B47, 0x8B48, 0x8B49, 0x8B4A, 0x8B4B, 0x8B4C, 0x8B4D, 0x8B4E, 0x8B4F, 0x8B50, 0x8B51, 0x8B52, 0x8B53, 0x8B54, 0x8B55, 0x8B56, 0x8B57, 0x8B58, 0x8B59, 0x8B5A, 0x8B5B, 0x8B5C, 0x8B5D, 0x8B5E, 0x8B5F, 0x8B60, 0x8B61, 0x8B62, 0x8B63, 0x8B64, 0x8B65, 0x8B67, 0x8B68, 0x8B69, 0x8B6A, 0x8B6B, 0x8B6D, 0x8B6E, 0x8B6F, 0x8B70, 0x8B71, 0x8B72, 0x8B73, 0x8B74, 0x8B75, 0x8B76, 0x8B77, 0x8B78, 0x8B79, 0x8B7A, 0x8B7B, 0x8B7C, 0x8B7D, 0x8B7E, 0x8B7F, 0x8B80, 0x8B81, 0x8B82, 0x8B83, 0x8B84, 0x8B85, 0x8B86, 0x8B87, 0x8B88, 0x8B89, 0x8B8A, 0x8B8B, 0x8B8C, 0x8B8D, 0x8B8E, 0x8B8F, 0x8B90, 0x8B91, 0x8B92, 0x8B93, 0x8B94, 0x8B95, 0x8B96, 0x8B97, 0x8B98, 0x8B99, 0x8B9A, 0x8B9B, 0x8B9C, 0x8B9D, 0x8B9E, 0x8B9F, 0x8BAC, 0x8BB1, 0x8BBB, 0x8BC7, 0x8BD0, 0x8BEA, 0x8C09, + 0x8C1E, 0x4F4F, 0x6CE8, 0x795D, 0x9A7B, 0x6293, 0x722A, 0x62FD, 0x4E13, 0x7816, 0x8F6C, 0x64B0, 0x8D5A, 0x7BC6, 0x6869, 0x5E84, 0x88C5, 0x5986, 0x649E, 0x58EE, 0x72B6, 0x690E, 0x9525, 0x8FFD, 0x8D58, 0x5760, 0x7F00, 0x8C06, 0x51C6, 0x6349, 0x62D9, 0x5353, 0x684C, 0x7422, 0x8301, 0x914C, 0x5544, 0x7740, 0x707C, 0x6D4A, 0x5179, 0x54A8, 0x8D44, 0x59FF, 0x6ECB, 0x6DC4, 0x5B5C, 0x7D2B, 0x4ED4, 0x7C7D, 0x6ED3, 0x5B50, 0x81EA, 0x6E0D, 0x5B57, 0x9B03, 0x68D5, 0x8E2A, 0x5B97, 0x7EFC, 0x603B, 0x7EB5, 0x90B9, 0x8D70, 0x594F, 0x63CD, 0x79DF, 0x8DB3, 0x5352, 0x65CF, 0x7956, 0x8BC5, 0x963B, 0x7EC4, 0x94BB, 0x7E82, 0x5634, 0x9189, 0x6700, 0x7F6A, 0x5C0A, 0x9075, 0x6628, 0x5DE6, 0x4F50, 0x67DE, 0x505A, 0x4F5C, 0x5750, 0x5EA7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x8C38, 0x8C39, 0x8C3A, 0x8C3B, 0x8C3C, 0x8C3D, 0x8C3E, 0x8C3F, 0x8C40, 0x8C42, 0x8C43, 0x8C44, 0x8C45, 0x8C48, 0x8C4A, 0x8C4B, 0x8C4D, 0x8C4E, 0x8C4F, 0x8C50, 0x8C51, 0x8C52, 0x8C53, 0x8C54, 0x8C56, 0x8C57, 0x8C58, 0x8C59, 0x8C5B, 0x8C5C, 0x8C5D, 0x8C5E, 0x8C5F, 0x8C60, 0x8C63, 0x8C64, 0x8C65, 0x8C66, 0x8C67, 0x8C68, 0x8C69, 0x8C6C, 0x8C6D, 0x8C6E, 0x8C6F, 0x8C70, 0x8C71, 0x8C72, 0x8C74, 0x8C75, 0x8C76, 0x8C77, 0x8C7B, 0x8C7C, 0x8C7D, 0x8C7E, 0x8C7F, 0x8C80, 0x8C81, 0x8C83, 0x8C84, 0x8C86, 0x8C87, 0x8C88, 0x8C8B, 0x8C8D, 0x8C8E, 0x8C8F, 0x8C90, 0x8C91, 0x8C92, 0x8C93, 0x8C95, 0x8C96, 0x8C97, 0x8C99, 0x8C9A, 0x8C9B, 0x8C9C, 0x8C9D, 0x8C9E, 0x8C9F, 0x8CA0, 0x8CA1, 0x8CA2, 0x8CA3, 0x8CA4, 0x8CA5, 0x8CA6, 0x8CA7, 0x8CA8, 0x8CA9, 0x8CAA, 0x8CAB, 0x8CAC, + 0x8CAD, 0x4E8D, 0x4E0C, 0x5140, 0x4E10, 0x5EFF, 0x5345, 0x4E15, 0x4E98, 0x4E1E, 0x9B32, 0x5B6C, 0x5669, 0x4E28, 0x79BA, 0x4E3F, 0x5315, 0x4E47, 0x592D, 0x723B, 0x536E, 0x6C10, 0x56DF, 0x80E4, 0x9997, 0x6BD3, 0x777E, 0x9F17, 0x4E36, 0x4E9F, 0x9F10, 0x4E5C, 0x4E69, 0x4E93, 0x8288, 0x5B5B, 0x556C, 0x560F, 0x4EC4, 0x538D, 0x539D, 0x53A3, 0x53A5, 0x53AE, 0x9765, 0x8D5D, 0x531A, 0x53F5, 0x5326, 0x532E, 0x533E, 0x8D5C, 0x5366, 0x5363, 0x5202, 0x5208, 0x520E, 0x522D, 0x5233, 0x523F, 0x5240, 0x524C, 0x525E, 0x5261, 0x525C, 0x84AF, 0x527D, 0x5282, 0x5281, 0x5290, 0x5293, 0x5182, 0x7F54, 0x4EBB, 0x4EC3, 0x4EC9, 0x4EC2, 0x4EE8, 0x4EE1, 0x4EEB, 0x4EDE, 0x4F1B, 0x4EF3, 0x4F22, 0x4F64, 0x4EF5, 0x4F25, 0x4F27, 0x4F09, 0x4F2B, 0x4F5E, 0x4F67, 0x6538, 0x4F5A, 0x4F5D, + 0x8CAE, 0x8CAF, 0x8CB0, 0x8CB1, 0x8CB2, 0x8CB3, 0x8CB4, 0x8CB5, 0x8CB6, 0x8CB7, 0x8CB8, 0x8CB9, 0x8CBA, 0x8CBB, 0x8CBC, 0x8CBD, 0x8CBE, 0x8CBF, 0x8CC0, 0x8CC1, 0x8CC2, 0x8CC3, 0x8CC4, 0x8CC5, 0x8CC6, 0x8CC7, 0x8CC8, 0x8CC9, 0x8CCA, 0x8CCB, 0x8CCC, 0x8CCD, 0x8CCE, 0x8CCF, 0x8CD0, 0x8CD1, 0x8CD2, 0x8CD3, 0x8CD4, 0x8CD5, 0x8CD6, 0x8CD7, 0x8CD8, 0x8CD9, 0x8CDA, 0x8CDB, 0x8CDC, 0x8CDD, 0x8CDE, 0x8CDF, 0x8CE0, 0x8CE1, 0x8CE2, 0x8CE3, 0x8CE4, 0x8CE5, 0x8CE6, 0x8CE7, 0x8CE8, 0x8CE9, 0x8CEA, 0x8CEB, 0x8CEC, 0x8CED, 0x8CEE, 0x8CEF, 0x8CF0, 0x8CF1, 0x8CF2, 0x8CF3, 0x8CF4, 0x8CF5, 0x8CF6, 0x8CF7, 0x8CF8, 0x8CF9, 0x8CFA, 0x8CFB, 0x8CFC, 0x8CFD, 0x8CFE, 0x8CFF, 0x8D00, 0x8D01, 0x8D02, 0x8D03, 0x8D04, 0x8D05, 0x8D06, 0x8D07, 0x8D08, 0x8D09, 0x8D0A, 0x8D0B, 0x8D0C, + 0x8D0D, 0x4F5F, 0x4F57, 0x4F32, 0x4F3D, 0x4F76, 0x4F74, 0x4F91, 0x4F89, 0x4F83, 0x4F8F, 0x4F7E, 0x4F7B, 0x4FAA, 0x4F7C, 0x4FAC, 0x4F94, 0x4FE6, 0x4FE8, 0x4FEA, 0x4FC5, 0x4FDA, 0x4FE3, 0x4FDC, 0x4FD1, 0x4FDF, 0x4FF8, 0x5029, 0x504C, 0x4FF3, 0x502C, 0x500F, 0x502E, 0x502D, 0x4FFE, 0x501C, 0x500C, 0x5025, 0x5028, 0x507E, 0x5043, 0x5055, 0x5048, 0x504E, 0x506C, 0x507B, 0x50A5, 0x50A7, 0x50A9, 0x50BA, 0x50D6, 0x5106, 0x50ED, 0x50EC, 0x50E6, 0x50EE, 0x5107, 0x510B, 0x4EDD, 0x6C3D, 0x4F58, 0x4F65, 0x4FCE, 0x9FA0, 0x6C46, 0x7C74, 0x516E, 0x5DFD, 0x9EC9, 0x9998, 0x5181, 0x5914, 0x52F9, 0x530D, 0x8A07, 0x5310, 0x51EB, 0x5919, 0x5155, 0x4EA0, 0x5156, 0x4EB3, 0x886E, 0x88A4, 0x4EB5, 0x8114, 0x88D2, 0x7980, 0x5B34, 0x8803, 0x7FB8, 0x51AB, 0x51B1, 0x51BD, 0x51BC, + 0x8D0E, 0x8D0F, 0x8D10, 0x8D11, 0x8D12, 0x8D13, 0x8D14, 0x8D15, 0x8D16, 0x8D17, 0x8D18, 0x8D19, 0x8D1A, 0x8D1B, 0x8D1C, 0x8D20, 0x8D51, 0x8D52, 0x8D57, 0x8D5F, 0x8D65, 0x8D68, 0x8D69, 0x8D6A, 0x8D6C, 0x8D6E, 0x8D6F, 0x8D71, 0x8D72, 0x8D78, 0x8D79, 0x8D7A, 0x8D7B, 0x8D7C, 0x8D7D, 0x8D7E, 0x8D7F, 0x8D80, 0x8D82, 0x8D83, 0x8D86, 0x8D87, 0x8D88, 0x8D89, 0x8D8C, 0x8D8D, 0x8D8E, 0x8D8F, 0x8D90, 0x8D92, 0x8D93, 0x8D95, 0x8D96, 0x8D97, 0x8D98, 0x8D99, 0x8D9A, 0x8D9B, 0x8D9C, 0x8D9D, 0x8D9E, 0x8DA0, 0x8DA1, 0x8DA2, 0x8DA4, 0x8DA5, 0x8DA6, 0x8DA7, 0x8DA8, 0x8DA9, 0x8DAA, 0x8DAB, 0x8DAC, 0x8DAD, 0x8DAE, 0x8DAF, 0x8DB0, 0x8DB2, 0x8DB6, 0x8DB7, 0x8DB9, 0x8DBB, 0x8DBD, 0x8DC0, 0x8DC1, 0x8DC2, 0x8DC5, 0x8DC7, 0x8DC8, 0x8DC9, 0x8DCA, 0x8DCD, 0x8DD0, 0x8DD2, 0x8DD3, + 0x8DD4, 0x51C7, 0x5196, 0x51A2, 0x51A5, 0x8BA0, 0x8BA6, 0x8BA7, 0x8BAA, 0x8BB4, 0x8BB5, 0x8BB7, 0x8BC2, 0x8BC3, 0x8BCB, 0x8BCF, 0x8BCE, 0x8BD2, 0x8BD3, 0x8BD4, 0x8BD6, 0x8BD8, 0x8BD9, 0x8BDC, 0x8BDF, 0x8BE0, 0x8BE4, 0x8BE8, 0x8BE9, 0x8BEE, 0x8BF0, 0x8BF3, 0x8BF6, 0x8BF9, 0x8BFC, 0x8BFF, 0x8C00, 0x8C02, 0x8C04, 0x8C07, 0x8C0C, 0x8C0F, 0x8C11, 0x8C12, 0x8C14, 0x8C15, 0x8C16, 0x8C19, 0x8C1B, 0x8C18, 0x8C1D, 0x8C1F, 0x8C20, 0x8C21, 0x8C25, 0x8C27, 0x8C2A, 0x8C2B, 0x8C2E, 0x8C2F, 0x8C32, 0x8C33, 0x8C35, 0x8C36, 0x5369, 0x537A, 0x961D, 0x9622, 0x9621, 0x9631, 0x962A, 0x963D, 0x963C, 0x9642, 0x9649, 0x9654, 0x965F, 0x9667, 0x966C, 0x9672, 0x9674, 0x9688, 0x968D, 0x9697, 0x96B0, 0x9097, 0x909B, 0x909D, 0x9099, 0x90AC, 0x90A1, 0x90B4, 0x90B3, 0x90B6, 0x90BA, + 0x8DD5, 0x8DD8, 0x8DD9, 0x8DDC, 0x8DE0, 0x8DE1, 0x8DE2, 0x8DE5, 0x8DE6, 0x8DE7, 0x8DE9, 0x8DED, 0x8DEE, 0x8DF0, 0x8DF1, 0x8DF2, 0x8DF4, 0x8DF6, 0x8DFC, 0x8DFE, 0x8DFF, 0x8E00, 0x8E01, 0x8E02, 0x8E03, 0x8E04, 0x8E06, 0x8E07, 0x8E08, 0x8E0B, 0x8E0D, 0x8E0E, 0x8E10, 0x8E11, 0x8E12, 0x8E13, 0x8E15, 0x8E16, 0x8E17, 0x8E18, 0x8E19, 0x8E1A, 0x8E1B, 0x8E1C, 0x8E20, 0x8E21, 0x8E24, 0x8E25, 0x8E26, 0x8E27, 0x8E28, 0x8E2B, 0x8E2D, 0x8E30, 0x8E32, 0x8E33, 0x8E34, 0x8E36, 0x8E37, 0x8E38, 0x8E3B, 0x8E3C, 0x8E3E, 0x8E3F, 0x8E43, 0x8E45, 0x8E46, 0x8E4C, 0x8E4D, 0x8E4E, 0x8E4F, 0x8E50, 0x8E53, 0x8E54, 0x8E55, 0x8E56, 0x8E57, 0x8E58, 0x8E5A, 0x8E5B, 0x8E5C, 0x8E5D, 0x8E5E, 0x8E5F, 0x8E60, 0x8E61, 0x8E62, 0x8E63, 0x8E64, 0x8E65, 0x8E67, 0x8E68, 0x8E6A, 0x8E6B, 0x8E6E, + 0x8E71, 0x90B8, 0x90B0, 0x90CF, 0x90C5, 0x90BE, 0x90D0, 0x90C4, 0x90C7, 0x90D3, 0x90E6, 0x90E2, 0x90DC, 0x90D7, 0x90DB, 0x90EB, 0x90EF, 0x90FE, 0x9104, 0x9122, 0x911E, 0x9123, 0x9131, 0x912F, 0x9139, 0x9143, 0x9146, 0x520D, 0x5942, 0x52A2, 0x52AC, 0x52AD, 0x52BE, 0x54FF, 0x52D0, 0x52D6, 0x52F0, 0x53DF, 0x71EE, 0x77CD, 0x5EF4, 0x51F5, 0x51FC, 0x9B2F, 0x53B6, 0x5F01, 0x755A, 0x5DEF, 0x574C, 0x57A9, 0x57A1, 0x587E, 0x58BC, 0x58C5, 0x58D1, 0x5729, 0x572C, 0x572A, 0x5733, 0x5739, 0x572E, 0x572F, 0x575C, 0x573B, 0x5742, 0x5769, 0x5785, 0x576B, 0x5786, 0x577C, 0x577B, 0x5768, 0x576D, 0x5776, 0x5773, 0x57AD, 0x57A4, 0x578C, 0x57B2, 0x57CF, 0x57A7, 0x57B4, 0x5793, 0x57A0, 0x57D5, 0x57D8, 0x57DA, 0x57D9, 0x57D2, 0x57B8, 0x57F4, 0x57EF, 0x57F8, 0x57E4, 0x57DD, + 0x8E73, 0x8E75, 0x8E77, 0x8E78, 0x8E79, 0x8E7A, 0x8E7B, 0x8E7D, 0x8E7E, 0x8E80, 0x8E82, 0x8E83, 0x8E84, 0x8E86, 0x8E88, 0x8E89, 0x8E8A, 0x8E8B, 0x8E8C, 0x8E8D, 0x8E8E, 0x8E91, 0x8E92, 0x8E93, 0x8E95, 0x8E96, 0x8E97, 0x8E98, 0x8E99, 0x8E9A, 0x8E9B, 0x8E9D, 0x8E9F, 0x8EA0, 0x8EA1, 0x8EA2, 0x8EA3, 0x8EA4, 0x8EA5, 0x8EA6, 0x8EA7, 0x8EA8, 0x8EA9, 0x8EAA, 0x8EAD, 0x8EAE, 0x8EB0, 0x8EB1, 0x8EB3, 0x8EB4, 0x8EB5, 0x8EB6, 0x8EB7, 0x8EB8, 0x8EB9, 0x8EBB, 0x8EBC, 0x8EBD, 0x8EBE, 0x8EBF, 0x8EC0, 0x8EC1, 0x8EC2, 0x8EC3, 0x8EC4, 0x8EC5, 0x8EC6, 0x8EC7, 0x8EC8, 0x8EC9, 0x8ECA, 0x8ECB, 0x8ECC, 0x8ECD, 0x8ECF, 0x8ED0, 0x8ED1, 0x8ED2, 0x8ED3, 0x8ED4, 0x8ED5, 0x8ED6, 0x8ED7, 0x8ED8, 0x8ED9, 0x8EDA, 0x8EDB, 0x8EDC, 0x8EDD, 0x8EDE, 0x8EDF, 0x8EE0, 0x8EE1, 0x8EE2, 0x8EE3, + 0x8EE4, 0x580B, 0x580D, 0x57FD, 0x57ED, 0x5800, 0x581E, 0x5819, 0x5844, 0x5820, 0x5865, 0x586C, 0x5881, 0x5889, 0x589A, 0x5880, 0x99A8, 0x9F19, 0x61FF, 0x8279, 0x827D, 0x827F, 0x828F, 0x828A, 0x82A8, 0x8284, 0x828E, 0x8291, 0x8297, 0x8299, 0x82AB, 0x82B8, 0x82BE, 0x82B0, 0x82C8, 0x82CA, 0x82E3, 0x8298, 0x82B7, 0x82AE, 0x82CB, 0x82CC, 0x82C1, 0x82A9, 0x82B4, 0x82A1, 0x82AA, 0x829F, 0x82C4, 0x82CE, 0x82A4, 0x82E1, 0x8309, 0x82F7, 0x82E4, 0x830F, 0x8307, 0x82DC, 0x82F4, 0x82D2, 0x82D8, 0x830C, 0x82FB, 0x82D3, 0x8311, 0x831A, 0x8306, 0x8314, 0x8315, 0x82E0, 0x82D5, 0x831C, 0x8351, 0x835B, 0x835C, 0x8308, 0x8392, 0x833C, 0x8334, 0x8331, 0x839B, 0x835E, 0x832F, 0x834F, 0x8347, 0x8343, 0x835F, 0x8340, 0x8317, 0x8360, 0x832D, 0x833A, 0x8333, 0x8366, 0x8365, + 0x8EE5, 0x8EE6, 0x8EE7, 0x8EE8, 0x8EE9, 0x8EEA, 0x8EEB, 0x8EEC, 0x8EED, 0x8EEE, 0x8EEF, 0x8EF0, 0x8EF1, 0x8EF2, 0x8EF3, 0x8EF4, 0x8EF5, 0x8EF6, 0x8EF7, 0x8EF8, 0x8EF9, 0x8EFA, 0x8EFB, 0x8EFC, 0x8EFD, 0x8EFE, 0x8EFF, 0x8F00, 0x8F01, 0x8F02, 0x8F03, 0x8F04, 0x8F05, 0x8F06, 0x8F07, 0x8F08, 0x8F09, 0x8F0A, 0x8F0B, 0x8F0C, 0x8F0D, 0x8F0E, 0x8F0F, 0x8F10, 0x8F11, 0x8F12, 0x8F13, 0x8F14, 0x8F15, 0x8F16, 0x8F17, 0x8F18, 0x8F19, 0x8F1A, 0x8F1B, 0x8F1C, 0x8F1D, 0x8F1E, 0x8F1F, 0x8F20, 0x8F21, 0x8F22, 0x8F23, 0x8F24, 0x8F25, 0x8F26, 0x8F27, 0x8F28, 0x8F29, 0x8F2A, 0x8F2B, 0x8F2C, 0x8F2D, 0x8F2E, 0x8F2F, 0x8F30, 0x8F31, 0x8F32, 0x8F33, 0x8F34, 0x8F35, 0x8F36, 0x8F37, 0x8F38, 0x8F39, 0x8F3A, 0x8F3B, 0x8F3C, 0x8F3D, 0x8F3E, 0x8F3F, 0x8F40, 0x8F41, 0x8F42, 0x8F43, + 0x8F44, 0x8368, 0x831B, 0x8369, 0x836C, 0x836A, 0x836D, 0x836E, 0x83B0, 0x8378, 0x83B3, 0x83B4, 0x83A0, 0x83AA, 0x8393, 0x839C, 0x8385, 0x837C, 0x83B6, 0x83A9, 0x837D, 0x83B8, 0x837B, 0x8398, 0x839E, 0x83A8, 0x83BA, 0x83BC, 0x83C1, 0x8401, 0x83E5, 0x83D8, 0x5807, 0x8418, 0x840B, 0x83DD, 0x83FD, 0x83D6, 0x841C, 0x8438, 0x8411, 0x8406, 0x83D4, 0x83DF, 0x840F, 0x8403, 0x83F8, 0x83F9, 0x83EA, 0x83C5, 0x83C0, 0x8426, 0x83F0, 0x83E1, 0x845C, 0x8451, 0x845A, 0x8459, 0x8473, 0x8487, 0x8488, 0x847A, 0x8489, 0x8478, 0x843C, 0x8446, 0x8469, 0x8476, 0x848C, 0x848E, 0x8431, 0x846D, 0x84C1, 0x84CD, 0x84D0, 0x84E6, 0x84BD, 0x84D3, 0x84CA, 0x84BF, 0x84BA, 0x84E0, 0x84A1, 0x84B9, 0x84B4, 0x8497, 0x84E5, 0x84E3, 0x850C, 0x750D, 0x8538, 0x84F0, 0x8539, 0x851F, 0x853A, + 0x8F45, 0x8F46, 0x8F47, 0x8F48, 0x8F49, 0x8F4A, 0x8F4B, 0x8F4C, 0x8F4D, 0x8F4E, 0x8F4F, 0x8F50, 0x8F51, 0x8F52, 0x8F53, 0x8F54, 0x8F55, 0x8F56, 0x8F57, 0x8F58, 0x8F59, 0x8F5A, 0x8F5B, 0x8F5C, 0x8F5D, 0x8F5E, 0x8F5F, 0x8F60, 0x8F61, 0x8F62, 0x8F63, 0x8F64, 0x8F65, 0x8F6A, 0x8F80, 0x8F8C, 0x8F92, 0x8F9D, 0x8FA0, 0x8FA1, 0x8FA2, 0x8FA4, 0x8FA5, 0x8FA6, 0x8FA7, 0x8FAA, 0x8FAC, 0x8FAD, 0x8FAE, 0x8FAF, 0x8FB2, 0x8FB3, 0x8FB4, 0x8FB5, 0x8FB7, 0x8FB8, 0x8FBA, 0x8FBB, 0x8FBC, 0x8FBF, 0x8FC0, 0x8FC3, 0x8FC6, 0x8FC9, 0x8FCA, 0x8FCB, 0x8FCC, 0x8FCD, 0x8FCF, 0x8FD2, 0x8FD6, 0x8FD7, 0x8FDA, 0x8FE0, 0x8FE1, 0x8FE3, 0x8FE7, 0x8FEC, 0x8FEF, 0x8FF1, 0x8FF2, 0x8FF4, 0x8FF5, 0x8FF6, 0x8FFA, 0x8FFB, 0x8FFC, 0x8FFE, 0x8FFF, 0x9007, 0x9008, 0x900C, 0x900E, 0x9013, 0x9015, + 0x9018, 0x8556, 0x853B, 0x84FF, 0x84FC, 0x8559, 0x8548, 0x8568, 0x8564, 0x855E, 0x857A, 0x77A2, 0x8543, 0x8572, 0x857B, 0x85A4, 0x85A8, 0x8587, 0x858F, 0x8579, 0x85AE, 0x859C, 0x8585, 0x85B9, 0x85B7, 0x85B0, 0x85D3, 0x85C1, 0x85DC, 0x85FF, 0x8627, 0x8605, 0x8629, 0x8616, 0x863C, 0x5EFE, 0x5F08, 0x593C, 0x5941, 0x8037, 0x5955, 0x595A, 0x5958, 0x530F, 0x5C22, 0x5C25, 0x5C2C, 0x5C34, 0x624C, 0x626A, 0x629F, 0x62BB, 0x62CA, 0x62DA, 0x62D7, 0x62EE, 0x6322, 0x62F6, 0x6339, 0x634B, 0x6343, 0x63AD, 0x63F6, 0x6371, 0x637A, 0x638E, 0x63B4, 0x636D, 0x63AC, 0x638A, 0x6369, 0x63AE, 0x63BC, 0x63F2, 0x63F8, 0x63E0, 0x63FF, 0x63C4, 0x63DE, 0x63CE, 0x6452, 0x63C6, 0x63BE, 0x6445, 0x6441, 0x640B, 0x641B, 0x6420, 0x640C, 0x6426, 0x6421, 0x645E, 0x6484, 0x646D, 0x6496, + 0x9019, 0x901C, 0x9023, 0x9024, 0x9025, 0x9027, 0x9028, 0x9029, 0x902A, 0x902B, 0x902C, 0x9030, 0x9031, 0x9032, 0x9033, 0x9034, 0x9037, 0x9039, 0x903A, 0x903D, 0x903F, 0x9040, 0x9043, 0x9045, 0x9046, 0x9048, 0x9049, 0x904A, 0x904B, 0x904C, 0x904E, 0x9054, 0x9055, 0x9056, 0x9059, 0x905A, 0x905C, 0x905D, 0x905E, 0x905F, 0x9060, 0x9061, 0x9064, 0x9066, 0x9067, 0x9069, 0x906A, 0x906B, 0x906C, 0x906F, 0x9070, 0x9071, 0x9072, 0x9073, 0x9076, 0x9077, 0x9078, 0x9079, 0x907A, 0x907B, 0x907C, 0x907E, 0x9081, 0x9084, 0x9085, 0x9086, 0x9087, 0x9089, 0x908A, 0x908C, 0x908D, 0x908E, 0x908F, 0x9090, 0x9092, 0x9094, 0x9096, 0x9098, 0x909A, 0x909C, 0x909E, 0x909F, 0x90A0, 0x90A4, 0x90A5, 0x90A7, 0x90A8, 0x90A9, 0x90AB, 0x90AD, 0x90B2, 0x90B7, 0x90BC, 0x90BD, 0x90BF, + 0x90C0, 0x647A, 0x64B7, 0x64B8, 0x6499, 0x64BA, 0x64C0, 0x64D0, 0x64D7, 0x64E4, 0x64E2, 0x6509, 0x6525, 0x652E, 0x5F0B, 0x5FD2, 0x7519, 0x5F11, 0x535F, 0x53F1, 0x53FD, 0x53E9, 0x53E8, 0x53FB, 0x5412, 0x5416, 0x5406, 0x544B, 0x5452, 0x5453, 0x5454, 0x5456, 0x5443, 0x5421, 0x5457, 0x5459, 0x5423, 0x5432, 0x5482, 0x5494, 0x5477, 0x5471, 0x5464, 0x549A, 0x549B, 0x5484, 0x5476, 0x5466, 0x549D, 0x54D0, 0x54AD, 0x54C2, 0x54B4, 0x54D2, 0x54A7, 0x54A6, 0x54D3, 0x54D4, 0x5472, 0x54A3, 0x54D5, 0x54BB, 0x54BF, 0x54CC, 0x54D9, 0x54DA, 0x54DC, 0x54A9, 0x54AA, 0x54A4, 0x54DD, 0x54CF, 0x54DE, 0x551B, 0x54E7, 0x5520, 0x54FD, 0x5514, 0x54F3, 0x5522, 0x5523, 0x550F, 0x5511, 0x5527, 0x552A, 0x5567, 0x558F, 0x55B5, 0x5549, 0x556D, 0x5541, 0x5555, 0x553F, 0x5550, 0x553C, + 0x90C2, 0x90C3, 0x90C6, 0x90C8, 0x90C9, 0x90CB, 0x90CC, 0x90CD, 0x90D2, 0x90D4, 0x90D5, 0x90D6, 0x90D8, 0x90D9, 0x90DA, 0x90DE, 0x90DF, 0x90E0, 0x90E3, 0x90E4, 0x90E5, 0x90E9, 0x90EA, 0x90EC, 0x90EE, 0x90F0, 0x90F1, 0x90F2, 0x90F3, 0x90F5, 0x90F6, 0x90F7, 0x90F9, 0x90FA, 0x90FB, 0x90FC, 0x90FF, 0x9100, 0x9101, 0x9103, 0x9105, 0x9106, 0x9107, 0x9108, 0x9109, 0x910A, 0x910B, 0x910C, 0x910D, 0x910E, 0x910F, 0x9110, 0x9111, 0x9112, 0x9113, 0x9114, 0x9115, 0x9116, 0x9117, 0x9118, 0x911A, 0x911B, 0x911C, 0x911D, 0x911F, 0x9120, 0x9121, 0x9124, 0x9125, 0x9126, 0x9127, 0x9128, 0x9129, 0x912A, 0x912B, 0x912C, 0x912D, 0x912E, 0x9130, 0x9132, 0x9133, 0x9134, 0x9135, 0x9136, 0x9137, 0x9138, 0x913A, 0x913B, 0x913C, 0x913D, 0x913E, 0x913F, 0x9140, 0x9141, 0x9142, + 0x9144, 0x5537, 0x5556, 0x5575, 0x5576, 0x5577, 0x5533, 0x5530, 0x555C, 0x558B, 0x55D2, 0x5583, 0x55B1, 0x55B9, 0x5588, 0x5581, 0x559F, 0x557E, 0x55D6, 0x5591, 0x557B, 0x55DF, 0x55BD, 0x55BE, 0x5594, 0x5599, 0x55EA, 0x55F7, 0x55C9, 0x561F, 0x55D1, 0x55EB, 0x55EC, 0x55D4, 0x55E6, 0x55DD, 0x55C4, 0x55EF, 0x55E5, 0x55F2, 0x55F3, 0x55CC, 0x55CD, 0x55E8, 0x55F5, 0x55E4, 0x8F94, 0x561E, 0x5608, 0x560C, 0x5601, 0x5624, 0x5623, 0x55FE, 0x5600, 0x5627, 0x562D, 0x5658, 0x5639, 0x5657, 0x562C, 0x564D, 0x5662, 0x5659, 0x565C, 0x564C, 0x5654, 0x5686, 0x5664, 0x5671, 0x566B, 0x567B, 0x567C, 0x5685, 0x5693, 0x56AF, 0x56D4, 0x56D7, 0x56DD, 0x56E1, 0x56F5, 0x56EB, 0x56F9, 0x56FF, 0x5704, 0x570A, 0x5709, 0x571C, 0x5E0F, 0x5E19, 0x5E14, 0x5E11, 0x5E31, 0x5E3B, 0x5E3C, + 0x9145, 0x9147, 0x9148, 0x9151, 0x9153, 0x9154, 0x9155, 0x9156, 0x9158, 0x9159, 0x915B, 0x915C, 0x915F, 0x9160, 0x9166, 0x9167, 0x9168, 0x916B, 0x916D, 0x9173, 0x917A, 0x917B, 0x917C, 0x9180, 0x9181, 0x9182, 0x9183, 0x9184, 0x9186, 0x9188, 0x918A, 0x918E, 0x918F, 0x9193, 0x9194, 0x9195, 0x9196, 0x9197, 0x9198, 0x9199, 0x919C, 0x919D, 0x919E, 0x919F, 0x91A0, 0x91A1, 0x91A4, 0x91A5, 0x91A6, 0x91A7, 0x91A8, 0x91A9, 0x91AB, 0x91AC, 0x91B0, 0x91B1, 0x91B2, 0x91B3, 0x91B6, 0x91B7, 0x91B8, 0x91B9, 0x91BB, 0x91BC, 0x91BD, 0x91BE, 0x91BF, 0x91C0, 0x91C1, 0x91C2, 0x91C3, 0x91C4, 0x91C5, 0x91C6, 0x91C8, 0x91CB, 0x91D0, 0x91D2, 0x91D3, 0x91D4, 0x91D5, 0x91D6, 0x91D7, 0x91D8, 0x91D9, 0x91DA, 0x91DB, 0x91DD, 0x91DE, 0x91DF, 0x91E0, 0x91E1, 0x91E2, 0x91E3, 0x91E4, + 0x91E5, 0x5E37, 0x5E44, 0x5E54, 0x5E5B, 0x5E5E, 0x5E61, 0x5C8C, 0x5C7A, 0x5C8D, 0x5C90, 0x5C96, 0x5C88, 0x5C98, 0x5C99, 0x5C91, 0x5C9A, 0x5C9C, 0x5CB5, 0x5CA2, 0x5CBD, 0x5CAC, 0x5CAB, 0x5CB1, 0x5CA3, 0x5CC1, 0x5CB7, 0x5CC4, 0x5CD2, 0x5CE4, 0x5CCB, 0x5CE5, 0x5D02, 0x5D03, 0x5D27, 0x5D26, 0x5D2E, 0x5D24, 0x5D1E, 0x5D06, 0x5D1B, 0x5D58, 0x5D3E, 0x5D34, 0x5D3D, 0x5D6C, 0x5D5B, 0x5D6F, 0x5D5D, 0x5D6B, 0x5D4B, 0x5D4A, 0x5D69, 0x5D74, 0x5D82, 0x5D99, 0x5D9D, 0x8C73, 0x5DB7, 0x5DC5, 0x5F73, 0x5F77, 0x5F82, 0x5F87, 0x5F89, 0x5F8C, 0x5F95, 0x5F99, 0x5F9C, 0x5FA8, 0x5FAD, 0x5FB5, 0x5FBC, 0x8862, 0x5F61, 0x72AD, 0x72B0, 0x72B4, 0x72B7, 0x72B8, 0x72C3, 0x72C1, 0x72CE, 0x72CD, 0x72D2, 0x72E8, 0x72EF, 0x72E9, 0x72F2, 0x72F4, 0x72F7, 0x7301, 0x72F3, 0x7303, 0x72FA, + 0x91E6, 0x91E7, 0x91E8, 0x91E9, 0x91EA, 0x91EB, 0x91EC, 0x91ED, 0x91EE, 0x91EF, 0x91F0, 0x91F1, 0x91F2, 0x91F3, 0x91F4, 0x91F5, 0x91F6, 0x91F7, 0x91F8, 0x91F9, 0x91FA, 0x91FB, 0x91FC, 0x91FD, 0x91FE, 0x91FF, 0x9200, 0x9201, 0x9202, 0x9203, 0x9204, 0x9205, 0x9206, 0x9207, 0x9208, 0x9209, 0x920A, 0x920B, 0x920C, 0x920D, 0x920E, 0x920F, 0x9210, 0x9211, 0x9212, 0x9213, 0x9214, 0x9215, 0x9216, 0x9217, 0x9218, 0x9219, 0x921A, 0x921B, 0x921C, 0x921D, 0x921E, 0x921F, 0x9220, 0x9221, 0x9222, 0x9223, 0x9224, 0x9225, 0x9226, 0x9227, 0x9228, 0x9229, 0x922A, 0x922B, 0x922C, 0x922D, 0x922E, 0x922F, 0x9230, 0x9231, 0x9232, 0x9233, 0x9234, 0x9235, 0x9236, 0x9237, 0x9238, 0x9239, 0x923A, 0x923B, 0x923C, 0x923D, 0x923E, 0x923F, 0x9240, 0x9241, 0x9242, 0x9243, 0x9244, + 0x9245, 0x72FB, 0x7317, 0x7313, 0x7321, 0x730A, 0x731E, 0x731D, 0x7315, 0x7322, 0x7339, 0x7325, 0x732C, 0x7338, 0x7331, 0x7350, 0x734D, 0x7357, 0x7360, 0x736C, 0x736F, 0x737E, 0x821B, 0x5925, 0x98E7, 0x5924, 0x5902, 0x9963, 0x9967, 0x9968, 0x9969, 0x996A, 0x996B, 0x996C, 0x9974, 0x9977, 0x997D, 0x9980, 0x9984, 0x9987, 0x998A, 0x998D, 0x9990, 0x9991, 0x9993, 0x9994, 0x9995, 0x5E80, 0x5E91, 0x5E8B, 0x5E96, 0x5EA5, 0x5EA0, 0x5EB9, 0x5EB5, 0x5EBE, 0x5EB3, 0x8D53, 0x5ED2, 0x5ED1, 0x5EDB, 0x5EE8, 0x5EEA, 0x81BA, 0x5FC4, 0x5FC9, 0x5FD6, 0x5FCF, 0x6003, 0x5FEE, 0x6004, 0x5FE1, 0x5FE4, 0x5FFE, 0x6005, 0x6006, 0x5FEA, 0x5FED, 0x5FF8, 0x6019, 0x6035, 0x6026, 0x601B, 0x600F, 0x600D, 0x6029, 0x602B, 0x600A, 0x603F, 0x6021, 0x6078, 0x6079, 0x607B, 0x607A, 0x6042, + 0x9246, 0x9247, 0x9248, 0x9249, 0x924A, 0x924B, 0x924C, 0x924D, 0x924E, 0x924F, 0x9250, 0x9251, 0x9252, 0x9253, 0x9254, 0x9255, 0x9256, 0x9257, 0x9258, 0x9259, 0x925A, 0x925B, 0x925C, 0x925D, 0x925E, 0x925F, 0x9260, 0x9261, 0x9262, 0x9263, 0x9264, 0x9265, 0x9266, 0x9267, 0x9268, 0x9269, 0x926A, 0x926B, 0x926C, 0x926D, 0x926E, 0x926F, 0x9270, 0x9271, 0x9272, 0x9273, 0x9275, 0x9276, 0x9277, 0x9278, 0x9279, 0x927A, 0x927B, 0x927C, 0x927D, 0x927E, 0x927F, 0x9280, 0x9281, 0x9282, 0x9283, 0x9284, 0x9285, 0x9286, 0x9287, 0x9288, 0x9289, 0x928A, 0x928B, 0x928C, 0x928D, 0x928F, 0x9290, 0x9291, 0x9292, 0x9293, 0x9294, 0x9295, 0x9296, 0x9297, 0x9298, 0x9299, 0x929A, 0x929B, 0x929C, 0x929D, 0x929E, 0x929F, 0x92A0, 0x92A1, 0x92A2, 0x92A3, 0x92A4, 0x92A5, 0x92A6, + 0x92A7, 0x606A, 0x607D, 0x6096, 0x609A, 0x60AD, 0x609D, 0x6083, 0x6092, 0x608C, 0x609B, 0x60EC, 0x60BB, 0x60B1, 0x60DD, 0x60D8, 0x60C6, 0x60DA, 0x60B4, 0x6120, 0x6126, 0x6115, 0x6123, 0x60F4, 0x6100, 0x610E, 0x612B, 0x614A, 0x6175, 0x61AC, 0x6194, 0x61A7, 0x61B7, 0x61D4, 0x61F5, 0x5FDD, 0x96B3, 0x95E9, 0x95EB, 0x95F1, 0x95F3, 0x95F5, 0x95F6, 0x95FC, 0x95FE, 0x9603, 0x9604, 0x9606, 0x9608, 0x960A, 0x960B, 0x960C, 0x960D, 0x960F, 0x9612, 0x9615, 0x9616, 0x9617, 0x9619, 0x961A, 0x4E2C, 0x723F, 0x6215, 0x6C35, 0x6C54, 0x6C5C, 0x6C4A, 0x6CA3, 0x6C85, 0x6C90, 0x6C94, 0x6C8C, 0x6C68, 0x6C69, 0x6C74, 0x6C76, 0x6C86, 0x6CA9, 0x6CD0, 0x6CD4, 0x6CAD, 0x6CF7, 0x6CF8, 0x6CF1, 0x6CD7, 0x6CB2, 0x6CE0, 0x6CD6, 0x6CFA, 0x6CEB, 0x6CEE, 0x6CB1, 0x6CD3, 0x6CEF, 0x6CFE, + 0x92A8, 0x92A9, 0x92AA, 0x92AB, 0x92AC, 0x92AD, 0x92AF, 0x92B0, 0x92B1, 0x92B2, 0x92B3, 0x92B4, 0x92B5, 0x92B6, 0x92B7, 0x92B8, 0x92B9, 0x92BA, 0x92BB, 0x92BC, 0x92BD, 0x92BE, 0x92BF, 0x92C0, 0x92C1, 0x92C2, 0x92C3, 0x92C4, 0x92C5, 0x92C6, 0x92C7, 0x92C9, 0x92CA, 0x92CB, 0x92CC, 0x92CD, 0x92CE, 0x92CF, 0x92D0, 0x92D1, 0x92D2, 0x92D3, 0x92D4, 0x92D5, 0x92D6, 0x92D7, 0x92D8, 0x92D9, 0x92DA, 0x92DB, 0x92DC, 0x92DD, 0x92DE, 0x92DF, 0x92E0, 0x92E1, 0x92E2, 0x92E3, 0x92E4, 0x92E5, 0x92E6, 0x92E7, 0x92E8, 0x92E9, 0x92EA, 0x92EB, 0x92EC, 0x92ED, 0x92EE, 0x92EF, 0x92F0, 0x92F1, 0x92F2, 0x92F3, 0x92F4, 0x92F5, 0x92F6, 0x92F7, 0x92F8, 0x92F9, 0x92FA, 0x92FB, 0x92FC, 0x92FD, 0x92FE, 0x92FF, 0x9300, 0x9301, 0x9302, 0x9303, 0x9304, 0x9305, 0x9306, 0x9307, 0x9308, + 0x9309, 0x6D39, 0x6D27, 0x6D0C, 0x6D43, 0x6D48, 0x6D07, 0x6D04, 0x6D19, 0x6D0E, 0x6D2B, 0x6D4D, 0x6D2E, 0x6D35, 0x6D1A, 0x6D4F, 0x6D52, 0x6D54, 0x6D33, 0x6D91, 0x6D6F, 0x6D9E, 0x6DA0, 0x6D5E, 0x6D93, 0x6D94, 0x6D5C, 0x6D60, 0x6D7C, 0x6D63, 0x6E1A, 0x6DC7, 0x6DC5, 0x6DDE, 0x6E0E, 0x6DBF, 0x6DE0, 0x6E11, 0x6DE6, 0x6DDD, 0x6DD9, 0x6E16, 0x6DAB, 0x6E0C, 0x6DAE, 0x6E2B, 0x6E6E, 0x6E4E, 0x6E6B, 0x6EB2, 0x6E5F, 0x6E86, 0x6E53, 0x6E54, 0x6E32, 0x6E25, 0x6E44, 0x6EDF, 0x6EB1, 0x6E98, 0x6EE0, 0x6F2D, 0x6EE2, 0x6EA5, 0x6EA7, 0x6EBD, 0x6EBB, 0x6EB7, 0x6ED7, 0x6EB4, 0x6ECF, 0x6E8F, 0x6EC2, 0x6E9F, 0x6F62, 0x6F46, 0x6F47, 0x6F24, 0x6F15, 0x6EF9, 0x6F2F, 0x6F36, 0x6F4B, 0x6F74, 0x6F2A, 0x6F09, 0x6F29, 0x6F89, 0x6F8D, 0x6F8C, 0x6F78, 0x6F72, 0x6F7C, 0x6F7A, 0x6FD1, + 0x930A, 0x930B, 0x930C, 0x930D, 0x930E, 0x930F, 0x9310, 0x9311, 0x9312, 0x9313, 0x9314, 0x9315, 0x9316, 0x9317, 0x9318, 0x9319, 0x931A, 0x931B, 0x931C, 0x931D, 0x931E, 0x931F, 0x9320, 0x9321, 0x9322, 0x9323, 0x9324, 0x9325, 0x9326, 0x9327, 0x9328, 0x9329, 0x932A, 0x932B, 0x932C, 0x932D, 0x932E, 0x932F, 0x9330, 0x9331, 0x9332, 0x9333, 0x9334, 0x9335, 0x9336, 0x9337, 0x9338, 0x9339, 0x933A, 0x933B, 0x933C, 0x933D, 0x933F, 0x9340, 0x9341, 0x9342, 0x9343, 0x9344, 0x9345, 0x9346, 0x9347, 0x9348, 0x9349, 0x934A, 0x934B, 0x934C, 0x934D, 0x934E, 0x934F, 0x9350, 0x9351, 0x9352, 0x9353, 0x9354, 0x9355, 0x9356, 0x9357, 0x9358, 0x9359, 0x935A, 0x935B, 0x935C, 0x935D, 0x935E, 0x935F, 0x9360, 0x9361, 0x9362, 0x9363, 0x9364, 0x9365, 0x9366, 0x9367, 0x9368, 0x9369, + 0x936B, 0x6FC9, 0x6FA7, 0x6FB9, 0x6FB6, 0x6FC2, 0x6FE1, 0x6FEE, 0x6FDE, 0x6FE0, 0x6FEF, 0x701A, 0x7023, 0x701B, 0x7039, 0x7035, 0x704F, 0x705E, 0x5B80, 0x5B84, 0x5B95, 0x5B93, 0x5BA5, 0x5BB8, 0x752F, 0x9A9E, 0x6434, 0x5BE4, 0x5BEE, 0x8930, 0x5BF0, 0x8E47, 0x8B07, 0x8FB6, 0x8FD3, 0x8FD5, 0x8FE5, 0x8FEE, 0x8FE4, 0x8FE9, 0x8FE6, 0x8FF3, 0x8FE8, 0x9005, 0x9004, 0x900B, 0x9026, 0x9011, 0x900D, 0x9016, 0x9021, 0x9035, 0x9036, 0x902D, 0x902F, 0x9044, 0x9051, 0x9052, 0x9050, 0x9068, 0x9058, 0x9062, 0x905B, 0x66B9, 0x9074, 0x907D, 0x9082, 0x9088, 0x9083, 0x908B, 0x5F50, 0x5F57, 0x5F56, 0x5F58, 0x5C3B, 0x54AB, 0x5C50, 0x5C59, 0x5B71, 0x5C63, 0x5C66, 0x7FBC, 0x5F2A, 0x5F29, 0x5F2D, 0x8274, 0x5F3C, 0x9B3B, 0x5C6E, 0x5981, 0x5983, 0x598D, 0x59A9, 0x59AA, 0x59A3, + 0x936C, 0x936D, 0x936E, 0x936F, 0x9370, 0x9371, 0x9372, 0x9373, 0x9374, 0x9375, 0x9376, 0x9377, 0x9378, 0x9379, 0x937A, 0x937B, 0x937C, 0x937D, 0x937E, 0x937F, 0x9380, 0x9381, 0x9382, 0x9383, 0x9384, 0x9385, 0x9386, 0x9387, 0x9388, 0x9389, 0x938A, 0x938B, 0x938C, 0x938D, 0x938E, 0x9390, 0x9391, 0x9392, 0x9393, 0x9394, 0x9395, 0x9396, 0x9397, 0x9398, 0x9399, 0x939A, 0x939B, 0x939C, 0x939D, 0x939E, 0x939F, 0x93A0, 0x93A1, 0x93A2, 0x93A3, 0x93A4, 0x93A5, 0x93A6, 0x93A7, 0x93A8, 0x93A9, 0x93AA, 0x93AB, 0x93AC, 0x93AD, 0x93AE, 0x93AF, 0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7, 0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD, 0x93BE, 0x93BF, 0x93C0, 0x93C1, 0x93C2, 0x93C3, 0x93C4, 0x93C5, 0x93C6, 0x93C7, 0x93C8, 0x93C9, 0x93CB, 0x93CC, + 0x93CD, 0x5997, 0x59CA, 0x59AB, 0x599E, 0x59A4, 0x59D2, 0x59B2, 0x59AF, 0x59D7, 0x59BE, 0x5A05, 0x5A06, 0x59DD, 0x5A08, 0x59E3, 0x59D8, 0x59F9, 0x5A0C, 0x5A09, 0x5A32, 0x5A34, 0x5A11, 0x5A23, 0x5A13, 0x5A40, 0x5A67, 0x5A4A, 0x5A55, 0x5A3C, 0x5A62, 0x5A75, 0x80EC, 0x5AAA, 0x5A9B, 0x5A77, 0x5A7A, 0x5ABE, 0x5AEB, 0x5AB2, 0x5AD2, 0x5AD4, 0x5AB8, 0x5AE0, 0x5AE3, 0x5AF1, 0x5AD6, 0x5AE6, 0x5AD8, 0x5ADC, 0x5B09, 0x5B17, 0x5B16, 0x5B32, 0x5B37, 0x5B40, 0x5C15, 0x5C1C, 0x5B5A, 0x5B65, 0x5B73, 0x5B51, 0x5B53, 0x5B62, 0x9A75, 0x9A77, 0x9A78, 0x9A7A, 0x9A7F, 0x9A7D, 0x9A80, 0x9A81, 0x9A85, 0x9A88, 0x9A8A, 0x9A90, 0x9A92, 0x9A93, 0x9A96, 0x9A98, 0x9A9B, 0x9A9C, 0x9A9D, 0x9A9F, 0x9AA0, 0x9AA2, 0x9AA3, 0x9AA5, 0x9AA7, 0x7E9F, 0x7EA1, 0x7EA3, 0x7EA5, 0x7EA8, 0x7EA9, + 0x93CE, 0x93CF, 0x93D0, 0x93D1, 0x93D2, 0x93D3, 0x93D4, 0x93D5, 0x93D7, 0x93D8, 0x93D9, 0x93DA, 0x93DB, 0x93DC, 0x93DD, 0x93DE, 0x93DF, 0x93E0, 0x93E1, 0x93E2, 0x93E3, 0x93E4, 0x93E5, 0x93E6, 0x93E7, 0x93E8, 0x93E9, 0x93EA, 0x93EB, 0x93EC, 0x93ED, 0x93EE, 0x93EF, 0x93F0, 0x93F1, 0x93F2, 0x93F3, 0x93F4, 0x93F5, 0x93F6, 0x93F7, 0x93F8, 0x93F9, 0x93FA, 0x93FB, 0x93FC, 0x93FD, 0x93FE, 0x93FF, 0x9400, 0x9401, 0x9402, 0x9403, 0x9404, 0x9405, 0x9406, 0x9407, 0x9408, 0x9409, 0x940A, 0x940B, 0x940C, 0x940D, 0x940E, 0x940F, 0x9410, 0x9411, 0x9412, 0x9413, 0x9414, 0x9415, 0x9416, 0x9417, 0x9418, 0x9419, 0x941A, 0x941B, 0x941C, 0x941D, 0x941E, 0x941F, 0x9420, 0x9421, 0x9422, 0x9423, 0x9424, 0x9425, 0x9426, 0x9427, 0x9428, 0x9429, 0x942A, 0x942B, 0x942C, 0x942D, + 0x942E, 0x7EAD, 0x7EB0, 0x7EBE, 0x7EC0, 0x7EC1, 0x7EC2, 0x7EC9, 0x7ECB, 0x7ECC, 0x7ED0, 0x7ED4, 0x7ED7, 0x7EDB, 0x7EE0, 0x7EE1, 0x7EE8, 0x7EEB, 0x7EEE, 0x7EEF, 0x7EF1, 0x7EF2, 0x7F0D, 0x7EF6, 0x7EFA, 0x7EFB, 0x7EFE, 0x7F01, 0x7F02, 0x7F03, 0x7F07, 0x7F08, 0x7F0B, 0x7F0C, 0x7F0F, 0x7F11, 0x7F12, 0x7F17, 0x7F19, 0x7F1C, 0x7F1B, 0x7F1F, 0x7F21, 0x7F22, 0x7F23, 0x7F24, 0x7F25, 0x7F26, 0x7F27, 0x7F2A, 0x7F2B, 0x7F2C, 0x7F2D, 0x7F2F, 0x7F30, 0x7F31, 0x7F32, 0x7F33, 0x7F35, 0x5E7A, 0x757F, 0x5DDB, 0x753E, 0x9095, 0x738E, 0x7391, 0x73AE, 0x73A2, 0x739F, 0x73CF, 0x73C2, 0x73D1, 0x73B7, 0x73B3, 0x73C0, 0x73C9, 0x73C8, 0x73E5, 0x73D9, 0x987C, 0x740A, 0x73E9, 0x73E7, 0x73DE, 0x73BA, 0x73F2, 0x740F, 0x742A, 0x745B, 0x7426, 0x7425, 0x7428, 0x7430, 0x742E, 0x742C, + 0x942F, 0x9430, 0x9431, 0x9432, 0x9433, 0x9434, 0x9435, 0x9436, 0x9437, 0x9438, 0x9439, 0x943A, 0x943B, 0x943C, 0x943D, 0x943F, 0x9440, 0x9441, 0x9442, 0x9443, 0x9444, 0x9445, 0x9446, 0x9447, 0x9448, 0x9449, 0x944A, 0x944B, 0x944C, 0x944D, 0x944E, 0x944F, 0x9450, 0x9451, 0x9452, 0x9453, 0x9454, 0x9455, 0x9456, 0x9457, 0x9458, 0x9459, 0x945A, 0x945B, 0x945C, 0x945D, 0x945E, 0x945F, 0x9460, 0x9461, 0x9462, 0x9463, 0x9464, 0x9465, 0x9466, 0x9467, 0x9468, 0x9469, 0x946A, 0x946C, 0x946D, 0x946E, 0x946F, 0x9470, 0x9471, 0x9472, 0x9473, 0x9474, 0x9475, 0x9476, 0x9477, 0x9478, 0x9479, 0x947A, 0x947B, 0x947C, 0x947D, 0x947E, 0x947F, 0x9480, 0x9481, 0x9482, 0x9483, 0x9484, 0x9491, 0x9496, 0x9498, 0x94C7, 0x94CF, 0x94D3, 0x94D4, 0x94DA, 0x94E6, 0x94FB, 0x951C, + 0x9520, 0x741B, 0x741A, 0x7441, 0x745C, 0x7457, 0x7455, 0x7459, 0x7477, 0x746D, 0x747E, 0x749C, 0x748E, 0x7480, 0x7481, 0x7487, 0x748B, 0x749E, 0x74A8, 0x74A9, 0x7490, 0x74A7, 0x74D2, 0x74BA, 0x97EA, 0x97EB, 0x97EC, 0x674C, 0x6753, 0x675E, 0x6748, 0x6769, 0x67A5, 0x6787, 0x676A, 0x6773, 0x6798, 0x67A7, 0x6775, 0x67A8, 0x679E, 0x67AD, 0x678B, 0x6777, 0x677C, 0x67F0, 0x6809, 0x67D8, 0x680A, 0x67E9, 0x67B0, 0x680C, 0x67D9, 0x67B5, 0x67DA, 0x67B3, 0x67DD, 0x6800, 0x67C3, 0x67B8, 0x67E2, 0x680E, 0x67C1, 0x67FD, 0x6832, 0x6833, 0x6860, 0x6861, 0x684E, 0x6862, 0x6844, 0x6864, 0x6883, 0x681D, 0x6855, 0x6866, 0x6841, 0x6867, 0x6840, 0x683E, 0x684A, 0x6849, 0x6829, 0x68B5, 0x688F, 0x6874, 0x6877, 0x6893, 0x686B, 0x68C2, 0x696E, 0x68FC, 0x691F, 0x6920, 0x68F9, + 0x9527, 0x9533, 0x953D, 0x9543, 0x9548, 0x954B, 0x9555, 0x955A, 0x9560, 0x956E, 0x9574, 0x9575, 0x9577, 0x9578, 0x9579, 0x957A, 0x957B, 0x957C, 0x957D, 0x957E, 0x9580, 0x9581, 0x9582, 0x9583, 0x9584, 0x9585, 0x9586, 0x9587, 0x9588, 0x9589, 0x958A, 0x958B, 0x958C, 0x958D, 0x958E, 0x958F, 0x9590, 0x9591, 0x9592, 0x9593, 0x9594, 0x9595, 0x9596, 0x9597, 0x9598, 0x9599, 0x959A, 0x959B, 0x959C, 0x959D, 0x959E, 0x959F, 0x95A0, 0x95A1, 0x95A2, 0x95A3, 0x95A4, 0x95A5, 0x95A6, 0x95A7, 0x95A8, 0x95A9, 0x95AA, 0x95AB, 0x95AC, 0x95AD, 0x95AE, 0x95AF, 0x95B0, 0x95B1, 0x95B2, 0x95B3, 0x95B4, 0x95B5, 0x95B6, 0x95B7, 0x95B8, 0x95B9, 0x95BA, 0x95BB, 0x95BC, 0x95BD, 0x95BE, 0x95BF, 0x95C0, 0x95C1, 0x95C2, 0x95C3, 0x95C4, 0x95C5, 0x95C6, 0x95C7, 0x95C8, 0x95C9, 0x95CA, + 0x95CB, 0x6924, 0x68F0, 0x690B, 0x6901, 0x6957, 0x68E3, 0x6910, 0x6971, 0x6939, 0x6960, 0x6942, 0x695D, 0x6984, 0x696B, 0x6980, 0x6998, 0x6978, 0x6934, 0x69CC, 0x6987, 0x6988, 0x69CE, 0x6989, 0x6966, 0x6963, 0x6979, 0x699B, 0x69A7, 0x69BB, 0x69AB, 0x69AD, 0x69D4, 0x69B1, 0x69C1, 0x69CA, 0x69DF, 0x6995, 0x69E0, 0x698D, 0x69FF, 0x6A2F, 0x69ED, 0x6A17, 0x6A18, 0x6A65, 0x69F2, 0x6A44, 0x6A3E, 0x6AA0, 0x6A50, 0x6A5B, 0x6A35, 0x6A8E, 0x6A79, 0x6A3D, 0x6A28, 0x6A58, 0x6A7C, 0x6A91, 0x6A90, 0x6AA9, 0x6A97, 0x6AAB, 0x7337, 0x7352, 0x6B81, 0x6B82, 0x6B87, 0x6B84, 0x6B92, 0x6B93, 0x6B8D, 0x6B9A, 0x6B9B, 0x6BA1, 0x6BAA, 0x8F6B, 0x8F6D, 0x8F71, 0x8F72, 0x8F73, 0x8F75, 0x8F76, 0x8F78, 0x8F77, 0x8F79, 0x8F7A, 0x8F7C, 0x8F7E, 0x8F81, 0x8F82, 0x8F84, 0x8F87, 0x8F8B, + 0x95CC, 0x95CD, 0x95CE, 0x95CF, 0x95D0, 0x95D1, 0x95D2, 0x95D3, 0x95D4, 0x95D5, 0x95D6, 0x95D7, 0x95D8, 0x95D9, 0x95DA, 0x95DB, 0x95DC, 0x95DD, 0x95DE, 0x95DF, 0x95E0, 0x95E1, 0x95E2, 0x95E3, 0x95E4, 0x95E5, 0x95E6, 0x95E7, 0x95EC, 0x95FF, 0x9607, 0x9613, 0x9618, 0x961B, 0x961E, 0x9620, 0x9623, 0x9624, 0x9625, 0x9626, 0x9627, 0x9628, 0x9629, 0x962B, 0x962C, 0x962D, 0x962F, 0x9630, 0x9637, 0x9638, 0x9639, 0x963A, 0x963E, 0x9641, 0x9643, 0x964A, 0x964E, 0x964F, 0x9651, 0x9652, 0x9653, 0x9656, 0x9657, 0x9658, 0x9659, 0x965A, 0x965C, 0x965D, 0x965E, 0x9660, 0x9663, 0x9665, 0x9666, 0x966B, 0x966D, 0x966E, 0x966F, 0x9670, 0x9671, 0x9673, 0x9678, 0x9679, 0x967A, 0x967B, 0x967C, 0x967D, 0x967E, 0x967F, 0x9680, 0x9681, 0x9682, 0x9683, 0x9684, 0x9687, 0x9689, + 0x968A, 0x8F8D, 0x8F8E, 0x8F8F, 0x8F98, 0x8F9A, 0x8ECE, 0x620B, 0x6217, 0x621B, 0x621F, 0x6222, 0x6221, 0x6225, 0x6224, 0x622C, 0x81E7, 0x74EF, 0x74F4, 0x74FF, 0x750F, 0x7511, 0x7513, 0x6534, 0x65EE, 0x65EF, 0x65F0, 0x660A, 0x6619, 0x6772, 0x6603, 0x6615, 0x6600, 0x7085, 0x66F7, 0x661D, 0x6634, 0x6631, 0x6636, 0x6635, 0x8006, 0x665F, 0x6654, 0x6641, 0x664F, 0x6656, 0x6661, 0x6657, 0x6677, 0x6684, 0x668C, 0x66A7, 0x669D, 0x66BE, 0x66DB, 0x66DC, 0x66E6, 0x66E9, 0x8D32, 0x8D33, 0x8D36, 0x8D3B, 0x8D3D, 0x8D40, 0x8D45, 0x8D46, 0x8D48, 0x8D49, 0x8D47, 0x8D4D, 0x8D55, 0x8D59, 0x89C7, 0x89CA, 0x89CB, 0x89CC, 0x89CE, 0x89CF, 0x89D0, 0x89D1, 0x726E, 0x729F, 0x725D, 0x7266, 0x726F, 0x727E, 0x727F, 0x7284, 0x728B, 0x728D, 0x728F, 0x7292, 0x6308, 0x6332, 0x63B0, + 0x968C, 0x968E, 0x9691, 0x9692, 0x9693, 0x9695, 0x9696, 0x969A, 0x969B, 0x969D, 0x969E, 0x969F, 0x96A0, 0x96A1, 0x96A2, 0x96A3, 0x96A4, 0x96A5, 0x96A6, 0x96A8, 0x96A9, 0x96AA, 0x96AB, 0x96AC, 0x96AD, 0x96AE, 0x96AF, 0x96B1, 0x96B2, 0x96B4, 0x96B5, 0x96B7, 0x96B8, 0x96BA, 0x96BB, 0x96BF, 0x96C2, 0x96C3, 0x96C8, 0x96CA, 0x96CB, 0x96D0, 0x96D1, 0x96D3, 0x96D4, 0x96D6, 0x96D7, 0x96D8, 0x96D9, 0x96DA, 0x96DB, 0x96DC, 0x96DD, 0x96DE, 0x96DF, 0x96E1, 0x96E2, 0x96E3, 0x96E4, 0x96E5, 0x96E6, 0x96E7, 0x96EB, 0x96EC, 0x96ED, 0x96EE, 0x96F0, 0x96F1, 0x96F2, 0x96F4, 0x96F5, 0x96F8, 0x96FA, 0x96FB, 0x96FC, 0x96FD, 0x96FF, 0x9702, 0x9703, 0x9705, 0x970A, 0x970B, 0x970C, 0x9710, 0x9711, 0x9712, 0x9714, 0x9715, 0x9717, 0x9718, 0x9719, 0x971A, 0x971B, 0x971D, 0x971F, + 0x9720, 0x643F, 0x64D8, 0x8004, 0x6BEA, 0x6BF3, 0x6BFD, 0x6BF5, 0x6BF9, 0x6C05, 0x6C07, 0x6C06, 0x6C0D, 0x6C15, 0x6C18, 0x6C19, 0x6C1A, 0x6C21, 0x6C29, 0x6C24, 0x6C2A, 0x6C32, 0x6535, 0x6555, 0x656B, 0x724D, 0x7252, 0x7256, 0x7230, 0x8662, 0x5216, 0x809F, 0x809C, 0x8093, 0x80BC, 0x670A, 0x80BD, 0x80B1, 0x80AB, 0x80AD, 0x80B4, 0x80B7, 0x80E7, 0x80E8, 0x80E9, 0x80EA, 0x80DB, 0x80C2, 0x80C4, 0x80D9, 0x80CD, 0x80D7, 0x6710, 0x80DD, 0x80EB, 0x80F1, 0x80F4, 0x80ED, 0x810D, 0x810E, 0x80F2, 0x80FC, 0x6715, 0x8112, 0x8C5A, 0x8136, 0x811E, 0x812C, 0x8118, 0x8132, 0x8148, 0x814C, 0x8153, 0x8174, 0x8159, 0x815A, 0x8171, 0x8160, 0x8169, 0x817C, 0x817D, 0x816D, 0x8167, 0x584D, 0x5AB5, 0x8188, 0x8182, 0x8191, 0x6ED5, 0x81A3, 0x81AA, 0x81CC, 0x6726, 0x81CA, 0x81BB, + 0x9721, 0x9722, 0x9723, 0x9724, 0x9725, 0x9726, 0x9727, 0x9728, 0x9729, 0x972B, 0x972C, 0x972E, 0x972F, 0x9731, 0x9733, 0x9734, 0x9735, 0x9736, 0x9737, 0x973A, 0x973B, 0x973C, 0x973D, 0x973F, 0x9740, 0x9741, 0x9742, 0x9743, 0x9744, 0x9745, 0x9746, 0x9747, 0x9748, 0x9749, 0x974A, 0x974B, 0x974C, 0x974D, 0x974E, 0x974F, 0x9750, 0x9751, 0x9754, 0x9755, 0x9757, 0x9758, 0x975A, 0x975C, 0x975D, 0x975F, 0x9763, 0x9764, 0x9766, 0x9767, 0x9768, 0x976A, 0x976B, 0x976C, 0x976D, 0x976E, 0x976F, 0x9770, 0x9771, 0x9772, 0x9775, 0x9777, 0x9778, 0x9779, 0x977A, 0x977B, 0x977D, 0x977E, 0x977F, 0x9780, 0x9781, 0x9782, 0x9783, 0x9784, 0x9786, 0x9787, 0x9788, 0x9789, 0x978A, 0x978C, 0x978E, 0x978F, 0x9790, 0x9793, 0x9795, 0x9796, 0x9797, 0x9799, 0x979A, 0x979B, 0x979C, + 0x979D, 0x81C1, 0x81A6, 0x6B24, 0x6B37, 0x6B39, 0x6B43, 0x6B46, 0x6B59, 0x98D1, 0x98D2, 0x98D3, 0x98D5, 0x98D9, 0x98DA, 0x6BB3, 0x5F40, 0x6BC2, 0x89F3, 0x6590, 0x9F51, 0x6593, 0x65BC, 0x65C6, 0x65C4, 0x65C3, 0x65CC, 0x65CE, 0x65D2, 0x65D6, 0x7080, 0x709C, 0x7096, 0x709D, 0x70BB, 0x70C0, 0x70B7, 0x70AB, 0x70B1, 0x70E8, 0x70CA, 0x7110, 0x7113, 0x7116, 0x712F, 0x7131, 0x7173, 0x715C, 0x7168, 0x7145, 0x7172, 0x714A, 0x7178, 0x717A, 0x7198, 0x71B3, 0x71B5, 0x71A8, 0x71A0, 0x71E0, 0x71D4, 0x71E7, 0x71F9, 0x721D, 0x7228, 0x706C, 0x7118, 0x7166, 0x71B9, 0x623E, 0x623D, 0x6243, 0x6248, 0x6249, 0x793B, 0x7940, 0x7946, 0x7949, 0x795B, 0x795C, 0x7953, 0x795A, 0x7962, 0x7957, 0x7960, 0x796F, 0x7967, 0x797A, 0x7985, 0x798A, 0x799A, 0x79A7, 0x79B3, 0x5FD1, 0x5FD0, + 0x979E, 0x979F, 0x97A1, 0x97A2, 0x97A4, 0x97A5, 0x97A6, 0x97A7, 0x97A8, 0x97A9, 0x97AA, 0x97AC, 0x97AE, 0x97B0, 0x97B1, 0x97B3, 0x97B5, 0x97B6, 0x97B7, 0x97B8, 0x97B9, 0x97BA, 0x97BB, 0x97BC, 0x97BD, 0x97BE, 0x97BF, 0x97C0, 0x97C1, 0x97C2, 0x97C3, 0x97C4, 0x97C5, 0x97C6, 0x97C7, 0x97C8, 0x97C9, 0x97CA, 0x97CB, 0x97CC, 0x97CD, 0x97CE, 0x97CF, 0x97D0, 0x97D1, 0x97D2, 0x97D3, 0x97D4, 0x97D5, 0x97D6, 0x97D7, 0x97D8, 0x97D9, 0x97DA, 0x97DB, 0x97DC, 0x97DD, 0x97DE, 0x97DF, 0x97E0, 0x97E1, 0x97E2, 0x97E3, 0x97E4, 0x97E5, 0x97E8, 0x97EE, 0x97EF, 0x97F0, 0x97F1, 0x97F2, 0x97F4, 0x97F7, 0x97F8, 0x97F9, 0x97FA, 0x97FB, 0x97FC, 0x97FD, 0x97FE, 0x97FF, 0x9800, 0x9801, 0x9802, 0x9803, 0x9804, 0x9805, 0x9806, 0x9807, 0x9808, 0x9809, 0x980A, 0x980B, 0x980C, 0x980D, + 0x980E, 0x603C, 0x605D, 0x605A, 0x6067, 0x6041, 0x6059, 0x6063, 0x60AB, 0x6106, 0x610D, 0x615D, 0x61A9, 0x619D, 0x61CB, 0x61D1, 0x6206, 0x8080, 0x807F, 0x6C93, 0x6CF6, 0x6DFC, 0x77F6, 0x77F8, 0x7800, 0x7809, 0x7817, 0x7818, 0x7811, 0x65AB, 0x782D, 0x781C, 0x781D, 0x7839, 0x783A, 0x783B, 0x781F, 0x783C, 0x7825, 0x782C, 0x7823, 0x7829, 0x784E, 0x786D, 0x7856, 0x7857, 0x7826, 0x7850, 0x7847, 0x784C, 0x786A, 0x789B, 0x7893, 0x789A, 0x7887, 0x789C, 0x78A1, 0x78A3, 0x78B2, 0x78B9, 0x78A5, 0x78D4, 0x78D9, 0x78C9, 0x78EC, 0x78F2, 0x7905, 0x78F4, 0x7913, 0x7924, 0x791E, 0x7934, 0x9F9B, 0x9EF9, 0x9EFB, 0x9EFC, 0x76F1, 0x7704, 0x770D, 0x76F9, 0x7707, 0x7708, 0x771A, 0x7722, 0x7719, 0x772D, 0x7726, 0x7735, 0x7738, 0x7750, 0x7751, 0x7747, 0x7743, 0x775A, 0x7768, + 0x980F, 0x9810, 0x9811, 0x9812, 0x9813, 0x9814, 0x9815, 0x9816, 0x9817, 0x9818, 0x9819, 0x981A, 0x981B, 0x981C, 0x981D, 0x981E, 0x981F, 0x9820, 0x9821, 0x9822, 0x9823, 0x9824, 0x9825, 0x9826, 0x9827, 0x9828, 0x9829, 0x982A, 0x982B, 0x982C, 0x982D, 0x982E, 0x982F, 0x9830, 0x9831, 0x9832, 0x9833, 0x9834, 0x9835, 0x9836, 0x9837, 0x9838, 0x9839, 0x983A, 0x983B, 0x983C, 0x983D, 0x983E, 0x983F, 0x9840, 0x9841, 0x9842, 0x9843, 0x9844, 0x9845, 0x9846, 0x9847, 0x9848, 0x9849, 0x984A, 0x984B, 0x984C, 0x984D, 0x984E, 0x984F, 0x9850, 0x9851, 0x9852, 0x9853, 0x9854, 0x9855, 0x9856, 0x9857, 0x9858, 0x9859, 0x985A, 0x985B, 0x985C, 0x985D, 0x985E, 0x985F, 0x9860, 0x9861, 0x9862, 0x9863, 0x9864, 0x9865, 0x9866, 0x9867, 0x9868, 0x9869, 0x986A, 0x986B, 0x986C, 0x986D, + 0x986E, 0x7762, 0x7765, 0x777F, 0x778D, 0x777D, 0x7780, 0x778C, 0x7791, 0x779F, 0x77A0, 0x77B0, 0x77B5, 0x77BD, 0x753A, 0x7540, 0x754E, 0x754B, 0x7548, 0x755B, 0x7572, 0x7579, 0x7583, 0x7F58, 0x7F61, 0x7F5F, 0x8A48, 0x7F68, 0x7F74, 0x7F71, 0x7F79, 0x7F81, 0x7F7E, 0x76CD, 0x76E5, 0x8832, 0x9485, 0x9486, 0x9487, 0x948B, 0x948A, 0x948C, 0x948D, 0x948F, 0x9490, 0x9494, 0x9497, 0x9495, 0x949A, 0x949B, 0x949C, 0x94A3, 0x94A4, 0x94AB, 0x94AA, 0x94AD, 0x94AC, 0x94AF, 0x94B0, 0x94B2, 0x94B4, 0x94B6, 0x94B7, 0x94B8, 0x94B9, 0x94BA, 0x94BC, 0x94BD, 0x94BF, 0x94C4, 0x94C8, 0x94C9, 0x94CA, 0x94CB, 0x94CC, 0x94CD, 0x94CE, 0x94D0, 0x94D1, 0x94D2, 0x94D5, 0x94D6, 0x94D7, 0x94D9, 0x94D8, 0x94DB, 0x94DE, 0x94DF, 0x94E0, 0x94E2, 0x94E4, 0x94E5, 0x94E7, 0x94E8, 0x94EA, + 0x986F, 0x9870, 0x9871, 0x9872, 0x9873, 0x9874, 0x988B, 0x988E, 0x9892, 0x9895, 0x9899, 0x98A3, 0x98A8, 0x98A9, 0x98AA, 0x98AB, 0x98AC, 0x98AD, 0x98AE, 0x98AF, 0x98B0, 0x98B1, 0x98B2, 0x98B3, 0x98B4, 0x98B5, 0x98B6, 0x98B7, 0x98B8, 0x98B9, 0x98BA, 0x98BB, 0x98BC, 0x98BD, 0x98BE, 0x98BF, 0x98C0, 0x98C1, 0x98C2, 0x98C3, 0x98C4, 0x98C5, 0x98C6, 0x98C7, 0x98C8, 0x98C9, 0x98CA, 0x98CB, 0x98CC, 0x98CD, 0x98CF, 0x98D0, 0x98D4, 0x98D6, 0x98D7, 0x98DB, 0x98DC, 0x98DD, 0x98E0, 0x98E1, 0x98E2, 0x98E3, 0x98E4, 0x98E5, 0x98E6, 0x98E9, 0x98EA, 0x98EB, 0x98EC, 0x98ED, 0x98EE, 0x98EF, 0x98F0, 0x98F1, 0x98F2, 0x98F3, 0x98F4, 0x98F5, 0x98F6, 0x98F7, 0x98F8, 0x98F9, 0x98FA, 0x98FB, 0x98FC, 0x98FD, 0x98FE, 0x98FF, 0x9900, 0x9901, 0x9902, 0x9903, 0x9904, 0x9905, 0x9906, + 0x9907, 0x94E9, 0x94EB, 0x94EE, 0x94EF, 0x94F3, 0x94F4, 0x94F5, 0x94F7, 0x94F9, 0x94FC, 0x94FD, 0x94FF, 0x9503, 0x9502, 0x9506, 0x9507, 0x9509, 0x950A, 0x950D, 0x950E, 0x950F, 0x9512, 0x9513, 0x9514, 0x9515, 0x9516, 0x9518, 0x951B, 0x951D, 0x951E, 0x951F, 0x9522, 0x952A, 0x952B, 0x9529, 0x952C, 0x9531, 0x9532, 0x9534, 0x9536, 0x9537, 0x9538, 0x953C, 0x953E, 0x953F, 0x9542, 0x9535, 0x9544, 0x9545, 0x9546, 0x9549, 0x954C, 0x954E, 0x954F, 0x9552, 0x9553, 0x9554, 0x9556, 0x9557, 0x9558, 0x9559, 0x955B, 0x955E, 0x955F, 0x955D, 0x9561, 0x9562, 0x9564, 0x9565, 0x9566, 0x9567, 0x9568, 0x9569, 0x956A, 0x956B, 0x956C, 0x956F, 0x9571, 0x9572, 0x9573, 0x953A, 0x77E7, 0x77EC, 0x96C9, 0x79D5, 0x79ED, 0x79E3, 0x79EB, 0x7A06, 0x5D47, 0x7A03, 0x7A02, 0x7A1E, 0x7A14, + 0x9908, 0x9909, 0x990A, 0x990B, 0x990C, 0x990E, 0x990F, 0x9911, 0x9912, 0x9913, 0x9914, 0x9915, 0x9916, 0x9917, 0x9918, 0x9919, 0x991A, 0x991B, 0x991C, 0x991D, 0x991E, 0x991F, 0x9920, 0x9921, 0x9922, 0x9923, 0x9924, 0x9925, 0x9926, 0x9927, 0x9928, 0x9929, 0x992A, 0x992B, 0x992C, 0x992D, 0x992F, 0x9930, 0x9931, 0x9932, 0x9933, 0x9934, 0x9935, 0x9936, 0x9937, 0x9938, 0x9939, 0x993A, 0x993B, 0x993C, 0x993D, 0x993E, 0x993F, 0x9940, 0x9941, 0x9942, 0x9943, 0x9944, 0x9945, 0x9946, 0x9947, 0x9948, 0x9949, 0x994A, 0x994B, 0x994C, 0x994D, 0x994E, 0x994F, 0x9950, 0x9951, 0x9952, 0x9953, 0x9956, 0x9957, 0x9958, 0x9959, 0x995A, 0x995B, 0x995C, 0x995D, 0x995E, 0x995F, 0x9960, 0x9961, 0x9962, 0x9964, 0x9966, 0x9973, 0x9978, 0x9979, 0x997B, 0x997E, 0x9982, 0x9983, + 0x9989, 0x7A39, 0x7A37, 0x7A51, 0x9ECF, 0x99A5, 0x7A70, 0x7688, 0x768E, 0x7693, 0x7699, 0x76A4, 0x74DE, 0x74E0, 0x752C, 0x9E20, 0x9E22, 0x9E28, 0x9E29, 0x9E2A, 0x9E2B, 0x9E2C, 0x9E32, 0x9E31, 0x9E36, 0x9E38, 0x9E37, 0x9E39, 0x9E3A, 0x9E3E, 0x9E41, 0x9E42, 0x9E44, 0x9E46, 0x9E47, 0x9E48, 0x9E49, 0x9E4B, 0x9E4C, 0x9E4E, 0x9E51, 0x9E55, 0x9E57, 0x9E5A, 0x9E5B, 0x9E5C, 0x9E5E, 0x9E63, 0x9E66, 0x9E67, 0x9E68, 0x9E69, 0x9E6A, 0x9E6B, 0x9E6C, 0x9E71, 0x9E6D, 0x9E73, 0x7592, 0x7594, 0x7596, 0x75A0, 0x759D, 0x75AC, 0x75A3, 0x75B3, 0x75B4, 0x75B8, 0x75C4, 0x75B1, 0x75B0, 0x75C3, 0x75C2, 0x75D6, 0x75CD, 0x75E3, 0x75E8, 0x75E6, 0x75E4, 0x75EB, 0x75E7, 0x7603, 0x75F1, 0x75FC, 0x75FF, 0x7610, 0x7600, 0x7605, 0x760C, 0x7617, 0x760A, 0x7625, 0x7618, 0x7615, 0x7619, + 0x998C, 0x998E, 0x999A, 0x999B, 0x999C, 0x999D, 0x999E, 0x999F, 0x99A0, 0x99A1, 0x99A2, 0x99A3, 0x99A4, 0x99A6, 0x99A7, 0x99A9, 0x99AA, 0x99AB, 0x99AC, 0x99AD, 0x99AE, 0x99AF, 0x99B0, 0x99B1, 0x99B2, 0x99B3, 0x99B4, 0x99B5, 0x99B6, 0x99B7, 0x99B8, 0x99B9, 0x99BA, 0x99BB, 0x99BC, 0x99BD, 0x99BE, 0x99BF, 0x99C0, 0x99C1, 0x99C2, 0x99C3, 0x99C4, 0x99C5, 0x99C6, 0x99C7, 0x99C8, 0x99C9, 0x99CA, 0x99CB, 0x99CC, 0x99CD, 0x99CE, 0x99CF, 0x99D0, 0x99D1, 0x99D2, 0x99D3, 0x99D4, 0x99D5, 0x99D6, 0x99D7, 0x99D8, 0x99D9, 0x99DA, 0x99DB, 0x99DC, 0x99DD, 0x99DE, 0x99DF, 0x99E0, 0x99E1, 0x99E2, 0x99E3, 0x99E4, 0x99E5, 0x99E6, 0x99E7, 0x99E8, 0x99E9, 0x99EA, 0x99EB, 0x99EC, 0x99ED, 0x99EE, 0x99EF, 0x99F0, 0x99F1, 0x99F2, 0x99F3, 0x99F4, 0x99F5, 0x99F6, 0x99F7, 0x99F8, + 0x99F9, 0x761B, 0x763C, 0x7622, 0x7620, 0x7640, 0x762D, 0x7630, 0x763F, 0x7635, 0x7643, 0x763E, 0x7633, 0x764D, 0x765E, 0x7654, 0x765C, 0x7656, 0x766B, 0x766F, 0x7FCA, 0x7AE6, 0x7A78, 0x7A79, 0x7A80, 0x7A86, 0x7A88, 0x7A95, 0x7AA6, 0x7AA0, 0x7AAC, 0x7AA8, 0x7AAD, 0x7AB3, 0x8864, 0x8869, 0x8872, 0x887D, 0x887F, 0x8882, 0x88A2, 0x88C6, 0x88B7, 0x88BC, 0x88C9, 0x88E2, 0x88CE, 0x88E3, 0x88E5, 0x88F1, 0x891A, 0x88FC, 0x88E8, 0x88FE, 0x88F0, 0x8921, 0x8919, 0x8913, 0x891B, 0x890A, 0x8934, 0x892B, 0x8936, 0x8941, 0x8966, 0x897B, 0x758B, 0x80E5, 0x76B2, 0x76B4, 0x77DC, 0x8012, 0x8014, 0x8016, 0x801C, 0x8020, 0x8022, 0x8025, 0x8026, 0x8027, 0x8029, 0x8028, 0x8031, 0x800B, 0x8035, 0x8043, 0x8046, 0x804D, 0x8052, 0x8069, 0x8071, 0x8983, 0x9878, 0x9880, 0x9883, + 0x99FA, 0x99FB, 0x99FC, 0x99FD, 0x99FE, 0x99FF, 0x9A00, 0x9A01, 0x9A02, 0x9A03, 0x9A04, 0x9A05, 0x9A06, 0x9A07, 0x9A08, 0x9A09, 0x9A0A, 0x9A0B, 0x9A0C, 0x9A0D, 0x9A0E, 0x9A0F, 0x9A10, 0x9A11, 0x9A12, 0x9A13, 0x9A14, 0x9A15, 0x9A16, 0x9A17, 0x9A18, 0x9A19, 0x9A1A, 0x9A1B, 0x9A1C, 0x9A1D, 0x9A1E, 0x9A1F, 0x9A20, 0x9A21, 0x9A22, 0x9A23, 0x9A24, 0x9A25, 0x9A26, 0x9A27, 0x9A28, 0x9A29, 0x9A2A, 0x9A2B, 0x9A2C, 0x9A2D, 0x9A2E, 0x9A2F, 0x9A30, 0x9A31, 0x9A32, 0x9A33, 0x9A34, 0x9A35, 0x9A36, 0x9A37, 0x9A38, 0x9A39, 0x9A3A, 0x9A3B, 0x9A3C, 0x9A3D, 0x9A3E, 0x9A3F, 0x9A40, 0x9A41, 0x9A42, 0x9A43, 0x9A44, 0x9A45, 0x9A46, 0x9A47, 0x9A48, 0x9A49, 0x9A4A, 0x9A4B, 0x9A4C, 0x9A4D, 0x9A4E, 0x9A4F, 0x9A50, 0x9A51, 0x9A52, 0x9A53, 0x9A54, 0x9A55, 0x9A56, 0x9A57, 0x9A58, + 0x9A59, 0x9889, 0x988C, 0x988D, 0x988F, 0x9894, 0x989A, 0x989B, 0x989E, 0x989F, 0x98A1, 0x98A2, 0x98A5, 0x98A6, 0x864D, 0x8654, 0x866C, 0x866E, 0x867F, 0x867A, 0x867C, 0x867B, 0x86A8, 0x868D, 0x868B, 0x86AC, 0x869D, 0x86A7, 0x86A3, 0x86AA, 0x8693, 0x86A9, 0x86B6, 0x86C4, 0x86B5, 0x86CE, 0x86B0, 0x86BA, 0x86B1, 0x86AF, 0x86C9, 0x86CF, 0x86B4, 0x86E9, 0x86F1, 0x86F2, 0x86ED, 0x86F3, 0x86D0, 0x8713, 0x86DE, 0x86F4, 0x86DF, 0x86D8, 0x86D1, 0x8703, 0x8707, 0x86F8, 0x8708, 0x870A, 0x870D, 0x8709, 0x8723, 0x873B, 0x871E, 0x8725, 0x872E, 0x871A, 0x873E, 0x8748, 0x8734, 0x8731, 0x8729, 0x8737, 0x873F, 0x8782, 0x8722, 0x877D, 0x877E, 0x877B, 0x8760, 0x8770, 0x874C, 0x876E, 0x878B, 0x8753, 0x8763, 0x877C, 0x8764, 0x8759, 0x8765, 0x8793, 0x87AF, 0x87A8, 0x87D2, + 0x9A5A, 0x9A5B, 0x9A5C, 0x9A5D, 0x9A5E, 0x9A5F, 0x9A60, 0x9A61, 0x9A62, 0x9A63, 0x9A64, 0x9A65, 0x9A66, 0x9A67, 0x9A68, 0x9A69, 0x9A6A, 0x9A6B, 0x9A72, 0x9A83, 0x9A89, 0x9A8D, 0x9A8E, 0x9A94, 0x9A95, 0x9A99, 0x9AA6, 0x9AA9, 0x9AAA, 0x9AAB, 0x9AAC, 0x9AAD, 0x9AAE, 0x9AAF, 0x9AB2, 0x9AB3, 0x9AB4, 0x9AB5, 0x9AB9, 0x9ABB, 0x9ABD, 0x9ABE, 0x9ABF, 0x9AC3, 0x9AC4, 0x9AC6, 0x9AC7, 0x9AC8, 0x9AC9, 0x9ACA, 0x9ACD, 0x9ACE, 0x9ACF, 0x9AD0, 0x9AD2, 0x9AD4, 0x9AD5, 0x9AD6, 0x9AD7, 0x9AD9, 0x9ADA, 0x9ADB, 0x9ADC, 0x9ADD, 0x9ADE, 0x9AE0, 0x9AE2, 0x9AE3, 0x9AE4, 0x9AE5, 0x9AE7, 0x9AE8, 0x9AE9, 0x9AEA, 0x9AEC, 0x9AEE, 0x9AF0, 0x9AF1, 0x9AF2, 0x9AF3, 0x9AF4, 0x9AF5, 0x9AF6, 0x9AF7, 0x9AF8, 0x9AFA, 0x9AFC, 0x9AFD, 0x9AFE, 0x9AFF, 0x9B00, 0x9B01, 0x9B02, 0x9B04, 0x9B05, + 0x9B06, 0x87C6, 0x8788, 0x8785, 0x87AD, 0x8797, 0x8783, 0x87AB, 0x87E5, 0x87AC, 0x87B5, 0x87B3, 0x87CB, 0x87D3, 0x87BD, 0x87D1, 0x87C0, 0x87CA, 0x87DB, 0x87EA, 0x87E0, 0x87EE, 0x8816, 0x8813, 0x87FE, 0x880A, 0x881B, 0x8821, 0x8839, 0x883C, 0x7F36, 0x7F42, 0x7F44, 0x7F45, 0x8210, 0x7AFA, 0x7AFD, 0x7B08, 0x7B03, 0x7B04, 0x7B15, 0x7B0A, 0x7B2B, 0x7B0F, 0x7B47, 0x7B38, 0x7B2A, 0x7B19, 0x7B2E, 0x7B31, 0x7B20, 0x7B25, 0x7B24, 0x7B33, 0x7B3E, 0x7B1E, 0x7B58, 0x7B5A, 0x7B45, 0x7B75, 0x7B4C, 0x7B5D, 0x7B60, 0x7B6E, 0x7B7B, 0x7B62, 0x7B72, 0x7B71, 0x7B90, 0x7BA6, 0x7BA7, 0x7BB8, 0x7BAC, 0x7B9D, 0x7BA8, 0x7B85, 0x7BAA, 0x7B9C, 0x7BA2, 0x7BAB, 0x7BB4, 0x7BD1, 0x7BC1, 0x7BCC, 0x7BDD, 0x7BDA, 0x7BE5, 0x7BE6, 0x7BEA, 0x7C0C, 0x7BFE, 0x7BFC, 0x7C0F, 0x7C16, 0x7C0B, + 0x9B07, 0x9B09, 0x9B0A, 0x9B0B, 0x9B0C, 0x9B0D, 0x9B0E, 0x9B10, 0x9B11, 0x9B12, 0x9B14, 0x9B15, 0x9B16, 0x9B17, 0x9B18, 0x9B19, 0x9B1A, 0x9B1B, 0x9B1C, 0x9B1D, 0x9B1E, 0x9B20, 0x9B21, 0x9B22, 0x9B24, 0x9B25, 0x9B26, 0x9B27, 0x9B28, 0x9B29, 0x9B2A, 0x9B2B, 0x9B2C, 0x9B2D, 0x9B2E, 0x9B30, 0x9B31, 0x9B33, 0x9B34, 0x9B35, 0x9B36, 0x9B37, 0x9B38, 0x9B39, 0x9B3A, 0x9B3D, 0x9B3E, 0x9B3F, 0x9B40, 0x9B46, 0x9B4A, 0x9B4B, 0x9B4C, 0x9B4E, 0x9B50, 0x9B52, 0x9B53, 0x9B55, 0x9B56, 0x9B57, 0x9B58, 0x9B59, 0x9B5A, 0x9B5B, 0x9B5C, 0x9B5D, 0x9B5E, 0x9B5F, 0x9B60, 0x9B61, 0x9B62, 0x9B63, 0x9B64, 0x9B65, 0x9B66, 0x9B67, 0x9B68, 0x9B69, 0x9B6A, 0x9B6B, 0x9B6C, 0x9B6D, 0x9B6E, 0x9B6F, 0x9B70, 0x9B71, 0x9B72, 0x9B73, 0x9B74, 0x9B75, 0x9B76, 0x9B77, 0x9B78, 0x9B79, 0x9B7A, + 0x9B7B, 0x7C1F, 0x7C2A, 0x7C26, 0x7C38, 0x7C41, 0x7C40, 0x81FE, 0x8201, 0x8202, 0x8204, 0x81EC, 0x8844, 0x8221, 0x8222, 0x8223, 0x822D, 0x822F, 0x8228, 0x822B, 0x8238, 0x823B, 0x8233, 0x8234, 0x823E, 0x8244, 0x8249, 0x824B, 0x824F, 0x825A, 0x825F, 0x8268, 0x887E, 0x8885, 0x8888, 0x88D8, 0x88DF, 0x895E, 0x7F9D, 0x7F9F, 0x7FA7, 0x7FAF, 0x7FB0, 0x7FB2, 0x7C7C, 0x6549, 0x7C91, 0x7C9D, 0x7C9C, 0x7C9E, 0x7CA2, 0x7CB2, 0x7CBC, 0x7CBD, 0x7CC1, 0x7CC7, 0x7CCC, 0x7CCD, 0x7CC8, 0x7CC5, 0x7CD7, 0x7CE8, 0x826E, 0x66A8, 0x7FBF, 0x7FCE, 0x7FD5, 0x7FE5, 0x7FE1, 0x7FE6, 0x7FE9, 0x7FEE, 0x7FF3, 0x7CF8, 0x7D77, 0x7DA6, 0x7DAE, 0x7E47, 0x7E9B, 0x9EB8, 0x9EB4, 0x8D73, 0x8D84, 0x8D94, 0x8D91, 0x8DB1, 0x8D67, 0x8D6D, 0x8C47, 0x8C49, 0x914A, 0x9150, 0x914E, 0x914F, 0x9164, + 0x9B7C, 0x9B7D, 0x9B7E, 0x9B7F, 0x9B80, 0x9B81, 0x9B82, 0x9B83, 0x9B84, 0x9B85, 0x9B86, 0x9B87, 0x9B88, 0x9B89, 0x9B8A, 0x9B8B, 0x9B8C, 0x9B8D, 0x9B8E, 0x9B8F, 0x9B90, 0x9B91, 0x9B92, 0x9B93, 0x9B94, 0x9B95, 0x9B96, 0x9B97, 0x9B98, 0x9B99, 0x9B9A, 0x9B9B, 0x9B9C, 0x9B9D, 0x9B9E, 0x9B9F, 0x9BA0, 0x9BA1, 0x9BA2, 0x9BA3, 0x9BA4, 0x9BA5, 0x9BA6, 0x9BA7, 0x9BA8, 0x9BA9, 0x9BAA, 0x9BAB, 0x9BAC, 0x9BAD, 0x9BAE, 0x9BAF, 0x9BB0, 0x9BB1, 0x9BB2, 0x9BB3, 0x9BB4, 0x9BB5, 0x9BB6, 0x9BB7, 0x9BB8, 0x9BB9, 0x9BBA, 0x9BBB, 0x9BBC, 0x9BBD, 0x9BBE, 0x9BBF, 0x9BC0, 0x9BC1, 0x9BC2, 0x9BC3, 0x9BC4, 0x9BC5, 0x9BC6, 0x9BC7, 0x9BC8, 0x9BC9, 0x9BCA, 0x9BCB, 0x9BCC, 0x9BCD, 0x9BCE, 0x9BCF, 0x9BD0, 0x9BD1, 0x9BD2, 0x9BD3, 0x9BD4, 0x9BD5, 0x9BD6, 0x9BD7, 0x9BD8, 0x9BD9, 0x9BDA, + 0x9BDB, 0x9162, 0x9161, 0x9170, 0x9169, 0x916F, 0x917D, 0x917E, 0x9172, 0x9174, 0x9179, 0x918C, 0x9185, 0x9190, 0x918D, 0x9191, 0x91A2, 0x91A3, 0x91AA, 0x91AD, 0x91AE, 0x91AF, 0x91B5, 0x91B4, 0x91BA, 0x8C55, 0x9E7E, 0x8DB8, 0x8DEB, 0x8E05, 0x8E59, 0x8E69, 0x8DB5, 0x8DBF, 0x8DBC, 0x8DBA, 0x8DC4, 0x8DD6, 0x8DD7, 0x8DDA, 0x8DDE, 0x8DCE, 0x8DCF, 0x8DDB, 0x8DC6, 0x8DEC, 0x8DF7, 0x8DF8, 0x8DE3, 0x8DF9, 0x8DFB, 0x8DE4, 0x8E09, 0x8DFD, 0x8E14, 0x8E1D, 0x8E1F, 0x8E2C, 0x8E2E, 0x8E23, 0x8E2F, 0x8E3A, 0x8E40, 0x8E39, 0x8E35, 0x8E3D, 0x8E31, 0x8E49, 0x8E41, 0x8E42, 0x8E51, 0x8E52, 0x8E4A, 0x8E70, 0x8E76, 0x8E7C, 0x8E6F, 0x8E74, 0x8E85, 0x8E8F, 0x8E94, 0x8E90, 0x8E9C, 0x8E9E, 0x8C78, 0x8C82, 0x8C8A, 0x8C85, 0x8C98, 0x8C94, 0x659B, 0x89D6, 0x89DE, 0x89DA, 0x89DC, + 0x9BDC, 0x9BDD, 0x9BDE, 0x9BDF, 0x9BE0, 0x9BE1, 0x9BE2, 0x9BE3, 0x9BE4, 0x9BE5, 0x9BE6, 0x9BE7, 0x9BE8, 0x9BE9, 0x9BEA, 0x9BEB, 0x9BEC, 0x9BED, 0x9BEE, 0x9BEF, 0x9BF0, 0x9BF1, 0x9BF2, 0x9BF3, 0x9BF4, 0x9BF5, 0x9BF6, 0x9BF7, 0x9BF8, 0x9BF9, 0x9BFA, 0x9BFB, 0x9BFC, 0x9BFD, 0x9BFE, 0x9BFF, 0x9C00, 0x9C01, 0x9C02, 0x9C03, 0x9C04, 0x9C05, 0x9C06, 0x9C07, 0x9C08, 0x9C09, 0x9C0A, 0x9C0B, 0x9C0C, 0x9C0D, 0x9C0E, 0x9C0F, 0x9C10, 0x9C11, 0x9C12, 0x9C13, 0x9C14, 0x9C15, 0x9C16, 0x9C17, 0x9C18, 0x9C19, 0x9C1A, 0x9C1B, 0x9C1C, 0x9C1D, 0x9C1E, 0x9C1F, 0x9C20, 0x9C21, 0x9C22, 0x9C23, 0x9C24, 0x9C25, 0x9C26, 0x9C27, 0x9C28, 0x9C29, 0x9C2A, 0x9C2B, 0x9C2C, 0x9C2D, 0x9C2E, 0x9C2F, 0x9C30, 0x9C31, 0x9C32, 0x9C33, 0x9C34, 0x9C35, 0x9C36, 0x9C37, 0x9C38, 0x9C39, 0x9C3A, + 0x9C3B, 0x89E5, 0x89EB, 0x89EF, 0x8A3E, 0x8B26, 0x9753, 0x96E9, 0x96F3, 0x96EF, 0x9706, 0x9701, 0x9708, 0x970F, 0x970E, 0x972A, 0x972D, 0x9730, 0x973E, 0x9F80, 0x9F83, 0x9F85, 0x9F86, 0x9F87, 0x9F88, 0x9F89, 0x9F8A, 0x9F8C, 0x9EFE, 0x9F0B, 0x9F0D, 0x96B9, 0x96BC, 0x96BD, 0x96CE, 0x96D2, 0x77BF, 0x96E0, 0x928E, 0x92AE, 0x92C8, 0x933E, 0x936A, 0x93CA, 0x938F, 0x943E, 0x946B, 0x9C7F, 0x9C82, 0x9C85, 0x9C86, 0x9C87, 0x9C88, 0x7A23, 0x9C8B, 0x9C8E, 0x9C90, 0x9C91, 0x9C92, 0x9C94, 0x9C95, 0x9C9A, 0x9C9B, 0x9C9E, 0x9C9F, 0x9CA0, 0x9CA1, 0x9CA2, 0x9CA3, 0x9CA5, 0x9CA6, 0x9CA7, 0x9CA8, 0x9CA9, 0x9CAB, 0x9CAD, 0x9CAE, 0x9CB0, 0x9CB1, 0x9CB2, 0x9CB3, 0x9CB4, 0x9CB5, 0x9CB6, 0x9CB7, 0x9CBA, 0x9CBB, 0x9CBC, 0x9CBD, 0x9CC4, 0x9CC5, 0x9CC6, 0x9CC7, 0x9CCA, 0x9CCB, + 0x9C3C, 0x9C3D, 0x9C3E, 0x9C3F, 0x9C40, 0x9C41, 0x9C42, 0x9C43, 0x9C44, 0x9C45, 0x9C46, 0x9C47, 0x9C48, 0x9C49, 0x9C4A, 0x9C4B, 0x9C4C, 0x9C4D, 0x9C4E, 0x9C4F, 0x9C50, 0x9C51, 0x9C52, 0x9C53, 0x9C54, 0x9C55, 0x9C56, 0x9C57, 0x9C58, 0x9C59, 0x9C5A, 0x9C5B, 0x9C5C, 0x9C5D, 0x9C5E, 0x9C5F, 0x9C60, 0x9C61, 0x9C62, 0x9C63, 0x9C64, 0x9C65, 0x9C66, 0x9C67, 0x9C68, 0x9C69, 0x9C6A, 0x9C6B, 0x9C6C, 0x9C6D, 0x9C6E, 0x9C6F, 0x9C70, 0x9C71, 0x9C72, 0x9C73, 0x9C74, 0x9C75, 0x9C76, 0x9C77, 0x9C78, 0x9C79, 0x9C7A, 0x9C7B, 0x9C7D, 0x9C7E, 0x9C80, 0x9C83, 0x9C84, 0x9C89, 0x9C8A, 0x9C8C, 0x9C8F, 0x9C93, 0x9C96, 0x9C97, 0x9C98, 0x9C99, 0x9C9D, 0x9CAA, 0x9CAC, 0x9CAF, 0x9CB9, 0x9CBE, 0x9CBF, 0x9CC0, 0x9CC1, 0x9CC2, 0x9CC8, 0x9CC9, 0x9CD1, 0x9CD2, 0x9CDA, 0x9CDB, 0x9CE0, + 0x9CE1, 0x9CCC, 0x9CCD, 0x9CCE, 0x9CCF, 0x9CD0, 0x9CD3, 0x9CD4, 0x9CD5, 0x9CD7, 0x9CD8, 0x9CD9, 0x9CDC, 0x9CDD, 0x9CDF, 0x9CE2, 0x977C, 0x9785, 0x9791, 0x9792, 0x9794, 0x97AF, 0x97AB, 0x97A3, 0x97B2, 0x97B4, 0x9AB1, 0x9AB0, 0x9AB7, 0x9E58, 0x9AB6, 0x9ABA, 0x9ABC, 0x9AC1, 0x9AC0, 0x9AC5, 0x9AC2, 0x9ACB, 0x9ACC, 0x9AD1, 0x9B45, 0x9B43, 0x9B47, 0x9B49, 0x9B48, 0x9B4D, 0x9B51, 0x98E8, 0x990D, 0x992E, 0x9955, 0x9954, 0x9ADF, 0x9AE1, 0x9AE6, 0x9AEF, 0x9AEB, 0x9AFB, 0x9AED, 0x9AF9, 0x9B08, 0x9B0F, 0x9B13, 0x9B1F, 0x9B23, 0x9EBD, 0x9EBE, 0x7E3B, 0x9E82, 0x9E87, 0x9E88, 0x9E8B, 0x9E92, 0x93D6, 0x9E9D, 0x9E9F, 0x9EDB, 0x9EDC, 0x9EDD, 0x9EE0, 0x9EDF, 0x9EE2, 0x9EE9, 0x9EE7, 0x9EE5, 0x9EEA, 0x9EEF, 0x9F22, 0x9F2C, 0x9F2F, 0x9F39, 0x9F37, 0x9F3D, 0x9F3E, 0x9F44, + 0x9CE3, 0x9CE4, 0x9CE5, 0x9CE6, 0x9CE7, 0x9CE8, 0x9CE9, 0x9CEA, 0x9CEB, 0x9CEC, 0x9CED, 0x9CEE, 0x9CEF, 0x9CF0, 0x9CF1, 0x9CF2, 0x9CF3, 0x9CF4, 0x9CF5, 0x9CF6, 0x9CF7, 0x9CF8, 0x9CF9, 0x9CFA, 0x9CFB, 0x9CFC, 0x9CFD, 0x9CFE, 0x9CFF, 0x9D00, 0x9D01, 0x9D02, 0x9D03, 0x9D04, 0x9D05, 0x9D06, 0x9D07, 0x9D08, 0x9D09, 0x9D0A, 0x9D0B, 0x9D0C, 0x9D0D, 0x9D0E, 0x9D0F, 0x9D10, 0x9D11, 0x9D12, 0x9D13, 0x9D14, 0x9D15, 0x9D16, 0x9D17, 0x9D18, 0x9D19, 0x9D1A, 0x9D1B, 0x9D1C, 0x9D1D, 0x9D1E, 0x9D1F, 0x9D20, 0x9D21, 0x9D22, 0x9D23, 0x9D24, 0x9D25, 0x9D26, 0x9D27, 0x9D28, 0x9D29, 0x9D2A, 0x9D2B, 0x9D2C, 0x9D2D, 0x9D2E, 0x9D2F, 0x9D30, 0x9D31, 0x9D32, 0x9D33, 0x9D34, 0x9D35, 0x9D36, 0x9D37, 0x9D38, 0x9D39, 0x9D3A, 0x9D3B, 0x9D3C, 0x9D3D, 0x9D3E, 0x9D3F, 0x9D40, 0x9D41, + 0x9D42, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x9D43, 0x9D44, 0x9D45, 0x9D46, 0x9D47, 0x9D48, 0x9D49, 0x9D4A, 0x9D4B, 0x9D4C, 0x9D4D, 0x9D4E, 0x9D4F, 0x9D50, 0x9D51, 0x9D52, 0x9D53, 0x9D54, 0x9D55, 0x9D56, 0x9D57, 0x9D58, 0x9D59, 0x9D5A, 0x9D5B, 0x9D5C, 0x9D5D, 0x9D5E, 0x9D5F, 0x9D60, 0x9D61, 0x9D62, 0x9D63, 0x9D64, 0x9D65, 0x9D66, 0x9D67, 0x9D68, 0x9D69, 0x9D6A, 0x9D6B, 0x9D6C, 0x9D6D, 0x9D6E, 0x9D6F, 0x9D70, 0x9D71, 0x9D72, 0x9D73, 0x9D74, 0x9D75, 0x9D76, 0x9D77, 0x9D78, 0x9D79, 0x9D7A, 0x9D7B, 0x9D7C, 0x9D7D, 0x9D7E, 0x9D7F, 0x9D80, 0x9D81, 0x9D82, 0x9D83, 0x9D84, 0x9D85, 0x9D86, 0x9D87, 0x9D88, 0x9D89, 0x9D8A, 0x9D8B, 0x9D8C, 0x9D8D, 0x9D8E, 0x9D8F, 0x9D90, 0x9D91, 0x9D92, 0x9D93, 0x9D94, 0x9D95, 0x9D96, 0x9D97, 0x9D98, 0x9D99, 0x9D9A, 0x9D9B, 0x9D9C, 0x9D9D, 0x9D9E, 0x9D9F, 0x9DA0, 0x9DA1, + 0x9DA2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x9DA3, 0x9DA4, 0x9DA5, 0x9DA6, 0x9DA7, 0x9DA8, 0x9DA9, 0x9DAA, 0x9DAB, 0x9DAC, 0x9DAD, 0x9DAE, 0x9DAF, 0x9DB0, 0x9DB1, 0x9DB2, 0x9DB3, 0x9DB4, 0x9DB5, 0x9DB6, 0x9DB7, 0x9DB8, 0x9DB9, 0x9DBA, 0x9DBB, 0x9DBC, 0x9DBD, 0x9DBE, 0x9DBF, 0x9DC0, 0x9DC1, 0x9DC2, 0x9DC3, 0x9DC4, 0x9DC5, 0x9DC6, 0x9DC7, 0x9DC8, 0x9DC9, 0x9DCA, 0x9DCB, 0x9DCC, 0x9DCD, 0x9DCE, 0x9DCF, 0x9DD0, 0x9DD1, 0x9DD2, 0x9DD3, 0x9DD4, 0x9DD5, 0x9DD6, 0x9DD7, 0x9DD8, 0x9DD9, 0x9DDA, 0x9DDB, 0x9DDC, 0x9DDD, 0x9DDE, 0x9DDF, 0x9DE0, 0x9DE1, 0x9DE2, 0x9DE3, 0x9DE4, 0x9DE5, 0x9DE6, 0x9DE7, 0x9DE8, 0x9DE9, 0x9DEA, 0x9DEB, 0x9DEC, 0x9DED, 0x9DEE, 0x9DEF, 0x9DF0, 0x9DF1, 0x9DF2, 0x9DF3, 0x9DF4, 0x9DF5, 0x9DF6, 0x9DF7, 0x9DF8, 0x9DF9, 0x9DFA, 0x9DFB, 0x9DFC, 0x9DFD, 0x9DFE, 0x9DFF, 0x9E00, 0x9E01, + 0x9E02, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x9E03, 0x9E04, 0x9E05, 0x9E06, 0x9E07, 0x9E08, 0x9E09, 0x9E0A, 0x9E0B, 0x9E0C, 0x9E0D, 0x9E0E, 0x9E0F, 0x9E10, 0x9E11, 0x9E12, 0x9E13, 0x9E14, 0x9E15, 0x9E16, 0x9E17, 0x9E18, 0x9E19, 0x9E1A, 0x9E1B, 0x9E1C, 0x9E1D, 0x9E1E, 0x9E24, 0x9E27, 0x9E2E, 0x9E30, 0x9E34, 0x9E3B, 0x9E3C, 0x9E40, 0x9E4D, 0x9E50, 0x9E52, 0x9E53, 0x9E54, 0x9E56, 0x9E59, 0x9E5D, 0x9E5F, 0x9E60, 0x9E61, 0x9E62, 0x9E65, 0x9E6E, 0x9E6F, 0x9E72, 0x9E74, 0x9E75, 0x9E76, 0x9E77, 0x9E78, 0x9E79, 0x9E7A, 0x9E7B, 0x9E7C, 0x9E7D, 0x9E80, 0x9E81, 0x9E83, 0x9E84, 0x9E85, 0x9E86, 0x9E89, 0x9E8A, 0x9E8C, 0x9E8D, 0x9E8E, 0x9E8F, 0x9E90, 0x9E91, 0x9E94, 0x9E95, 0x9E96, 0x9E97, 0x9E98, 0x9E99, 0x9E9A, 0x9E9B, 0x9E9C, 0x9E9E, 0x9EA0, 0x9EA1, 0x9EA2, 0x9EA3, 0x9EA4, 0x9EA5, 0x9EA7, 0x9EA8, 0x9EA9, + 0x9EAA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x9EAB, 0x9EAC, 0x9EAD, 0x9EAE, 0x9EAF, 0x9EB0, 0x9EB1, 0x9EB2, 0x9EB3, 0x9EB5, 0x9EB6, 0x9EB7, 0x9EB9, 0x9EBA, 0x9EBC, 0x9EBF, 0x9EC0, 0x9EC1, 0x9EC2, 0x9EC3, 0x9EC5, 0x9EC6, 0x9EC7, 0x9EC8, 0x9ECA, 0x9ECB, 0x9ECC, 0x9ED0, 0x9ED2, 0x9ED3, 0x9ED5, 0x9ED6, 0x9ED7, 0x9ED9, 0x9EDA, 0x9EDE, 0x9EE1, 0x9EE3, 0x9EE4, 0x9EE6, 0x9EE8, 0x9EEB, 0x9EEC, 0x9EED, 0x9EEE, 0x9EF0, 0x9EF1, 0x9EF2, 0x9EF3, 0x9EF4, 0x9EF5, 0x9EF6, 0x9EF7, 0x9EF8, 0x9EFA, 0x9EFD, 0x9EFF, 0x9F00, 0x9F01, 0x9F02, 0x9F03, 0x9F04, 0x9F05, 0x9F06, 0x9F07, 0x9F08, 0x9F09, 0x9F0A, 0x9F0C, 0x9F0F, 0x9F11, 0x9F12, 0x9F14, 0x9F15, 0x9F16, 0x9F18, 0x9F1A, 0x9F1B, 0x9F1C, 0x9F1D, 0x9F1E, 0x9F1F, 0x9F21, 0x9F23, 0x9F24, 0x9F25, 0x9F26, 0x9F27, 0x9F28, 0x9F29, 0x9F2A, 0x9F2B, 0x9F2D, 0x9F2E, 0x9F30, + 0x9F31, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x9F32, 0x9F33, 0x9F34, 0x9F35, 0x9F36, 0x9F38, 0x9F3A, 0x9F3C, 0x9F3F, 0x9F40, 0x9F41, 0x9F42, 0x9F43, 0x9F45, 0x9F46, 0x9F47, 0x9F48, 0x9F49, 0x9F4A, 0x9F4B, 0x9F4C, 0x9F4D, 0x9F4E, 0x9F4F, 0x9F52, 0x9F53, 0x9F54, 0x9F55, 0x9F56, 0x9F57, 0x9F58, 0x9F59, 0x9F5A, 0x9F5B, 0x9F5C, 0x9F5D, 0x9F5E, 0x9F5F, 0x9F60, 0x9F61, 0x9F62, 0x9F63, 0x9F64, 0x9F65, 0x9F66, 0x9F67, 0x9F68, 0x9F69, 0x9F6A, 0x9F6B, 0x9F6C, 0x9F6D, 0x9F6E, 0x9F6F, 0x9F70, 0x9F71, 0x9F72, 0x9F73, 0x9F74, 0x9F75, 0x9F76, 0x9F77, 0x9F78, 0x9F79, 0x9F7A, 0x9F7B, 0x9F7C, 0x9F7D, 0x9F7E, 0x9F81, 0x9F82, 0x9F8D, 0x9F8E, 0x9F8F, 0x9F90, 0x9F91, 0x9F92, 0x9F93, 0x9F94, 0x9F95, 0x9F96, 0x9F97, 0x9F98, 0x9F9C, 0x9F9D, 0x9F9E, 0x9FA1, 0x9FA2, 0x9FA3, 0x9FA4, 0x9FA5, 0xF92C, 0xF979, 0xF995, 0xF9E7, + 0xF9F1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xFA0C, 0xFA0D, 0xFA0E, 0xFA0F, 0xFA11, 0xFA13, 0xFA14, 0xFA18, 0xFA1F, 0xFA20, 0xFA21, 0xFA23, 0xFA24, 0xFA27, 0xFA28, 0xFA29, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +} diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 63b2ac4ec3..919581343b 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -9,6 +9,7 @@ #include "prx/libc/include/General.hpp" #include "JisX0208.hpp" #include "JisX0212.hpp" +#include "Cp936.hpp" #include "Cp949.hpp" #include "Cp950.hpp" @@ -145,10 +146,84 @@ bool isSjis(const void* profile) { return profile != nullptr && std::memcmp(profile, SJIS_TAG, sizeof(SJIS_TAG)) == 0; } +constexpr uint8_t EUC_CN_TAG[8] = {'E', 'U', 'C', 'C', 'N', '9', '3', '6'}; +constexpr uint8_t GBK_TAG[8] = {'G', 'B', 'K', 'C', 'P', '9', '3', '6'}; + +bool isEucCn(const void* profile) { + return profile != nullptr && std::memcmp(profile, EUC_CN_TAG, sizeof(EUC_CN_TAG)) == 0; +} + +bool isGbk(const void* profile) { + return profile != nullptr && std::memcmp(profile, GBK_TAG, sizeof(GBK_TAG)) == 0; +} + bool isSbc(const void* profile) { return profile == CP1252_PROFILE || profile == CP1251_PROFILE || profile == CP1250_PROFILE; } +int decodeGb(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + const uint8_t lead = mbcs[0]; + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + const bool gbk = isGbk(profile); + if (gbk && lead == 0x80) { + length = 1; + code = 0x20AC; + return 0; + } + if (lead == 0xFF || (!gbk && lead < 0xA1)) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t trail = mbcs[1]; + if (trail == 0xFF || (gbk ? trail < 0x40 || trail == 0x7F : trail < 0xA1)) return CES_ERROR_INVALID_ENCODE; + code = Cp936::TO_UCS2[(lead - 0x81) * 190 + (trail < 0x7F ? trail - 0x40 : trail - 0x41)]; + if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + return 0; +} + +const std::vector>& cp936Reverse() { + static const auto reverse = [] { + std::vector> entries; + for (uint32_t cell = 0; cell < 126 * 190; ++cell) { + if (Cp936::TO_UCS2[cell] != 0) entries.emplace_back(Cp936::TO_UCS2[cell], static_cast(cell)); + } + std::sort(entries.begin(), entries.end()); + return entries; + }(); + return reverse; +} + +int encodeGb(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length) { + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast(code); + return 0; + } + const bool gbk = isGbk(profile); + if (gbk && code == 0x20AC) { + length = 1; + mbcs[0] = 0x80; + return 0; + } + if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; + const auto& reverse = cp936Reverse(); + for (auto found = std::lower_bound(reverse.begin(), reverse.end(), std::pair(static_cast(code), 0)); + found != reverse.end() && found->first == code; ++found) { + const uint8_t lead = static_cast(0x81 + found->second / 190); + const uint32_t cell = found->second % 190; + const uint8_t trail = static_cast(cell < 0x3F ? 0x40 + cell : 0x41 + cell); + if (!gbk && (lead < 0xA1 || trail < 0xA1)) continue; + mbcs[0] = lead; + mbcs[1] = trail; + length = 2; + return 0; + } + return CES_ERROR_UNASSIGNED_CODE; +} + bool sjisToJis(uint8_t lead, uint8_t trail, int& row, int& cell) { if (!((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC))) return false; if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return false; @@ -196,7 +271,7 @@ bool isBig5(const void* profile) { } bool isMbcs(const void* profile) { - return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); + return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucCn(profile) || isGbk(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); } int decodeCp949(bool unified, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { @@ -291,6 +366,7 @@ int encodeDbcs(const std::vector& reverse, bool euc, uint32_t code, ui int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { if (isEucKr(profile) || isUhc(profile)) return decodeCp949(isUhc(profile), mbcs, mbcsMax, length, code); if (isBig5(profile)) return decodeCp950(mbcs, mbcsMax, length, code); + if (isEucCn(profile) || isGbk(profile)) return decodeGb(profile, mbcs, mbcsMax, length, code); const uint8_t lead = mbcs[0]; if (isSbc(profile)) { length = 1; @@ -397,6 +473,7 @@ const std::vector& jisReverse() { int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length) { if (isEucKr(profile) || isUhc(profile)) return encodeDbcs(cp949Reverse(), isEucKr(profile), code, mbcs, length); if (isBig5(profile)) return encodeDbcs(cp950Reverse(), false, code, mbcs, length); + if (isEucCn(profile) || isGbk(profile)) return encodeGb(profile, code, mbcs, length); if (isSbc(profile)) { length = 1; return unicodeToSbc(static_cast(profile), code, mbcs[0]) ? 0 : CES_ERROR_UNASSIGNED_CODE; @@ -564,7 +641,7 @@ int decodeUtf16(const uint16_t* utf16, uint32_t utf16max, uint32_t& length, uint return 0; } -int decodeJapanese(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, const void* target, uint32_t& length, uint32_t& code) { +int decodeProfiled(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, const void* target, uint32_t& length, uint32_t& code) { if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; if (!source) return CES_ERROR_INVALID_SRC_BUFFER; if (sourceMax == 0) return CES_ERROR_SRC_BUFFER_END; @@ -572,19 +649,19 @@ int decodeJapanese(bool (*matches)(const void*), const void* profile, const uint return decodeMbcs(profile, source, sourceMax, length, code); } -int japaneseToUcs2(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* ucs2) { +int profiledToUcs2(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* ucs2) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeJapanese(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return result; *ucs2 = static_cast(code); if (sourceSize) *sourceSize = length; return 0; } -int japaneseToUtf8(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len) { +int profiledToUtf8(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeJapanese(matches, profile, source, sourceMax, utf8, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf8, length, code); result != 0) return result; uint8_t encoded[3]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8max < produced) return CES_ERROR_DST_BUFFER_END; @@ -595,10 +672,10 @@ int japaneseToUtf8(bool (*matches)(const void*), const void* profile, const uint } template -int japaneseToUtf16(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { +int profiledToUtf16(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeJapanese(matches, profile, source, sourceMax, utf16, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf16, length, code); result != 0) return result; if (utf16max < 1) return CES_ERROR_DST_BUFFER_END; storeUnit(utf16, code); if (sourceSize) *sourceSize = length; @@ -607,16 +684,16 @@ int japaneseToUtf16(bool (*matches)(const void*), const void* profile, const uin } template -int japaneseToUtf32(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint32_t* utf32) { +int profiledToUtf32(bool (*matches)(const void*), const void* profile, const uint8_t* source, uint32_t sourceMax, uint32_t* sourceSize, uint32_t* utf32) { uint32_t length = 0; uint32_t code = 0; - if (const int result = decodeJapanese(matches, profile, source, sourceMax, utf32, length, code); result != 0) return result; + if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf32, length, code); result != 0) return result; storeUnit(utf32, code); if (sourceSize) *sourceSize = length; return 0; } -int encodeJapanese(const void* profile, uint32_t code, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { +int encodeProfiled(const void* profile, uint32_t code, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { uint8_t encoded[3]; uint32_t size = 0; if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; @@ -626,22 +703,22 @@ int encodeJapanese(const void* profile, uint32_t code, uint8_t* target, uint32_t return 0; } -int scalarToJapanese(bool (*matches)(const void*), uint32_t code, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { +int scalarToProfiled(bool (*matches)(const void*), uint32_t code, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; if (!target) return CES_ERROR_INVALID_DST_BUFFER; if (!isScalar(code)) return CES_ERROR_INVALID_ENCODE; - return encodeJapanese(profile, code, target, targetMax, targetSize); + return encodeProfiled(profile, code, target, targetMax, targetSize); } template -int utf32ToJapanese(bool (*matches)(const void*), const uint32_t* utf32, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { +int utf32ToProfiled(bool (*matches)(const void*), const uint32_t* utf32, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; if (!utf32) return CES_ERROR_INVALID_SRC_BUFFER; - return scalarToJapanese(matches, loadUnit(utf32), profile, target, targetMax, targetSize); + return scalarToProfiled(matches, loadUnit(utf32), profile, target, targetMax, targetSize); } template -int bufferToJapanese(bool (*matches)(const void*), const Source* source, uint32_t sourceMax, uint32_t* sourceLen, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize, Decode decode) { +int bufferToProfiled(bool (*matches)(const void*), const Source* source, uint32_t sourceMax, uint32_t* sourceLen, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize, Decode decode) { if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; if (!source) return CES_ERROR_INVALID_SRC_BUFFER; if (sourceMax == 0) return CES_ERROR_SRC_BUFFER_END; @@ -649,7 +726,7 @@ int bufferToJapanese(bool (*matches)(const void*), const Source* source, uint32_ uint32_t length = 0; uint32_t code = 0; if (const int result = decode(source, sourceMax, length, code); result != 0) return result; - if (const int result = encodeJapanese(profile, code, target, targetMax, targetSize); result != 0) return result; + if (const int result = encodeProfiled(profile, code, target, targetMax, targetSize); result != 0) return result; if (sourceLen) *sourceLen = length; return 0; } @@ -875,123 +952,123 @@ int APS5_VABI sceCesUhcToUtf8(void* profile, uint8_t* uhcBuffer, uint32_t uhcMax } int APS5_VABI sceCesSJisToUcs2(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* ucs2) { - return japaneseToUcs2(isSjis, profile, sjisBuffer, sjisMax, sjisSize, ucs2); + return profiledToUcs2(isSjis, profile, sjisBuffer, sjisMax, sjisSize, ucs2); } int APS5_VABI sceCesSJisToUtf8(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { - return japaneseToUtf8(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf8Buffer, utf8Max, utf8Length); + return profiledToUtf8(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf8Buffer, utf8Max, utf8Length); } int APS5_VABI sceCesSJisToUtf16(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return japaneseToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesSJisToUtf16be(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return japaneseToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesSJisToUtf16le(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return japaneseToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesSJisToUtf32(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint32_t* utf32) { - return japaneseToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); + return profiledToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); } int APS5_VABI sceCesSJisToUtf32be(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint32_t* utf32) { - return japaneseToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); + return profiledToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); } int APS5_VABI sceCesSJisToUtf32le(const void* profile, const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint32_t* utf32) { - return japaneseToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); + return profiledToUtf32(isSjis, profile, sjisBuffer, sjisMax, sjisSize, utf32); } int APS5_VABI sceCesEucJpToUtf16(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return japaneseToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesEucJpToUtf16be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return japaneseToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesEucJpToUtf16le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return japaneseToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isEucJp, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesEucJpToUtf32(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { - return japaneseToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); + return profiledToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); } int APS5_VABI sceCesEucJpToUtf32be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { - return japaneseToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); + return profiledToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); } int APS5_VABI sceCesEucJpToUtf32le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { - return japaneseToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); + return profiledToUtf32(isEucJp, profile, eucBuffer, eucMax, eucSize, utf32); } int APS5_VABI sceCesUcs2ToSJis(uint16_t ucs2, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return scalarToJapanese(isSjis, ucs2, profile, sjisBuffer, sjisMax, sjisSize); + return scalarToProfiled(isSjis, ucs2, profile, sjisBuffer, sjisMax, sjisSize); } int APS5_VABI sceCesUtf8ToSJis(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return bufferToJapanese(isSjis, utf8Buffer, utf8Max, utf8Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf8); + return bufferToProfiled(isSjis, utf8Buffer, utf8Max, utf8Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf8); } int APS5_VABI sceCesUtf16ToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return bufferToJapanese(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); + return bufferToProfiled(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); } int APS5_VABI sceCesUtf16beToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return bufferToJapanese(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); + return bufferToProfiled(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); } int APS5_VABI sceCesUtf16leToSJis(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return bufferToJapanese(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); + return bufferToProfiled(isSjis, utf16Buffer, utf16Max, utf16Length, profile, sjisBuffer, sjisMax, sjisSize, decodeUtf16); } int APS5_VABI sceCesUtf32ToSJis(uint32_t utf32, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return scalarToJapanese(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); + return scalarToProfiled(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); } int APS5_VABI sceCesUtf32beToSJis(const uint32_t* utf32, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return utf32ToJapanese(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); + return utf32ToProfiled(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); } int APS5_VABI sceCesUtf32leToSJis(const uint32_t* utf32, const void* profile, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { - return utf32ToJapanese(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); + return utf32ToProfiled(isSjis, utf32, profile, sjisBuffer, sjisMax, sjisSize); } int APS5_VABI sceCesUcs2ToEucJp(uint16_t ucs2, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return scalarToJapanese(isEucJp, ucs2, profile, eucBuffer, eucMax, eucSize); + return scalarToProfiled(isEucJp, ucs2, profile, eucBuffer, eucMax, eucSize); } int APS5_VABI sceCesUtf8ToEucJp(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToJapanese(isEucJp, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); + return bufferToProfiled(isEucJp, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); } int APS5_VABI sceCesUtf16ToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToJapanese(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); + return bufferToProfiled(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); } int APS5_VABI sceCesUtf16beToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToJapanese(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); + return bufferToProfiled(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); } int APS5_VABI sceCesUtf16leToEucJp(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return bufferToJapanese(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); + return bufferToProfiled(isEucJp, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); } int APS5_VABI sceCesUtf32ToEucJp(uint32_t utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return scalarToJapanese(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); + return scalarToProfiled(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); } int APS5_VABI sceCesUtf32beToEucJp(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return utf32ToJapanese(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); + return utf32ToProfiled(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); } int APS5_VABI sceCesUtf32leToEucJp(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { - return utf32ToJapanese(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); + return utf32ToProfiled(isEucJp, utf32, profile, eucBuffer, eucMax, eucSize); } int APS5_VABI sceCesSJisCodeToJisX0208(uint16_t sjisCode, uint8_t* ku, uint8_t* ten) { @@ -1109,4 +1186,144 @@ int APS5_VABI sceCesMbcsStrGetUtf32Len(void* context, const uint8_t* mbcs, uint3 return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf32Len, encodeUtf32); } +void* APS5_VABI sceCesUcsProfileInitEucCnGb2312(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_CN_TAG, sizeof(EUC_CN_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitGbkCp936(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, GBK_TAG, sizeof(GBK_TAG)); + return sheet; +} + +int APS5_VABI sceCesEucCnToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { + return profiledToUcs2(isEucCn, profile, eucBuffer, eucMax, eucSize, ucs2); +} + +int APS5_VABI sceCesEucCnToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + return profiledToUtf8(isEucCn, profile, eucBuffer, eucMax, eucSize, utf8Buffer, utf8Max, utf8Length); +} + +int APS5_VABI sceCesEucCnToUtf16(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return profiledToUtf16(isEucCn, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucCnToUtf16be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return profiledToUtf16(isEucCn, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucCnToUtf16le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return profiledToUtf16(isEucCn, profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucCnToUtf32(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return profiledToUtf32(isEucCn, profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucCnToUtf32be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return profiledToUtf32(isEucCn, profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucCnToUtf32le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return profiledToUtf32(isEucCn, profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesUcs2ToEucCn(uint16_t ucs2, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToProfiled(isEucCn, ucs2, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf8ToEucCn(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToProfiled(isEucCn, utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToEucCn(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToProfiled(isEucCn, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToEucCn(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToProfiled(isEucCn, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToEucCn(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return bufferToProfiled(isEucCn, utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToEucCn(uint32_t utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToProfiled(isEucCn, utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32beToEucCn(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToProfiled(isEucCn, utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32leToEucCn(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToProfiled(isEucCn, utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesGbToUcs2(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* ucs2) { + return profiledToUcs2(isGbk, profile, gbBuffer, gbMax, gbSize, ucs2); +} + +int APS5_VABI sceCesGbToUtf8(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + return profiledToUtf8(isGbk, profile, gbBuffer, gbMax, gbSize, utf8Buffer, utf8Max, utf8Length); +} + +int APS5_VABI sceCesGbToUtf16(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return profiledToUtf16(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesGbToUtf16be(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return profiledToUtf16(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesGbToUtf16le(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return profiledToUtf16(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesGbToUtf32(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { + return profiledToUtf32(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); +} + +int APS5_VABI sceCesGbToUtf32be(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { + return profiledToUtf32(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); +} + +int APS5_VABI sceCesGbToUtf32le(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { + return profiledToUtf32(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); +} + +int APS5_VABI sceCesUcs2ToGb(uint16_t ucs2, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return scalarToProfiled(isGbk, ucs2, profile, gbBuffer, gbMax, gbSize); +} + +int APS5_VABI sceCesUtf8ToGb(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return bufferToProfiled(isGbk, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToGb(uint32_t utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return scalarToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); +} + +int APS5_VABI sceCesUtf32beToGb(const uint32_t* utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return utf32ToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); +} + +int APS5_VABI sceCesUtf32leToGb(const uint32_t* utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { + return utf32ToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); +} + } diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index a4aa4d1291..9652548d10 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -37,46 +37,6 @@ int APS5_VABI sceCesBig5ToUtf32le(void) { return 0; } -int APS5_VABI sceCesEucCnToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucCnToUtf8(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesEucCnUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -127,46 +87,6 @@ int APS5_VABI sceCesEucKrUcsProfileSetSbcs(void) { return 0; } -int APS5_VABI sceCesGbToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesGbToUtf8(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesGbUcsProfileSet4ByteCharRange(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -602,21 +522,11 @@ int APS5_VABI sceCesUcs2ToBig5(void) { return 0; } -int APS5_VABI sceCesUcs2ToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUcs2ToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -627,11 +537,6 @@ int APS5_VABI sceCesUcs2ToUhc(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitEucCnGb2312(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -662,11 +567,6 @@ int APS5_VABI sceCesUcsProfileInitGb18030_2005(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitGbkCp936(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -752,21 +652,11 @@ int APS5_VABI sceCesUtf16ToBig5(void) { return 0; } -int APS5_VABI sceCesUtf16ToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf16ToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -782,21 +672,11 @@ int APS5_VABI sceCesUtf16beToBig5(void) { return 0; } -int APS5_VABI sceCesUtf16beToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf16beToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -812,21 +692,11 @@ int APS5_VABI sceCesUtf16leToBig5(void) { return 0; } -int APS5_VABI sceCesUtf16leToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf16leToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -852,21 +722,11 @@ int APS5_VABI sceCesUtf32ToBig5(void) { return 0; } -int APS5_VABI sceCesUtf32ToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf32ToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -882,21 +742,11 @@ int APS5_VABI sceCesUtf32beToBig5(void) { return 0; } -int APS5_VABI sceCesUtf32beToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf32beToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -912,21 +762,11 @@ int APS5_VABI sceCesUtf32leToBig5(void) { return 0; } -int APS5_VABI sceCesUtf32leToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf32leToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -952,21 +792,11 @@ int APS5_VABI sceCesUtf8ToBig5(void) { return 0; } -int APS5_VABI sceCesUtf8ToEucCn(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToEucKr(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesUtf8ToGb(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToMbc(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp new file mode 100644 index 0000000000..8e4f16cf5a --- /dev/null +++ b/core/libs/tests/GuestCesChinese.cpp @@ -0,0 +1,138 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +void* APS5_VABI sceCesUcsProfileInitEucCnGb2312(void*); +void* APS5_VABI sceCesUcsProfileInitGbkCp936(void*); +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +int APS5_VABI sceCesEucCnToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesEucCnToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucCnToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucCnToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesGbToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesGbToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesGbToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesGbToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUcs2ToEucCn(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToGb(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToEucCn(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToGb(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToGb(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToEucCn(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToGb(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } + +int main() { + constexpr int invalidParameter = static_cast(0x805C0001); + constexpr int invalidSrcBuffer = static_cast(0x805C0010); + constexpr int srcBufferEnd = static_cast(0x805C0011); + constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int unassignedCode = static_cast(0x805C0020); + constexpr int invalidDstBuffer = static_cast(0x805C0030); + constexpr int dstBufferEnd = static_cast(0x805C0031); + + alignas(8) std::uint8_t eucSheet[64] = {}; + alignas(8) std::uint8_t gbkSheet[64] = {}; + alignas(8) std::uint8_t sjisSheet[64] = {}; + const void* euc = sceCesUcsProfileInitEucCnGb2312(eucSheet); + const void* gbk = sceCesUcsProfileInitGbkCp936(gbkSheet); + const void* sjis = sceCesUcsProfileInitSJis1997Cp932(sjisSheet); + Require(euc == eucSheet && gbk == gbkSheet); + Require(sceCesUcsProfileInitEucCnGb2312(nullptr) == nullptr && sceCesUcsProfileInitGbkCp936(nullptr) == nullptr); + + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + std::uint16_t ucs2 = 0; + Require(sceCesEucCnToUcs2(euc, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); + Require(sceCesEucCnToUcs2(euc, Bytes("A"), 1, &consumed, &ucs2) == 0 && consumed == 1 && ucs2 == 'A'); + Require(sceCesEucCnToUcs2(euc, Bytes("\xA1\xA4"), 2, &consumed, &ucs2) == 0 && ucs2 == 0x00B7); + Require(sceCesEucCnToUcs2(euc, Bytes("\x80"), 1, &consumed, &ucs2) == invalidEncode); + Require(sceCesEucCnToUcs2(euc, Bytes("\x81\x40"), 2, &consumed, &ucs2) == invalidEncode); + Require(sceCesEucCnToUcs2(euc, Bytes("\xD6\x40"), 2, &consumed, &ucs2) == invalidEncode); + Require(sceCesEucCnToUcs2(euc, Bytes("\xD6\xD0"), 1, &consumed, &ucs2) == srcBufferEnd); + Require(sceCesEucCnToUcs2(euc, Bytes("\xAA\xA1"), 2, &consumed, &ucs2) == unassignedCode); + Require(sceCesEucCnToUcs2(gbk, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == invalidParameter); + Require(sceCesEucCnToUcs2(euc, nullptr, 2, &consumed, &ucs2) == invalidSrcBuffer); + Require(sceCesEucCnToUcs2(euc, Bytes("\xD6\xD0"), 2, &consumed, nullptr) == invalidDstBuffer); + + std::uint8_t utf8[4] = {}; + Require(sceCesEucCnToUtf8(euc, Bytes("\xCE\xC4"), 2, &consumed, utf8, sizeof(utf8), &produced) == 0); + Require(produced == 3 && std::memcmp(utf8, "\xE6\x96\x87", 3) == 0); + std::uint16_t utf16 = 0; + Require(sceCesEucCnToUtf16be(euc, Bytes("\xD6\xD0"), 2, &consumed, &utf16, 1, &produced) == 0 && std::memcmp(&utf16, "\x4E\x2D", 2) == 0); + std::uint32_t utf32 = 0; + Require(sceCesEucCnToUtf32(euc, Bytes("\xD6\xD0"), 2, &consumed, &utf32) == 0 && utf32 == 0x4E2D); + + Require(sceCesGbToUcs2(gbk, Bytes("\x80"), 1, &consumed, &ucs2) == 0 && consumed == 1 && ucs2 == 0x20AC); + Require(sceCesGbToUcs2(gbk, Bytes("\x81\x40"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E02); + Require(sceCesGbToUcs2(gbk, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && ucs2 == 0x4E2D); + Require(sceCesGbToUcs2(gbk, Bytes("\xFF"), 1, &consumed, &ucs2) == invalidEncode); + Require(sceCesGbToUcs2(gbk, Bytes("\x81\x7F"), 2, &consumed, &ucs2) == invalidEncode); + Require(sceCesGbToUcs2(gbk, Bytes("\x81\x3F"), 2, &consumed, &ucs2) == invalidEncode); + Require(sceCesGbToUcs2(euc, Bytes("\x81\x40"), 2, &consumed, &ucs2) == invalidParameter); + Require(sceCesGbToUcs2(sjis, Bytes("A"), 1, &consumed, &ucs2) == invalidParameter); + Require(sceCesGbToUtf8(gbk, Bytes("\x80"), 1, &consumed, utf8, sizeof(utf8), &produced) == 0); + Require(produced == 3 && std::memcmp(utf8, "\xE2\x82\xAC", 3) == 0); + Require(sceCesGbToUtf8(gbk, Bytes("\x80"), 1, &consumed, utf8, 2, &produced) == dstBufferEnd); + Require(sceCesGbToUtf16(gbk, Bytes("\x81\x40"), 2, &consumed, &utf16, 1, &produced) == 0 && utf16 == 0x4E02 && produced == 1); + Require(sceCesGbToUtf32le(gbk, Bytes("\x81\x40"), 2, &consumed, &utf32) == 0 && std::memcmp(&utf32, "\x02\x4E\x00\x00", 4) == 0); + + std::uint8_t mbcs[4] = {}; + Require(sceCesUcs2ToEucCn(0x4E2D, euc, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xD6\xD0", 2) == 0); + Require(sceCesUcs2ToEucCn(0x4E02, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesUcs2ToEucCn(0x20AC, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesUcs2ToEucCn(0x4E2D, euc, mbcs, 1, &produced) == dstBufferEnd); + Require(sceCesUcs2ToEucCn(0xDC00, euc, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToGb(0x4E02, gbk, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x81\x40", 2) == 0); + Require(sceCesUcs2ToGb(0x20AC, gbk, mbcs, sizeof(mbcs), &produced) == 0 && produced == 1 && mbcs[0] == 0x80); + Require(sceCesUcs2ToGb(0x3042, euc, mbcs, sizeof(mbcs), &produced) == invalidParameter); + Require(sceCesUcs2ToGb(0x3042, gbk, nullptr, sizeof(mbcs), &produced) == invalidDstBuffer); + Require(sceCesUtf8ToEucCn(Bytes("\xE6\x96\x87"), 3, &consumed, euc, mbcs, sizeof(mbcs), &produced) == 0); + Require(consumed == 3 && std::memcmp(mbcs, "\xCE\xC4", 2) == 0); + Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gbk, mbcs, sizeof(mbcs), &produced) == unassignedCode); + std::uint16_t zhongLe = 0; + std::memcpy(&zhongLe, "\x2D\x4E", 2); + Require(sceCesUtf16leToGb(&zhongLe, 1, &consumed, gbk, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\xD6\xD0", 2) == 0); + Require(sceCesUtf32ToEucCn(0x110000, euc, mbcs, sizeof(mbcs), &produced) == invalidEncode); + std::uint32_t zhongBe = 0; + std::memcpy(&zhongBe, "\x00\x00\x4E\x2D", 4); + Require(sceCesUtf32beToGb(&zhongBe, gbk, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\xD6\xD0", 2) == 0); + + int gbkCount = 0; + int eucCount = 0; + for (int code = 1; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::uint8_t encoded[4] = {}; + std::uint32_t size = 0; + std::uint16_t back = 0; + if (sceCesUcs2ToGb(static_cast(code), gbk, encoded, sizeof(encoded), &size) == 0) { + Require(sceCesGbToUcs2(gbk, encoded, size, &consumed, &back) == 0 && consumed == size && back == code); + ++gbkCount; + } + if (sceCesUcs2ToEucCn(static_cast(code), euc, encoded, sizeof(encoded), &size) == 0) { + Require(size == 1 || (encoded[0] >= 0xA1 && encoded[1] >= 0xA1)); + Require(sceCesEucCnToUcs2(euc, encoded, size, &consumed, &back) == 0 && consumed == size && back == code); + ++eucCount; + } + } + Require(gbkCount == 127 + 1 + 21791 && eucCount > 127 + 7000 && eucCount < gbkCount); + + alignas(8) std::uint8_t context[64] = {}; + Require(sceCesMbcsUcsContextInit(context, gbk) == 0); + std::uint8_t text[16] = {}; + Require(sceCesUtf8StrToMbcsStr(context, Bytes("A\xE4\xB8\xAD\xE2\x82\xAC"), 16, &consumed, text, sizeof(text), &produced) == 0); + Require(consumed == 7 && produced == 4 && std::memcmp(text, "A\xD6\xD0\x80", 4) == 0); + std::uint8_t back[16] = {}; + Require(sceCesMbcsStrToUtf8Str(context, text, produced, &consumed, back, sizeof(back), &produced) == 0); + Require(consumed == 4 && produced == 7 && std::memcmp(back, "A\xE4\xB8\xAD\xE2\x82\xAC", 7) == 0); + return 0; +} diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 7384c2ad94..30c5d3bdd9 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -12,6 +12,7 @@ - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units +- [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas, GB 18030 four-byte forms and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 195 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 161 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure From c020ce89346b31c3e8e0cf4d1c303ecb4ef598b5 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 10:57:01 +0200 Subject: [PATCH 0391/1360] feat(libSceCesCs): implement generic MBCS character, Shift-JIS code and JIS X 0213 helpers Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/CMakeLists.txt | 8 +- core/libs/prx/libSceCesCs/Export.cpp | 108 +++++++++++++ core/libs/prx/libSceCesCs/PendingExports.cpp | 65 -------- core/libs/tests/GuestCesMbc.cpp | 156 +++++++++++++++++++ docs/dev/debt/libSceCesCs.md | 3 +- 5 files changed, 271 insertions(+), 69 deletions(-) create mode 100644 core/libs/tests/GuestCesMbc.cpp diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 04182062c0..c59a762ef5 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -25,8 +25,10 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) - foreach(variant IN ITEMS unicode unicode_module japanese japanese_module wide wide_module chinese chinese_module) - if(variant MATCHES "^chinese") + foreach(variant IN ITEMS unicode unicode_module japanese japanese_module wide wide_module chinese chinese_module mbc mbc_module) + if(variant MATCHES "^mbc") + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesMbc.cpp) + elseif(variant MATCHES "^chinese") add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesChinese.cpp) elseif(variant MATCHES "^wide") add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesWide.cpp) @@ -44,7 +46,7 @@ if(BUILD_TESTING) add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) endforeach() if(WIN32) - set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module guest_ces_wide guest_ces_wide_module guest_ces_chinese guest_ces_chinese_module PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces_unicode guest_ces_unicode_module guest_ces_japanese guest_ces_japanese_module guest_ces_wide guest_ces_wide_module guest_ces_chinese guest_ces_chinese_module guest_ces_mbc guest_ces_mbc_module PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 919581343b..917d51492a 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -247,6 +248,12 @@ void jisToSjis(int row, int cell, uint8_t& lead, uint8_t& trail) { } } +constexpr uint8_t JIS_X0213_PLANE2_ROWS[26] = {1, 8, 3, 4, 5, 12, 13, 14, 15, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94}; + +bool isSjisSingle(uint8_t byte) { + return byte < 0x80 || (byte >= 0xA1 && byte <= 0xDF); +} + const void* contextProfile(const void* context) { const void* profile = nullptr; std::memcpy(&profile, context, sizeof(profile)); @@ -823,6 +830,13 @@ int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { return 0; } +int APS5_VABI sceCesMbcsUcsContextInitCopy(void* context, const void* source) { + if (!context || !source) return CES_ERROR_INVALID_PARAMETER; + const void* profile = contextProfile(source); + std::memcpy(context, &profile, sizeof(profile)); + return 0; +} + void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void* sheet) { if (!sheet) return nullptr; std::memcpy(sheet, EUC_JP_TAG, sizeof(EUC_JP_TAG)); @@ -1091,6 +1105,100 @@ int APS5_VABI sceCesJisX0208ToSJisCode(uint8_t ku, uint8_t ten, uint16_t* sjisCo return 0; } +int APS5_VABI sceCesSJisCodeToJisX0213(uint16_t sjisCode, uint8_t* men, uint8_t* ku, uint8_t* ten) { + if (!men || !ku || !ten) return CES_ERROR_INVALID_DST_BUFFER; + int row = 0; + int cell = 0; + if (!sjisToJis(static_cast(sjisCode >> 8), static_cast(sjisCode), row, cell)) return CES_ERROR_INVALID_ENCODE; + *men = row <= 94 ? 1 : 2; + *ku = row <= 94 ? static_cast(row) : JIS_X0213_PLANE2_ROWS[row - 95]; + *ten = static_cast(cell); + return 0; +} + +int APS5_VABI sceCesJisX0213ToSJisCode(uint8_t men, uint8_t ku, uint8_t ten, uint16_t* sjisCode) { + if (!sjisCode) return CES_ERROR_INVALID_DST_BUFFER; + if (men < 1 || men > 2 || ku < 1 || ku > 94 || ten < 1 || ten > 94) return CES_ERROR_INVALID_ENCODE; + int row = ku; + if (men == 2) { + const auto* found = std::find(std::begin(JIS_X0213_PLANE2_ROWS), std::end(JIS_X0213_PLANE2_ROWS), ku); + if (found == std::end(JIS_X0213_PLANE2_ROWS)) return CES_ERROR_INVALID_ENCODE; + row = 95 + static_cast(found - std::begin(JIS_X0213_PLANE2_ROWS)); + } + uint8_t lead = 0; + uint8_t trail = 0; + jisToSjis(row, ten, lead, trail); + *sjisCode = static_cast(lead << 8 | trail); + return 0; +} + +int APS5_VABI sceCesSJisGetCode(const uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize, uint16_t* sjisCode) { + if (!sjisBuffer) return CES_ERROR_INVALID_SRC_BUFFER; + if (sjisMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!sjisCode) return CES_ERROR_INVALID_DST_BUFFER; + const uint8_t lead = sjisBuffer[0]; + if (isSjisSingle(lead)) { + *sjisCode = lead; + if (sjisSize) *sjisSize = 1; + return 0; + } + if (!((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC))) return CES_ERROR_INVALID_ENCODE; + if (sjisMax < 2) return CES_ERROR_SRC_BUFFER_END; + int row = 0; + int cell = 0; + if (!sjisToJis(lead, sjisBuffer[1], row, cell)) return CES_ERROR_INVALID_ENCODE; + *sjisCode = static_cast(lead << 8 | sjisBuffer[1]); + if (sjisSize) *sjisSize = 2; + return 0; +} + +int APS5_VABI sceCesSJisPutCode(uint16_t sjisCode, uint8_t* sjisBuffer, uint32_t sjisMax, uint32_t* sjisSize) { + if (!sjisBuffer) return CES_ERROR_INVALID_DST_BUFFER; + const uint8_t lead = static_cast(sjisCode >> 8); + const uint8_t trail = static_cast(sjisCode); + int row = 0; + int cell = 0; + const uint32_t size = lead == 0 ? 1 : 2; + if (size == 1 ? !isSjisSingle(trail) : !sjisToJis(lead, trail, row, cell)) return CES_ERROR_INVALID_ENCODE; + if (sjisMax < size) return CES_ERROR_DST_BUFFER_END; + if (size == 2) sjisBuffer[0] = lead; + sjisBuffer[size - 1] = trail; + if (sjisSize) *sjisSize = size; + return 0; +} + +int APS5_VABI sceCesMbcToUcs2(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint16_t* ucs2) { + return profiledToUcs2(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, ucs2); +} + +int APS5_VABI sceCesMbcToUtf8(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Len) { + return profiledToUtf8(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf8Buffer, utf8Max, utf8Len); +} + +int APS5_VABI sceCesMbcToUtf16(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len) { + return profiledToUtf16(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf16Buffer, utf16Max, utf16Len); +} + +int APS5_VABI sceCesMbcToUtf16be(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len) { + return profiledToUtf16(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf16Buffer, utf16Max, utf16Len); +} + +int APS5_VABI sceCesMbcToUtf16le(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len) { + return profiledToUtf16(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf16Buffer, utf16Max, utf16Len); +} + +int APS5_VABI sceCesMbcToUtf32(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint32_t* utf32) { + return profiledToUtf32(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf32); +} + +int APS5_VABI sceCesMbcToUtf32be(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint32_t* utf32) { + return profiledToUtf32(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf32); +} + +int APS5_VABI sceCesMbcToUtf32le(const void* profile, const uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen, uint32_t* utf32) { + return profiledToUtf32(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf32); +} + int APS5_VABI sceCesSbcToUcs2(const uint8_t* profile, uint8_t sbc, uint16_t* ucs2) { uint32_t code = 0; if (const int result = sbcToUnit(profile, sbc, ucs2, code); result != 0) return result; diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 9652548d10..efadff0563 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -162,61 +162,11 @@ int APS5_VABI sceCesJisGetLevel(void) { return 0; } -int APS5_VABI sceCesJisX0213ToSJisCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesJiscsToUcs(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesMbcToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcToUtf8(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesMbcsUcsContextInitCopy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesMbcsUcsSetMbcsReplacementCharCode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -487,21 +437,6 @@ int APS5_VABI sceCesRefersUcsProfileKsX1003Tilde0x7e(void) { return 0; } -int APS5_VABI sceCesSJisCodeToJisX0213(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisGetCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesSJisPutCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesSJisUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesMbc.cpp b/core/libs/tests/GuestCesMbc.cpp new file mode 100644 index 0000000000..d586891262 --- /dev/null +++ b/core/libs/tests/GuestCesMbc.cpp @@ -0,0 +1,156 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +void* APS5_VABI sceCesUcsProfileInitGbkCp936(void*); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +int APS5_VABI sceCesMbcsUcsContextInitCopy(void*, const void*); +int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesMbcToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesMbcToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesMbcToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesSJisGetCode(const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesSJisPutCode(std::uint16_t, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesSJisCodeToJisX0213(std::uint16_t, std::uint8_t*, std::uint8_t*, std::uint8_t*); +int APS5_VABI sceCesJisX0213ToSJisCode(std::uint8_t, std::uint8_t, std::uint8_t, std::uint16_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } + +static void RequireX0213(std::uint16_t sjis, std::uint8_t men, std::uint8_t ku, std::uint8_t ten) { + std::uint8_t m = 0; + std::uint8_t k = 0; + std::uint8_t t = 0; + Require(sceCesSJisCodeToJisX0213(sjis, &m, &k, &t) == 0 && m == men && k == ku && t == ten); + std::uint16_t back = 0; + Require(sceCesJisX0213ToSJisCode(men, ku, ten, &back) == 0 && back == sjis); +} + +int main() { + constexpr int invalidParameter = static_cast(0x805C0001); + constexpr int invalidSrcBuffer = static_cast(0x805C0010); + constexpr int srcBufferEnd = static_cast(0x805C0011); + constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int unassignedCode = static_cast(0x805C0020); + constexpr int invalidDstBuffer = static_cast(0x805C0030); + constexpr int dstBufferEnd = static_cast(0x805C0031); + + alignas(8) std::uint8_t sjisSheet[64] = {}; + alignas(8) std::uint8_t eucSheet[64] = {}; + alignas(8) std::uint8_t gbkSheet[64] = {}; + alignas(8) std::uint8_t blankSheet[64] = {}; + const void* sjis = sceCesUcsProfileInitSJis1997Cp932(sjisSheet); + const void* euc = sceCesUcsProfileInitEucJpCp51932(eucSheet); + const void* gbk = sceCesUcsProfileInitGbkCp936(gbkSheet); + const void* cp1252 = sceCesRefersUcsProfileCp1252(); + + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + std::uint16_t ucs2 = 0; + Require(sceCesMbcToUcs2(sjis, Bytes("\x82\xA0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x3042); + Require(sceCesMbcToUcs2(sjis, Bytes("\xB1"), 1, &consumed, &ucs2) == 0 && consumed == 1 && ucs2 == 0xFF71); + Require(sceCesMbcToUcs2(euc, Bytes("\xA4\xA2"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x3042); + Require(sceCesMbcToUcs2(gbk, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); + Require(sceCesMbcToUcs2(cp1252, Bytes("\x80"), 1, &consumed, &ucs2) == 0 && consumed == 1 && ucs2 == 0x20AC); + Require(sceCesMbcToUcs2(nullptr, Bytes("A"), 1, &consumed, &ucs2) == invalidParameter); + Require(sceCesMbcToUcs2(blankSheet, Bytes("A"), 1, &consumed, &ucs2) == invalidParameter); + Require(sceCesMbcToUcs2(sjis, nullptr, 1, &consumed, &ucs2) == invalidSrcBuffer); + Require(sceCesMbcToUcs2(sjis, Bytes("A"), 0, &consumed, &ucs2) == srcBufferEnd); + Require(sceCesMbcToUcs2(sjis, Bytes("A"), 1, &consumed, nullptr) == invalidDstBuffer); + Require(sceCesMbcToUcs2(sjis, Bytes("\x82"), 1, &consumed, &ucs2) == srcBufferEnd); + Require(sceCesMbcToUcs2(sjis, Bytes("\x80"), 1, &consumed, &ucs2) == invalidEncode); + Require(sceCesMbcToUcs2(sjis, Bytes("\xF0\x40"), 2, &consumed, &ucs2) == unassignedCode); + + std::uint8_t utf8[4] = {}; + Require(sceCesMbcToUtf8(gbk, Bytes("\xD6\xD0"), 2, &consumed, utf8, sizeof(utf8), &produced) == 0 && consumed == 2 && produced == 3); + Require(std::memcmp(utf8, "\xE4\xB8\xAD", 3) == 0); + Require(sceCesMbcToUtf8(gbk, Bytes("\xD6\xD0"), 2, &consumed, utf8, 2, &produced) == dstBufferEnd); + + std::uint16_t utf16 = 0; + Require(sceCesMbcToUtf16(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf16, 1, &produced) == 0 && produced == 1 && utf16 == 0x3042); + Require(sceCesMbcToUtf16le(euc, Bytes("\xA4\xA2"), 2, &consumed, &utf16, 1, &produced) == 0 && std::memcmp(&utf16, "\x42\x30", 2) == 0); + Require(sceCesMbcToUtf16be(euc, Bytes("\xA4\xA2"), 2, &consumed, &utf16, 1, &produced) == 0 && std::memcmp(&utf16, "\x30\x42", 2) == 0); + Require(sceCesMbcToUtf16(sjis, Bytes("\x82\xA0"), 2, &consumed, &utf16, 0, &produced) == dstBufferEnd); + + std::uint32_t utf32 = 0; + Require(sceCesMbcToUtf32(gbk, Bytes("A"), 1, &consumed, &utf32) == 0 && consumed == 1 && utf32 == 'A'); + Require(sceCesMbcToUtf32le(gbk, Bytes("\xD6\xD0"), 2, &consumed, &utf32) == 0 && std::memcmp(&utf32, "\x2D\x4E\x00\x00", 4) == 0); + Require(sceCesMbcToUtf32be(gbk, Bytes("\xD6\xD0"), 2, &consumed, &utf32) == 0 && std::memcmp(&utf32, "\x00\x00\x4E\x2D", 4) == 0); + + std::uint16_t code = 0; + std::uint32_t size = 0; + Require(sceCesSJisGetCode(Bytes("\x82\xA0"), 2, &size, &code) == 0 && size == 2 && code == 0x82A0); + Require(sceCesSJisGetCode(Bytes("A\x82"), 2, &size, &code) == 0 && size == 1 && code == 'A'); + Require(sceCesSJisGetCode(Bytes("\xDF"), 1, &size, &code) == 0 && size == 1 && code == 0xDF); + Require(sceCesSJisGetCode(Bytes("\xFC\xFC"), 2, nullptr, &code) == 0 && code == 0xFCFC); + Require(sceCesSJisGetCode(Bytes("\x82"), 1, &size, &code) == srcBufferEnd); + Require(sceCesSJisGetCode(Bytes("\x80"), 1, &size, &code) == invalidEncode); + Require(sceCesSJisGetCode(Bytes("\xA0"), 1, &size, &code) == invalidEncode); + Require(sceCesSJisGetCode(Bytes("\xFD\x40"), 2, &size, &code) == invalidEncode); + Require(sceCesSJisGetCode(Bytes("\x82\x7F"), 2, &size, &code) == invalidEncode); + Require(sceCesSJisGetCode(nullptr, 2, &size, &code) == invalidSrcBuffer); + Require(sceCesSJisGetCode(Bytes("A"), 0, &size, &code) == srcBufferEnd); + Require(sceCesSJisGetCode(Bytes("A"), 1, &size, nullptr) == invalidDstBuffer); + + std::uint8_t sjisOut[2] = {}; + Require(sceCesSJisPutCode(0x82A0, sjisOut, 2, &size) == 0 && size == 2 && sjisOut[0] == 0x82 && sjisOut[1] == 0xA0); + Require(sceCesSJisPutCode('A', sjisOut, 1, &size) == 0 && size == 1 && sjisOut[0] == 'A'); + Require(sceCesSJisPutCode(0xB1, sjisOut, 1, nullptr) == 0 && sjisOut[0] == 0xB1); + Require(sceCesSJisPutCode(0x82A0, sjisOut, 1, &size) == dstBufferEnd); + Require(sceCesSJisPutCode('A', sjisOut, 0, &size) == dstBufferEnd); + Require(sceCesSJisPutCode(0x80, sjisOut, 2, &size) == invalidEncode); + Require(sceCesSJisPutCode(0x0141, sjisOut, 2, &size) == invalidEncode); + Require(sceCesSJisPutCode(0x827F, sjisOut, 2, &size) == invalidEncode); + Require(sceCesSJisPutCode(0x82A0, nullptr, 2, &size) == invalidDstBuffer); + + RequireX0213(0x8140, 1, 1, 1); + RequireX0213(0x889F, 1, 16, 1); + RequireX0213(0xEFFC, 1, 94, 94); + RequireX0213(0xF040, 2, 1, 1); + RequireX0213(0xF09F, 2, 8, 1); + RequireX0213(0xF140, 2, 3, 1); + RequireX0213(0xF19F, 2, 4, 1); + RequireX0213(0xF240, 2, 5, 1); + RequireX0213(0xF29F, 2, 12, 1); + RequireX0213(0xF340, 2, 13, 1); + RequireX0213(0xF39F, 2, 14, 1); + RequireX0213(0xF440, 2, 15, 1); + RequireX0213(0xF49F, 2, 78, 1); + RequireX0213(0xF540, 2, 79, 1); + RequireX0213(0xFCFC, 2, 94, 94); + std::uint8_t men = 0; + std::uint8_t ku = 0; + std::uint8_t ten = 0; + Require(sceCesSJisCodeToJisX0213(0x8040, &men, &ku, &ten) == invalidEncode); + Require(sceCesSJisCodeToJisX0213(0xFD40, &men, &ku, &ten) == invalidEncode); + Require(sceCesSJisCodeToJisX0213(0x827F, &men, &ku, &ten) == invalidEncode); + Require(sceCesSJisCodeToJisX0213(0x82A0, nullptr, &ku, &ten) == invalidDstBuffer); + Require(sceCesJisX0213ToSJisCode(2, 2, 1, &code) == invalidEncode); + Require(sceCesJisX0213ToSJisCode(2, 77, 1, &code) == invalidEncode); + Require(sceCesJisX0213ToSJisCode(3, 1, 1, &code) == invalidEncode); + Require(sceCesJisX0213ToSJisCode(0, 1, 1, &code) == invalidEncode); + Require(sceCesJisX0213ToSJisCode(1, 95, 1, &code) == invalidEncode); + Require(sceCesJisX0213ToSJisCode(1, 1, 0, &code) == invalidEncode); + Require(sceCesJisX0213ToSJisCode(1, 1, 1, nullptr) == invalidDstBuffer); + + alignas(8) std::uint8_t context[64] = {}; + alignas(8) std::uint8_t copy[64] = {}; + Require(sceCesMbcsUcsContextInit(context, gbk) == 0); + Require(sceCesMbcsUcsContextInitCopy(copy, context) == 0); + Require(sceCesMbcsStrToUtf8Str(copy, Bytes("\xD6\xD0"), 2, &consumed, utf8, sizeof(utf8), &produced) == 0 && consumed == 2 && produced == 3); + Require(std::memcmp(utf8, "\xE4\xB8\xAD", 3) == 0); + Require(sceCesMbcsUcsContextInitCopy(nullptr, context) == invalidParameter); + Require(sceCesMbcsUcsContextInitCopy(copy, nullptr) == invalidParameter); +} diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 30c5d3bdd9..9a8d0e232c 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -14,5 +14,6 @@ - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas, GB 18030 four-byte forms and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 161 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 148 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure From 4fb70223ddeb01ddd5cdf6a26c706e7be166fc63 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 15:26:05 +0200 Subject: [PATCH 0392/1360] fix(libSceNgs2.native): report one ATRAC9 frame as the parsed decoder delay --- core/libs/prx/libSceNgs2.native/src/Atrac9.cpp | 2 +- core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp | 10 ++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 6c08a090c1..51bf111bcb 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -333,7 +333,7 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) { info.num_audio_unit_per_frame = static_cast(codec.framesInSuperframe); info.audio_frame_size = static_cast(codec.superframeSize); info.num_audio_frame_samples = static_cast(codec.frameSamples * codec.framesInSuperframe); - info.num_delay_samples = ReadLe32(chunks.fact + 4); + info.num_delay_samples = static_cast(codec.frameSamples); info.num_blocks = 1; info.block[0].data_offset = chunks.dataOffset; info.block[0].data_size = chunks.dataSize; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index eed5eaf1f1..94a3a66ab7 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -174,6 +175,15 @@ static void TestParse() { Require(info.num_audio_unit_per_frame == 4 && info.audio_frame_size == SuperframeBytes && info.num_audio_frame_samples == SuperframeSamples); Require(info.num_blocks == 1 && info.block[0].data_offset == info.data_offset && info.block[0].data_size == SuperframeBytes); Require(info.block[0].num_skip_samples == 256 && info.block[0].num_samples == 900); + for (const std::uint32_t factDelay : {0u, 1u, 900u, 0xffffffffu}) { + auto other = file; + constexpr char factTag[] = {'f', 'a', 'c', 't'}; + const auto fact = static_cast(std::search(other.begin(), other.end(), std::begin(factTag), std::end(factTag)) - other.begin()); + std::memcpy(other.data() + fact + 12, &factDelay, sizeof(factDelay)); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(other.data(), other.size(), &parsed) == SCE_NGS2_OK); + Require(parsed.num_delay_samples == 256 && parsed.block[0].num_skip_samples == 256 && parsed.num_samples == 900); + } Require(sceNgs2ParseWaveformData(file.data(), 11, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); Require(sceNgs2ParseWaveformData(Superframe, SuperframeBytes, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..112355b02c 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -8,6 +8,7 @@ - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. - [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. +- [NGS2 ATRAC9 parsed delay](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reports one codec frame as `num_delay_samples` whatever the `fact` chunk's delay word holds, as [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) sets it for ATRAC9 (its [test](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/tests/hle/test_ngs2_waveform.cpp#L326-L342) expects 256 for a `fact` of 900 and 0). Not measured on a console. The block skip still comes from the `fact` chunk's third word, where prosper uses the same one-frame delay - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. - [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour. From 61fca3ae9b0fb38b5aadcc2b001f8f70475347fb Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:28:50 +0200 Subject: [PATCH 0393/1360] fix(libSceUlt): keep the reader-writer lock state alive until sceUltReaderWriterLockDestroy has unlocked it Destroy locked the state's mutex through a reference into the map entry and then erased the entry. When that held the last shared_ptr, the state was freed before the lock guard unlocked its mutex. Take a shared_ptr copy before the guard so the state outlives it. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp index 7e1273394a..c947260041 100644 --- a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -107,7 +107,8 @@ int APS5_VABI sceUltReaderWriterLockDestroy(void* rwlock) { if (it == gLocks.end()) { return ULT_ERROR_STATE; } - auto& state = *it->second; + const auto owner = it->second; + auto& state = *owner; std::lock_guard stateLock(state._mutex); if (state._readers != 0 || state._writing || state._waiters != 0) { return ULT_ERROR_BUSY; From 2e706ed2e1b0c5507c8a572e5ffa107fb6c2ea78 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 15:28:54 +0200 Subject: [PATCH 0394/1360] feat(libSceNgs2.native): map VAG waveform blocks in sceNgs2CalcWaveformBlock --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 18 +++++++++++++++- .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 21 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 6c08a090c1..35052a1e6b 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -26,6 +26,7 @@ static constexpr std::uint32_t MAX_SAMPLE_RATE = 192000; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_FIRST = 0x10; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_LAST = 0x1b; static constexpr std::uint32_t WAVEFORM_TYPE_VAG = 0x1c; +static constexpr std::uint32_t VAG_FRAME_SAMPLES = 28; static constexpr std::uint32_t PCM_BYTES_PER_SAMPLE[] = {1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8}; static bool IsPcmType(std::uint32_t type) { @@ -43,6 +44,18 @@ static Ngs2WaveformBlock PcmBlock(std::uint32_t type, std::uint32_t channels, st return block; } +static Ngs2WaveformBlock VagBlock(std::uint32_t channels, std::uint32_t pos, std::uint32_t count) { + const std::uint32_t frameBytes = 16u * channels; + const std::uint32_t first = pos / VAG_FRAME_SAMPLES; + const std::uint32_t past = (pos + count + VAG_FRAME_SAMPLES - 1u) / VAG_FRAME_SAMPLES; + const auto offset = static_cast(static_cast(first) * frameBytes); + Ngs2WaveformBlock block{}; + block.data_offset = offset; + block.data_size = static_cast(past * frameBytes - offset); + block.num_samples = (past - first) * VAG_FRAME_SAMPLES; + return block; +} + void Ngs2Atrac9DecoderDeleter::operator()(void* handle) const { Atrac9ReleaseHandle(handle); } @@ -432,7 +445,10 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ *block = PcmBlock(type, format->num_channels, sample_pos, num_samples); return SCE_NGS2_OK; } - if (type == WAVEFORM_TYPE_VAG) throw std::runtime_error("NGS2: waveform blocks of type " + Ngs2Hex(type) + " are not implemented"); + if (type == WAVEFORM_TYPE_VAG) { + *block = VagBlock(format->num_channels, sample_pos, num_samples); + return SCE_NGS2_OK; + } if (type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; if (format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index eed5eaf1f1..d056c6a8c1 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -229,6 +229,26 @@ static void TestCalcPcmBlock() { Require(sceNgs2CalcWaveformBlock(nullptr, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT && block.data_size == 0 && block.num_samples == 0); } +static void RequireVagBlock(std::uint32_t channels, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size, std::uint32_t samples) { + const Ngs2WaveformFormat format{0x1c, channels, 48000, 0, 0, 0}; + Ngs2WaveformBlock block; + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(&format, pos, count, &block) == SCE_NGS2_OK); + Require(block.data_offset == offset && block.data_size == size && block.num_samples == samples); + Require(block.num_repeats == 0 && block.num_skip_samples == 0 && block.reserved == 0 && block.user_data == 0); +} + +static void TestCalcVagBlock() { + RequireVagBlock(1, 0, 28, 0, 16, 28); + RequireVagBlock(1, 27, 2, 0, 32, 56); + RequireVagBlock(1, 28, 28, 16, 16, 28); + RequireVagBlock(2, 30, 1, 32, 32, 28); + RequireVagBlock(6, 56, 57, 192, 288, 84); + RequireVagBlock(1, 0, 0, 0, 0, 0); + RequireVagBlock(1, 5, 0, 0, 16, 28); + RequireVagBlock(0, 100, 50, 0, 0, 84); +} + static void Set32(std::vector& bytes, std::size_t offset, std::uint32_t value) { for (int i = 0; i < 4; ++i) bytes[offset + i] = static_cast(value >> (i * 8)); } @@ -539,6 +559,7 @@ int main() { TestMalformedLoops(); TestCalcBlock(); TestCalcPcmBlock(); + TestCalcVagBlock(); TestCorruptSuperframeAtPageEnd(); TestStreamPartitions(); TestRefill(reference); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..732be4ae80 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -36,7 +36,7 @@ - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Unknown flags, zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified -- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - VAG (0x1c) throws. ATRAC9 keeps AnyPS5's own mapping: it checks the config against the channels and rate (0x804A8431), leaves out the decoder delay from the skip and size, leaves both 0 for zero samples and computes the size in 64 bits, while [prosper reads the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) without the check, with the delay, filling both and modulo 2^32. Linear PCM and float (0x10 to 0x1b), the null checks and the 0x804A8432 of other types follow that reading and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466). Closes when: VAG is mapped and ATRAC9 follows the module +- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - ATRAC9 keeps AnyPS5's own mapping: it checks the config against the channels and rate (0x804A8431), leaves out the decoder delay from the skip and size, leaves both 0 for zero samples and computes the size in 64 bits, while [prosper reads the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) without the check, with the delay, filling both and modulo 2^32. Linear PCM and float (0x10 to 0x1b), VAG (0x1c: whole 28-sample frames of 16 bytes per channel, the frame-rounded sample count and no skip), the null checks and the 0x804A8432 of other types follow that reading and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466). Closes when: ATRAC9 follows the module ### Functional From 9f5640af66e4973d9292b7010bb24b1d1beff291 Mon Sep 17 00:00:00 2001 From: KernelPanicNT Date: Tue, 20 Oct 2026 17:18:29 +0000 Subject: [PATCH 0395/1360] fix(libSceNgs2): return the custom rack creation errors sceNgs2RackCreate and sceNgs2RackCreateWithAllocator now return the firmware's errors for a custom rack whose buffer count is outside 1 to 4 or below the submixer's max_inputs, whose module buffer id is at or above the count, or whose submixer has more than 37 channels, and for the state sizes of the Gain, Reverb and PitchShift modules, the Reverb option size and word, and the PitchShift option word. Racks that pass these checks with one of these modules or Convolution, with more than one buffer, more than 8 channels or a submixer max_inputs other than 1 or 2 still throw, and so does sceNgs2RackQueryBufferSize for the rejected options. --- .../libSceNgs2.native/include/Ngs2Types.hpp | 17 +++ .../libs/prx/libSceNgs2.native/src/Custom.cpp | 55 ++++++- .../libSceNgs2.native/src/Ngs2Internal.hpp | 2 +- core/libs/prx/libSceNgs2.native/src/Rack.cpp | 39 +++-- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 144 ++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 5 +- 6 files changed, 243 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index a604adba3d..f10d57afb0 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -17,6 +17,13 @@ static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0 static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); +static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast(0x804A8013); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast(0x804A8052); +static constexpr int SCE_NGS2_ERROR_INVALID_PITCH_SHIFT_OPTION = static_cast(0x804A8098); +static constexpr int SCE_NGS2_ERROR_INVALID_REVERB_OPTION = static_cast(0x804A8099); +static constexpr int SCE_NGS2_ERROR_INVALID_STATE_SIZE = static_cast(0x804A8302); +static constexpr int SCE_NGS2_ERROR_INVALID_BUFFER_INDEX = static_cast(0x804A8306); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_BUFFERS = static_cast(0x804A830B); static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; @@ -72,9 +79,19 @@ 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_MAX_BUFFERS = 4; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS = 37; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS = 2; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE = 12; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE = 8; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE = 8; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_GAIN = 0x15; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_REVERB = 0x1b; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x1d; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT = 0x20; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION = 0x23; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND = 2; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index d3e586a4d9..f4e682173b 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -12,28 +12,71 @@ 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) { - if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); +static const char* BuiltInModuleName(std::uint32_t moduleId) { + switch (moduleId) { + case SCE_NGS2_CUSTOM_MODULE_ID_GAIN: return "Gain"; + case SCE_NGS2_CUSTOM_MODULE_ID_REVERB: return "Reverb"; + case SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT: return "PitchShift"; + case SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION: return "Convolution"; + default: return nullptr; + } +} + +static std::uint32_t OptionWord(const Ngs2CustomModuleOption& option) { + std::uint32_t word = 0; + std::memcpy(&word, reinterpret_cast(&option) + sizeof(option), sizeof(word)); + return word; +} + +static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { + const auto stateSizeIs = [&info](std::uint32_t size) { return info.state_size == 0 || info.state_size == size; }; + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { + if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_REVERB) { + if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; + if (info.option != nullptr && info.option->size != SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE) return SCE_NGS2_ERROR_INVALID_OPTION_SIZE; + if (info.option != nullptr && OptionWord(*info.option) > 1) return SCE_NGS2_ERROR_INVALID_REVERB_OPTION; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT) { + if (!stateSizeIs(SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; + if (info.option != nullptr && info.option->size == SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE && OptionWord(*info.option) != 0) { + return SCE_NGS2_ERROR_INVALID_PITCH_SHIFT_OPTION; + } + } + return SCE_NGS2_OK; +} + +int Ngs2CheckCustomRack(const Ngs2CustomRackOption& option, std::uint32_t maxInputs) { + if (option.num_buffers == 0 || option.num_buffers > SCE_NGS2_CUSTOM_MAX_BUFFERS || option.num_buffers < maxInputs) { + return SCE_NGS2_ERROR_INVALID_NUM_BUFFERS; + } 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) { 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; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; - if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { - if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX; - } else { + } else if (BuiltInModuleName(info.module_id) == nullptr) { throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); } + if (info.source_buffer_id >= option.num_buffers || info.extra_buffer_id >= option.num_buffers || info.dest_buffer_id >= option.num_buffers) { + return SCE_NGS2_ERROR_INVALID_BUFFER_INDEX; + } + const int result = CheckBuiltInModule(info); + if (result != SCE_NGS2_OK) return result; + } + if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); + for (std::uint32_t m = 0; m < option.num_modules; m++) { + const char* name = BuiltInModuleName(option.module[m].module_id); + if (name != nullptr) throw std::runtime_error(std::string("NGS2: the custom ") + name + " module is not implemented"); } 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; } + return SCE_NGS2_OK; } static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, std::uint32_t voiceIndex) { diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index adc9f3d665..72fa69c3ce 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -229,7 +229,7 @@ std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); bool Ngs2FinishBlock(Ngs2Voice& voice); -void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option); +int Ngs2CheckCustomRack(const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); void Ngs2CleanupUserFx(Ngs2Rack& rack); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 47897d3215..93072c82fb 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -87,22 +87,35 @@ static std::uint32_t RackMaxChannels(std::uint32_t rackId, const RackOptions& op return NGS2_MAX_CHANNELS; } -static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* option) { +static int CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* option, RackOptions& options) { const auto size = RackOptionSize(rackId); - if (option == nullptr) return DefaultRackOption(rackId); + if (option == nullptr) { + options = DefaultRackOption(rackId); + return SCE_NGS2_OK; + } if (option->size != size) throw std::invalid_argument("NGS2: unexpected rack option size " + std::to_string(option->size)); - RackOptions options{}; + options = {}; std::memcpy(&options, option, size); const auto maxChannels = RackMaxChannels(rackId, options); - if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER && maxChannels > SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS) return SCE_NGS2_ERROR_INVALID_NUM_CHANNELS; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER && (options.customSubmixer.max_inputs == 0 || options.customSubmixer.max_inputs > SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS)) { + throw std::runtime_error("NGS2: custom submixer racks with " + std::to_string(options.customSubmixer.max_inputs) + " inputs are not implemented"); + } + if (options.common.max_voices == 0 || maxChannels == 0) throw std::invalid_argument("NGS2: invalid rack voice or channel count"); + int result = SCE_NGS2_OK; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) result = Ngs2CheckCustomRack(options.customSampler.custom_rack_option, 0); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) result = Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option, options.customSubmixer.max_inputs); + if (result != SCE_NGS2_OK) return result; + if (maxChannels > NGS2_MAX_CHANNELS) { + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::runtime_error("NGS2: custom submixer racks with " + std::to_string(maxChannels) + " channels are not implemented"); + } throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2CheckCustomRack(options.customSampler.custom_rack_option); - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_REVERB && options.reverb.reverb_size != 1) { throw std::runtime_error("NGS2: reverb rack size " + std::to_string(options.reverb.reverb_size) + " is not implemented"); } - return options; + return SCE_NGS2_OK; } static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackOptions& options, const Ngs2ContextBufferInfo& bufferInfo, @@ -165,7 +178,9 @@ extern "C" { int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t rack_id, const Ngs2RackOption* option, Ngs2ContextBufferInfo* buffer_info) { if (buffer_info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - CheckedRackOption(rack_id, option); + RackOptions options{}; + const int result = CheckedRackOption(rack_id, option, options); + if (result != SCE_NGS2_OK) throw std::invalid_argument("NGS2: invalid rack option, rejected at creation with " + Ngs2Hex(static_cast(result))); *buffer_info = {}; buffer_info->host_buffer_size = sizeof(Ngs2Rack); return SCE_NGS2_OK; @@ -173,16 +188,20 @@ int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t rack_id, const Ngs2RackOption* int APS5_VABI sceNgs2RackCreate(uintptr_t system_handle, uint32_t rack_id, const Ngs2RackOption* option, const Ngs2ContextBufferInfo* buffer_info, uintptr_t* handle) { if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - const auto options = CheckedRackOption(rack_id, option); if (buffer_info == nullptr) APS5_INVALID_ARG_EX; + RackOptions options{}; + const int result = CheckedRackOption(rack_id, option, options); + if (result != SCE_NGS2_OK) return result; const Ngs2Lock lock; return CreateRack(system_handle, rack_id, options, *buffer_info, {}, handle); } int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t rack_id, const Ngs2RackOption* option, const Ngs2BufferAllocator* allocator, uintptr_t* handle) { if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - const auto options = CheckedRackOption(rack_id, option); if (allocator == nullptr || allocator->alloc_handler == nullptr || allocator->free_handler == nullptr) APS5_INVALID_ARG_EX; + RackOptions options{}; + const int checked = CheckedRackOption(rack_id, option, options); + if (checked != SCE_NGS2_OK) return checked; const Ngs2Lock lock; if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; Ngs2ContextBufferInfo bufferInfo{}; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 379c277110..bdb48ba82a 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -356,6 +356,149 @@ static void TestCustomFilter() { } +struct ModuleOptionWords { + std::uint32_t size; + std::uint32_t words[15]; +}; + +static int allocCalls = 0; + +static std::int32_t APS5_VABI CountAlloc(Ngs2ContextBufferInfo*) { + allocCalls++; + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI CountFree(Ngs2ContextBufferInfo*) { + return SCE_NGS2_OK; +} + +static Ngs2CustomSubmixerRackOption BuiltInRackOption(const Ngs2CustomUserFx2ModuleOption& userFx2, std::uint32_t moduleId, + const ModuleOptionWords* moduleOption, std::uint32_t stateSize) { + auto option = RackOption(&userFx2, 1); + auto& module = option.custom_rack_option.module[0]; + module.module_id = moduleId; + module.option = reinterpret_cast(moduleOption); + module.state_size = stateSize; + return option; +} + +static void TestCustomRackChecks() { + calls = {}; + const auto system = CreateSystem(); + const auto userFx2 = Module(Gain); + const auto good = RackOption(&userFx2, 1); + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &good.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + const auto create = [&](const Ngs2CustomSubmixerRackOption& option) { + uintptr_t rack = 0; + const int result = sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack); + Require(rack == 0); + return result; + }; + + auto option = good; + option.custom_rack_option.num_buffers = 0; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + Require(CreateThrows(option)); + option.custom_rack_option.num_buffers = SCE_NGS2_CUSTOM_MAX_BUFFERS + 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + option = good; + option.max_inputs = 2; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + option.custom_rack_option.num_buffers = 2; + Require(CreateThrows(option)); + option.custom_rack_option.num_buffers = SCE_NGS2_CUSTOM_MAX_BUFFERS; + option.custom_rack_option.module[0].dest_buffer_id = SCE_NGS2_CUSTOM_MAX_BUFFERS - 1; + Require(CreateThrows(option)); + option.custom_rack_option.module[0].dest_buffer_id = SCE_NGS2_CUSTOM_MAX_BUFFERS; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + option = good; + option.max_inputs = 0; + Require(CreateThrows(option)); + option.max_inputs = SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS + 1; + Require(CreateThrows(option)); + + option = good; + option.custom_rack_option.module[0].source_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + option = good; + option.custom_rack_option.module[0].extra_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + + option = good; + option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS + 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + option.max_channels = 16; + Require(CreateThrows(option)); + option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS; + Require(CreateThrows(option)); + + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + + const ModuleOptionWords reverb{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {1}}; + const ModuleOptionWords reverbOff{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {0}}; + const ModuleOptionWords reverbSize{16, {1}}; + const ModuleOptionWords reverbWord{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {2}}; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverb, 0))); + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverbOff, 0))); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverbSize, 0)) == SCE_NGS2_ERROR_INVALID_OPTION_SIZE); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverbWord, 0)) == SCE_NGS2_ERROR_INVALID_REVERB_OPTION); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverb, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + + const ModuleOptionWords pitchShift{SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE, {0}}; + const ModuleOptionWords pitchShiftWord{SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE, {1}}; + const auto pitchShiftState = SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShift, pitchShiftState))); + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShift, 0))); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShift, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShiftWord, pitchShiftState)) == SCE_NGS2_ERROR_INVALID_PITCH_SHIFT_OPTION); + + const ModuleOptionWords convolution{64, {}}; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION, &convolution, 64))); + option = BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION, &convolution, 64); + option.custom_rack_option.module[0].dest_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + + option = BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0); + option.custom_rack_option.num_modules = 2; + option.custom_rack_option.module[1] = good.custom_rack_option.module[0]; + option.custom_rack_option.module[1].dest_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + Require(CreateThrows(BuiltInRackOption(userFx2, 0x19, nullptr, 0))); + + option = good; + option.custom_rack_option.num_buffers = 0; + bool rejected = false; + uintptr_t rack = 0; + try { + sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, nullptr, &rack); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + const Ngs2BufferAllocator allocator{CountAlloc, CountFree, 0}; + Require(sceNgs2RackCreateWithAllocator(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &allocator, &rack) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + Require(rack == 0 && allocCalls == 0 && calls.setupVoices.empty()); + + Ngs2CustomSamplerRackOption sampler{}; + sampler.custom_rack_option.rack_option.size = sizeof(sampler); + sampler.custom_rack_option.rack_option.max_grain_samples = 512; + sampler.custom_rack_option.rack_option.max_voices = 1; + sampler.custom_rack_option.num_modules = 1; + sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + sampler.max_channel_works = 1; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + sampler.custom_rack_option.num_buffers = 1; + sampler.custom_rack_option.module[0].extra_buffer_id = 1; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + Require(rack == 0); + sampler.custom_rack_option.module[0].extra_buffer_id = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + struct LegacyUserFxState { std::uint32_t calls = 0; std::uint32_t flags = 0; @@ -470,6 +613,7 @@ static void TestCustomLegacyUserFx() { int main() { TestUserFxChain(); TestRackOptions(); + TestCustomRackChecks(); TestCustomSampler(); TestCustomFilter(); TestCustomLegacyUserFx(); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..a9a718b5ee 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -19,8 +19,9 @@ - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain -- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Gain (0x15), Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) +- [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before From 3e0feb4bef5d9b9d0c6af62730786954ef6ee1fc Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 19:07:15 +0530 Subject: [PATCH 0396/1360] feat(libc): export _Thrd_yield --- core/libs/prx/libc/src/Threads.cpp | 4 ++++ core/libs/tests/GuestLibcThreads.cpp | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index a00ba39463..1e7ea35688 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -379,6 +379,10 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { } } +int APS5_VABI _Thrd_yield_nid_postfix() { + std::this_thread::yield(); + return threadSuccess; +} int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 001558be39..e811e6f77d 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -55,6 +55,7 @@ int APS5_VABI _Thrd_equal_nid_postfix(void* first, void* second); void* APS5_VABI _Thrd_current_nid_postfix(void); int APS5_VABI _Mtx_current_owns_nid_postfix(void** handle); void* APS5_VABI scePthreadSelf(); +int APS5_VABI _Thrd_yield_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -374,6 +375,14 @@ static void ForwardedCalls() { RequireThrows([] { _Tss_get_nid_postfix(0); }); } +static void ThreadHelpers() { + Require(_Thrd_yield_nid_postfix() == success); + std::thread other([&] { + Require(_Thrd_yield_nid_postfix() == success); + }); + other.join(); +} + int main() { KernelKeys(); ForwardedCalls(); @@ -384,4 +393,5 @@ int main() { ConditionSignal(); PadLaunchAndJoin(); PadDetach(); + ThreadHelpers(); } From 78a31eecda1c3ccc452a0777803aeaa7ab0ee762 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 09:41:16 -0400 Subject: [PATCH 0397/1360] test(agc): initialize geometry point-size registers in stage fixture --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ff6d85226a..7c78555857 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1144,10 +1144,15 @@ void ShaderStageTests() { lines.context[0x29b] = 1; Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 1, "a last-vertex geometry draw does not take its flat inputs and layer from the second vertex of each output line"); lines.context[0x29b] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "missing register in context bank at DWORD 0x280"); + lines.context[0x280] = 0x00080008u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "missing register in context bank at DWORD 0x281"); + lines.context[0x281] = 0xffff0000u; state = AgcDriver::Graphics::DecodeState(lines); mesh = *state.stages.mesh; Require(mesh.outputPrimitiveSize == 1 && mesh.maxPrimitives == 255 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a point-list subgroup did not decode as points"); Require(mesh.provokingVertex == 0, "a point draw has provoking vertex 0"); + Require(state.pointSize == 1.0f && state.pointSizeMin == 0.0f && state.pointSizeMax == 8191.875f, "geometry points did not decode the queue's default point size and limits"); lines.context[0x205] = raster; auto points = lines; points.userConfig[0x242] = 6; From 3c242fdd8143ff28040709620ceae38bcfcc87a7 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 15:57:30 +0200 Subject: [PATCH 0398/1360] feat(libSceNgs2.native): map ATRAC9 waveform blocks as the module does in sceNgs2CalcWaveformBlock --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 34 ++++++++++------- .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 37 ++++++++++++++----- docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 35052a1e6b..58e239a3f6 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -27,6 +27,7 @@ static constexpr std::uint32_t WAVEFORM_TYPE_PCM_FIRST = 0x10; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_LAST = 0x1b; static constexpr std::uint32_t WAVEFORM_TYPE_VAG = 0x1c; static constexpr std::uint32_t VAG_FRAME_SAMPLES = 28; +static constexpr std::uint32_t ATRAC9_FRAME_SAMPLES[16] = {0, 64, 0, 0, 128, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0}; static constexpr std::uint32_t PCM_BYTES_PER_SAMPLE[] = {1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8}; static bool IsPcmType(std::uint32_t type) { @@ -56,6 +57,25 @@ static Ngs2WaveformBlock VagBlock(std::uint32_t channels, std::uint32_t pos, std return block; } +static Ngs2WaveformBlock Atrac9Block(std::uint32_t config, std::uint32_t pos, std::uint32_t count) { + const std::uint32_t rateIndex = (config >> 20) & 0xfu; + const std::uint32_t frameSamples = ATRAC9_FRAME_SAMPLES[rateIndex]; + if (frameSamples == 0) throw std::runtime_error("NGS2: ATRAC9 waveform blocks at sampling rate index " + std::to_string(rateIndex) + " are not implemented"); + const std::uint32_t superframeShift = (config >> 3) & 3u; + const std::uint32_t superframeBytes = (((config >> 5) & 0x7ffu) + 1u) << superframeShift; + const std::uint32_t superframeSamples = frameSamples << superframeShift; + const std::uint32_t first = pos / superframeSamples; + const std::uint32_t lead = frameSamples + pos; + const std::uint32_t past = (count + lead + superframeSamples - 1u) / superframeSamples; + const std::uint32_t offset = first * superframeBytes; + Ngs2WaveformBlock block{}; + block.data_offset = offset; + block.data_size = past * superframeBytes - offset; + block.num_skip_samples = lead - first * superframeSamples; + block.num_samples = count; + return block; +} + void Ngs2Atrac9DecoderDeleter::operator()(void* handle) const { Atrac9ReleaseHandle(handle); } @@ -450,19 +470,7 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ return SCE_NGS2_OK; } if (type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; - if (format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { - return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; - } - Atrac9CodecInfo codec{}; - if (!MatchingCodecInfo(*format, codec)) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; - const std::uint64_t superframeSamples = static_cast(codec.frameSamples) * codec.framesInSuperframe; - const std::uint64_t skip = sample_pos % superframeSamples; - block->data_offset = sample_pos / superframeSamples * codec.superframeSize; - if (num_samples != 0) { - block->data_size = (skip + num_samples + superframeSamples - 1) / superframeSamples * codec.superframeSize; - block->num_skip_samples = static_cast(skip); - } - block->num_samples = num_samples; + *block = Atrac9Block(format->config_data, sample_pos, num_samples); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index d056c6a8c1..7ff25f730a 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -181,18 +181,37 @@ static void TestParse() { Require(sceNgs2ParseWaveformData(wrongRate.data(), wrongRate.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static void RequireAtrac9Block(std::uint32_t config, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size, std::uint32_t skip) { + const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, config, 0, 0}; + Ngs2WaveformBlock block; + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(&format, pos, count, &block) == SCE_NGS2_OK); + Require(block.data_offset == offset && block.data_size == size && block.num_skip_samples == skip && block.num_samples == count); + Require(block.num_repeats == 0 && block.reserved == 0 && block.user_data == 0); +} + static void TestCalcBlock() { const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}; - Ngs2WaveformBlock block{}; Require(sceNgs2CalcWaveformBlock(&format, 1500, 600, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - Require(sceNgs2CalcWaveformBlock(&format, 1500, 600, &block) == SCE_NGS2_OK); - Require(block.data_offset == SuperframeBytes && block.data_size == 2 * SuperframeBytes && block.num_skip_samples == 476 && block.num_samples == 600); - Require(sceNgs2CalcWaveformBlock(&format, 1500, 0, &block) == SCE_NGS2_OK); - Require(block.data_offset == SuperframeBytes && block.data_size == 0 && block.num_skip_samples == 0 && block.num_samples == 0); - const Ngs2WaveformFormat stereo{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 2, 48000, Config, 0, 0}; - Require(sceNgs2CalcWaveformBlock(&stereo, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); - const Ngs2WaveformFormat silent{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 0, 48000, Config, 0, 0}; - Require(sceNgs2CalcWaveformBlock(&silent, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + RequireAtrac9Block(Config, 0, 768, 0, SuperframeBytes, 256); + RequireAtrac9Block(Config, 0, 769, 0, 2 * SuperframeBytes, 256); + RequireAtrac9Block(Config, 1500, 600, SuperframeBytes, 2 * SuperframeBytes, 732); + RequireAtrac9Block(Config, 1500, 0, SuperframeBytes, SuperframeBytes, 732); + RequireAtrac9Block(Config, 4294967195u, 1000, 1073741568u, 3221226240u, 1179); + RequireAtrac9Block(0xFE7007E0, 300, 10, 64, 128, 300); + RequireAtrac9Block(0xFE4007F0, 600, 100, SuperframeBytes, SuperframeBytes, 216); + RequireAtrac9Block(0xFE1007F0, 0, 1, 0, SuperframeBytes, 64); + Ngs2WaveformBlock block{}; + const Ngs2WaveformFormat stereo{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 2, 44100, Config, 0, 0}; + Require(sceNgs2CalcWaveformBlock(&stereo, 0, 1, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes && block.num_skip_samples == 256); + const Ngs2WaveformFormat silent{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 0, 0, Config, 0, 0}; + Require(sceNgs2CalcWaveformBlock(&silent, 0, 1, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes && block.num_skip_samples == 256); + for (const std::uint32_t rateIndex : {0u, 2u, 3u, 5u, 6u, 8u, 15u}) { + const Ngs2WaveformFormat unknown{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, (Config & ~0x00F00000u) | rateIndex << 20, 0, 0}; + bool threw = false; + try { sceNgs2CalcWaveformBlock(&unknown, 0, 1, &block); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } } static void RequirePcmBlock(std::uint32_t type, std::uint32_t channels, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size) { diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 732be4ae80..60dfacf17a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -36,7 +36,7 @@ - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Unknown flags, zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified -- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - ATRAC9 keeps AnyPS5's own mapping: it checks the config against the channels and rate (0x804A8431), leaves out the decoder delay from the skip and size, leaves both 0 for zero samples and computes the size in 64 bits, while [prosper reads the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) without the check, with the delay, filling both and modulo 2^32. Linear PCM and float (0x10 to 0x1b), VAG (0x1c: whole 28-sample frames of 16 bytes per channel, the frame-rounded sample count and no skip), the null checks and the 0x804A8432 of other types follow that reading and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466). Closes when: ATRAC9 follows the module +- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - an ATRAC9 config whose sampling-rate index (bits 20 to 23) is not 1, 4 or 7 throws, where the module reads 0 frame samples from its table and divides by zero. Every type follows [prosper's reading of the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466): linear PCM and float (0x10 to 0x1b), VAG (0x1c) and ATRAC9 (whole superframes from the config alone, a skip of one frame of decoder delay plus the position in the superframe, modulo 2^32), the null checks and the 0x804A8432 of other types. Closes when: the other sampling rates are known ### Functional From 1fcfacb27e238f53614171a2cdfb9249ab4a481f Mon Sep 17 00:00:00 2001 From: janzezno Date: Wed, 21 Oct 2026 16:05:49 +0200 Subject: [PATCH 0399/1360] fix(agc): keep completion write-backs off the full sync in the recorded-store recheck --- core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 0e37bddcd8..ea9e30be81 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1064,7 +1064,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } else { stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published); } - if (stored || published || SnapshotOverlaps(address, bytes)) { + if (stored || published || (completionDepth == 0 && SnapshotOverlaps(address, bytes))) { // Stores into imported memory were only recorded; the CPU is about to read them. The wait // targets the batch holding them (the open one: SyncThrough submits it) and runs with the // mutex released when this acquisition is the outermost; a nested hook waits locked through From 038d9a97d77ea4e60575348f7bac5e38f9fc2983 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 11:06:19 -0300 Subject: [PATCH 0400/1360] fix(libSceAgcDriver): return the console's workload stream registration errors Measured on firmware 13.60: registering an id outside 1 to 31 or one already registered returns 0x8a6c0033, checked before a null stream, which returns 0x8a6c0035; unregistering an id outside 1 to 31 returns 0x8a6c0033 and one that is not registered 0x8a6c003a. Co-Authored-By: Claude Opus 5.5 --- .../Resource/src/Registration.cpp | 20 +++++++++++-------- core/libs/tests/GuestAgcDriverResources.cpp | 17 ++++++++++++++++ docs/dev/debt/libSceAgc.md | 2 +- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index f3fbd0ba67..7cae8c3afb 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -9,6 +9,9 @@ // 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; +static constexpr int SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID = static_cast(0x8A6C0033); +static constexpr int SCE_AGC_ERROR_WORKLOAD_STREAM_NULL = static_cast(0x8A6C0035); +static constexpr int SCE_AGC_ERROR_WORKLOAD_STREAM_NOT_REGISTERED = static_cast(0x8A6C003A); static constexpr uint32_t WORKLOAD_STREAM_MIN_ID = 1; static constexpr uint32_t WORKLOAD_STREAM_MAX_ID = 31; @@ -49,10 +52,11 @@ int APS5_VABI sceAgcDriverRegisterMultipleResources(uint32_t* resourceHandle, ui } int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { - if (stream != nullptr && stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { - std::lock_guard lock(g_workloadStreamLock); - g_workloadStreams |= 1u << stream_id; - } + if (stream_id < WORKLOAD_STREAM_MIN_ID || stream_id > WORKLOAD_STREAM_MAX_ID) return SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID; + std::lock_guard lock(g_workloadStreamLock); + if ((g_workloadStreams & (1u << stream_id)) != 0) return SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID; + if (stream == nullptr) return SCE_AGC_ERROR_WORKLOAD_STREAM_NULL; + g_workloadStreams |= 1u << stream_id; return 0; } @@ -83,10 +87,10 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(uint32_t owner_handle) } int APS5_VABI sceAgcDriverUnregisterWorkloadStream(uint32_t stream_id) { - if (stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { - std::lock_guard lock(g_workloadStreamLock); - g_workloadStreams &= ~(1u << stream_id); - } + if (stream_id < WORKLOAD_STREAM_MIN_ID || stream_id > WORKLOAD_STREAM_MAX_ID) return SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID; + std::lock_guard lock(g_workloadStreamLock); + if ((g_workloadStreams & (1u << stream_id)) == 0) return SCE_AGC_ERROR_WORKLOAD_STREAM_NOT_REGISTERED; + g_workloadStreams &= ~(1u << stream_id); return 0; } diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index fc55b15a8a..5ee342a763 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -92,8 +92,25 @@ int main() { Require(sceAgcDriverFindResourcesPublic(nullptr, nullptr) == Unavailable); Require(sceAgcDriverUnregisterAllResourcesForOwner(owner) == Unavailable); Require(std::string_view(name) == "unchanged"); + constexpr int InvalidStream = static_cast(0x8A6C0033); + constexpr int NullStream = static_cast(0x8A6C0035); + constexpr int StreamNotRegistered = static_cast(0x8A6C003A); + for (const std::uint32_t id : {0u, 32u, 0x101u, 0xffffffffu}) { + Require(sceAgcDriverRegisterWorkloadStream(id, memory) == InvalidStream); + Require(sceAgcDriverRegisterWorkloadStream(id, nullptr) == InvalidStream); + Require(sceAgcDriverUnregisterWorkloadStream(id) == InvalidStream); + } + Require(sceAgcDriverRegisterWorkloadStream(2u, nullptr) == NullStream); + Require(sceAgcDriverUnregisterWorkloadStream(2u) == StreamNotRegistered); + Require(sceAgcDriverRegisterWorkloadStream(1u, memory) == 0); + Require(sceAgcDriverRegisterWorkloadStream(1u, memory) == InvalidStream); + Require(sceAgcDriverRegisterWorkloadStream(1u, nullptr) == InvalidStream); + Require(sceAgcDriverRegisterWorkloadStream(31u, reinterpret_cast(memory) + 1) == 0); + Require(sceAgcDriverUnregisterWorkloadStream(1u) == 0); + Require(sceAgcDriverUnregisterWorkloadStream(1u) == StreamNotRegistered); Require(sceAgcDriverRegisterWorkloadStream(1u, memory) == 0); Require(sceAgcDriverUnregisterWorkloadStream(1u) == 0); + Require(sceAgcDriverUnregisterWorkloadStream(31u) == 0); Require(!sceAgcDriverIsCaptureInProgress()); Require(!sceAgcDriverIsTraceInProgress()); Require(!sceAgcDriverIsSubmitValidationEnabled()); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 0bd4347123..96d78d4983 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -45,7 +45,7 @@ - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. -- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From 66b63149895aee7a7d2d4f179e265191f89ee3ea Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 Date: Wed, 21 Oct 2026 16:07:53 +0200 Subject: [PATCH 0401/1360] fix(relinker): reject section header file ranges outside the input file --- .../elfpatcher/general/ElfConstants.hpp | 1 + .../relinker/src/parsing/ElfReader.cpp | 6 ++++ .../relinker/tests/ElfReaderBoundsTests.cpp | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 9da009bc08..78ed999eab 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -170,6 +170,7 @@ 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_NOBITS = 8; inline constexpr std::uint32_t SHT_DYNSYM = 11; inline constexpr std::uint32_t SHF_WRITE = 0x1; inline constexpr std::uint32_t SHF_ALLOC = 0x2; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 0fc55afebd..545d4cb19c 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -216,6 +216,12 @@ std::vector ElfReader::ReadSectionHeaders() const { sh.Info = _readU32At(offset + 0x2c); sh.EntrySize = _readU64At(offset + kShdrEntSizeOffset); + if (sh.Type != SHT_NULL && sh.Type != SHT_NOBITS && sh.SectionSize != 0) { + if (!_rangeFits(sh.Offset, sh.SectionSize, _fileBuffer.size())) { + throw RelinkerException("Section offset out of bounds", sh.Offset); + } + } + sh.Name = _resolveShdrName(nameOffset, header); headers.push_back(sh); diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index 154238155d..b1122ee8a7 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -233,6 +233,41 @@ int main() { bytes.resize(128 + 3 * 64 - 1); RejectSections(std::move(bytes)); }}, + {"section file range past end of file", [] { + auto bytes = Fixture(); + Write(bytes, 216, 0x2f0); + RejectSections(std::move(bytes)); + }}, + {"section file range offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 216, std::numeric_limits::max() - 15); + RejectSections(std::move(bytes)); + }}, + {"section ending at end of file", [] { + auto bytes = Fixture(); + Write(bytes, 216, 0x2e0); + Write(bytes, 224, 32); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections.size() == 3 && sections[1].Offset + sections[1].SectionSize == 0x300); + }}, + {"unbacked SHT_NOBITS section with range past end of file", [] { + auto bytes = Fixture(); + Write(bytes, 196, 8); + Write(bytes, 216, 0x2f0); + Write(bytes, 224, 0x1000); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections.size() == 3 && sections[1].Type == 8); + }}, + {"empty section beyond end of file", [] { + constexpr std::uint64_t offsets[] = {0x300, 0x301, std::numeric_limits::max()}; + for (const std::uint64_t offset : offsets) { + auto bytes = Fixture(); + Write(bytes, 216, offset); + Write(bytes, 224, 0); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections.size() == 3 && sections[1].Offset == offset && sections[1].SectionSize == 0); + } + }}, {"translation program header stride", [] { for (const std::uint16_t stride : {0, 48, 64}) { auto bytes = Fixture(); From f63840070996b554af6c5af97c138c8f1130d206 Mon Sep 17 00:00:00 2001 From: Maz Date: Thu, 15 Oct 2026 15:01:08 +0400 Subject: [PATCH 0402/1360] fix(savedata): queue conversion completion events --- core/libs/prx/libSceSaveData.native/Export.cpp | 8 ++++++++ core/libs/tests/GuestSaveDataConvert.cpp | 6 ++++++ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index a95498f7d9..af2f0fddc2 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -37,6 +37,7 @@ static std::string save_root() { static std::mutex g_state_mutex; static std::deque g_events; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_BACKUP = 2; +constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_CONVERT = 5; static bool dir_name_match(const char* str, const char* pattern) { if (pattern == nullptr || pattern[0] == '\0') { @@ -952,6 +953,13 @@ static int convert(const SaveDataConvert* param) { return SAVE_DATA_ERROR_NOT_FOUND; } g_convertProgress = 1.0f; + SaveDataEvent event{}; + event.type = SAVE_DATA_EVENT_TYPE_CONVERT; + event.error_code = SAVE_DATA_OK; + event.user_id = param->user_id; + event.dir_name = *param->dir_name; + std::lock_guard lock(g_state_mutex); + g_events.push_back(event); return SAVE_DATA_OK; } diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp index 580d22e892..df8f6743ce 100644 --- a/core/libs/tests/GuestSaveDataConvert.cpp +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -15,6 +15,7 @@ int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); int APS5_VABI sceSaveDataGetConvertProgress(float*); int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(); +int APS5_VABI sceSaveDataGetEventResult(const void*, SaveDataEvent*); } namespace { @@ -67,6 +68,11 @@ int main() { Check(progress < 1.0f, "convert progress is below 1.0f after failed convert"); Check(progress == 0.0f, "convert progress reports 0.0f after failed convert"); Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + SaveDataEvent event{}; + Check(sceSaveDataGetEventResult(nullptr, &event) == 0, "successful conversion queues completion"); + Check(event.type == 5 && event.error_code == 0, "conversion event reports successful conversion"); + Check(event.user_id == 0x10000000 && std::strcmp(event.dir_name.data, "kept") == 0, "conversion event identifies the user and save"); + Check(sceSaveDataGetEventResult(nullptr, &event) == SaveDataErrorNotFound, "conversion event is delivered once"); Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after successful convert"); Check(progress == 1.0f, "convert progress reports 1.0f after successful convert"); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index db7aeb7660..e9b5b9cfcf 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -5,7 +5,7 @@ - [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown - [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits -- [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does +- [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and queues a successful type-5 completion event with the user and directory; PPSA03671 polls for this event after conversion. What the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware From 3e1b0f5aa41a7f7745c1930335279787aabcdf3b Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 20 Oct 2026 22:34:45 +0200 Subject: [PATCH 0403/1360] feat(libSceLibcInternal): export __inet_ntoa_r The PS5 3.20 export inventory of libSceLibcInternal lists __inet_ntoa_r next to inet_ntoa_r, and no AnyPS5 library exported it, so a title importing it could not resolve it. It now runs inet_ntoa_r: FreeBSD 11.4's arpa/inet.h names the function __inet_ntoa_r, and inet_ntoa.c makes inet_ntoa_r a weak alias of it. __inet_addr, __inet_aton and __inet_ntoa are in the same inventory, but libc already exports them and the relinker also looks up libSceLibcInternal imports in libc, so they are not added here. guest_inet_address formats an address through __inet_ntoa_r into a buffer of exactly the text's size, and checks that a buffer one byte short is returned unchanged with ENOSPC and that inet_ntoa's buffer is left alone. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibcInternal/Inet.cpp | 4 ++++ core/libs/tests/GuestInetAddress.cpp | 11 +++++++++++ docs/dev/debt/libSceLibcInternal.md | 1 + 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceLibcInternal/Inet.cpp b/core/libs/prx/libSceLibcInternal/Inet.cpp index 90996aacf6..c11c6201ac 100644 --- a/core/libs/prx/libSceLibcInternal/Inet.cpp +++ b/core/libs/prx/libSceLibcInternal/Inet.cpp @@ -97,6 +97,10 @@ char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std return buffer; } +char* APS5_VABI __inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size) { + return inet_ntoa_r_nid_postfix(address, buffer, size); +} + char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address) { std::memcpy(g_ntoaBuffer, "[inet_ntoa error]", sizeof(g_ntoaBuffer)); __inet_ntop_nid_postfix(AddressFamilyInet, &address, g_ntoaBuffer, sizeof(g_ntoaBuffer)); diff --git a/core/libs/tests/GuestInetAddress.cpp b/core/libs/tests/GuestInetAddress.cpp index 80447cad5d..ad2275caee 100644 --- a/core/libs/tests/GuestInetAddress.cpp +++ b/core/libs/tests/GuestInetAddress.cpp @@ -10,6 +10,8 @@ int APS5_VABI inet_aton_nid_postfix(const char* text, std::uint32_t* address); std::uint32_t APS5_VABI inet_addr_nid_postfix(const char* text); char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address); char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size); +char* APS5_VABI __inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -64,5 +66,14 @@ int main() { Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), buffer, sizeof(buffer)) == buffer && std::string(buffer) == "1.2.3.4", "inet_ntoa_r"); char tiny[4] = {'k', 'e', 'e', 'p'}; Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), tiny, sizeof(tiny)) == tiny, "inet_ntoa_r small buffer returns it"); + + char exact[14]; + std::memset(exact, 'z', sizeof(exact)); + Require(__inet_ntoa_r_nid_postfix(Network(192, 168, 0, 255), exact, sizeof(exact)) == exact && std::string(exact) == "192.168.0.255", "__inet_ntoa_r"); + char oneShort[13]; + std::memset(oneShort, 'z', sizeof(oneShort)); + *__error_nid_postfix() = 0; + Require(__inet_ntoa_r_nid_postfix(Network(192, 168, 0, 255), oneShort, sizeof(oneShort)) == oneShort && *__error_nid_postfix() == 28, "__inet_ntoa_r small buffer"); + Require(std::string(oneShort, sizeof(oneShort)) == std::string(sizeof(oneShort), 'z') && std::string(text) == "255.255.255.255", "__inet_ntoa_r leaves the buffers"); return 0; } diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index ff8c271f1f..35343d02c3 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -17,3 +17,4 @@ - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read - [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer +- [__inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) was not measured on the console: it runs `inet_ntoa_r`, as FreeBSD 11.4 names the function `__inet_ntoa_r` ([arpa/inet.h](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/arpa/inet.h#L129)) and makes `inet_ntoa_r` a weak alias of it ([inet_ntoa.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/inet/inet_ntoa.c#L62-L76)) From 22cfc4b0bbdb86bcf6176add276a4c3bcd366d79 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:57:36 +0530 Subject: [PATCH 0404/1360] fix(libSceSaveDataDialog.native): initialize result directory name on empty selection When sceSaveDataDialogGetResult is passed a non-null dir_name pointer but no directory name was selected by the dialog, write the empty string into dir_name so the caller's output buffer is properly null-terminated instead of retaining uninitialized bytes. --- .../prx/libSceSaveDataDialog.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataDialog.cpp | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp index 54c4fd0926..0a07c63d79 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp @@ -44,7 +44,7 @@ int APS5_VABI sceSaveDataDialogGetResult(void* result) { r->result = SAVE_DATA_DIALOG_RESULT_OK; r->button_id = SAVE_DATA_DIALOG_BUTTON_ID_OK; r->user_data = g_user_data; - if (r->dir_name != nullptr && g_dir_name[0] != '\0') { + if (r->dir_name != nullptr) { std::snprintf(static_cast(r->dir_name)->data, sizeof(SaveDataDirName::data), "%s", g_dir_name); } diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 1c3ffc702d..5cc72f7671 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -106,6 +106,22 @@ int main() { Require(sceSaveDataDialogTerminate() == 0, "terminate"); Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + + Require(sceSaveDataDialogInitialize() == 0, "re-initialize for empty result"); + param = {}; + param.size = sizeof(param); + param.mode = SAVE_DATA_DIALOG_MODE_PROGRESS_BAR; + Require(sceSaveDataDialogOpen(¶m) == 0, "open progress without items"); + Require(sceSaveDataDialogClose(nullptr) == 0, "close empty progress"); + Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "finish empty progress"); + SaveDataDirName emptySelected{}; + std::memset(emptySelected.data, 'X', sizeof(emptySelected.data)); + SaveDataDialogResult emptyResult{}; + emptyResult.dir_name = &emptySelected; + Require(sceSaveDataDialogGetResult(&emptyResult) == 0, "read empty result"); + Require(emptySelected.data[0] == '\0', "empty directory name must be null-terminated"); + Require(sceSaveDataDialogTerminate() == 0, "terminate empty progress"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc after terminate"); Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value after terminate"); } From 27c3f6ba2d2147b5c33b32eacaa0003474842143 Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 21 Oct 2026 08:12:38 +0200 Subject: [PATCH 0405/1360] feat(libSceCesCs): add 27 single-byte code page profiles sceCesRefersUcsProfile threw for every code page except 1250, 1251 and 1252. Twenty-seven more now return a profile that sceCesSbcToUtf8, sceCesUtf8ToSbc and the MBCS context conversions accept: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16, code pages 437, 737, 775, 850, 852, 855, 860, 861, 862, 863, 865 and 866, KOI8-R, KOI8-U and Windows 1254, 1256 and 1258. SingleByte.hpp holds the high half of each table, taken from the public mapping files at unicode.org: ISO8859, Microsoft's PC tables, VENDORS/MISC for KOI8 and Microsoft's best-fit tables for 1254, 1256 and 1258, the same source and derivation as the existing Cp1250 table, whose high half equals bestfit1250's byte table. Bytes 0x00-0x7F are unchanged in all of them, and every table maps its 256 bytes to 256 distinct BMP characters. The three existing profiles keep their tables; one lookup now serves them all. Profiles whose public table leaves bytes undefined (ISO 8859-3, -6, -7, -8, -11, code pages 857, 864, 869), maps bytes to private use (874, 1253, 1255, 1257) or has no public table here still throw. guest_ces checks that each new profile is a distinct, stable pointer, four bytes per table against the source, a round trip of every byte through sceCesSbcToUtf8 and sceCesUtf8ToSbc, UNASSIGNED_CODE for a character outside the table, and a KOI8-R string through an MBCS context in both directions. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/Export.cpp | 134 +++++++- core/libs/prx/libSceCesCs/PendingExports.cpp | 135 -------- core/libs/prx/libSceCesCs/SingleByte.hpp | 312 +++++++++++++++++++ core/libs/tests/GuestCes.cpp | 199 ++++++++++++ docs/dev/debt/libSceCesCs.md | 3 +- 5 files changed, 639 insertions(+), 144 deletions(-) create mode 100644 core/libs/prx/libSceCesCs/SingleByte.hpp diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 25dcb94514..efded2a6c2 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -10,6 +10,7 @@ #include "JisX0212.hpp" #include "Cp949.hpp" #include "Cp950.hpp" +#include "SingleByte.hpp" namespace { @@ -21,7 +22,7 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); -constexpr uint8_t PROFILES[3] = {0, 1, 2}; +constexpr uint8_t PROFILES[3 + SingleByte::Count] = {0, 1, 2}; constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0]; constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1]; constexpr const uint8_t* CP1250_PROFILE = &PROFILES[2]; @@ -59,21 +60,30 @@ constexpr uint16_t CP932_IBM_HEAD[28] = { 0xFFE2, 0xFFE4, 0xFF07, 0xFF02, 0x3231, 0x2116, 0x2121, 0x2235, }; -uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { - if (profile == CP1251_PROFILE || profile == CP1250_PROFILE) { - const uint16_t* high = profile == CP1251_PROFILE ? CP1251_HIGH : CP1250_HIGH; - return sbc >= 0x80 ? high[sbc - 0x80] : sbc; +const uint8_t* singleByteProfile(SingleByte::Table table) { + return &PROFILES[3 + table]; +} + +const uint16_t* sbcHighTable(const void* profile) { + if (profile == CP1251_PROFILE) return CP1251_HIGH; + if (profile == CP1250_PROFILE) return CP1250_HIGH; + for (std::size_t table = 0; table < SingleByte::Count; ++table) { + if (profile == singleByteProfile(static_cast(table))) return SingleByte::HIGH[table]; } + return nullptr; +} + +uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { + if (const uint16_t* high = sbcHighTable(profile)) return sbc >= 0x80 ? high[sbc - 0x80] : sbc; return sbc >= 0x80 && sbc < 0xA0 ? CP1252_HIGH[sbc - 0x80] : sbc; } bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { - if (profile == CP1251_PROFILE || profile == CP1250_PROFILE) { + if (const uint16_t* high = sbcHighTable(profile)) { if (code < 0x80) { sbc = static_cast(code); return true; } - const uint16_t* high = profile == CP1251_PROFILE ? CP1251_HIGH : CP1250_HIGH; for (uint32_t i = 0; i < 128; ++i) { if (high[i] == code) { sbc = static_cast(0x80 + i); @@ -145,7 +155,7 @@ bool isSjis(const void* profile) { } bool isSbc(const void* profile) { - return profile == CP1252_PROFILE || profile == CP1251_PROFILE || profile == CP1250_PROFILE; + return profile == CP1252_PROFILE || sbcHighTable(profile) != nullptr; } const void* contextProfile(const void* context) { @@ -534,6 +544,114 @@ const void* APS5_VABI sceCesRefersUcsProfileCp1250(void) { return CP1250_PROFILE; } +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_1(void) { + return singleByteProfile(SingleByte::Iso8859_1); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_2(void) { + return singleByteProfile(SingleByte::Iso8859_2); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_4(void) { + return singleByteProfile(SingleByte::Iso8859_4); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_5(void) { + return singleByteProfile(SingleByte::Iso8859_5); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_9(void) { + return singleByteProfile(SingleByte::Iso8859_9); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_10(void) { + return singleByteProfile(SingleByte::Iso8859_10); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_13(void) { + return singleByteProfile(SingleByte::Iso8859_13); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_14(void) { + return singleByteProfile(SingleByte::Iso8859_14); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_15(void) { + return singleByteProfile(SingleByte::Iso8859_15); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_16(void) { + return singleByteProfile(SingleByte::Iso8859_16); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp437(void) { + return singleByteProfile(SingleByte::Cp437); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp737(void) { + return singleByteProfile(SingleByte::Cp737); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp775(void) { + return singleByteProfile(SingleByte::Cp775); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp850(void) { + return singleByteProfile(SingleByte::Cp850); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp852(void) { + return singleByteProfile(SingleByte::Cp852); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp855(void) { + return singleByteProfile(SingleByte::Cp855); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp860(void) { + return singleByteProfile(SingleByte::Cp860); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp861(void) { + return singleByteProfile(SingleByte::Cp861); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp862(void) { + return singleByteProfile(SingleByte::Cp862); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp863(void) { + return singleByteProfile(SingleByte::Cp863); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp865(void) { + return singleByteProfile(SingleByte::Cp865); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp866(void) { + return singleByteProfile(SingleByte::Cp866); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8R(void) { + return singleByteProfile(SingleByte::Koi8R); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8U(void) { + return singleByteProfile(SingleByte::Koi8U); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1254(void) { + return singleByteProfile(SingleByte::Cp1254); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1256(void) { + return singleByteProfile(SingleByte::Cp1256); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1258(void) { + return singleByteProfile(SingleByte::Cp1258); +} + void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { if (!sheet) return nullptr; std::memcpy(sheet, SJIS_TAG, sizeof(SJIS_TAG)); diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 165fe278e8..31f8c54b02 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -382,61 +382,16 @@ int APS5_VABI sceCesRefersUcsProfileCp1253(void) { return 0; } -int APS5_VABI sceCesRefersUcsProfileCp1254(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileCp1255(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesRefersUcsProfileCp1256(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileCp1257(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesRefersUcsProfileCp1258(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp437(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp737(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp775(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp850(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp852(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp855(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileCp857(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -447,41 +402,11 @@ int APS5_VABI sceCesRefersUcsProfileCp858(void) { return 0; } -int APS5_VABI sceCesRefersUcsProfileCp860(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp861(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp862(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp863(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileCp864(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesRefersUcsProfileCp865(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileCp866(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileCp869(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -502,61 +427,16 @@ int APS5_VABI sceCesRefersUcsProfileGbT1988Tilde0x7e(void) { return 0; } -int APS5_VABI sceCesRefersUcsProfileIso8859_1(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileIso8859_10(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileIso8859_11(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesRefersUcsProfileIso8859_13(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileIso8859_14(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileIso8859_15(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileIso8859_16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileIso8859_2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileIso8859_3(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCesRefersUcsProfileIso8859_4(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileIso8859_5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileIso8859_6(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -572,11 +452,6 @@ int APS5_VABI sceCesRefersUcsProfileIso8859_8(void) { return 0; } -int APS5_VABI sceCesRefersUcsProfileIso8859_9(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileJisX0201(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -602,16 +477,6 @@ int APS5_VABI sceCesRefersUcsProfileJisX0201Tilde0x7e(void) { return 0; } -int APS5_VABI sceCesRefersUcsProfileKoi8R(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesRefersUcsProfileKoi8U(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesRefersUcsProfileKsX1003(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceCesCs/SingleByte.hpp b/core/libs/prx/libSceCesCs/SingleByte.hpp new file mode 100644 index 0000000000..ebe46104cd --- /dev/null +++ b/core/libs/prx/libSceCesCs/SingleByte.hpp @@ -0,0 +1,312 @@ +#pragma once + +#include +#include + +namespace SingleByte { + +enum Table : std::size_t { + Iso8859_1, + Iso8859_2, + Iso8859_4, + Iso8859_5, + Iso8859_9, + Iso8859_10, + Iso8859_13, + Iso8859_14, + Iso8859_15, + Iso8859_16, + Cp437, + Cp737, + Cp775, + Cp850, + Cp852, + Cp855, + Cp860, + Cp861, + Cp862, + Cp863, + Cp865, + Cp866, + Koi8R, + Koi8U, + Cp1254, + Cp1256, + Cp1258, + Count, +}; + +inline constexpr uint16_t HIGH[Count][128] = { + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x02D8, 0x0141, 0x00A4, 0x013D, 0x015A, 0x00A7, 0x00A8, 0x0160, 0x015E, 0x0164, 0x0179, 0x00AD, 0x017D, 0x017B, + 0x00B0, 0x0105, 0x02DB, 0x0142, 0x00B4, 0x013E, 0x015B, 0x02C7, 0x00B8, 0x0161, 0x015F, 0x0165, 0x017A, 0x02DD, 0x017E, 0x017C, + 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, + 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, + 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, + 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x0138, 0x0156, 0x00A4, 0x0128, 0x013B, 0x00A7, 0x00A8, 0x0160, 0x0112, 0x0122, 0x0166, 0x00AD, 0x017D, 0x00AF, + 0x00B0, 0x0105, 0x02DB, 0x0157, 0x00B4, 0x0129, 0x013C, 0x02C7, 0x00B8, 0x0161, 0x0113, 0x0123, 0x0167, 0x014A, 0x017E, 0x014B, + 0x0100, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x012E, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x012A, + 0x0110, 0x0145, 0x014C, 0x0136, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x0172, 0x00DA, 0x00DB, 0x00DC, 0x0168, 0x016A, 0x00DF, + 0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x012B, + 0x0111, 0x0146, 0x014D, 0x0137, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x02D9, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x00AD, 0x040E, 0x040F, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, + 0x2116, 0x0451, 0x0452, 0x0453, 0x0454, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A, 0x045B, 0x045C, 0x00A7, 0x045E, 0x045F, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131, 0x015F, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x0112, 0x0122, 0x012A, 0x0128, 0x0136, 0x00A7, 0x013B, 0x0110, 0x0160, 0x0166, 0x017D, 0x00AD, 0x016A, 0x014A, + 0x00B0, 0x0105, 0x0113, 0x0123, 0x012B, 0x0129, 0x0137, 0x00B7, 0x013C, 0x0111, 0x0161, 0x0167, 0x017E, 0x2015, 0x016B, 0x014B, + 0x0100, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x012E, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x0145, 0x014C, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x0168, 0x00D8, 0x0172, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x0146, 0x014D, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x0169, 0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x0138, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x201D, 0x00A2, 0x00A3, 0x00A4, 0x201E, 0x00A6, 0x00A7, 0x00D8, 0x00A9, 0x0156, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00C6, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x201C, 0x00B5, 0x00B6, 0x00B7, 0x00F8, 0x00B9, 0x0157, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00E6, + 0x0104, 0x012E, 0x0100, 0x0106, 0x00C4, 0x00C5, 0x0118, 0x0112, 0x010C, 0x00C9, 0x0179, 0x0116, 0x0122, 0x0136, 0x012A, 0x013B, + 0x0160, 0x0143, 0x0145, 0x00D3, 0x014C, 0x00D5, 0x00D6, 0x00D7, 0x0172, 0x0141, 0x015A, 0x016A, 0x00DC, 0x017B, 0x017D, 0x00DF, + 0x0105, 0x012F, 0x0101, 0x0107, 0x00E4, 0x00E5, 0x0119, 0x0113, 0x010D, 0x00E9, 0x017A, 0x0117, 0x0123, 0x0137, 0x012B, 0x013C, + 0x0161, 0x0144, 0x0146, 0x00F3, 0x014D, 0x00F5, 0x00F6, 0x00F7, 0x0173, 0x0142, 0x015B, 0x016B, 0x00FC, 0x017C, 0x017E, 0x2019, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x1E02, 0x1E03, 0x00A3, 0x010A, 0x010B, 0x1E0A, 0x00A7, 0x1E80, 0x00A9, 0x1E82, 0x1E0B, 0x1EF2, 0x00AD, 0x00AE, 0x0178, + 0x1E1E, 0x1E1F, 0x0120, 0x0121, 0x1E40, 0x1E41, 0x00B6, 0x1E56, 0x1E81, 0x1E57, 0x1E83, 0x1E60, 0x1EF3, 0x1E84, 0x1E85, 0x1E61, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x0174, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x1E6A, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x0176, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x0175, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x1E6B, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x0177, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x20AC, 0x00A5, 0x0160, 0x00A7, 0x0161, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x017D, 0x00B5, 0x00B6, 0x00B7, 0x017E, 0x00B9, 0x00BA, 0x00BB, 0x0152, 0x0153, 0x0178, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x0105, 0x0141, 0x20AC, 0x201E, 0x0160, 0x00A7, 0x0161, 0x00A9, 0x0218, 0x00AB, 0x0179, 0x00AD, 0x017A, 0x017B, + 0x00B0, 0x00B1, 0x010C, 0x0142, 0x017D, 0x201D, 0x00B6, 0x00B7, 0x017E, 0x010D, 0x0219, 0x00BB, 0x0152, 0x0153, 0x0178, 0x017C, + 0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0106, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x0110, 0x0143, 0x00D2, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x015A, 0x0170, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0118, 0x021A, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x0107, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x0111, 0x0144, 0x00F2, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x015B, 0x0171, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0119, 0x021B, 0x00FF, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, + 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, + 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03C9, 0x03AC, 0x03AD, 0x03AE, 0x03CA, 0x03AF, 0x03CC, 0x03CD, 0x03CB, 0x03CE, 0x0386, 0x0388, 0x0389, 0x038A, 0x038C, 0x038E, + 0x038F, 0x00B1, 0x2265, 0x2264, 0x03AA, 0x03AB, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x0106, 0x00FC, 0x00E9, 0x0101, 0x00E4, 0x0123, 0x00E5, 0x0107, 0x0142, 0x0113, 0x0156, 0x0157, 0x012B, 0x0179, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x014D, 0x00F6, 0x0122, 0x00A2, 0x015A, 0x015B, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x00A4, + 0x0100, 0x012A, 0x00F3, 0x017B, 0x017C, 0x017A, 0x201D, 0x00A6, 0x00A9, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x0141, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x0104, 0x010C, 0x0118, 0x0116, 0x2563, 0x2551, 0x2557, 0x255D, 0x012E, 0x0160, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0172, 0x016A, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x017D, + 0x0105, 0x010D, 0x0119, 0x0117, 0x012F, 0x0161, 0x0173, 0x016B, 0x017E, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x00D3, 0x00DF, 0x014C, 0x0143, 0x00F5, 0x00D5, 0x00B5, 0x0144, 0x0136, 0x0137, 0x013B, 0x013C, 0x0146, 0x0112, 0x0145, 0x2019, + 0x00AD, 0x00B1, 0x201C, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x201E, 0x00B0, 0x2219, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x00F0, 0x00D0, 0x00CA, 0x00CB, 0x00C8, 0x0131, 0x00CD, 0x00CE, 0x00CF, 0x2518, 0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, + 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0x00FE, 0x00DE, 0x00DA, 0x00DB, 0x00D9, 0x00FD, 0x00DD, 0x00AF, 0x00B4, + 0x00AD, 0x00B1, 0x2017, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x016F, 0x0107, 0x00E7, 0x0142, 0x00EB, 0x0150, 0x0151, 0x00EE, 0x0179, 0x00C4, 0x0106, + 0x00C9, 0x0139, 0x013A, 0x00F4, 0x00F6, 0x013D, 0x013E, 0x015A, 0x015B, 0x00D6, 0x00DC, 0x0164, 0x0165, 0x0141, 0x00D7, 0x010D, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x0104, 0x0105, 0x017D, 0x017E, 0x0118, 0x0119, 0x00AC, 0x017A, 0x010C, 0x015F, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x011A, 0x015E, 0x2563, 0x2551, 0x2557, 0x255D, 0x017B, 0x017C, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0102, 0x0103, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x0111, 0x0110, 0x010E, 0x00CB, 0x010F, 0x0147, 0x00CD, 0x00CE, 0x011B, 0x2518, 0x250C, 0x2588, 0x2584, 0x0162, 0x016E, 0x2580, + 0x00D3, 0x00DF, 0x00D4, 0x0143, 0x0144, 0x0148, 0x0160, 0x0161, 0x0154, 0x00DA, 0x0155, 0x0170, 0x00FD, 0x00DD, 0x0163, 0x00B4, + 0x00AD, 0x02DD, 0x02DB, 0x02C7, 0x02D8, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x02D9, 0x0171, 0x0158, 0x0159, 0x25A0, 0x00A0, + }, + { + 0x0452, 0x0402, 0x0453, 0x0403, 0x0451, 0x0401, 0x0454, 0x0404, 0x0455, 0x0405, 0x0456, 0x0406, 0x0457, 0x0407, 0x0458, 0x0408, + 0x0459, 0x0409, 0x045A, 0x040A, 0x045B, 0x040B, 0x045C, 0x040C, 0x045E, 0x040E, 0x045F, 0x040F, 0x044E, 0x042E, 0x044A, 0x042A, + 0x0430, 0x0410, 0x0431, 0x0411, 0x0446, 0x0426, 0x0434, 0x0414, 0x0435, 0x0415, 0x0444, 0x0424, 0x0433, 0x0413, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x0445, 0x0425, 0x0438, 0x0418, 0x2563, 0x2551, 0x2557, 0x255D, 0x0439, 0x0419, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x043A, 0x041A, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x043B, 0x041B, 0x043C, 0x041C, 0x043D, 0x041D, 0x043E, 0x041E, 0x043F, 0x2518, 0x250C, 0x2588, 0x2584, 0x041F, 0x044F, 0x2580, + 0x042F, 0x0440, 0x0420, 0x0441, 0x0421, 0x0442, 0x0422, 0x0443, 0x0423, 0x0436, 0x0416, 0x0432, 0x0412, 0x044C, 0x042C, 0x2116, + 0x00AD, 0x044B, 0x042B, 0x0437, 0x0417, 0x0448, 0x0428, 0x044D, 0x042D, 0x0449, 0x0429, 0x0447, 0x0427, 0x00A7, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E3, 0x00E0, 0x00C1, 0x00E7, 0x00EA, 0x00CA, 0x00E8, 0x00CD, 0x00D4, 0x00EC, 0x00C3, 0x00C2, + 0x00C9, 0x00C0, 0x00C8, 0x00F4, 0x00F5, 0x00F2, 0x00DA, 0x00F9, 0x00CC, 0x00D5, 0x00DC, 0x00A2, 0x00A3, 0x00D9, 0x20A7, 0x00D3, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00D2, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00D0, 0x00F0, 0x00DE, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00FE, 0x00FB, 0x00DD, 0x00FD, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00C1, 0x00CD, 0x00D3, 0x00DA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF, + 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00C2, 0x00E0, 0x00B6, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x2017, 0x00C0, 0x00A7, + 0x00C9, 0x00C8, 0x00CA, 0x00F4, 0x00CB, 0x00CF, 0x00FB, 0x00F9, 0x00A4, 0x00D4, 0x00DC, 0x00A2, 0x00A3, 0x00D9, 0x00DB, 0x0192, + 0x00A6, 0x00B4, 0x00F3, 0x00FA, 0x00A8, 0x00B8, 0x00B3, 0x00AF, 0x00CE, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00BE, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00A4, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, + 0x0401, 0x0451, 0x0404, 0x0454, 0x0407, 0x0457, 0x040E, 0x045E, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x2116, 0x00A4, 0x25A0, 0x00A0, + }, + { + 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, + 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248, 0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7, + 0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255D, 0x255E, + 0x255F, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x00A9, + 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x044B, 0x0437, 0x0448, 0x044D, 0x0449, 0x0447, 0x044A, + 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x042B, 0x0417, 0x0428, 0x042D, 0x0429, 0x0427, 0x042A, + }, + { + 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, + 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248, 0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7, + 0x2550, 0x2551, 0x2552, 0x0451, 0x0454, 0x2554, 0x0456, 0x0457, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x0491, 0x255D, 0x255E, + 0x255F, 0x2560, 0x2561, 0x0401, 0x0404, 0x2563, 0x0406, 0x0407, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x0490, 0x256C, 0x00A9, + 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x044B, 0x0437, 0x0448, 0x044D, 0x0449, 0x0447, 0x044A, + 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x042B, 0x0417, 0x0428, 0x042D, 0x0429, 0x0427, 0x042A, + }, + { + 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x008E, 0x008F, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x009E, 0x0178, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131, 0x015F, 0x00FF, + }, + { + 0x20AC, 0x067E, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0679, 0x2039, 0x0152, 0x0686, 0x0698, 0x0688, + 0x06AF, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x06A9, 0x2122, 0x0691, 0x203A, 0x0153, 0x200C, 0x200D, 0x06BA, + 0x00A0, 0x060C, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x06BE, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x061B, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x061F, + 0x06C1, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627, 0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, + 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x00D7, 0x0637, 0x0638, 0x0639, 0x063A, 0x0640, 0x0641, 0x0642, 0x0643, + 0x00E0, 0x0644, 0x00E2, 0x0645, 0x0646, 0x0647, 0x0648, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0649, 0x064A, 0x00EE, 0x00EF, + 0x064B, 0x064C, 0x064D, 0x064E, 0x00F4, 0x064F, 0x0650, 0x00F7, 0x0651, 0x00F9, 0x0652, 0x00FB, 0x00FC, 0x200E, 0x200F, 0x06D2, + }, + { + 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x008A, 0x2039, 0x0152, 0x008D, 0x008E, 0x008F, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x009A, 0x203A, 0x0153, 0x009D, 0x009E, 0x0178, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0300, 0x00CD, 0x00CE, 0x00CF, + 0x0110, 0x00D1, 0x0309, 0x00D3, 0x00D4, 0x01A0, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x01AF, 0x0303, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0301, 0x00ED, 0x00EE, 0x00EF, + 0x0111, 0x00F1, 0x0323, 0x00F3, 0x00F4, 0x01A1, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x01B0, 0x20AB, 0x00FF, + }, +}; + +} diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index 7e5238dce7..de0903e090 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include #include #include @@ -7,6 +9,33 @@ extern "C" { const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1250(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_1(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_2(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_4(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_5(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_9(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_10(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_13(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_14(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_15(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_16(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp437(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp737(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp775(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp850(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp852(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp855(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp860(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp861(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp862(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp863(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp865(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp866(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8R(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8U(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1254(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1256(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1258(void); int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); @@ -46,6 +75,175 @@ static int Utf8ToSbc(const std::uint8_t* profile, const char* utf8, std::uint32_ return sceCesUtf8ToSbc(reinterpret_cast(utf8), utf8max, &length, profile, &sbc); } +static void RequireSbcCode(const std::uint8_t* profile, std::uint8_t sbc, std::uint32_t code) { + char expected[4] = {}; + if (code < 0x80) { + expected[0] = static_cast(code); + } else if (code < 0x800) { + expected[0] = static_cast(0xC0 | (code >> 6)); + expected[1] = static_cast(0x80 | (code & 0x3F)); + } else { + expected[0] = static_cast(0xE0 | (code >> 12)); + expected[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); + expected[2] = static_cast(0x80 | (code & 0x3F)); + } + RequireSbcToUtf8(profile, sbc, expected); +} + +static void CheckSingleByteProfiles() { + constexpr int unassignedCode = static_cast(0x805C0020); + const auto getters = {sceCesRefersUcsProfileIso8859_1, sceCesRefersUcsProfileIso8859_2, sceCesRefersUcsProfileIso8859_4, sceCesRefersUcsProfileIso8859_5, sceCesRefersUcsProfileIso8859_9, sceCesRefersUcsProfileIso8859_10, sceCesRefersUcsProfileIso8859_13, sceCesRefersUcsProfileIso8859_14, sceCesRefersUcsProfileIso8859_15, sceCesRefersUcsProfileIso8859_16, sceCesRefersUcsProfileCp437, sceCesRefersUcsProfileCp737, sceCesRefersUcsProfileCp775, sceCesRefersUcsProfileCp850, sceCesRefersUcsProfileCp852, sceCesRefersUcsProfileCp855, sceCesRefersUcsProfileCp860, sceCesRefersUcsProfileCp861, sceCesRefersUcsProfileCp862, sceCesRefersUcsProfileCp863, sceCesRefersUcsProfileCp865, sceCesRefersUcsProfileCp866, sceCesRefersUcsProfileKoi8R, sceCesRefersUcsProfileKoi8U, sceCesRefersUcsProfileCp1254, sceCesRefersUcsProfileCp1256, sceCesRefersUcsProfileCp1258}; + const std::uint8_t* existing[] = {sceCesRefersUcsProfileCp1252(), sceCesRefersUcsProfileCp1251(), sceCesRefersUcsProfileCp1250()}; + std::size_t index = 0; + for (const auto getter : getters) { + const std::uint8_t* profile = getter(); + Require(profile != nullptr && profile == getter()); + for (const std::uint8_t* other : existing) Require(profile != other); + std::size_t inner = 0; + for (const auto earlier : getters) { + if (inner++ == index) break; + Require(profile != earlier()); + } + ++index; + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t cp[4] = {}; + std::uint32_t cpLength = 0; + std::uint8_t back = 0; + Require(sceCesSbcToUtf8(profile, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); + if (sbc < 0x80) Require(cpLength == 1 && cp[0] == sbc); + Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, profile, &back) == 0 && back == sbc); + } + Require(Utf8ToSbc(profile, "\xE3\x81\x82", 3) == unassignedCode); + } + struct Check { + decltype(&sceCesRefersUcsProfileIso8859_1) profile; + std::uint8_t sbc; + std::uint32_t code; + }; + const Check checks[] = { + {sceCesRefersUcsProfileIso8859_1, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_1, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_1, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_1, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_2, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_2, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_2, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_2, 0xFF, 0x02D9}, + {sceCesRefersUcsProfileIso8859_4, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_4, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_4, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_4, 0xFF, 0x02D9}, + {sceCesRefersUcsProfileIso8859_5, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_5, 0xA4, 0x0404}, + {sceCesRefersUcsProfileIso8859_5, 0xC1, 0x0421}, + {sceCesRefersUcsProfileIso8859_5, 0xFF, 0x045F}, + {sceCesRefersUcsProfileIso8859_9, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_9, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_9, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_9, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_10, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_10, 0xA4, 0x012A}, + {sceCesRefersUcsProfileIso8859_10, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_10, 0xFF, 0x0138}, + {sceCesRefersUcsProfileIso8859_13, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_13, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_13, 0xC1, 0x012E}, + {sceCesRefersUcsProfileIso8859_13, 0xFF, 0x2019}, + {sceCesRefersUcsProfileIso8859_14, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_14, 0xA4, 0x010A}, + {sceCesRefersUcsProfileIso8859_14, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_14, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_15, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_15, 0xA4, 0x20AC}, + {sceCesRefersUcsProfileIso8859_15, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_15, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_16, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_16, 0xA4, 0x20AC}, + {sceCesRefersUcsProfileIso8859_16, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_16, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileCp437, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp437, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp437, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp437, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp737, 0x80, 0x0391}, + {sceCesRefersUcsProfileCp737, 0xA4, 0x03BD}, + {sceCesRefersUcsProfileCp737, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp737, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp775, 0x80, 0x0106}, + {sceCesRefersUcsProfileCp775, 0xA4, 0x017C}, + {sceCesRefersUcsProfileCp775, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp775, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp850, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp850, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp850, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp850, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp852, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp852, 0xA4, 0x0104}, + {sceCesRefersUcsProfileCp852, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp852, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp855, 0x80, 0x0452}, + {sceCesRefersUcsProfileCp855, 0xA4, 0x0446}, + {sceCesRefersUcsProfileCp855, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp855, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp860, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp860, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp860, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp860, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp861, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp861, 0xA4, 0x00C1}, + {sceCesRefersUcsProfileCp861, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp861, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp862, 0x80, 0x05D0}, + {sceCesRefersUcsProfileCp862, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp862, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp862, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp863, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp863, 0xA4, 0x00A8}, + {sceCesRefersUcsProfileCp863, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp863, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp865, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp865, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp865, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp865, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp866, 0x80, 0x0410}, + {sceCesRefersUcsProfileCp866, 0xA4, 0x0434}, + {sceCesRefersUcsProfileCp866, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp866, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileKoi8R, 0x80, 0x2500}, + {sceCesRefersUcsProfileKoi8R, 0xA4, 0x2553}, + {sceCesRefersUcsProfileKoi8R, 0xC1, 0x0430}, + {sceCesRefersUcsProfileKoi8R, 0xFF, 0x042A}, + {sceCesRefersUcsProfileKoi8U, 0x80, 0x2500}, + {sceCesRefersUcsProfileKoi8U, 0xA4, 0x0454}, + {sceCesRefersUcsProfileKoi8U, 0xC1, 0x0430}, + {sceCesRefersUcsProfileKoi8U, 0xFF, 0x042A}, + {sceCesRefersUcsProfileCp1254, 0x80, 0x20AC}, + {sceCesRefersUcsProfileCp1254, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileCp1254, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileCp1254, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileCp1256, 0x80, 0x20AC}, + {sceCesRefersUcsProfileCp1256, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileCp1256, 0xC1, 0x0621}, + {sceCesRefersUcsProfileCp1256, 0xFF, 0x06D2}, + {sceCesRefersUcsProfileCp1258, 0x80, 0x20AC}, + {sceCesRefersUcsProfileCp1258, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileCp1258, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileCp1258, 0xFF, 0x00FF}, + }; + for (const Check& check : checks) RequireSbcCode(check.profile(), check.sbc, check.code); + + char context[256] = {}; + Require(sceCesMbcsUcsContextInit(context, sceCesRefersUcsProfileKoi8R()) == 0); + std::uint8_t koi8[16] = {}; + std::uint32_t used = 0; + std::uint32_t produced = 0; + Require(sceCesUtf8StrToMbcsStr(context, reinterpret_cast("\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"), 12, &used, koi8, sizeof(koi8), &produced) == 0); + Require(used == 12 && produced == 6 && std::memcmp(koi8, "\xF0\xD2\xC9\xD7\xC5\xD4", 6) == 0); + std::uint8_t utf8[32] = {}; + Require(sceCesMbcsStrToUtf8Str(context, koi8, 6, &used, utf8, sizeof(utf8), &produced) == 0); + Require(used == 6 && produced == 12 && std::memcmp(utf8, "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82", 12) == 0); +} + int main() { constexpr int invalidParameter = static_cast(0x805C0001); constexpr int invalidSrcBuffer = static_cast(0x805C0010); @@ -57,6 +255,7 @@ int main() { const std::uint8_t* profile = sceCesRefersUcsProfileCp1252(); Require(profile != nullptr && profile == sceCesRefersUcsProfileCp1252()); + CheckSingleByteProfiles(); RequireSbcToUtf8(profile, 'A', "A"); RequireSbcToUtf8(profile, 0x80, "\xE2\x82\xAC"); diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 14cef5e67e..55ef735ddc 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,5 +11,6 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 249 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 222 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure From b32a5ee32b4490d069f305821af027cd5bd58a64 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:08:17 +0530 Subject: [PATCH 0406/1360] test(libSceSaveDataDialog.native): cover empty selection dialog result Verify that sceSaveDataDialogGetResult writes an empty null-terminated directory name string when a selection dialog finishes with no selected directory items. --- core/libs/tests/GuestSaveDataDialog.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 5cc72f7671..c520246469 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -107,6 +107,22 @@ int main() { Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogInitialize() == 0, "re-initialize for empty selection"); + SaveDataDialogParam emptySelectionParam{}; + emptySelectionParam.size = sizeof(emptySelectionParam); + emptySelectionParam.mode = 1; + Require(sceSaveDataDialogOpen(&emptySelectionParam) == 0, "open selection dialog without items"); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "selection finishes immediately"); + SaveDataDirName emptySelectionDir{}; + std::memset(emptySelectionDir.data, 'X', sizeof(emptySelectionDir.data)); + SaveDataDialogResult emptySelectionResult{}; + emptySelectionResult.dir_name = &emptySelectionDir; + Require(sceSaveDataDialogGetResult(&emptySelectionResult) == 0, "read empty selection result"); + Require(emptySelectionDir.data[0] == '\0', "empty selection directory name must be null-terminated"); + Require(emptySelectionResult.mode == emptySelectionParam.mode && + emptySelectionResult.result == SAVE_DATA_DIALOG_RESULT_OK, "selection result status"); + Require(sceSaveDataDialogTerminate() == 0, "terminate selection"); + Require(sceSaveDataDialogInitialize() == 0, "re-initialize for empty result"); param = {}; param.size = sizeof(param); From 7129b6956bd77c1327f871878e5ac9d48c0b3e9d Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 16:49:52 +0200 Subject: [PATCH 0407/1360] feat: run the common-dialog state machine in four dialog libraries Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceHmdSetupDialog/CMakeLists.txt | 14 +++ core/libs/prx/libSceHmdSetupDialog/Export.cpp | 68 ++++++++++---- .../libSceNpFriendListDialog/CMakeLists.txt | 14 +++ .../prx/libSceNpFriendListDialog/Export.cpp | 92 ++++++++++++------- .../libSceSocialScreenDialog/CMakeLists.txt | 14 +++ .../prx/libSceSocialScreenDialog/Export.cpp | 68 ++++++++++---- .../prx/libSceVrServiceDialog/CMakeLists.txt | 14 +++ .../libs/prx/libSceVrServiceDialog/Export.cpp | 68 ++++++++++---- core/libs/tests/GuestHmdSetupDialog.cpp | 72 +++++++++++++++ core/libs/tests/GuestNpFriendListDialog.cpp | 72 +++++++++++++++ core/libs/tests/GuestSocialScreenDialog.cpp | 72 +++++++++++++++ core/libs/tests/GuestVrServiceDialog.cpp | 72 +++++++++++++++ docs/dev/debt/libSceHmdSetupDialog.md | 4 +- docs/dev/debt/libSceNpFriendListDialog.md | 6 +- docs/dev/debt/libSceSocialScreenDialog.md | 4 +- docs/dev/debt/libSceVrServiceDialog.md | 4 +- 16 files changed, 559 insertions(+), 99 deletions(-) create mode 100644 core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpFriendListDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceVrServiceDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestHmdSetupDialog.cpp create mode 100644 core/libs/tests/GuestNpFriendListDialog.cpp create mode 100644 core/libs/tests/GuestSocialScreenDialog.cpp create mode 100644 core/libs/tests/GuestVrServiceDialog.cpp diff --git a/core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt b/core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt new file mode 100644 index 0000000000..a294abe2b0 --- /dev/null +++ b/core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceHmdSetupDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceHmdSetupDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceHmdSetupDialog) + +if(BUILD_TESTING) + add_test_executable(guest_hmd_setup_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmdSetupDialog.cpp) + target_include_directories(guest_hmd_setup_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hmd_setup_dialog_tests PRIVATE libSceHmdSetupDialog) + add_test(NAME guest_hmd_setup_dialog COMMAND guest_hmd_setup_dialog_tests) + if(WIN32) + set_tests_properties(guest_hmd_setup_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHmdSetupDialog/Export.cpp b/core/libs/prx/libSceHmdSetupDialog/Export.cpp index 6fb905167d..571643bf02 100644 --- a/core/libs/prx/libSceHmdSetupDialog/Export.cpp +++ b/core/libs/prx/libSceHmdSetupDialog/Export.cpp @@ -1,42 +1,70 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceHmdSetupDialogClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHmdSetupDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceHmdSetupDialogGetResult() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHmdSetupDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceHmdSetupDialogGetStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHmdSetupDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceHmdSetupDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceHmdSetupDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); return 0; } -int APS5_VABI sceHmdSetupDialogInitialize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHmdSetupDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceHmdSetupDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmdSetupDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmdSetupDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHmdSetupDialogTerminate(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/prx/libSceNpFriendListDialog/CMakeLists.txt b/core/libs/prx/libSceNpFriendListDialog/CMakeLists.txt new file mode 100644 index 0000000000..219499b30b --- /dev/null +++ b/core/libs/prx/libSceNpFriendListDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpFriendListDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpFriendListDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpFriendListDialog) + +if(BUILD_TESTING) + add_test_executable(guest_np_friend_list_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpFriendListDialog.cpp) + target_include_directories(guest_np_friend_list_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_friend_list_dialog_tests PRIVATE libSceNpFriendListDialog) + add_test(NAME guest_np_friend_list_dialog COMMAND guest_np_friend_list_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_friend_list_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpFriendListDialog/Export.cpp b/core/libs/prx/libSceNpFriendListDialog/Export.cpp index e744f30761..4ca7288353 100644 --- a/core/libs/prx/libSceNpFriendListDialog/Export.cpp +++ b/core/libs/prx/libSceNpFriendListDialog/Export.cpp @@ -1,12 +1,70 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceNpFriendListDialogClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpFriendListDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpFriendListDialogOpenA(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNpFriendListDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpFriendListDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpFriendListDialogGetResultA(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + +int APS5_VABI sceNpFriendListDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNpFriendListDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } @@ -15,39 +73,9 @@ int APS5_VABI sceNpFriendListDialogGetResult() { return 0; } -int APS5_VABI sceNpFriendListDialogGetResultA() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpFriendListDialogGetStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpFriendListDialogInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpFriendListDialogOpen() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpFriendListDialogOpenA() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpFriendListDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpFriendListDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt b/core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt new file mode 100644 index 0000000000..83af8dda26 --- /dev/null +++ b/core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceSocialScreenDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSocialScreenDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceSocialScreenDialog) + +if(BUILD_TESTING) + add_test_executable(guest_social_screen_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocialScreenDialog.cpp) + target_include_directories(guest_social_screen_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_social_screen_dialog_tests PRIVATE libSceSocialScreenDialog) + add_test(NAME guest_social_screen_dialog COMMAND guest_social_screen_dialog_tests) + if(WIN32) + set_tests_properties(guest_social_screen_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSocialScreenDialog/Export.cpp b/core/libs/prx/libSceSocialScreenDialog/Export.cpp index 17987cec00..c3c5849381 100644 --- a/core/libs/prx/libSceSocialScreenDialog/Export.cpp +++ b/core/libs/prx/libSceSocialScreenDialog/Export.cpp @@ -1,42 +1,70 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceSocialScreenDialogClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSocialScreenDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceSocialScreenDialogGetResult() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSocialScreenDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceSocialScreenDialogGetStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSocialScreenDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceSocialScreenDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceSocialScreenDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); return 0; } -int APS5_VABI sceSocialScreenDialogInitialize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSocialScreenDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceSocialScreenDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSocialScreenDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSocialScreenDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSocialScreenDialogTerminate(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/prx/libSceVrServiceDialog/CMakeLists.txt b/core/libs/prx/libSceVrServiceDialog/CMakeLists.txt new file mode 100644 index 0000000000..56c0611c39 --- /dev/null +++ b/core/libs/prx/libSceVrServiceDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceVrServiceDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVrServiceDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVrServiceDialog) + +if(BUILD_TESTING) + add_test_executable(guest_vr_service_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrServiceDialog.cpp) + target_include_directories(guest_vr_service_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_service_dialog_tests PRIVATE libSceVrServiceDialog) + add_test(NAME guest_vr_service_dialog COMMAND guest_vr_service_dialog_tests) + if(WIN32) + set_tests_properties(guest_vr_service_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVrServiceDialog/Export.cpp b/core/libs/prx/libSceVrServiceDialog/Export.cpp index 355ae01310..a4c1d1f62e 100644 --- a/core/libs/prx/libSceVrServiceDialog/Export.cpp +++ b/core/libs/prx/libSceVrServiceDialog/Export.cpp @@ -1,42 +1,70 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceVrServiceDialogClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVrServiceDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceVrServiceDialogGetResult() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVrServiceDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceVrServiceDialogGetStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVrServiceDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceVrServiceDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceVrServiceDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); return 0; } -int APS5_VABI sceVrServiceDialogInitialize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVrServiceDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceVrServiceDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrServiceDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrServiceDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVrServiceDialogTerminate(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/GuestHmdSetupDialog.cpp b/core/libs/tests/GuestHmdSetupDialog.cpp new file mode 100644 index 0000000000..7939b5eb8f --- /dev/null +++ b/core/libs/tests/GuestHmdSetupDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceHmdSetupDialogInitialize(void); +int APS5_VABI sceHmdSetupDialogOpen(const void* param); +int APS5_VABI sceHmdSetupDialogUpdateStatus(void); +int APS5_VABI sceHmdSetupDialogGetStatus(void); +int APS5_VABI sceHmdSetupDialogGetResult(void* result); +int APS5_VABI sceHmdSetupDialogClose(void); +int APS5_VABI sceHmdSetupDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceHmdSetupDialogClose() == kErrNotInitialized); + Require(sceHmdSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusNone); + Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); + + Require(sceHmdSetupDialogInitialize() == 0); + Require(sceHmdSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusInitialized); + Require(sceHmdSetupDialogUpdateStatus() == kStatusInitialized); + + Require(sceHmdSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceHmdSetupDialogGetResult(result) == kErrNotFinished); + + Require(sceHmdSetupDialogOpen(¶m) == 0); + Require(sceHmdSetupDialogGetStatus() == kStatusFinished); + Require(sceHmdSetupDialogUpdateStatus() == kStatusFinished); + + Require(sceHmdSetupDialogGetResult(nullptr) == kErrArgNull); + Require(sceHmdSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceHmdSetupDialogClose() == 0); + Require(sceHmdSetupDialogTerminate() == 0); + Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusNone); + Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); + + Require(sceHmdSetupDialogInitialize() == 0); + Require(sceHmdSetupDialogUpdateStatus() == kStatusInitialized); + Require(sceHmdSetupDialogClose() == 0); + Require(sceHmdSetupDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceHmdSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceHmdSetupDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestNpFriendListDialog.cpp b/core/libs/tests/GuestNpFriendListDialog.cpp new file mode 100644 index 0000000000..70385c595c --- /dev/null +++ b/core/libs/tests/GuestNpFriendListDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpFriendListDialogInitialize(void); +int APS5_VABI sceNpFriendListDialogOpenA(const void* param); +int APS5_VABI sceNpFriendListDialogUpdateStatus(void); +int APS5_VABI sceNpFriendListDialogGetStatus(void); +int APS5_VABI sceNpFriendListDialogGetResultA(void* result); +int APS5_VABI sceNpFriendListDialogClose(void); +int APS5_VABI sceNpFriendListDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(¶m) == kErrNotInitialized); + Require(sceNpFriendListDialogClose() == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusNone); + Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); + + Require(sceNpFriendListDialogInitialize() == 0); + Require(sceNpFriendListDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusInitialized); + Require(sceNpFriendListDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpFriendListDialogOpenA(nullptr) == kErrArgNull); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotFinished); + + Require(sceNpFriendListDialogOpenA(¶m) == 0); + Require(sceNpFriendListDialogGetStatus() == kStatusFinished); + Require(sceNpFriendListDialogUpdateStatus() == kStatusFinished); + + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrArgNull); + Require(sceNpFriendListDialogGetResultA(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceNpFriendListDialogClose() == 0); + Require(sceNpFriendListDialogTerminate() == 0); + Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusNone); + Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); + + Require(sceNpFriendListDialogInitialize() == 0); + Require(sceNpFriendListDialogUpdateStatus() == kStatusInitialized); + Require(sceNpFriendListDialogClose() == 0); + Require(sceNpFriendListDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceNpFriendListDialogGetResultA(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceNpFriendListDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp new file mode 100644 index 0000000000..61d42e6488 --- /dev/null +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceSocialScreenDialogInitialize(void); +int APS5_VABI sceSocialScreenDialogOpen(const void* param); +int APS5_VABI sceSocialScreenDialogUpdateStatus(void); +int APS5_VABI sceSocialScreenDialogGetStatus(void); +int APS5_VABI sceSocialScreenDialogGetResult(void* result); +int APS5_VABI sceSocialScreenDialogClose(void); +int APS5_VABI sceSocialScreenDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(¶m) == kErrNotInitialized); + Require(sceSocialScreenDialogClose() == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(result) == kErrNotInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusNone); + Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); + + Require(sceSocialScreenDialogInitialize() == 0); + Require(sceSocialScreenDialogInitialize() == kErrAlreadyInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusInitialized); + Require(sceSocialScreenDialogUpdateStatus() == kStatusInitialized); + + Require(sceSocialScreenDialogOpen(nullptr) == kErrArgNull); + Require(sceSocialScreenDialogGetResult(result) == kErrNotFinished); + + Require(sceSocialScreenDialogOpen(¶m) == 0); + Require(sceSocialScreenDialogGetStatus() == kStatusFinished); + Require(sceSocialScreenDialogUpdateStatus() == kStatusFinished); + + Require(sceSocialScreenDialogGetResult(nullptr) == kErrArgNull); + Require(sceSocialScreenDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceSocialScreenDialogClose() == 0); + Require(sceSocialScreenDialogTerminate() == 0); + Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusNone); + Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); + + Require(sceSocialScreenDialogInitialize() == 0); + Require(sceSocialScreenDialogUpdateStatus() == kStatusInitialized); + Require(sceSocialScreenDialogClose() == 0); + Require(sceSocialScreenDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceSocialScreenDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceSocialScreenDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestVrServiceDialog.cpp b/core/libs/tests/GuestVrServiceDialog.cpp new file mode 100644 index 0000000000..8d72100186 --- /dev/null +++ b/core/libs/tests/GuestVrServiceDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceVrServiceDialogInitialize(void); +int APS5_VABI sceVrServiceDialogOpen(const void* param); +int APS5_VABI sceVrServiceDialogUpdateStatus(void); +int APS5_VABI sceVrServiceDialogGetStatus(void); +int APS5_VABI sceVrServiceDialogGetResult(void* result); +int APS5_VABI sceVrServiceDialogClose(void); +int APS5_VABI sceVrServiceDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrServiceDialogClose() == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(result) == kErrNotInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusNone); + Require(sceVrServiceDialogUpdateStatus() == kStatusNone); + + Require(sceVrServiceDialogInitialize() == 0); + Require(sceVrServiceDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusInitialized); + Require(sceVrServiceDialogUpdateStatus() == kStatusInitialized); + + Require(sceVrServiceDialogOpen(nullptr) == kErrArgNull); + Require(sceVrServiceDialogGetResult(result) == kErrNotFinished); + + Require(sceVrServiceDialogOpen(¶m) == 0); + Require(sceVrServiceDialogGetStatus() == kStatusFinished); + Require(sceVrServiceDialogUpdateStatus() == kStatusFinished); + + Require(sceVrServiceDialogGetResult(nullptr) == kErrArgNull); + Require(sceVrServiceDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceVrServiceDialogClose() == 0); + Require(sceVrServiceDialogTerminate() == 0); + Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusNone); + Require(sceVrServiceDialogUpdateStatus() == kStatusNone); + + Require(sceVrServiceDialogInitialize() == 0); + Require(sceVrServiceDialogUpdateStatus() == kStatusInitialized); + Require(sceVrServiceDialogClose() == 0); + Require(sceVrServiceDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceVrServiceDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceVrServiceDialogTerminate() == 0); +} diff --git a/docs/dev/debt/libSceHmdSetupDialog.md b/docs/dev/debt/libSceHmdSetupDialog.md index cd9e1a5ae2..1566cbc62a 100644 --- a/docs/dev/debt/libSceHmdSetupDialog.md +++ b/docs/dev/debt/libSceHmdSetupDialog.md @@ -1,5 +1,5 @@ # libSceHmdSetupDialog technical debt -### Unknown function info +### Silent stubs -- [sceHmdSetupDialogClose, sceHmdSetupDialogGetResult, sceHmdSetupDialogGetStatus, sceHmdSetupDialogInitialize, sceHmdSetupDialogOpen, sceHmdSetupDialogTerminate and sceHmdSetupDialogUpdateStatus](../../../core/libs/prx/libSceHmdSetupDialog/Export.cpp) (libSceHmdSetupDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHmdSetupDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [libSceHmdSetupDialog](../../../core/libs/prx/libSceHmdSetupDialog/Export.cpp) - the HMD setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. diff --git a/docs/dev/debt/libSceNpFriendListDialog.md b/docs/dev/debt/libSceNpFriendListDialog.md index 1c3824ba59..2d5b73e230 100644 --- a/docs/dev/debt/libSceNpFriendListDialog.md +++ b/docs/dev/debt/libSceNpFriendListDialog.md @@ -1,5 +1,9 @@ # libSceNpFriendListDialog technical debt +### Silent stubs + +- [libSceNpFriendListDialog](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) - the friend list dialog (`sceNpFriendListDialogOpenA` and `sceNpFriendListDialogGetResultA`) runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN friend list. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. + ### Unknown function info -- [sceNpFriendListDialogClose, sceNpFriendListDialogGetResult, sceNpFriendListDialogGetResultA, sceNpFriendListDialogGetStatus, sceNpFriendListDialogInitialize, sceNpFriendListDialogOpen, sceNpFriendListDialogOpenA, sceNpFriendListDialogTerminate and sceNpFriendListDialogUpdateStatus](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) (libSceNpFriendListDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpFriendListDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpFriendListDialogGetResult and sceNpFriendListDialogOpen](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) (libSceNpFriendListDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpFriendListDialog.c), but Orbital has prototypes only for the `A` variants and no credible prototypes were found for these. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSocialScreenDialog.md b/docs/dev/debt/libSceSocialScreenDialog.md index a1b39f0af9..b1b16a216d 100644 --- a/docs/dev/debt/libSceSocialScreenDialog.md +++ b/docs/dev/debt/libSceSocialScreenDialog.md @@ -1,5 +1,5 @@ # libSceSocialScreenDialog technical debt -### Unknown function info +### Silent stubs -- [sceSocialScreenDialogClose, sceSocialScreenDialogGetResult, sceSocialScreenDialogGetStatus, sceSocialScreenDialogInitialize, sceSocialScreenDialogOpen, sceSocialScreenDialogTerminate and sceSocialScreenDialogUpdateStatus](../../../core/libs/prx/libSceSocialScreenDialog/Export.cpp) (libSceSocialScreenDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSocialScreenDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [libSceSocialScreenDialog](../../../core/libs/prx/libSceSocialScreenDialog/Export.cpp) - the social screen dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no second-screen viewer. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. diff --git a/docs/dev/debt/libSceVrServiceDialog.md b/docs/dev/debt/libSceVrServiceDialog.md index f6be543e6d..fe48267534 100644 --- a/docs/dev/debt/libSceVrServiceDialog.md +++ b/docs/dev/debt/libSceVrServiceDialog.md @@ -1,5 +1,5 @@ # libSceVrServiceDialog technical debt -### Unknown function info +### Silent stubs -- [sceVrServiceDialogClose, sceVrServiceDialogGetResult, sceVrServiceDialogGetStatus, sceVrServiceDialogInitialize, sceVrServiceDialogOpen, sceVrServiceDialogTerminate and sceVrServiceDialogUpdateStatus](../../../core/libs/prx/libSceVrServiceDialog/Export.cpp) (libSceVrServiceDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrServiceDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [libSceVrServiceDialog](../../../core/libs/prx/libSceVrServiceDialog/Export.cpp) - the VR service dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. From 0858e024fbccfccf2921e616c91a6290ab0c96ce Mon Sep 17 00:00:00 2001 From: KernelPanicNT Date: Wed, 21 Oct 2026 14:54:33 +0000 Subject: [PATCH 0408/1360] feat(libkernel_sys): implement lchmod lchmod forwards to fchmodat with AT_FDCWD and AT_SYMLINK_NOFOLLOW, as FreeBSD 11.4's sys_lchmod does. On Linux and Windows, which cannot change the mode of a symbolic link itself, a symbolic link throws instead of succeeding without a change. --- .../prx/libkernel_sys/Export.Discovery.cpp | 5 --- core/libs/prx/libkernel_sys/Export.cpp | 17 ++++++++++ core/libs/tests/GuestKernelSys.cpp | 33 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..7d384cb55a 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -313,11 +313,6 @@ int APS5_VABI lchflags_nid_postfix(void) { return 0; } -int APS5_VABI lchmod_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI lchown_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..cfb1eb212d 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -532,6 +532,23 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) return 0; } +int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { +#ifndef __APPLE__ + std::filesystem::path native; + if (path != nullptr && *path != '\0' && ResolvePathError_nid_no_patch(path, native) == 0) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code status; + const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); +#endif + if (symbolic) NotImplemented_nid_no_patch("lchmod: the mode of a symbolic link on a host that cannot change it"); + } +#endif + return fchmodat_nid_postfix(GuestAtFdcwd, path, mode, GuestAtSymlinkNofollow); +} + int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch("fchown: an owner other than the guest user"); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a60abca020..57bc914a1e 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -55,6 +55,7 @@ std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); +int APS5_VABI lchmod_nid_postfix(const char*, std::uint16_t); int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); @@ -636,6 +637,38 @@ int main() { Require(symlink_nid_postfix("loop", (root / "loop").string().c_str()) == 0); Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); +#endif + const auto modeFile = (root / "file.txt").string(); +#ifndef _WIN32 + const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; + const auto readable = ownerOnly | std::filesystem::perms::group_read | std::filesystem::perms::others_read; +#endif + Require(lchmod_nid_postfix(modeFile.c_str(), 0600) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == ownerOnly); +#endif + Require(lchmod_nid_postfix(guestFile.c_str(), 0100644) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); +#endif + Require(lchmod_nid_postfix((root / "missing.bin").string().c_str(), 0600) == -1 && Error() == 2); + Require(lchmod_nid_postfix(nullptr, 0600) == -1 && Error() == 14); + Require(lchmod_nid_postfix("", 0600) == -1 && Error() == 2); +#ifndef _WIN32 + Require(lchmod_nid_postfix((root / "loop" / "x").string().c_str(), 0600) == -1 && Error() == 62); + Require(lchmod_nid_postfix((root / longName).string().c_str(), 0600) == -1 && Error() == 63); + const auto modeLink = (root / "mode-link.txt").string(); + Require(symlink_nid_postfix("file.txt", modeLink.c_str()) == 0); +#ifdef __APPLE__ + Require(lchmod_nid_postfix(modeLink.c_str(), 0600) == 0); + Require((std::filesystem::symlink_status(modeLink).permissions() & std::filesystem::perms::all) == ownerOnly); +#else + bool lchmodThrew = false; + try { lchmod_nid_postfix(modeLink.c_str(), 0600); } catch (const std::exception&) { lchmodThrew = true; } + Require(lchmodThrew); +#endif + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); + Require(RemoveSymlink(modeLink)); #endif Require(fchown_nid_postfix(base, 0, 0) == 0); Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0xFFFFFFFFu) == 0); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..8a56890c7f 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -8,6 +8,7 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified +- [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link From b53ca6243db59531d5de5658a35c6ab90fcf8275 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 16:59:49 +0200 Subject: [PATCH 0409/1360] feat(libSceNpParty): report no party from the remaining signed exports Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpParty/Export.cpp | 62 +++++++++-------- core/libs/tests/GuestNpParty.cpp | 92 +++++++++++++++++++++----- docs/dev/debt/libSceNpParty.md | 5 +- 3 files changed, 116 insertions(+), 43 deletions(-) diff --git a/core/libs/prx/libSceNpParty/Export.cpp b/core/libs/prx/libSceNpParty/Export.cpp index 67fdadc679..305da3f6cc 100644 --- a/core/libs/prx/libSceNpParty/Export.cpp +++ b/core/libs/prx/libSceNpParty/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include "SceTypes.hpp" @@ -5,9 +6,19 @@ namespace { +constexpr int NP_PARTY_ERROR_ALREADY_INITIALIZED = static_cast(0x80552502u); +constexpr int NP_PARTY_ERROR_NOT_INITIALIZED = static_cast(0x80552503u); constexpr int NP_PARTY_ERROR_INVALID_ARGUMENT = static_cast(0x80552504u); +constexpr int NP_PARTY_ERROR_NOT_IN_PARTY = static_cast(0x80552506u); constexpr std::uint16_t NP_PARTY_STATE_NOT_IN_PARTY = 2; +std::atomic g_initialized{false}; + +int notInParty(const void* out) { + if (!out) return NP_PARTY_ERROR_INVALID_ARGUMENT; + return NP_PARTY_ERROR_NOT_IN_PARTY; +} + } extern "C" { @@ -17,8 +28,9 @@ int APS5_VABI sceNpPartyGetState(std::uint16_t* state) { *state = NP_PARTY_STATE_NOT_IN_PARTY; return 0; } + int APS5_VABI sceNpPartyCheckCallback(void) { - NotImplemented_nid_no_patch(__func__); + if (!g_initialized) return NP_PARTY_ERROR_NOT_INITIALIZED; return 0; } @@ -32,9 +44,8 @@ int APS5_VABI sceNpPartyCreateA(void) { return 0; } -int APS5_VABI sceNpPartyGetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetId(void* id) { + return notInParty(id); } int APS5_VABI sceNpPartyGetMemberInfo(void) { @@ -42,19 +53,16 @@ int APS5_VABI sceNpPartyGetMemberInfo(void) { return 0; } -int APS5_VABI sceNpPartyGetMemberInfoA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetMemberInfoA(std::uint16_t, void* memberInfo) { + return notInParty(memberInfo); } -int APS5_VABI sceNpPartyGetMemberSessionInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetMemberSessionInfo(std::uint16_t, void* memberGameSessionInfo) { + return notInParty(memberGameSessionInfo); } -int APS5_VABI sceNpPartyGetMemberVoiceInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetMemberVoiceInfo(std::uint16_t, std::uint16_t, std::uint8_t* memberVoiceState) { + return notInParty(memberVoiceState); } int APS5_VABI sceNpPartyGetMembers(void) { @@ -62,9 +70,8 @@ int APS5_VABI sceNpPartyGetMembers(void) { return 0; } -int APS5_VABI sceNpPartyGetMembersA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetMembersA(void* memberList) { + return notInParty(memberList); } int APS5_VABI sceNpPartyGetStateAsUser(void) { @@ -72,9 +79,8 @@ int APS5_VABI sceNpPartyGetStateAsUser(void) { return 0; } -int APS5_VABI sceNpPartyGetStateAsUserA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartyGetStateAsUserA(std::int32_t, std::uint16_t* state) { + return sceNpPartyGetState(state); } int APS5_VABI sceNpPartyGetVoiceChatPriority(void) { @@ -82,8 +88,10 @@ int APS5_VABI sceNpPartyGetVoiceChatPriority(void) { return 0; } -int APS5_VABI sceNpPartyInitialize(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpPartyInitialize(const void* param) { + if (!param) return NP_PARTY_ERROR_INVALID_ARGUMENT; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) return NP_PARTY_ERROR_ALREADY_INITIALIZED; return 0; } @@ -102,8 +110,9 @@ int APS5_VABI sceNpPartyRegisterHandler(void) { return 0; } -int APS5_VABI sceNpPartyRegisterHandlerA(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpPartyRegisterHandlerA(const void* handlers, void*) { + if (!g_initialized) return NP_PARTY_ERROR_NOT_INITIALIZED; + if (!handlers) return NP_PARTY_ERROR_INVALID_ARGUMENT; return 0; } @@ -112,9 +121,8 @@ int APS5_VABI sceNpPartyRegisterPrivateHandler(void) { return 0; } -int APS5_VABI sceNpPartySendBinaryMessage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpPartySendBinaryMessage(const void* sendParams) { + return notInParty(sendParams); } int APS5_VABI sceNpPartySetVoiceChatPriority(void) { @@ -133,7 +141,7 @@ int APS5_VABI sceNpPartyShowInvitationListA(void) { } int APS5_VABI sceNpPartyTerminate(void) { - NotImplemented_nid_no_patch(__func__); + if (!g_initialized.exchange(false)) return NP_PARTY_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/tests/GuestNpParty.cpp b/core/libs/tests/GuestNpParty.cpp index d47398d76a..e60834546c 100644 --- a/core/libs/tests/GuestNpParty.cpp +++ b/core/libs/tests/GuestNpParty.cpp @@ -4,23 +4,85 @@ #include #include -extern "C" int APS5_VABI sceNpPartyGetState(std::uint16_t* state); +extern "C" { +int APS5_VABI sceNpPartyGetState(std::uint16_t* state); +int APS5_VABI sceNpPartyInitialize(const void* param); +int APS5_VABI sceNpPartyTerminate(void); +int APS5_VABI sceNpPartyCheckCallback(void); +int APS5_VABI sceNpPartyRegisterHandlerA(const void* handlers, void* userdata); +int APS5_VABI sceNpPartyGetStateAsUserA(std::int32_t asUserId, std::uint16_t* state); +int APS5_VABI sceNpPartyGetId(void* id); +int APS5_VABI sceNpPartyGetMembersA(void* memberList); +int APS5_VABI sceNpPartyGetMemberInfoA(std::uint16_t memberId, void* memberInfo); +int APS5_VABI sceNpPartyGetMemberSessionInfo(std::uint16_t memberId, void* memberGameSessionInfo); +int APS5_VABI sceNpPartyGetMemberVoiceInfo(std::uint16_t asMemberId, std::uint16_t memberId, std::uint8_t* memberVoiceState); +int APS5_VABI sceNpPartySendBinaryMessage(const void* sendParams); +} -int main() { +namespace { + +constexpr int kAlreadyInitialized = static_cast(0x80552502u); +constexpr int kNotInitialized = static_cast(0x80552503u); +constexpr int kInvalidArgument = static_cast(0x80552504u); +constexpr int kNotInParty = static_cast(0x80552506u); + +bool check(bool value, const char* message) { + if (!value) std::fprintf(stderr, "%s\n", message); + return value; +} + +bool stateTests() { std::uint16_t states[3]; std::memset(states, 0xa5, sizeof(states)); - if (sceNpPartyGetState(nullptr) != static_cast(0x80552504u)) { - std::fprintf(stderr, "a null state was not refused with INVALID_ARGUMENT\n"); - return 1; - } - if (sceNpPartyGetState(&states[1]) != 0 || states[1] != 2) { - std::fprintf(stderr, "the state is not NOT_IN_PARTY\n"); - return 1; - } - if (states[0] != 0xa5a5 || states[2] != 0xa5a5) { - std::fprintf(stderr, "sceNpPartyGetState wrote past its 16-bit state\n"); - return 1; - } - std::puts("NpParty state tests passed"); + return check(sceNpPartyGetState(nullptr) == kInvalidArgument, "a null state was not refused with INVALID_ARGUMENT") + && check(sceNpPartyGetState(&states[1]) == 0 && states[1] == 2, "the state is not NOT_IN_PARTY") + && check(states[0] == 0xa5a5 && states[2] == 0xa5a5, "sceNpPartyGetState wrote past its 16-bit state") + && check(sceNpPartyGetStateAsUserA(1, nullptr) == kInvalidArgument, "a null user state was not refused with INVALID_ARGUMENT") + && check(sceNpPartyGetStateAsUserA(1, &states[0]) == 0 && states[0] == 2, "the user state is not NOT_IN_PARTY") + && check(states[2] == 0xa5a5, "sceNpPartyGetStateAsUserA wrote past its 16-bit state"); +} + +bool lifecycleTests() { + std::uint64_t param[4] = {}; + std::uint64_t handlers[8] = {}; + return check(sceNpPartyTerminate() == kNotInitialized, "terminate before initialize did not fail") + && check(sceNpPartyCheckCallback() == kNotInitialized, "check callback before initialize did not fail") + && check(sceNpPartyRegisterHandlerA(handlers, nullptr) == kNotInitialized, "register handler before initialize did not fail") + && check(sceNpPartyInitialize(nullptr) == kInvalidArgument, "a null initialize param was not refused") + && check(sceNpPartyInitialize(param) == 0, "initialize failed") + && check(sceNpPartyInitialize(param) == kAlreadyInitialized, "a second initialize did not fail") + && check(sceNpPartyRegisterHandlerA(nullptr, nullptr) == kInvalidArgument, "null handlers were not refused") + && check(sceNpPartyRegisterHandlerA(handlers, param) == 0, "register handler failed") + && check(sceNpPartyCheckCallback() == 0, "check callback failed") + && check(sceNpPartyTerminate() == 0, "terminate failed") + && check(sceNpPartyTerminate() == kNotInitialized, "a second terminate did not fail") + && check(sceNpPartyInitialize(param) == 0, "initialize after terminate failed") + && check(sceNpPartyTerminate() == 0, "terminate after reinitialize failed"); +} + +bool notInPartyTests() { + std::uint8_t out[64]; + std::memset(out, 0xa5, sizeof(out)); + bool ok = check(sceNpPartyGetId(nullptr) == kInvalidArgument, "a null party id was not refused") + && check(sceNpPartyGetId(out) == kNotInParty, "get id did not report NOT_IN_PARTY") + && check(sceNpPartyGetMembersA(nullptr) == kInvalidArgument, "a null member list was not refused") + && check(sceNpPartyGetMembersA(out) == kNotInParty, "get members did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberInfoA(1, nullptr) == kInvalidArgument, "a null member info was not refused") + && check(sceNpPartyGetMemberInfoA(1, out) == kNotInParty, "get member info did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberSessionInfo(1, nullptr) == kInvalidArgument, "a null session info was not refused") + && check(sceNpPartyGetMemberSessionInfo(1, out) == kNotInParty, "get session info did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberVoiceInfo(1, 2, nullptr) == kInvalidArgument, "a null voice state was not refused") + && check(sceNpPartyGetMemberVoiceInfo(1, 2, out) == kNotInParty, "get voice info did not report NOT_IN_PARTY") + && check(sceNpPartySendBinaryMessage(nullptr) == kInvalidArgument, "null send params were not refused") + && check(sceNpPartySendBinaryMessage(out) == kNotInParty, "send did not report NOT_IN_PARTY"); + for (std::uint8_t byte : out) ok = ok && check(byte == 0xa5, "a NOT_IN_PARTY call wrote its output"); + return ok; +} + +} + +int main() { + if (!stateTests() || !lifecycleTests() || !notInPartyTests()) return 1; + std::puts("NpParty tests passed"); return 0; } diff --git a/docs/dev/debt/libSceNpParty.md b/docs/dev/debt/libSceNpParty.md index 45d68abf16..e88c71d92d 100644 --- a/docs/dev/debt/libSceNpParty.md +++ b/docs/dev/debt/libSceNpParty.md @@ -2,4 +2,7 @@ ### Unknown function info -- [sceNpPartyGetState](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the 16-bit state, its `NOT_IN_PARTY` value 2 and the `INVALID_ARGUMENT` (`0x80552504`) error follow shadPS4's [np_party.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_party.h) and [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_error.h) (PS4); shadPS4 does not check for a null state, and whether the console needs an initialized party library first is not known. Not measured on a PS5 +- [sceNpPartyGetState and sceNpPartyGetStateAsUserA](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the 16-bit state, its `NOT_IN_PARTY` value 2 and the `INVALID_ARGUMENT` (`0x80552504`) error follow shadPS4's [np_party.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_party.h) and [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_error.h) (PS4); shadPS4 does not check for a null state, and whether the console needs an initialized party library first is not known. Not measured on a PS5 +- [sceNpPartyGetId, sceNpPartyGetMembersA, sceNpPartyGetMemberInfoA, sceNpPartyGetMemberSessionInfo, sceNpPartyGetMemberVoiceInfo and sceNpPartySendBinaryMessage](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the user is never in a party, so these return `NOT_IN_PARTY` (`0x80552506`, shadPS4's np_error.h) and write nothing, or `INVALID_ARGUMENT` for a null pointer; the signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json). Like `sceNpPartyGetState` they do not require `sceNpPartyInitialize`, and the order of the null and party checks on the console is not known. Not measured on a PS5 +- [sceNpPartyInitialize, sceNpPartyTerminate, sceNpPartyCheckCallback and sceNpPartyRegisterHandlerA](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - an initialized flag with `ALREADY_INITIALIZED` (`0x80552502`) and `NOT_INITIALIZED` (`0x80552503`) from shadPS4's np_error.h; the handlers are accepted but never called, since no party event can happen. Whether the console refuses a null initialize parameter, and which check comes first, is not known +- [sceNpPartyCreate, sceNpPartyCreateA, sceNpPartyGetMemberInfo, sceNpPartyGetMembers, sceNpPartyGetStateAsUser, sceNpPartyGetVoiceChatPriority, sceNpPartyJoin, sceNpPartyLeave, sceNpPartyRegisterHandler, sceNpPartyRegisterPrivateHandler, sceNpPartySetVoiceChatPriority, sceNpPartyShowInvitationList, sceNpPartyShowInvitationListA and sceNpPartyUnregisterPrivateHandler](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - Orbital has no prototypes for these and no credible ones were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified From 0ac2b0bc6f5765708201d09e5b0ea402bbac106f Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 20 Oct 2026 22:50:09 +0200 Subject: [PATCH 0410/1360] feat(libkernel): export the underscore entry points of the socket, directory and signal calls The PS5 libkernel exports _accept, _bind, _connect, _getdirentries, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt, _sigaction and _sigsuspend (PS5 3.20 export list, DNNDHH/PS5-3.20_Libs b2a77c0 libkernel.c), and main exports none of them, so a title importing one fails at startup. Each now forwards to the export without the underscore, as _close, _ioctl and _nanosleep already do. In FreeBSD 11.4 (8c639945a4) these are libc's plain system call entries: lib/libc/amd64/SYS.h makes bind, getdirentries, getpeername, getsockname, getsockopt, listen and setsockopt one stub under both names, and lib/libc/sys/Makefile.inc gives accept, connect, recvfrom, recvmsg, sendmsg, sendto, sigaction and sigsuspend a separate public name that goes through libthr's interposing table, which adds the cancellation points and the sigaction wrapper. The exports without the underscore have no libthr layer here, so forwarding gives the system call. docs/dev/debt/libkernel.md records what stays unverified: the console is not measured, _sigaction shares the handler table of sigaction, and a null set throws in _sigsuspend as in sigsuspend, where FreeBSD's system call returns EFAULT. guest_underscore_entries calls every new export: a loopback TCP connection, UDP datagrams with scatter, truncation and sender addresses, a directory listing compared with getdirentries, a SIGTERM handler set by _sigaction, and a pending SIGUSR1 that _sigsuspend delivers before returning EINTR with the mask restored. It checks the errors they pass on (EBADF, EINVAL, EFAULT, ENOPROTOOPT, EOPNOTSUPP) and that outputs stay untouched on failure. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 + core/libs/prx/libkernel/File/src/Stdio.cpp | 4 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 41 +++ .../libs/prx/libkernel/System/src/Signals.cpp | 7 + core/libs/tests/GuestUnderscoreEntries.cpp | 330 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 6 files changed, 393 insertions(+) create mode 100644 core/libs/tests/GuestUnderscoreEntries.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..57626d6a00 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1549,6 +1549,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_connect_listening PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_underscore_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestUnderscoreEntries.cpp) + target_include_directories(guest_underscore_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_underscore_entry_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_underscore_entry_tests) + add_test(NAME guest_underscore_entries COMMAND guest_underscore_entry_tests) + set_tests_properties(guest_underscore_entries PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_underscore_entries PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_elf_phdr_match_tests ${LIBS_INCLUDE_DIR}/tests/GuestElfPhdrMatch.cpp) target_include_directories(guest_elf_phdr_match_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_elf_phdr_match_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2146bbe2d6..8a73e8fc12 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1173,6 +1173,10 @@ int APS5_VABI getdirentries_nid_postfix(int fd, char* buf, int nbytes, int64_t* return PosixResult(sceKernelGetdirentries(fd, buf, nbytes, basep)); } +int APS5_VABI _getdirentries_nid_postfix(int fd, char* buf, int nbytes, int64_t* basep) { + return getdirentries_nid_postfix(fd, buf, nbytes, basep); +} + int APS5_VABI getdents_nid_postfix(int fd, char* buf, int nbytes) { return PosixResult(sceKernelGetdents(fd, buf, nbytes)); } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fbf308a2e4..0e8ed98cd7 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1824,3 +1824,44 @@ std::int64_t GuestSockets::Writev(int descriptor, const Iovec* iov, int iovcnt) } return Write(descriptor, buffer.data(), total); } + +extern "C" { +int APS5_VABI _setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { + return setsockopt_nid_postfix(descriptor, level, option, value, length); +} +int APS5_VABI _getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { + return getsockopt_nid_postfix(descriptor, level, option, value, length); +} +int APS5_VABI _connect_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + return connect_nid_postfix(descriptor, address, length); +} +int APS5_VABI _listen_nid_postfix(int descriptor, int backlog) { + return listen_nid_postfix(descriptor, backlog); +} +int APS5_VABI _accept_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + return accept_nid_postfix(descriptor, address, length); +} +int APS5_VABI _bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + return bind_nid_postfix(descriptor, address, length); +} +int APS5_VABI _getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + return getsockname_nid_postfix(descriptor, address, length); +} +int APS5_VABI _getpeername_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + return getpeername_nid_postfix(descriptor, address, length); +} +std::int64_t APS5_VABI _sendto_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, + int flags, const void* address, std::uint32_t addressLength) { + return sendto_nid_postfix(descriptor, buffer, length, flags, address, addressLength); +} +std::int64_t APS5_VABI _recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, + int flags, void* address, std::uint32_t* addressLength) { + return recvfrom_nid_postfix(descriptor, buffer, length, flags, address, addressLength); +} +std::int64_t APS5_VABI _sendmsg_nid_postfix(int descriptor, const GuestMsghdr* message, int flags) { + return sendmsg_nid_postfix(descriptor, message, flags); +} +std::int64_t APS5_VABI _recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { + return recvmsg_nid_postfix(descriptor, message, flags); +} +} diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 231d3d2643..80c15cf21e 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -499,6 +499,9 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue if (previous) *previous = old; return 0; } +int APS5_VABI _sigaction_nid_postfix(int guest, const GuestSigaction* action, GuestSigaction* previous) { + return sigaction_nid_postfix(guest, action, previous); +} int APS5_VABI raise_nid_postfix(int guest) { if (guest == 0) return 0; if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } @@ -644,6 +647,10 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { return -1; } +int APS5_VABI _sigsuspend_nid_postfix(const GuestSignalSet* set) { + return sigsuspend_nid_postfix(set); +} + int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } auto& pending = Pending(); diff --git a/core/libs/tests/GuestUnderscoreEntries.cpp b/core/libs/tests/GuestUnderscoreEntries.cpp new file mode 100644 index 0000000000..edb44de05b --- /dev/null +++ b/core/libs/tests/GuestUnderscoreEntries.cpp @@ -0,0 +1,330 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +struct GuestSigaction { + std::uintptr_t handler; + int flags; + GuestSignalSet mask; +}; +static_assert(sizeof(GuestSigaction) == 32); +struct Iovec { + void* base; + std::uint64_t length; +}; +struct Msghdr { + void* name; + std::uint32_t nameLength; + Iovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; +static_assert(sizeof(Msghdr) == 48); + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI _bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI _listen_nid_postfix(int, int); +int APS5_VABI _accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI _connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI _getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI _getpeername_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI _setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI _getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI _sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI _recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI _sendmsg_nid_postfix(int, const Msghdr*, int); +std::int64_t APS5_VABI _recvmsg_nid_postfix(int, Msghdr*, int); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI getdirentries_nid_postfix(int, char*, int, std::int64_t*); +int APS5_VABI _getdirentries_nid_postfix(int, char*, int, std::int64_t*); +int APS5_VABI raise_nid_postfix(int); +int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); +int APS5_VABI _sigaction_nid_postfix(int, const GuestSigaction*, GuestSigaction*); +int APS5_VABI _sigsuspend_nid_postfix(const GuestSignalSet*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "underscore entry check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int Inet = 2; +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int TcpLevel = 6; +constexpr int TcpNoDelay = 1; +constexpr int TcpMaximumSegment = 2; +constexpr int OutOfBand = 0x1; +constexpr int Truncated = 0x10; +constexpr int Interrupted = 4; +constexpr int BadDescriptor = 9; +constexpr int BadAddress = 14; +constexpr int Invalid = 22; +constexpr int NoProtocolOption = 42; +constexpr int NotSupported = 45; + +using Address = std::array; + +static bool Fails(std::int64_t result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static bool Filled(const Address& address, std::uint8_t value) { + return std::all_of(address.begin(), address.end(), [value](std::uint8_t byte) { return byte == value; }); +} + +static void StreamSockets() { + const int listener = socket_nid_postfix(Inet, Stream, 0); + Require(listener >= 0); + Address address{16, Inet, 0, 0, 127, 0, 0, 1}; + Require(_bind_nid_postfix(listener, address.data(), address.size()) == 0); + Require(Fails(_bind_nid_postfix(listener, address.data(), address.size()), Invalid)); + Require(_listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(_getsockname_nid_postfix(listener, address.data(), &size) == 0 && size == 16); + Require(address[0] == 16 && address[1] == Inet && (address[2] != 0 || address[3] != 0) && address[4] == 127 && address[7] == 1); + + const int client = socket_nid_postfix(Inet, Stream, 0); + Require(client >= 0); + Require(Fails(_accept_nid_postfix(client, nullptr, nullptr), Invalid)); + Require(_connect_nid_postfix(client, address.data(), address.size()) == 0); + Address peer; + peer.fill(0xa5); + size = peer.size(); + const int accepted = _accept_nid_postfix(listener, peer.data(), &size); + Require(accepted >= 0 && size == 16); + Address local{}; + size = local.size(); + Require(_getsockname_nid_postfix(client, local.data(), &size) == 0 && size == 16 && local == peer); + Address remote{}; + size = remote.size(); + Require(_getpeername_nid_postfix(client, remote.data(), &size) == 0 && size == 16 && remote == address); + remote.fill(0); + size = remote.size(); + Require(_getpeername_nid_postfix(accepted, remote.data(), &size) == 0 && size == 16 && remote == local); + + int value = -1; + std::uint32_t valueSize = sizeof(value); + Require(_getsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &value, &valueSize) == 0 && value == 0 && valueSize == sizeof(value)); + const int enabled = 1; + Require(_setsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &enabled, sizeof(enabled)) == 0); + valueSize = sizeof(value); + Require(_getsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &value, &valueSize) == 0 && value == 4 && valueSize == sizeof(value)); + Require(Fails(_setsockopt_nid_postfix(client, TcpLevel, TcpMaximumSegment, &enabled, sizeof(enabled)), NoProtocolOption)); + + const char text[] = "underscore stream"; + Require(_sendto_nid_postfix(client, text, sizeof(text), 0, nullptr, 0) == sizeof(text)); + char received[sizeof(text)]{}; + Address sender; + sender.fill(0xa5); + std::uint32_t senderSize = sender.size(); + Require(_recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == sizeof(text)); + Require(std::memcmp(received, text, sizeof(text)) == 0 && senderSize == 0 && Filled(sender, 0xa5)); + + for (const int descriptor : {accepted, client, listener}) Require(close_nid_postfix(descriptor) == 0); + Address untouched; + untouched.fill(0xa5); + std::uint32_t untouchedSize = 7; + value = -1; + Require(Fails(_getsockname_nid_postfix(listener, untouched.data(), &untouchedSize), BadDescriptor)); + Require(Fails(_getpeername_nid_postfix(client, untouched.data(), &untouchedSize), BadDescriptor)); + Require(Fails(_accept_nid_postfix(listener, untouched.data(), &untouchedSize), BadDescriptor)); + Require(Fails(_getsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &value, &untouchedSize), BadDescriptor)); + Require(Fails(_listen_nid_postfix(listener, 4), BadDescriptor)); + Require(Filled(untouched, 0xa5) && untouchedSize == 7 && value == -1); +} + +static void DatagramSockets() { + const int receiver = socket_nid_postfix(Inet, Datagram, 0); + const int sender = socket_nid_postfix(Inet, Datagram, 0); + Require(receiver >= 0 && sender >= 0); + Address destination{16, Inet, 0, 0, 127, 0, 0, 1}; + Require(_bind_nid_postfix(receiver, destination.data(), destination.size()) == 0); + std::uint32_t size = destination.size(); + Require(_getsockname_nid_postfix(receiver, destination.data(), &size) == 0 && size == 16 && (destination[2] != 0 || destination[3] != 0)); + Require(Fails(_listen_nid_postfix(receiver, 1), NotSupported)); + + const char datagram[] = "underscore datagram"; + Require(Fails(_sendto_nid_postfix(sender, datagram, sizeof(datagram), OutOfBand, destination.data(), destination.size()), NotSupported)); + Require(_sendto_nid_postfix(sender, datagram, sizeof(datagram), 0, destination.data(), destination.size()) == sizeof(datagram)); + Address senderName{}; + size = senderName.size(); + Require(_getsockname_nid_postfix(sender, senderName.data(), &size) == 0 && size == 16); + char buffer[32]{}; + Address source{}; + size = source.size(); + Require(_recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, source.data(), &size) == sizeof(datagram)); + Require(std::strcmp(buffer, datagram) == 0 && size == 16 && source[0] == 16 && source[1] == Inet); + Require(source[2] == senderName[2] && source[3] == senderName[3] && source[4] == 127 && source[7] == 1); + + char head[] = "scatter "; + char tail[] = "gather"; + Iovec parts[2]{{head, 8}, {tail, 6}}; + Msghdr outgoing{destination.data(), static_cast(destination.size()), parts, 2, nullptr, 0, 0}; + Require(_sendmsg_nid_postfix(sender, &outgoing, 0) == 14); + char first[4]{}; + char second[16]{}; + Iovec pieces[2]{{first, sizeof(first)}, {second, sizeof(second)}}; + source.fill(0); + Msghdr incoming{source.data(), static_cast(source.size()), pieces, 2, nullptr, 0, -1}; + Require(_recvmsg_nid_postfix(receiver, &incoming, 0) == 14); + Require(std::memcmp(first, "scat", 4) == 0 && std::memcmp(second, "ter gather", 10) == 0); + Require(incoming.flags == 0 && incoming.nameLength == 16 && source[2] == senderName[2] && source[3] == senderName[3] && source[4] == 127); + Require(_sendmsg_nid_postfix(sender, &outgoing, 0) == 14); + char shortBuffer[5]{}; + Iovec single{shortBuffer, sizeof(shortBuffer)}; + Msghdr truncated{nullptr, 0, &single, 1, nullptr, 0, 0}; + Require(_recvmsg_nid_postfix(receiver, &truncated, 0) == 5 && std::memcmp(shortBuffer, "scatt", 5) == 0 && truncated.flags == Truncated); + Require(Fails(_sendmsg_nid_postfix(sender, nullptr, 0), BadAddress)); + Require(Fails(_recvmsg_nid_postfix(receiver, nullptr, 0), BadAddress)); + + Require(Fails(_connect_nid_postfix(sender, destination.data(), 15), Invalid)); + Require(_connect_nid_postfix(sender, destination.data(), destination.size()) == 0); + Address peer{}; + size = peer.size(); + Require(_getpeername_nid_postfix(sender, peer.data(), &size) == 0 && size == 16 && peer == destination); + Require(_sendto_nid_postfix(sender, datagram, sizeof(datagram), 0, nullptr, 0) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(_recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, nullptr, nullptr) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + + Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); + std::memset(buffer, 0x5a, sizeof(buffer)); + source.fill(0xa5); + size = 9; + const int enabled = 1; + Require(Fails(_recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, source.data(), &size), BadDescriptor)); + Require(Fails(_recvmsg_nid_postfix(receiver, &incoming, 0), BadDescriptor)); + Require(Fails(_sendto_nid_postfix(sender, datagram, sizeof(datagram), 0, nullptr, 0), BadDescriptor)); + Require(Fails(_sendmsg_nid_postfix(sender, &outgoing, 0), BadDescriptor)); + Require(Fails(_bind_nid_postfix(receiver, destination.data(), destination.size()), BadDescriptor)); + Require(Fails(_connect_nid_postfix(sender, destination.data(), destination.size()), BadDescriptor)); + Require(Fails(_setsockopt_nid_postfix(sender, TcpLevel, TcpNoDelay, &enabled, sizeof(enabled)), BadDescriptor)); + Require(std::all_of(std::begin(buffer), std::end(buffer), [](char byte) { return byte == 0x5a; })); + Require(Filled(source, 0xa5) && size == 9); +} + +static std::set Names(const char* records, int bytes) { + std::set names; + for (int offset = 0; offset < bytes;) { + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, records + offset + 4, sizeof(recordLength)); + Require(recordLength >= 12 && offset + recordLength <= bytes); + names.emplace(records + offset + 8, static_cast(records[offset + 7])); + offset += recordLength; + } + return names; +} + +static void DirectoryEntries() { + const auto root = std::filesystem::path("anyps5-underscore-entries-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { std::ofstream stream(root / "entry"); } + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array records{}; + std::int64_t base = -1; + const int read = _getdirentries_nid_postfix(directory, records.data(), static_cast(records.size()), &base); + Require(read > 0 && base == 0); + const std::set listed{".", "..", "entry"}; + Require(Names(records.data(), read) == listed); + Require(sceKernelLseek(directory, 0, 0) == 0); + std::array expected{}; + std::int64_t expectedBase = -1; + Require(getdirentries_nid_postfix(directory, expected.data(), static_cast(expected.size()), &expectedBase) == read); + Require(expectedBase == base && std::memcmp(expected.data(), records.data(), static_cast(read)) == 0); + std::int64_t end = -1; + Require(_getdirentries_nid_postfix(directory, records.data(), static_cast(records.size()), &end) == 0 && end > 0); + + records.fill(0x5a); + std::int64_t untouched = 1234; + Require(Fails(_getdirentries_nid_postfix(directory, records.data(), 0, &untouched), Invalid)); + Require(Fails(_getdirentries_nid_postfix(directory, nullptr, 64, &untouched), BadAddress)); + Require(sceKernelClose(directory) == 0); + Require(Fails(_getdirentries_nid_postfix(directory, records.data(), static_cast(records.size()), &untouched), BadDescriptor)); + Require(untouched == 1234 && std::all_of(records.begin(), records.end(), [](char byte) { return byte == 0x5a; })); + std::filesystem::remove_all(root); +} + +volatile std::sig_atomic_t received = 0; + +void APS5_VABI Handler(int value) { + received = value; +} + +static bool Same(const GuestSigaction& left, const GuestSigaction& right) { + return left.handler == right.handler && left.flags == right.flags && std::memcmp(&left.mask, &right.mask, sizeof(left.mask)) == 0; +} + +static void Signals() { + const auto handler = reinterpret_cast(Handler); + const GuestSigaction term{handler, 0, {{0x4000, 0, 0, 0x80000000u}}}; + GuestSigaction initial{}; + Require(_sigaction_nid_postfix(15, nullptr, &initial) == 0); + GuestSigaction previous; + std::memset(&previous, 0xa5, sizeof(previous)); + Require(_sigaction_nid_postfix(15, &term, &previous) == 0 && Same(previous, initial)); + received = 0; + Require(raise_nid_postfix(15) == 0 && received == 15); + GuestSigaction current; + std::memset(¤t, 0xa5, sizeof(current)); + Require(_sigaction_nid_postfix(15, nullptr, ¤t) == 0 && Same(current, term)); + + GuestSigaction untouched; + std::memset(&untouched, 0xa5, sizeof(untouched)); + for (const int invalid : {0, 129}) { + std::memset(¤t, 0xa5, sizeof(current)); + Require(Fails(_sigaction_nid_postfix(invalid, &term, ¤t), Invalid) && std::memcmp(¤t, &untouched, sizeof(current)) == 0); + } + for (const int fixed : {9, 17}) { + std::memset(¤t, 0xa5, sizeof(current)); + Require(Fails(_sigaction_nid_postfix(fixed, &term, ¤t), Invalid) && std::memcmp(¤t, &untouched, sizeof(current)) == 0); + } + Require(_sigaction_nid_postfix(15, &initial, ¤t) == 0 && Same(current, term)); + + const GuestSigaction user{handler, 0, {}}; + GuestSigaction initialUser{}; + Require(_sigaction_nid_postfix(30, &user, &initialUser) == 0); + const GuestSignalSet blocked{{1u << 29, 0, 0, 0}}; + const GuestSignalSet empty{{0, 0, 0, 0}}; + GuestSignalSet saved{}; + Require(sigprocmask_nid_postfix(3, &blocked, &saved) == 0); + received = 0; + Require(raise_nid_postfix(30) == 0 && received == 0); + Require(Fails(_sigsuspend_nid_postfix(&empty), Interrupted) && received == 30); + GuestSignalSet restored{}; + Require(sigprocmask_nid_postfix(1, nullptr, &restored) == 0 && std::memcmp(&restored, &blocked, sizeof(restored)) == 0); + Require(sigprocmask_nid_postfix(3, &saved, nullptr) == 0); + Require(_sigaction_nid_postfix(30, &initialUser, nullptr) == 0); +} + +int main() { + StreamSockets(); + DatagramSockets(); + DirectoryEntries(); + Signals(); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..d35396ce80 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -215,6 +215,7 @@ - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the calls without the underscore, which have no libthr layer here, as FreeBSD 11.4 makes the underscore names the plain system calls. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it From c8ad92f8783612f8bf4431cb92ed66139cda3dac Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 20 Oct 2026 22:34:24 +0200 Subject: [PATCH 0411/1360] feat(libSceLibcInternal): export _err, _warn and _getprogname The PS5 3.20 export inventory lists _err, _warn and _getprogname in libSceLibcInternal next to err, warn and getprogname, with the NIDs UhKI6z9WWuo, XQFE8Y58WZM and iI6kGxgXzcU. In FreeBSD 11.4 libc the underscore names are the definitions and the public names are weak references to them (lib/libc/gen/err.c, lib/libc/gen/getprogname.c), and libthr interposes none of them, so each pair is one function. _err and _warn now hand their arguments to verr and vwarn, which use errno at the call as err and warn do; _getprogname returns getprogname(). The debt file records that the console's module was not read. guest_err checks the _warn and _err text for errno 13 with a formatted message, that _err passes its status to the err_set_exit hook, and that _warn without a format returns without calling the hook. guest_program_name checks that _getprogname returns the current __progname pointer. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibcInternal/Err.cpp | 13 +++++++++++++ core/libs/prx/libSceLibcInternal/Export.cpp | 4 ++++ core/libs/tests/GuestErr.cpp | 11 +++++++++++ core/libs/tests/GuestProgramName.cpp | 3 +++ docs/dev/debt/libSceLibcInternal.md | 1 + 5 files changed, 32 insertions(+) diff --git a/core/libs/prx/libSceLibcInternal/Err.cpp b/core/libs/prx/libSceLibcInternal/Err.cpp index f16dd24ca1..08f9ae7622 100644 --- a/core/libs/prx/libSceLibcInternal/Err.cpp +++ b/core/libs/prx/libSceLibcInternal/Err.cpp @@ -108,6 +108,13 @@ void APS5_VABI warn_nid_postfix(const char* format, ...) { APS5_ERR_VA_END(args); } +void APS5_VABI _warn_nid_postfix(const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarn_nid_postfix(format, reinterpret_cast(args)); + APS5_ERR_VA_END(args); +} + void APS5_VABI warnx_nid_postfix(const char* format, ...) { APS5_ERR_VA_LIST args; APS5_ERR_VA_START(args, format); @@ -128,6 +135,12 @@ void APS5_VABI warnx_nid_postfix(const char* format, ...) { verrc_nid_postfix(status, code, format, reinterpret_cast(args)); } +[[noreturn]] void APS5_VABI _err_nid_postfix(int status, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verr_nid_postfix(status, format, reinterpret_cast(args)); +} + [[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...) { APS5_ERR_VA_LIST args; APS5_ERR_VA_START(args, format); diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 7ca113639e..b8c3288715 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -109,6 +109,10 @@ const char* APS5_VABI getprogname_nid_postfix() { return __progname_nid_postfix; } +const char* APS5_VABI _getprogname_nid_postfix() { + return getprogname_nid_postfix(); +} + void APS5_VABI __cxa_finalize_nid_postfix(void* dsoHandle) { CxaFinalize_nid_no_patch(dsoHandle); } diff --git a/core/libs/tests/GuestErr.cpp b/core/libs/tests/GuestErr.cpp index cf7f656594..14c8772bc2 100644 --- a/core/libs/tests/GuestErr.cpp +++ b/core/libs/tests/GuestErr.cpp @@ -17,9 +17,11 @@ extern "C" { void APS5_VABI err_set_file_nid_postfix(FileStream* stream); void APS5_VABI err_set_exit_nid_postfix(ErrExit function); void APS5_VABI warn_nid_postfix(const char* format, ...); +void APS5_VABI _warn_nid_postfix(const char* format, ...); void APS5_VABI warnc_nid_postfix(int code, const char* format, ...); void APS5_VABI warnx_nid_postfix(const char* format, ...); [[noreturn]] void APS5_VABI err_nid_postfix(int status, const char* format, ...); +[[noreturn]] void APS5_VABI _err_nid_postfix(int status, const char* format, ...); [[noreturn]] void APS5_VABI errc_nid_postfix(int status, int code, const char* format, ...); [[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...); void APS5_VABI vwarn_nid_postfix(const char* format, VaList* args); @@ -148,6 +150,9 @@ int main() { *__error_nid_postfix() = 2; warn_nid_postfix(nullptr); Require(capture.Take() == "prog: " + noEntry + "\n", "warn without format"); + *__error_nid_postfix() = 13; + _warn_nid_postfix("open %s", "b.txt"); + Require(capture.Take() == "prog: open b.txt: " + denied + "\n", "_warn"); warnc_nid_postfix(13, "%d-%s", 7, "x"); Require(capture.Take() == "prog: 7-x: " + denied + "\n", "warnc"); warnx_nid_postfix("plain %c", 'q'); @@ -163,6 +168,12 @@ int main() { *__error_nid_postfix() = 2; Require(ExitStatus([] { err_nid_postfix(5, "e"); }) == 5, "err status"); Require(capture.Take() == "prog: e: " + noEntry + "\n", "err text"); + *__error_nid_postfix() = 13; + Require(ExitStatus([] { _err_nid_postfix(7, "%s %d", "f", 8); }) == 7 && g_hookStatus == 7, "_err status"); + Require(capture.Take() == "prog: f 8: " + denied + "\n", "_err text"); + *__error_nid_postfix() = 2; + Require(ExitStatus([] { _warn_nid_postfix(nullptr); }) == -1 && g_hookStatus == 7, "_warn does not exit"); + Require(capture.Take() == "prog: " + noEntry + "\n", "_warn without format"); *__error_nid_postfix() = 2; CallVariant(Variant::Warn, 0, 0, "open %s", "a.txt"); diff --git a/core/libs/tests/GuestProgramName.cpp b/core/libs/tests/GuestProgramName.cpp index 48be372cc8..7f14669adb 100644 --- a/core/libs/tests/GuestProgramName.cpp +++ b/core/libs/tests/GuestProgramName.cpp @@ -4,6 +4,7 @@ extern "C" { const char* APS5_VABI getprogname_nid_postfix(); +const char* APS5_VABI _getprogname_nid_postfix(); extern const char* __progname_nid_postfix; int* APS5_VABI __error_nid_postfix(); } @@ -14,12 +15,14 @@ int main() { const char* const original = __progname_nid_postfix; *__error_nid_postfix() = 45; Require(getprogname_nid_postfix() == original); + Require(_getprogname_nid_postfix() == original); Require(std::strcmp(getprogname_nid_postfix(), "eboot.bin") == 0); Require(*__error_nid_postfix() == 45); char renamed[] = "quake2"; __progname_nid_postfix = renamed; Require(getprogname_nid_postfix() == renamed); + Require(_getprogname_nid_postfix() == renamed); renamed[0] = 'Q'; Require(std::strcmp(getprogname_nid_postfix(), "Quake2") == 0); diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index ff8c271f1f..d61e6d1e18 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -16,4 +16,5 @@ - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read +- [_err, _warn](../../../core/libs/prx/libSceLibcInternal/Err.cpp) and [_getprogname](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) are assumed to be the same functions as `err`, `warn` and `getprogname` and run their code; the console's module was not read. FreeBSD 11.4 defines the underscore names and makes the public names weak references to them ([`_err`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/err.c#L70-L79), [`_warn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/err.c#L135-L144), [`_getprogname`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/getprogname.c#L10-L17)) - [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer From 6586741a7dc9dcfa4ac0564235d9f9e71bc8b00f Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:04:23 +0300 Subject: [PATCH 0412/1360] feat(libSceGnmDriverForNeoMode): answer the debug and GNM interface queries as retail firmware --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index f8a8c67c6a..7bdd7f71a9 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -3,6 +3,13 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int Failure = static_cast(0x8EEE00FFu); +constexpr int NoInterface = static_cast(0x80000000u); + +} // namespace + extern "C" { int APS5_VABI sceGnmBeginWorkload(void) { @@ -26,13 +33,11 @@ int APS5_VABI sceGnmCreateWorkloadStream(void) { } int APS5_VABI sceGnmDebugModuleReset(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmDebugReset(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmDestroyWorkloadStream(void) { @@ -151,28 +156,23 @@ int APS5_VABI sceGnmDrawOpaqueAuto(void) { } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { @@ -181,7 +181,6 @@ int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { } int APS5_VABI sceGnmDriverTraceInProgress(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -216,8 +215,7 @@ int APS5_VABI sceGnmGetCoredumpProtectionFaultTimestamp(void) { } int APS5_VABI sceGnmGetDbgGcHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return -1; } int APS5_VABI sceGnmGetGpuBlockStatus(void) { From 24054aea6cf4f74d6135809908c46b30ff789e5a Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:04:23 +0300 Subject: [PATCH 0413/1360] feat(libSceGnmDriverForNeoMode): refuse resource and owner registration as retail firmware --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 57 +++++++------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 7bdd7f71a9..41b45069f5 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -190,13 +190,11 @@ int APS5_VABI sceGnmEndWorkload(void) { } int APS5_VABI sceGnmFindResources(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmFindResourcesPublic(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetCoredumpAddress(void) { @@ -249,8 +247,7 @@ int APS5_VABI sceGnmGetOffChipTessellationBufferSize(void) { } int APS5_VABI sceGnmGetOwnerName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void) { @@ -264,33 +261,27 @@ int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { } int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceRegistrationBuffers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceShaderGuid(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceType(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceUserData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetShaderProgramBaseAddress(void) { @@ -354,8 +345,7 @@ int APS5_VABI sceGnmPaHeartbeat(void) { } int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { @@ -364,8 +354,7 @@ int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { } int APS5_VABI sceGnmRegisterGdsResource(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { @@ -374,18 +363,15 @@ int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { } int APS5_VABI sceGnmRegisterOwner(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRegisterOwnerForSystem(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRegisterResource(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void) { @@ -459,13 +445,11 @@ int APS5_VABI sceGnmSetPsShader350(void) { } int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetResourceUserData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetSpiEnableSqCounters(void) { @@ -504,18 +488,15 @@ int APS5_VABI sceGnmSubmitCommandBuffersForWorkload(void) { } int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmUnregisterOwnerAndResources(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmUnregisterResource(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmUpdateGsShader(void) { From 188ec19ad94ac27f6e2f661e2166f7c1d4409ae1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:04:23 +0300 Subject: [PATCH 0414/1360] feat(libSceGnmDriverForNeoMode): refuse the SDMA calls as retail firmware --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 41b45069f5..248010da89 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -385,43 +385,35 @@ int APS5_VABI sceGnmResetVgtControl(void) { } int APS5_VABI sceGnmSdmaClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaConstFill(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaCopyLinear(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaCopyTiled(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaCopyWindow(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaFlush(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaGetMinCmdSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetCsShaderWithModifier(void) { From 4c7364e1408facf41ba4ec7d30c32102d829fb2d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:04:23 +0300 Subject: [PATCH 0415/1360] feat(libSceGnmDriverForNeoMode): refuse the SQ counter calls as retail firmware --- core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 248010da89..71720ff9ec 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -251,8 +251,7 @@ int APS5_VABI sceGnmGetOwnerName(void) { } int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { @@ -445,13 +444,11 @@ int APS5_VABI sceGnmSetResourceUserData(void) { } int APS5_VABI sceGnmSetSpiEnableSqCounters(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetVgtControl(void) { From 43a9845b22b06ebc51150040114112fc28aa5612 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:04:23 +0300 Subject: [PATCH 0416/1360] feat(libSceGnmDriverForNeoMode): report validation as not enabled --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 71720ff9ec..9b7f2684c4 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -7,6 +7,7 @@ namespace { constexpr int Failure = static_cast(0x8EEE00FFu); constexpr int NoInterface = static_cast(0x80000000u); +constexpr int ValidationNotEnabled = static_cast(0x80D13FFFu); } // namespace @@ -514,33 +515,27 @@ int APS5_VABI sceGnmUpdateVsShader(void) { } int APS5_VABI sceGnmValidateDisableDiagnostics(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateDisableDiagnostics2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateDispatchCommandBuffers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateDrawCommandBuffers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateGetDiagnosticInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateGetDiagnostics(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateGetVersion(void) { @@ -549,18 +544,15 @@ int APS5_VABI sceGnmValidateGetVersion(void) { } int APS5_VABI sceGnmValidateOnSubmitEnabled(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceGnmValidateResetState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } } From e44e334332e8182e7f262286d83fe7ab261f035d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:05:23 +0300 Subject: [PATCH 0417/1360] test(libSceGnmDriverForNeoMode): check the retail answers of the debug, resource, SDMA, counter and validation calls --- core/libs/CMakeLists.txt | 10 ++ core/libs/tests/GuestGnmNeoRetail.cpp | 131 ++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 core/libs/tests/GuestGnmNeoRetail.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..2c16723f70 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -360,6 +360,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_gnm_neo_retail_tests tests/GuestGnmNeoRetail.cpp) + target_include_directories(guest_gnm_neo_retail_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_gnm_neo_retail_tests PRIVATE libSceGnmDriverForNeoMode libc) + configure_windows_unwind(guest_gnm_neo_retail_tests) + add_test(NAME guest_gnm_neo_retail COMMAND guest_gnm_neo_retail_tests) + if(WIN32) + set_tests_properties(guest_gnm_neo_retail PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_move_tests tests/GuestMove.cpp) target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_move_tests PRIVATE libSceMove libc) diff --git a/core/libs/tests/GuestGnmNeoRetail.cpp b/core/libs/tests/GuestGnmNeoRetail.cpp new file mode 100644 index 0000000000..29148976fa --- /dev/null +++ b/core/libs/tests/GuestGnmNeoRetail.cpp @@ -0,0 +1,131 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceGnmDebugModuleReset(void); +int APS5_VABI sceGnmDebugReset(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void); +int APS5_VABI sceGnmDriverTraceInProgress(void); +int APS5_VABI sceGnmFindResources(void); +int APS5_VABI sceGnmFindResourcesPublic(void); +int APS5_VABI sceGnmGetDbgGcHandle(void); +int APS5_VABI sceGnmGetOwnerName(void); +int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void); +int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void); +int APS5_VABI sceGnmGetResourceName(void); +int APS5_VABI sceGnmGetResourceRegistrationBuffers(void); +int APS5_VABI sceGnmGetResourceShaderGuid(void); +int APS5_VABI sceGnmGetResourceType(void); +int APS5_VABI sceGnmGetResourceUserData(void); +int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void); +int APS5_VABI sceGnmRegisterGdsResource(void); +int APS5_VABI sceGnmRegisterOwner(void); +int APS5_VABI sceGnmRegisterOwnerForSystem(void); +int APS5_VABI sceGnmRegisterResource(void); +int APS5_VABI sceGnmSdmaClose(void); +int APS5_VABI sceGnmSdmaConstFill(void); +int APS5_VABI sceGnmSdmaCopyLinear(void); +int APS5_VABI sceGnmSdmaCopyTiled(void); +int APS5_VABI sceGnmSdmaCopyWindow(void); +int APS5_VABI sceGnmSdmaFlush(void); +int APS5_VABI sceGnmSdmaGetMinCmdSize(void); +int APS5_VABI sceGnmSdmaOpen(void); +int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void); +int APS5_VABI sceGnmSetResourceUserData(void); +int APS5_VABI sceGnmSetSpiEnableSqCounters(void); +int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void); +int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void); +int APS5_VABI sceGnmUnregisterOwnerAndResources(void); +int APS5_VABI sceGnmUnregisterResource(void); +int APS5_VABI sceGnmValidateDisableDiagnostics(void); +int APS5_VABI sceGnmValidateDisableDiagnostics2(void); +int APS5_VABI sceGnmValidateDispatchCommandBuffers(void); +int APS5_VABI sceGnmValidateDrawCommandBuffers(void); +int APS5_VABI sceGnmValidateGetDiagnosticInfo(void); +int APS5_VABI sceGnmValidateGetDiagnostics(void); +int APS5_VABI sceGnmValidateOnSubmitEnabled(void); +int APS5_VABI sceGnmValidateResetState(void); +int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void); +} + +namespace { + +constexpr int Failure = static_cast(0x8EEE00FFu); +constexpr int NoInterface = static_cast(0x80000000u); +constexpr int ValidationNotEnabled = static_cast(0x80D13FFFu); + +using Query = int (APS5_VABI *)(void); + +void RequireAll(int expected, std::initializer_list queries) { + for (const auto query : queries) { + if (query() != expected) std::abort(); + } +} + +} // namespace + +int main() { + RequireAll(Failure, { + sceGnmDebugModuleReset, + sceGnmDebugReset, + sceGnmFindResources, + sceGnmFindResourcesPublic, + sceGnmGetOwnerName, + sceGnmGetResourceBaseAddressAndSizeInBytes, + sceGnmGetResourceName, + sceGnmGetResourceRegistrationBuffers, + sceGnmGetResourceShaderGuid, + sceGnmGetResourceType, + sceGnmGetResourceUserData, + sceGnmQueryResourceRegistrationUserMemoryRequirements, + sceGnmRegisterGdsResource, + sceGnmRegisterOwner, + sceGnmRegisterOwnerForSystem, + sceGnmRegisterResource, + sceGnmSetResourceRegistrationUserMemory, + sceGnmSetResourceUserData, + sceGnmUnregisterAllResourcesForOwner, + sceGnmUnregisterOwnerAndResources, + sceGnmUnregisterResource, + sceGnmSdmaClose, + sceGnmSdmaConstFill, + sceGnmSdmaCopyLinear, + sceGnmSdmaCopyTiled, + sceGnmSdmaCopyWindow, + sceGnmSdmaFlush, + sceGnmSdmaGetMinCmdSize, + sceGnmSdmaOpen, + sceGnmGetPhysicalCounterFromVirtualized, + sceGnmSetSpiEnableSqCounters, + sceGnmSetSpiEnableSqCountersForUnitInstance, + }); + RequireAll(NoInterface, { + sceGnmDriverInternalRetrieveGnmInterface, + sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger, + sceGnmDriverInternalRetrieveGnmInterfaceForReplay, + sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration, + sceGnmDriverInternalRetrieveGnmInterfaceForValidation, + }); + RequireAll(0, { + sceGnmDriverTraceInProgress, + sceGnmValidateOnSubmitEnabled, + }); + RequireAll(-1, { + sceGnmGetDbgGcHandle, + }); + RequireAll(ValidationNotEnabled, { + sceGnmValidateDisableDiagnostics, + sceGnmValidateDisableDiagnostics2, + sceGnmValidateDispatchCommandBuffers, + sceGnmValidateDrawCommandBuffers, + sceGnmValidateGetDiagnosticInfo, + sceGnmValidateGetDiagnostics, + sceGnmValidateResetState, + sceGnmValidationRegisterMemoryCheckCallback, + }); +} From a4e9e7ce20e9d5f292e0436407d57f04cb5842d5 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:05:43 +0300 Subject: [PATCH 0418/1360] docs(debt): record the retail answers of libSceGnmDriverForNeoMode --- docs/dev/debt/libSceGnmDriverForNeoMode.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 726f106af0..46f92c14e9 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,6 +2,8 @@ ### Unknown function info -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 118 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 70 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 45 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c). +### Functional + +- [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read From 9e3cac0d89d79e861a8f3a338b43a028966a6088 Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 21 Oct 2026 11:07:06 -0400 Subject: [PATCH 0419/1360] feat(hw-oracle): run on Windows through the AMD driver's HIP runtime and comgr tools/hw-oracle needed Linux with ROCm, so Windows measurements went through private runners. oracle_hip.py assembles and links template.s kernels with the driver's amd_comgr and dispatches them with its HIP runtime, with the same rows, padding, dispatch splitting and report as oracle.c. hw_oracle.py selects it with HW_ORACLE_RUNTIME, by default on Windows, and loads it in-process so the DLLs are loaded once per run. The driver's comgr 2.9 numbers its assemble and link actions 0x8 and 0x7, not 0xA and 0x9, so each step tries both and keeps the one that returns exactly one output of the expected kind. Co-Authored-By: Claude Opus 5.5 --- docs/dev/HW_ORACLE.md | 13 ++ tools/hw-oracle/hw_oracle.py | 38 ++++- tools/hw-oracle/oracle_hip.py | 235 ++++++++++++++++++++++++++++++ tools/hw-oracle/test_hw_oracle.py | 136 +++++++++++++++++ 4 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 tools/hw-oracle/oracle_hip.py diff --git a/docs/dev/HW_ORACLE.md b/docs/dev/HW_ORACLE.md index e48f86ed1a..c675934529 100644 --- a/docs/dev/HW_ORACLE.md +++ b/docs/dev/HW_ORACLE.md @@ -10,6 +10,8 @@ the tool, but differs from gfx1013 in more places. Say which GPU you measured on ## Requirements +### Linux + - 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 @@ -20,6 +22,17 @@ the tool, but differs from gfx1013 in more places. Say which GPU you measured on 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). +### Windows + +- AMD's Windows driver, which installs the HIP runtime (`amdhip64_6.dll`) and the code object manager (`amd_comgr_2.dll`) + in `System32`. ROCm, a C compiler, `clang` and `ld.lld` are not needed. +- Python 3 from python.org or the Microsoft Store. + +On Windows `oracle_hip.py` replaces `oracle.c`: comgr assembles and links the kernel, and HIP runs it. The CLI, the Python +interface and the results are the same as on Linux. `HW_ORACLE_RUNTIME` picks the runtime (`hip` on Windows, `hsa` +elsewhere). `HW_ORACLE_HIP` and `HW_ORACLE_COMGR` name other DLLs. `ROCR_VISIBLE_DEVICES` selects the GPU, through +`HIP_VISIBLE_DEVICES`, unless that variable is already set. + ## Kernel `template.s` wraps the body under test. Per lane: diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index ffac30f5c7..f1dc2da856 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,5 +1,6 @@ import argparse import hashlib +import importlib.util import json import os import operator @@ -17,6 +18,22 @@ CACHE = Path(os.environ.get("HW_ORACLE_CACHE") or Path(os.environ.get("XDG_CACHE ROWS_PER_DISPATCH = 1024 DEFAULT_LDS = 4096 MAX_LDS = 65536 +RUNTIMES = ("hsa", "hip") + + +def runtime(): + selected = os.environ.get("HW_ORACLE_RUNTIME") or ("hip" if os.name == "nt" else "hsa") + if selected not in RUNTIMES: + raise ValueError(f"HW_ORACLE_RUNTIME must be one of {', '.join(RUNTIMES)}") + return selected + + +@cache +def hip_runtime(): + spec = importlib.util.spec_from_file_location("oracle_hip", HERE / "oracle_hip.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module def rocm_root(): @@ -78,7 +95,11 @@ def detected_target(binary, visible_devices): def target(): - return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle(), os.environ.get("ROCR_VISIBLE_DEVICES")) + if os.environ.get("HW_ORACLE_TARGET"): + return os.environ["HW_ORACLE_TARGET"] + if runtime() == "hip": + return hip_runtime().target() + return detected_target(oracle(), os.environ.get("ROCR_VISIBLE_DEVICES")) def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -97,6 +118,9 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 ("@FP16_OVERFLOW@", str(int(fp16_overflow))), ("@LDS@", str(lds)), ("@BODY@", body)): text = text.replace(key, value) (work / "k.s").write_text(text) + if runtime() == "hip": + (work / "k.co").write_bytes(hip_runtime().assemble(text.encode("utf-8"), target(), wave64)) + return work / "k.co" subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", *(["-mwavefrontsize64"] if wave64 else []), "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True, stdout=sys.stderr) subprocess.run([tool("ld.lld"), "-m", "elf64_amdgpu", "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True, stdout=sys.stderr) return work / "k.co" @@ -131,10 +155,14 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de rows_per_dispatch = ROWS_PER_DISPATCH // 2 if wave64 else ROWS_PER_DISPATCH for start in range(0, len(padded), rows_per_dispatch): chunk = padded[start:start + rows_per_dispatch] - (work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra) - (work / "out.bin").unlink(missing_ok=True) - subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env, stdout=sys.stderr) - data = (work / "out.bin").read_bytes() + payload = b"".join(struct.pack("<4I", *r) for r in chunk) + extra + if runtime() == "hip": + data = hip_runtime().dispatch(Path(code).read_bytes(), len(chunk), payload, 16, coarse) + else: + (work / "in.bin").write_bytes(payload) + (work / "out.bin").unlink(missing_ok=True) + subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env, stdout=sys.stderr) + data = (work / "out.bin").read_bytes() expected = len(chunk) * 64 if len(data) != expected: raise ValueError(f"Oracle output size: expected {expected} bytes, got {len(data)}") diff --git a/tools/hw-oracle/oracle_hip.py b/tools/hw-oracle/oracle_hip.py new file mode 100644 index 0000000000..596b82a8f7 --- /dev/null +++ b/tools/hw-oracle/oracle_hip.py @@ -0,0 +1,235 @@ +import argparse +import ctypes +import os +import sys +from functools import cache +from pathlib import Path + +HIP_LIBRARIES = ("amdhip64_7.dll", "amdhip64_6.dll", "amdhip64.dll") +COMGR_LIBRARIES = ("amd_comgr_3.dll", "amd_comgr_2.dll", "amd_comgr.dll") +DATA_SOURCE = 0x1 +DATA_LOG = 0x5 +DATA_RELOCATABLE = 0x7 +DATA_EXECUTABLE = 0x8 +ASSEMBLE_ACTIONS = (0x8, 0xA) +LINK_ACTIONS = (0x7, 0x9) +HIP_MEMCPY_HOST_TO_DEVICE = 1 +HIP_MEMCPY_DEVICE_TO_HOST = 2 +DEVICE_PROPERTIES_SIZE = 8192 +DEVICE_NAME_SIZE = 256 + + +class OracleError(Exception): + pass + + +class Handle(ctypes.Structure): + _fields_ = [("handle", ctypes.c_uint64)] + + +def check(status, what): + if status != 0: + raise OracleError(f"{what} failed with status {status}") + + +def load(variable, names): + candidates = (os.environ[variable],) if os.environ.get(variable) else names + errors = [] + for name in candidates: + try: + return ctypes.WinDLL(name) + except OSError as error: + errors.append(f"{name}: {error}") + raise OracleError(f"no library found ({variable} overrides it): " + "; ".join(errors)) + + +@cache +def hip(): + if os.environ.get("ROCR_VISIBLE_DEVICES") is not None and "HIP_VISIBLE_DEVICES" not in os.environ: + os.environ["HIP_VISIBLE_DEVICES"] = os.environ["ROCR_VISIBLE_DEVICES"] + library = load("HW_ORACLE_HIP", HIP_LIBRARIES) + library.hipModuleLoadData.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_char_p] + library.hipModuleGetFunction.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_void_p, ctypes.c_char_p] + library.hipMemcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int] + library.hipMemset.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t] + library.hipModuleLaunchKernel.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, + ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + library.hipModuleUnload.argtypes = [ctypes.c_void_p] + library.hipFree.argtypes = [ctypes.c_void_p] + library.hipHostFree.argtypes = [ctypes.c_void_p] + check(library.hipInit(0), "hipInit") + count = ctypes.c_int() + check(library.hipGetDeviceCount(ctypes.byref(count)), "hipGetDeviceCount") + if count.value == 0: + raise OracleError("no HIP device") + return library + + +@cache +def comgr(): + library = load("HW_ORACLE_COMGR", COMGR_LIBRARIES) + library.amd_comgr_create_data.argtypes = [ctypes.c_int, ctypes.POINTER(Handle)] + library.amd_comgr_create_data_set.argtypes = [ctypes.POINTER(Handle)] + library.amd_comgr_create_action_info.argtypes = [ctypes.POINTER(Handle)] + library.amd_comgr_set_data.argtypes = [Handle, ctypes.c_size_t, ctypes.c_char_p] + library.amd_comgr_set_data_name.argtypes = [Handle, ctypes.c_char_p] + library.amd_comgr_data_set_add.argtypes = [Handle, Handle] + library.amd_comgr_action_info_set_isa_name.argtypes = [Handle, ctypes.c_char_p] + library.amd_comgr_action_info_set_option_list.argtypes = [Handle, ctypes.POINTER(ctypes.c_char_p), ctypes.c_size_t] + library.amd_comgr_action_info_set_logging.argtypes = [Handle, ctypes.c_bool] + library.amd_comgr_do_action.argtypes = [ctypes.c_int, Handle, Handle, Handle] + library.amd_comgr_action_data_count.argtypes = [Handle, ctypes.c_int, ctypes.POINTER(ctypes.c_size_t)] + library.amd_comgr_action_data_get_data.argtypes = [Handle, ctypes.c_int, ctypes.c_size_t, ctypes.POINTER(Handle)] + library.amd_comgr_get_data.argtypes = [Handle, ctypes.POINTER(ctypes.c_size_t), ctypes.c_char_p] + library.amd_comgr_release_data.argtypes = [Handle] + library.amd_comgr_destroy_data_set.argtypes = [Handle] + library.amd_comgr_destroy_action_info.argtypes = [Handle] + return library + + +@cache +def target(): + properties = ctypes.create_string_buffer(DEVICE_PROPERTIES_SIZE) + library = hip() + getter = getattr(library, "hipGetDevicePropertiesR0600", None) or library.hipGetDeviceProperties + check(getter(properties, 0), "hipGetDeviceProperties") + start = properties.raw.find(b"gfx", DEVICE_NAME_SIZE) + if start < 0: + raise OracleError("the HIP device properties name no gfx target") + return properties.raw[start:properties.raw.index(b"\0", start)].decode("ascii").split(":")[0] + + +def data_bytes(data_set, kind): + library = comgr() + count = ctypes.c_size_t() + check(library.amd_comgr_action_data_count(data_set, kind, ctypes.byref(count)), "amd_comgr_action_data_count") + chunks = [] + for index in range(count.value): + data = Handle() + check(library.amd_comgr_action_data_get_data(data_set, kind, index, ctypes.byref(data)), "amd_comgr_action_data_get_data") + size = ctypes.c_size_t() + check(library.amd_comgr_get_data(data, ctypes.byref(size), None), "amd_comgr_get_data") + buffer = ctypes.create_string_buffer(size.value) + check(library.amd_comgr_get_data(data, ctypes.byref(size), buffer), "amd_comgr_get_data") + chunks.append(buffer.raw[:size.value]) + library.amd_comgr_release_data(data) + return chunks + + +def data_set(kind, name, payload): + library = comgr() + data = Handle() + check(library.amd_comgr_create_data(kind, ctypes.byref(data)), "amd_comgr_create_data") + check(library.amd_comgr_set_data(data, len(payload), payload), "amd_comgr_set_data") + check(library.amd_comgr_set_data_name(data, name), "amd_comgr_set_data_name") + result = Handle() + check(library.amd_comgr_create_data_set(ctypes.byref(result)), "amd_comgr_create_data_set") + check(library.amd_comgr_data_set_add(result, data), "amd_comgr_data_set_add") + library.amd_comgr_release_data(data) + return result + + +def comgr_step(actions, isa, options, inputs, kind): + library = comgr() + logs = [] + for action in actions: + info = Handle() + check(library.amd_comgr_create_action_info(ctypes.byref(info)), "amd_comgr_create_action_info") + outputs = Handle() + try: + check(library.amd_comgr_action_info_set_isa_name(info, isa.encode("ascii")), "amd_comgr_action_info_set_isa_name") + check(library.amd_comgr_action_info_set_logging(info, True), "amd_comgr_action_info_set_logging") + if options: + check(library.amd_comgr_action_info_set_option_list(info, (ctypes.c_char_p * len(options))(*options), len(options)), + "amd_comgr_action_info_set_option_list") + check(library.amd_comgr_create_data_set(ctypes.byref(outputs)), "amd_comgr_create_data_set") + status = library.amd_comgr_do_action(action, info, inputs, outputs) + result = data_bytes(outputs, kind) + log = b"".join(data_bytes(outputs, DATA_LOG)).decode("utf-8", errors="replace").strip() + library.amd_comgr_destroy_data_set(outputs) + finally: + library.amd_comgr_destroy_action_info(info) + if status == 0 and len(result) == 1: + return result[0] + logs.append(f"action {action:#x} status {status}" + (f": {log}" if log else "")) + raise OracleError("comgr produced no output\n" + "\n".join(logs)) + + +def assemble(source, isa_target, wave64): + isa = f"amdgcn-amd-amdhsa--{isa_target}" + inputs = data_set(DATA_SOURCE, b"k.s", source) + try: + relocatable = comgr_step(ASSEMBLE_ACTIONS, isa, [b"-mwavefrontsize64"] if wave64 else [], inputs, DATA_RELOCATABLE) + finally: + comgr().amd_comgr_destroy_data_set(inputs) + objects = data_set(DATA_RELOCATABLE, b"k.o", relocatable) + try: + return comgr_step(LINK_ACTIONS, isa, [], objects, DATA_EXECUTABLE) + finally: + comgr().amd_comgr_destroy_data_set(objects) + + +def dispatch(code, threads, payload, out_dwords, coarse): + library = hip() + module = ctypes.c_void_p() + check(library.hipModuleLoadData(ctypes.byref(module), code), "hipModuleLoadData") + try: + function = ctypes.c_void_p() + check(library.hipModuleGetFunction(ctypes.byref(function), module, b"probe"), "hipModuleGetFunction") + out_size = threads * out_dwords * 4 + buffers = [] + try: + for size in (len(payload), out_size): + pointer = ctypes.c_void_p() + if coarse: + check(library.hipMalloc(ctypes.byref(pointer), ctypes.c_size_t(size)), "hipMalloc") + else: + check(library.hipHostMalloc(ctypes.byref(pointer), ctypes.c_size_t(size), 0), "hipHostMalloc") + buffers.append(pointer) + device_in, device_out = buffers + check(library.hipMemcpy(device_in, payload, len(payload), HIP_MEMCPY_HOST_TO_DEVICE), "hipMemcpy") + check(library.hipMemset(device_out, 0xee, out_size), "hipMemset") + arguments = (ctypes.c_void_p * 2)(ctypes.addressof(device_in), ctypes.addressof(device_out)) + check(library.hipModuleLaunchKernel(function, 1, 1, 1, min(threads, 1024), 1, 1, 0, None, arguments, None), "hipModuleLaunchKernel") + check(library.hipDeviceSynchronize(), "hipDeviceSynchronize") + output = ctypes.create_string_buffer(out_size) + check(library.hipMemcpy(output, device_out, out_size, HIP_MEMCPY_DEVICE_TO_HOST), "hipMemcpy") + return output.raw + finally: + for pointer in buffers: + (library.hipFree if coarse else library.hipHostFree)(pointer) + finally: + library.hipModuleUnload(module) + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Assemble and run hw-oracle kernels through the HIP runtime and comgr of AMD's Windows driver.") + parser.add_argument("--target", action="store_true", help="print the gfx target of the HIP device") + parser.add_argument("--assemble", nargs=3, metavar=("SOURCE", "OUTPUT", "TARGET"), help="assemble and link SOURCE for TARGET into OUTPUT") + parser.add_argument("--wave64", action="store_true", help="assemble for wave64") + parser.add_argument("dispatch", nargs="*", metavar="CODE THREADS IN OUT OUT_DWORDS", help="run CODE as oracle does") + args = parser.parse_args(argv) + try: + if args.target and not args.assemble and not args.dispatch: + print(target()) + return 0 + if args.assemble and not args.target and not args.dispatch: + source, output, isa_target = args.assemble + Path(output).write_bytes(assemble(Path(source).read_bytes(), isa_target, args.wave64)) + return 0 + if len(args.dispatch) != 5 or args.target or args.assemble or args.wave64: + parser.error("give --target, --assemble SOURCE OUTPUT TARGET, or CODE THREADS IN OUT OUT_DWORDS") + code, threads, inputs, output, out_dwords = args.dispatch + threads, out_dwords = int(threads), int(out_dwords) + if threads not in range(1, 1025) or out_dwords not in range(1, 17): + parser.error("THREADS must be 1 to 1024 and OUT_DWORDS 1 to 16") + result = dispatch(Path(code).read_bytes(), threads, Path(inputs).read_bytes(), out_dwords, os.environ.get("COARSE") is not None) + Path(output).write_bytes(result) + return 0 + except (OSError, OracleError) as error: + print(error, file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 58f66cf38c..e234145299 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -10,6 +10,7 @@ import sys import tempfile import unittest from pathlib import Path +import unittest.mock from unittest.mock import patch import hw_oracle @@ -18,6 +19,10 @@ import hw_oracle MODES = dict(ieee=0, denorm32=0, denorm16=3, dx10_clamp=1, round32=0, round16=0, fp16_overflow=0) +def setUpModule(): + os.environ["HW_ORACLE_RUNTIME"] = "hsa" + + class OracleCacheTests(unittest.TestCase): def test_failed_build_is_not_cached_and_can_be_retried(self): for previous in (None, b"old executable"): @@ -642,5 +647,136 @@ class AssemblyTests(unittest.TestCase): self.assertTrue(code.is_file()) +class HipRuntimeTests(unittest.TestCase): + def test_runtime_defaults_to_hip_on_windows_only(self): + environment = {key: value for key, value in os.environ.items() if key != "HW_ORACLE_RUNTIME"} + with patch.dict(os.environ, environment, clear=True): + self.assertEqual(hw_oracle.runtime(), "hip" if os.name == "nt" else "hsa") + for selected in hw_oracle.RUNTIMES: + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": selected}): + self.assertEqual(hw_oracle.runtime(), selected) + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": "rocm"}), self.assertRaises(ValueError): + hw_oracle.runtime() + + def test_hip_run_assembles_and_dispatches_in_process(self): + calls = [] + + class Runtime: + @staticmethod + def target(): + calls.append(("target",)) + return "gfx1010" + + @staticmethod + def assemble(source, isa_target, wave64): + calls.append(("assemble", isa_target, wave64, b"v_mov_b32 v10, v4" in source)) + return b"code" + + @staticmethod + def dispatch(code, threads, payload, out_dwords, coarse): + calls.append(("dispatch", code, threads, len(payload), out_dwords, coarse)) + return bytes(threads * out_dwords * 4) + + environment = {key: value for key, value in os.environ.items() if key != "HW_ORACLE_TARGET"} + for wave64, coarse in ((False, False), (True, True)): + calls.clear() + with self.subTest(wave64=wave64), patch.dict(os.environ, {**environment, "HW_ORACLE_RUNTIME": "hip"}, clear=True), patch.object(hw_oracle, "hip_runtime", return_value=Runtime), patch.object(hw_oracle.subprocess, "run", side_effect=AssertionError("subprocess called")), patch.object(hw_oracle, "oracle", side_effect=AssertionError("oracle built")): + results = hw_oracle.run(" v_mov_b32 v10, v4", [(1, 2, 3, 4)], b"xy", wave64, coarse, **MODES) + wave = 64 if wave64 else 32 + self.assertEqual(results, [(0,) * 16]) + self.assertIn(("target",), calls) + self.assertEqual([call for call in calls if call != ("target",)], + [("assemble", "gfx1010", wave64, True), ("dispatch", b"code", wave, wave * 16 + 2, 16, coarse)]) + + def test_hip_output_must_match_the_padded_dispatch_size(self): + class Runtime: + @staticmethod + def assemble(source, isa_target, wave64): + return b"code" + + @staticmethod + def dispatch(code, threads, payload, out_dwords, coarse): + return bytes(threads * out_dwords * 4 - 4) + + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": "hip", "HW_ORACLE_TARGET": "gfx1010"}), patch.object(hw_oracle, "hip_runtime", return_value=Runtime), self.assertRaises(ValueError): + hw_oracle.run(" s_nop 0", [(0, 0, 0, 0)], **MODES) + + def test_hip_runtime_loads_the_script_next_to_the_tool(self): + self.assertEqual(Path(hw_oracle.hip_runtime().__file__), hw_oracle.HERE / "oracle_hip.py") + + +class HipOracleTests(unittest.TestCase): + def setUp(self): + import oracle_hip + self.oracle_hip = oracle_hip + + def test_cli_rejects_mixed_or_incomplete_requests(self): + for argv in ([], ["--target", "code", "32", "in", "out", "16"], ["--wave64", "code", "32", "in", "out", "16"], + ["code", "32", "in", "out"], ["code", "0", "in", "out", "16"], ["code", "1025", "in", "out", "16"], + ["code", "32", "in", "out", "17"]): + with self.subTest(argv=argv), contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exit: + self.oracle_hip.main(argv) + self.assertEqual(exit.exception.code, 2) + + def test_comgr_step_tries_each_action_until_one_produces_output(self): + library = unittest.mock.MagicMock() + for name in ("amd_comgr_create_action_info", "amd_comgr_create_data_set", "amd_comgr_action_info_set_isa_name", + "amd_comgr_action_info_set_logging", "amd_comgr_action_info_set_option_list"): + getattr(library, name).return_value = 0 + outputs = {0x8: [], 0xA: [b"object"]} + actions = [] + library.amd_comgr_do_action.side_effect = lambda action, *args: actions.append(action) or (0 if action == 0xA else 1) + + def data_bytes(data_set, kind): + return [b"log"] if kind == self.oracle_hip.DATA_LOG else outputs[actions[-1]] + + isa = "amdgcn-amd-amdhsa--gfx1010" + with patch.object(self.oracle_hip, "comgr", return_value=library), patch.object(self.oracle_hip, "data_bytes", side_effect=data_bytes): + self.assertEqual(self.oracle_hip.comgr_step((0x8, 0xA), isa, [b"-mwavefrontsize64"], self.oracle_hip.Handle(), + self.oracle_hip.DATA_RELOCATABLE), b"object") + self.assertEqual(actions, [0x8, 0xA]) + outputs[0xA] = [] + with self.assertRaises(self.oracle_hip.OracleError) as error: + self.oracle_hip.comgr_step((0x8, 0xA), isa, [], self.oracle_hip.Handle(), self.oracle_hip.DATA_RELOCATABLE) + self.assertIn("action 0x8 status 1: log", str(error.exception)) + self.assertIn("action 0xa status 0: log", str(error.exception)) + + def test_device_selection_follows_rocr_visible_devices(self): + library = unittest.mock.MagicMock() + library.hipInit.return_value = 0 + + def count(pointer): + pointer._obj.value = 1 + return 0 + + library.hipGetDeviceCount.side_effect = count + base = {key: value for key, value in os.environ.items() if key not in ("ROCR_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES")} + for environment, expected in (({"ROCR_VISIBLE_DEVICES": "1"}, "1"), ({"ROCR_VISIBLE_DEVICES": "1", "HIP_VISIBLE_DEVICES": "0"}, "0")): + self.oracle_hip.hip.cache_clear() + with self.subTest(environment=environment), patch.dict(os.environ, {**base, **environment}, clear=True), \ + patch.object(self.oracle_hip, "load", return_value=library): + self.oracle_hip.hip() + self.assertEqual(os.environ["HIP_VISIBLE_DEVICES"], expected) + self.oracle_hip.hip.cache_clear() + + def test_assembles_both_wave_sizes_with_the_driver_comgr(self): + if os.name != "nt": + self.skipTest("comgr from AMD's Windows driver") + try: + self.oracle_hip.comgr() + except self.oracle_hip.OracleError as error: + self.skipTest(str(error)) + template = (hw_oracle.HERE / "template.s").read_text() + for wave64, mask in ((False, "s8"), (True, "s[8:9]")): + text = template + for key, value in (("@TARGET@", "gfx1010"), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"), + ("@DENORM@", "0"), ("@DENORM16@", "3"), ("@IEEE@", "0"), ("@DX10_CLAMP@", "1"), ("@ROUND32@", "0"), + ("@ROUND16@", "0"), ("@FP16_OVERFLOW@", "0"), ("@LDS@", "4096"), + ("@BODY@", f" v_cmp_lt_f32 {mask}, v4, v5\n v_cndmask_b32 v10, v6, v7, {mask}\n")): + text = text.replace(key, value) + with self.subTest(wave64=wave64): + self.assertEqual(self.oracle_hip.assemble(text.encode(), "gfx1010", wave64)[:4], b"\x7fELF") + + if __name__ == "__main__": unittest.main() From cbdbc601dc526a10d474eba1d486a17b9a2b60ce Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:11:13 +0300 Subject: [PATCH 0420/1360] fix(shader): keep undecoded OpExtInst and runtime logical guards in specialization --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 11 --------- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 8 +++---- .../SpirvBackend/src/SpirvSpecialization.cpp | 24 ++----------------- 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index d78cb62a9f..00796cd2b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -322,8 +322,6 @@ void testPrivateCompositeTableLoadFolding() { const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); - const auto glsl = module.Import("GLSL.std.450"); - const auto customSet = module.Import("NonSemantic.Test"); const auto mainId = module.AllocateId(); const auto lane = module.AllocateId(); const auto rawCond = module.AllocateId(); @@ -341,11 +339,6 @@ void testPrivateCompositeTableLoadFolding() { module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); module.AddFunction(spv::OpStore, writePtr, lane); } - const auto liveGlsl = module.AllocateId(); - module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), glsl, 37u, lane, zero); - module.AddFunction(spv::OpExtInst, typeU32, liveGlsl, glsl, 37u, lane, one); - module.AddFunction(spv::OpStore, output, liveGlsl); - module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), customSet, 1u, lane); module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); @@ -364,16 +357,12 @@ void testPrivateCompositeTableLoadFolding() { check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); - check(foldedCounts.contains(spv::OpExtInst) && foldedCounts.at(spv::OpExtInst) == 2u, "an unused GLSL.std.450 OpExtInst was not removed or a live/non-GLSL.std.450 OpExtInst was removed"); bool addedThirty = false; - bool keptCustomExtInst = false; for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { if ((folded[cursor] >> 16u) == 0u) break; if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; - if ((folded[cursor] & 0xffffu) == spv::OpExtInst && folded[cursor + 4u] == 1u) keptCustomExtInst = true; } check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); - check(keptCustomExtInst, "a non-GLSL.std.450 OpExtInst was not preserved"); const auto [dynamic, unusedDynamic] = build(true, false); static_cast(unusedDynamic); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 433c58aad1..35d5e131dd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -446,7 +446,7 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue const auto fromMemory = Binary(state, spv::OpUGreaterThanEqual, boolean, selectors[output], constant(4u)); required[component] = Binary(state, spv::OpLogicalOr, boolean, required[component], both(state, fromMemory, equal(state, selected[output], constant(component)))); } - required[component] = both(state, active[component], required[component]); + required[component] = Select(state, boolean, active[component], required[component], ConstantBool(state, false)); const auto componentOffset = field(state, byteOffsets, component * 8u, 8u); const auto bytes = Select(state, u32, packed, constant(4u), Binary(state, spv::OpShiftRightLogical, u32, bits[component], constant(3u))); const auto offset = Binary(state, spv::OpIAdd, u32, resource.offset != 0u ? resource.offset : Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), constant(memory.offset)), componentOffset); @@ -454,16 +454,16 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue if (memory.gpuDescriptor) { const auto u64 = TypeScalarU64(state); const auto guest = Binary(state, spv::OpIAdd, u64, resource.base, Unary(state, spv::OpUConvert, u64, addresses[component].guest)); - const auto overflow = both(state, active[component], Binary(state, spv::OpULessThan, boolean, guest, resource.base)); + const auto overflow = Select(state, boolean, active[component], Binary(state, spv::OpULessThan, boolean, guest, resource.base), ConstantBool(state, false)); EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, resource.base, constant(0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Overflow); }); StopBdaInvocationIf(state, overflow); addresses[component].guest = guest; } - inBounds = both(state, inBounds, Binary(state, spv::OpLogicalOr, boolean, Unary(state, spv::OpLogicalNot, boolean, required[component]), addresses[component].inBounds)); + inBounds = Select(state, boolean, required[component], both(state, inBounds, addresses[component].inBounds), inBounds); } std::array values{}; for (std::uint32_t component = 0; component < 4u; ++component) { - values[component] = EmitValueOrZeroIfCondition(state, both(state, inBounds, required[component]), [&] { + values[component] = EmitValueOrZeroIfCondition(state, Select(state, boolean, required[component], inBounds, ConstantBool(state, false)), [&] { const auto width = Select(state, u32, packed, constant(32u), bits[component]); const auto dword = equal(state, width, constant(32u)); const auto raw = memory.gpuDescriptor diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 2b7b1eb792..b20fa14351 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -100,7 +100,7 @@ private: std::vector present; }; -bool PureInstruction(const Instruction& instruction, const IdSet& glslExtInstSets = {}) { +bool PureInstruction(const Instruction& instruction) { switch (Opcode(instruction)) { case spv::OpVectorExtractDynamic: case spv::OpVectorInsertDynamic: @@ -219,8 +219,6 @@ bool PureInstruction(const Instruction& instruction, const IdSet& glslExtInstSet case spv::OpArrayLength: case spv::OpPhi: return true; - case spv::OpExtInst: - return instruction.size() >= 5u && glslExtInstSets.contains(instruction[3]); case spv::OpLoad: return instruction.size() == 4u || (instruction.size() == 5u && instruction[4] == spv::MemoryAccessMaskNone); default: @@ -233,9 +231,6 @@ bool VisitInputs(const Instruction& instruction, const TVisitor& visit) { std::size_t first = 3u; std::size_t end = instruction.size(); switch (Opcode(instruction)) { - case spv::OpExtInst: - first = 5u; - break; case spv::OpVectorShuffle: case spv::OpCompositeInsert: end = 5u; @@ -583,7 +578,6 @@ public: values.Reset(bound); resultTypes.Reset(bound); vectorTypes.Reset(bound); - glslExtInstSets.Reset(bound); removed.Reset(bound); aliases.Reset(bound); composites.Reset(bound); @@ -598,7 +592,6 @@ public: bool hasType = false; spv::HasResultAndType(op, &hasResult, &hasType); if (hasResult && hasType) resultTypes.emplace(instruction.at(2), instruction.at(1)); - if (op == spv::OpExtInstImport && instruction.size() == 6u && instruction[2] == 0x4c534c47u && instruction[3] == 0x6474732eu && instruction[4] == 0x3035342eu && instruction[5] == 0u) glslExtInstSets.insert(instruction.at(1)); if (op == spv::OpTypeBool) types.emplace(instruction.at(1), ScalarType{1u, true}); if (op == spv::OpTypeInt) types.emplace(instruction.at(1), ScalarType{instruction.at(2), false}); if (op == spv::OpTypeVector) vectorTypes.insert(instruction.at(1)); @@ -680,18 +673,6 @@ private: return pointer->second < elements.size() ? value(elements[pointer->second]) : std::nullopt; } const auto left = value(instruction[3]); - if (op == spv::OpLogicalAnd && instruction.size() == 5u) { - const auto right = value(instruction[4]); - if ((left && *left == 0u) || (right && *right == 0u)) return 0u; - if (left && right) return *left != 0u && *right != 0u; - return std::nullopt; - } - if (op == spv::OpLogicalOr && instruction.size() == 5u) { - const auto right = value(instruction[4]); - if ((left && *left != 0u) || (right && *right != 0u)) return 1u; - if (left && right) return *left != 0u || *right != 0u; - return std::nullopt; - } if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; if (op == spv::OpLogicalNot) return *left == 0u; @@ -1128,7 +1109,7 @@ private: } continue; } - if (PureInstruction(instruction, glslExtInstSets)) { + if (PureInstruction(instruction)) { definitions.emplace(Result(instruction), index); continue; } @@ -1165,7 +1146,6 @@ private: IdTable values; IdTable resultTypes; IdSet vectorTypes; - IdSet glslExtInstSets; IdSet removed; IdTable aliases; IdTable> composites; From df2173214d285994dcc29e1b977d273bcd4737c2 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 21 Oct 2026 17:16:34 +0200 Subject: [PATCH 0421/1360] fix(libSceCesCs): size SBC profiles for the 8-byte tag checks Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/Export.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 25dcb94514..fe24b79623 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -21,10 +21,10 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); -constexpr uint8_t PROFILES[3] = {0, 1, 2}; -constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0]; -constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1]; -constexpr const uint8_t* CP1250_PROFILE = &PROFILES[2]; +constexpr uint8_t PROFILES[3][8] = {{0}, {1}, {2}}; +constexpr const uint8_t* CP1252_PROFILE = PROFILES[0]; +constexpr const uint8_t* CP1251_PROFILE = PROFILES[1]; +constexpr const uint8_t* CP1250_PROFILE = PROFILES[2]; constexpr uint16_t CP1252_HIGH[32] = { 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, From c6ce329c474d05197b39b77e8dfcf09884ba13ae Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:20:34 +0000 Subject: [PATCH 0422/1360] fix(relinker): reject link-time relocation types in executables The executable's relocation allowlist accepted R_X86_64_GOT32, PLT32, GOTPCREL, 32S, GOTPCRELX and REX_GOTPCRELX, which the linker resolves and glibc's ld.so does not apply. The default and --lazy-binding Linux output copied them from DT_RELA into its dynamic relocation table, and --linux-fixed-base did so for 32S, so the relink succeeded and ld.so then refused to load the program ("unexpected reloc type"). They are now rejected when the executable is validated, for every target. --- core/relinker/relinker/src/analysis/ValidationPolicy.cpp | 6 ------ .../relinker/tests/test_executable_tls_relocations.py | 9 +++++++-- core/relinker/relinker/tests/test_linux_fixed_base.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index dfd0a11798..47d65ac011 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -11,20 +11,14 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_NONE, R_X86_64_64, R_X86_64_PC32, - R_X86_64_GOT32, - R_X86_64_PLT32, R_X86_64_COPY, R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT, R_X86_64_RELATIVE, - R_X86_64_GOTPCREL, R_X86_64_32, - R_X86_64_32S, R_X86_64_DTPMOD64, R_X86_64_DTPOFF64, R_X86_64_TPOFF64, - R_X86_64_GOTPCRELX, - R_X86_64_REX_GOTPCRELX, }; } diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..b1857f1556 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -3,7 +3,9 @@ has them in its GOT. The Linux ELF keeps them dynamic and symbolless, with the v TLS block as the addend, for the dynamic loader to resolve against where it puts the block; a named one against a defined STT_TLS symbol is folded the same way, and one against an import stays named, typed STT_TLS. The Windows and macOS images resolve TPOFF64 and DTPOFF64 statically against their block, -AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express.""" +AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express. Every target +rejects the link-time types GOT32, PLT32, GOTPCREL, 32S, GOTPCRELX and REX_GOTPCRELX, which glibc's +ld.so does not apply.""" from pathlib import Path import struct @@ -18,6 +20,8 @@ from test_tls_function_coverage import pe_bytes_at PT_LOAD = 1 PT_TLS = 7 DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 +LINK_TIME = {3: "got32", 4: "plt32", 9: "gotpcrel", 11: "32s", 41: "gotpcrelx", 42: "rex-gotpcrelx"} +TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", ("--macos",): "macos"} TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 STRINGS = b"\0Tls\0Data\0ImportedTls\0" @@ -142,7 +146,8 @@ def main(): ("windows-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--windows",), "Imported TLS relocation"), ("macos-dtpmod", [(0x1000, DTPMOD64, NULL, 0)], True, ("--macos",), "DTPMOD64 relocation is not supported"), ("macos-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--macos",), "Imported TLS relocation"), - ] + ] + [(f"{target}-{name}", [(0x1000, kind, NULL, 0)], True, flags, f"Unsupported relocation type {kind:#x}") + for kind, name in LINK_TIME.items() for flags, target in TARGETS.items()] for name, relocations, has_tls, flags, error in failures: result, output = relink(relinker, directory, name, image(relocations, has_tls), *flags) assert result.returncode == 2 and error in result.stderr, (name, result.returncode, result.stderr) diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 84244f4533..6b9d45466b 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -154,7 +154,7 @@ def main(): for name, image, base, error in ( ("unaligned", fixture(alignment=0x20000), 0x410000, "is not a multiple of the PT_LOAD alignment 0x20000"), ("beyond-user-space", fixture(memory_size=0x100000), 0x7FFFFFFF0000, "outside the Linux user address space"), - ("unsupported-type", fixture(relocation_type=9), BASE, "Relocation type 9 cannot be placed at a fixed Linux base"), + ("unsupported-type", fixture(relocation_type=11), BASE, "Unsupported relocation type 0xb"), ("negative-target", fixture(addend=-16), BASE, "Relocation target -16 lies below the image at a fixed Linux base"), ): result, output = relink(relinker, work, name, image, "--linux-fixed-base", hex(base)) From e5569a8e3555fb0a7e131e71b9e5192ad2f3929e Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 21 Oct 2026 17:24:48 +0200 Subject: [PATCH 0423/1360] fix(libkernel): read a watched file's path before its state on Windows TakeSnapshot read the file information and then the path. A delete between the two left a snapshot whose path was already under \$Extend\$Deleted\ but whose link count and delete flag were not, so IsDeleted was false and the watch thread asked for a parent watch of \$Extend\$Deleted\. FindFirstChangeNotification refuses it with ERROR_ACCESS_DENIED, and the exception on the detached watch thread ended the process. guest_kqueue hit this when FileWatch removed its file while a watch thread was still polling. The path is now read first. A path under \$Extend\$Deleted\ is only returned after the delete, so the information read after it reports the delete and the parent watch is skipped. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Equeue/src/FileEvent.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index 04fe98539f..cd24ff30d0 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -165,6 +165,16 @@ struct Snapshot { }; bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { + std::wstring path(MAX_PATH, L'\0'); + auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); + if (length >= path.size()) { + path.resize(length); + length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); + } + if (length != 0 && length < path.size()) { + path.resize(length); + snapshot.path = std::move(path); + } FILE_BASIC_INFO basic{}; FILE_STANDARD_INFO standard{}; if (!::GetFileInformationByHandleEx(handle, FileBasicInfo, &basic, sizeof(basic)) || @@ -178,16 +188,6 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { snapshot.creation = basic.CreationTime.QuadPart; snapshot.lastWrite = basic.LastWriteTime.QuadPart; snapshot.change = basic.ChangeTime.QuadPart; - std::wstring path(MAX_PATH, L'\0'); - auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); - if (length >= path.size()) { - path.resize(length); - length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); - } - if (length != 0 && length < path.size()) { - path.resize(length); - snapshot.path = std::move(path); - } return true; } From d28c74af94957db485d1f47e7422680301d2a420 Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 Date: Wed, 21 Oct 2026 17:25:25 +0200 Subject: [PATCH 0424/1360] feat(libkernel): implement setresuid and setresgid Follow FreeBSD 11.4 sys_setresuid and sys_setresgid (sys/kern/kern_prot.c): accept real, effective, and saved user and group IDs for the guest process, which is assumed to run as root with IDs 0. Setting an ID to 0 or -1 (unchanged) is accepted without changing credentials and returns 0; any other value throws. Cover both in guest_credentials test and update libkernel debt documentation. --- .../libs/prx/libkernel/System/src/Process.cpp | 22 +++++++++++ core/libs/tests/GuestCredentials.cpp | 37 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 348464a177..a72f3ee38c 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -770,6 +770,28 @@ int APS5_VABI setregid_nid_postfix(std::uint32_t rgid, std::uint32_t egid) { return 0; } +int APS5_VABI setresuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid, std::uint32_t suid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (ruid != 0 && ruid != unchanged) + throw std::runtime_error("setresuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setresuid: unsupported effective user id " + std::to_string(euid)); + if (suid != 0 && suid != unchanged) + throw std::runtime_error("setresuid: unsupported saved user id " + std::to_string(suid)); + return 0; +} + +int APS5_VABI setresgid_nid_postfix(std::uint32_t rgid, std::uint32_t egid, std::uint32_t sgid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (rgid != 0 && rgid != unchanged) + throw std::runtime_error("setresgid: unsupported real group id " + std::to_string(rgid)); + if (egid != 0 && egid != unchanged) + throw std::runtime_error("setresgid: unsupported effective group id " + std::to_string(egid)); + if (sgid != 0 && sgid != unchanged) + throw std::runtime_error("setresgid: unsupported saved group id " + std::to_string(sgid)); + return 0; +} + int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { if (count < 0 || count > freebsdGroupsMax) { *__error_nid_postfix() = errnoInvalidArgument; diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index 887f69c727..171be1a296 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -11,6 +11,8 @@ int APS5_VABI setuid_nid_postfix(std::uint32_t); int APS5_VABI setgid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setregid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setresuid_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI setresgid_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); int APS5_VABI getuid_nid_postfix(void); @@ -43,6 +45,17 @@ static bool ThrowsRe(SetReId function, std::uint32_t r, std::uint32_t e) { return false; } +using SetResId = int (APS5_VABI *)(std::uint32_t, std::uint32_t, std::uint32_t); + +static bool ThrowsRes(SetResId function, std::uint32_t r, std::uint32_t e, std::uint32_t s) { + try { + function(r, e, s); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr std::uint32_t Unchanged = 0xffffffffu; @@ -82,6 +95,30 @@ int main() { Require(ThrowsRe(setregid_nid_postfix, 1000, 0)); Require(ThrowsRe(setregid_nid_postfix, 0, 1000)); + Require(setresuid_nid_postfix(Unchanged, Unchanged, Unchanged) == 0); + Require(setresuid_nid_postfix(0, Unchanged, Unchanged) == 0); + Require(setresuid_nid_postfix(Unchanged, 0, Unchanged) == 0); + Require(setresuid_nid_postfix(Unchanged, Unchanged, 0) == 0); + Require(setresuid_nid_postfix(0, 0, 0) == 0); + Require(ThrowsRes(setresuid_nid_postfix, 1000, 0, 0)); + Require(ThrowsRes(setresuid_nid_postfix, 0, 1000, 0)); + Require(ThrowsRes(setresuid_nid_postfix, 0, 0, 1000)); + Require(ThrowsRes(setresuid_nid_postfix, 1000, Unchanged, Unchanged)); + Require(ThrowsRes(setresuid_nid_postfix, Unchanged, 1000, Unchanged)); + Require(ThrowsRes(setresuid_nid_postfix, Unchanged, Unchanged, 1000)); + + Require(setresgid_nid_postfix(Unchanged, Unchanged, Unchanged) == 0); + Require(setresgid_nid_postfix(0, Unchanged, Unchanged) == 0); + Require(setresgid_nid_postfix(Unchanged, 0, Unchanged) == 0); + Require(setresgid_nid_postfix(Unchanged, Unchanged, 0) == 0); + Require(setresgid_nid_postfix(0, 0, 0) == 0); + Require(ThrowsRes(setresgid_nid_postfix, 1000, 0, 0)); + Require(ThrowsRes(setresgid_nid_postfix, 0, 1000, 0)); + Require(ThrowsRes(setresgid_nid_postfix, 0, 0, 1000)); + Require(ThrowsRes(setresgid_nid_postfix, 1000, Unchanged, Unchanged)); + Require(ThrowsRes(setresgid_nid_postfix, Unchanged, 1000, Unchanged)); + Require(ThrowsRes(setresgid_nid_postfix, Unchanged, Unchanged, 1000)); + std::array groups{}; Require(getgroups_nid_postfix(0, nullptr) == 1); Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..a1193af5ca 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -80,7 +80,7 @@ - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid, setuid, setgid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, setgid, setreuid, setregid, setresuid, setresgid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid`, `sys_setregid`, `sys_setresuid` and `sys_setresgid`, id 0 (or -1 for an unchanged `setreuid`/`setregid`/`setresuid`/`setresgid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported From 8f13fe70b97752d824b6e56adf8279e7f9d7cb82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 12:44:43 +0000 Subject: [PATCH 0425/1360] feat(libkernel): implement pthread_rwlock_reltimedrdlock_np and pthread_rwlock_reltimedwrlock_np Both were missing from libkernel.prx, so a title importing either (dYv-+If2GPk, RRnSj8h8VR4) failed to load. They take a relative timeout in microseconds and are scePthreadRwlockTimedrdlock and scePthreadRwlockTimedwrlock with POSIX error numbers; a zero lock is initialized as by pthread_rwlock_rdlock. The signature comes from shadPS4, which registers one function for each NID and its scePthreadRwlockTimed* twin; FreeBSD 11.4 has no such calls. The throwing libkernel_sys copies are removed, as an export may live in one library only. Recorded in the libkernel debt file. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libkernel/Pthread/Posix/Rwlock.cpp | 10 ++++ .../prx/libkernel_sys/Export.Discovery.cpp | 10 ---- core/libs/tests/GuestPthreadRwlock.cpp | 49 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index c07eadf645..203af9f0f4 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -93,6 +93,16 @@ int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, cons return toPosix(scePthreadRwlockTimedwrlock(rwlock, usec)); } +int APS5_VABI pthread_rwlock_reltimedrdlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec) { + initializeStatic(rwlock, __func__); + return toPosix(scePthreadRwlockTimedrdlock(rwlock, usec)); +} + +int APS5_VABI pthread_rwlock_reltimedwrlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec) { + initializeStatic(rwlock, __func__); + return toPosix(scePthreadRwlockTimedwrlock(rwlock, usec)); +} + int APS5_VABI pthread_rwlock_tryrdlock_nid_postfix(PthreadRwlock* rwlock) { initializeStatic(rwlock, __func__); return toPosix(scePthreadRwlockTryrdlock(rwlock)); diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..3e3bf9caea 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -463,16 +463,6 @@ int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_rwlock_reltimedrdlock_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI pthread_rwlock_reltimedwrlock_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index b7f54df37e..91c219a1cb 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -23,6 +23,8 @@ int APS5_VABI pthread_rwlock_tryrdlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_trywrlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); +int APS5_VABI pthread_rwlock_reltimedrdlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec); +int APS5_VABI pthread_rwlock_reltimedwrlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec); int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); @@ -312,6 +314,52 @@ static void CheckWaitingWriterHoldsBackNewReaders() { Require(pthread_rwlock_destroy_nid_postfix(&recursive) == 0); } +using RelativeLock = int (APS5_VABI *)(PthreadRwlock*, KernelUseconds); + +static void ExpectRelativeTimeout(RelativeLock lock, PthreadRwlock* rwlock) { + const auto start = std::chrono::steady_clock::now(); + Require(lock(rwlock, 20000) == GUEST_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(lock(rwlock, 0) == GUEST_ETIMEDOUT); +} + +static void CheckRelativeTimeouts() { + PthreadRwlock fresh[2] = {}; + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&fresh[0], 0) == 0); + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&fresh[1], 0) == 0); + for (auto& lock : fresh) { + Require(lock != nullptr && lock != DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&lock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&lock) == 0); + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&lock, 0) == GUEST_EINVAL); + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&lock, 0) == GUEST_EINVAL); + } + + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&rwlock, 5000000) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + + Holder reader{&rwlock, false}; + Start(reader); + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&rwlock, 0) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + ExpectRelativeTimeout(pthread_rwlock_reltimedwrlock_np_nid_postfix, &rwlock); + reader.release.store(true); + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&rwlock, 5000000) == 0); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + + Holder writer{&rwlock, true}; + Start(writer); + ExpectRelativeTimeout(pthread_rwlock_reltimedrdlock_np_nid_postfix, &rwlock); + ExpectRelativeTimeout(pthread_rwlock_reltimedwrlock_np_nid_postfix, &rwlock); + writer.release.store(true); + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&rwlock, 5000000) == 0); + Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); +} + template static bool Throws(Call call) { try { @@ -354,6 +402,7 @@ int main() { CheckWaitingWriterHoldsBackNewReaders(); CheckConcurrentFirstWrlockExcludes(); CheckDeadlinesBeyondTheClockRange(); + CheckRelativeTimeouts(); const KernelTimespec invalid{0, NANOS_PER_SECOND}; PthreadRwlock fresh[4] = {}; Require(pthread_rwlock_tryrdlock_nid_postfix(&fresh[0]) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..1a1c5f4b0f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -65,6 +65,7 @@ - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) - [pthread_getprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) is unmeasured on the console. NID `aO+WS2xWuo4` is named in the firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c). Its return and errno contract follows FreeBSD 11 [thr_getprio.c](https://github.com/freebsd/freebsd-src/blob/671fb7df97631020c42ce55527dc49fffe0656b7/lib/libthr/thread/thr_getprio.c): return the recorded guest priority, or -1 with `ESRCH` for an unknown or finished thread. The live-thread registry lock protects the read from a concurrent join. This reports guest scheduler state, not the host scheduling priority. +- [pthread_rwlock_reltimedrdlock_np, pthread_rwlock_reltimedwrlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) take `(rwlock, usec)`, a relative timeout in microseconds, and are `scePthreadRwlockTimedrdlock` and `scePthreadRwlockTimedwrlock` with POSIX error numbers, so they return what those return (`ETIMEDOUT` when the time runs out, at once for 0 while another thread holds the lock, and `EINVAL` for a destroyed lock); a zero lock is initialized as by `pthread_rwlock_rdlock`. FreeBSD 11.4 libthr has no such functions; the signature comes only from shadPS4, which registers one function for each NID and its `scePthreadRwlockTimed*` twin ([`rwlock.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/rwlock.cpp#L167-L179), 32-bit `usec`). Unverified on the console - [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 - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it From f65a3f334162654b9fb2818abe9d63670e7f9b4c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:29:16 +0200 Subject: [PATCH 0426/1360] fix(libc): publish the guest FILE status of a stream with atomic accesses Two threads writing to the same stream each ran FileStream::SyncStatus after their stdio call, outside the FILE lock, with plain writes to the read and write counters and a read-modify-write of the flag word, so the flags of one thread could be lost. Publish the counters with atomic stores and the flags with a compare-exchange loop; the values stay the same. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/FileStream.hpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 1b9954d873..d901f6ebb8 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -200,18 +200,21 @@ public: _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast(descriptor) : -1; return true; } + void PublishStatus(bool endOfFile, bool error) { + __atomic_store_n(&_guest.readRemaining, 0, __ATOMIC_RELAXED); + __atomic_store_n(&_guest.writeRemaining, 0, __ATOMIC_RELAXED); + auto old = __atomic_load_n(&_guest.flags, __ATOMIC_RELAXED); + std::int16_t next; + do next = static_cast((old & ~0x60) | (endOfFile ? 0x20 : 0) | (error ? 0x40 : 0)); + while (!__atomic_compare_exchange_n(&_guest.flags, &old, next, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)); + } void SyncStatus() { if (!_handle) return; - _guest.readRemaining = 0; - _guest.writeRemaining = 0; - _guest.flags = static_cast((_guest.flags & ~0x60) | - (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || guestError ? 0x40 : 0)); + PublishStatus(std::feof(GetHandle()) != 0, std::ferror(GetHandle()) != 0 || guestError); } void SyncUnlockedStatus() { if (!_handle) return; - _guest.readRemaining = 0; - _guest.writeRemaining = 0; #ifdef _WIN32 const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; @@ -219,8 +222,7 @@ public: const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; const bool error = ::ferror_unlocked(GetHandle()) != 0; #endif - _guest.flags = static_cast((_guest.flags & ~0x60) | - (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); + PublishStatus(endOfFile, error || guestError); } bool SwitchToReading() { From b03a8ea18490a4d4b7f3dce295a8d31425ba0e9f Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:31:37 +0300 Subject: [PATCH 0427/1360] feat(libSceNpManager): report that the user is not a PlayStation Plus member --- core/libs/prx/libSceNpManager/Export.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 1b45d38294..c2a0ffc370 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -348,8 +348,9 @@ int APS5_VABI sceNpIntRegisterGamePresenceCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpIsPlusMember(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpIsPlusMember(int, bool* isMember) { + if (!isMember) return SCE_NP_ERROR_INVALID_ARGUMENT; + *isMember = false; return 0; } int APS5_VABI sceNpManagerIntAbortRequest(void) { From fa4415ba5dfea5be301fe92f21c0a5fdc13226d0 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:31:37 +0300 Subject: [PATCH 0428/1360] feat(libSceNpManager): check the content restriction passed to sceNpSetContentRestriction --- core/libs/prx/libSceNpManager/Export.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index c2a0ffc370..98120380ca 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -14,6 +14,7 @@ static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); +static constexpr int SCE_NP_ERROR_INVALID_SIZE = static_cast(0x80550011); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; @@ -872,9 +873,19 @@ int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdat } int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction) { - (void)restriction; - NotImplemented_nid_no_patch(__func__); - return 0; + struct Fields { + std::uint64_t size; + std::int8_t defaultAgeRestriction; + std::uint8_t padding[3]; + std::int32_t ageRestrictionCount; + const void* ageRestrictions; + }; + if (!restriction) return SCE_NP_ERROR_INVALID_ARGUMENT; + const auto* fields = reinterpret_cast(restriction); + if (fields->size != sizeof(Fields)) return SCE_NP_ERROR_INVALID_SIZE; + if (fields->defaultAgeRestriction < 0 || fields->ageRestrictionCount > 0x100) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (fields->ageRestrictionCount > 0 && !fields->ageRestrictions) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* title_secret) { From 28d2b827dd7c14a0dfb908a08235ac794e81d479 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:33:30 +0300 Subject: [PATCH 0429/1360] test(libSceNpManager): check the Plus membership query and the content restriction checks --- core/libs/tests/GuestNpManager.cpp | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 83a5566664..f840d91469 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -25,6 +25,8 @@ int APS5_VABI sceNpGetAccountId(const NpOnlineId* online_id, std::uint64_t* acco int APS5_VABI sceNpSetGamePresenceOnline(int req_id, const NpOnlineId* online_id, std::uint32_t presence); int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata); int APS5_VABI sceNpUnregisterPlusEventCallback(void); +int APS5_VABI sceNpIsPlusMember(int user_id, bool* is_member); +int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction); } extern "C" { @@ -50,6 +52,7 @@ namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidSize = static_cast(0x80550011u); constexpr int UserNotFound = static_cast(0x80550007u); constexpr int AlreadyRegistered = static_cast(0x80550008u); constexpr int NotRegistered = static_cast(0x80550009u); @@ -275,11 +278,52 @@ void CheckOnlineIdQueries() { } +struct ContentRestrictionFields { + std::uint64_t size; + std::int8_t defaultAgeRestriction; + std::uint8_t padding[3]; + std::int32_t ageRestrictionCount; + const void* ageRestrictions; +}; + +int SetContentRestriction(const ContentRestrictionFields& fields) { + return sceNpSetContentRestriction(reinterpret_cast(&fields)); +} + +void CheckPlusAndContentRestriction() { + bool isMember = true; + Require(sceNpIsPlusMember(0x10000, &isMember) == 0 && !isMember, "sceNpIsPlusMember must report no Plus membership"); + Require(sceNpIsPlusMember(0x10000, nullptr) == InvalidArgument, "sceNpIsPlusMember must reject a null output"); + + const std::uint8_t ages[8]{}; + ContentRestrictionFields fields{sizeof(ContentRestrictionFields), 12, {}, 2, ages}; + Require(SetContentRestriction(fields) == 0, "sceNpSetContentRestriction must accept a valid restriction"); + fields.ageRestrictionCount = 0; + fields.ageRestrictions = nullptr; + Require(SetContentRestriction(fields) == 0, "sceNpSetContentRestriction must accept no age restrictions"); + Require(sceNpSetContentRestriction(nullptr) == InvalidArgument, "sceNpSetContentRestriction must reject null"); + auto bad = fields; + bad.size = sizeof(ContentRestrictionFields) + 8; + Require(SetContentRestriction(bad) == InvalidSize, "sceNpSetContentRestriction must check the size"); + bad = fields; + bad.defaultAgeRestriction = -1; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject a negative default age"); + bad = fields; + bad.ageRestrictionCount = 0x101; + bad.ageRestrictions = ages; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject more than 256 restrictions"); + bad.ageRestrictionCount = 0x100; + Require(SetContentRestriction(bad) == 0, "sceNpSetContentRestriction must accept 256 restrictions"); + bad.ageRestrictions = nullptr; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject restrictions without an array"); +} + int main() { CheckOnlineIdQueries(); CheckStateCallbacksA(); CheckLegacyStateCallback(); CheckRequestTimeouts(); + CheckPlusAndContentRestriction(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); NpId untouched{}; From 3cf57aa463ccbe7c5b159825ae3de4a29fdf8a17 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:33:30 +0300 Subject: [PATCH 0430/1360] docs(debt): record the libSceNpManager Plus membership and content restriction calls --- docs/dev/debt/libSceNpManager.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 9cf1c6accd..aff94cf9a7 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -6,7 +6,7 @@ ### Unknown function info -- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 150 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. +- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 149 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory. @@ -25,5 +25,6 @@ - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests - [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured - [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 +- [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpCheckCallbackForLib`](../../../core/libs/prx/libSceNpManager/Export.cpp). From 959efbb8d319d0a0ee22f11cf38a8301c73e14c2 Mon Sep 17 00:00:00 2001 From: zmeizy Date: Wed, 21 Oct 2026 18:29:55 +0300 Subject: [PATCH 0431/1360] fix(shader): take the hardware f32 flush only in shaders that flush f32 denormals When the startup probe chose the hardware f32 flush, every module declared DenormFlushToZero 32, also shaders whose FLOAT_MODE keeps f32 denormals (f32 denormal field 1, 2 or 3), so the device flushed what they keep and DenormPreserve 32 was never declared. The hardware flush now applies to shaders that flush f32 denormals (field 0, or no float mode); the others declare DenormPreserve 32 where the device supports it. Co-Authored-By: Claude Opus 5.5 --- .../tests/F32DenormPreserve.cpp | 19 ++++++++++++++++--- core/shader/recompiler/Recompiler.cpp | 8 ++++++-- docs/dev/debt/shader.md | 1 + 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp index 0f3fe85da7..f47e2d0ebc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -19,17 +20,19 @@ void require(bool condition, const std::string& message) { } constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; +constexpr std::array MovStore{0x7e020300u, 0xe0700000u, 0x80000100u, 0xbf810000u}; -std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { +std::vector recompile(bool preserve, bool flush, bool flushCapability = true, std::optional floatMode = std::nullopt, std::span code = CvtF16F32Store) { const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 0, {}}; + 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.context.floatMode = floatMode; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 64; @@ -79,6 +82,16 @@ void verify() { require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); const auto nativeFlush = scan(recompile(true, true, false)); require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); + const auto flushingMode = scan(recompile(true, true, true, ShaderFloatMode{0xc0u, true, false, false}, MovStore)); + require(flushingMode.flush32 && !flushingMode.preserve32, "FLOAT_MODE 0xc0 flushes f32 denormals but the module did not take the hardware flush"); + for (const std::uint32_t mode : {0xd0u, 0xe0u, 0xf0u}) { + char name[64]; + std::snprintf(name, sizeof(name), "FLOAT_MODE 0x%02x keeps f32 denormals", mode); + const auto kept = scan(recompile(true, true, true, ShaderFloatMode{mode, true, false, false}, MovStore)); + require(kept.preserve32 && kept.preserveCapability && !kept.flush32, std::string(name) + ", but the module did not declare DenormPreserve 32 instead of the hardware flush"); + const auto unpreserved = scan(recompile(false, true, true, ShaderFloatMode{mode, true, false, false}, MovStore)); + require(!unpreserved.flush32 && !unpreserved.preserve32, std::string(name) + ", but the module took the hardware flush on a device that cannot preserve them"); + } } } @@ -90,6 +103,6 @@ int main() { std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); return 1; } - std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush in shaders that flush f32 denormals"); return 0; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..bae14c10ed 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -153,6 +153,10 @@ std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; } +bool HardwareFloat32Flush(const RecompileRequest& request) { + return request.target.float32FlushToZero && (!request.context.floatMode.has_value() || ((request.context.floatMode->floatMode >> 4u) & 3u) == 0u); +} + std::atomic frontEndBuilds{0}; } @@ -239,7 +243,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); translateOptions.interpolationFunctions = supports(spv::CapabilityInterpolationFunction); translateOptions.floatMode = request.context.floatMode; - translateOptions.float32FlushToZero = request.target.float32FlushToZero; + translateOptions.float32FlushToZero = HardwareFloat32Flush(request); translateOptions.inputInfo = inputInfo; constexpr InstructionTranslator translator; @@ -602,7 +606,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; targetOptions.spannedWaves = request.target.spannedWaves; targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; - targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32FlushToZero = HardwareFloat32Flush(request); targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d76932d773..9a4ac1f273 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -116,6 +116,7 @@ - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. +- [32-bit denormal execution mode](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler) - the hardware f32 flush that the startup probe chooses (`DenormFlushToZero 32`) is declared only in shaders whose `FLOAT_MODE` flushes f32 denormals (field 0, or no known float mode). A shader that keeps them on either side (field 1, 2 or 3) declares `DenormPreserve 32` when the device has `shaderDenormPreserveFloat32` with a `denormBehaviorIndependence` other than NONE; on other devices it gets the driver's default f32 denormal handling, which flushes the denormals it keeps on MoltenVK. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked From e3110be3a37343eecc9f486b645235d07b984f98 Mon Sep 17 00:00:00 2001 From: Amr Yasser <154547867+AMRYB@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:36:39 +0300 Subject: [PATCH 0432/1360] fix(tools): normalize Windows module suffix case before matching --- tools/import_audit.py | 4 +++- tools/tests/test_import_audit.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 6ee58ad673..9e2c15b9fd 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -538,11 +538,13 @@ MODULE_FILE_SUFFIXES = (".debug_prx", ".suprx", ".sprx", ".prx") def module_stem(name, fold): + if fold: + name = name.lower() for suffix in MODULE_FILE_SUFFIXES: if len(name) > len(suffix) and name.endswith(suffix): name = name[:-len(suffix)] break - return name.lower() if fold else name + return name class ModuleNames(set): diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index a29775c13b..ff0787e7ed 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -605,6 +605,46 @@ class ImportAuditTests(unittest.TestCase): records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"})) self.assertEqual((records[0]["class"], missing), ("absent", {"SDK-Shipping.debug_prx": 1})) + def test_windows_module_suffix_case_does_not_report_existing_modules_as_missing(self): + work = self.work + (work.libs / "libc.prx").write_bytes(build_pe([])) + registry, report = work.root / "game.registry.json", work.root / "audit.json" + cases = (("sdk-shipping.prx", "SDK-Shipping.DEBUG_PRX"), + ("SDK-SHIPPING.PRX", "sdk-shipping.sprx"), + ("Sdk-Shipping.SuPrX", "SDK-SHIPPING.debug_prx"), + ("SDK-SHIPPING.DEBUG_PRX", "sdk-shipping.PRX")) + for filename, needed in cases: + with self.subTest(filename=filename, needed=needed): + module = work.modules / filename + module.write_bytes(exporting("AAAAAAAAAAA")) + try: + write_registry(registry, [("AAAAAAAAAAA", needed)]) + import_audit.needed_registry_path(registry).write_text(json.dumps([needed]), encoding="utf-8") + code, _, err = work.run(registry, "--json", str(report), "--strict-providers") + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["missing_libraries"], {}) + self.assertEqual(data["unique_by_class"], {"implemented": 0, "stub": 0, "absent": 0, "module": 1}) + self.assertEqual(data["references"], 1) + self.assertEqual(data["imports"][0]["library"], needed) + finally: + module.unlink() + + def test_elf_module_suffix_case_stays_significant(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "sdk-shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, report = work.root / "game.registry.json", work.root / "audit.json" + for suffix in ("PRX", "SPRX", "SUPRX", "DEBUG_PRX"): + needed = "sdk-shipping." + suffix + with self.subTest(needed=needed): + write_registry(registry, [("AAAAAAAAAAA", needed)]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["missing_libraries"], {needed: 1}) + self.assertEqual(data["unique_by_class"], {"implemented": 0, "stub": 0, "absent": 1, "module": 0}) + def test_converted_module_matches_imports_of_its_base_name(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) From deeea9d284c8de1d01c999c89dfd79772db0864b Mon Sep 17 00:00:00 2001 From: erickerxhalli <313650569+erickerxhalli@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:09:21 +0000 Subject: [PATCH 0433/1360] feat(libkernel_sys): implement sceKernelGetCompiledSdkVersion Write the low 32 bits of the SDK version field at offset 0x10 of the title's process parameters and return EINVAL (0x80020016) for a null output, following shadPS4. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WYjMwvLdPR8iX8EP8tQYWa --- core/libs/CMakeLists.txt | 9 ++++ .../prx/libkernel_sys/Export.Discovery.cpp | 5 --- core/libs/prx/libkernel_sys/Export.cpp | 9 ++++ core/libs/tests/GuestKernelSysSdkVersion.cpp | 43 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 4 ++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysSdkVersion.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..6b6deceb6f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -239,6 +239,15 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_sdk_version_tests tests/GuestKernelSysSdkVersion.cpp) + target_include_directories(guest_kernel_sys_sdk_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_sdk_version_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_sdk_version_tests) + add_test(NAME guest_kernel_sys_sdk_version COMMAND guest_kernel_sys_sdk_version_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_sdk_version PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..98bcb69987 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -938,11 +938,6 @@ int APS5_VABI sceKernelGetChildApp(void) { return 0; } -int APS5_VABI sceKernelGetCompiledSdkVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetCompiledSdkVersionByPath(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..12cd8c1138 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -19,6 +19,8 @@ #endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/CreationMask.hpp" @@ -255,6 +257,13 @@ int RenamePath(const std::filesystem::path& source, const std::filesystem::path& extern "C" { +int APS5_VABI sceKernelGetCompiledSdkVersion(std::int32_t* version) { + if (version == nullptr) return SCE_KERNEL_ERROR_EINVAL; + static const auto* parameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::memcpy(version, parameters + 0x10, sizeof(*version)); + return 0; +} + int APS5_VABI fork_nid_postfix(void) { NotImplemented_nid_no_patch("fork: child processes"); return -1; diff --git a/core/libs/tests/GuestKernelSysSdkVersion.cpp b/core/libs/tests/GuestKernelSysSdkVersion.cpp new file mode 100644 index 0000000000..2acba5d6d9 --- /dev/null +++ b/core/libs/tests/GuestKernelSysSdkVersion.cpp @@ -0,0 +1,43 @@ +#include "ProcessParametersFixture.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelGetCompiledSdkVersion(std::int32_t* version); + +namespace { + +alignas(16) std::array process{}; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "KERNEL SYS SDK VERSION: %s\n", message); + std::abort(); + } +} + +} + +int main() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + const std::uint64_t sdkVersion = 0xFFFFFFFF07008001ull; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); + + Require(sceKernelGetCompiledSdkVersion(nullptr) == static_cast(0x80020016), "null output is not EINVAL"); + std::array output{-1, 0x5a5a5a5a}; + Require(sceKernelGetCompiledSdkVersion(output.data()) == 0, "query failed"); + Require(output[0] == 0x07008001, "version is not the low 32 bits of the process parameters' SDK version"); + Require(output[1] == 0x5a5a5a5a, "the query wrote past the version"); + + std::puts("KERNEL SYS SDK VERSION tests passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..3767f130f3 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -3,6 +3,10 @@ ### Silent stubs - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `AT_SYMLINK_NOFOLLOW` on a symbolic link (on Windows and Linux) +### Unknown function info + +- [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a PS5: its `int (int32_t*)` signature and behaviour follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62) (PS4). A null output returns `EINVAL` (`0x80020016`); otherwise it writes the low 32 bits of the 64-bit SDK version at offset `0x10` of the title's process parameters, the field shadPS4 [reads from the eboot](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138), and returns 0. Closes when: measured on a PS5 + ### Functional - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified From c6f86e1cc333c3b57daac86e8dfffb8474dfc32e Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 Date: Wed, 21 Oct 2026 17:42:24 +0200 Subject: [PATCH 0434/1360] fix(relinker): validate relocation types during table extraction Validate relocation types using validationPolicy immediately upon reading rInfo in extractRela, ensuring unsupported relocation types are rejected at their table offset before symbol table index lookups or TLS checks occur. Reject non-JUMP_SLOT relocation types in DT_JMPREL to prevent invalid PLT dispatch and downstream filter/builder errors. --- .../src/pipeline/RelinkerPipeline.cpp | 8 ++-- .../relinker/tests/ConversionTests.cpp | 44 +++++++++++++++++++ .../tests/test_executable_tls_relocations.py | 2 + .../tests/test_jmprel_table_bounds.py | 5 +++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 78ce7cd35a..77044446d2 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -346,9 +346,14 @@ RelinkResult RelinkerPipeline::Relink() { const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); + validationPolicy->ValidateRelocationTypeSupported(relType, pos); + if (relType == R_X86_64_NONE) continue; + if (plt && relType != R_X86_64_JUMP_SLOT) + throw RelinkerException("Unsupported PLT relocation type", pos); + if (relType == 8) { if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); @@ -377,9 +382,6 @@ RelinkResult RelinkerPipeline::Relink() { extractRela(dynRelaOffset, dynRelaSize, false); extractRela(dynJmpRelOffset, dynJmpRelSize, true); - for (const auto& ref : nidRefs) - validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); - std::vector> unwindRanges; std::vector> segmentUnwindRanges(executableSegments.size()); if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 468921cf4f..07c35d6e9a 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -292,6 +292,49 @@ void runtimeDirectories() { } } +void relocationValidation() { + TestProgress progress; + const Relinker::Conversion converter(progress); + { + auto source = input(); + Io::WriteU64(source.Executable, 0x2058, 24); + Io::WriteU64(source.Executable, 0x2700, 0x2000); + Io::WriteU64(source.Executable, 0x2708, (static_cast(0x9999) << 32) | 0x99); + Io::WriteU64(source.Executable, 0x2710, 0); + try { + converter.Convert(source, {}); + throw std::runtime_error("Unsupported relocation type was accepted"); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find("Unsupported relocation type 0x99") != std::string::npos, "Wrong error for unsupported relocation"); + require(error.FailureOffset == 0x2700, "Wrong failure offset for unsupported relocation"); + } + } + { + auto source = input(); + const std::vector> tags{ + {5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, + {3, 0x2000}, {20, 7}, {23, 0x2780}, {2, 24}, {0, 0} + }; + const auto at = 64 + 2 * 56; + Io::WriteU64(source.Executable, at + 32, tags.size() * 16); + Io::WriteU64(source.Executable, at + 40, tags.size() * 16); + for (std::size_t index = 0; index < tags.size(); ++index) { + Io::WriteU64(source.Executable, 0x2000 + index * 16, tags[index].first); + Io::WriteU64(source.Executable, 0x2008 + index * 16, tags[index].second); + } + Io::WriteU64(source.Executable, 0x2780, 0x2000); + Io::WriteU64(source.Executable, 0x2788, (static_cast(1) << 32) | 8); + Io::WriteU64(source.Executable, 0x2790, 0); + try { + converter.Convert(source, {}); + throw std::runtime_error("Unsupported PLT relocation type was accepted"); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()) == "Unsupported PLT relocation type", "Wrong error for unsupported PLT relocation"); + require(error.FailureOffset == 0x2780, "Wrong failure offset for unsupported PLT relocation"); + } + } +} + } int main() { @@ -303,6 +346,7 @@ int main() { cancellation(); baseRelocations(); runtimeDirectories(); + relocationValidation(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..b18e6f1d32 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -135,6 +135,8 @@ def main(): failures = [ ("tpoff32", [(0x1000, TPOFF32, NULL, 0)], True, (), "Unsupported relocation type 0x17"), + ("tpoff32-invalid-sym", [(0x1000, TPOFF32, 0x1234, 0)], True, (), "Unsupported relocation type 0x17"), + ("unknown-reloc-invalid-sym", [(0x1000, 0x99, 0x1234, 0)], True, (), "Unsupported relocation type 0x99"), ("not-tls-symbol", [(0x1000, TPOFF64, DATA_SYMBOL, 0)], True, (), "not STT_TLS"), ("no-tls-segment", [(0x1000, TPOFF64, NULL, 0)], False, (), "without a PT_TLS segment"), ("dtpmod-addend", [(0x1000, DTPMOD64, NULL, 1)], True, (), "DTPMOD64 relocation has a nonzero addend"), diff --git a/core/relinker/relinker/tests/test_jmprel_table_bounds.py b/core/relinker/relinker/tests/test_jmprel_table_bounds.py index 504e1f9109..ed7bb8f41e 100644 --- a/core/relinker/relinker/tests/test_jmprel_table_bounds.py +++ b/core/relinker/relinker/tests/test_jmprel_table_bounds.py @@ -124,6 +124,11 @@ def main(): "Symbol table entry out of bounds", target=output_target) run(relinker, work, prefix + "-none", fixture(symbol_bytes=0, symbol_index=0xFFFFFFFF, relocation_type=0, **options), target=output_target) + else: + run(relinker, work, prefix + "-unsupported-plt-type", fixture(relocation_type=6, **options), + "Unsupported PLT relocation type", target=output_target) + run(relinker, work, prefix + "-relative-in-plt", fixture(relocation_type=8, **options), + "Unsupported PLT relocation type", target=output_target) print("Jump relocation table bounds tests passed") From 4bed8ea1723f61f191faa9130b1eab6b7983af54 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 11:48:16 -0400 Subject: [PATCH 0435/1360] chore: refresh point-size PR synchronization From 8c9444b5ae013359172d55a99887f9a98e6d963d Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:51:44 +0200 Subject: [PATCH 0436/1360] feat(libSceNgs2): process the custom Gain module A custom submixer rack now accepts the Gain module (0x15). Its voice parameter 0x40001500 | module index sets one linear level per channel (37 levels, default 0), and each grain multiplies the voice's samples by a level that ramps linearly from the previous one to the new one, following fma-rc's reading of the PS5 12.70 module (#8039). Gain on a custom sampler rack, whose parameter index is offset, still throws, as do levels outside -32 to 32. Co-Authored-By: Claude Opus 5.5 --- .../libSceNgs2.native/include/Ngs2Types.hpp | 7 +++ .../libs/prx/libSceNgs2.native/src/Custom.cpp | 37 +++++++++++-- .../libSceNgs2.native/src/Ngs2Internal.hpp | 8 ++- core/libs/prx/libSceNgs2.native/src/Rack.cpp | 4 +- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 54 ++++++++++++++++++- docs/dev/debt/libSceNgs2.native.md | 3 +- 6 files changed, 105 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index f10d57afb0..984ea4ab56 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -74,6 +74,7 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0 static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN = 0x40001500; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001d00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; @@ -540,6 +541,12 @@ struct Ngs2CustomVoiceUserFx2Param { }; static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); +struct Ngs2CustomVoiceGainParam { + Ngs2VoiceParamHeader header; + float gain[SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS]; +}; +static_assert(sizeof(Ngs2CustomVoiceGainParam) == 156); + struct Ngs2CustomVoiceFilterParam { Ngs2VoiceParamHeader header; std::uint32_t type; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index f4e682173b..ac34cef5c3 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -45,7 +45,7 @@ static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { return SCE_NGS2_OK; } -int Ngs2CheckCustomRack(const Ngs2CustomRackOption& option, std::uint32_t maxInputs) { +int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs) { if (option.num_buffers == 0 || option.num_buffers > SCE_NGS2_CUSTOM_MAX_BUFFERS || option.num_buffers < maxInputs) { return SCE_NGS2_ERROR_INVALID_NUM_BUFFERS; } @@ -70,7 +70,10 @@ int Ngs2CheckCustomRack(const Ngs2CustomRackOption& option, std::uint32_t maxInp } if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); for (std::uint32_t m = 0; m < option.num_modules; m++) { - const char* name = BuiltInModuleName(option.module[m].module_id); + const auto moduleId = option.module[m].module_id; + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue; + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented"); + const char* name = BuiltInModuleName(moduleId); if (name != nullptr) throw std::runtime_error(std::string("NGS2: the custom ") + name + " module is not implemented"); } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { @@ -92,6 +95,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { for (auto& voice : rack.voices) { voice.userFx.resize(option.num_modules); voice.customUserFx.resize(option.num_modules); + voice.customGain.assign(option.num_modules, {}); } for (std::uint32_t m = 0; m < option.num_modules; m++) { rack.customModuleIds[m] = option.module[m].module_id; @@ -159,6 +163,26 @@ static void SetCustomUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ng fx.flags = 1; } +static void SetGainParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceGainParam& param) { + if (index >= voice.customGain.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) APS5_INVALID_ARG_EX; + for (const float gain : param.gain) { + if (!(gain >= -32.0f && gain <= 32.0f)) throw std::runtime_error("NGS2: a custom Gain level outside -32 to 32 is not implemented"); + } + std::copy(std::begin(param.gain), std::end(param.gain), voice.customGain[index].target.begin()); +} + +static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std::vector& channels, std::uint32_t grain) { + if (voice.hasSamples) { + for (std::uint32_t c = 0; c < voice.channels; c++) { + const float from = gain.current[c]; + const float step = (gain.target[c] - from) / static_cast(grain); + for (std::uint32_t i = 0; i < grain; i++) channels[c][i] *= from + step * static_cast(i + 1); + } + voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + } + gain.current = gain.target; +} + void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { const auto rackId = voice.rack->rackId; if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { @@ -191,6 +215,11 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { Ngs2SetFilter(voice, filter); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + SetGainParam(voice, param.id & 0x1f, ParamAs(param)); + return; + } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); @@ -212,7 +241,9 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp 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.rack->customModuleIds.size(); m++) { - if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { + ProcessGain(voice, voice.customGain[m], channels, grain); + } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { if (!voice.hasSamples) continue; auto& fx = voice.userFx[m]; const auto& module = voice.rack->userFx[m]; diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 72fa69c3ce..a0e886d230 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -108,6 +108,11 @@ struct Ngs2VoiceCustomUserFx { std::uint32_t flags = 0; }; +struct Ngs2VoiceCustomGain { + std::array current{}; + std::array target{}; +}; + struct Ngs2Voice { Ngs2Rack* rack = nullptr; Ngs2PlayState state = Ngs2PlayState::Empty; @@ -136,6 +141,7 @@ struct Ngs2Voice { float lfeLevel = 1.0f; std::vector userFx; std::vector customUserFx; + std::vector customGain; Ngs2UserFxProcessHandler userFxHandler = nullptr; std::array userFxData{}; std::uint32_t userFxFlags = 0; @@ -229,7 +235,7 @@ std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); bool Ngs2FinishBlock(Ngs2Voice& voice); -int Ngs2CheckCustomRack(const Ngs2CustomRackOption& option, std::uint32_t maxInputs); +int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); void Ngs2CleanupUserFx(Ngs2Rack& rack); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 93072c82fb..f5c85495eb 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -103,8 +103,8 @@ static int CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* option, } if (options.common.max_voices == 0 || maxChannels == 0) throw std::invalid_argument("NGS2: invalid rack voice or channel count"); int result = SCE_NGS2_OK; - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) result = Ngs2CheckCustomRack(options.customSampler.custom_rack_option, 0); - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) result = Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option, options.customSubmixer.max_inputs); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) result = Ngs2CheckCustomRack(rackId, options.customSampler.custom_rack_option, 0); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) result = Ngs2CheckCustomRack(rackId, options.customSubmixer.custom_rack_option, options.customSubmixer.max_inputs); if (result != SCE_NGS2_OK) return result; if (maxChannels > NGS2_MAX_CHANNELS) { if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index bdb48ba82a..648c1de62f 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -434,7 +434,7 @@ static void TestCustomRackChecks() { option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS; Require(CreateThrows(option)); - Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); + Require(!CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); const ModuleOptionWords reverb{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {1}}; @@ -496,6 +496,8 @@ static void TestCustomRackChecks() { Require(rack == 0); sampler.custom_rack_option.module[0].extra_buffer_id = 0; Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + Require(CreateSamplerThrows(sampler)); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -610,6 +612,55 @@ static void TestCustomLegacyUserFx() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestCustomGain() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto userFx2 = Module(Gain); + const auto rack = CustomRack(system, BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0)); + 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.0f && out[i * 2 + 1] == 0.0f); + + Ngs2CustomVoiceGainParam gain{}; + gain.gain[0] = 2.0f; + gain.gain[1] = -0.5f; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(out[i * 2] == 0.5f * 2.0f * static_cast(i + 1) / Grain); + Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast(i + 1) / Grain); + } + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 1.0f && out[i * 2 + 1] == -0.25f); + + gain.gain[0] = 1.0f; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast(i + 1) / Grain)); + + auto outside = gain; + outside.gain[2] = 33.0f; + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside)); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, gain)); + Require(ControlThrows(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain)); + struct ShortGainParam { + Ngs2VoiceParamHeader header; + float gain[2]; + }; + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, ShortGainParam{})); + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestUserFxChain(); TestRackOptions(); @@ -617,5 +668,6 @@ int main() { TestCustomSampler(); TestCustomFilter(); TestCustomLegacyUserFx(); + TestCustomGain(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index a9a718b5ee..a6f68fb8a1 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -20,7 +20,8 @@ - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input -- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Gain (0x15), Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed, nor Gain (0x15) on a custom sampler rack: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) +- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. The extra buffer is ignored, as fma-rc reads it - [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. From 059d3f2a1620f50a1e2a090cdd1688eda78ca6d8 Mon Sep 17 00:00:00 2001 From: zmeizy Date: Wed, 14 Oct 2026 21:03:03 +0300 Subject: [PATCH 0437/1360] fix(shader): round v_cvt_pk_u8_f32 in the FLOAT_MODE f32 round mode v_cvt_pk_u8_f32 rounds its source in the f32 round mode of FLOAT_MODE and flushes a denormal source when f32 denormals are flushed, as measured on RDNA2 (gfx1030). It was always rounded to nearest even, so the three directed modes gave wrong bytes for non-integer sources. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/VCvtPkU8F32RoundMode.cpp | 141 ++++++++++++++++++ .../Translation/src/ConvertInstructions.cpp | 13 +- docs/dev/debt/shader.md | 1 + 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp new file mode 100644 index 0000000000..777bd3e290 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd55e000au, 0x02010104u, 0xe0701000u, + 0x80010a02u, 0xbf810000u, +}; + +struct PackVector { + std::uint32_t value; + std::uint32_t denormalsFlushed[4]; + std::uint32_t denormalsKept[4]; +}; + +constexpr std::array Vectors{{ + {0x00000000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3e000000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x3e800000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x3f000000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x3f400000u, {0x01u, 0x01u, 0x00u, 0x00u}, {0x01u, 0x01u, 0x00u, 0x00u}}, + {0x3f800000u, {0x01u, 0x01u, 0x01u, 0x01u}, {0x01u, 0x01u, 0x01u, 0x01u}}, + {0x3fa00000u, {0x01u, 0x02u, 0x01u, 0x01u}, {0x01u, 0x02u, 0x01u, 0x01u}}, + {0x3fc00000u, {0x02u, 0x02u, 0x01u, 0x01u}, {0x02u, 0x02u, 0x01u, 0x01u}}, + {0x40200000u, {0x02u, 0x03u, 0x02u, 0x02u}, {0x02u, 0x03u, 0x02u, 0x02u}}, + {0x40600000u, {0x04u, 0x04u, 0x03u, 0x03u}, {0x04u, 0x04u, 0x03u, 0x03u}}, + {0x40700000u, {0x04u, 0x04u, 0x03u, 0x03u}, {0x04u, 0x04u, 0x03u, 0x03u}}, + {0x42ff0000u, {0x80u, 0x80u, 0x7fu, 0x7fu}, {0x80u, 0x80u, 0x7fu, 0x7fu}}, + {0x43008000u, {0x80u, 0x81u, 0x80u, 0x80u}, {0x80u, 0x81u, 0x80u, 0x80u}}, + {0x43482000u, {0xc8u, 0xc9u, 0xc8u, 0xc8u}, {0xc8u, 0xc9u, 0xc8u, 0xc8u}}, + {0x437e8000u, {0xfeu, 0xffu, 0xfeu, 0xfeu}, {0xfeu, 0xffu, 0xfeu, 0xfeu}}, + {0x437ea000u, {0xffu, 0xffu, 0xfeu, 0xfeu}, {0xffu, 0xffu, 0xfeu, 0xfeu}}, + {0x437ee000u, {0xffu, 0xffu, 0xfeu, 0xfeu}, {0xffu, 0xffu, 0xfeu, 0xfeu}}, + {0x437f0000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437f4000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437f7fffu, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437f8000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437fe000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x43800000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x501502f9u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xbe000000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xbf000000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xbeffffffu, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xbf800000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xd01502f9u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00000001u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x80000001u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x007fffffu, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x00800000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x7f800000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xff800000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7fc00000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xffc00000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7f800001u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) Input[lane * Inputs] = Vectors[lane].value; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(bool kept, std::uint32_t rounding, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + const auto& vector = Vectors[lane]; + const auto expected = kept ? vector.denormalsKept[rounding] : vector.denormalsFlushed[rounding]; + Require(Output[lane] == expected, std::string("v_cvt_pk_u8_f32 round mode ") + std::to_string(rounding) + (kept ? ", f32 denormals kept" : ", f32 denormals flushed") + + (ieee ? ", IEEE=1" : ", IEEE=0") + " of " + Hex(vector.value) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool kept : {false, true}) { + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + for (const bool ieee : {false, true}) { + Run(*device, ShaderRecompiler::ShaderFloatMode{(kept ? 0xf0u : 0xc0u) | rounding, true, ieee, false}); + Check(kept, rounding, ieee); + } + } + } + std::puts("v_cvt_pk_u8_f32 round mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index d14b698c77..4dfdf3ed3b 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -215,15 +215,22 @@ void TranslationContext::vCvtPknormF32(const RdnaInstruction& inst, bool signedV } void TranslationContext::vCvtPkU8F32(const RdnaInstruction& inst) { - const IrF32 source(ir.BitCastF32(*readOperand(sourceAt(inst, 0u), IrType::U32))); + const IrU32 bits = flushF32Denormal(IrU32(*readOperand(sourceAt(inst, 0u), IrType::U32))); + const IrF32 source(ir.BitCastF32(bits.Value())); const IrF32 zero(ir.ConstantF32(0.0f)); const IrU1 nan(ir.Emit(IrOpcode::FPIsNan32, IrType::U1, {&source.Value()})); const IrU1 low(ir.Emit(IrOpcode::FPOrdLessThanEqual32, IrType::U1, {&source.Value(), &zero.Value()})); const IrU1 high(ir.Emit(IrOpcode::FPOrdGreaterThanEqual32, IrType::U1, {&source.Value(), &ir.ConstantF32(255.0f)})); - const IrF32 rounded(ir.Emit(IrOpcode::FPRoundEven32, IrType::F32, {&source.Value()})); + const std::uint32_t roundMode = floatMode.has_value() ? floatMode->floatMode & 3u : 0u; + const IrOpcode rounding = roundMode == 1u ? IrOpcode::FPCeil32 : roundMode == 2u ? IrOpcode::FPFloor32 : roundMode == 3u ? IrOpcode::FPTrunc32 : IrOpcode::FPRoundEven32; + const IrF32 rounded(ir.Emit(rounding, IrType::F32, {&source.Value()})); const IrF32 bounded = selectF32(high, IrF32(ir.ConstantF32(255.0f)), rounded); const IrF32 safe = selectF32(IrU1(ir.LogicalOr(nan.Value(), low.Value())), zero, bounded); - const IrU32 byteValue(ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&safe.Value()})); + IrU32 byteValue(ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&safe.Value()})); + if (roundMode == 1u && (f32DenormalFlush & 1u) == 0u) { + const IrU1 positiveDenormal(ir.ULessThan(ir.ISub(bits.Value(), ir.Constant(1u)), ir.Constant(0x007fffffu))); + byteValue = IrU32(ir.Select(positiveDenormal.Value(), ir.Constant(1u), byteValue.Value())); + } const IrU32 index(ir.BitwiseAnd(readU32(sourceAt(inst, 1u)).Value(), ir.Constant(3u))); const IrU32 shift(ir.ShiftLeftLogical(index.Value(), ir.Constant(3u))); const IrU32 mask(ir.ShiftLeftLogical(ir.Constant(0xffu), shift.Value())); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d76932d773..5735d758d3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -76,6 +76,7 @@ - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - [Flat scalar-read reachability](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) keeps both successors active for non-clean predicates and loop headers, so an inaccessible direct root on such a path still refuses capture. Skipping a read under a clean, evaluable scalar branch follows RDNA2 conditional PC semantics and synthetic fixtures; console execution and title behavior are unverified. +- [v_cvt_pk_u8_f32](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) throws in f32 denormal modes 1 and 2 and with a directed round mode set at run time (see the previous entry); the PS5's gfx1013 is not measured. Otherwise it rounds its source in the `FLOAT_MODE` f32 round mode (ties to even, toward +infinity, -infinity or zero), clamps it to 0..255 (a NaN gives 0) and flushes a denormal source when f32 denormals are flushed. A kept positive denormal gives 1 toward +infinity, decided on the bits, as a driver without `DenormPreserve 32` may flush it. Measured on RDNA2 (gfx1030, RX 6800, AMD Windows driver 32.0.21045.5002) over 2,133 sources (k/8 for k = -16 to 2,099, values next to 0.5 and 255.5, ±1e10, denormals, infinities, NaNs) at IEEE 0 and 1, f32 denormal modes 0 and 3 and f32 round modes 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even, FP16_OVFL=0. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. From 4bd46aa9efd6bbe3c594d2e14fc99aeb5d468c0b Mon Sep 17 00:00:00 2001 From: Nixitov Date: Tue, 20 Oct 2026 00:09:11 +0200 Subject: [PATCH 0438/1360] feat(libSceVideodec2): declare sceVideodec2InitLibrary AI-assisted: yes --- core/libs/prx/libSceVideodec2/Unimplemented.cpp | 3 +++ docs/dev/debt/libSceVideodec2.md | 1 + 2 files changed, 4 insertions(+) diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp index 1cad37fd28..b427d652ac 100644 --- a/core/libs/prx/libSceVideodec2/Unimplemented.cpp +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -55,4 +55,7 @@ int APS5_VABI sceVideodec2GetHevcPictureInfo() { NotImplemented_nid_no_patch(__func__); } +int APS5_VABI sceVideodec2InitLibrary() { + NotImplemented_nid_no_patch(__func__); +} } diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 3706171e00..1745a0ae5e 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceVideodec2InitLibrary](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) throws: the FFmpeg decoders this module creates need no library-level initialisation, and neither SharpProspero nor EVO-PLAYER-PS5 declares this call, so its arguments and result are unknown. A title imports it from this module - [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) - [sceVideodec2TermLibrary and twelve other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder`, `QueryHevcDecoderMemoryInfo` and `GetHevcPictureInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here From 63d39623e351bb536c525bd01ca2327e1e046fae Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 16:09:50 +0000 Subject: [PATCH 0439/1360] fix(build): run guest_unused_import_dependencies without the ASan link order check The test loads guest_module_args_host into a relinked guest, so in an ANYPS5_ENABLE_ASAN build the ASan runtime is not first in the initial library list and the guest stops before it runs. The other relinker tests that load that host already take verify_asan_link_order=0 through add_asan_test_options; this one was added after them. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa78fbaa42..6d0a7af013 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -622,6 +622,7 @@ if(BUILD_TESTING) add_asan_test_options(guest_module_args verify_asan_link_order=0) add_asan_test_options(guest_deferred_dependencies verify_asan_link_order=0) add_asan_test_options(guest_module_fini verify_asan_link_order=0) + add_asan_test_options(guest_unused_import_dependencies verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) add_test(NAME guest_zero_initializer COMMAND ${Python3_EXECUTABLE} From ba09143027adec1b494493224946e2822340d379 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 12:10:51 -0400 Subject: [PATCH 0440/1360] fix(tools): list controller buttons in chord input help --- tools/pad_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pad_input.py b/tools/pad_input.py index 465957651c..69b1d64579 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -65,7 +65,7 @@ def button_mask(value): def main(): parser = argparse.ArgumentParser(description="Queue a controller pulse for APS5_PAD_INPUT_FILE.") parser.add_argument("path", type=Path) - parser.add_argument("button", help="a button name, or several joined by '+' to press them together (for example l1+r1)") + parser.add_argument("button", help=f"a button name ({', '.join(BUTTONS)}), or several joined by '+' to press them together (for example l1+r1); release must be used alone") parser.add_argument("--hold-ms", type=int, default=300) parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) From 6f3e4a147faaa3754467475d5cb9e18448222593 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:00:20 +0300 Subject: [PATCH 0441/1360] fix(relinker): read guest TLS relocations against the module's own section symbol Limbo's bundled libSceFios2.prx (CUSA01664) has DTPMOD64 relocations against a local section symbol with section index 0 and value 0, in a module without section headers. As for a TLS relocation without a symbol, the module and offset are the module's own TLS block, so a DTPMOD64 or DTPOFF64 against such a symbol is passed on without it. A section symbol that names a section or has a value, a TPOFF64, a module without PT_TLS and a DTPMOD64 with an addend still fail. --- core/relinker/CMakeLists.txt | 3 + .../relinker/src/guest/GuestImageReader.cpp | 6 +- .../tests/test_guest_own_tls_relocations.py | 78 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/test_guest_own_tls_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa78fbaa42..31560112a0 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -475,6 +475,9 @@ if(BUILD_TESTING) add_test(NAME overlapping_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_overlapping_relocations.py $) set_tests_properties(overlapping_relocations PROPERTIES TIMEOUT 30) + add_test(NAME guest_own_tls_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_own_tls_relocations.py $) + set_tests_properties(guest_own_tls_relocations PROPERTIES TIMEOUT 30) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME optional_rela COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index a630010692..49a209c9d4 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -244,6 +244,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); relocationTargets.emplace(target, target + 8); + bool ownTlsBlock = false; if (relocation == 8) { if ((info >> 32) != 0) { const auto& symbol = image.Symbols[info >> 32]; @@ -263,9 +264,12 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if ((info >> 32) == 0 || (symbol.Info & 15) == 6 || (relocation != 1 && addend != 0) || (symbol.Section == 0 && symbol.Name.empty())) fail("Invalid guest symbol relocation"); } else { const auto& symbol = image.Symbols[info >> 32]; - if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); + ownTlsBlock = (info >> 32) != 0 && (relocation == 16 || relocation == 17) && symbol.Info == 3 && symbol.Section == 0 && symbol.Value == 0 && tls != nullptr; + if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6 && !ownTlsBlock) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + const auto position = output.size(); output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); + if (ownTlsBlock) Io::WriteU64(output, position + 8, relocation); } }; copyRelocations(relaOffset, relaSize, image.Dynamic.RelaData, false); diff --git a/core/relinker/relinker/tests/test_guest_own_tls_relocations.py b/core/relinker/relinker/tests/test_guest_own_tls_relocations.py new file mode 100644 index 0000000000..add91dcf70 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_own_tls_relocations.py @@ -0,0 +1,78 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_none_relocations import TARGET, change_tag, module +from test_guest_symbol_names import dynamic_tags, file_offset + + +DTPMOD64 = 16 +DTPOFF64 = 17 +TPOFF64 = 18 +TEMPLATE = 0x2780 + + +def fixture(sce=False, tls=True, info=3, value=0, kind=DTPMOD64, addend=0, section=0): + image = module([(TARGET, (3 << 32) | kind, addend)], sce=sce) + struct.pack_into(" Date: Wed, 21 Oct 2026 18:31:33 +0200 Subject: [PATCH 0442/1360] fix(shader): accept 3D image stores on 2D-array T#s A 3D image_store bound to a 2D-array T# addresses the layer as z. Add a 2D-array runtime mode for write-only 3D storage images and drop stores whose layer is past the array. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 19 ++ .../ImageStoreVolumeThroughArray.cpp | 173 ++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 6 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 8 +- 4 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a754f42271..91ae729214 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7553,6 +7553,24 @@ void verifyDynamicStorageMipChain() { require(ResourceMaterializer::TryRuntimeImageMode(image, allLevels, modes).mode.has_value(), "a dynamic-mip storage image with sixteen levels was rejected"); } +void verifyArrayVolumeStoreMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x11800100u, 0xc3800000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u}; + ImageResource store{}; + store.resourceClass = ImageResourceClass::Storage; + store.dimension = RdnaImageDimension::Dim3D; + store.written = true; + store.fmaskCompatible = false; + const auto modes = ResourceMaterializer::RuntimeImageModes(store); + require(modes[ResourceMaterializer::RuntimeImageMode(store, descriptor, modes)].dimension == RdnaImageDimension::Dim2DArray, "3D image store on a 2D-array T# did not select an array runtime mode"); + store.read = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image load and store on a 2D-array T# was accepted"); + store.read = false; + store.atomic = true; + const DescriptorValue uintDescriptor{{0x11800100u, 0xc1400000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u}; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, uintDescriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image atomic on a 2D-array T# was accepted"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -7725,6 +7743,7 @@ int main(int argc, char** argv) { verifyFmaskImageMode(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyArrayVolumeStoreMode(); verifyVolumeBiasTableRejection(); verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp new file mode 100644 index 0000000000..b82fa4ba07 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp @@ -0,0 +1,173 @@ +#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 Size = 4; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 16384; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201110, 0x00011714, 0x7e2c0282, 0xf0201110, + 0x00011714, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store volume through array: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid) { + return (tid + 1u) * 0x9e3779b1u; +} + +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::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array ArrayDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (Type2DArray << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)}; + std::copy(words.begin(), words.end(), Input.begin() + tid * 4u); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = ArrayDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + 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), bytes, nullptr, "test"); + device.WaitIdle(); +} + +void Check(const std::uint8_t* texels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels))); + const auto& mip = geometry.mips.at(0); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + const auto z = tid / (Size * Size); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u, 4u); + Require(actual == Value(tid), "image_store 3d through a 2D-array T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid))); + } + const auto initial = Initial(); + Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast(geometry.guestBytes)), "image_store 3d through a 2D-array T#: a slice past the array changed bytes after it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels))); + Require(geometry.guestBytes <= TexelBytes, "image store volume through array: the array does not fit the texel storage"); + Dispatch(*device, texels, geometry.guestBytes); + Check(texels); + std::puts("image store volume through array 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 2ccd9a4039..f6d2900df7 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1863,6 +1863,12 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); } + const bool arrayStore = image.dimension == RdnaImageDimension::Dim3D && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; + if (arrayStore && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + auto array = mode; + array.dimension = RdnaImageDimension::Dim2DArray; + modes.push_back(array); + } if (image.dimension == RdnaImageDimension::Dim2DMsaa && image.fmaskCompatible && image.cubeFaceCompatible && !table && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto plane = mode; plane.dimension = RdnaImageDimension::Dim2D; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 772f4f8f4c..de08a9338f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -246,6 +246,10 @@ bool VolumeSliceAddress(const ImageEmitAccess& access) { return access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim3D; } +bool ArraySliceAddress(const ImageEmitAccess& access) { + return access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2DArray; +} + std::uint32_t VolumeSliceInRange(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t image, std::optional lod) { auto& state = ctx.state; state.module.EmitCapability(spv::CapabilityImageQuery); @@ -1073,8 +1077,8 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; auto condition = ctx.Arg(access.inst, 3); - if (VolumeSliceAddress(access)) { - if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image with dynamic mips, which is not implemented"); + if (VolumeSliceAddress(access) || ArraySliceAddress(access)) { + if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image or a 3D address to a 2D-array image with dynamic mips, which is not implemented"); const auto image = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, ImageType(state, access.image), image, StorageImageDescriptorPointer(state, access.mem.resource)); condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, image, std::nullopt)); From 1f87b51ea5ad473e5a0b51b30f58d5ae3736f205 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 16:32:20 +0000 Subject: [PATCH 0443/1360] fix(agc): value-initialize a pipeline journal entry before decoding it parse() declared each LoadedEntry without an initializer. Several State members (hasColorTarget, topology, viewport, negativeOneToOne, cullMode, frontFace, blend) have no default member initializer, so the graphics recipe of a compute record, which is never decoded, held indeterminate values. Moving the entry into the loaded list then read them: UBSan reports "load of value 6, which is not a valid value for type 'bool'" in State's move constructor in agc_driver_pipeline_journal. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index d0669ed238..e18e5bb667 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -621,7 +621,7 @@ private: if (decoded) loaded.indexModules.insert(identity); } else if (record.kind == GraphicsRecord || record.kind == ComputeRecord) { if (decode && !loaded.indexRecords.contains(record.payloadHash)) { - LoadedEntry entry; + LoadedEntry entry{}; entry.kind = record.kind; bool decoded = false; if (record.kind == GraphicsRecord) { From 1e01841072badcde54e9a61ca476f27279bade90 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 21:04:11 +0300 Subject: [PATCH 0444/1360] fix(tools): recognize markdown links and pull request URLs in pr_overlap dependencies --- tools/pr_overlap.py | 3 +++ tools/tests/test_pr_overlap_dependencies.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index ca1bc134e5..923b1bcb54 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -41,6 +41,9 @@ def upstream(): def depends_on(body): body = re.sub(r"", "", body or "", flags=re.S) + body = re.sub(r"\[([^\]]*)\]\(https?://github\.com/[^/\s]+/[^/\s]+/pull/(\d+)\)", r"#\2", body) + body = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", body) + body = re.sub(r"https?://github\.com/[^/\s]+/[^/\s]+/pull/(\d+)\b", r"#\1", body) match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index 11754c81b7..f66a443af5 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -47,6 +47,19 @@ class DependencyTests(unittest.TestCase): with self.subTest(body=body): self.assertEqual(pull_requests(body)[1]["depends"], {2, 3}) + def test_markdown_links_and_full_urls(self): + for body in ( + "- [x] Depends on: [#2](https://github.com/owner/repo/pull/2)", + "- [x] Depends on: https://github.com/owner/repo/pull/2", + "- [x] Depends on: [PR #2](https://github.com/owner/repo/pull/2)", + "Depends on: [#2](https://github.com/owner/repo/pull/2), [#3](https://github.com/owner/repo/pull/3)", + "Depends on:\n- [#2](https://github.com/owner/repo/pull/2)\n- [#3](https://github.com/owner/repo/pull/3)", + "- [x] Depends on: [none](https://example.com)", + ): + with self.subTest(body=body): + expected = set() if "none" in body else ({2, 3} if "3" in body else {2}) + self.assertEqual(pull_requests(body)[1]["depends"], expected) + def test_later_references_are_not_dependencies(self): for newline in ("\n", "\r\n"): for declaration in ("Depends on #2.", "Depends on: #2"): From 90e0781dccfe010a1bf5e0271771338cc00ef381 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 16:35:13 +0000 Subject: [PATCH 0445/1360] test(agc): return a zero cancelling coordinate before taking its exponent The interpolation modes test rounds a generated value to half precision with std::ilogb(value) - 10. For a zero value std::ilogb returns FP_ILOGB0 (INT_MIN on Linux), so the subtraction overflows: UBSan reports a signed integer overflow in agc_driver_interpolation_modes. A zero is already a half value, so it is returned as is, as the wrapped result gave. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp index 9c332e8aaa..913426408a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp @@ -262,6 +262,7 @@ struct Generator { if (half) return bits; const auto value = std::bit_cast(bits); if (!std::isfinite(value) || std::fabs(value) > 65504.0f) return Half(); + if (value == 0.0f) return bits; const auto exponent = std::max(std::ilogb(value) - 10, -24); return std::bit_cast(static_cast(std::ldexp(std::nearbyint(std::ldexp(value, -exponent)), exponent))); } From 486eb18dc115a7bf3b64fc0937963de34a5a1b36 Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:36:15 +0300 Subject: [PATCH 0446/1360] fix(tools): restrict pull request URL matching to current repository in pr_overlap --- tools/pr_overlap.py | 8 +++++--- tools/tests/test_pr_overlap_dependencies.py | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 923b1bcb54..6699df9ebf 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -41,13 +41,15 @@ def upstream(): def depends_on(body): body = re.sub(r"", "", body or "", flags=re.S) - body = re.sub(r"\[([^\]]*)\]\(https?://github\.com/[^/\s]+/[^/\s]+/pull/(\d+)\)", r"#\2", body) + repo = re.escape(globals().get("REPO", "boykopovar/AnyPS5")) + repo_pattern = rf"(?:{repo}|boykopovar/AnyPS5)" + body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/{repo_pattern}/pull/(\d+)(?:[#/?][^)\s]*)?\)", r"#\2", body, flags=re.IGNORECASE) + body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/(?!{repo_pattern}/)[^/\s]+/[^/\s]+/pull/\d+[^)\s]*\)", "", body, flags=re.IGNORECASE) body = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", body) - body = re.sub(r"https?://github\.com/[^/\s]+/[^/\s]+/pull/(\d+)\b", r"#\1", body) + body = re.sub(rf"https?://github\.com/{repo_pattern}/pull/(\d+)\b(?:/[^\s]*)?", r"#\1", body, flags=re.IGNORECASE) match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() - def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index f66a443af5..9af379f868 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -52,12 +52,17 @@ class DependencyTests(unittest.TestCase): "- [x] Depends on: [#2](https://github.com/owner/repo/pull/2)", "- [x] Depends on: https://github.com/owner/repo/pull/2", "- [x] Depends on: [PR #2](https://github.com/owner/repo/pull/2)", + "- [x] Depends on: https://github.com/owner/repo/pull/2/files", + "- [x] Depends on: https://github.com/owner/repo/pull/2#issuecomment-1", + "- [x] Depends on: https://github.com/BOYKOPOVAR/AnyPS5/pull/2", "Depends on: [#2](https://github.com/owner/repo/pull/2), [#3](https://github.com/owner/repo/pull/3)", "Depends on:\n- [#2](https://github.com/owner/repo/pull/2)\n- [#3](https://github.com/owner/repo/pull/3)", "- [x] Depends on: [none](https://example.com)", + "- [x] Depends on: https://github.com/shadps4-emu/shadPS4/pull/1234", + "- [x] Depends on: [#1234](https://github.com/shadps4-emu/shadPS4/pull/1234)", ): with self.subTest(body=body): - expected = set() if "none" in body else ({2, 3} if "3" in body else {2}) + expected = set() if "none" in body or "shadps4" in body else ({2, 3} if "3" in body else {2}) self.assertEqual(pull_requests(body)[1]["depends"], expected) def test_later_references_are_not_dependencies(self): From c2c9aa75b84f0cfe2c30f1857dcc2672399cc0a4 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 20:32:40 +0300 Subject: [PATCH 0447/1360] fix(tests): guard main and provide defaults in shader cache sources test --- tools/tests/test_shader_cache_runtime_sources.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index 0c462bb7ea..539064b3b5 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -11,8 +11,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import select_tests -SOURCE = Path(sys.argv.pop(1)) -CMAKE = sys.argv.pop(1) +if len(sys.argv) >= 3 and Path(sys.argv[1]).is_dir(): + SOURCE = Path(sys.argv.pop(1)) + CMAKE = sys.argv.pop(1) +else: + SOURCE = Path(__file__).resolve().parents[2] / 'core/shader/recompiler' + CMAKE = shutil.which('cmake') or 'cmake' class Fixture: @@ -106,6 +110,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource return {Path(line.split(' ', 1)[1]) for line in self.listing.read_text(encoding='utf-8').splitlines() if line.startswith('sources ')} +@unittest.skipUnless(shutil.which('ninja') and shutil.which(CMAKE) and SOURCE.is_dir(), 'ninja, cmake, and shader recompiler source required') class ShaderCacheRuntimeSourceTests(unittest.TestCase): def fixture(self, spirv_tools=False): temporary = tempfile.TemporaryDirectory(prefix='shader_cache_sources_') @@ -213,4 +218,5 @@ class ShaderCacheRuntimeSourceTests(unittest.TestCase): self.assertIn('has not been reviewed', fixture.version(False)) -unittest.main() +if __name__ == '__main__': + unittest.main() From ea51b1191f93496b15608d59d919cd58a391cc8b Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:37:31 +0300 Subject: [PATCH 0448/1360] fix(tests): limit tool availability skip to argument-free discovery in shader cache test --- tools/tests/test_shader_cache_runtime_sources.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index 539064b3b5..d4d43946e7 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -11,7 +11,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import select_tests -if len(sys.argv) >= 3 and Path(sys.argv[1]).is_dir(): +EXPLICIT_ARGS = len(sys.argv) >= 3 and Path(sys.argv[1]).is_dir() +if EXPLICIT_ARGS: SOURCE = Path(sys.argv.pop(1)) CMAKE = sys.argv.pop(1) else: @@ -110,7 +111,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource return {Path(line.split(' ', 1)[1]) for line in self.listing.read_text(encoding='utf-8').splitlines() if line.startswith('sources ')} -@unittest.skipUnless(shutil.which('ninja') and shutil.which(CMAKE) and SOURCE.is_dir(), 'ninja, cmake, and shader recompiler source required') +@unittest.skipUnless(EXPLICIT_ARGS or (shutil.which('ninja') and shutil.which(CMAKE) and SOURCE.is_dir()), 'ninja, cmake, and shader recompiler source required') class ShaderCacheRuntimeSourceTests(unittest.TestCase): def fixture(self, spirv_tools=False): temporary = tempfile.TemporaryDirectory(prefix='shader_cache_sources_') From b85e8f9e1576066e889611369981a0c6fc34e19f Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:48:31 +0300 Subject: [PATCH 0449/1360] fix(tools): read CMakeCache, graph and changed list with utf-8-sig in select_tests --- tools/select_tests.py | 17 +++++++++------- tools/tests/test_select_tests.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index efb972d0a1..a69c482e43 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -16,7 +16,7 @@ BATCH = 20000 class Build: def __init__(self, path): cache = {} - for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): name, _, value = line.partition("=") cache[name.partition(":")[0]] = value self.path = os.path.normpath(cache["CMAKE_CACHEFILE_DIR"]) @@ -225,7 +225,10 @@ def escape(name): return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ") -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") sub = parser.add_subparsers(dest="command", required=True) d = sub.add_parser("dump", help="write the test graph of a built tree") @@ -241,15 +244,15 @@ def main(): c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") - args = parser.parse_args() + args = parser.parse_args(argv) if args.command == "ctest": selected = Path(args.build, "select.tests") - extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] + extra = ["-I", selected.read_text(encoding="utf-8-sig").strip()] if selected.exists() else [] return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode if args.command == "explain": try: - graph = json.loads(args.graph.read_text(encoding="utf-8")) - changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").splitlines() if line.strip()] + graph = json.loads(args.graph.read_text(encoding="utf-8-sig")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] reasons = {} picked, full = select(graph, changed, reasons) except (OSError, ValueError) as error: @@ -270,7 +273,7 @@ def main(): if not os.path.exists(args.graph): print("full run: no test graph") return 0 - graph = json.loads(Path(args.graph).read_text(encoding="utf-8")) + graph = json.loads(Path(args.graph).read_text(encoding="utf-8-sig")) changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] picked, full = select(graph, changed) if picked is None: diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index e86b16ccad..7dbeb88bee 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -230,6 +230,40 @@ class BuildErrorTests(unittest.TestCase): self.assertIn("CMAKE_CACHEFILE_DIR", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_CACHEFILE_DIR:INTERNAL=C:/work/build\nCMAKE_HOME_DIRECTORY:INTERNAL=C:/work/source\n", encoding="utf-8") + with mock.patch.object(select_tests.Build, "run", return_value="Build file encoding: UTF-8"): + build = select_tests.Build(directory) + self.assertEqual(build.path, os.path.normpath("C:/work/build")) + self.assertEqual(build.source, os.path.normpath("C:/work/source")) + + def test_explain_accepts_utf8_bom_in_graph_and_changed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + graph_path = root / "graph.json" + graph = { + "tests": { + "unit": { + "target": "tests/unit", + "arguments": [], + "registered": [], + "sets": [], + "requires": [], + "depends": [], + } + }, + "closures": {"tests/unit": ["src/main.c"]}, + } + graph_path.write_text("\ufeff" + json.dumps(graph), encoding="utf-8") + changed_path = root / "changed.txt" + changed_path.write_text("\ufeffsrc/main.c\n", encoding="utf-8") + result = self.run_tool("explain", graph_path, changed_path) + self.assertEqual(result.returncode, 0, result.stderr) + data = json.loads(result.stdout) + self.assertEqual(data["tests"], ["unit"]) + class CaseInsensitivePathTests(unittest.TestCase): def build(self): From b7c5dd03fd0ce14f74aeb8c560fa9705c81e560f Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:53:43 +0300 Subject: [PATCH 0450/1360] fix(tools): handle unwritable seed output and reconfigure streams in configure_seed --- tools/configure_seed.py | 16 +++++++++++----- tools/tests/test_configure_seed.py | 8 ++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index d1936ae21a..8e042f9722 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -25,7 +25,10 @@ def quote(value): return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + '"' -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") parser.add_argument("cache", help="CMakeCache.txt of a configured tree") parser.add_argument("seed", help="file to pass to cmake -C") @@ -33,7 +36,7 @@ def main(): help="seed matching check names only; repeat to include several patterns") parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", help="omit matching check names after inclusion; patterns are case-sensitive") - args = parser.parse_args() + args = parser.parse_args(argv) cache, seed = Path(args.cache), Path(args.seed) if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): parser.error("seed output would overwrite the input cache") @@ -44,9 +47,12 @@ def main(): found = {name: entry for name, entry in results.items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} - seed.parent.mkdir(parents=True, exist_ok=True) - seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" - for name, (value, doc) in sorted(found.items())), encoding="utf-8") + try: + seed.parent.mkdir(parents=True, exist_ok=True) + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") + except OSError as error: + parser.error(f"{seed}: {error}") print(f"{len(found)} check results") return 0 diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 2cff72cd94..785b724136 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -62,6 +62,14 @@ class ConfigureSeedPathTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("HAVE_FEATURE", seed.read_text(encoding="utf-8")) + def test_unwritable_seed_is_reported_without_a_traceback(self): + seed_dir = self.root / "seed_dir" + seed_dir.mkdir() + result = self.run_seed(seed_dir) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(seed_dir), result.stderr) + self.assertNotIn("Traceback", result.stderr) + def test_same_path_preserves_cache(self): self.assert_rejected(self.cache) From 89da7e6c0ae7d1230010dde754089f51c523bc5e Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 21 Oct 2026 23:26:44 +0630 Subject: [PATCH 0451/1360] fix(net): follow measured PS5 Ethernet pair decoding --- core/libs/prx/libSceNet/Export.cpp | 32 ++++++++++-------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 35287a7ec9..98855d4ec0 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1809,29 +1809,22 @@ int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { } int APS5_VABI sceNetEtherStrton(const char* text, NetEtherAddr* address) { - if (text == nullptr || address == nullptr) return fail(NET_EFAULT); + if (text == nullptr || address == nullptr) return fail(NET_EINVAL); + for (std::size_t index = 0; index < 17; ++index) { + if (text[index] == '\0') return fail(NET_EINVAL); + } const auto hexDigit = [](char character) { if (character >= '0' && character <= '9') return character - '0'; - if (character >= 'a' && character <= 'f') return character - 'a' + 10; - if (character >= 'A' && character <= 'F') return character - 'A' + 10; - return -1; + if (character >= 'a' && character <= 'z') return character - 'a' + 10; + if (character >= 'A' && character <= 'Z') return character - 'A' + 10; + return 0x7f; }; NetEtherAddr parsed{}; for (std::size_t index = 0; index < sizeof(parsed.data); ++index) { - int value = hexDigit(*text); - if (value < 0) return fail(NET_EINVAL); - ++text; - if (const int digit = hexDigit(*text); digit >= 0) { - value = value * 16 + digit; - ++text; + if (static_cast(text[index * 3]) >= 128 || static_cast(text[index * 3 + 1]) >= 128) { + NotImplemented_nid_no_patch(__func__); } - if (index + 1 == sizeof(parsed.data)) { - if (*text != '\0') return fail(NET_EINVAL); - } else { - if (*text != ':' && *text != '-') return fail(NET_EINVAL); - ++text; - } - parsed.data[index] = static_cast(value); + parsed.data[index] = static_cast(hexDigit(text[index * 3]) * 16 + hexDigit(text[index * 3 + 1])); } *address = parsed; return 0; @@ -2918,9 +2911,4 @@ int APS5_VABI sceNetEmulationGet(void) { return 0; } -int APS5_VABI sceNetEtherStrton(const char* str, void* address) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } From d5e2042c5b824e2110868358125c54c3a4708d4e Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 21 Oct 2026 23:28:55 +0630 Subject: [PATCH 0452/1360] test(net): cover measured decoding and safe short inputs --- core/libs/tests/GuestSceNet.cpp | 59 +++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 2999be4200..4ce30d1b59 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -878,7 +878,7 @@ static void CheckEtherAddress() { }; static_assert(sizeof(GuardedAddress) == 8); const GuardedAddress original{0x12, {{0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}}, 0x34}; - for (const char* text : {"0:1:2:a:B:f", "00:01:02:0A:0b:0F", "0-1-2-a-B-f", "0:1-2:a-B:f"}) { + for (const char* text : {"00:01:02:0A:0b:0F", "00-01-02-0A-0b-0F", "00.01 02+0A/0b_0F", "00:01:02:0A:0b:0F trailing"}) { auto output = original; const std::array expected{0, 1, 2, 10, 11, 15}; *sceNetErrnoLoc() = 33; @@ -896,31 +896,68 @@ static void CheckEtherAddress() { Require(std::memcmp(input.data, output.address.data, sizeof(input.data)) == 0); Require(output.before == original.before && output.after == original.after); } - for (const char* text : {"", "0", "0:1:2:3:4", "0:1:2:3:4:", "0:1:2:3:4:5:6", "000:1:2:3:4:5", - "0:1:2:3:4:555", ":0:1:2:3:4:5", "0::1:2:3:4:5", "0.1.2.3.4.5", "0x0:1:2:3:4:5", - "0:1:2:3:4:g", "+0:1:2:3:4:5", " 0:1:2:3:4:5", "0:1:2:3:4:5 ", "0:1:2:3:4:5\n"}) { + const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-. +"; + for (std::size_t index = 0; index + 1 < sizeof(digits); ++index) { + const unsigned digit = index < 36 ? index : index < 62 ? index - 26 : 0x7f; + for (std::size_t position = 0; position < 2; ++position) { + std::string text = "01:23:45:67:89:ab"; + text[position] = digits[index]; + const std::array expected{static_cast(position == 0 ? digit * 16 + 1 : digit), 0x23, 0x45, 0x67, 0x89, 0xab}; + auto output = original; + *sceNetErrnoLoc() = 33; + Require(sceNetEtherStrton(text.c_str(), &output.address) == 0); + Require(std::memcmp(output.address.data, expected.data(), expected.size()) == 0); + Require(output.before == original.before && output.after == original.after && *sceNetErrnoLoc() == 33); + } + } + for (const char* text : {"", "0", "0:1:2:a:B:f", "0-1-2-a-B-f", "0:1-2:a-B:f", "0:1:2:3:4", "0:1:2:3:4:", + "0:1:2:3:4:5:6", "000:1:2:3:4:5", "0:1:2:3:4:555", ":0:1:2:3:4:5", "0::1:2:3:4:5", "0.1.2.3.4.5", + "0x0:1:2:3:4:5", "0:1:2:3:4:g", "+0:1:2:3:4:5", " 0:1:2:3:4:5", "0:1:2:3:4:5 ", "0:1:2:3:4:5\n", "001a2bcdeff0"}) { auto output = original; Require(Failed(sceNetEtherStrton(text, &output.address), 22)); Require(std::memcmp(&output, &original, sizeof(output)) == 0); } const std::string valid = "00:11:22:33:44:55"; - for (std::size_t length = 0; length + 1 < valid.size(); ++length) { + std::vector fixed(valid.begin(), valid.end()); + auto fixedOutput = original; + Require(sceNetEtherStrton(fixed.data(), &fixedOutput.address) == 0); + const std::array fixedExpected{0, 0x11, 0x22, 0x33, 0x44, 0x55}; + Require(std::memcmp(fixedOutput.address.data, fixedExpected.data(), fixedExpected.size()) == 0); + Require(fixedOutput.before == original.before && fixedOutput.after == original.after); + for (std::size_t length = 0; length < valid.size(); ++length) { + std::vector text(length + 1, '\0'); + std::memcpy(text.data(), valid.data(), length); auto output = original; - Require(Failed(sceNetEtherStrton(valid.substr(0, length).c_str(), &output.address), 22)); + Require(Failed(sceNetEtherStrton(text.data(), &output.address), 22)); Require(std::memcmp(&output, &original, sizeof(output)) == 0); } - for (unsigned value = 128; value < 256; ++value) { + for (std::size_t position = 0; position < valid.size(); ++position) { auto text = valid; - text[0] = static_cast(value); + text[position] = '\0'; auto output = original; Require(Failed(sceNetEtherStrton(text.c_str(), &output.address), 22)); Require(std::memcmp(&output, &original, sizeof(output)) == 0); } auto output = original; - Require(Failed(sceNetEtherStrton(nullptr, &output.address), 14)); + Require(Failed(sceNetEtherStrton(nullptr, &output.address), 22)); Require(std::memcmp(&output, &original, sizeof(output)) == 0); - Require(Failed(sceNetEtherStrton(valid.c_str(), nullptr), 14)); - Require(Failed(sceNetEtherStrton(nullptr, nullptr), 14)); + Require(Failed(sceNetEtherStrton(valid.c_str(), nullptr), 22)); + Require(Failed(sceNetEtherStrton(nullptr, nullptr), 22)); + for (std::size_t position = 0; position < valid.size(); ++position) { + auto text = valid; + text[position] = static_cast(0xff); + output = original; + bool threw = false; + try { + Require(sceNetEtherStrton(text.c_str(), &output.address) == 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw == (position % 3 != 2)); + if (threw) Require(std::memcmp(&output, &original, sizeof(output)) == 0); + else Require(std::memcmp(output.address.data, fixedExpected.data(), fixedExpected.size()) == 0); + Require(output.before == original.before && output.after == original.after); + } } static void CheckSystemTime() { From f923ebb20011a746bf734c7cf5c9d8d86c248980 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 21 Oct 2026 23:29:59 +0630 Subject: [PATCH 0453/1360] docs(net): state measured parser compatibility gaps --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f790050c2f..449d691398 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII decoding and other firmware versions remain unverified; non-alphanumeric bytes decode as 0x7f. Fixed character pairs, ignored separators, alphabetic values beyond f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) returns 0 without doing anything: every `sceNetResolverStart*` lookup goes to the host resolver, so the library keeps no DNS cache to drop, and the host's own cache is left alone. The `(int flags)` signature and its meaning come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L928-L930); nonzero flags throw because their meaning is unknown - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 5360173da5a03e6ce012a29dadd0518a27b3bac0 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 17:05:38 +0000 Subject: [PATCH 0454/1360] fix(libkernel): keep the thread stack maps alive through static destruction liveStacks, protectedStackPages and guestStackMappings were plain globals, so libkernel's static destructors freed them at exit while detached threads could still unregister their stacks or look one up. ThreadSanitizer reports the race in guest_pthread_join_detach: the main thread's static destruction of liveStacks against a detached thread's UnregisterStack under stackLock. The maps are now created once and never destroyed, as Apr.cpp does for its globals. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Thread.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 8bc51e38db..a8d8520a91 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -76,8 +76,8 @@ static GuestThreadState& CurrentGuestState() { #endif } static std::mutex stackLock; -static std::map> liveStacks; -static std::map protectedStackPages; +static auto& liveStacks = *new std::map>(); +static auto& protectedStackPages = *new std::map(); #if defined(__linux__) struct GuestStackMapping { std::uintptr_t mappingEnd; @@ -85,7 +85,7 @@ struct GuestStackMapping { std::uintptr_t stackEnd; }; static std::mutex guestStackMappingsLock; -static std::map guestStackMappings; +static auto& guestStackMappings = *new std::map(); static void RegisterGuestStackMapping(PthreadPrivate* thread) { const auto mappingStart = reinterpret_cast(thread->guestStack); From 121dc64d11523c4b137975fb56717d7f248dca22 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:06:06 +0100 Subject: [PATCH 0455/1360] feat(libkernel_sys): implement pselect over select --- .../prx/libkernel_sys/Export.Discovery.cpp | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..dcf727762f 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -353,9 +353,31 @@ int APS5_VABI profil_nid_postfix(void) { return 0; } -int APS5_VABI pselect_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); + +int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { + constexpr int SetMask = 3; + struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; + } limit{}; + const Timeval* wait = nullptr; + if (timeout != nullptr) { + const auto* spec = static_cast(timeout); + limit = {spec[0], spec[1] / 1000}; + wait = &limit; + } + std::uint32_t previous[4]{}; + if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + if (sigmask != nullptr) { + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, previous, nullptr); + *__error_nid_postfix() = error; + } + return result; } int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { From 360206e6db68465f901c0821581948270789f8a7 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:06:06 +0100 Subject: [PATCH 0456/1360] test(libkernel_sys): cover pselect timeouts, readiness, errors and the signal mask --- core/libs/CMakeLists.txt | 10 ++ core/libs/tests/GuestKernelSysPselect.cpp | 108 ++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 core/libs/tests/GuestKernelSysPselect.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..5b327884d7 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -337,6 +337,16 @@ if(BUILD_TESTING) target_compile_options(crash_text_tests PRIVATE -fasynchronous-unwind-tables) endif() add_test(NAME crash_text COMMAND crash_text_tests) + add_test_executable(guest_kernel_sys_pselect_tests tests/GuestKernelSysPselect.cpp) + target_include_directories(guest_kernel_sys_pselect_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_pselect_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_pselect_tests) + add_test(NAME guest_kernel_sys_pselect COMMAND guest_kernel_sys_pselect_tests) + set_tests_properties(guest_kernel_sys_pselect PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_pselect PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp) diff --git a/core/libs/tests/GuestKernelSysPselect.cpp b/core/libs/tests/GuestKernelSysPselect.cpp new file mode 100644 index 0000000000..e3924a7f43 --- /dev/null +++ b/core/libs/tests/GuestKernelSysPselect.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI pipe_nid_postfix(int* descriptors); +int APS5_VABI close_nid_postfix(int descriptor); +std::int64_t APS5_VABI write_nid_postfix(int descriptor, const char* buffer, std::int64_t size); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); +int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask); +} + +namespace { + +constexpr int Einval = 22; +constexpr int SigBlock = 1; +constexpr int SigSetmask = 3; +constexpr int SigUsr1 = 30; + +struct Timespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +struct SignalSet { + std::uint32_t bits[4]{}; +}; + +struct DescriptorSet { + std::array words{}; + void Add(int descriptor) { words[descriptor / 64] |= std::uint64_t{1} << (descriptor % 64); } + bool Has(int descriptor) const { return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; } +}; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +SignalSet CurrentMask() { + SignalSet current{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, ¤t) == 0); + return current; +} + +bool Same(const SignalSet& left, const SignalSet& right) { + return std::memcmp(left.bits, right.bits, sizeof(left.bits)) == 0; +} + +} + +int main() { + int descriptors[2]{-1, -1}; + Require(pipe_nid_postfix(descriptors) == 0); + const int reader = descriptors[0]; + const int writer = descriptors[1]; + SignalSet blocked{}; + blocked.bits[(SigUsr1 - 1) / 32] |= 1u << ((SigUsr1 - 1) % 32); + const SignalSet before = CurrentMask(); + { + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 200000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, &blocked) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(!read.Has(reader)); + Require(Same(CurrentMask(), before)); + } + Require(write_nid_postfix(writer, "x", 1) == 1); + { + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 999999999}; + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, nullptr) == 1); + Require(read.Has(reader)); + } + { + DescriptorSet read{}; + read.Add(reader); + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, nullptr, &blocked) == 1); + Require(read.Has(reader)); + Require(Same(CurrentMask(), before)); + } + { + const Timespec invalid{0, 1000000000}; + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &invalid, &blocked) == -1 && Error() == Einval); + Require(Same(CurrentMask(), before)); + const Timespec zero{0, 0}; + Require(pselect_nid_postfix(-1, nullptr, nullptr, nullptr, &zero, nullptr) == -1 && Error() == Einval); + } + Require(sigprocmask_nid_postfix(SigSetmask, &before, nullptr) == 0); + Require(close_nid_postfix(reader) == 0 && close_nid_postfix(writer) == 0); + std::puts("pselect passed"); + return 0; +} From 17650eed5f911ead4c7c32edf3a3d4815d56bca9 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:06:07 +0100 Subject: [PATCH 0457/1360] docs(debt): record that libkernel_sys pselect swaps the mask apart from the wait --- docs/dev/debt/libkernel_sys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..ff16c7e9af 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -14,6 +14,7 @@ - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified +- [pselect](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) From 01e7d158b661edf37bc1e32327fb2f79875f21a7 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 15:24:54 +0700 Subject: [PATCH 0458/1360] fix(shader): link a table call whose V# is a slice of an s_load_dwordx8 or s_load_dwordx16 Co-Authored-By: Claude --- .../tests/execution/ComputeTableCalls.cpp | 14 ++++++++++++-- core/libs/tests/ScalarCall.cpp | 13 +++++++++++++ .../recompiler/ControlFlow/src/GraphBuilder.cpp | 6 ++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp index d9a6a8aa1d..f85c5c7d8a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include @@ -32,10 +33,14 @@ alignas(256) constexpr std::array HoistedCaller{ 0x7e020280u, 0xf4080400u, 0xfa000000u, 0xbf8cc07fu, 0xbf068203u, 0xbf850001u, 0x7e020280u, 0x8f148402u, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array SliceCaller{ + 0x7e020280u, 0xf40c0600u, 0xfa000010u, 0x8f148402u, 0xbf8cc07fu, 0xf428020eu, 0x28000000u, 0xbf8cc07fu, + 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, + 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; alignas(256) constexpr std::array SelectSixteen{0x85159091u, 0x7e020215u, 0xbe80200eu}; alignas(256) constexpr std::array SelectThirtyTwo{0x8515a0a1u, 0x7e020215u, 0xbe80200eu}; alignas(16) std::array Table{}; -alignas(16) std::array Descriptors{}; +alignas(16) std::array Descriptors{}; alignas(256) std::array Output{}; void Fill() { @@ -43,7 +48,10 @@ void Fill() { const auto second = reinterpret_cast(SelectThirtyTwo.data()); Table = {static_cast(first), static_cast(first >> 32u), 0u, 0u, static_cast(second), static_cast(second >> 32u), 0u, 0u}; const auto table = reinterpret_cast(Table.data()); - Descriptors = {static_cast(table), static_cast((table >> 32u) & 0xffffu) | (16u << 16u), 2u, 0x31016facu}; + const std::array descriptor{static_cast(table), static_cast((table >> 32u) & 0xffffu) | (16u << 16u), 2u, 0x31016facu}; + Descriptors = {}; + std::copy(descriptor.begin(), descriptor.end(), Descriptors.begin()); + std::copy(descriptor.begin(), descriptor.end(), Descriptors.begin() + 8); } std::array UserData(std::uint32_t index, bool scc) { @@ -149,6 +157,8 @@ int main() { Check(1u, true, wave32, 33u, "the second record's callee with SCC 1"); Check(0u, false, wave32, 16u, "the first record's callee through a descriptor loaded before a branch", HoistedCaller.data()); Check(1u, true, wave32, 33u, "the second record's callee through a descriptor loaded before a branch", HoistedCaller.data()); + Check(0u, false, wave32, 16u, "the first record's callee through the second half of an s_load_dwordx8", SliceCaller.data()); + Check(1u, true, wave32, 33u, "the second record's callee through the second half of an s_load_dwordx8", SliceCaller.data()); const auto log = Dispatch(2u, false, wave32); Require(log.find("called address 0x0 at program counter") != std::string::npos && log.find("which no linked callee of that call matches") != std::string::npos, std::string("a record past the table did not report an unlinked call") + (wave32 ? " (wave32)" : " (wave64)") + ": " + log); diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index c4d38b87c4..66216862ba 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include using namespace ShaderRecompiler; @@ -236,6 +237,18 @@ static void CheckTableCalls() { Require(table(10u, 0xbf810000u, 12u).empty()); Require(table(5u, 0x28000008u).size() == 1u && table(5u, 0x28000008u)[0].entryOffset == 8u); Require(table(0u, 0xbe940380u).empty()); + const auto slice = [&](std::uint32_t load, std::uint32_t bufferLoad) { + auto code = caller; + code[1] = load; + code[4] = bufferLoad; + return FindTableCalls(RdnaInstructionDecoder{}.Decode(code), swappc); + }; + for (const auto& [load, bufferLoad, offset] : {std::tuple{0xf40c0606u, 0xf428020cu, 0x270u}, std::tuple{0xf40c0606u, 0xf428020eu, 0x280u}, std::tuple{0xf4100606u, 0xf4280210u, 0x290u}, std::tuple{0xf4100606u, 0xf4280212u, 0x2a0u}}) { + const auto sliced = slice(load, bufferLoad); + Require(sliced.size() == 1u && sliced[0].userDataRegister == 12u && sliced[0].descriptorOffset == offset && sliced[0].entryOffset == 0u && sliced[0].entryStride == 16u); + } + Require(slice(0xf40c0606u, 0xf4280210u).empty()); + Require(slice(0xf40c0606u, 0xf428020du).empty()); auto readsLane = caller; readsLane.insert(readsLane.begin(), {0xd4c2000bu, 0x00020300u}); const auto readsLaneProgram = RdnaInstructionDecoder{}.Decode(readsLane); diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 5a2459f5af..45e66c5f65 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1526,7 +1526,9 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn if (!sameBlockWriter && !reachingWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize)) continue; const auto& table = program.instructions[descriptorLoad]; const auto pointer = scalarIndex(table.source0); - if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || + const auto loaded = scalarIndex(table.destination); + if ((table.op != RdnaOpcode::SLoadDwordx4 && table.op != RdnaOpcode::SLoadDwordx8 && table.op != RdnaOpcode::SLoadDwordx16) || loaded == NoScalarRegister || descriptor < loaded || + (descriptor - loaded) % 4u != 0u || descriptor - loaded + 4u > table.dataDwordCount || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u) continue; std::optional constantPointer; if (!(userData(pointer) && userData(pointer + 1u) && unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) && unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize))) { @@ -1548,7 +1550,7 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn if (shifted.op != RdnaOpcode::SLshlB32 || !isScalar(shifted.destination, offset) || !isImmediate(shifted.source1, shift) || shift < 2u || shift > 24u) continue; stride = 1u << shift; } - calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride, constantPointer}); + calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset + (descriptor - loaded) * 4u, load.memoryOffset + lowDword * 4u, stride, constantPointer}); } return calls; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c4ea080793..1fa1aeee34 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -199,7 +199,7 @@ - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. - [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. From 73c9c2a7628a563f1b0785914f16e43ae8ba3597 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 15:26:38 +0700 Subject: [PATCH 0459/1360] test(shader): reject misaligned and partial V# slices of a table call's s_load_dwordx8 Co-Authored-By: Claude --- core/libs/tests/ScalarCall.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 66216862ba..2da9bd35ef 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -249,6 +249,9 @@ static void CheckTableCalls() { } Require(slice(0xf40c0606u, 0xf4280210u).empty()); Require(slice(0xf40c0606u, 0xf428020du).empty()); + Require(slice(0xf40c0686u, 0xf428020eu).empty()); + Require(slice(0xf40c0686u, 0xf428020cu).empty()); + Require(slice(0xf40c0586u, 0xf428020eu).empty()); auto readsLane = caller; readsLane.insert(readsLane.begin(), {0xd4c2000bu, 0x00020300u}); const auto readsLaneProgram = RdnaInstructionDecoder{}.Decode(readsLane); From 5998ff889dc4ae41af5a38b089e4605bc87428cb Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 21 Oct 2026 07:37:55 -0500 Subject: [PATCH 0460/1360] test(libSceUserService): cover sceUserServiceGetUserNumber and sceUserServiceGetPlatformPrivacyWs1 --- core/libs/tests/GuestUserService.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 4e6e1809a4..3e6d7559da 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -5,6 +5,8 @@ #include extern "C" { +int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number); +int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* value); int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); @@ -28,6 +30,17 @@ void Require(bool value) { if (!value) std::abort(); } int main() { Require(sceUserServiceGetForegroundUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + int32_t userNumber = -1; + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(123, &userNumber) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, &userNumber) == USER_SERVICE_OK); + Require(userNumber == 1); + + int32_t privacy = -1; + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, &privacy) == USER_SERVICE_OK); + Require(privacy == 0); + int foreground = -1; Require(sceUserServiceGetForegroundUser(&foreground) == USER_SERVICE_OK); Require(foreground == USER_SERVICE_INITIAL_USER_ID); From 58ad4eda77577412aab42a180a82fc164d1cfab1 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 22 Oct 2026 04:06:03 +1300 Subject: [PATCH 0461/1360] fix(libkernel): report app0 symlinks as their target type in getdirentries On Linux sceKernelGetdirentries copied the host d_type, so a directory under app0 that is a host symlink was listed as a link, and a title that only descends into directory entries skipped it. Inside the app0 tree, report a symlink (or an entry of unknown type) as the type of its target; links elsewhere, and dangling links, stay links. --- core/libs/prx/libkernel/CMakeLists.txt | 7 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 32 ++++++++- core/libs/tests/GuestDirectoryLinkTypes.cpp | 80 +++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + docs/user/USAGE.md | 2 +- 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestDirectoryLinkTypes.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..74d8ec56af 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1696,6 +1696,13 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_directory_link_type_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryLinkTypes.cpp) + target_include_directories(guest_directory_link_type_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_link_type_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_link_types COMMAND guest_directory_link_type_tests) + set_tests_properties(guest_directory_link_types PROPERTIES TIMEOUT 15) + endif() add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2146bbe2d6..b80b65097c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1115,6 +1115,35 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { #else +static bool InsideApp0(int fd) { + const int savedErrno = errno; + const bool inside = [fd] { + const auto directory = File::DirectoryDescriptorPath(fd); + if (!directory) return false; + std::error_code error; + const auto app0 = std::filesystem::weakly_canonical(ResolveDiskPath_nid_no_patch("/app0"), error); + if (error || app0.empty()) return false; + const auto relative = directory->lexically_relative(app0); + return !relative.empty() && *relative.begin() != ".."; + }(); + errno = savedErrno; + return inside; +} + +static unsigned char GuestEntryType(int fd, const struct dirent64& entry, bool followLinks) { + if (entry.d_type != DT_LNK && entry.d_type != DT_UNKNOWN) return entry.d_type; + if (entry.d_type == DT_LNK && !followLinks) return DT_LNK; + const int savedErrno = errno; + struct stat target{}; + const bool found = ::fstatat(fd, entry.d_name, &target, followLinks ? 0 : AT_SYMLINK_NOFOLLOW) == 0; + errno = savedErrno; + if (!found) return entry.d_type; + if (S_ISDIR(target.st_mode)) return DT_DIR; + if (S_ISREG(target.st_mode)) return DT_REG; + if (S_ISLNK(target.st_mode)) return DT_LNK; + return entry.d_type; +} + int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(fd, "getdirentries"); @@ -1136,6 +1165,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas if (read < 0) return SceErrorFromErrno(errno); std::size_t written = 0; off_t resume = base; + const bool followLinks = InsideApp0(fd); for (long offset = 0; offset < read;) { const auto* entry = reinterpret_cast(host.data() + offset); const auto nameLength = std::strlen(entry->d_name); @@ -1152,7 +1182,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas const auto nameBytes = static_cast(nameLength); std::memcpy(out, &fileNumber, sizeof(fileNumber)); std::memcpy(out + 4, &recordLength, sizeof(recordLength)); - out[6] = static_cast(entry->d_type); + out[6] = static_cast(GuestEntryType(fd, *entry, followLinks)); out[7] = static_cast(nameBytes); std::memcpy(out + GuestHeaderBytes, entry->d_name, nameLength); written += record; diff --git a/core/libs/tests/GuestDirectoryLinkTypes.cpp b/core/libs/tests/GuestDirectoryLinkTypes.cpp new file mode 100644 index 0000000000..344b0c3a1c --- /dev/null +++ b/core/libs/tests/GuestDirectoryLinkTypes.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelGetdents(int, char*, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory link type check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::uint8_t TypeDirectory = 4; +static constexpr std::uint8_t TypeRegular = 8; +static constexpr std::uint8_t TypeLink = 10; + +static std::map Types(const char* path) { + const int directory = sceKernelOpen(path, SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array buffer{}; + std::map types; + for (;;) { + const int read = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(read >= 0); + if (read == 0) break; + for (int offset = 0; offset < read;) { + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, buffer.data() + offset + 4, sizeof(recordLength)); + Require(recordLength != 0 && offset + recordLength <= read); + types[std::string(buffer.data() + offset + 8)] = static_cast(buffer[offset + 6]); + offset += recordLength; + } + } + Require(sceKernelClose(directory) == 0); + return types; +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-link-types-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "app0" / "real")); + Require(std::filesystem::create_directories(root / "dump" / "packed")); + Require(std::filesystem::create_directories(root / "data")); + { std::ofstream stream(root / "dump" / "eboot.bin", std::ios::binary); stream << "x"; } + std::filesystem::create_directory_symlink(root / "dump" / "packed", root / "app0" / "packed"); + std::filesystem::create_symlink(root / "dump" / "eboot.bin", root / "app0" / "eboot.bin"); + std::filesystem::create_symlink(root / "dump" / "missing", root / "app0" / "dangling"); + std::filesystem::create_directory_symlink(root / "dump" / "packed", root / "data" / "saved"); + std::filesystem::current_path(root); + + auto app0 = Types("/app0"); + Require(app0.contains("real") && app0["real"] == TypeDirectory); + Require(app0.contains("packed") && app0["packed"] == TypeDirectory); + Require(app0.contains("eboot.bin") && app0["eboot.bin"] == TypeRegular); + Require(app0.contains("dangling") && app0["dangling"] == TypeLink); + + auto data = Types("/data"); + Require(data.contains("saved") && data["saved"] == TypeLink); + + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + std::puts("directory link type tests passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..b5c8ebaa09 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -134,6 +134,7 @@ - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified +- [Linux directory entry types](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` reports a host symlink inside the app0 tree as the type of its target (directory or regular file), since a console app0 has no links and links there come from how the title was unpacked; a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside that target keep the link type. - [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. - [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 42270f520c..21ea03c1d0 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -166,7 +166,7 @@ app0/ ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. On Linux, `app0/` must not contain symbolic links: a directory listing gives the title the link's own type, not the type of the file or directory it points to, so a title that walks its directories skips a linked directory. Use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/<title id>` and fail to load their fonts without it). Only the converted executable runs. On Linux, a symbolic link inside the `app0/` tree is listed with the type of the file or directory it points to, so a linked directory is walked like a real one. Links inside a linked directory whose target is outside `app0/` keep the link type; for nested trees, link only the top-level entries or use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. From 99493bb59081284ea106a1575ad45379a4cba986 Mon Sep 17 00:00:00 2001 From: Bram910 <bram@makowski.be> Date: Wed, 21 Oct 2026 19:18:22 +0200 Subject: [PATCH 0462/1360] fix(relinker): validate programmatic patch widths --- .../relinker/src/patching/GamePatch.cpp | 21 +++++-- .../relinker/tests/GamePatchTests.cpp | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 05d334868a..1945eb159b 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -240,6 +240,15 @@ PatchEntry BuildEntry(const Table& table, const std::size_t headerLine, std::set return entry; } +std::string Describe(const PatchEntry& entry) { + return "patch " + entry.Target + " va=" + HexValue(entry.VirtualAddress) + " (line " + std::to_string(entry.Line) + ")"; +} + +void ValidateWidth(const PatchEntry& entry) { + if (entry.Expected.empty() || entry.Expected.size() != entry.Value.size()) + throw Domain::RelinkerException(Describe(entry) + ": expected and value must have the same nonzero width"); +} + struct EntryRange { std::optional<PatchedRange> Range; std::string Reason; @@ -275,14 +284,13 @@ std::vector<EntryRange> ResolveEach(const ElfReader& reader, const std::vector<P const auto segments = reader.ReadProgramHeaders(); const auto header = reader.ReadHeader(); std::vector<EntryRange> resolved; - for (const auto& entry : entries) resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), header, entry)); + for (const auto& entry : entries) { + ValidateWidth(entry); + resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), header, entry)); + } return resolved; } -std::string Describe(const PatchEntry& entry) { - return "patch " + entry.Target + " va=" + HexValue(entry.VirtualAddress) + " (line " + std::to_string(entry.Line) + ")"; -} - bool Overlaps(const std::uint64_t firstOffset, const std::uint64_t firstLength, const std::uint64_t secondOffset, const std::uint64_t secondLength) { return firstOffset < secondOffset + secondLength && secondOffset < firstOffset + firstLength; } @@ -427,7 +435,10 @@ std::vector<PatchedRange> ResolveRanges(const ElfReader& reader, const std::vect std::vector<AppliedPatch> ApplyPatches(std::vector<std::uint8_t>& bytes, const std::string& target, const std::vector<PatchEntry>& entries, const std::vector<PatchedRange>& ranges) { if (entries.size() != ranges.size()) throw Domain::RelinkerException("Patch ranges do not match patch entries"); for (std::size_t index = 0; index < entries.size(); ++index) { + ValidateWidth(entries[index]); const auto& range = ranges[index]; + if (range.Length != entries[index].Expected.size()) + throw Domain::RelinkerException(Describe(entries[index]) + ": range length does not match the patch width", range.Offset); if (range.Offset > bytes.size() || range.Length > bytes.size() - range.Offset) throw Domain::RelinkerException(Describe(entries[index]) + ": range exceeds the image", range.Offset); const auto first = bytes.begin() + static_cast<std::ptrdiff_t>(range.Offset); if (!std::equal(entries[index].Expected.begin(), entries[index].Expected.end(), first)) diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index f81bc1f2fe..1ed801fe45 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -282,6 +282,54 @@ void applyAndMismatch() { require(PatchTarget(bytes, "other", entries).Applied.empty() && bytes == original, "entry hit another target"); } +void programmaticWidths() { + auto bytes = twoSegmentFixture(); + const auto original = bytes; + const ElfReader reader(original); + const std::vector<PatchEntry> invalid{ + entry("t", 0x1000, {0}, {9, 8}), + entry("t", 0x1000, {0, 1}, {9}), + entry("t", 0x1000, {}, {}), + entry("t", 0x1000, {}, {9}), + entry("t", 0x1000, {0}, {}), + }; + for (const auto& patch : invalid) { + const std::vector<PatchEntry> entries{entry("t", 0x1100, {0x80}, {1}), patch}; + rejects([&] { ResolveRanges(reader, entries); }, "same nonzero width"); + rejects([&] { PatchTarget(bytes, "t", entries); }, "same nonzero width"); + require(bytes == original, "an invalid patch width wrote bytes"); + rejects([&] { ApplyPatches(bytes, "t", entries, {{0x2000, 1, 0x1100}, {0x1000, patch.Expected.size(), 0x1000}}); }, "same nonzero width"); + require(bytes == original, "direct application of an invalid patch width wrote bytes"); + for (const bool outputMode : {false, true}) { + PatchFile file; + file.MainTarget = "t"; + file.Entries = entries; + rejects([&] { VerifyPatches(file, {{"t", original}}, {}, outputMode); }, "same nonzero width"); + } + } +} + +void applicationRangeWidths() { + const std::vector<std::uint8_t> original{0, 1, 2, 3}; + auto bytes = original; + const std::vector<PatchEntry> entries{entry("t", 0x1000, {0}, {9}), entry("t", 0x1002, {2, 3}, {8, 7})}; + for (const std::uint64_t length : {0u, 1u, 3u}) { + rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {2, length, 0x1002}}); }, "range length does not match"); + require(bytes == original, "a mismatched range length wrote bytes"); + } + rejects([&] { ApplyPatches(bytes, "t", {entry("t", 0x1003, {3, 4}, {8, 7})}, {{3, 1, 0x1003}}); }, "range length does not match"); + require(bytes == original, "a truncated comparison range wrote bytes"); + for (const std::uint64_t offset : std::array<std::uint64_t, 4>{3, 4, 5, UINT64_MAX}) { + rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {offset, 2, 0x1002}}); }, "range exceeds the image"); + require(bytes == original, "an out-of-bounds patch wrote bytes"); + } + rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}}); }, "ranges do not match"); + require(bytes == original, "a missing range wrote bytes"); + const auto applied = ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {2, 2, 0x1002}}); + require(bytes == std::vector<std::uint8_t>({9, 1, 8, 7}) && applied.size() == 2, "valid boundary patches failed"); + require(ApplyPatches(bytes, "t", {}, {}).empty(), "empty patch list was rejected"); +} + void overlapWithWriters() { const std::vector<PatchedRange> patched{{0x1010, 2, 0x401010}}; const auto reject = [&](const WriterSites& sites, const std::string& writer) { rejects([&] { CheckOverlaps("t", patched, sites); }, writer); }; @@ -340,6 +388,13 @@ void conversion() { rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00"); failing.Patches = {entry("custom-name.bin", 0x1000, {0xc3, 0}, {0, 0}), entry("custom-name.bin", 0x1001, {0}, {1})}; rejects([&] { converter.Convert(conversionInput(executable), failing); }, "overlaps"); + for (const auto target : {TargetSystem::Linux, TargetSystem::Windows, TargetSystem::MacOs}) { + failing.Target = target; + for (const char* name : {"custom-name.bin", "sce_module/plugin.prx"}) { + failing.Patches = {entry(name, 0x1001, {0}, {9, 8})}; + rejects([&] { converter.Convert(conversionInput(executable), failing); }, "same nonzero width"); + } + } const auto repeated = converter.Convert(conversionInput(executable), plain); require(repeated.Executable == baseline.Executable && repeated.Modules.front().Bytes == baseline.Modules.front().Bytes, "no entries changed the output"); @@ -511,6 +566,8 @@ int main() { ranges(); headerRange(); applyAndMismatch(); + programmaticWidths(); + applicationRangeWidths(); overlapWithWriters(); conversion(); targets(); From 64675a3756f3614ab33bf7664704f25183d9ca21 Mon Sep 17 00:00:00 2001 From: klosixgaming-arch <klosixgaming-arch@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:22:39 +0000 Subject: [PATCH 0463/1360] fix(shader): require the phi arm table offset to dominate the table read When a table column pointer is read through a phi, the offset taken from one arm is substituted into the image handle after the merge. An offset computed inside an arm does not dominate that use, so planning now refuses it instead of producing an invalid module. Adds a case where each arm computes its own offset; it plans as a table on the previous head and is refused with this change. AI-assisted: yes. Written with AI assistance and reviewed by me. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 9 +++++++ .../Optimization/src/ResourceTracker.cpp | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 058046a306..cc24a594d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3487,6 +3487,15 @@ void verifyPointerChainImageTable() { auto nonTableRequest = TableRequest(nonTableCode, userData); nonTableRequest.shader.codeAddress = 0x3a000u; expectFailure([&] { static_cast<void>(GetResourcePlan(nonTableRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi with a non-table branch was planned as a table"); + + auto armOffsetCode = code; + armOffsetCode.erase(armOffsetCode.begin() + 14, armOffsetCode.begin() + 17); + armOffsetCode.insert(armOffsetCode.begin() + 14, { + 0xbf068010u, 0xbf850004u, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xbf820003u, 0x8f128411u, 0xf4240b02u, 0x24000008u + }); + auto armOffsetRequest = TableRequest(armOffsetCode, userData); + armOffsetRequest.shader.codeAddress = 0x3b000u; + expectFailure([&] { static_cast<void>(GetResourcePlan(armOffsetRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi whose arms compute their own table offset was planned as a table"); } void verifyDescriptorPhis() { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index ceb8017633..04d2be4845 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -732,6 +732,27 @@ private: return candidate; } + bool BlockDominates(const IrBlock* dominator, const IrBlock* block) const { + if (dominator == block) return true; + std::vector<const IrBlock*> pending{block}; + std::unordered_set<const IrBlock*> visited{block}; + while (!pending.empty()) { + const IrBlock* current = pending.back(); + pending.pop_back(); + if (current->Predecessors().empty()) return false; + for (const IrBlock* predecessor : current->Predecessors()) { + if (predecessor != dominator && visited.insert(predecessor).second) pending.push_back(predecessor); + } + } + return true; + } + + bool AvailableAt(IrValue* value, const IrBlock* block) const { + value = value->Resolve(); + if (value->HasImmediate() || value->Parent() == nullptr) return true; + return BlockDominates(value->Parent(), block); + } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& descriptorPointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { std::vector<PointerHop> hops; IrValue* pointer = &descriptorPointer; @@ -786,6 +807,9 @@ private: return false; } } + if (!AvailableAt(offset, handle.Parent())) { + return false; + } IrValue* key = nullptr; std::uint32_t stride = 0; std::uint32_t addend = 0; From da1d9407ed495e8d6b29b913d030a831a374b604 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Wed, 21 Oct 2026 23:23:07 +0600 Subject: [PATCH 0464/1360] fix(tools): bound total SELF alignment padding --- docs/user/USAGE.md | 2 + tools/tests/test_unwrap_self.py | 96 ++++++++++++++++++++++++++++++--- tools/unwrap_self.py | 9 ++-- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 672e35bbbf..c3bb6d2171 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -12,6 +12,8 @@ Select or drop multiple app ZIP archives to convert them into one ZIP bundle. Th ## Input and conversion +The SELF unwrapper refuses containers requiring more than 64 MiB of total alignment padding before allocating the output buffer. This limit counts padding across file-backed and empty segments, not segment data; a refusal preserves the input and any existing output file. + Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path <path>`. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py <input.self> <output.elf>` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 6ee6d2095e..7ba9a38d9d 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -473,7 +473,7 @@ class UnwrapTest(unittest.TestCase): def allocate(value): if isinstance(value, int): - self.assertLessEqual(value, len(container) + unwrap_self.MAX_EMPTY_LOAD_PADDING) + self.assertLessEqual(value, len(container) + unwrap_self.MAX_ALIGNMENT_PADDING) return bytearray(value) for previous in (None, b"previous output"): @@ -486,7 +486,7 @@ class UnwrapTest(unittest.TestCase): contextlib.redirect_stderr(io.StringIO()) as stderr: self.assertEqual(unwrap_self.main(), 2) self.assertEqual(stdout.getvalue(), "") - self.assertIn("empty load alignment requires more than", stderr.getvalue()) + self.assertIn("segment alignment requires more than", stderr.getvalue()) self.assertNotIn("Traceback", stderr.getvalue()) if previous is not None: self.assertEqual(output.read_bytes(), previous) @@ -494,20 +494,104 @@ class UnwrapTest(unittest.TestCase): self.assertFalse(output.exists()) self.assertEqual(source.read_bytes(), container) + def test_excessive_file_backed_padding_is_refused_before_allocation(self): + cases = [ + ([(PT_LOAD, 5, 0x1234, (1 << 40) - 1, 0x100, 1 << 40)], + [(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100}), + ([(PT_LOAD, 5, 0x1234, 0, 0x100, PAGE), + (SCE_COMMENT, 0, 0xB00, 0, 0x20, 1 << 40)], + [(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100, 0xB00: pattern(9, 0x20)}), + ] + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + for segments, records, payloads in cases: + container = build_container(segments, records, payloads) + source.write_bytes(container) + + def allocate(value): + if isinstance(value, int): + self.assertLessEqual(value, len(container) + 64 * 1024 * 1024) + return bytearray(value) + + for previous in (None, b"previous output"): + with self.subTest(segments=segments, previous=previous): + output.unlink(missing_ok=True) + if previous is not None: + output.write_bytes(previous) + with mock.patch.object(unwrap_self, "bytearray", side_effect=allocate, create=True), \ + mock.patch.object(sys, "argv", ["unwrap_self.py", str(source), str(output)]), \ + contextlib.redirect_stdout(io.StringIO()) as stdout, \ + contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertEqual(unwrap_self.main(), 2) + self.assertEqual(stdout.getvalue(), "") + self.assertIn("alignment requires more than", stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + self.assertEqual(source.read_bytes(), container) + if previous is None: + self.assertFalse(output.exists()) + else: + self.assertEqual(output.read_bytes(), previous) + + def test_alignment_padding_limit_is_cumulative(self): + for empty in (False, True): + with self.subTest(empty=empty): + segments = [(PT_LOAD, 5, 0x1234, 0x80, 0x20, 0x100), + (PT_LOAD, 6, 0x5678, 0x580, 0 if empty else 0x10, 0x1000)] + records = [(0x800, 0x800, 0x20)] + payloads = {0x800: pattern(1, 0x20)} + if not empty: + records.append((0x100800, 0xA00, 0x10)) + payloads[0xA00] = pattern(9, 0x10) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + container = build_container(segments, records, payloads) + source.write_bytes(container) + output.write_bytes(b"previous output") + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x15F): + with self.assertRaisesRegex(unwrap_self.Refused, "alignment requires more than"): + unwrap_self.unwrap(source, output) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(source.read_bytes(), container) + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x160): + unwrap_self.unwrap(source, output) + data, headers = read_program_headers(output) + for header in headers: + self.assertEqual(header[2] % header[7], header[3] % header[7]) + self.assertEqual(data[headers[0][2]:headers[0][2] + 0x20], payloads[0x800]) + if not empty: + self.assertEqual(data[headers[1][2]:headers[1][2] + 0x10], payloads[0xA00]) + + def test_large_alignment_with_small_padding_remains_supported(self): + payload = b"x" * 0x100 + segments = [(PT_LOAD, 5, 0x1234, FIRST_REGION + 0x10, len(payload), 1 << 40)] + container = build_container(segments, [(0x2804, 0x800, len(payload))], {0x800: payload}) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x10): + unwrap_self.unwrap(source, output) + data, headers = read_program_headers(output) + load = headers[0] + self.assertEqual(load[2] % load[7], load[3] % load[7]) + self.assertEqual(load[2], FIRST_REGION + 0x10) + self.assertEqual(data[load[2]:load[2] + load[5]], payload) + self.assertEqual(source.read_bytes(), container) + def test_empty_load_padding_limit_counts_only_added_bytes(self): - baseline, output = self.convert(build_container(default_segments(), default_records(), default_payloads())) + backed = [(*segment[:5], 1) for segment in default_segments()] + baseline, output = self.convert(build_container(backed, default_records(), default_payloads())) self.assertEqual(baseline.returncode, 0, baseline.stderr) baseline_size = output.stat().st_size limit = 0x100 for extra in (-1, limit, limit + 1): with self.subTest(extra=extra): address = 0x10000 + baseline_size + extra - segments = default_segments() + [(PT_LOAD, 6, 0, address, 0, 0x10000)] + segments = backed + [(PT_LOAD, 6, 0, address, 0, 0x10000)] container = build_container(segments, default_records(), default_payloads()) source = Path(self.directory) / "input.self" source.write_bytes(container) previous = output.read_bytes() - with mock.patch.object(unwrap_self, "MAX_EMPTY_LOAD_PADDING", limit): + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", limit): if extra <= limit: unwrap_self.unwrap(source, output) data, headers = read_program_headers(output) @@ -515,7 +599,7 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(headers[3][2], baseline_size + extra) self.assertEqual(headers[3][2] % headers[3][7], address % headers[3][7]) else: - with self.assertRaisesRegex(unwrap_self.Refused, "empty load alignment requires more than"): + with self.assertRaisesRegex(unwrap_self.Refused, "segment alignment requires more than"): unwrap_self.unwrap(source, output) self.assertEqual(output.read_bytes(), previous) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 1ce81f2e12..491c499633 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -43,7 +43,7 @@ RECORD_SIZE = 0x20 RECORD_HAS_SEGMENT = 0x800 RECORD_SEGMENT_SHIFT = 20 FIRST_REGION = 0x400 -MAX_EMPTY_LOAD_PADDING = 64 * 1024 * 1024 +MAX_ALIGNMENT_PADDING = 64 * 1024 * 1024 PT_LOAD = 1 PT_NOTE = 4 ENTROPY_LIMIT = 7.5 @@ -273,9 +273,11 @@ def unwrap(path, output, verbose=False): regions.sort(key=lambda region: region["old"]) program_offset = struct.unpack_from("<Q", elf_header, 0x20)[0] cursor = max(FIRST_REGION, program_offset + len(headers) * 56) + padding = 0 for region in regions: alignment = region["align"] if region["align"] and (region["align"] & (region["align"] - 1)) == 0 else 1 placed = cursor + (region["vaddr"] - cursor) % alignment + padding += placed - cursor region["new"] = placed cursor = placed + region["size"] @@ -283,8 +285,9 @@ def unwrap(path, output, verbose=False): for header in headers: if header["type"] == PT_LOAD and header["size"] == 0: empty_end = max(empty_end, header["vaddr"] % max(header["align"], 1)) - if empty_end - cursor > MAX_EMPTY_LOAD_PADDING: - raise Refused("empty load alignment requires more than %d bytes of output padding" % MAX_EMPTY_LOAD_PADDING) + padding += empty_end - cursor + if padding > MAX_ALIGNMENT_PADDING: + raise Refused("segment alignment requires more than %d bytes of output padding" % MAX_ALIGNMENT_PADDING) cursor = empty_end result = bytearray(cursor) From bc09ce31285b2e1ab48e81ce15510c8c37f0d32c Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:16:35 +0300 Subject: [PATCH 0465/1360] feat(libkernel): keep the default thread stack size in one settable place --- .../libs/prx/libkernel/Pthread/include/Pthread.hpp | 2 ++ core/libs/prx/libkernel/Pthread/src/Attr.cpp | 14 +++++++++++++- core/libs/prx/libkernel/Pthread/src/Thread.cpp | 3 +-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 0cef7ba498..c4442bd188 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -182,5 +182,7 @@ std::unique_lock<std::mutex> LockLiveGuestThread(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); +extern "C" std::size_t GetDefaultStackSize_nid_no_patch(); +extern "C" bool SetDefaultStackSize_nid_no_patch(std::size_t size); #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ce6ccac93c..0b3b82078c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -12,6 +12,18 @@ static constexpr int DETACH_JOINABLE = 0; static constexpr int DETACH_DETACHED = 1; static constexpr int SCHED_FIFO_PS5 = 1; +static std::atomic<std::size_t> defaultStackSize{DEFAULT_STACK_SIZE}; + +std::size_t GetDefaultStackSize_nid_no_patch() { + return defaultStackSize.load(std::memory_order_relaxed); +} + +bool SetDefaultStackSize_nid_no_patch(std::size_t size) { + if (size <= MIN_STACK_SIZE) return false; + defaultStackSize.store(size, std::memory_order_relaxed); + return true; +} + #ifdef _WIN32 #include <windows.h> #include <limits> @@ -23,7 +35,7 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { if (!attr) throw std::runtime_error(std::string(__func__) + ": null attr"); auto* p = new (std::nothrow) PthreadAttrPrivate{}; if (!p) return SCE_KERNEL_ERROR_ENOMEM; - p->_stacksize = DEFAULT_STACK_SIZE; + p->_stacksize = GetDefaultStackSize_nid_no_patch(); p->_detachstate = DETACH_JOINABLE; p->_schedpriority = 700; p->_schedpolicy = SCHED_FIFO_PS5; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 8bc51e38db..31451006c2 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -35,7 +35,6 @@ static constexpr int SCE_OK = 0; -static constexpr std::size_t DEFAULT_STACK_SIZE = 1u << 20; static constexpr std::size_t HOST_STACK_MARGIN = 1u << 20; static constexpr int DETACH_DETACHED = 1; static constexpr std::size_t THREAD_NAME_CAPACITY = 32; @@ -825,7 +824,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread bool detached = false; if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED); p->_detached = detached; - p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE; + p->stackSize = attr ? (*attr)->_stacksize : GetDefaultStackSize_nid_no_patch(); if (attr) { if ((*attr)->_inheritsched != GUEST_INHERIT_SCHED) p->scope = (*attr)->_scope; From 9c4943eec0df19c6eb819cbec50373337134423e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:16:35 +0300 Subject: [PATCH 0466/1360] feat(libkernel_sys): implement pthread_set_defaultstacksize_np, scePthreadSetDefaultstacksize and scePthreadSetName --- .../libs/prx/libkernel_sys/Export.Discovery.cpp | 15 --------------- core/libs/prx/libkernel_sys/Export.cpp | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..702eff446b 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -483,11 +483,6 @@ int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_set_user_context_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2428,16 +2423,6 @@ int APS5_VABI scePthreadSetBesteffort(void) { return 0; } -int APS5_VABI scePthreadSetDefaultstacksize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePthreadSetName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePthreadSetconcurrency(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..2fcaa28651 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -23,6 +23,7 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -690,4 +691,20 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const return 0; } + +bool SetDefaultStackSize_nid_no_patch(std::size_t size); +void APS5_VABI pthread_set_name_np_nid_postfix(void* thread, const char* name); + +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(std::size_t size) { + return SetDefaultStackSize_nid_no_patch(size) ? 0 : GuestEinval; +} + +int APS5_VABI scePthreadSetDefaultstacksize(std::size_t size) { + return SetDefaultStackSize_nid_no_patch(size) ? 0 : SceKernelError(GuestEinval); +} + +void APS5_VABI scePthreadSetName(void* thread, const char* name) { + pthread_set_name_np_nid_postfix(thread, name); +} + } From 92f760c461e7dcd3c3f90d15e43015aee3843039 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:16:35 +0300 Subject: [PATCH 0467/1360] test(libkernel_sys): cover the default stack size and scePthreadSetName --- core/libs/CMakeLists.txt | 8 +- .../tests/GuestKernelSysThreadDefaults.cpp | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestKernelSysThreadDefaults.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..7b58809003 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -300,12 +300,18 @@ if(BUILD_TESTING) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_tests) add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests) + add_test_executable(guest_kernel_sys_thread_defaults_tests tests/GuestKernelSysThreadDefaults.cpp) + target_include_directories(guest_kernel_sys_thread_defaults_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_thread_defaults_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_thread_defaults_tests) + add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests) + set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>) endif() if(WIN32) - set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") endif() if(APPLE) diff --git a/core/libs/tests/GuestKernelSysThreadDefaults.cpp b/core/libs/tests/GuestKernelSysThreadDefaults.cpp new file mode 100644 index 0000000000..18a48b8f15 --- /dev/null +++ b/core/libs/tests/GuestKernelSysThreadDefaults.cpp @@ -0,0 +1,84 @@ +#include "SceTypes.hpp" +#include <cstddef> +#include <cstdlib> +#include <cstring> + +extern "C" { +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(std::size_t size); +int APS5_VABI scePthreadSetDefaultstacksize(std::size_t size); +void APS5_VABI scePthreadSetName(Pthread thread, const char* name); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016); +static constexpr int POSIX_EINVAL = 22; +static constexpr std::size_t INITIAL_DEFAULT = 1u << 20; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::size_t NewAttrStackSize() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == 0); + std::size_t size = 0; + Require(scePthreadAttrGetstacksize(&attr, &size) == 0); + Require(scePthreadAttrDestroy(&attr) == 0); + return size; +} + +static void* APS5_VABI ReportStackSize(void* out) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == 0); + Require(scePthreadAttrGet(scePthreadSelf(), &attr) == 0); + Require(scePthreadAttrGetstacksize(&attr, static_cast<std::size_t*>(out)) == 0); + Require(scePthreadAttrDestroy(&attr) == 0); + return nullptr; +} + +static std::size_t DefaultThreadStackSize() { + std::size_t size = 0; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, ReportStackSize, &size, "stack") == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + return size; +} + +static void CheckDefaultStackSize() { + Require(NewAttrStackSize() == INITIAL_DEFAULT); + Require(pthread_set_defaultstacksize_np_nid_postfix(0) == POSIX_EINVAL); + Require(pthread_set_defaultstacksize_np_nid_postfix(16384) == POSIX_EINVAL); + Require(scePthreadSetDefaultstacksize(16384) == SCE_KERNEL_ERROR_EINVAL); + Require(NewAttrStackSize() == INITIAL_DEFAULT); + + Require(pthread_set_defaultstacksize_np_nid_postfix(16385) == 0); + Require(NewAttrStackSize() == 16385); + + Require(scePthreadSetDefaultstacksize(0x40000) == 0); + Require(NewAttrStackSize() == 0x40000); + Require(DefaultThreadStackSize() == 0x40000); + + Require(scePthreadSetDefaultstacksize(INITIAL_DEFAULT) == 0); + Require(DefaultThreadStackSize() == INITIAL_DEFAULT); +} + +static void CheckSetName() { + char name[32] = {}; + scePthreadSetName(scePthreadSelf(), "renamed main"); + Require(scePthreadGetname(scePthreadSelf(), name) == 0); + Require(std::strcmp(name, "renamed main") == 0); + scePthreadSetName(scePthreadSelf(), nullptr); + Require(scePthreadGetname(scePthreadSelf(), name) == 0); + Require(name[0] == '\0'); +} + +int main() { + CheckDefaultStackSize(); + CheckSetName(); + return 0; +} From 5cca233e989d2ef5fc6a26172a0a7c23ba0db4d1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:16:35 +0300 Subject: [PATCH 0468/1360] docs(debt): record where the libkernel_sys default stack size rule comes from --- docs/dev/debt/libkernel_sys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..48fd4f894e 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -18,3 +18,4 @@ - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it +- [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 From 145acfb254d496e75d87ef5280b97d4be1b8910d Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Wed, 21 Oct 2026 12:25:06 -0500 Subject: [PATCH 0469/1360] test(libSceContentSearch): cover content binder return values and unimplemented application search stubs --- core/libs/tests/GuestContentSearch.cpp | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index 2447383112..58b75f914d 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -15,12 +15,34 @@ int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, st int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId); int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size); int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value); +int APS5_VABI sceContentBinderEnd(void); +int APS5_VABI sceContentBinderGetNumOfDifference(void); +int APS5_VABI sceContentBinderInitialize(void); +int APS5_VABI sceContentBinderIsUpdated(void); +int APS5_VABI sceContentBinderReset(void); +int APS5_VABI sceContentBinderSearchDifference(void); +int APS5_VABI sceContentBinderStart(void); +int APS5_VABI sceContentBinderTerminate(void); +int APS5_VABI sceContentSearchGetApplicationLastUpdateId(void); +int APS5_VABI sceContentSearchGetMyApplicationIndex(void); +int APS5_VABI sceContentSearchGetNumOfApplication(void); +int APS5_VABI sceContentSearchSearchApplication(void); } namespace { void Require(bool value) { if (!value) std::abort(); } +template <typename TAction> +bool ThrowsRuntimeError(TAction action) { + try { + action(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + template <typename TAction> bool Throws(TAction action) { try { @@ -118,4 +140,18 @@ int main() { Require(sceContentSearchTerm() == 0); Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); Require(updateId == -1); + constexpr int binderResult = static_cast<int>(0x809D000F); + Require(sceContentBinderInitialize() == binderResult); + Require(sceContentBinderStart() == binderResult); + Require(sceContentBinderReset() == binderResult); + Require(sceContentBinderIsUpdated() == binderResult); + Require(sceContentBinderGetNumOfDifference() == binderResult); + Require(sceContentBinderSearchDifference() == binderResult); + Require(sceContentBinderEnd() == binderResult); + Require(sceContentBinderTerminate() == binderResult); + + Require(ThrowsRuntimeError([] { sceContentSearchGetApplicationLastUpdateId(); })); + Require(ThrowsRuntimeError([] { sceContentSearchGetMyApplicationIndex(); })); + Require(ThrowsRuntimeError([] { sceContentSearchGetNumOfApplication(); })); + Require(ThrowsRuntimeError([] { sceContentSearchSearchApplication(); })); } From 5b6ae43b26bf53a47164b808fd46805a9e8036ca Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Wed, 21 Oct 2026 12:25:38 -0500 Subject: [PATCH 0470/1360] test(libSceErrorDialog): cover sceErrorDialogOpenDetail and sceErrorDialogOpenWithReport --- core/libs/tests/GuestErrorDialog.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp index 106819b6a8..51779896a1 100644 --- a/core/libs/tests/GuestErrorDialog.cpp +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -9,6 +9,8 @@ int APS5_VABI sceErrorDialogUpdateStatus(void); int APS5_VABI sceErrorDialogGetStatus(void); int APS5_VABI sceErrorDialogClose(void); int APS5_VABI sceErrorDialogTerminate(void); +int APS5_VABI sceErrorDialogOpenDetail(void); +int APS5_VABI sceErrorDialogOpenWithReport(void); } namespace { @@ -20,6 +22,7 @@ constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast<int>(0x80ED0001); constexpr int kErrAlreadyInitialized = static_cast<int>(0x80ED0002); constexpr int kErrParam = static_cast<int>(0x80ED0003); +constexpr int kErrUnexpectedFatal = static_cast<int>(0x80ED0004); constexpr int kErrInvalidState = static_cast<int>(0x80ED0005); struct ErrorDialogParam { @@ -68,4 +71,6 @@ int main() { Require(sceErrorDialogGetStatus() == 0); Require(sceErrorDialogUpdateStatus() == 0); Require(sceErrorDialogClose() == kErrInvalidState); + Require(sceErrorDialogOpenDetail() == kErrUnexpectedFatal); + Require(sceErrorDialogOpenWithReport() == kErrUnexpectedFatal); } From 9c1eeedf6f13e69f739c4a6705d449137612e7f7 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Wed, 21 Oct 2026 12:30:20 -0500 Subject: [PATCH 0471/1360] test(libSceAmpr): cover completion command measurement and buffer base address query --- core/libs/tests/GuestAmpr.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index ca17fc2c23..de1a74b6a3 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -37,6 +37,10 @@ int APS5_VABI sceAmprCommandBufferSetMarkerWithColor(Apr::CommandBufferObject*, std::uint64_t APS5_VABI sceAmprMeasureCommandSizePushMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizePushMarkerWithColor(const char*, std::uint32_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressOnCompletion(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounterOnCompletion(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion(); +void* APS5_VABI sceAmprCommandBufferGetBufferBaseAddress(const Apr::CommandBufferObject*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char*, std::uint32_t); int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); @@ -578,6 +582,10 @@ void TestVersionedCounters() { void TestClearBuffer() { Recorder recorder; + Require(sceAmprMeasureCommandSizeWriteAddressOnCompletion() == sizeof(Apr::WriteAddressCommand)); + Require(sceAmprMeasureCommandSizeWriteCounterOnCompletion() == sizeof(Apr::WriteCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() == sizeof(Apr::WriteKernelEventQueueCommand)); + Require(sceAmprCommandBufferGetBufferBaseAddress(&recorder.buffer) == recorder.buffer.base); Require(sceAmprCommandBufferNop(&recorder.buffer, 1) == 0); Require(recorder.Offset() != 0 && recorder.Commands() == 1); Require(sceAmprCommandBufferClearBuffer(&recorder.buffer) == recorder.memory.data()); From acf96f32d0824b1d361fcc792402446c6a138784 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:18:26 +0200 Subject: [PATCH 0472/1360] fix(libc): fail snprintf and vsnprintf with EOVERFLOW when the size exceeds INT_MAX FreeBSD 11.4 vsnprintf checks the size minus the terminator against INT_MAX before formatting and, when it is larger, sets errno to EOVERFLOW, writes a terminator to the buffer and returns EOF (lib/libc/stdio/vsnprintf.c:61-67). The guest formatted into the buffer and returned the length. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Formatting.cpp | 11 +++++++++++ core/libs/tests/Formatting.cpp | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 07fc5468a8..21541b9b2a 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -7,6 +7,7 @@ #include <cstring> #include <string> #include <cerrno> +#include <climits> #include <cstdlib> #include <memory> #include <climits> @@ -33,6 +34,13 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); namespace { +bool SizeOverflowsInt(char* buffer, size_t size) { + if (size == 0 || size - 1 <= static_cast<size_t>(INT_MAX)) return false; + errno = 84; + if (buffer != nullptr) *buffer = '\0'; + return true; +} + #ifndef _WIN32 bool RequiresGuestFormatting(const char* format) { if (format == nullptr) return false; @@ -402,6 +410,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { } int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { + if (SizeOverflowsInt(buffer, size)) return -1; __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::FormatWindows(buffer, size, format, args); @@ -438,6 +447,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { } int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { + if (SizeOverflowsInt(buffer, size)) return -1; std::va_list args; va_start(args, format); const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); @@ -540,6 +550,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) } int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, VaList* c) { + if (SizeOverflowsInt(str, size)) return -1; #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index f22d7880dd..4f07070e3c 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -3,6 +3,8 @@ #include "prx/libc/include/GuestHeap.hpp" #include <algorithm> #include <array> +#include <cerrno> +#include <climits> #include <cmath> #include <cstdlib> #include <cstring> @@ -101,6 +103,16 @@ static bool CheckWidePrecision() { } if (!correct) return false; char output[32]; + for (const std::size_t oversized : {static_cast<std::size_t>(INT_MAX) + 2, static_cast<std::size_t>(SIZE_MAX)}) { + std::memset(output, 'Z', sizeof(output)); + errno = 0; + Require(snprintf_nid_postfix(output, oversized, "x") == -1 && errno == 84 && output[0] == 0); + std::memset(output, 'Z', sizeof(output)); + errno = 0; + Require(FormatList(output, oversized, "x") == -1 && errno == 84 && output[0] == 0); + } + Require(snprintf_nid_postfix(output, static_cast<std::size_t>(INT_MAX) + 1, "x") == 1 && std::strcmp(output, "x") == 0); + Require(FormatList(output, static_cast<std::size_t>(INT_MAX) + 1, "y") == 1 && std::strcmp(output, "y") == 0); 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); From eecffc924b22213f86992ef4a2a3e247164473f6 Mon Sep 17 00:00:00 2001 From: Superhe11 <yankevichyaroslav@gmail.com> Date: Wed, 21 Oct 2026 20:26:13 +0300 Subject: [PATCH 0473/1360] test(libc): cover snprintf size bounds and formatting side effects --- core/libs/prx/libc/src/Formatting.cpp | 15 +++++----- core/libs/tests/Formatting.cpp | 41 +++++++++++++++++++-------- docs/dev/debt/libc.md | 2 ++ 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 21541b9b2a..2d8db357d3 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -7,7 +7,6 @@ #include <cstring> #include <string> #include <cerrno> -#include <climits> #include <cstdlib> #include <memory> #include <climits> @@ -34,13 +33,6 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); namespace { -bool SizeOverflowsInt(char* buffer, size_t size) { - if (size == 0 || size - 1 <= static_cast<size_t>(INT_MAX)) return false; - errno = 84; - if (buffer != nullptr) *buffer = '\0'; - return true; -} - #ifndef _WIN32 bool RequiresGuestFormatting(const char* format) { if (format == nullptr) return false; @@ -369,6 +361,13 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { return result; } +static bool SizeOverflowsInt(char* buffer, size_t size) { + if (size == 0 || size - 1 <= static_cast<size_t>(INT_MAX)) return false; + errno = 84; + if (buffer != nullptr) *buffer = '\0'; + return true; +} + int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 4f07070e3c..937f348f17 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -3,7 +3,6 @@ #include "prx/libc/include/GuestHeap.hpp" #include <algorithm> #include <array> -#include <cerrno> #include <climits> #include <cmath> #include <cstdlib> @@ -86,6 +85,34 @@ static int APS5_VABI PrintList(const char* format, ...) { return result; } +static void CheckSizeLimits() { + using Formatter = int (APS5_VABI*)(char*, size_t, const char*, ...); + for (const Formatter format : {&snprintf_nid_postfix, &FormatList}) { + char output[8]; + for (const std::size_t oversized : {static_cast<std::size_t>(INT_MAX) + 2, static_cast<std::size_t>(SIZE_MAX)}) { + for (const bool wide : {false, true}) { + std::memset(output, 'Z', sizeof(output)); + int consumed = -1; + *__error_nid_postfix() = 0; + const int result = wide ? format(output, oversized, "%ls%n", u"x", &consumed) + : format(output, oversized, "x%n", &consumed); + Require(result == -1 && *__error_nid_postfix() == 84 && output[0] == 0 && consumed == -1); + Require(std::all_of(output + 1, output + sizeof(output), [](char value) { return value == 'Z'; })); + } + } + for (const std::size_t capacity : {std::size_t{0}, std::size_t{1}, static_cast<std::size_t>(INT_MAX), static_cast<std::size_t>(INT_MAX) + 1}) { + std::memset(output, 'Z', sizeof(output)); + *__error_nid_postfix() = 35; + Require(format(output, capacity, "x") == 1 && *__error_nid_postfix() == 35); + Require(output[0] == (capacity == 0 ? 'Z' : capacity == 1 ? '\0' : 'x')); + const std::size_t written = capacity < 2 ? capacity : 2; + if (capacity > 1) Require(output[1] == 0); + Require(std::all_of(output + written, output + sizeof(output), [](char value) { return value == 'Z'; })); + } + Require(format(nullptr, 0, "x") == 1); + } +} + 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", @@ -103,16 +130,6 @@ static bool CheckWidePrecision() { } if (!correct) return false; char output[32]; - for (const std::size_t oversized : {static_cast<std::size_t>(INT_MAX) + 2, static_cast<std::size_t>(SIZE_MAX)}) { - std::memset(output, 'Z', sizeof(output)); - errno = 0; - Require(snprintf_nid_postfix(output, oversized, "x") == -1 && errno == 84 && output[0] == 0); - std::memset(output, 'Z', sizeof(output)); - errno = 0; - Require(FormatList(output, oversized, "x") == -1 && errno == 84 && output[0] == 0); - } - Require(snprintf_nid_postfix(output, static_cast<std::size_t>(INT_MAX) + 1, "x") == 1 && std::strcmp(output, "x") == 0); - Require(FormatList(output, static_cast<std::size_t>(INT_MAX) + 1, "y") == 1 && std::strcmp(output, "y") == 0); 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); @@ -380,4 +397,4 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +int main() { CheckSizeLimits(); RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..66033be89c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,8 @@ ### Unknown function info +- [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) reject sizes above `INT_MAX + 1` with `EOVERFLOW` (84), a terminating NUL and return value -1, following FreeBSD 11.4 [snprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/snprintf.c) and [vsnprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/vsnprintf.c). This limit is unverified on the console. A null buffer at an oversized nonzero size returns the error without writing, where FreeBSD dereferences it. + - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw From 048b67c1c48e0e72b95e070d1eb6484ca14c3156 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 19:43:24 +0200 Subject: [PATCH 0474/1360] fix(libSceNgs2.native): reset the system option to the module's defaults, 8 voice channels included --- .../libs/prx/libSceNgs2.native/src/System.cpp | 3 ++- .../tests/Ngs2HandleErrors.cpp | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 99eede7a7c..2fe15a7311 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -212,6 +212,7 @@ static Ngs2SystemOption DefaultSystemOption() { option.max_grain_samples = 512; option.num_grain_samples = 256; option.sample_rate = 48000; + option.max_voice_channels = 8; return option; } @@ -422,7 +423,7 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe } int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption* option) { - if (option == nullptr) APS5_INVALID_ARG_EX; + if (option == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *option = DefaultSystemOption(); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index 56246ca178..c9a0094eda 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -1,5 +1,7 @@ #include "Ngs2Test.hpp" +#include <cstdint> +#include <cstring> #include <iostream> #include <stdexcept> @@ -7,6 +9,28 @@ static void Status(int actual, int expected, const char* message) { if (actual != expected) throw std::runtime_error(message); } +static void CheckResetOption() { + Status(sceNgs2SystemResetOption(nullptr), static_cast<int>(0x804A8010u), "null system option did not return the invalid out address error"); + constexpr std::uint64_t size = 0x90; + constexpr std::uint32_t maxGrainSamples = 512; + constexpr std::uint32_t numGrainSamples = 256; + constexpr std::uint32_t sampleRate = 48000; + constexpr std::uint32_t maxVoiceChannels = 8; + unsigned char expected[0x90] = {}; + std::memcpy(expected + 0x00, &size, sizeof(size)); + std::memcpy(expected + 0x6c, &maxGrainSamples, sizeof(maxGrainSamples)); + std::memcpy(expected + 0x70, &numGrainSamples, sizeof(numGrainSamples)); + std::memcpy(expected + 0x74, &sampleRate, sizeof(sampleRate)); + std::memcpy(expected + 0x78, &maxVoiceChannels, sizeof(maxVoiceChannels)); + static_assert(sizeof(Ngs2SystemOption) == sizeof(expected)); + Ngs2SystemOption option; + std::memset(&option, 0xAB, sizeof(option)); + Status(sceNgs2SystemResetOption(&option), SCE_NGS2_OK, "system option reset failed"); + Require(std::memcmp(&option, expected, sizeof(expected)) == 0); + Ngs2ContextBufferInfo query{}; + Status(sceNgs2SystemQueryBufferSize(&option, &query), SCE_NGS2_OK, "reset system option was rejected"); +} + int main() { try { constexpr int systemError = static_cast<int>(0x804A8201u); @@ -39,6 +63,7 @@ int main() { Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); Require(value == sentinel); + CheckResetOption(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; return 0; } catch (const std::exception& error) { From be51ab7bae405612f27bd0eb6e56c4dedb4209b1 Mon Sep 17 00:00:00 2001 From: mossyanwar <moezanwar1505@gmail.com> Date: Wed, 21 Oct 2026 16:49:04 +0100 Subject: [PATCH 0475/1360] fix(relinker): resolve symbol-name imports through NIDs on Linux Homebrew built with the ps5-payload-dev SDK names its system libraries with .sprx and imports by symbol name, while the libraries in libs/ are .prx files that export NIDs. On Linux such an executable converted but stopped at load time on the first .sprx dependency and, once that was found, on the first symbol name. The Linux patcher now loads a dependency named with .sprx, .suprx or .debug_prx as the .prx library of the same stem. In an executable that names such a dependency, an import without a module that is named by symbol gets the NID of that name. A name that a bundled module exports still binds to that module. There is no lookup by the name as written, because the host loader also searches the host C library. Other executables keep their unqualified imports as written. --- core/relinker/CMakeLists.txt | 8 ++ core/relinker/domain/include/domain/Types.hpp | 1 + .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 46 +++++++++- .../src/output/SysVDynamicSectionBuilder.cpp | 6 ++ .../tests/test_guest_module_identity.py | 4 +- .../tests/test_guest_needed_modules.py | 2 +- .../tests/test_guest_needed_nested_modules.py | 2 +- .../tests/test_linux_named_imports.py | 84 +++++++++++++++++++ docs/dev/debt/relinker.md | 3 +- 9 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 core/relinker/relinker/tests/test_linux_named_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa78fbaa42..9b9c265029 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -369,12 +369,15 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/StubBodyBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) target_include_directories(linux_load_pages_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include ) add_test(NAME linux_load_pages COMMAND linux_load_pages_tests) set_tests_properties(linux_load_pages PROPERTIES TIMEOUT 20) @@ -409,12 +412,15 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) target_include_directories(amd64_only_converter_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include ) if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR (NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))) target_link_options(amd64_only_converter_tests PRIVATE "LINKER:-pagezero_size,0x10000") @@ -595,6 +601,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $<TARGET_FILE:relinker>) add_test(NAME windows_named_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_named_imports.py $<TARGET_FILE:relinker>) + add_test(NAME linux_named_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_named_imports.py $<TARGET_FILE:relinker>) add_library(guest_module_args_host SHARED elfpatcher/tests/GuestModuleArgsHost.cpp) target_include_directories(guest_module_args_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) set_target_properties(guest_module_args_host PROPERTIES diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index dd181853e0..6757f8507a 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -106,6 +106,7 @@ struct SysVDynamicSection { std::vector<GuestRuntime> GuestModules; std::map<VirtualAddress, std::string> ImportModules; std::set<VirtualAddress> FunctionImports; + std::set<VirtualAddress> NamedImports; std::set<std::string> UnmatchedImportModules; std::vector<std::string> NeededLibraries; }; diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index df116a307c..7453d82aca 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -4,10 +4,13 @@ #include <elfpatcher/general/SectionHeaderTableRequest.hpp> #include <codegen/x86/Amd64OnlySubstitutionTable.hpp> #include <codegen/x86/StubBodyBuilder.hpp> +#include <domain/ImportModule.hpp> #include <io/BufferUtils.hpp> +#include <nid/NidCompute.hpp> #include <algorithm> #include <limits> #include <map> +#include <set> #include <span> #include <sstream> #include <string> @@ -190,6 +193,46 @@ std::vector<TracedImport> TracedImports(const Domain::SysVDynamicSection& dynSec return imports; } +Domain::SysVDynamicSection ImportsByNid(Domain::SysVDynamicSection dynSection) { + RequireWholeEntries(dynSection.DynamicSegmentData, kDynEntrySize, "Executable dependency table"); + for (std::size_t entry = 0; entry < dynSection.DynamicSegmentData.size(); entry += kDynEntrySize) { + if (static_cast<std::int64_t>(Io::ReadU64(dynSection.DynamicSegmentData, entry)) != DT_NEEDED) + continue; + const auto offset = Io::ReadU64(dynSection.DynamicSegmentData, entry + 8); + if (offset >= dynSection.DynStrData.size()) + throw Domain::RelinkerException("DT_NEEDED string offset is out of bounds", offset); + const auto name = ReadDynamicString(dynSection.DynStrData, static_cast<std::uint32_t>(offset)); + const auto library = Domain::NeededLibraryName(name); + if (library == name) + continue; + Io::WriteU64(dynSection.DynamicSegmentData, entry + 8, dynSection.DynStrData.size()); + Io::AppendString(dynSection.DynStrData, library); + } + for (auto& library : dynSection.NeededLibraries) + library = Domain::NeededLibraryName(library); + + std::set<std::uint64_t> symbols; + for (const auto* table : {&dynSection.RelaData, &dynSection.RelaPltData}) { + RequireWholeEntries(*table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table->size(); entry += kRelaEntrySize) + if (dynSection.NamedImports.contains(Io::ReadU64(*table, entry))) + symbols.insert(Io::ReadU64(*table, entry + 8) >> 32); + } + for (const auto symbol : symbols) { + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", symbol); + const auto symbolOffset = static_cast<std::size_t>(symbol * kSymEntrySize); + const auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty() || name.find('#') != std::string::npos) + continue; + if (dynSection.DynStrData.size() > std::numeric_limits<std::uint32_t>::max()) + throw Domain::RelinkerException("Dynamic string table too large"); + Io::WriteU32(dynSection.DynSymData, symbolOffset, static_cast<std::uint32_t>(dynSection.DynStrData.size())); + Io::AppendString(dynSection.DynStrData, Nid::ComputeNid(name, "")); + } + return dynSection; +} + std::vector<std::uint8_t> TracePrologue(const std::uint64_t vaddr, const std::uint64_t table, const std::uint64_t tableEnd, const std::uint64_t function) { std::vector<std::uint8_t> code = {0x52, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; const auto rip = [&](std::initializer_list<std::uint8_t> opcode, const std::uint64_t target) { @@ -297,13 +340,14 @@ void LinuxElfPatcher::_appendTrampoline( std::vector<std::uint8_t> LinuxElfPatcher::Patch( std::vector<std::uint8_t> sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, - const Domain::SysVDynamicSection& dynSection, + const Domain::SysVDynamicSection& sourceDynSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector<Codegen::TrampolineSite>& trampolines) { + const auto dynSection = ImportsByNid(sourceDynSection); if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 0d8bdd763f..a0044b4765 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -1,5 +1,7 @@ #include <relinker/output/SysVDynamicSectionBuilder.hpp> #include <elfpatcher/general/ElfConstants.hpp> +#include <domain/ImportModule.hpp> +#include <algorithm> #include <cstring> namespace Relinker { @@ -77,6 +79,9 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( _appendElfSym(result.DynSymData, 0, 0, 0, 0, 0, 0); + const bool consoleFileNames = std::any_of(neededLibraries.begin(), neededLibraries.end(), + [](const std::string& name) { return Domain::NeededLibraryName(name) != name; }); + auto stripHashSuffix = [](const std::string& value) -> std::string { const auto hashPos = value.find('#'); if (hashPos == std::string::npos) return value; @@ -98,6 +103,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const auto& ref : nidReferences) { if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); if (ref.Function) result.FunctionImports.insert(ref.RelocationAddress); + if (consoleFileNames && ref.Library.empty() && !ref.Nid.empty() && ref.Nid.find('#') == std::string::npos) result.NamedImports.insert(ref.RelocationAddress); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index 68e60adc56..ec440ea2f8 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -98,7 +98,7 @@ def main(): assert not artifact.exists(), artifact assert len(list((output.parent / 'app0').rglob('*.guest.prx'))) == 1 if not windows: - assert 'libGuest.suprx' in needed_libraries(output.read_bytes()) + assert 'libGuest.prx' in needed_libraries(output.read_bytes()) result, output = convert(f'outside-ambiguous-{windows}', windows, 'libGuest.suprx', module_directory='.', providers={'one.prx': declared_provider(22, ('libGuest',))}, @@ -187,7 +187,7 @@ def main(): if windows: run(output, error='Failed to load module:') else: - assert request in needed_libraries(output.read_bytes()) + assert request.replace('.suprx', '.prx') in needed_libraries(output.read_bytes()) providers = {'wrong.prx': declared_provider(11, ('other',)), 'right.prx': declared_provider(22, ('libGuest',))} diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 6b2b18f2c8..6388feb414 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -186,7 +186,7 @@ def main(): assert output.exists(), output assert not list((case / "app0").rglob("*.guest.prx")) if not windows: - assert needed_libraries(output.read_bytes()) == ["needed.debug_prx" if "debug-name" in case.name else "needed.prx"] + assert needed_libraries(output.read_bytes()) == ["needed.prx"] case = work / f"{windows}-ambiguous" for name in ("first", "second"): diff --git a/core/relinker/relinker/tests/test_guest_needed_nested_modules.py b/core/relinker/relinker/tests/test_guest_needed_nested_modules.py index 4b226cff99..7dac9fb615 100644 --- a/core/relinker/relinker/tests/test_guest_needed_nested_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_nested_modules.py @@ -62,7 +62,7 @@ def main(): assert output.is_file() assert not list((output.parent / 'app0').rglob('*.guest.prx')) if not windows: - assert needed_libraries(output.read_bytes()) == ['libGuest.suprx'] + assert needed_libraries(output.read_bytes()) == ['libGuest.prx'] result, output = convert(f'{label}-{identity}-excluded', windows, 'libGuest.suprx', files, (filename,)) assert result.returncode == 2 and 'Excluded guest module file not found' in result.stderr, result.stderr assert not output.exists() diff --git a/core/relinker/relinker/tests/test_linux_named_imports.py b/core/relinker/relinker/tests/test_linux_named_imports.py new file mode 100644 index 0000000000..ca23674315 --- /dev/null +++ b/core/relinker/relinker/tests/test_linux_named_imports.py @@ -0,0 +1,84 @@ +import os +from pathlib import Path +import platform +import subprocess +import sys +import tempfile + +sys.path.insert(0, str(Path(__file__).resolve().parent / "macos")) + +from guest_import_fixtures import executable +from nid import nid +from test_guest_module_directories import module_with_symbol, needed_libraries +from test_guest_symbol_names import imported_symbols + + +def library(work, name, symbol, value): + source = work / (name + ".c") + source.write_text("#include <unistd.h>\n" + f"int exported(void) __asm__(\"\\\"{symbol}\\\"\");\n" + f"int exported(void) {{ _exit({value}); }}\n") + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def convert(relinker, case, image, module=None): + case.mkdir() + if module is not None: + (case / "sce_module").mkdir() + (case / "sce_module" / "bundled.prx").write_bytes(module) + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + options = [] if module is not None else ["--skip-sce-module"] + result = subprocess.run([str(relinker), *options, str(source), str(output)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (case.name, result.stdout, result.stderr) + return output + + +def run(output, library): + (output.parent / "libs").mkdir() + (output.parent / "libs" / "available.prx").write_bytes(library) + output.chmod(0o755) + return subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-named-imports-") as directory: + work = Path(directory) + cases = { + "sprx": convert(relinker, work / "sprx", executable("available.sprx", "shared")), + "prx": convert(relinker, work / "prx", executable("available.prx", "shared")), + "qualified": convert(relinker, work / "qualified", executable("available.sprx", "shared#A#B")), + "name": convert(relinker, work / "name", executable("available.sprx", "shared")), + "guest": convert(relinker, work / "guest", executable("bundled.prx", "shared", extra_dependencies=("available.sprx",)), + module_with_symbol(True)), + } + image = cases["sprx"].read_bytes() + assert imported_symbols(image) == {nid("shared")}, imported_symbols(image) + assert needed_libraries(image) == ["available.prx"], needed_libraries(image) + assert imported_symbols(cases["prx"].read_bytes()) == {"shared"} + assert imported_symbols(cases["qualified"].read_bytes()) == {"shared"} + image = cases["guest"].read_bytes() + assert imported_symbols(image) == {"shared#guest"}, imported_symbols(image) + assert "available.prx" in needed_libraries(image), needed_libraries(image) + if not runnable: + return + by_nid = library(work, "by-nid", nid("shared"), 22) + by_name = library(work, "by-name", "shared", 44) + result = run(cases["sprx"], by_nid) + assert result.returncode == 22, (result.returncode, result.stdout, result.stderr) + result = run(cases["prx"], by_name) + assert result.returncode == 44, (result.returncode, result.stdout, result.stderr) + result = run(cases["qualified"], by_nid) + assert result.returncode != 0 and "undefined symbol: shared" in result.stderr, (result.returncode, result.stderr) + result = run(cases["name"], by_name) + assert result.returncode != 0 and f"undefined symbol: {nid('shared')}" in result.stderr, (result.returncode, result.stderr) + print("Linux named import tests passed") + + +if __name__ == "__main__": + main() diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 43ff7062d0..cfa6fb6665 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -12,7 +12,8 @@ - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. -- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. Linux and macOS resolve these imports through the host loader by name and are unchanged. +- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. +- [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables, and imports of bundled modules, are still bound by name. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by <appro@openssl.org>" in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. From ab4be13c94822ac6f4da18c2fcdfad8d1a0db35c Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:59:04 +0000 Subject: [PATCH 0476/1360] feat(libSceLibreSsl): implement the MD4, MD5, SHA-1 and SHA-2 digests MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and the SHA256 one-shot threw. They now hash as LibreSSL does, on the caller's MD4_CTX, MD5_CTX, SHA_CTX, SHA256_CTX and SHA512_CTX laid out as in LibreSSL's md4.h, md5.h and sha.h, so a context copied or inspected by the title stays valid: the bit count in Nl/Nh, the partial block in data with its length in num, and md_len selecting the truncated digest. Final pads and clears the buffer as LibreSSL's sources do. Cases that differ between LibreSSL versions throw: SHA256 with a null digest buffer and SHA512_Final with the SHA-512/224 or SHA-512/256 length. The test checks the RFC 1320, RFC 1321 and FIPS 180 vectors over several chunk sizes, the context fields, copied contexts and those cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 16 + core/libs/prx/libSceLibreSsl/Export.cpp | 110 ---- core/libs/prx/libSceLibreSsl/src/Digest.cpp | 549 ++++++++++++++++++++ core/libs/tests/GuestLibreSslDigest.cpp | 195 +++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 762 insertions(+), 111 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/CMakeLists.txt create mode 100644 core/libs/prx/libSceLibreSsl/src/Digest.cpp create mode 100644 core/libs/tests/GuestLibreSslDigest.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt new file mode 100644 index 0000000000..d9d266270c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLibreSsl) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDigest.cpp) + target_include_directories(guest_libressl_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_digest_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_digest_tests) + add_test(NAME guest_libressl_digest COMMAND guest_libressl_digest_tests) + set_tests_properties(guest_libressl_digest PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 93b05e0fec..670bb8911e 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -846,36 +846,6 @@ int APS5_VABI HMAC_Update_nid_postfix(void) { return 0; } -int APS5_VABI MD4_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD4_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD4_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OBJ_cleanup_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1116,86 +1086,6 @@ int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { return 0; } -int APS5_VABI SHA1_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA1_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA1_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Digest.cpp b/core/libs/prx/libSceLibreSsl/src/Digest.cpp new file mode 100644 index 0000000000..e58974597e --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Digest.cpp @@ -0,0 +1,549 @@ +#include <bit> +#include <cstddef> +#include <cstdint> +#include <cstring> +#include <stdexcept> +#include <string> +#include "prx/libc/include/General.hpp" + +namespace { +struct Md4Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Md5Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha1Context { + std::uint32_t h[5]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha256Context { + std::uint32_t h[8]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num, mdLength; +}; + +struct Sha512Context { + std::uint64_t h[8]; + std::uint64_t Nl, Nh; + std::uint64_t data[16]; + std::uint32_t num, mdLength; +}; + +static_assert(sizeof(Md4Context) == 92); +static_assert(sizeof(Md5Context) == 92); +static_assert(sizeof(Sha1Context) == 96); +static_assert(sizeof(Sha256Context) == 112); +static_assert(sizeof(Sha512Context) == 216); + +constexpr std::size_t Sha224Length = 28; +constexpr std::size_t Sha256Length = 32; +constexpr std::size_t Sha384Length = 48; +constexpr std::size_t Sha512Length = 64; + +std::uint32_t LoadLittle32(const unsigned char* p) { + return std::uint32_t(p[0]) | std::uint32_t(p[1]) << 8 | std::uint32_t(p[2]) << 16 | std::uint32_t(p[3]) << 24; +} + +std::uint32_t LoadBig32(const unsigned char* p) { + return std::uint32_t(p[0]) << 24 | std::uint32_t(p[1]) << 16 | std::uint32_t(p[2]) << 8 | std::uint32_t(p[3]); +} + +std::uint64_t LoadBig64(const unsigned char* p) { + return std::uint64_t(LoadBig32(p)) << 32 | LoadBig32(p + 4); +} + +void StoreLittle32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast<unsigned char>(value >> (8 * i)); +} + +void StoreBig32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast<unsigned char>(value >> (24 - 8 * i)); +} + +void StoreBig64(unsigned char* p, std::uint64_t value) { + StoreBig32(p, static_cast<std::uint32_t>(value >> 32)); + StoreBig32(p + 4, static_cast<std::uint32_t>(value)); +} + +void Md4Block(Md4Context& c, const unsigned char* block) { + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + const auto f = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (~x1 & z); }; + const auto g = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (x1 & z) | (y & z); }; + const auto h = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return x1 ^ y ^ z; }; + static constexpr int shifts1[4] {3, 7, 11, 19}; + static constexpr int shifts2[4] {3, 5, 9, 13}; + static constexpr int shifts3[4] {3, 9, 11, 15}; + static constexpr int order2[16] {0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}; + static constexpr int order3[16] {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}; + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + f(b, cc, d) + x[i], shifts1[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + g(b, cc, d) + x[order2[i]] + 0x5a827999u, shifts2[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + h(b, cc, d) + x[order3[i]] + 0x6ed9eba1u, shifts3[i % 4]); + a = d; d = cc; cc = b; b = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Md5Block(Md5Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static constexpr int shifts[16] {7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + for (int i = 0; i < 64; ++i) { + std::uint32_t mix; + int index; + switch (i / 16) { + case 0: mix = (b & cc) | (~b & d); index = i; break; + case 1: mix = (d & b) | (~d & cc); index = (5 * i + 1) % 16; break; + case 2: mix = b ^ cc ^ d; index = (3 * i + 5) % 16; break; + default: mix = cc ^ (b | ~d); index = (7 * i) % 16; break; + } + const auto t = d; + d = cc; + cc = b; + b += std::rotl(a + mix + constants[i] + x[index], shifts[(i / 16) * 4 + i % 4]); + a = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Sha1Block(Sha1Context& c, const unsigned char* block) { + std::uint32_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 80; ++i) w[i] = std::rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + std::uint32_t a = c.h[0], b = c.h[1], cc = c.h[2], d = c.h[3], e = c.h[4]; + for (int i = 0; i < 80; ++i) { + std::uint32_t f, k; + if (i < 20) { f = (b & cc) | (~b & d); k = 0x5a827999; } + else if (i < 40) { f = b ^ cc ^ d; k = 0x6ed9eba1; } + else if (i < 60) { f = (b & cc) | (b & d) | (cc & d); k = 0x8f1bbcdc; } + else { f = b ^ cc ^ d; k = 0xca62c1d6; } + const auto t = std::rotl(a, 5) + f + e + k + w[i]; + e = d; d = cc; cc = std::rotl(b, 30); b = a; a = t; + } + c.h[0] += a; c.h[1] += b; c.h[2] += cc; c.h[3] += d; c.h[4] += e; +} + +void Sha256Block(Sha256Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + std::uint32_t w[64]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 64; ++i) { + const auto s0 = std::rotr(w[i - 15], 7) ^ std::rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const auto s1 = std::rotr(w[i - 2], 17) ^ std::rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint32_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 64; ++i) { + const auto s1 = std::rotr(v[4], 6) ^ std::rotr(v[4], 11) ^ std::rotr(v[4], 25); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 2) ^ std::rotr(v[0], 13) ^ std::rotr(v[0], 22); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +void Sha512Block(Sha512Context& c, const unsigned char* block) { + static constexpr std::uint64_t constants[80] { + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, + 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, + 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, + 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, + 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, + 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, + 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, + 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, + 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, + }; + std::uint64_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig64(block + 8 * i); + for (int i = 16; i < 80; ++i) { + const auto s0 = std::rotr(w[i - 15], 1) ^ std::rotr(w[i - 15], 8) ^ (w[i - 15] >> 7); + const auto s1 = std::rotr(w[i - 2], 19) ^ std::rotr(w[i - 2], 61) ^ (w[i - 2] >> 6); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint64_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 80; ++i) { + const auto s1 = std::rotr(v[4], 14) ^ std::rotr(v[4], 18) ^ std::rotr(v[4], 41); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 28) ^ std::rotr(v[0], 34) ^ std::rotr(v[0], 39); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +template<class TContext> TContext& RequireContext(TContext* context, const char* function) { + if (!context) throw std::invalid_argument(std::string(function) + ": null context"); + return *context; +} + +void RequireInput(const void* data, std::size_t length, const char* function) { + if (!data && length) throw std::invalid_argument(std::string(function) + ": null input"); +} + +void RequireDigest(const unsigned char* digest, const char* function) { + if (!digest) throw std::invalid_argument(std::string(function) + ": null digest buffer"); +} + +void RequireBuffered(std::uint32_t buffered, std::size_t block, const char* function) { + if (buffered >= block) throw std::invalid_argument(std::string(function) + ": buffered byte count is not below the block size"); +} + +template<class TContext, class TBlock> void Update32(TContext& c, const void* input, std::size_t length, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast<unsigned char*>(c.data); + auto* data = static_cast<const unsigned char*>(input); + const std::uint64_t bits = (std::uint64_t(c.Nh) << 32 | c.Nl) + (std::uint64_t(length) << 3); + c.Nl = static_cast<std::uint32_t>(bits); + c.Nh = static_cast<std::uint32_t>(bits >> 32); + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast<std::uint32_t>(length); + return; + } + std::memcpy(buffer + c.num, data, room); + block(c, buffer); + data += room; + length -= room; + c.num = 0; + std::memset(buffer, 0, blockSize); + } + for (; length >= blockSize; data += blockSize, length -= blockSize) block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast<std::uint32_t>(length); + } +} + +template<class TContext, class TBlock> void Final32(TContext& c, bool bigEndian, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast<unsigned char*>(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 8) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 8 - n); + if (bigEndian) { + StoreBig32(buffer + 56, c.Nh); + StoreBig32(buffer + 60, c.Nl); + } else { + StoreLittle32(buffer + 56, c.Nl); + StoreLittle32(buffer + 60, c.Nh); + } + block(c, buffer); + c.num = 0; + std::memset(buffer, 0, blockSize); +} + +void Sha512Update(Sha512Context& c, const void* input, std::size_t length) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast<unsigned char*>(c.data); + auto* data = static_cast<const unsigned char*>(input); + const std::uint64_t low = c.Nl + (std::uint64_t(length) << 3); + if (low < c.Nl) ++c.Nh; + c.Nh += std::uint64_t(length) >> 61; + c.Nl = low; + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast<std::uint32_t>(length); + return; + } + std::memcpy(buffer + c.num, data, room); + Sha512Block(c, buffer); + data += room; + length -= room; + c.num = 0; + } + for (; length >= blockSize; data += blockSize, length -= blockSize) Sha512Block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast<std::uint32_t>(length); + } +} + +int Sha512Final(unsigned char* digest, Sha512Context& c) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast<unsigned char*>(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 16) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + Sha512Block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 16 - n); + StoreBig64(buffer + 112, c.Nh); + StoreBig64(buffer + 120, c.Nl); + Sha512Block(c, buffer); + if (!digest) return 0; + if (c.mdLength == Sha224Length || c.mdLength == Sha256Length) + NotImplemented_nid_no_patch("SHA512_Final: SHA-512/224 and SHA-512/256 digest lengths"); + if (c.mdLength != Sha384Length && c.mdLength != Sha512Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 8; ++i) StoreBig64(digest + 8 * i, c.h[i]); + return 1; +} + +int Sha256Final(unsigned char* digest, Sha256Context& c, const char* function) { + RequireDigest(digest, function); + RequireBuffered(c.num, 64, function); + Final32(c, true, Sha256Block); + if (c.mdLength > Sha256Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 4; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} +} + +extern "C" { + +int APS5_VABI MD4_Init_nid_postfix(Md4Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD4_Update_nid_postfix(Md4Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md4Block); + return 1; +} + +int APS5_VABI MD4_Final_nid_postfix(unsigned char* digest, Md4Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md4Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI MD5_Init_nid_postfix(Md5Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD5_Update_nid_postfix(Md5Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md5Block); + return 1; +} + +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, Md5Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md5Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI SHA1_Init_nid_postfix(Sha1Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.h[0] = 0x67452301; + c.h[1] = 0xefcdab89; + c.h[2] = 0x98badcfe; + c.h[3] = 0x10325476; + c.h[4] = 0xc3d2e1f0; + return 1; +} + +int APS5_VABI SHA1_Update_nid_postfix(Sha1Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha1Block); + return 1; +} + +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, Sha1Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, true, Sha1Block); + for (int i = 0; i < 5; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} + +int APS5_VABI SHA224_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha224Length; + return 1; +} + +int APS5_VABI SHA256_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha256Length; + return 1; +} + +int APS5_VABI SHA256_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha256Block); + return 1; +} + +int APS5_VABI SHA224_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + return SHA256_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA256_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +int APS5_VABI SHA224_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + RequireInput(data, length, __func__); + if (!digest) NotImplemented_nid_no_patch("SHA256: null digest buffer"); + Sha256Context c; + SHA256_Init_nid_postfix(&c); + if (length) Update32(c, data, length, Sha256Block); + Sha256Final(digest, c, __func__); + return digest; +} + +int APS5_VABI SHA384_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, + 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha384Length; + return 1; +} + +int APS5_VABI SHA512_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha512Length; + return 1; +} + +int APS5_VABI SHA512_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 128, __func__); + Sha512Update(c, data, length); + return 1; +} + +int APS5_VABI SHA384_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + return SHA512_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA512_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + auto& c = RequireContext(context, __func__); + RequireBuffered(c.num, 128, __func__); + return Sha512Final(digest, c); +} + +int APS5_VABI SHA384_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + return SHA512_Final_nid_postfix(digest, context); +} + +} diff --git a/core/libs/tests/GuestLibreSslDigest.cpp b/core/libs/tests/GuestLibreSslDigest.cpp new file mode 100644 index 0000000000..8b9ba6183b --- /dev/null +++ b/core/libs/tests/GuestLibreSslDigest.cpp @@ -0,0 +1,195 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <stdexcept> +#include <string> +#include <vector> + +extern "C" { +int APS5_VABI MD4_Init_nid_postfix(void*); +int APS5_VABI MD4_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD4_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI MD5_Init_nid_postfix(void*); +int APS5_VABI MD5_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD5_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA1_Init_nid_postfix(void*); +int APS5_VABI SHA1_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA224_Init_nid_postfix(void*); +int APS5_VABI SHA224_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA224_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA256_Init_nid_postfix(void*); +int APS5_VABI SHA256_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA256_Final_nid_postfix(unsigned char*, void*); +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +int APS5_VABI SHA384_Init_nid_postfix(void*); +int APS5_VABI SHA384_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA384_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA512_Init_nid_postfix(void*); +int APS5_VABI SHA512_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA512_Final_nid_postfix(unsigned char*, void*); +} + +using InitFunction = int (APS5_VABI*)(void*); +using UpdateFunction = int (APS5_VABI*)(void*, const void*, std::size_t); +using FinalFunction = int (APS5_VABI*)(unsigned char*, void*); + +struct Algorithm { + const char* name; + InitFunction init; + UpdateFunction update; + FinalFunction final; + std::size_t contextSize; + std::size_t blockSize; + std::size_t digestSize; + const char* expected[6]; +}; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +template<typename TAction> static void RequireThrows(TAction action, const char* what) { + try { action(); } catch (const std::exception&) { return; } + Require(false, what); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +static std::string Digest(const Algorithm& algorithm, const std::string& message, std::size_t chunk) { + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(algorithm.init(context) == 1, "init returns 1"); + for (std::size_t offset = 0; offset < message.size(); offset += chunk) { + const auto length = std::min(chunk, message.size() - offset); + Require(algorithm.update(context, message.data() + offset, length) == 1, "update returns 1"); + } + Require(algorithm.final(digest, context) == 1, "final returns 1"); + return Hex(digest, algorithm.digestSize); +} + +int main() { + const std::string messages[6] { + "", + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + std::string(1000000, 'a'), + }; + const Algorithm algorithms[] { + {"MD4", MD4_Init_nid_postfix, MD4_Update_nid_postfix, MD4_Final_nid_postfix, 92, 64, 16, { + "31d6cfe0d16ae931b73c59d7e0c089c0", "a448017aaf21d8525fc10ae87aa6729d", "4691a9ec81b1a6bd1ab8557240b245c5", + nullptr, "e33b4ddc9c38f2199c3e7b164fcc0536", nullptr}}, + {"MD5", MD5_Init_nid_postfix, MD5_Update_nid_postfix, MD5_Final_nid_postfix, 92, 64, 16, { + "d41d8cd98f00b204e9800998ecf8427e", "900150983cd24fb0d6963f7d28e17f72", "8215ef0796a20bcaaae116d3876c664a", + "03dd8807a93175fb062dfb55dc7d359c", "57edf4a22be3c955ac49da2e2107b67a", "7707d6ae4e027c70eea2a935c2296f21"}}, + {"SHA1", SHA1_Init_nid_postfix, SHA1_Update_nid_postfix, SHA1_Final_nid_postfix, 96, 64, 20, { + "da39a3ee5e6b4b0d3255bfef95601890afd80709", "a9993e364706816aba3e25717850c26c9cd0d89d", "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "a49b2446a02c645bf419f995b67091253a04a259", "50abf5706a150990a08b2c5ea40fa0e585554732", "34aa973cd4c4daa4f61eeb2bdbad27316534016f"}}, + {"SHA224", SHA224_Init_nid_postfix, SHA224_Update_nid_postfix, SHA224_Final_nid_postfix, 112, 64, 28, { + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", "23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7", + "75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525", "c97ca9a559850ce97a04a96def6d99a9e0e0e2ab14e6b8df265fc0b3", + "b50aecbe4e9bb0b57bc5f3ae760a8e01db24f203fb3cdcd13148046e", "20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67"}}, + {"SHA256", SHA256_Init_nid_postfix, SHA256_Update_nid_postfix, SHA256_Final_nid_postfix, 112, 64, 32, { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1", + "f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e", "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"}}, + {"SHA384", SHA384_Init_nid_postfix, SHA384_Update_nid_postfix, SHA384_Final_nid_postfix, 216, 128, 48, { + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + "3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b", + "09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039", + "b12932b0627d1c060942f5447764155655bd4da0c9afa6dd9b9ef53129af1b8fb0195996d2de9ca0df9d821ffee67026", + "9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985"}}, + {"SHA512", SHA512_Init_nid_postfix, SHA512_Update_nid_postfix, SHA512_Final_nid_postfix, 216, 128, 64, { + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445", + "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909", + "72ec1ef1124a45b047e8b7c75a932195135bb61de24ec0d1914042246e0aec3a2354e093d76f3048b456764346900cb130d2a4fd5dd16abb5e30bcb850dee843", + "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b"}}, + }; + + for (const auto& algorithm : algorithms) { + for (int i = 0; i < 6; ++i) { + if (!algorithm.expected[i]) continue; + const auto& message = messages[i]; + const std::size_t chunks[] {message.size() ? message.size() : 1, 1, algorithm.blockSize - 1, algorithm.blockSize, algorithm.blockSize + 1, 1000}; + for (auto chunk : chunks) { + if (i == 5 && chunk < 64) continue; + Require(Digest(algorithm, message, chunk) == algorithm.expected[i], algorithm.name); + } + } + + alignas(16) unsigned char context[256] {}; + alignas(16) unsigned char copy[256] {}; + unsigned char digest[64] {}; + unsigned char copyDigest[64] {}; + const bool wide = algorithm.blockSize == 128; + const auto num = [&](const unsigned char* c) { + std::uint32_t value; + std::memcpy(&value, c + (wide ? 208 : (algorithm.contextSize == 112 ? 104 : algorithm.contextSize - 4)), 4); + return value; + }; + const auto bitsLow = [&](const unsigned char* c) { + const std::size_t offset = wide ? 64 : (algorithm.contextSize == 96 ? 20 : algorithm.contextSize == 112 ? 32 : 16); + std::uint64_t value = 0; + std::memcpy(&value, c + offset, wide ? 8 : 4); + return value; + }; + Require(algorithm.init(context) == 1, "init"); + Require(algorithm.update(context, nullptr, 0) == 1, "empty update with null data"); + Require(algorithm.update(context, "ab", 2) == 1, "partial update"); + Require(num(context) == 2 && bitsLow(context) == 16, "context records the buffered bytes and the bit count"); + std::memcpy(copy, context, sizeof(copy)); + Require(algorithm.update(context, "c", 1) == 1 && algorithm.update(copy, "c", 1) == 1, "update of a copied context"); + Require(algorithm.final(digest, context) == 1 && algorithm.final(copyDigest, copy) == 1, "final of a copied context"); + Require(Hex(digest, algorithm.digestSize) == algorithm.expected[1] && !std::memcmp(digest, copyDigest, sizeof(digest)), "copied context digest"); + if (!wide) Require(num(context) == 0, "final clears the buffered count"); + RequireThrows([&] { algorithm.init(nullptr); }, "null context"); + RequireThrows([&] { algorithm.update(context, nullptr, 1); }, "null data"); + if (!wide) RequireThrows([&] { algorithm.final(nullptr, context); }, "null digest"); + } + + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(SHA512_Init_nid_postfix(context) == 1 && SHA512_Final_nid_postfix(nullptr, context) == 0, "SHA512_Final without a digest buffer"); + SHA256_Init_nid_postfix(context); + std::uint32_t length = 33; + std::memcpy(context + 108, &length, 4); + Require(SHA256_Final_nid_postfix(digest, context) == 0, "SHA256_Final with an oversized digest length"); + SHA256_Init_nid_postfix(context); + length = 16; + std::memcpy(context + 108, &length, 4); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA256_Final_nid_postfix(digest, context) == 1 && Hex(digest, 16) == "e3b0c44298fc1c149afbf4c8996fb924" && digest[16] == 0xee, "SHA256_Final truncates to the digest length"); + SHA512_Init_nid_postfix(context); + length = 32; + std::memcpy(context + 212, &length, 4); + RequireThrows([&] { SHA512_Final_nid_postfix(digest, context); }, "SHA-512/256 digest length"); + SHA256_Init_nid_postfix(context); + length = 64; + std::memcpy(context + 104, &length, 4); + RequireThrows([&] { SHA256_Update_nid_postfix(context, "a", 1); }, "corrupt buffered count"); + + const auto* abc = reinterpret_cast<const unsigned char*>("abc"); + Require(SHA256_nid_postfix(abc, 3, digest) == digest && Hex(digest, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot"); + Require(SHA256_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot of nothing"); + RequireThrows([&] { SHA256_nid_postfix(abc, 3, nullptr); }, "SHA256 one-shot without a digest buffer"); + std::puts("libSceLibreSsl digest tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index b8330484a5..bcaab509a1 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,4 +2,5 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 377 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw From 71ce2fe6da7c84fa94cf1074ed3a3bbeac945c2f Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:33:53 +0000 Subject: [PATCH 0477/1360] feat(libSceLibreSsl3): implement MD5 and SHA1 Both one-shots threw. They run Init, Update and Final through libSceLibreSsl's MD5_* and SHA1_*, which libSceLibreSsl3 now links, and return the digest buffer. A null digest buffer throws, since LibreSSL up to 3.8 hashes into a static buffer and later versions do not check it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- core/libs/prx/libSceLibreSsl3/CMakeLists.txt | 11 +++- core/libs/prx/libSceLibreSsl3/Export.cpp | 10 ---- core/libs/prx/libSceLibreSsl3/src/Digest.cpp | 30 +++++++++++ core/libs/tests/GuestLibreSsl3Digest.cpp | 54 ++++++++++++++++++++ docs/dev/debt/libSceLibreSsl3.md | 3 +- 5 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl3/src/Digest.cpp create mode 100644 core/libs/tests/GuestLibreSsl3Digest.cpp diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt index 8fb846319c..1a3d213378 100644 --- a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -1,5 +1,6 @@ -add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl) configure_windows_unwind(libSceLibreSsl3) if(BUILD_TESTING) @@ -8,8 +9,14 @@ if(BUILD_TESTING) target_link_libraries(guest_crypto_memcmp_tests PRIVATE libSceLibreSsl3) add_test(NAME guest_crypto_memcmp COMMAND guest_crypto_memcmp_tests) set_tests_properties(guest_crypto_memcmp PROPERTIES TIMEOUT 20) + add_test_executable(guest_libressl3_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSsl3Digest.cpp) + target_include_directories(guest_libressl3_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_digest_tests PRIVATE libSceLibreSsl3 libc) + configure_windows_unwind(guest_libressl3_digest_tests) + add_test(NAME guest_libressl3_digest COMMAND guest_libressl3_digest_tests) + set_tests_properties(guest_libressl3_digest PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_crypto_memcmp PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl3>") endif() endif() diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp index e44d5b5673..623b05d144 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -220,11 +220,6 @@ int APS5_VABI EVP_sha224_nid_postfix(void) { return 0; } -int APS5_VABI MD5_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OBJ_cmp_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -285,11 +280,6 @@ int APS5_VABI RSA_up_ref_nid_postfix(void) { return 0; } -int APS5_VABI SHA1_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CTX_add0_chain_cert_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp new file mode 100644 index 0000000000..373654f3d5 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp @@ -0,0 +1,30 @@ +#include <cstddef> +#include "prx/libc/include/General.hpp" + +extern "C" { +int APS5_VABI MD5_Init_nid_postfix(void* context); +int APS5_VABI MD5_Update_nid_postfix(void* context, const void* data, std::size_t length); +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, void* context); +int APS5_VABI SHA1_Init_nid_postfix(void* context); +int APS5_VABI SHA1_Update_nid_postfix(void* context, const void* data, std::size_t length); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, void* context); + +unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + if (!digest) NotImplemented_nid_no_patch("MD5: null digest buffer"); + alignas(8) unsigned char context[92]; + MD5_Init_nid_postfix(context); + MD5_Update_nid_postfix(context, data, length); + MD5_Final_nid_postfix(digest, context); + return digest; +} + +unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + if (!digest) NotImplemented_nid_no_patch("SHA1: null digest buffer"); + alignas(8) unsigned char context[96]; + SHA1_Init_nid_postfix(context); + SHA1_Update_nid_postfix(context, data, length); + SHA1_Final_nid_postfix(digest, context); + return digest; +} + +} diff --git a/core/libs/tests/GuestLibreSsl3Digest.cpp b/core/libs/tests/GuestLibreSsl3Digest.cpp new file mode 100644 index 0000000000..19f6f8cb2b --- /dev/null +++ b/core/libs/tests/GuestLibreSsl3Digest.cpp @@ -0,0 +1,54 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstddef> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <source_location> +#include <stdexcept> +#include <string> + +extern "C" { +unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL3 digest check failed at line %u\n", location.line()); + std::abort(); +} + +template<typename TAction> static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +int main() { + const auto* abc = reinterpret_cast<const unsigned char*>("abc"); + const std::string million(1000000, 'a'); + const auto* many = reinterpret_cast<const unsigned char*>(million.data()); + unsigned char digest[24]; + std::memset(digest, 0xee, sizeof(digest)); + Require(MD5_nid_postfix(abc, 3, digest) == digest && Hex(digest, 16) == "900150983cd24fb0d6963f7d28e17f72" && digest[16] == 0xee); + Require(MD5_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 16) == "d41d8cd98f00b204e9800998ecf8427e"); + Require(MD5_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 16) == "7707d6ae4e027c70eea2a935c2296f21"); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA1_nid_postfix(abc, 3, digest) == digest && Hex(digest, 20) == "a9993e364706816aba3e25717850c26c9cd0d89d" && digest[20] == 0xee); + Require(SHA1_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 20) == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + Require(SHA1_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 20) == "34aa973cd4c4daa4f61eeb2bdbad27316534016f"); + RequireThrows([&] { MD5_nid_postfix(abc, 3, nullptr); }); + RequireThrows([&] { SHA1_nid_postfix(abc, 3, nullptr); }); + RequireThrows([&] { SHA1_nid_postfix(nullptr, 1, digest); }); + + std::puts("libSceLibreSsl3 digest tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index b3c055be23..dba672f91f 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,8 +2,9 @@ ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 86 named callable exports, excluding `CRYPTO_memcmp`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 84 named callable exports, excluding `CRYPTO_memcmp`, `MD5` and `SHA1`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. +- [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer throws: LibreSSL up to 3.8 returns a static buffer, later versions do not check it. Nothing is checked on the console From 87465459f0028de9e07160b8a0113fe9125b1492 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 20:58:45 +0300 Subject: [PATCH 0478/1360] fix(tools): wrap entrypoint in main with stream reconfigure in check_main_sync --- tools/check_main_sync.py | 19 ++++++++++++------- tools/tests/test_check_main_sync.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index e1f41677d7..63d952ee12 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -88,14 +88,17 @@ def render(data): return "\n".join(out) -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(errors="backslashreplace") parser = argparse.ArgumentParser(description="report whether a branch is in step with the upstream main before you push or test") parser.add_argument("--base", help="the main ref to compare against (default: upstream/main, then origin/main, then main)") parser.add_argument("--head", default="HEAD", help="the branch or commit to check (default: HEAD)") parser.add_argument("--repo", type=Path, default=Path("."), help="the repository to check (default: the current directory)") parser.add_argument("--fetch", action="store_true", help="fetch the base's remote first") parser.add_argument("--strict", action="store_true", help="also fail when the branch is behind the base") - args = parser.parse_args() + args = parser.parse_args(argv) try: def fetch(ref): remote, _, branch = ref.partition("/") @@ -117,9 +120,11 @@ if __name__ == "__main__": data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) - sys.stdout.reconfigure(errors="backslashreplace") print(render(data)) - if data["conflicts"]: - raise SystemExit(1) - if args.strict and data["behind"]: - raise SystemExit(1) + if data["conflicts"] or (args.strict and data["behind"]): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index a4590a60e5..22b06de59d 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import os import subprocess import sys @@ -65,6 +67,14 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual((data["behind"], data["ahead"], data["conflicts"], data["shared"]), (0, 1, [], [])) self.assertEqual(run_tool(self.repo.path, "--head", "feature", "--strict").returncode, 0) + def test_main_with_explicit_argv(self): + self.branch("feature") + self.repo.write("other.txt", "y\n") + self.repo.commit("feature change") + with contextlib.redirect_stdout(io.StringIO()): + code = check_main_sync.main(["--repo", str(self.repo.path), "--base", "main", "--head", "feature"]) + self.assertEqual(code, 0) + def test_behind_main_is_reported_and_only_fails_when_strict(self): self.branch("feature") self.repo.write("other.txt", "y\n") From a2938685612ca2d256e39efa03dab3a28f60e4ad Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:56:21 -0700 Subject: [PATCH 0479/1360] fix(libkernel_sys): follow a symbolic link in link on Windows link_nid_postfix decided whether to follow the source with std::filesystem::is_symlink, which MinGW-w64's libstdc++ never reports for a Windows symbolic link, so on Windows link made a hard link to the link itself. Follow a reparse-point source to its final path with GetFinalPathNameByHandleW instead, and run the link-through-a-link case of guest_kernel_sys on Windows too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 35 ++++++++++++++++++++++++++ core/libs/tests/GuestKernelSys.cpp | 12 +++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..65867eb15d 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -335,6 +335,35 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { } } +#ifdef _WIN32 +static int FollowLinkPath(const std::filesystem::path& source, std::filesystem::path& target) { + target = source; + const DWORD attributes = GetFileAttributesW(source.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return 0; + const HANDLE handle = CreateFileW(source.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD code = GetLastError(); + if (code == ERROR_FILE_NOT_FOUND || code == ERROR_PATH_NOT_FOUND) return GuestEnoent; + if (code == ERROR_CANT_RESOLVE_FILENAME) return GuestEloop; + throw std::system_error(static_cast<int>(code), std::system_category(), "link: CreateFileW"); + } + std::wstring name(MAX_PATH, L'\0'); + DWORD length = GetFinalPathNameByHandleW(handle, name.data(), static_cast<DWORD>(name.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (length >= name.size()) { + name.resize(length); + length = GetFinalPathNameByHandleW(handle, name.data(), static_cast<DWORD>(name.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + } + const DWORD code = GetLastError(); + CloseHandle(handle); + if (length == 0 || length >= name.size()) + throw std::system_error(static_cast<int>(code), std::system_category(), "link: GetFinalPathNameByHandleW"); + name.resize(length); + target = std::filesystem::path(name); + return 0; +} +#endif + int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (existing == nullptr || created == nullptr) return static_cast<int>(Fail(GuestEfault)); if (*existing == '\0' || *created == '\0') return static_cast<int>(Fail(GuestEnoent)); @@ -347,7 +376,13 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (std::filesystem::is_directory(status)) return static_cast<int>(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast<int>(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast<int>(Fail(GuestEnoent)); +#ifdef _WIN32 + std::filesystem::path target; + if (const int followError = FollowLinkPath(source, target)) return static_cast<int>(Fail(followError)); + if (std::filesystem::is_directory(target, error)) return static_cast<int>(Fail(GuestEperm)); +#else const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; +#endif if (!error) std::filesystem::create_hard_link(target, destination, error); if (error == std::errc::cross_device_link) return static_cast<int>(Fail(GuestExdev)); if (error) throw std::system_error(error, "link"); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a60abca020..fbe2f592e5 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -685,20 +685,22 @@ int main() { Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif -#ifndef _WIN32 if (created == 0) { const auto throughLink = (root / "through-link.bin").string(); Require(link_nid_postfix(symbolic.c_str(), throughLink.c_str()) == 0); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(throughLink).c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(throughLink))); +#endif Require(std::filesystem::equivalent(throughLink, original) && std::filesystem::hard_link_count(original) == 2); Require(std::filesystem::remove(throughLink) && std::filesystem::hard_link_count(original) == 1); Require(link_nid_postfix((root / "dangling").string().c_str(), throughLink.c_str()) == -1 && Error() == 2); - const auto directoryLink = (root / "directory-link").string(); - Require(symlink_nid_postfix(".", directoryLink.c_str()) == 0); - Require(link_nid_postfix(directoryLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); + const auto dotLink = (root / "dot-link").string(); + Require(symlink_nid_postfix(".", dotLink.c_str()) == 0); + Require(link_nid_postfix(dotLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } -#endif RemoveTree(root); return 0; } From 83d94a9cd8ea9442c81ae9bd6361f563354c6e44 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:00:47 +0300 Subject: [PATCH 0480/1360] fix(ci): wrap entrypoint in main with stream reconfigure in check_skips --- tools/check_skips.py | 17 +++++++++++++---- tools/tests/test_check_skips.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 99628018e3..c837fb1096 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -1,3 +1,4 @@ +import sys import argparse import re import xml.etree.ElementTree as ET @@ -40,12 +41,15 @@ def skips(path): yield name, "\n".join(output) -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="fail when a test is skipped that the job's device is not expected to skip") parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--job", choices=("linux", "windows"), default="linux") parser.add_argument("--list", action="store_true", help="print the skipped tests and why the file says they skipped") - args = parser.parse_args() + args = parser.parse_args(argv) try: found = sorted(skips(args.junit)) except (OSError, ET.ParseError, ValueError) as error: @@ -54,7 +58,7 @@ if __name__ == "__main__": for name, output in found: reason = next((line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped")), "") print(f"{name}: {reason}") - raise SystemExit + return 0 if args.job == "windows": unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not NO_VULKAN_DEVICE.search(output) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" @@ -65,5 +69,10 @@ if __name__ == "__main__": print(f"error: {name} skipped") if unexpected: print(f"{len(unexpected)} test(s) skipped for a reason that is not expected: the job lost that coverage and ctest still reports every test as passed") - raise SystemExit(1) + return 1 print(f"{len(found)} test(s) skipped, all expected ({expected})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index f4de797f05..ea82a476d1 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import subprocess import sys import tempfile @@ -192,6 +194,15 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("indented: skipped, host imports are unavailable", result.stdout) self.assertIn("capitalized: Skipped, no display", result.stdout) + def test_main_with_explicit_argv(self): + xml = '<testsuite><testcase name="normal"><skipped message="the device lacks geometryShader"/></testcase></testsuite>' + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + with contextlib.redirect_stdout(io.StringIO()): + code = check_skips.main([str(junit), "--job", "linux"]) + self.assertEqual(code, 0) + From b74f4e24e8a316d8253e26c933eb60f7e46ec54e Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:55:16 +0200 Subject: [PATCH 0481/1360] feat(libSceFreeType): implement FT_Set_Char_Size --- core/libs/prx/libSceFreeType/Export.cpp | 22 ++++++++ core/libs/tests/GuestFreeType.cpp | 71 +++++++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 3 files changed, 95 insertions(+) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0c4f8d79b1..1a935d33fa 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -149,6 +149,28 @@ int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec* face, std::uint32_t p return error; } +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, + std::uint32_t horizontalResolution, std::uint32_t verticalResolution) { + GuestFace* owner = FaceOf(face); + if (!owner) return FT_Err_Invalid_Face_Handle; + if (!owner->host->size) return FT_Err_Invalid_Size_Handle; + if (!charWidth) charWidth = charHeight; + else if (!charHeight) charHeight = charWidth; + if (charWidth < 64) charWidth = 64; + if (charHeight < 64) charHeight = 64; + if (!horizontalResolution) horizontalResolution = verticalResolution; + else if (!verticalResolution) verticalResolution = horizontalResolution; + if (!horizontalResolution) horizontalResolution = verticalResolution = 72; + const auto maximum = std::numeric_limits<FT_Long>::max(); + if (charWidth > (maximum - 36) / horizontalResolution || charHeight > (maximum - 36) / verticalResolution) { + throw std::runtime_error("FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented"); + } + const FT_Error error = FT_Set_Char_Size(owner->host, static_cast<FT_F26Dot6>(charWidth), static_cast<FT_F26Dot6>(charHeight), + horizontalResolution, verticalResolution); + SyncSize(*owner); + return error; +} + std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec* face, std::uint64_t charcode) { const GuestFace* owner = FaceOf(face); return FT_Get_Char_Index(owner ? owner->host : nullptr, static_cast<FT_ULong>(static_cast<std::uint32_t>(charcode))); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 8e5c7e7d0f..b1e356a7ea 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -14,6 +14,7 @@ #include <filesystem> #include <fstream> #include <initializer_list> +#include <limits> #include <random> #include <stdexcept> #include <string> @@ -29,6 +30,7 @@ int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFac int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t); +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64_t); int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::int32_t); @@ -89,6 +91,15 @@ static void SlotMatches(const GuestGlyphSlotRec* slot, FT_GlyphSlot host) { Require(slot->lsb_delta == host->lsb_delta && slot->rsb_delta == host->rsb_delta && slot->control_len == host->control_len); } +static void SizeMatches(const GuestFaceRec* face, FT_Face host) { + const auto& actual = face->size->metrics; + const auto& expected = host->size->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); +} + struct Finalizers { std::vector<int> calls; }; @@ -191,6 +202,8 @@ int main() { Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, -1, &probe) == 0 && probe != nullptr); Require(probe->num_faces == nativeProbe->num_faces && probe->face_index == nativeProbe->face_index); Require((probe->glyph == nullptr) == (nativeProbe->glyph == nullptr) && (probe->size == nullptr) == (nativeProbe->size == nullptr)); + Require(FT_Set_Char_Size_nid_postfix(probe, std::numeric_limits<std::int64_t>::max(), 0, 0, 0) == + FT_Set_Char_Size(nativeProbe, 64, 64, 72, 72)); Finalizers nativeProbeEvents; Finalizers guestProbeEvents; nativeProbe->generic = {&nativeProbeEvents, NativeFaceFinalizer}; @@ -234,6 +247,64 @@ int main() { Require(guestMetrics.x_ppem == 100 && guestMetrics.y_ppem == 100 && guestMetrics.x_scale == metrics.x_scale && guestMetrics.y_scale == metrics.y_scale); Require(guestMetrics.ascender == metrics.ascender && guestMetrics.descender == metrics.descender && guestMetrics.height == metrics.height && guestMetrics.max_advance == metrics.max_advance); + { + struct Request { + std::int64_t width; + std::int64_t height; + std::uint32_t horizontal; + std::uint32_t vertical; + }; + const std::array requests{ + Request{0, 0, 0, 0}, Request{0, 12 * 64, 0, 0}, Request{12 * 64, 0, 0, 0}, + Request{12 * 64, 18 * 64, 96, 144}, Request{12 * 64 + 32, 12 * 64 + 32, 72, 72}, + Request{1, 63, 72, 72}, Request{-1, -100, 0, 0}, Request{0, -100, 96, 0}, + Request{12 * 64, 18 * 64, 0, 144}, Request{12 * 64, 18 * 64, 96, 0}, + Request{65535ll * 64, 64, 72, 72}, Request{65536ll * 64, 64, 72, 72}, + Request{64, 65536ll * 64, 72, 72}, + Request{18 * 64, 12 * 64, 72, 72} + }; + std::size_t successes = 0; + std::size_t failures = 0; + for (const auto& request : requests) { + std::array<unsigned char, sizeof(GuestGlyphSlotRec)> previousSlot{}; + std::memcpy(previousSlot.data(), face->glyph, previousSlot.size()); + const auto expected = FT_Set_Char_Size(host, static_cast<FT_F26Dot6>(request.width), static_cast<FT_F26Dot6>(request.height), + request.horizontal, request.vertical); + Require(FT_Set_Char_Size_nid_postfix(face, request.width, request.height, request.horizontal, request.vertical) == expected); + Require(std::memcmp(previousSlot.data(), face->glyph, previousSlot.size()) == 0); + expected == 0 ? ++successes : ++failures; + SizeMatches(face, host); + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_DEFAULT) == FT_Load_Char(host, 'A', FT_LOAD_DEFAULT)); + SlotMatches(face->glyph, host->glyph); + } + Require(successes != 0 && failures != 0); + for (const auto value : std::array<std::int64_t, 2>{std::numeric_limits<std::int64_t>::min(), -0x80000001ll}) { + Require(FT_Set_Char_Size_nid_postfix(face, value, 0, 0, 0) == FT_Set_Char_Size(host, 64, 64, 0, 0)); + SizeMatches(face, host); + Require(FT_Set_Char_Size_nid_postfix(face, 0, value, 0, 0) == 0); + SizeMatches(face, host); + } + for (const auto value : {std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max()}) { + Require(FT_Set_Char_Size_nid_postfix(nullptr, value, value, 0, 0) == FT_Err_Invalid_Face_Handle); + } + if constexpr (sizeof(FT_F26Dot6) < sizeof(std::int64_t)) { + for (const auto value : std::array<std::int64_t, 4>{0x7FFFFFFFll, 0x80000000ll, 0x100000040ll, std::numeric_limits<std::int64_t>::max()}) { + for (const bool width : {false, true}) { + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, width ? value : 64, width ? 64 : value, 72, 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + } + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, 64, 64, std::numeric_limits<std::uint32_t>::max(), 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + Require(FT_Set_Char_Size_nid_postfix(memoryFace, 0, 48 * 64, 0, 144) == 0); + Require(memoryFace->size->metrics.y_ppem == 96 && face->size->metrics.y_ppem == 1); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); + } + Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..b0cc316c3a 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,6 +2,8 @@ ### Unknown function info +- [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. + - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions From 7210515acff18da4bf8dd4bd0fa90d8e024b111f Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:59:05 +0200 Subject: [PATCH 0482/1360] test(libSceFreeType): separate char-size coverage from pending kerning work --- core/libs/tests/GuestFreeType.cpp | 118 +++++++++++++++--------------- docs/dev/debt/libSceFreeType.md | 4 +- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index b1e356a7ea..6e427fd2fe 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -30,11 +30,11 @@ int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFac int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t); -int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64_t); int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::int32_t); int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_t); +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); } static void Check(bool value, int line) { @@ -247,64 +247,6 @@ int main() { Require(guestMetrics.x_ppem == 100 && guestMetrics.y_ppem == 100 && guestMetrics.x_scale == metrics.x_scale && guestMetrics.y_scale == metrics.y_scale); Require(guestMetrics.ascender == metrics.ascender && guestMetrics.descender == metrics.descender && guestMetrics.height == metrics.height && guestMetrics.max_advance == metrics.max_advance); - { - struct Request { - std::int64_t width; - std::int64_t height; - std::uint32_t horizontal; - std::uint32_t vertical; - }; - const std::array requests{ - Request{0, 0, 0, 0}, Request{0, 12 * 64, 0, 0}, Request{12 * 64, 0, 0, 0}, - Request{12 * 64, 18 * 64, 96, 144}, Request{12 * 64 + 32, 12 * 64 + 32, 72, 72}, - Request{1, 63, 72, 72}, Request{-1, -100, 0, 0}, Request{0, -100, 96, 0}, - Request{12 * 64, 18 * 64, 0, 144}, Request{12 * 64, 18 * 64, 96, 0}, - Request{65535ll * 64, 64, 72, 72}, Request{65536ll * 64, 64, 72, 72}, - Request{64, 65536ll * 64, 72, 72}, - Request{18 * 64, 12 * 64, 72, 72} - }; - std::size_t successes = 0; - std::size_t failures = 0; - for (const auto& request : requests) { - std::array<unsigned char, sizeof(GuestGlyphSlotRec)> previousSlot{}; - std::memcpy(previousSlot.data(), face->glyph, previousSlot.size()); - const auto expected = FT_Set_Char_Size(host, static_cast<FT_F26Dot6>(request.width), static_cast<FT_F26Dot6>(request.height), - request.horizontal, request.vertical); - Require(FT_Set_Char_Size_nid_postfix(face, request.width, request.height, request.horizontal, request.vertical) == expected); - Require(std::memcmp(previousSlot.data(), face->glyph, previousSlot.size()) == 0); - expected == 0 ? ++successes : ++failures; - SizeMatches(face, host); - Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_DEFAULT) == FT_Load_Char(host, 'A', FT_LOAD_DEFAULT)); - SlotMatches(face->glyph, host->glyph); - } - Require(successes != 0 && failures != 0); - for (const auto value : std::array<std::int64_t, 2>{std::numeric_limits<std::int64_t>::min(), -0x80000001ll}) { - Require(FT_Set_Char_Size_nid_postfix(face, value, 0, 0, 0) == FT_Set_Char_Size(host, 64, 64, 0, 0)); - SizeMatches(face, host); - Require(FT_Set_Char_Size_nid_postfix(face, 0, value, 0, 0) == 0); - SizeMatches(face, host); - } - for (const auto value : {std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max()}) { - Require(FT_Set_Char_Size_nid_postfix(nullptr, value, value, 0, 0) == FT_Err_Invalid_Face_Handle); - } - if constexpr (sizeof(FT_F26Dot6) < sizeof(std::int64_t)) { - for (const auto value : std::array<std::int64_t, 4>{0x7FFFFFFFll, 0x80000000ll, 0x100000040ll, std::numeric_limits<std::int64_t>::max()}) { - for (const bool width : {false, true}) { - Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, width ? value : 64, width ? 64 : value, 72, 72); }, - "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); - SizeMatches(face, host); - } - } - Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, 64, 64, std::numeric_limits<std::uint32_t>::max(), 72); }, - "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); - SizeMatches(face, host); - } - Require(FT_Set_Char_Size_nid_postfix(memoryFace, 0, 48 * 64, 0, 144) == 0); - Require(memoryFace->size->metrics.y_ppem == 96 && face->size->metrics.y_ppem == 1); - Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); - std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); - } - Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); @@ -373,6 +315,64 @@ int main() { slot->outline.points = original; Require(FT_Render_Glyph_nid_postfix(slot, FT_RENDER_MODE_NORMAL) == 0 && slot->bitmap_left == 10); + { + struct Request { + std::int64_t width; + std::int64_t height; + std::uint32_t horizontal; + std::uint32_t vertical; + }; + const std::array requests{ + Request{0, 0, 0, 0}, Request{0, 12 * 64, 0, 0}, Request{12 * 64, 0, 0, 0}, + Request{12 * 64, 18 * 64, 96, 144}, Request{12 * 64 + 32, 12 * 64 + 32, 72, 72}, + Request{1, 63, 72, 72}, Request{-1, -100, 0, 0}, Request{0, -100, 96, 0}, + Request{12 * 64, 18 * 64, 0, 144}, Request{12 * 64, 18 * 64, 96, 0}, + Request{65535ll * 64, 64, 72, 72}, Request{65536ll * 64, 64, 72, 72}, + Request{64, 65536ll * 64, 72, 72}, + Request{18 * 64, 12 * 64, 72, 72} + }; + std::size_t successes = 0; + std::size_t failures = 0; + for (const auto& request : requests) { + std::array<unsigned char, sizeof(GuestGlyphSlotRec)> previousSlot{}; + std::memcpy(previousSlot.data(), face->glyph, previousSlot.size()); + const auto expected = FT_Set_Char_Size(host, static_cast<FT_F26Dot6>(request.width), static_cast<FT_F26Dot6>(request.height), + request.horizontal, request.vertical); + Require(FT_Set_Char_Size_nid_postfix(face, request.width, request.height, request.horizontal, request.vertical) == expected); + Require(std::memcmp(previousSlot.data(), face->glyph, previousSlot.size()) == 0); + expected == 0 ? ++successes : ++failures; + SizeMatches(face, host); + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_DEFAULT) == FT_Load_Char(host, 'A', FT_LOAD_DEFAULT)); + SlotMatches(face->glyph, host->glyph); + } + Require(successes != 0 && failures != 0); + for (const auto value : std::array<std::int64_t, 2>{std::numeric_limits<std::int64_t>::min(), -0x80000001ll}) { + Require(FT_Set_Char_Size_nid_postfix(face, value, 0, 0, 0) == FT_Set_Char_Size(host, 64, 64, 0, 0)); + SizeMatches(face, host); + Require(FT_Set_Char_Size_nid_postfix(face, 0, value, 0, 0) == 0); + SizeMatches(face, host); + } + for (const auto value : {std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max()}) { + Require(FT_Set_Char_Size_nid_postfix(nullptr, value, value, 0, 0) == FT_Err_Invalid_Face_Handle); + } + if constexpr (sizeof(FT_F26Dot6) < sizeof(std::int64_t)) { + for (const auto value : std::array<std::int64_t, 4>{0x7FFFFFFFll, 0x80000000ll, 0x100000040ll, std::numeric_limits<std::int64_t>::max()}) { + for (const bool width : {false, true}) { + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, width ? value : 64, width ? 64 : value, 72, 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + } + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, 64, 64, std::numeric_limits<std::uint32_t>::max(), 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + Require(FT_Set_Char_Size_nid_postfix(memoryFace, 0, 48 * 64, 0, 144) == 0); + Require(memoryFace->size->metrics.y_ppem == 96 && face->size->metrics.y_ppem == 1); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); + } + Finalizers nativeDone; Finalizers guestDone; SetFinalizers(host, nativeDone); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index b0cc316c3a..acaae0d8b1 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,8 +2,6 @@ ### Unknown function info -- [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. - - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions @@ -15,3 +13,5 @@ - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read + +- [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. From c74eae639335064c9e6d589ffb9839c57f16955d Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:04:09 +0300 Subject: [PATCH 0483/1360] fix(tools): handle errors cleanly and reconfigure streams in build_web --- tools/build_web.py | 32 ++++++++++++++++++++------------ tools/tests/test_build_web.py | 25 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/tools/build_web.py b/tools/build_web.py index 66798820d2..ed871bb1d4 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -4,6 +4,7 @@ import io import json import os from pathlib import Path +import sys import re import shutil import tempfile @@ -141,7 +142,7 @@ def publish_release(output): target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(archive.read(file)) publish_libraries(staging, release) - manifest = json.loads((staging / "release.json").read_text(encoding="utf-8")) + manifest = json.loads((staging / "release.json").read_text(encoding="utf-8-sig")) manifest["revision"] = metadata["revision"] (staging / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") (staging / ".nojekyll").touch() @@ -163,23 +164,30 @@ def publish_release(output): print(f"Published web converter and libraries from {tag} ({metadata['revision']})") -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) parser.add_argument("--release", "--libraries", dest="release", action="store_true", help="Download the matching converter and libraries from the latest release") - args = parser.parse_args() + args = parser.parse_args(argv) output = args.build.resolve() / "web" - if args.release: - publish_release(output) + try: + if args.release: + publish_release(output) + print(output) + return 0 + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + if not (output / "native" / name).is_file(): + parser.error(f"Build relinker_wasm first: missing {name}") + copy_sources(output) print(output) - return - for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): - if not (output / "native" / name).is_file(): - raise ValueError(f"Build relinker_wasm first: missing {name}") - copy_sources(output) - print(output) + return 0 + except (OSError, ValueError) as error: + parser.error(str(error)) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 3a56e49429..91e093b70d 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -1,8 +1,10 @@ +import os import contextlib import hashlib import io import json from pathlib import Path +import subprocess import sys import tempfile import unittest @@ -125,5 +127,28 @@ class LibraryPublicationTests(unittest.TestCase): self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) +class BuildWebCliTests(unittest.TestCase): + def test_missing_relinker_wasm_reports_error_without_traceback(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + result = subprocess.run([sys.executable, str(Path(build_web.__file__)), "--build", str(build)], + capture_output=True, text=True, + env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1])}) + self.assertEqual(result.returncode, 2) + self.assertIn("Build relinker_wasm first: missing", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_main_with_explicit_argv(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + native = build / "web" / "native" + native.mkdir(parents=True) + (native / "relinker_wasm.mjs").touch() + (native / "relinker_wasm.wasm").touch() + with contextlib.redirect_stdout(io.StringIO()): + code = build_web.main(["--build", str(build)]) + self.assertEqual(code, 0) + + if __name__ == "__main__": unittest.main() From 310cb0c4b5cf411c977c51c8de9cb4c8eabd205b Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 20:00:23 +0200 Subject: [PATCH 0484/1360] fix(libSceNgs2.native): validate the system option with the module's error codes --- .../libSceNgs2.native/include/Ngs2Types.hpp | 5 + .../libs/prx/libSceNgs2.native/src/System.cpp | 47 ++++-- .../tests/Ngs2HandleErrors.cpp | 147 ++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 4 files changed, 188 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index a604adba3d..e964c63b51 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,9 +9,14 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast<int>(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast<int>(0x804A8011); +static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast<int>(0x804A8013); +static constexpr int SCE_NGS2_ERROR_INVALID_SAMPLE_RATE = static_cast<int>(0x804A8051); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast<int>(0x804A8052); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast<int>(0x804A8201); static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast<int>(0x804A8202); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast<int>(0x804A8305); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_GRAIN_SAMPLES = static_cast<int>(0x804A8310); +static constexpr int SCE_NGS2_ERROR_INVALID_MAX_GRAIN_SAMPLES = static_cast<int>(0x804A8311); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast<int>(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast<int>(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast<int>(0x804A8432); diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 2fe15a7311..b4251c0f0e 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -4,6 +4,7 @@ #include <cstdint> #include <cstdio> #include <cstring> +#include <iterator> #include <map> #include <memory> #include <mutex> @@ -216,15 +217,31 @@ static Ngs2SystemOption DefaultSystemOption() { return option; } -static Ngs2SystemOption CheckedSystemOption(const Ngs2SystemOption* option) { - if (option == nullptr) return DefaultSystemOption(); - if (option->size != sizeof(Ngs2SystemOption)) throw std::invalid_argument("NGS2: unexpected system option size " + std::to_string(option->size)); - if (option->flags != 0) throw std::runtime_error("NGS2: system option flags are not implemented"); - if (option->sample_rate == 0 || option->max_grain_samples < MIN_GRAIN_SAMPLES || option->num_grain_samples == 0 || - option->num_grain_samples > option->max_grain_samples) { - throw std::invalid_argument("NGS2: invalid system grain or sample rate"); +static constexpr std::uint32_t MAX_GRAIN_SAMPLES = 2048; +static constexpr std::uint32_t MAX_VOICE_CHANNELS = 37; + +static bool ValidGrainSamples(std::uint32_t grainSamples, std::uint32_t maxSamples) { + return grainSamples >= MIN_GRAIN_SAMPLES && grainSamples <= maxSamples && grainSamples % MIN_GRAIN_SAMPLES == 0; +} + +static bool ValidSampleRate(std::uint32_t sampleRate) { + static constexpr std::uint32_t rates[] = {11025, 12000, 22050, 24000, 44100, 48000, 88200, 96000, 176400, 192000}; + return std::find(std::begin(rates), std::end(rates), sampleRate) != std::end(rates); +} + +static int CheckSystemOption(const Ngs2SystemOption* option, Ngs2SystemOption& checked) { + if (option == nullptr) { + checked = DefaultSystemOption(); + return SCE_NGS2_OK; } - return *option; + if (option->size != sizeof(Ngs2SystemOption)) return SCE_NGS2_ERROR_INVALID_OPTION_SIZE; + if (option->flags != 0) throw std::runtime_error("NGS2: system option flags are not implemented"); + if (!ValidGrainSamples(option->max_grain_samples, MAX_GRAIN_SAMPLES)) return SCE_NGS2_ERROR_INVALID_MAX_GRAIN_SAMPLES; + if (!ValidGrainSamples(option->num_grain_samples, option->max_grain_samples)) return SCE_NGS2_ERROR_INVALID_NUM_GRAIN_SAMPLES; + if (!ValidSampleRate(option->sample_rate)) return SCE_NGS2_ERROR_INVALID_SAMPLE_RATE; + if (option->max_voice_channels < 1 || option->max_voice_channels > MAX_VOICE_CHANNELS) return SCE_NGS2_ERROR_INVALID_NUM_CHANNELS; + checked = *option; + return SCE_NGS2_OK; } static int CreateSystem(const Ngs2SystemOption& option, const Ngs2ContextBufferInfo& bufferInfo, const Ngs2BufferAllocator& allocator, Ngs2Handle* handle) { @@ -247,15 +264,19 @@ extern "C" { int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption* option, Ngs2ContextBufferInfo* buffer_info) { if (buffer_info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - CheckedSystemOption(option); - *buffer_info = {}; + std::memset(buffer_info, 0, offsetof(Ngs2ContextBufferInfo, user_data)); + Ngs2SystemOption checked{}; + const int result = CheckSystemOption(option, checked); + if (result != SCE_NGS2_OK) return result; buffer_info->host_buffer_size = sizeof(Ngs2System); return SCE_NGS2_OK; } int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption* option, const Ngs2ContextBufferInfo* buffer_info, uintptr_t* handle) { if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - const auto checked = CheckedSystemOption(option); + Ngs2SystemOption checked{}; + const int result = CheckSystemOption(option, checked); + if (result != SCE_NGS2_OK) return result; if (buffer_info == nullptr) APS5_INVALID_ARG_EX; const Ngs2Lock lock; return CreateSystem(checked, *buffer_info, {}, handle); @@ -263,7 +284,9 @@ int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption* option, const Ngs2Cont int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption* option, const Ngs2BufferAllocator* allocator, uintptr_t* handle) { if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - const auto checked = CheckedSystemOption(option); + Ngs2SystemOption checked{}; + const int result = CheckSystemOption(option, checked); + if (result != SCE_NGS2_OK) return result; const auto bufferInfo = Allocate(allocator, sizeof(Ngs2System)); const Ngs2Lock lock; return CreateSystem(checked, bufferInfo, *allocator, handle); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index c9a0094eda..230c58b289 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -1,9 +1,13 @@ #include "Ngs2Test.hpp" #include <cstdint> +#include <cstdio> +#include <cstdlib> #include <cstring> #include <iostream> #include <stdexcept> +#include <string> +#include <vector> static void Status(int actual, int expected, const char* message) { if (actual != expected) throw std::runtime_error(message); @@ -31,6 +35,148 @@ static void CheckResetOption() { Status(sceNgs2SystemQueryBufferSize(&option, &query), SCE_NGS2_OK, "reset system option was rejected"); } +static int allocations = 0; + +static std::int32_t APS5_VABI CountedAllocate(Ngs2ContextBufferInfo* info) { + info->host_buffer = std::calloc(1, info->host_buffer_size); + allocations++; + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI CountedRelease(Ngs2ContextBufferInfo* info) { + std::free(info->host_buffer); + allocations--; + return SCE_NGS2_OK; +} + +static std::string Hex(int value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%x", static_cast<unsigned>(value)); + return text; +} + +static void Returned(int actual, int expected, const char* entry, const std::string& label) { + if (actual != expected) throw std::runtime_error(std::string(entry) + " with " + label + " returned " + Hex(actual) + " instead of " + Hex(expected)); +} + +static Ngs2SystemOption Option(std::uint32_t maxGrainSamples = 512, std::uint32_t numGrainSamples = 256, std::uint32_t sampleRate = 48000, std::uint32_t maxVoiceChannels = 8) { + Ngs2SystemOption option{}; + option.size = sizeof(option); + option.max_grain_samples = maxGrainSamples; + option.num_grain_samples = numGrainSamples; + option.sample_rate = sampleRate; + option.max_voice_channels = maxVoiceChannels; + return option; +} + +static void CheckOption(const Ngs2SystemOption& option, int expected, const std::string& label) { + constexpr uintptr_t sentinel = 0x5a5a; + Ngs2ContextBufferInfo query{}; + Returned(sceNgs2SystemQueryBufferSize(&option, &query), expected, "sceNgs2SystemQueryBufferSize", label); + Require((query.host_buffer_size != 0) == (expected == SCE_NGS2_OK)); + std::vector<std::uint64_t> storage(query.host_buffer_size / sizeof(std::uint64_t) + 1); + Ngs2ContextBufferInfo buffer{}; + buffer.host_buffer = storage.data(); + buffer.host_buffer_size = query.host_buffer_size; + const Ngs2BufferAllocator allocator{CountedAllocate, CountedRelease, 0}; + uintptr_t created = sentinel; + uintptr_t allocated = sentinel; + Returned(sceNgs2SystemCreate(&option, &buffer, &created), expected, "sceNgs2SystemCreate", label); + Returned(sceNgs2SystemCreateWithAllocator(&option, &allocator, &allocated), expected, "sceNgs2SystemCreateWithAllocator", label); + if (expected != SCE_NGS2_OK) { + Require(created == sentinel && allocated == sentinel && allocations == 0); + return; + } + Require(created != sentinel && allocated != sentinel && allocations == 1); + for (const auto system : {created, allocated}) { + Ngs2SystemInfo info{}; + Status(sceNgs2SystemGetInfo(system, &info, sizeof(info)), SCE_NGS2_OK, "system info of a valid option failed"); + Require(info.max_grain_samples == option.max_grain_samples && info.num_grain_samples == option.num_grain_samples && info.sample_rate == option.sample_rate); + Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "system destroy of a valid option failed"); + } + Require(allocations == 0); +} + +static void CheckQueryBuffer(const Ngs2SystemOption* option, int expected, const char* label) { + constexpr uintptr_t userData = 0x1234; + Ngs2ContextBufferInfo query; + std::memset(&query, 0xAB, sizeof(query)); + query.user_data = userData; + Returned(sceNgs2SystemQueryBufferSize(option, &query), expected, "sceNgs2SystemQueryBufferSize", label); + Require(query.host_buffer == nullptr && query.user_data == userData); + for (const auto value : query.reserved) Require(value == 0); + Require((query.host_buffer_size != 0) == (expected == SCE_NGS2_OK)); +} + +static void CheckSystemOption() { + constexpr int outputError = static_cast<int>(0x804A8010u); + constexpr int sizeError = static_cast<int>(0x804A8013u); + constexpr int rateError = static_cast<int>(0x804A8051u); + constexpr int channelsError = static_cast<int>(0x804A8052u); + constexpr int grainError = static_cast<int>(0x804A8310u); + constexpr int maxGrainError = static_cast<int>(0x804A8311u); + constexpr std::uint32_t invalid = 0xffffffffu; + + for (const std::uint32_t size : {0u, 8u, 0x88u, 0x98u, invalid}) { + auto option = Option(); + option.size = size; + CheckOption(option, sizeError, "size " + std::to_string(size)); + } + for (const std::uint32_t maxGrain : {0u, 32u, 63u, 65u, 96u, 2047u, 2049u, 2112u, 4096u, invalid}) { + CheckOption(Option(maxGrain, 64), maxGrainError, "max grain samples " + std::to_string(maxGrain)); + } + for (const std::uint32_t maxGrain : {64u, 128u, 576u, 2048u}) { + CheckOption(Option(maxGrain, 64), SCE_NGS2_OK, "max grain samples " + std::to_string(maxGrain)); + CheckOption(Option(maxGrain, maxGrain), SCE_NGS2_OK, "grain samples " + std::to_string(maxGrain)); + } + for (const std::uint32_t grain : {0u, 32u, 63u, 65u, 100u, 255u, 257u, 513u, 576u, 2048u, invalid}) { + CheckOption(Option(512, grain), grainError, "grain samples " + std::to_string(grain)); + } + for (const std::uint32_t grain : {64u, 192u, 448u, 512u}) { + CheckOption(Option(512, grain), SCE_NGS2_OK, "grain samples " + std::to_string(grain)); + } + for (const std::uint32_t rate : {11025u, 12000u, 22050u, 24000u, 44100u, 48000u, 88200u, 96000u, 176400u, 192000u}) { + CheckOption(Option(512, 256, rate), SCE_NGS2_OK, "sample rate " + std::to_string(rate)); + } + for (const std::uint32_t rate : {0u, 8000u, 11024u, 11026u, 16000u, 32000u, 47999u, 48001u, 192001u, invalid}) { + CheckOption(Option(512, 256, rate), rateError, "sample rate " + std::to_string(rate)); + } + for (const std::uint32_t channels : {1u, 2u, 8u, 36u, 37u}) { + CheckOption(Option(512, 256, 48000, channels), SCE_NGS2_OK, "voice channels " + std::to_string(channels)); + } + for (const std::uint32_t channels : {0u, 38u, 39u, invalid}) { + CheckOption(Option(512, 256, 48000, channels), channelsError, "voice channels " + std::to_string(channels)); + } + + auto badSize = Option(100, 100, 7000, 0); + badSize.size = 0x88; + CheckOption(badSize, sizeError, "a bad size and every other field bad"); + CheckOption(Option(100, 100, 7000, 0), maxGrainError, "a bad max grain, grain, rate and channels"); + CheckOption(Option(100, 0, 48000, 8), maxGrainError, "a bad max grain and grain"); + CheckOption(Option(100, 256, 7000, 8), maxGrainError, "a bad max grain and rate"); + CheckOption(Option(100, 256, 48000, 0), maxGrainError, "a bad max grain and channels"); + CheckOption(Option(512, 100, 7000, 0), grainError, "a bad grain, rate and channels"); + CheckOption(Option(512, 576, 48000, 8), grainError, "a grain above the max grain"); + CheckOption(Option(512, 100, 48000, 0), grainError, "a bad grain and channels"); + CheckOption(Option(512, 256, 7000, 0), rateError, "a bad rate and channels"); + + auto scheduled = Option(); + for (auto& character : scheduled.name) character = 'n'; + for (auto& pointer : scheduled.job_scheduler_options) pointer = 0x1000; + CheckOption(scheduled, SCE_NGS2_OK, "a name and job scheduler options"); + + const auto invalidRate = Option(512, 256, 7000); + Status(sceNgs2SystemQueryBufferSize(&invalidRate, nullptr), outputError, "invalid option took precedence over the null output"); + Status(sceNgs2SystemQueryBufferSize(nullptr, nullptr), outputError, "null option took precedence over the null output"); + CheckQueryBuffer(nullptr, SCE_NGS2_OK, "a null option"); + CheckQueryBuffer(&invalidRate, rateError, "a refused rate"); + auto invalidSize = Option(); + invalidSize.size = 0x88; + CheckQueryBuffer(&invalidSize, sizeError, "a refused size"); + const auto valid = Option(); + CheckQueryBuffer(&valid, SCE_NGS2_OK, "a valid option"); +} + int main() { try { constexpr int systemError = static_cast<int>(0x804A8201u); @@ -64,6 +210,7 @@ int main() { Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); Require(value == sentinel); CheckResetOption(); + CheckSystemOption(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..2ba78623d7 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -26,6 +26,7 @@ - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [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 +- [NGS2 system option checks](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - not verified on a console. `sceNgs2SystemQueryBufferSize`, `sceNgs2SystemCreate` and `sceNgs2SystemCreateWithAllocator` return `0x804A8013` (size), `0x804A8311` (max grain, a multiple of 64 up to 2048), `0x804A8310` (grain, a multiple of 64 up to the max), `0x804A8051` (one of ten sample rates) and `0x804A8052` (1 to 37 channels) in that order, following [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4151-L4226). The four job scheduler pointers are neither inspected nor used. `sceNgs2SystemSetGrainSamples` still accepts any grain up to the maximum. - [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 - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. From 595dcacdf2db34a283be4716ef7d3ddaf50014ec Mon Sep 17 00:00:00 2001 From: Superhe11 <yankevichyaroslav@gmail.com> Date: Wed, 21 Oct 2026 21:08:18 +0300 Subject: [PATCH 0485/1360] docs(libc): avoid competing debt insertions at the section start --- docs/dev/debt/libc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 66033be89c..5560872f1a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,8 +2,6 @@ ### Unknown function info -- [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) reject sizes above `INT_MAX + 1` with `EOVERFLOW` (84), a terminating NUL and return value -1, following FreeBSD 11.4 [snprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/snprintf.c) and [vsnprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/vsnprintf.c). This limit is unverified on the console. A null buffer at an oversized nonzero size returns the error without writing, where FreeBSD dereferences it. - - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw @@ -54,6 +52,8 @@ - [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead - [__modti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead +- [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) reject sizes above `INT_MAX + 1` with `EOVERFLOW` (84), a terminating NUL and return value -1, following FreeBSD 11.4 [snprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/snprintf.c) and [vsnprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/vsnprintf.c). This limit is unverified on the console. A null buffer at an oversized nonzero size returns the error without writing, where FreeBSD dereferences it. + ### Functional - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. From bbd9ee6d2c3aa3e537fa6359f95e5d104a1d3167 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Wed, 21 Oct 2026 19:02:53 +0100 Subject: [PATCH 0486/1360] feat(libkernel_sys): implement SIGEV_NONE POSIX timers --- core/libs/CMakeLists.txt | 11 + .../prx/libkernel_sys/Export.Discovery.cpp | 25 --- core/libs/prx/libkernel_sys/Export.cpp | 152 +++++++++++++ core/libs/tests/GuestKernelSysKtimer.cpp | 204 ++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 5 files changed, 368 insertions(+), 25 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysKtimer.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..e5aad2009f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -611,6 +611,17 @@ if(BUILD_TESTING) configure_windows_unwind(savedata_replace_file_failure_tests) add_test(NAME savedata_replace_file_failure COMMAND savedata_replace_file_failure_tests) + add_test_executable(guest_kernel_sys_ktimer_tests tests/GuestKernelSysKtimer.cpp) + target_include_directories(guest_kernel_sys_ktimer_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_ktimer_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_ktimer_tests) + add_test(NAME guest_kernel_sys_ktimer COMMAND guest_kernel_sys_ktimer_tests) + set_tests_properties(guest_kernel_sys_ktimer PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_ktimer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") + endif() + add_test_executable(guest_crt_tests tests/GuestCrt.cpp) target_include_directories(guest_crt_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_crt_tests PRIVATE libSceAmpr libc) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..fb6c8e3cea 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -278,31 +278,6 @@ int APS5_VABI ksem_wait_nid_postfix(void) { return 0; } -int APS5_VABI ktimer_create_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_delete_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_getoverrun_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_gettime_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_settime_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ktrace_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..0ac387a960 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -6,6 +6,7 @@ #include <cstdlib> #include <filesystem> #include <limits> +#include <mutex> #include <stdexcept> #include <string> #include <string_view> @@ -691,3 +692,154 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const } } + +extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp); + +namespace { + +constexpr int ClockRealtime = 0; +constexpr int ClockMonotonic = 4; +constexpr int SigevNone = 0; +constexpr int SigevSignal = 1; +constexpr int SigevThreadId = 4; +constexpr int SignalMax = 128; +constexpr int TimerAbstime = 1; +constexpr int FirstTimer = 3; +constexpr int TimerMax = 32; +constexpr long long NanosecondsPerSecond = 1000000000; +constexpr long long TickNanoseconds = 1000000; + +struct GuestSigevent { + int notify; + int signo; +}; + +struct GuestTimespec { + std::int64_t tv_sec; + std::int64_t tv_nsec; +}; + +struct GuestItimerspec { + GuestTimespec it_interval; + GuestTimespec it_value; +}; + +using Nanoseconds = __int128; + +struct Ktimer { + bool live = false; + int clock = ClockRealtime; + Nanoseconds value = 0; + Nanoseconds interval = 0; +}; + +std::mutex g_ktimerLock; +Ktimer g_ktimers[TimerMax]; + +Nanoseconds ToNanoseconds(const GuestTimespec& time) { + return static_cast<Nanoseconds>(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec; +} + +GuestTimespec ToTimespec(Nanoseconds time) { + return {static_cast<std::int64_t>(time / NanosecondsPerSecond), + static_cast<std::int64_t>(time % NanosecondsPerSecond)}; +} + +Nanoseconds ClockNow(int clock) { + GuestTimespec now{}; + clock_gettime_nid_postfix(clock, &now); + return ToNanoseconds(now); +} + +bool FixTimespec(GuestTimespec& time) { + if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false; + if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds; + return true; +} + +Ktimer* FindTimer(int id) { + if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr; + return &g_ktimers[id]; +} + +void Expire(Ktimer& timer, Nanoseconds now) { + if (timer.value == 0 || now < timer.value) return; + if (timer.interval == 0) { + timer.value = 0; + return; + } + timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval; +} + +GuestItimerspec Remaining(Ktimer& timer) { + const Nanoseconds now = ClockNow(timer.clock); + Expire(timer, now); + GuestItimerspec result{ToTimespec(timer.interval), {}}; + if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1}; + return result; +} + +} + +extern "C" { + +int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) { + if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast<int>(Fail(GuestEinval)); + if (event != nullptr) { + if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId) + return static_cast<int>(Fail(GuestEinval)); + if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax)) + return static_cast<int>(Fail(GuestEinval)); + } + std::lock_guard lock(g_ktimerLock); + int id = FirstTimer; + while (id < TimerMax && g_ktimers[id].live) ++id; + if (id == TimerMax) return static_cast<int>(Fail(GuestEagain)); + if (timerid == nullptr) return static_cast<int>(Fail(GuestEfault)); + if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal"); + g_ktimers[id] = {true, clock, 0, 0}; + *timerid = id; + return 0; +} + +int APS5_VABI ktimer_delete_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); + *timer = {}; + return 0; +} + +int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { + if (value == nullptr) return static_cast<int>(Fail(GuestEfault)); + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); + GuestItimerspec next = *value; + if (!FixTimespec(next.it_value)) return static_cast<int>(Fail(GuestEinval)); + const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0; + if (armed && !FixTimespec(next.it_interval)) return static_cast<int>(Fail(GuestEinval)); + if (!armed) next.it_interval = {}; + if (oldValue != nullptr) *oldValue = Remaining(*timer); + timer->interval = ToNanoseconds(next.it_interval); + timer->value = ToNanoseconds(next.it_value); + if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock); + return 0; +} + +int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); + if (value == nullptr) return static_cast<int>(Fail(GuestEfault)); + *value = Remaining(*timer); + return 0; +} + +int APS5_VABI ktimer_getoverrun_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + if (FindTimer(id) == nullptr) return static_cast<int>(Fail(GuestEinval)); + return 0; +} + +} diff --git a/core/libs/tests/GuestKernelSysKtimer.cpp b/core/libs/tests/GuestKernelSysKtimer.cpp new file mode 100644 index 0000000000..2dc1cc763f --- /dev/null +++ b/core/libs/tests/GuestKernelSysKtimer.cpp @@ -0,0 +1,204 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <chrono> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <source_location> +#include <thread> +#include <vector> + +namespace { + +struct Sigevent { + int notify; + int signo; + std::int64_t value; + std::int64_t extra[6]; +}; + +struct Timespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +struct Itimerspec { + Timespec interval; + Timespec value; +}; + +} + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI clock_gettime_nid_postfix(int clock, Timespec* time); +int APS5_VABI ktimer_create_nid_postfix(int clock, const Sigevent* event, int* timerid); +int APS5_VABI ktimer_delete_nid_postfix(int timerid); +int APS5_VABI ktimer_settime_nid_postfix(int timerid, int flags, const Itimerspec* value, Itimerspec* oldValue); +int APS5_VABI ktimer_gettime_nid_postfix(int timerid, Itimerspec* value); +int APS5_VABI ktimer_getoverrun_nid_postfix(int timerid); +} + +namespace { + +constexpr int Efault = 14; +constexpr int Einval = 22; +constexpr int Eagain = 35; +constexpr int ClockRealtime = 0; +constexpr int ClockMonotonic = 4; +constexpr int ClockUptime = 5; +constexpr int TimerAbstime = 1; +constexpr std::int64_t Millisecond = 1000000; +constexpr std::int64_t Second = 1000000000; + +const Sigevent None{0, 0, 0, {}}; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast<unsigned>(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +std::int64_t Nanoseconds(const Timespec& time) { + return time.seconds * Second + time.nanoseconds; +} + +int Create(int clock = ClockMonotonic) { + int id = -1; + Require(ktimer_create_nid_postfix(clock, &None, &id) == 0); + return id; +} + +Itimerspec Get(int id) { + Itimerspec value{{-1, -1}, {-1, -1}}; + Require(ktimer_gettime_nid_postfix(id, &value) == 0); + return value; +} + +void Sleep(std::int64_t nanoseconds) { + std::this_thread::sleep_for(std::chrono::nanoseconds(nanoseconds)); +} + +void Creation() { + int id = -1; + const Sigevent badNotify{2, 0, 0, {}}; + const Sigevent noSignal{1, 0, 0, {}}; + const Sigevent badSignal{4, 129, 0, {}}; + Require(ktimer_create_nid_postfix(ClockUptime, &None, &id) == -1 && Error() == Einval && id == -1); + Require(ktimer_create_nid_postfix(-1, &None, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &badNotify, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &noSignal, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &badSignal, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &None, nullptr) == -1 && Error() == Efault); + + std::vector<int> ids; + for (int expected = 3; expected < 32; ++expected) { + ids.push_back(Create(expected % 2 == 0 ? ClockRealtime : ClockMonotonic)); + Require(ids.back() == expected); + } + Require(ktimer_create_nid_postfix(ClockMonotonic, &None, &id) == -1 && Error() == Eagain); + Require(ktimer_delete_nid_postfix(7) == 0); + Require(Create() == 7); + for (const int each : ids) Require(ktimer_delete_nid_postfix(each) == 0); +} + +void InvalidIds() { + Itimerspec value{}; + const int id = Create(); + Require(ktimer_delete_nid_postfix(id) == 0); + for (const int bad : {-1, 0, 2, id, 32}) { + Require(ktimer_delete_nid_postfix(bad) == -1 && Error() == Einval); + Require(ktimer_settime_nid_postfix(bad, 0, &value, nullptr) == -1 && Error() == Einval); + Require(ktimer_gettime_nid_postfix(bad, &value) == -1 && Error() == Einval); + Require(ktimer_getoverrun_nid_postfix(bad) == -1 && Error() == Einval); + } + Require(ktimer_settime_nid_postfix(id, 0, nullptr, nullptr) == -1 && Error() == Efault); +} + +void Values() { + const int id = Create(); + Itimerspec disarmed = Get(id); + Require(Nanoseconds(disarmed.value) == 0 && Nanoseconds(disarmed.interval) == 0); + for (const Itimerspec bad : {Itimerspec{{0, 0}, {-1, 0}}, Itimerspec{{0, 0}, {0, -1}}, Itimerspec{{0, 0}, {0, Second}}, + Itimerspec{{-1, 0}, {1, 0}}, Itimerspec{{0, Second}, {1, 0}}}) + Require(ktimer_settime_nid_postfix(id, 0, &bad, nullptr) == -1 && Error() == Einval); + + const Itimerspec clear{{-1, 0}, {0, 0}}; + Require(ktimer_settime_nid_postfix(id, 0, &clear, nullptr) == 0); + disarmed = Get(id); + Require(Nanoseconds(disarmed.value) == 0 && Nanoseconds(disarmed.interval) == 0); + + const Itimerspec small{{0, 1}, {10, 0}}; + Require(ktimer_settime_nid_postfix(id, 0, &small, nullptr) == 0); + Itimerspec old{}; + const Itimerspec later{{0, 0}, {20, 0}}; + Require(ktimer_settime_nid_postfix(id, 0, &later, &old) == 0); + Require(Nanoseconds(old.interval) == Millisecond); + Require(Nanoseconds(old.value) > 9 * Second && Nanoseconds(old.value) <= 10 * Second); + const Itimerspec tiny{{0, 0}, {0, 1}}; + Require(ktimer_settime_nid_postfix(id, 0, &tiny, &old) == 0); + Require(Nanoseconds(old.value) > 19 * Second && Nanoseconds(old.value) <= 20 * Second); + const Itimerspec off{}; + Require(ktimer_settime_nid_postfix(id, 0, &off, &old) == 0); + Require(Nanoseconds(old.interval) == 0 && Nanoseconds(old.value) <= Millisecond); + Require(ktimer_delete_nid_postfix(id) == 0); +} + +void Expiry() { + const int id = Create(); + const Itimerspec once{{0, 0}, {0, 30 * Millisecond}}; + Require(ktimer_settime_nid_postfix(id, 0, &once, nullptr) == 0); + const auto running = Get(id); + Require(Nanoseconds(running.value) > 0 && Nanoseconds(running.value) <= 30 * Millisecond); + Sleep(60 * Millisecond); + const auto expired = Get(id); + Require(Nanoseconds(expired.value) == 0 && Nanoseconds(expired.interval) == 0); + Require(ktimer_getoverrun_nid_postfix(id) == 0); + + const Itimerspec periodic{{0, 10 * Millisecond}, {0, 10 * Millisecond}}; + Require(ktimer_settime_nid_postfix(id, 0, &periodic, nullptr) == 0); + Sleep(55 * Millisecond); + const auto period = Get(id); + Require(Nanoseconds(period.interval) == 10 * Millisecond); + Require(Nanoseconds(period.value) > 0 && Nanoseconds(period.value) <= 10 * Millisecond); + Require(ktimer_getoverrun_nid_postfix(id) == 0); + Require(ktimer_delete_nid_postfix(id) == 0); +} + +void Absolute() { + for (const int clock : {ClockRealtime, ClockMonotonic}) { + const int id = Create(clock); + Timespec now{}; + Require(clock_gettime_nid_postfix(clock, &now) == 0); + const Itimerspec future{{0, 0}, {now.seconds + 5, now.nanoseconds}}; + Require(ktimer_settime_nid_postfix(id, TimerAbstime, &future, nullptr) == 0); + const auto remaining = Get(id); + Require(Nanoseconds(remaining.value) > 4 * Second && Nanoseconds(remaining.value) <= 5 * Second); + + const Itimerspec past{{0, 0}, {now.seconds - 1, now.nanoseconds}}; + Require(ktimer_settime_nid_postfix(id, TimerAbstime, &past, nullptr) == 0); + Require(Nanoseconds(Get(id).value) == 0); + const Itimerspec pastPeriodic{{1, 0}, {now.seconds - 10, now.nanoseconds}}; + Require(ktimer_settime_nid_postfix(id, TimerAbstime, &pastPeriodic, nullptr) == 0); + const auto moved = Get(id); + Require(Nanoseconds(moved.value) > 0 && Nanoseconds(moved.value) <= Second); + Require(Nanoseconds(moved.interval) == Second); + Require(ktimer_delete_nid_postfix(id) == 0); + } +} + +} + +int main() { + Creation(); + InvalidIds(); + Values(); + Expiry(); + Absolute(); + std::puts("guest_kernel_sys_ktimer passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..1bceba3297 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -7,6 +7,7 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified +- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified From b6f96055bf558fb3e156c359b389926e1f5ee3a7 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros <savvaniss@yahoo.gr> Date: Wed, 21 Oct 2026 20:01:05 +0300 Subject: [PATCH 0487/1360] feat(libSceGameLiveStreaming): implement sceGameLiveStreamingEnableLiveStreaming The signature, int sceGameLiveStreamingEnableLiveStreaming(const bool isEnableLiveStreaming), follows Orbital and fpPS4. No broadcast is ever on air, so both values succeed and change nothing. A call outside sceGameLiveStreamingInitialize and sceGameLiveStreamingTerminate throws, as the console's error for it is unknown. Monopoly Plus (CUSA03312) stopped at this call after its language screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceGameLiveStreaming/Export.cpp | 14 ++++++++++++-- core/libs/tests/GuestGameLiveStreaming.cpp | 15 +++++++++++++++ docs/dev/debt/libSceGameLiveStreaming.md | 5 ++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index 71b57a3129..8e859d0a15 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -3,7 +3,10 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceUserService/UserService.hpp" +#include <atomic> #include <cstring> +#include <stdexcept> +#include <string> struct GameLiveStreamingStatus2 { std::int32_t userId; @@ -17,14 +20,20 @@ struct GameLiveStreamingStatus2 { }; static_assert(sizeof(GameLiveStreamingStatus2) == 72); +namespace { +std::atomic<bool> g_initialized{false}; +} + extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { if (heap_size == 0) APS5_INVALID_ARG_EX; + g_initialized.store(true); return 0; } int APS5_VABI sceGameLiveStreamingTerminate(void) { + g_initialized.store(false); return 0; } @@ -250,8 +259,9 @@ int APS5_VABI sceGameLiveStreamingStopDebugBroadcast(void) { return 0; } -int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(bool isEnableLiveStreaming) { + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + (void)isEnableLiveStreaming; return 0; } diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp index a60c2538cd..a20b7b425c 100644 --- a/core/libs/tests/GuestGameLiveStreaming.cpp +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -22,6 +22,7 @@ int APS5_VABI sceGameLiveStreamingInitialize(std::size_t heap_size); int APS5_VABI sceGameLiveStreamingTerminate(void); int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status); int APS5_VABI sceGameLiveStreamingGetProgramInfo(void); +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(bool isEnableLiveStreaming); } namespace { @@ -30,6 +31,15 @@ void Require(bool value) { if (!value) std::abort(); } constexpr std::int32_t kInvalidUserId = -1; +bool EnableThrowsNotInitialized(bool isEnableLiveStreaming) { + try { + sceGameLiveStreamingEnableLiveStreaming(isEnableLiveStreaming); + } catch (const std::logic_error&) { + return true; + } + return false; +} + } int main() { @@ -40,9 +50,12 @@ int main() { threwZeroHeap = true; } Require(threwZeroHeap); + Require(EnableThrowsNotInitialized(true)); Require(sceGameLiveStreamingInitialize(1024 * 1024) == 0); + Require(sceGameLiveStreamingEnableLiveStreaming(true) == 0); + bool threwNullStatus = false; try { sceGameLiveStreamingGetCurrentStatus2(nullptr); @@ -75,5 +88,7 @@ int main() { } Require(threwProgramInfo); + Require(sceGameLiveStreamingEnableLiveStreaming(false) == 0); Require(sceGameLiveStreamingTerminate() == 0); + Require(EnableThrowsNotInitialized(false)); } diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index ae477a6346..b59e7aa102 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -2,7 +2,6 @@ ### Unknown function info -- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id - [sceGameLiveStreamingScreenInitialize](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingScreenInitializeSeparateModeParameter](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature @@ -46,3 +45,7 @@ - [sceGameLiveStreamingSetLinkCommentPreset](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingStartDebugBroadcast](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingStopDebugBroadcast](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature + +### Functional + +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - a call outside `sceGameLiveStreamingInitialize` and `sceGameLiveStreamingTerminate` throws, as the console's error for it is unknown ([fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L18) defines `SCE_GAME_LIVE_STREAMING_ERROR_NOT_INITIALIZED` but does not return it from this call). Otherwise the flag is accepted for both values and changes nothing, as no broadcast is ever on air to allow or block. The signature, `int sceGameLiveStreamingEnableLiveStreaming(const bool isEnableLiveStreaming)`, follows [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L810) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L111) From 424a12fd0ea9af35ee51e52595c0487cf084fd0d Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 20:11:27 +0200 Subject: [PATCH 0488/1360] fix(libSceNgs2.native): return the module's codes from sceNgs2SystemSetSampleRate and sceNgs2SystemSetGrainSamples --- .../libs/prx/libSceNgs2.native/src/System.cpp | 4 +- .../tests/Ngs2HandleErrors.cpp | 40 +++++++++++++++++++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 13 +----- docs/dev/debt/libSceNgs2.native.md | 5 ++- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index b4251c0f0e..93391baaf3 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -343,7 +343,7 @@ int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; - if (num_samples == 0 || num_samples > system->option.max_grain_samples) APS5_INVALID_ARG_EX; + if (num_samples == 0 || num_samples > system->option.max_grain_samples) return SCE_NGS2_ERROR_INVALID_NUM_GRAIN_SAMPLES; system->option.num_grain_samples = num_samples; return SCE_NGS2_OK; } @@ -352,7 +352,7 @@ int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sampl const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; - if (sample_rate == 0) APS5_INVALID_ARG_EX; + if (!ValidSampleRate(sample_rate)) return SCE_NGS2_ERROR_INVALID_SAMPLE_RATE; for (const auto* rack : system->racks) { for (const auto& voice : rack->voices) { for (const auto& filter : voice.filters) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index 230c58b289..a1f70cc5cd 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -177,6 +177,44 @@ static void CheckSystemOption() { CheckQueryBuffer(&valid, SCE_NGS2_OK, "a valid option"); } +static void CheckSetGrainSamples() { + constexpr int systemError = static_cast<int>(0x804A8201u); + constexpr int grainError = static_cast<int>(0x804A8310u); + const auto system = CreateSystem(); + Ngs2SystemInfo info{}; + Status(sceNgs2SystemSetGrainSamples(0x1234, 100), systemError, "an invalid system handle lost to an invalid grain"); + for (const std::uint32_t grain : {64u, 192u, 512u}) { + Status(sceNgs2SystemSetGrainSamples(system, grain), SCE_NGS2_OK, "a valid grain was refused"); + Status(sceNgs2SystemGetInfo(system, &info, sizeof(info)), SCE_NGS2_OK, "system info failed"); + Require(info.num_grain_samples == grain); + } + for (const std::uint32_t grain : {0u, 513u, 576u, 2048u, 0xffffffffu}) { + Status(sceNgs2SystemSetGrainSamples(system, grain), grainError, "an invalid grain was accepted"); + Status(sceNgs2SystemGetInfo(system, &info, sizeof(info)), SCE_NGS2_OK, "system info failed"); + Require(info.num_grain_samples == 512); + } + Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "system destroy failed"); +} + +static void CheckSetSampleRate() { + constexpr int systemError = static_cast<int>(0x804A8201u); + constexpr int rateError = static_cast<int>(0x804A8051u); + const auto system = CreateSystem(); + Ngs2SystemInfo info{}; + Status(sceNgs2SystemSetSampleRate(0x1234, 7000), systemError, "an invalid system handle lost to an invalid rate"); + for (const std::uint32_t rate : {11025u, 12000u, 22050u, 24000u, 44100u, 48000u, 88200u, 96000u, 176400u, 192000u}) { + Status(sceNgs2SystemSetSampleRate(system, rate), SCE_NGS2_OK, "a valid rate was refused"); + Status(sceNgs2SystemGetInfo(system, &info, sizeof(info)), SCE_NGS2_OK, "system info failed"); + Require(info.sample_rate == rate); + } + for (const std::uint32_t rate : {0u, 7000u, 8000u, 16000u, 32000u, 47999u, 48001u, 192001u, 0xffffffffu}) { + Status(sceNgs2SystemSetSampleRate(system, rate), rateError, "an invalid rate was accepted"); + Status(sceNgs2SystemGetInfo(system, &info, sizeof(info)), SCE_NGS2_OK, "system info failed"); + Require(info.sample_rate == 192000); + } + Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "system destroy failed"); +} + int main() { try { constexpr int systemError = static_cast<int>(0x804A8201u); @@ -211,6 +249,8 @@ int main() { Require(value == sentinel); CheckResetOption(); CheckSystemOption(); + CheckSetGrainSamples(); + CheckSetSampleRate(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 4186d67946..12d23873c9 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -700,10 +700,7 @@ static void TestReverbSetup() { Require(loudest > 0.0f && loudest < 1e-8f); Ngs2VoiceState state{}; Require(sceNgs2VoiceGetState(reverb, &state, sizeof(state)) == SCE_NGS2_OK && (state.state_flags & SCE_NGS2_VOICE_STATE_FLAG_INUSE) != 0); - Require(sceNgs2SystemSetSampleRate(system, 7000) == SCE_NGS2_OK); - bool lowRate = false; - try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { lowRate = true; } - Require(lowRate); + Require(sceNgs2SystemSetSampleRate(system, 7000) == SCE_NGS2_ERROR_INVALID_SAMPLE_RATE); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); Ngs2ReverbRackOption option{}; @@ -728,13 +725,7 @@ static void TestReverbSetup() { 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, 0) == SCE_NGS2_ERROR_INVALID_SAMPLE_RATE); Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); Ngs2SystemInfo info{}; Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.sample_rate == 96000); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 2ba78623d7..69aa322833 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -23,10 +23,11 @@ - [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. -- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before +- [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 null user data pointer throws. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [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 -- [NGS2 system option checks](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - not verified on a console. `sceNgs2SystemQueryBufferSize`, `sceNgs2SystemCreate` and `sceNgs2SystemCreateWithAllocator` return `0x804A8013` (size), `0x804A8311` (max grain, a multiple of 64 up to 2048), `0x804A8310` (grain, a multiple of 64 up to the max), `0x804A8051` (one of ten sample rates) and `0x804A8052` (1 to 37 channels) in that order, following [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4151-L4226). The four job scheduler pointers are neither inspected nor used. `sceNgs2SystemSetGrainSamples` still accepts any grain up to the maximum. +- [NGS2 system option checks](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - not verified on a console. `sceNgs2SystemQueryBufferSize`, `sceNgs2SystemCreate` and `sceNgs2SystemCreateWithAllocator` return `0x804A8013` (size), `0x804A8311` (max grain, a multiple of 64 up to 2048), `0x804A8310` (grain, a multiple of 64 up to the max), `0x804A8051` (one of ten sample rates) and `0x804A8052` (1 to 37 channels) in that order, following [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4151-L4226). The four job scheduler pointers are neither inspected nor used. +- [sceNgs2SystemSetGrainSamples, sceNgs2SystemSetSampleRate](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - a grain that is not a multiple of 64 is accepted, where the module returns `0x804A8310`, because the NGS2 tests render 8-sample grains. Changing the rate while a sampler filter is enabled throws, as the filter coefficients were computed for the old rate. After the handle check, a grain of 0 or above the system's maximum returns `0x804A8310`, and a rate other than 11025, 12000, 22050, 24000, 44100, 48000, 88200, 96000, 176400 or 192000 returns `0x804A8051`, per [prosper's reading of the module's command validators](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4355-L4399); not measured on a console - [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 - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. From 62799443d59bccde0414ae7e347f20c2b2def74f Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 20:15:07 +0200 Subject: [PATCH 0489/1360] fix(libSceNgs2.native): return the module's buffer and allocator errors from sceNgs2SystemCreate and sceNgs2SystemCreateWithAllocator --- .../libSceNgs2.native/include/Ngs2Types.hpp | 2 + .../libs/prx/libSceNgs2.native/src/System.cpp | 5 +- .../tests/Ngs2HandleErrors.cpp | 65 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index e964c63b51..5e2e950d9c 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -10,6 +10,8 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast<int>(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast<int>(0x804A8011); static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast<int>(0x804A8013); +static constexpr int SCE_NGS2_ERROR_INVALID_BUFFER_INFO = static_cast<int>(0x804A8020); +static constexpr int SCE_NGS2_ERROR_INVALID_BUFFER_ALLOCATOR = static_cast<int>(0x804A8023); static constexpr int SCE_NGS2_ERROR_INVALID_SAMPLE_RATE = static_cast<int>(0x804A8051); static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast<int>(0x804A8052); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast<int>(0x804A8201); diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index b4251c0f0e..a9f22f68d8 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -198,7 +198,7 @@ int Ngs2ReleaseBuffer(const Ngs2BufferAllocator& allocator, Ngs2ContextBufferInf } static Ngs2ContextBufferInfo Allocate(const Ngs2BufferAllocator* allocator, std::size_t size) { - if (allocator == nullptr || allocator->alloc_handler == nullptr || allocator->free_handler == nullptr) APS5_INVALID_ARG_EX; + if (allocator->free_handler == nullptr) APS5_INVALID_ARG_EX; Ngs2ContextBufferInfo bufferInfo{}; bufferInfo.host_buffer_size = size; bufferInfo.user_data = allocator->user_data; @@ -273,16 +273,17 @@ int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption* option, Ngs2C } int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption* option, const Ngs2ContextBufferInfo* buffer_info, uintptr_t* handle) { + if (buffer_info == nullptr || buffer_info->host_buffer == nullptr) return SCE_NGS2_ERROR_INVALID_BUFFER_INFO; if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; Ngs2SystemOption checked{}; const int result = CheckSystemOption(option, checked); if (result != SCE_NGS2_OK) return result; - if (buffer_info == nullptr) APS5_INVALID_ARG_EX; const Ngs2Lock lock; return CreateSystem(checked, *buffer_info, {}, handle); } int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption* option, const Ngs2BufferAllocator* allocator, uintptr_t* handle) { + if (allocator == nullptr || allocator->alloc_handler == nullptr) return SCE_NGS2_ERROR_INVALID_BUFFER_ALLOCATOR; if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; Ngs2SystemOption checked{}; const int result = CheckSystemOption(option, checked); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index 230c58b289..2af6c7dc55 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -108,6 +108,70 @@ static void CheckQueryBuffer(const Ngs2SystemOption* option, int expected, const Require((query.host_buffer_size != 0) == (expected == SCE_NGS2_OK)); } +static void CheckCreateArguments() { + constexpr int outputError = static_cast<int>(0x804A8010u); + constexpr int bufferError = static_cast<int>(0x804A8020u); + constexpr int allocatorError = static_cast<int>(0x804A8023u); + constexpr int rateError = static_cast<int>(0x804A8051u); + constexpr uintptr_t sentinel = 0x5a5a; + const auto valid = Option(); + const auto badRate = Option(512, 256, 7000); + Ngs2ContextBufferInfo query{}; + Status(sceNgs2SystemQueryBufferSize(&valid, &query), SCE_NGS2_OK, "system buffer query failed"); + std::vector<std::uint64_t> storage(query.host_buffer_size / sizeof(std::uint64_t) + 1); + Ngs2ContextBufferInfo buffer{}; + buffer.host_buffer = storage.data(); + buffer.host_buffer_size = query.host_buffer_size; + Ngs2ContextBufferInfo noHost = buffer; + noHost.host_buffer = nullptr; + const Ngs2BufferAllocator allocator{CountedAllocate, CountedRelease, 0}; + const Ngs2BufferAllocator noAlloc{nullptr, CountedRelease, 0}; + + struct CreateCase { + const Ngs2SystemOption* option; + const Ngs2ContextBufferInfo* buffer; + bool handle; + int expected; + const char* label; + }; + for (const auto& c : {CreateCase{&valid, nullptr, true, bufferError, "a null buffer info"}, + CreateCase{&valid, &noHost, true, bufferError, "a null host buffer"}, + CreateCase{&badRate, nullptr, false, bufferError, "a null buffer info, handle and bad option"}, + CreateCase{&badRate, &noHost, false, bufferError, "a null host buffer, handle and bad option"}, + CreateCase{&badRate, &buffer, false, outputError, "a null handle and bad option"}, + CreateCase{&badRate, &buffer, true, rateError, "a bad option"}}) { + uintptr_t created = sentinel; + Returned(sceNgs2SystemCreate(c.option, c.buffer, c.handle ? &created : nullptr), c.expected, "sceNgs2SystemCreate", c.label); + Require(created == sentinel); + } + uintptr_t created = sentinel; + Returned(sceNgs2SystemCreate(nullptr, &buffer, &created), SCE_NGS2_OK, "sceNgs2SystemCreate", "a null option"); + Status(sceNgs2SystemDestroy(created, nullptr), SCE_NGS2_OK, "system destroy failed"); + + struct AllocatorCase { + const Ngs2SystemOption* option; + const Ngs2BufferAllocator* allocator; + bool handle; + int expected; + const char* label; + }; + for (const auto& c : {AllocatorCase{&valid, nullptr, true, allocatorError, "a null allocator"}, + AllocatorCase{&valid, &noAlloc, true, allocatorError, "a null alloc handler"}, + AllocatorCase{&badRate, nullptr, false, allocatorError, "a null allocator, handle and bad option"}, + AllocatorCase{&badRate, &noAlloc, false, allocatorError, "a null alloc handler, handle and bad option"}, + AllocatorCase{&badRate, &allocator, false, outputError, "a null handle and bad option"}, + AllocatorCase{&badRate, &allocator, true, rateError, "a bad option"}}) { + uintptr_t allocated = sentinel; + Returned(sceNgs2SystemCreateWithAllocator(c.option, c.allocator, c.handle ? &allocated : nullptr), c.expected, "sceNgs2SystemCreateWithAllocator", c.label); + Require(allocated == sentinel && allocations == 0); + } + uintptr_t allocated = sentinel; + Returned(sceNgs2SystemCreateWithAllocator(nullptr, &allocator, &allocated), SCE_NGS2_OK, "sceNgs2SystemCreateWithAllocator", "a null option"); + Require(allocations == 1); + Status(sceNgs2SystemDestroy(allocated, nullptr), SCE_NGS2_OK, "system destroy failed"); + Require(allocations == 0); +} + static void CheckSystemOption() { constexpr int outputError = static_cast<int>(0x804A8010u); constexpr int sizeError = static_cast<int>(0x804A8013u); @@ -209,6 +273,7 @@ int main() { Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); Require(value == sentinel); + CheckCreateArguments(); CheckResetOption(); CheckSystemOption(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 2ba78623d7..ddcc55caed 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -28,6 +28,7 @@ - [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 - [NGS2 system option checks](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - not verified on a console. `sceNgs2SystemQueryBufferSize`, `sceNgs2SystemCreate` and `sceNgs2SystemCreateWithAllocator` return `0x804A8013` (size), `0x804A8311` (max grain, a multiple of 64 up to 2048), `0x804A8310` (grain, a multiple of 64 up to the max), `0x804A8051` (one of ten sample rates) and `0x804A8052` (1 to 37 channels) in that order, following [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4151-L4226). The four job scheduler pointers are neither inspected nor used. `sceNgs2SystemSetGrainSamples` still accepts any grain up to the maximum. - [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 +- [sceNgs2SystemCreateWithAllocator free handler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - an allocator with an `alloc_handler` but no `free_handler` throws after the option checks: the module only checks the `alloc_handler`, and what it does without a free handler is unknown. Otherwise `sceNgs2SystemCreate` returns `0x804A8020` for a null buffer info or host buffer, and `sceNgs2SystemCreateWithAllocator` `0x804A8023` for a null allocator or `alloc_handler`, before the null-handle `0x804A8010` and the option checks, per [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4246-L4334); not measured on a console - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw From 58afce1ad1a286897fc5c4f535466e3395df6528 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Tue, 20 Oct 2026 16:43:01 +0200 Subject: [PATCH 0490/1360] fix(agc): follow RDNA 2's range checks for zero-stride vertex fetches --- .../src/Driver/Memory/WrittenBuffers.cpp | 3 +- .../Graphics/include/VertexInput.hpp | 14 +++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 63 +++++++++++++++---- .../prx/libSceAgcDriver/tests/VertexInput.cpp | 17 ++++- .../tests/execution/ZeroStrideVertexFetch.cpp | 26 ++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 6 files changed, 98 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index 8b0687ac4f..58fa8648a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -181,10 +181,11 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> QueuedDrawReadRanges(const } if (stages.empty() || stages.front().program == nullptr) return ranges; for (const auto& attribute : stages.front().program->vertexAttributes) { - if (Graphics::NullVertexDescriptor(attribute) || Graphics::VertexFetchOutOfRange(attribute)) continue; + if (Graphics::NullVertexDescriptor(attribute)) continue; const auto address = attribute.resource.fields[0] | (static_cast<std::uint64_t>(attribute.resource.fields[1] & 0xffffu) << 32u); std::uint64_t bytes = 0; try { + if (Graphics::VertexFetchOutOfRange(attribute)) continue; bytes = Graphics::VertexBufferExtent(attribute); } catch (const std::runtime_error&) { continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index a8d93a9a85..aad386bfc1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -230,14 +230,16 @@ inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::Verte inline bool VertexFetchOutOfRange(const ShaderRecompiler::VertexAttribute& attribute) { const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + if (stride != 0 || NullVertexDescriptor(attribute)) return false; const auto select = VertexBufferOutOfBoundsSelect(attribute); - return stride == 0 && (select == 2u || select == 3u) && attribute.resource.fields[2] == 0; + const auto records = attribute.resource.fields[2]; + if (select == 0u) return true; + if (select == 3u) return records < DecodeVertexFormat(attribute).bytes; + return records == 0; } inline std::uint64_t ZeroStrideAvailableBytes(const ShaderRecompiler::VertexAttribute& attribute) { - const auto records = attribute.resource.fields[2]; - if (VertexBufferOutOfBoundsSelect(attribute) == 2u && records != 0) return std::max<std::uint64_t>(records, DecodeVertexFormat(attribute).bytes); - return records; + return VertexFetchOutOfRange(attribute) ? 0u : DecodeVertexFormat(attribute).bytes; } inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& attribute) { @@ -261,7 +263,9 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute& Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); const auto index = attribute.fetchIndex == 0 ? maxIndex : firstInstance + instances - 1u; const auto bytes = DecodeVertexFormat(attribute).bytes; - Require(stride == 0 || VertexBufferOutOfBoundsSelect(attribute) <= 1u || index < records, "vertex fetch exceeds descriptor record count"); + const auto select = VertexBufferOutOfBoundsSelect(attribute); + const auto recordsUnchecked = stride == 0 ? select != 1u : select <= 1u; + Require(recordsUnchecked || index < records, "vertex fetch exceeds descriptor record count"); const auto required = static_cast<std::uint64_t>(stride) * index + bytes; Require(stride != 0 || required <= ZeroStrideAvailableBytes(attribute), "vertex fetch exceeds descriptor byte range"); Require(required <= std::numeric_limits<std::size_t>::max(), "vertex fetch exceeds addressable byte range"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 49ca8b83cb..428c1ce75e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9890,26 +9890,63 @@ void validationTests() { attribute.resource.fields[3] = 77u << 12u; attribute.fetchIndex = 1; attribute.resource.fields[1] = 0; - attribute.resource.fields[2] = 16; + const auto zeroStride = [&](std::uint32_t select, std::uint32_t records) { + attribute.resource.fields[3] = (77u << 12u) | (select << 28u); + attribute.resource.fields[2] = records; + }; + for (const auto records : {0u, 1u, 16u, 0xffffffffu}) { + zeroStride(0, records); + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 0 must read every zero-stride fetch as zero"); + Require(AgcDriver::Graphics::ZeroStrideAvailableBytes(attribute) == 0, "OOB_SELECT 0 zero-stride fetch has bytes to read"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferExtent(attribute); }, "empty or oversized"); + } + zeroStride(1, 16); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value"); Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value"); - attribute.resource.fields[2] = 8; - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); - Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range"); - attribute.resource.fields[3] = (77u << 12u) | (2u << 28u); - attribute.resource.fields[2] = 1; + Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 1 extent must cover the whole fetch"); + zeroStride(1, 0); + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 without records must read zero"); + zeroStride(1, 8); + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 counted NUM_RECORDS in bytes"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 8) == 16, "OOB_SELECT 1 must read the whole element for every index below NUM_RECORDS"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 7) == 16, "OOB_SELECT 1 must read the whole element for the last index below NUM_RECORDS"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 9); }, "record count"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 8); }, "record count"); + zeroStride(1, 1); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 1 with one record must read the element at index 0"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2); }, "record count"); + attribute.fetchIndex = 0; + zeroStride(1, 3); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 16, "OOB_SELECT 1 must read the element for index 2 of 3 records"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); + attribute.fetchIndex = 1; + zeroStride(2, 1); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 2 must read a zero-stride fetch whole while NUM_RECORDS is nonzero"); Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 2 extent must cover the whole fetch"); Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 with records was treated as out of range"); - attribute.resource.fields[2] = 0; + zeroStride(2, 0); Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 without records must read zero"); - attribute.resource.fields[3] = (77u << 12u) | (3u << 28u); + zeroStride(3, 0); Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 without records must read zero"); - attribute.resource.fields[2] = 8; - Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with records was treated as out of range"); - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); - attribute.resource.fields[2] = 16; - Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 3 must read a fetch inside the range"); + for (const auto records : {1u, 8u, 15u}) { + zeroStride(3, records); + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 must read a fetch that only partly fits as zero"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); + } + for (const auto records : {16u, 17u, 0xffffffffu}) { + zeroStride(3, records); + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with the whole fetch in range was treated as out of range"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 3 must read a fetch inside the range, whatever the index"); + Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 3 extent must cover only the fetch"); + } + attribute.resource.fields[3] = (22u << 12u) | (3u << 28u); + attribute.resource.fields[2] = 4; + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 compared NUM_RECORDS with the wrong fetch size"); + attribute.resource.fields[2] = 3; + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 read a 4-byte fetch past 3 bytes"); + attribute.resource.fields[3] = (113u << 12u) | (3u << 28u); + expectFailure([&] { AgcDriver::Graphics::VertexFetchOutOfRange(attribute); }, "unsupported vertex format"); attribute.resource.fields[3] = 113u << 12u; expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format"); attribute.resource.fields[3] = 50u << 12u; diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp index f0f999a04c..5d56b47f05 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -151,10 +151,25 @@ void run() { const auto zeroStride = live.TryGet(101, input); Require(VertexFetchOutOfRange(input[0]), "zero records did not select zero fill"); input[0].resource.fields[2] = 64; - Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 64, "cached zero-stride layout froze the live zero-fill decision"); + Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "cached zero-stride layout froze the live zero-fill decision"); input[0].resource.fields[2] = 0; Require(live.TryGet(101, input) == zeroStride && VertexFetchOutOfRange(input[0]), "record count zero reused a live fetch"); input[0] = attribute(); + input[0].resource.fields[1] = 0; + input[0].resource.fields[3] |= 3u << 28u; + input[0].resource.fields[2] = 16; + const auto wholeFetch = live.TryGet(105, input); + for (const auto records : {0u, 8u, 15u}) { + input[0].resource.fields[2] = records; + Require(live.TryGet(105, input) == wholeFetch && VertexFetchOutOfRange(input[0]), "a fetch that only partly fits was not read as zero"); + } + for (const auto records : {16u, 17u, 0xffffffffu}) { + input[0].resource.fields[2] = records; + Require(live.TryGet(105, input) == wholeFetch && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "a fetch inside the byte range was read as zero or sized by NUM_RECORDS"); + } + input[0].resource.fields[3] &= ~(3u << 28u); + Require(VertexFetchOutOfRange(input[0]), "OOB_SELECT 0 read a zero-stride fetch"); + input[0] = attribute(); input[0].formatComponents = 1; const auto narrow = live.TryGet(102, input); input[0].formatComponents = 2; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index 5715c39d27..e5804bf70a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -142,14 +142,28 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - constexpr std::array<Case, 7> cases{{ - {2u, 1u, true, "OOB_SELECT 2, one record"}, + constexpr std::uint32_t Wzyx = 7u | (6u << 3u) | (1u << 6u) | (0u << 9u); + constexpr std::array<Case, 20> cases{{ + {0u, 0u, false, "OOB_SELECT 0, no records"}, + {0u, 1u, false, "OOB_SELECT 0, one record"}, + {0u, 16u, false, "OOB_SELECT 0, records cover the element's bytes"}, + {0u, 0xffffffffu, false, "OOB_SELECT 0, NUM_RECORDS 0xffffffff"}, + {0u, 0xffffffffu, false, "OOB_SELECT 0, NUM_RECORDS 0xffffffff, selectors W Z 1 0", Wzyx}, + {1u, 0u, false, "OOB_SELECT 1, no records"}, + {1u, 3u, true, "OOB_SELECT 1, NUM_RECORDS counts the three vertices' records"}, + {1u, 4u, true, "OOB_SELECT 1, NUM_RECORDS past the vertices"}, + {1u, 0xffffffffu, true, "OOB_SELECT 1, NUM_RECORDS 0xffffffff"}, + {1u, 3u, true, "OOB_SELECT 1, NUM_RECORDS 3, selectors W Z 1 0", Wzyx}, {2u, 0u, false, "OOB_SELECT 2, no records"}, - {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, + {2u, 1u, true, "OOB_SELECT 2, one record"}, + {2u, 16u, true, "OOB_SELECT 2, whole element in range"}, {3u, 0u, false, "OOB_SELECT 3, no records"}, - {0u, 16u, true, "OOB_SELECT 0, whole element in range"}, - {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, - {3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, + {3u, 8u, false, "OOB_SELECT 3, the element only partly fits"}, + {3u, 15u, false, "OOB_SELECT 3, the element misses its last byte"}, + {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, + {3u, 0xffffffffu, true, "OOB_SELECT 3, NUM_RECORDS 0xffffffff"}, + {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", Wzyx}, + {3u, 8u, false, "OOB_SELECT 3, partly fits, selectors W Z 1 0", Wzyx}, }}; for (const auto& value : cases) { Draw(*device, value); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 449bc18609..ad2915b79d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -45,7 +45,7 @@ - [Buffer descriptor CACHE_SWIZZLE hint](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - bit 30 of word 1 in a buffer resource descriptor (`words[1] & 0x40000000u`) is normalized to zero when binding guest buffers in `ShaderResources::addGuestBuffer`. On GFX10 (gfx1013), bit 30 of V# word 1 is `CACHE_SWIZZLE` (Mesa's `gfx10-rsrc.json`: STRIDE [29:16], CACHE_SWIZZLE [30], SWIZZLE_ENABLE [31]), a cache-bank hint with no effect on the address or data, which is dropped here. - [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. - [Vertex fetch overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a zero-padded vertex fetch whose valid range (`num_records * stride`) ends less than one stride past the end of the readable memory at its base copies only the readable bytes and reads the rest as zeros, instead of failing the draw; a longer overhang still fails. This extends the strided buffer overhang rule above (the last record is the rounded-up `num_records`) to the staging copy and is not measured on hardware. The observed case is a fetch of 14858 records of 44 bytes that ends 8 bytes past the end of the 0xa0000-byte mapping holding it. -- [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. +- [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a draw whose indices span NUM_RECORDS of a stride-0 `OOB_SELECT` 1 V# (some below, some at or past) throws: the pipeline binding has stride 0, so no vertex can read zero while another reads the element. An indirect draw is not checked. Unmeasured on gfx1013 (the console), with an instruction offset or SOFFSET, with a swizzled V# and in a vertex shader. Rules from #8299, measured on an RX 6700 XT (gfx1031) with raw ISA through the hardware oracle ([data](https://gist.github.com/thuanquang/3b25d677c1a32d79123dd2df1819dd83)): `buffer_load_format_x/xy/xyzw` and `buffer_load_dwordx4`, stride 0 and 16, all NUM_RECORDS. Stride 0 reads zero for `OOB_SELECT` 0 always; for 1 when index >= NUM_RECORDS (records); for 2 when NUM_RECORDS is 0; for 3 when NUM_RECORDS is under the fetch's bytes. Untyped loads check each dword under 3; only typed fetches are used here. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceAgcDriverRegisterMultipleResources](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown From bea48f5ee010915d29bf1dc8817e24f0a360aced Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 13:34:37 +0200 Subject: [PATCH 0491/1360] test(agc): keep the zero-stride render-target alias check on a fetch that reads --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 888c3485c1..30747ce5d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8940,8 +8940,8 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor const std::array range{std::pair{address + unit, address + 2 * unit}}; Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending"); }; - const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) { - return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), 77u << 12u}}, fetchIndex}; + const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex, std::uint32_t outOfBoundsSelect = 0) { + return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), (77u << 12u) | (outOfBoundsSelect << 28u)}}, fetchIndex}; }; const auto check = [&](std::uint32_t edge) { std::vector<std::byte> texels(64 * 64 * 4); @@ -9012,9 +9012,12 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target"); } reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target"); - vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)}; + vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex, 3)}; reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target"); } + vertex.vertexAttributes = {attributeFor(address, 0, 0)}; + Draw(context, consumer, triangle, shaders); + WaitForOptimizedPipelines(context.device); std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n"; return true; } From a3d739f170d9147d8fd52b5e51aa0bafca97f818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 21 Oct 2026 19:55:34 +0200 Subject: [PATCH 0492/1360] docs(agc): record the all-past OOB_SELECT 1 throw and the gfx1010 measurement Co-Authored-By: Claude <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ad2915b79d..44cde2dbcc 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -45,7 +45,7 @@ - [Buffer descriptor CACHE_SWIZZLE hint](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - bit 30 of word 1 in a buffer resource descriptor (`words[1] & 0x40000000u`) is normalized to zero when binding guest buffers in `ShaderResources::addGuestBuffer`. On GFX10 (gfx1013), bit 30 of V# word 1 is `CACHE_SWIZZLE` (Mesa's `gfx10-rsrc.json`: STRIDE [29:16], CACHE_SWIZZLE [30], SWIZZLE_ENABLE [31]), a cache-bank hint with no effect on the address or data, which is dropped here. - [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. - [Vertex fetch overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a zero-padded vertex fetch whose valid range (`num_records * stride`) ends less than one stride past the end of the readable memory at its base copies only the readable bytes and reads the rest as zeros, instead of failing the draw; a longer overhang still fails. This extends the strided buffer overhang rule above (the last record is the rounded-up `num_records`) to the staging copy and is not measured on hardware. The observed case is a fetch of 14858 records of 44 bytes that ends 8 bytes past the end of the 0xa0000-byte mapping holding it. -- [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a draw whose indices span NUM_RECORDS of a stride-0 `OOB_SELECT` 1 V# (some below, some at or past) throws: the pipeline binding has stride 0, so no vertex can read zero while another reads the element. An indirect draw is not checked. Unmeasured on gfx1013 (the console), with an instruction offset or SOFFSET, with a swizzled V# and in a vertex shader. Rules from #8299, measured on an RX 6700 XT (gfx1031) with raw ISA through the hardware oracle ([data](https://gist.github.com/thuanquang/3b25d677c1a32d79123dd2df1819dd83)): `buffer_load_format_x/xy/xyzw` and `buffer_load_dwordx4`, stride 0 and 16, all NUM_RECORDS. Stride 0 reads zero for `OOB_SELECT` 0 always; for 1 when index >= NUM_RECORDS (records); for 2 when NUM_RECORDS is 0; for 3 when NUM_RECORDS is under the fetch's bytes. Untyped loads check each dword under 3; only typed fetches are used here. +- [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a draw whose highest index is at or past a nonzero NUM_RECORDS of a stride-0 `OOB_SELECT` 1 V# throws: the pipeline binding has stride 0, so vertices cannot differ. Only the highest index is checked, so a draw with every index past it throws too, where the hardware reads zero. An indirect draw is not checked. Unmeasured on gfx1013 (the console), with an instruction offset or SOFFSET, with a swizzled V# and in a vertex shader. Rules from #8299, measured on an RX 6700 XT (gfx1031), same results on gfx1010, with raw ISA ([data](https://gist.github.com/thuanquang/3b25d677c1a32d79123dd2df1819dd83)). Stride 0 reads zero for `OOB_SELECT` 0 always; for 1 when index >= NUM_RECORDS (records); for 2 when NUM_RECORDS is 0; for 3 when NUM_RECORDS is under the fetch's bytes. Untyped loads check each dword under 3; only typed fetches are used here. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceAgcDriverRegisterMultipleResources](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown From 738308a2bef17e67e3e74a4ae5bb8a3e3046af66 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 20:17:38 +0200 Subject: [PATCH 0493/1360] fix(libSceNgs2.native): return the module's errors for render buffer counts, types and null buffers --- .../libSceNgs2.native/include/Ngs2Types.hpp | 5 + .../libs/prx/libSceNgs2.native/src/System.cpp | 9 ++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 91 ++++++++++++++++++- docs/dev/debt/libSceNgs2.native.md | 2 +- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index a604adba3d..ced44887d1 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -11,6 +11,9 @@ static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast<int>(0x804 static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast<int>(0x804A8011); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast<int>(0x804A8201); static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast<int>(0x804A8202); +static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE = static_cast<int>(0x804A8050); +static constexpr int SCE_NGS2_ERROR_INVALID_RENDER_BUFFER = static_cast<int>(0x804A8251); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_RENDER_BUFFERS = static_cast<int>(0x804A830B); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast<int>(0x804A8305); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast<int>(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast<int>(0x804A8431); @@ -26,7 +29,9 @@ static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SAMPLER = 0x4001; 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_I16B = 0x13; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32L = 0x18; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32B = 0x19; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_ATRAC9 = 0x40; static constexpr std::uint32_t SCE_NGS2_VOICE_EVENT_PLAY = 1; diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 99eede7a7c..7e3e3c0f06 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -390,7 +390,16 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe system = Ngs2FindSystem(system_handle); } if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (num_buffer_info > 31) return SCE_NGS2_ERROR_INVALID_NUM_RENDER_BUFFERS; if (buffer_info == nullptr || num_buffer_info == 0) APS5_INVALID_ARG_EX; + for (std::uint32_t i = 0; i < num_buffer_info; i++) { + const auto waveform_type = buffer_info[i].waveform_type; + if (waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16B && + waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_F32L && waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_F32B) { + return SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE; + } + if (buffer_info[i].buffer == nullptr) return SCE_NGS2_ERROR_INVALID_RENDER_BUFFER; + } SystemLockState* state = nullptr; { const auto self = std::this_thread::get_id(); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 4186d67946..d14ebc118a 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -1407,6 +1407,81 @@ static void TestRenderCapacity(std::uint32_t waveformType, std::uint32_t channel Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestRenderBufferCount() { + const auto system = CreateSystem(); + std::vector<int16_t> out(Grain); + Ngs2RenderBufferInfo infos[32]; + for (int i = 0; i < 32; i++) { + infos[i] = Ngs2RenderBufferInfo{out.data(), out.size() * sizeof(int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + } + Require(sceNgs2SystemRender(system, infos, 31) == SCE_NGS2_OK); + Require(sceNgs2SystemRender(system, infos, 32) == SCE_NGS2_ERROR_INVALID_NUM_RENDER_BUFFERS); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRenderBufferTypeValidation() { + const auto system = CreateSystem(); + std::vector<int16_t> out(Grain); + Ngs2RenderBufferInfo info0{out.data(), out.size() * sizeof(int16_t), 0x50, 1}; + Require(sceNgs2SystemRender(system, &info0, 1) == SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE); + Ngs2RenderBufferInfo valid{out.data(), out.size() * sizeof(int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + Ngs2RenderBufferInfo infos[2]; + infos[0] = valid; + infos[1] = Ngs2RenderBufferInfo{out.data(), out.size() * sizeof(int16_t), 0x50, 1}; + Require(sceNgs2SystemRender(system, infos, 2) == SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRenderBufferTypeBeforeNull() { + const auto system = CreateSystem(); + std::vector<int16_t> out(Grain); + Ngs2RenderBufferInfo info{nullptr, out.size() * sizeof(int16_t), 0x50, 1}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRenderBufferNull() { + const auto system = CreateSystem(); + std::vector<int16_t> out(Grain); + Ngs2RenderBufferInfo info{nullptr, out.size() * sizeof(int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_ERROR_INVALID_RENDER_BUFFER); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRenderRefusedDoesNotRender() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + std::vector<std::int16_t> pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector<int16_t> out(Grain * 2, 123); + Ngs2RenderBufferInfo infos[2]; + infos[0] = Ngs2RenderBufferInfo{out.data(), out.size() * sizeof(int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + infos[1] = Ngs2RenderBufferInfo{out.data() + Grain, out.size() * sizeof(int16_t), 0x50, 1}; + Require(sceNgs2SystemRender(system, infos, 2) == SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE); + Require(std::all_of(out.begin(), out.end(), [](int16_t value) { return value == 123; })); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRenderBigEndianThrows() { + const auto system = CreateSystem(); + std::vector<int16_t> out(Grain); + Ngs2RenderBufferInfo infoI16B{out.data(), out.size() * sizeof(int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16B, 1}; + bool i16bThrows = false; + try { sceNgs2SystemRender(system, &infoI16B, 1); } catch (const std::runtime_error&) { i16bThrows = true; } + Require(i16bThrows); + std::vector<float> outF(Grain); + Ngs2RenderBufferInfo infoF32B{outF.data(), outF.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32B, 1}; + bool f32bThrows = false; + try { sceNgs2SystemRender(system, &infoF32B, 1); } catch (const std::runtime_error&) { f32bThrows = true; } + Require(f32bThrows); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestSilentRenderValidation() { const auto system = CreateSystem(); std::vector<float> out(Grain, 123.0f); @@ -1425,12 +1500,12 @@ static void TestSilentRenderValidation() { invalid(); info.num_channels = 1; info.waveform_type = UINT32_MAX; - bool unsupported = false; - try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { unsupported = true; } - Require(unsupported); + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_ERROR_INVALID_WAVEFORM_TYPE); + Require(std::all_of(out.begin(), out.end(), [](float value) { return value == 123.0f; })); + info.waveform_type = SCE_NGS2_WAVEFORM_TYPE_PCM_F32L; + info.buffer = nullptr; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_ERROR_INVALID_RENDER_BUFFER); Require(std::all_of(out.begin(), out.end(), [](float value) { return value == 123.0f; })); - info = {}; - Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -1488,6 +1563,12 @@ int main() { TestRenderCapacity<float>(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2); TestRenderCapacity<std::int16_t>(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1); TestRenderCapacity<std::int16_t>(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2); + TestRenderBufferCount(); + TestRenderBufferTypeValidation(); + TestRenderBufferTypeBeforeNull(); + TestRenderBufferNull(); + TestRenderRefusedDoesNotRender(); + TestRenderBigEndianThrows(); TestSilentRenderValidation(); TestAllocator(); exitSystem = CreateSystem(); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..f2241904df 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -6,7 +6,7 @@ - [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. -- [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. +- [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. A zero buffer count throws: the module accepts it, but whether it renders voices is unknown. Before rendering, over 31 buffers return `0x804A830B`, a type other than 0x12, 0x13, 0x18 or 0x19 `0x804A8050` and a NULL buffer `0x804A8251`, per [prosper's reading](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/hle_audio.cpp#L4582-L4606); 0x13 and 0x19 then throw. Surplus capacity is unmeasured; channel and size refusals throw. - [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. From 9998385047f557cef18cf00d3260d61f8e599cd3 Mon Sep 17 00:00:00 2001 From: Tamalero <11452991+Tamalero@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:34:25 -0700 Subject: [PATCH 0494/1360] fix(recompiler): match image-table T# halves offset before and after readfirstlane A T# loaded as two s_load_dwordx4 halves from a pointer table, where the second half's offset is formed per lane (v_lshl_add_u32 key, 5, 16) before v_readfirstlane and the first half's after it (readfirstlane(key) << 5), was refused: the second half's +16 sat inside ReadFirstLane(Select(exec, ..., old), exec), so TryMakeTableColumn saw a different base and a zero addend. Compare such offsets in a first-lane normal form (lane value, mask, shift, addend), peeling selects the mask implies as the SRT walker already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 39 ++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 45 ++++++++++++++++++- docs/dev/debt/shader.md | 2 +- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a754f42271..3b602479d0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3551,6 +3551,43 @@ void verifyWaterfallImageTable() { require(!SrtWalker().ValidateRuntimeValue(planA, &firstOther), "waterfall image table: non-implied condition was accepted by validator"); } +void verifySplitWaterfallImageTable() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture texture; + alignas(256) static std::array<std::uint32_t, 0x3000> root{}; + static std::array<std::uint32_t, 4> output{}; + const auto outputV = TableV(output.data(), 0u, static_cast<std::uint32_t>(sizeof(output))); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + const auto descriptor = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D); + std::copy(descriptor.begin(), descriptor.end(), root.begin() + 0x40u / 4u); + const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data())); + const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)}; + // v_cmpx_gt_u32 exec, 6, v0; s17 = v_readfirstlane(v0); v1 = v_lshl_add_u32(v0, 5, addend) under that exec; + // s19 = v_readfirstlane(v1); s18 = s17 << 5; s_load_dwordx4 s[36:39] at s18 + 0x40 and s[40:43] at s19 + 0x40: + // the two halves of one T# record, with the second half's offset added before the lane read. + const auto code = [](std::uint32_t addend) { + return std::vector<std::uint32_t>{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7da80086u, 0x7e220500u, 0xd7460001u, 0x00010b00u | (addend << 18u), 0x7e260501u, 0x8f128511u, + 0xf4080900u, 0x24000040u, 0xf4080a00u, 0x26000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + }; + const auto split = code(0x90u); + auto request = TableRequest(split, userData); + request.shader.codeAddress = 0x3a000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "split waterfall image table: the T# was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.offset == 0x40u && column.dwordCount == 8u && column.foldedCarry == 16u, "split waterfall image table: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require((snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[0]).dwords == descriptor, "split waterfall image table: record 0 was not read whole"); + for (const std::uint32_t addend : {0xa0u, 0x88u}) { + const auto otherCode = code(addend); + auto other = TableRequest(otherCode, userData); + other.shader.codeAddress = 0x3b000u + addend * 0x100u; + expectFailure([&] { static_cast<void>(GetResourcePlan(other)); }, "GetImageResource dword 0 is not a valid runtime value", "split waterfall image table: a second half outside the record was accepted"); + } +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -7622,6 +7659,7 @@ int main(int argc, char** argv) { verifyChainedTableOffset(); verifyPointerChainImageTable(); verifyWaterfallImageTable(); + verifySplitWaterfallImageTable(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -7685,6 +7723,7 @@ int main(int argc, char** argv) { verifyChainedTableOffset(); verifyPointerChainImageTable(); verifyWaterfallImageTable(); + verifySplitWaterfallImageTable(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 6f52b4489a..0ffeb3be74 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -577,6 +577,38 @@ private: return value; } + struct FirstLaneOffset { + const IrValue* lane = nullptr; + const IrValue* mask = nullptr; + std::uint32_t shift = 0; + std::uint32_t addend = 0; + }; + + static bool ParseFirstLaneOffset(IrValue* value, const IrValue* mask, FirstLaneOffset& form, std::uint32_t depth) { + std::uint32_t addend = 0; + value = SplitImmediateAddend(value, addend); + while (mask != nullptr && Detail::IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3u && Detail::ActiveMaskImplies(mask, value->Argument(0))) { + value = SplitImmediateAddend(value->Argument(1), addend); + } + std::uint32_t shift = 0; + if (depth != 0u && value->Opcode() == IrOpcode::ShiftLeftLogical32 && value->ArgumentCount() == 2u && immediateU32(value->Argument(1), shift) && shift < 32u) { + if (!ParseFirstLaneOffset(value->Argument(0)->Resolve(), mask, form, depth - 1u) || form.shift + shift >= 32u) return false; + form.shift += shift; + form.addend <<= shift; + } else if (depth != 0u && mask == nullptr && value->Opcode() == IrOpcode::ReadFirstLane && value->ArgumentCount() == 2u) { + if (!ParseFirstLaneOffset(value->Argument(0)->Resolve(), value->Argument(1)->Resolve(), form, depth - 1u)) return false; + } else { + if (mask == nullptr) return false; + form = FirstLaneOffset{value, mask, 0u, 0u}; + } + form.addend += addend; + return true; + } + + bool SameFirstLaneOffset(const FirstLaneOffset& left, const FirstLaneOffset& right) const { + return left.shift == right.shift && EquivalentValue(m_program.Resources(), left.mask, right.mask) && EquivalentValue(m_program.Resources(), left.lane, right.lane); + } + bool TryMakeTableColumn(IrValue& handle, bool sampler, bool r128, TableColumnPlan& plan) { const auto expected = sampler ? IrOpcode::GetSamplerResource : IrOpcode::GetImageResource; const std::uint32_t width = sampler ? 4u : 8u; @@ -602,6 +634,8 @@ private: bool folded = false; std::uint32_t first = 0; bool address = false; + FirstLaneOffset baseLane; + bool laneBase = false; for (std::uint32_t dword = 0; dword < words; dword++) { IrValue* read = ResolveInvariantPhi(m_program.Resources(), handle.Argument(dword)); if (read == nullptr) return false; @@ -613,11 +647,20 @@ private: } std::uint32_t readAddend = 0; IrValue* readBase = SplitImmediateAddend(read->Argument(1), readAddend); + bool sameBase = dword == 0u || EquivalentValue(m_program.Resources(), base, readBase); + if (!sameBase && laneBase) { + FirstLaneOffset lane; + if (ParseFirstLaneOffset(read->Argument(1)->Resolve(), nullptr, lane, phiSearchDepth) && SameFirstLaneOffset(baseLane, lane)) { + readAddend = lane.addend; + sameBase = true; + } + } if (dword == 0u) { first = memory->offset; address = addressRead; base = readBase; baseAddend = readAddend; + laneBase = ParseFirstLaneOffset(read->Argument(1)->Resolve(), nullptr, baseLane, phiSearchDepth) && baseLane.addend == baseAddend; } else if (addressRead != address || memory->offset + readAddend != first + baseAddend + dword * static_cast<std::uint32_t>(sizeof(std::uint32_t))) { return false; } else if (readAddend != baseAddend) { @@ -634,7 +677,7 @@ private: if (heapHandle == nullptr) heapHandle = currentHandle; if (dword == 0u) { offset = read->Argument(1)->Resolve(); - } else if (!EquivalentValue(m_program.Resources(), base, readBase)) { + } else if (!sameBase) { return false; } plan.memory.push_back(memoryIndex); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 222397a960..40c427b7f5 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -41,7 +41,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. -- [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. +- [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. From 78178e940e967076c4f942f4d51fb803ae930399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 21 Oct 2026 20:21:43 +0200 Subject: [PATCH 0495/1360] test(recompiler): drop code comments from split waterfall image table test Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3b602479d0..16e54ee281 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3563,9 +3563,6 @@ void verifySplitWaterfallImageTable() { std::copy(descriptor.begin(), descriptor.end(), root.begin() + 0x40u / 4u); const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data())); const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)}; - // v_cmpx_gt_u32 exec, 6, v0; s17 = v_readfirstlane(v0); v1 = v_lshl_add_u32(v0, 5, addend) under that exec; - // s19 = v_readfirstlane(v1); s18 = s17 << 5; s_load_dwordx4 s[36:39] at s18 + 0x40 and s[40:43] at s19 + 0x40: - // the two halves of one T# record, with the second half's offset added before the lane read. const auto code = [](std::uint32_t addend) { return std::vector<std::uint32_t>{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7da80086u, 0x7e220500u, 0xd7460001u, 0x00010b00u | (addend << 18u), 0x7e260501u, 0x8f128511u, 0xf4080900u, 0x24000040u, 0xf4080a00u, 0x26000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; From 123ba70839dbdb8888ea64223262a9582254e535 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:25:33 +0100 Subject: [PATCH 0496/1360] fix(relinker): load Linux executables at 0x400000 by default --- .../elfpatcher/linux/LinuxElfPatcher.hpp | 2 + core/relinker/main.cpp | 5 +- .../include/relinker/patching/GamePatch.hpp | 3 +- .../include/relinker/pipeline/Conversion.hpp | 1 + .../relinker/src/patching/GamePatch.cpp | 31 +++++-- .../relinker/src/pipeline/Conversion.cpp | 9 +- .../relinker/tests/ConversionTests.cpp | 16 +++- core/relinker/relinker/tests/ElfFixture.hpp | 16 ++-- .../relinker/tests/GamePatchTests.cpp | 22 ++++- .../tests/test_executable_tls_relocations.py | 6 +- .../relinker/tests/test_game_patches.py | 1 + .../tests/test_linux_eh_frame_header.py | 8 +- .../relinker/tests/test_linux_entry_argv.py | 3 +- .../relinker/tests/test_linux_fixed_base.py | 82 +++++++++++++------ .../relinker/tests/test_linux_gnu_segments.py | 11 +-- .../tests/test_linux_program_header_order.py | 7 +- .../relinker/tests/test_none_relocations.py | 11 ++- .../tests/test_null_symbol_relocations.py | 5 +- .../relinker/tests/test_optional_rela.py | 5 +- .../tests/test_overlapping_relocations.py | 5 +- .../relinker/relinker/tests/test_sce_relro.py | 7 +- docs/dev/debt/relinker.md | 1 + docs/user/USAGE.md | 14 ++-- 23 files changed, 191 insertions(+), 80 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index c24b3114f0..58aebbcd67 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -14,6 +14,8 @@ namespace Elfpatcher::Linux { +inline constexpr std::uint64_t DefaultFixedBase = 0x400000; + class LinuxElfPatcher : public IElfPatcher { public: LinuxElfPatcher( diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 33240459b0..9e328dd454 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -35,6 +35,7 @@ struct ConvertedExecutable { std::vector<std::uint8_t> Bytes; std::string Diagnostics; std::vector<Relinker::GamePatch::AppliedPatch> AppliedPatches; + std::optional<std::uint64_t> LinuxBase; }; ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector<Relinker::GamePatch::PatchEntry>& patches, Cli::ConsoleProgress& progress) { @@ -73,7 +74,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches)}; + return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } class OutputIdentityDirectory { @@ -332,7 +333,7 @@ int Run(const int argc, char* argv[]) { std::filesystem::path manifestPath; std::string manifestText; if (!appliedPatches.empty()) manifestPath = Io::NativePath(absPath).parent_path() / Io::NativePath(Io::Utf8Path(Io::NativePath(absPath).stem()) + Relinker::GamePatch::ManifestSuffix); - if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile)); + if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase); const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); diff --git a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp index d893a7015e..6b76048dfb 100644 --- a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp +++ b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp @@ -40,6 +40,7 @@ struct PatchEntry { struct PatchFile { std::vector<PatchEntry> Entries; std::optional<std::string> MainTarget; + std::optional<std::uint64_t> LinuxBase; }; struct PatchedRange { @@ -114,7 +115,7 @@ void MatchTargets(const std::vector<PatchEntry>& entries, const std::vector<std: std::vector<AppliedPatch> AggregateManifest(const std::vector<std::vector<AppliedPatch>>& records); -std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath); +std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath, std::optional<std::uint64_t> linuxBase = std::nullopt); std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector<VerifyTarget>& executables, const std::vector<VerifyTarget>& modules, bool outputMode); diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 194262b78e..6998e95873 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -46,6 +46,7 @@ struct ConversionOutput { RelinkResult Analysis; std::string WindowsDiagnostics; std::vector<GamePatch::AppliedPatch> AppliedPatches; + std::optional<std::uint64_t> LinuxBase; }; class Conversion { diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 05d334868a..403264c79b 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -33,8 +33,8 @@ constexpr std::array<IntegerType, 8> kIntegerTypes{{ {"i8", 8, true}, {"i16", 16, true}, {"i32", 32, true}, {"i64", 64, true}, }}; -constexpr std::array<std::string_view, 7> kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}; -constexpr std::array<std::string_view, 8> kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}; +constexpr std::array<std::string_view, 8> kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; +constexpr std::array<std::string_view, 9> kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; constexpr std::array<std::string_view, 10> kEntryKeys{"name", "target", "va", "type", "expected", "value", "min", "max", "align", "description"}; std::string HexValue(const std::uint64_t value) { @@ -300,8 +300,8 @@ void RejectEntryOverlaps(const std::vector<PatchEntry>& entries, const std::vect } } -bool IsElfDyn(const std::vector<std::uint8_t>& bytes) { - return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == Elfpatcher::ET_DYN && bytes[17] == 0; +bool IsElfOfType(const std::vector<std::uint8_t>& bytes, const std::uint16_t type) { + return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == (type & 0xff) && bytes[17] == (type >> 8); } VerifyResult VerifyOne(const PatchEntry& entry, const EntryRange& resolved, const std::vector<std::uint8_t>& bytes, const bool outputMode) { @@ -410,6 +410,10 @@ PatchFile ParsePatchFile(const std::string_view text) { if (!mainTarget->IsString || mainTarget->Text.empty()) FailLine(mainTarget->Line, "main_target must be a nonempty string", "main_target"); file.MainTarget = mainTarget->Text; } + if (const auto* linuxBase = Find(top, "linux_base")) { + if (linuxBase->IsString || linuxBase->Number.Negative) FailLine(linuxBase->Line, "linux_base must be a non-negative integer", "linux_base"); + file.LinuxBase = linuxBase->Number.Magnitude; + } return file; } @@ -505,7 +509,7 @@ std::vector<AppliedPatch> AggregateManifest(const std::vector<std::vector<Applie return merged; } -std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath) { +std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath, const std::optional<std::uint64_t> linuxBase) { const auto holdable = [](const std::string& text) { return std::none_of(text.begin(), text.end(), [](const char character) { return character == '"' || character == '\\' || (static_cast<unsigned char>(character) < 0x20 && character != '\t') || character == 0x7f; @@ -519,6 +523,7 @@ std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::s if (!config.empty() && !holdable(config)) config = std::filesystem::path(Io::NativePath(configPath)).filename().generic_string(); std::ostringstream manifest; manifest << "format = 1\nmain_target = " << quoted(mainTarget, "main_target") << '\n'; + if (linuxBase) manifest << "linux_base = " << HexValue(*linuxBase) << '\n'; if (!config.empty()) manifest << "config = " << quoted(config, "config") << '\n'; for (const auto& patch : patches) { manifest << "\n[[patch]]\n"; @@ -553,8 +558,12 @@ std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector for (std::size_t index = 0; index < executables.size(); ++index) for (std::size_t other = index + 1; other < executables.size(); ++other) if (executables[index].Name == executables[other].Name) throw Domain::RelinkerException("ambiguous patch target: " + executables[index].Name); - for (const auto& executable : executables) - if (!IsElfDyn(executable.Bytes)) throw Domain::RelinkerException(executable.Name + " is not an ET_DYN Linux ELF output (ET_EXEC from --linux-fixed-base, PE and Mach-O outputs are not supported): verify the input instead"); + for (const auto& executable : executables) { + const bool fixed = IsElfOfType(executable.Bytes, Elfpatcher::ET_EXEC); + if (!fixed && !IsElfOfType(executable.Bytes, Elfpatcher::ET_DYN)) throw Domain::RelinkerException(executable.Name + " is not a Linux ELF output (PE and Mach-O outputs are not supported): verify the input instead"); + if (fixed && !file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_EXEC Linux ELF output, but the patch file has no linux_base: verify the manifest written by its conversion"); + if (!fixed && file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_DYN Linux ELF output, but the patch file has linux_base = " + HexValue(*file.LinuxBase) + ": verify the output of the conversion that wrote it"); + } } else { std::vector<std::string> names; for (const auto& executable : executables) names.push_back(executable.Name); @@ -583,13 +592,19 @@ std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector } continue; } + const std::uint64_t base = outputMode && IsElfOfType(target->Bytes, Elfpatcher::ET_EXEC) ? *file.LinuxBase : 0; std::vector<PatchEntry> own; - for (const auto other : indices) own.push_back(file.Entries[other]); + for (const auto other : indices) { + own.push_back(file.Entries[other]); + if (own.back().VirtualAddress > std::numeric_limits<std::uint64_t>::max() - base) throw Domain::RelinkerException(Describe(own.back()) + " lies outside the address space at linux_base = " + HexValue(base)); + own.back().VirtualAddress += base; + } const ElfReader reader(target->Bytes); const auto resolved = ResolveEach(reader, own); RejectEntryOverlaps(own, resolved); for (std::size_t position = 0; position < indices.size(); ++position) { results[indices[position]] = VerifyOne(own[position], resolved[position], target->Bytes, outputMode); + results[indices[position]].VirtualAddress = file.Entries[indices[position]].VirtualAddress; done[indices[position]] = true; } } diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index ad008fa002..1cf1879466 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -125,6 +125,11 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti auto byteWriter = std::make_shared<Io::ByteWriter>(); + std::optional<std::uint64_t> linuxBase; + if (!windows && !macos) + linuxBase = options.LinuxFixedBase || options.LazyBinding ? options.LinuxFixedBase : std::optional<std::uint64_t>(Elfpatcher::Linux::DefaultFixedBase); + if (!windows && !macos && options.LazyBinding) + inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n"); std::shared_ptr<Elfpatcher::IElfPatcher> patcher; if (macos) { patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); @@ -139,7 +144,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti ), std::make_shared<Elfpatcher::SectionHeaderTableBuilder>(byteWriter), byteWriter, - options.LinuxFixedBase, + linuxBase, options.TraceImports ); } @@ -157,7 +162,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::string diagnostics; if (windows) diagnostics = std::static_pointer_cast<Elfpatcher::Windows::WindowsPePatcher>(patcher)->GetDiagnostics(); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); - return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches)}; + return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches), linuxBase}; } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); throw; diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 468921cf4f..3357892f5e 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -132,14 +132,22 @@ void fixedBase() { const Relinker::Conversion converter(progress); auto source = input(); source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); - const auto plain = converter.Convert(source, {}); + const auto standard = converter.Convert(source, {}); + Relinker::ConversionOptions lazy; + lazy.LazyBinding = true; + const auto independent = converter.Convert(input(), lazy); Relinker::ConversionOptions options; options.LinuxFixedBase = 0x400000000; const auto fixed = converter.Convert(source, options); - require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); - require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); - require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + require(Io::ReadU16(standard.Executable, 16) == 2 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU16(independent.Executable, 16) == 3, "Lazy binding did not keep ET_DYN"); + require(standard.LinuxBase == 0x400000 && fixed.LinuxBase == *options.LinuxFixedBase && !independent.LinuxBase, "Conversion reported the wrong Linux base"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(standard.Executable, 24) + *options.LinuxFixedBase - 0x400000, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == standard.Modules.front().Bytes, "Executable fixed base changed a bundled module"); for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + Relinker::ConversionOptions other; + other.Target = target; + require(!converter.Convert(source, other).LinuxBase, "A non-Linux conversion reported a Linux base"); options.Target = target; reject([&] { converter.Convert(source, options); }); } diff --git a/core/relinker/relinker/tests/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp index daa1f322cb..2ed1a52cae 100644 --- a/core/relinker/relinker/tests/ElfFixture.hpp +++ b/core/relinker/relinker/tests/ElfFixture.hpp @@ -3,6 +3,7 @@ #include <algorithm> #include <cstdint> #include <cstring> +#include <limits> #include <span> #include <stdexcept> #include <utility> @@ -117,12 +118,17 @@ public: const auto phOffset = Read<std::uint64_t>(elf, 32); const auto phSize = Read<std::uint16_t>(elf, 54); const auto phCount = Read<std::uint16_t>(elf, 56); + const auto page = static_cast<std::uint64_t>(sysconf(_SC_PAGESIZE)); + linkBase = std::numeric_limits<std::uint64_t>::max(); + std::uint64_t last = 0; for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read<std::uint32_t>(elf, header) != 1) continue; - size = std::max<std::size_t>(size, Read<std::uint64_t>(elf, header + 16) + Read<std::uint64_t>(elf, header + 40)); + linkBase = std::min(linkBase, Read<std::uint64_t>(elf, header + 16) / page * page); + last = std::max(last, Read<std::uint64_t>(elf, header + 16) + Read<std::uint64_t>(elf, header + 40)); } - if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + if (last == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + size = static_cast<std::size_t>(last - linkBase); 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) { @@ -131,15 +137,14 @@ public: const auto offset = Read<std::uint64_t>(elf, header + 8); const auto fileSize = Read<std::uint64_t>(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<std::uint8_t*>(base) + Read<std::uint64_t>(elf, header + 16), elf.data() + offset, fileSize); + std::memcpy(static_cast<std::uint8_t*>(base) + (Read<std::uint64_t>(elf, header + 16) - linkBase), 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<std::uint64_t>(sysconf(_SC_PAGESIZE)); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read<std::uint32_t>(elf, header) != 1) continue; const auto flags = Read<std::uint32_t>(elf, header + 4); - const auto vaddr = Read<std::uint64_t>(elf, header + 16); + const auto vaddr = Read<std::uint64_t>(elf, header + 16) - linkBase; const auto start = vaddr / page * page; const auto end = (vaddr + Read<std::uint64_t>(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); @@ -163,6 +168,7 @@ public: private: void* base = nullptr; std::size_t size = 0; + std::uint64_t linkBase = 0; }; } // namespace RelinkerTests diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index f81bc1f2fe..bde267682d 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -385,7 +385,20 @@ void verify() { rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), outputs, {}, true); }, "no main_target"); auto exec = twoSegmentFixture(); Io::WriteU16(exec, 16, 2); - rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "verify the input instead"); + rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "has no linux_base"); + const auto based = ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n")); + require(based.LinuxBase == 0x400000, "linux_base was not read"); + auto rebased = patchedBytes; + Io::WriteU16(rebased, 16, 2); + for (const std::size_t field : {64 + 16, 64 + 24, 120 + 16, 120 + 24}) Io::WriteU64(rebased, field, Io::ReadU64(rebased, field) + 0x400000); + output = VerifyPatches(based, {{"main.elf", rebased}}, {}, true); + require(output.size() == 2 && output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "fixed-base output applied"); + require(output[0].VirtualAddress == 0x1000 && output[0].Offset == 0x1000 && output[1].Offset == 0x2000, "fixed-base output reported the rebased address"); + output = VerifyPatches(based, {{"main.elf", exec}}, {}, true); + require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("PT_LOAD") != std::string::npos, "linux_base was not applied"); + rejects([&] { VerifyPatches(based, outputs, {}, true); }, "has linux_base = 0x400000"); + rejects([&] { VerifyPatches(ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\ntarget = \"main.elf\"\nva = 0xffffffffffffff00\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", rebased}}, {}, true); }, "lies outside the address space at linux_base"); rejects([&] { VerifyPatches(file, {{"main.elf", {'M', 'Z', 0x90, 0}}}, {}, true); }, "verify the input instead"); rejects([&] { VerifyPatches(file, {{"main.elf", {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1}}}, {}, true); }, "verify the input instead"); rejects([&] { VerifyPatches(file, {{"main.elf", {}}}, {}, true); }, "verify the input instead"); @@ -424,10 +437,15 @@ void manifest() { require(parsed.Entries[1].Name.empty() && parsed.Entries[1].Target == "sce_module/libc.prx" && parsed.Entries[1].Description.empty(), "second entry"); require(text.find("name = \"alpha\"") != std::string::npos && text.find("main_target = \"main.elf\"") != std::string::npos, "manifest text"); require(WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "") == text, "manifest is not deterministic"); + require(text.find("linux_base") == std::string::npos && !parsed.LinuxBase, "a manifest without a base wrote linux_base"); + const auto based = WriteManifest({plain}, "main.elf", "", 0x400000); + require(based.find("main_target = \"main.elf\"\nlinux_base = 0x400000\n") != std::string::npos && ParsePatchFile(based).LinuxBase == 0x400000, "linux_base did not round-trip"); + rejectsText(header("linux_base = \"0x400000\"\n"), 2, "linux_base must be a non-negative integer"); + rejectsText(header("linux_base = -1\n"), 2, "linux_base must be a non-negative integer"); } void reservedNames() { - for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}) + for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}) rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved"); ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n")); } diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..5ce918047b 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -20,6 +20,7 @@ PT_TLS = 7 DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 +DEFAULT_BASE = 0x400000 STRINGS = b"\0Tls\0Data\0ImportedTls\0" TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 @@ -107,8 +108,9 @@ def main(): elf = output.read_bytes() relocations = dynamic_relocations(elf) tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) - assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), - (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert tls == [(DEFAULT_BASE + 0x1000, TPOFF64, "", 0, 4), (DEFAULT_BASE + 0x1008, DTPOFF64, "", 0, 8), + (DEFAULT_BASE + 0x1010, TPOFF64, "", 0, 0x12), (DEFAULT_BASE + 0x1018, DTPMOD64, "", 0, 0), + (DEFAULT_BASE + 0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations assert any(struct.unpack_from("<I", elf, 64 + index * 56)[0] == PT_TLS for index in range(struct.unpack_from("<H", elf, 0x38)[0])), "PT_TLS must be kept" diff --git a/core/relinker/relinker/tests/test_game_patches.py b/core/relinker/relinker/tests/test_game_patches.py index 7169739a04..4fb2fd064a 100644 --- a/core/relinker/relinker/tests/test_game_patches.py +++ b/core/relinker/relinker/tests/test_game_patches.py @@ -66,6 +66,7 @@ def main(): manifest = output.parent / "app.patches.toml" text = manifest.read_text() assert 'main_target = "eboot.bin"' in text and 'name = "flip_bit"' in text, text + assert 'main_target = "eboot.bin"\nlinux_base = 0x400000\n' in text, text assert f"Patch manifest: {manifest}\n" in result.stdout assert f'config = "{source.parent.as_posix()}/relinker-patches.toml"\n' in text, text diff --git a/core/relinker/relinker/tests/test_linux_eh_frame_header.py b/core/relinker/relinker/tests/test_linux_eh_frame_header.py index aa6e08da8a..a80a1eb1ed 100644 --- a/core/relinker/relinker/tests/test_linux_eh_frame_header.py +++ b/core/relinker/relinker/tests/test_linux_eh_frame_header.py @@ -10,6 +10,8 @@ PT_GNU_EH_FRAME = 0x6474E550 PT_GNU_STACK = 0x6474E551 PT_SCE_VERSION = 0x6FFFFF01 EH_FRAME_HEADER = (PT_GNU_EH_FRAME, 4, 0x4800, 0x800, 0x800, 16, 16, 4) +DEFAULT_BASE = 0x400000 +EH_FRAME_OUTPUT = (*EH_FRAME_HEADER[:3], 0x800 + DEFAULT_BASE, 0x800 + DEFAULT_BASE, *EH_FRAME_HEADER[5:]) def fixture(header_count, with_frame=True): @@ -93,7 +95,7 @@ def main(): assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr headers = program_headers(elf) frames = [header for header in headers if header[0] == PT_GNU_EH_FRAME] - assert frames == [EH_FRAME_HEADER], ("PT_GNU_EH_FRAME must be kept unchanged", headers) + assert frames == [EH_FRAME_OUTPUT], ("PT_GNU_EH_FRAME must only move by the base", headers) offset, size = EH_FRAME_HEADER[2], EH_FRAME_HEADER[5] assert elf[offset:offset + size] == image[offset:offset + size], "eh_frame_hdr bytes changed" assert all(header[0] != PT_SCE_VERSION for header in headers), headers @@ -103,7 +105,7 @@ def main(): image[table_end:table_end + 56] = bytes([0xa5]) * 56 result, elf = relink(relinker, directory, "full-slots", image) headers = program_headers(elf) - assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == [EH_FRAME_HEADER], headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == [EH_FRAME_OUTPUT], headers assert len(headers) == 8, headers assert struct.unpack_from("<Q", elf, 0x20)[0] >= len(image), "The grown program header table must be moved past the input" assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr @@ -125,7 +127,7 @@ def main(): result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) headers = program_headers(elf) assert len(headers) == (8 if with_frame else 7), headers - assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_OUTPUT] if with_frame else []), headers assert struct.unpack_from("<Q", elf, 0x20)[0] >= len(image), "The grown program header table must be moved past the input" assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 4dad8c6e07..87604a5743 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -12,6 +12,7 @@ from signal_run import run_without_core_dump from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads ENTRY = 0x10 +DEFAULT_BASE = 0x400000 ARGV_CHECK = bytes.fromhex( "833f02" "751e" "48837f0800" "7417" @@ -79,7 +80,7 @@ def main(): capture_output=True, text=True, timeout=20) if error is None: assert result.returncode == 0, (address, result.stdout, result.stderr) - assert entry_call_target(far.read_bytes()) == ENTRY, hex(address) + assert entry_call_target(far.read_bytes()) == DEFAULT_BASE + ENTRY, hex(address) else: assert result.returncode == 2 and error in result.stderr and not far.exists(), (address, result) print("Linux entry argv test passed") diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 84244f4533..ee31ad59ef 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -1,4 +1,4 @@ -"""Check that --linux-fixed-base places the executable at the requested address and rebases every address it holds.""" +"""Check that the Linux executable is placed at 0x400000 by default or at the --linux-fixed-base address, with every address it holds rebased, that --lazy-binding keeps it position independent and warns, and that the largest image the relinker accepts ends below the guest arena.""" from pathlib import Path import platform @@ -16,9 +16,13 @@ PT_LOAD = 1 PT_DYNAMIC = 2 PT_GNU_STACK = 0x6474E551 BASE = 0x400000 +HIGH_BASE = 0x400000000 +SHIFT = HIGH_BASE - BASE ENTRY = 0x10 SLOT = 0x800 ADDRESS_TAGS = {3, 4, 5, 6, 7, 23} +GUEST_ARENA_START = 0x200000000 +LAZY_WARNING = "with --lazy-binding the Linux executable stays position independent" def check_code(base): @@ -86,58 +90,83 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-fixed-base-") as directory: work = Path(directory) - result, plain_path = relink(relinker, work, "plain", fixture()) + result, fixed_path = relink(relinker, work, "default", fixture()) assert result.returncode == 0, (result.stdout, result.stderr) - result, fixed_path = relink(relinker, work, "fixed", fixture(), "--linux-fixed-base", hex(BASE)) + assert LAZY_WARNING not in result.stdout + result.stderr, (result.stdout, result.stderr) + result, explicit_path = relink(relinker, work, "explicit", fixture(), "--linux-fixed-base", hex(BASE)) assert result.returncode == 0, (result.stdout, result.stderr) - plain = plain_path.read_bytes() + result, high_path = relink(relinker, work, "high", fixture(), "--linux-fixed-base", hex(HIGH_BASE)) + assert result.returncode == 0, (result.stdout, result.stderr) + result, lazy_path = relink(relinker, work, "lazy", fixture(), "--lazy-binding") + assert result.returncode == 0, (result.stdout, result.stderr) + assert LAZY_WARNING in result.stdout + result.stderr, (result.stdout, result.stderr) fixed = fixed_path.read_bytes() + high = high_path.read_bytes() + lazy = lazy_path.read_bytes() - assert struct.unpack_from("<H", plain, 16)[0] == 3, "a relink without the option is no longer ET_DYN" - assert struct.unpack_from("<H", fixed, 16)[0] == 2, "a fixed-base relink is not ET_EXEC" - assert struct.unpack_from("<Q", fixed, 24)[0] == struct.unpack_from("<Q", plain, 24)[0] + BASE, "entry not rebased" + assert fixed == explicit_path.read_bytes(), "a relink without the option is not placed at 0x400000" + assert struct.unpack_from("<H", fixed, 16)[0] == 2, "a default relink is not ET_EXEC" + assert struct.unpack_from("<H", high, 16)[0] == 2, "a fixed-base relink is not ET_EXEC" + assert struct.unpack_from("<H", lazy, 16)[0] == 3, "a lazy-binding relink is no longer ET_DYN" + assert relocations(lazy) == [(SLOT, 8, ENTRY)], relocations(lazy) + assert struct.unpack_from("<Q", high, 24)[0] == struct.unpack_from("<Q", fixed, 24)[0] + SHIFT, "entry not rebased" - plain_headers = headers(plain) fixed_headers = headers(fixed) - assert len(plain_headers) == len(fixed_headers) - for before, after in zip(plain_headers, fixed_headers): - shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else BASE + high_headers = headers(high) + assert len(fixed_headers) == len(high_headers) + for before, after in zip(fixed_headers, high_headers): + shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else SHIFT assert after == (*before[:3], before[3] + shift, before[4] + shift, *before[5:]), (before, after) assert min(header[3] for header in fixed_headers if header[0] == PT_LOAD) == BASE, "image does not start at the base" + assert min(header[3] for header in high_headers if header[0] == PT_LOAD) == HIGH_BASE, "image does not start at the base" - plain_dynamic = dynamic_tags(plain) fixed_dynamic = dynamic_tags(fixed) - assert len(plain_dynamic) == len(fixed_dynamic), (len(plain_dynamic), len(fixed_dynamic)) - for (tag, value), (fixed_tag, fixed_value) in zip(plain_dynamic, fixed_dynamic): - assert tag == fixed_tag and fixed_value == value + (BASE if tag in ADDRESS_TAGS else 0), (tag, value, fixed_value) + high_dynamic = dynamic_tags(high) + assert len(fixed_dynamic) == len(high_dynamic), (len(fixed_dynamic), len(high_dynamic)) + for (tag, value), (high_tag, high_value) in zip(fixed_dynamic, high_dynamic): + assert tag == high_tag and high_value == value + (SHIFT if tag in ADDRESS_TAGS else 0), (tag, value, high_value) - plain_relocations = relocations(plain) - assert plain_relocations == [(SLOT, 8, ENTRY)], plain_relocations assert relocations(fixed) == [(SLOT + BASE, 8, ENTRY + BASE)], relocations(fixed) + assert relocations(high) == [(SLOT + HIGH_BASE, 8, ENTRY + HIGH_BASE)], relocations(high) relocated = fixture(slots=5) - result, relocated_plain_path = relink(relinker, work, "relocated-plain", relocated) + result, relocated_fixed_path = relink(relinker, work, "relocated-default", relocated) assert result.returncode == 0, (result.stdout, result.stderr) - result, relocated_fixed_path = relink(relinker, work, "relocated-fixed", relocated, "--linux-fixed-base", hex(BASE)) + result, relocated_high_path = relink(relinker, work, "relocated-high", relocated, "--linux-fixed-base", hex(HIGH_BASE)) assert result.returncode == 0, (result.stdout, result.stderr) - relocated_plain = relocated_plain_path.read_bytes() relocated_fixed = relocated_fixed_path.read_bytes() + relocated_high = relocated_high_path.read_bytes() assert struct.unpack_from("<H", relocated_fixed, 0x38)[0] > struct.unpack_from("<H", relocated, 0x38)[0], "the output program header table did not grow past the input's slots" assert struct.unpack_from("<Q", relocated_fixed, 0x20)[0] != 0x40, "the input table had slots for the output layout" - relocated_plain_headers = headers(relocated_plain) relocated_fixed_headers = headers(relocated_fixed) - assert len(relocated_plain_headers) == len(relocated_fixed_headers) - for before, after in zip(relocated_plain_headers, relocated_fixed_headers): - shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else BASE + relocated_high_headers = headers(relocated_high) + assert len(relocated_fixed_headers) == len(relocated_high_headers) + for before, after in zip(relocated_fixed_headers, relocated_high_headers): + shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else SHIFT assert after == (*before[:3], before[3] + shift, before[4] + shift, *before[5:]), (before, after) assert min(header[3] for header in relocated_fixed_headers if header[0] == PT_LOAD) == BASE, "the relocated image does not start at the base" relocated_entry = struct.unpack_from("<Q", relocated_fixed, 24)[0] - assert relocated_entry == struct.unpack_from("<Q", relocated_plain, 24)[0] + BASE, "entry not rebased" + assert struct.unpack_from("<Q", relocated_high, 24)[0] == relocated_entry + SHIFT, "entry not rebased" entry_offset = mapped(relocated_fixed, relocated_entry) assert entry_offset < len(relocated_fixed), ("the entry's mapping lies outside the output file", entry_offset) covering = next(header for header in relocated_fixed_headers if header[0] == PT_LOAD and header[3] <= relocated_entry < header[3] + header[5]) assert covering[1] & 1, "the segment covering the entry is not executable" + large_path = None + for far, accepted in ((0x7F000000, True), (0x80000000, False), (GUEST_ARENA_START - BASE - 0x1000, False)): + spanning = fixture() + struct.pack_into("<IIQQQQQQ", spanning, 64 + 2 * 56, PT_LOAD, 6, 0x5000, far, far, 0, 0x2000, 0x1000) + for options in ((), ("--lazy-binding",)): + result, output = relink(relinker, work, f"span-{far:x}{''.join(options)}", spanning, *options) + if not accepted: + assert result.returncode == 2 and "Entry stub call exceeds rel32 range" in result.stderr and not output.exists(), (hex(far), options, result.returncode, result.stderr) + continue + assert result.returncode == 0, (hex(far), options, result.stdout, result.stderr) + if not options: + large_path = output + end = max(header[3] + header[6] for header in headers(output.read_bytes()) if header[0] == PT_LOAD) + assert BASE + far < end < GUEST_ARENA_START, ("the largest accepted image does not end below the guest arena", hex(end)) + filled = bytearray(relocated) filled[0x158:0x198] = b"\x41" * 0x40 result, _ = relink(relinker, work, "relocated-filled", bytes(filled), "--linux-fixed-base", hex(BASE)) @@ -150,6 +179,9 @@ def main(): relocated_fixed_path.chmod(0o755) executed = run_without_core_dump([str(relocated_fixed_path)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, ("the relocated-table image did not run at the base with its relocation applied", executed.returncode) + large_path.chmod(0o755) + executed = run_without_core_dump([str(large_path)], capture_output=True, timeout=20) + assert executed.returncode == -signal.SIGTRAP, ("the image spanning 2 GiB did not run at the base", executed.returncode) for name, image, base, error in ( ("unaligned", fixture(alignment=0x20000), 0x410000, "is not a multiple of the PT_LOAD alignment 0x20000"), diff --git a/core/relinker/relinker/tests/test_linux_gnu_segments.py b/core/relinker/relinker/tests/test_linux_gnu_segments.py index 223c131881..c9941bf0a9 100644 --- a/core/relinker/relinker/tests/test_linux_gnu_segments.py +++ b/core/relinker/relinker/tests/test_linux_gnu_segments.py @@ -13,7 +13,8 @@ from test_linux_load_alignment import fixture PT_GNU_STACK = 0x6474E551 PT_GNU_RELRO = 0x6474E552 PT_SCE_VERSION = 0x6FFFFF01 -BASE = 0x400000 +DEFAULT_BASE = 0x400000 +BASE = 0x800000 def image(): @@ -48,8 +49,8 @@ def main(): work = Path(directory) source = work / "input.elf" source.write_bytes(image()) - for options, base, keeps_relro in (([], 0, True), (["--linux-fixed-base", hex(BASE)], BASE, True), (["--lazy-binding"], 0, False)): - output = work / ("fixed.elf" if base else "plain.elf") + for name, options, base, keeps_relro in (("default", [], DEFAULT_BASE, True), ("fixed", ["--linux-fixed-base", hex(BASE)], BASE, True), ("lazy", ["--lazy-binding"], 0, False)): + output = work / (name + ".elf") result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) @@ -72,7 +73,7 @@ def main(): assert result.returncode == 0, (result.stdout, result.stderr) found = {header[0]: header for header in headers(output.read_bytes())} assert found[PT_GNU_STACK] == (PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16), found - assert found[PT_GNU_RELRO][3:5] == (0x500, 0x500), found + assert found[PT_GNU_RELRO][3:5] == (0x500 + DEFAULT_BASE, 0x500 + DEFAULT_BASE), found if sys.platform.startswith("linux"): for options, mode in (([], "default"), (["--linux-fixed-base", hex(BASE)], "fixed")): @@ -97,7 +98,7 @@ def main(): assert result.returncode == 0, (result.stdout, result.stderr) found = {header[0]: header for header in headers(output.read_bytes())} assert found[PT_GNU_STACK] == (PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16), found - assert found[PT_GNU_RELRO][3:5] == (0x4000, 0x4000), found + assert found[PT_GNU_RELRO][3:5] == (0x4000 + DEFAULT_BASE, 0x4000 + DEFAULT_BASE), found output.chmod(output.stat().st_mode | 0o111) execution = run_without_core_dump([str(output)], capture_output=True, text=True, timeout=20) assert execution.returncode == -signal.SIGTRAP, (execution.returncode, execution.stdout, execution.stderr) diff --git a/core/relinker/relinker/tests/test_linux_program_header_order.py b/core/relinker/relinker/tests/test_linux_program_header_order.py index 6d223a4135..3a8be9aca1 100644 --- a/core/relinker/relinker/tests/test_linux_program_header_order.py +++ b/core/relinker/relinker/tests/test_linux_program_header_order.py @@ -21,6 +21,7 @@ PT_OS_PROCPARAM = 0x61000001 PT_GNU_STACK = 0x6474E551 PT_SCE_VERSION = 0x6FFFFF01 PAGE = 0x1000 +DEFAULT_BASE = 0x400000 TEXT = (PT_LOAD, 5, 0x4000, 0x0000, 0x1000, 0x1000, 0x1000) RODATA = (PT_LOAD, 4, 0x5000, 0x1000, 0x0800, 0x0800, 0x1000) @@ -102,13 +103,13 @@ def main(): ordered = convert("ordered-loads", multi_load_fixture([TEXT, RODATA, PROCPARAM, DATA])) headers = check_order("ordered-loads", ordered.read_bytes()) - assert (PT_OS_PROCPARAM, PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers - assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers + assert (PT_OS_PROCPARAM, DEFAULT_BASE + PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers + assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers run_expecting_trap("ordered-loads", ordered) unordered = convert("unordered-loads", multi_load_fixture([DATA, PROCPARAM, TEXT, RODATA])) headers = check_order("unordered-loads", unordered.read_bytes()) - assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers + assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers assert kinds_between_loads(headers) == [PT_OS_PROCPARAM, PT_GNU_STACK], headers run_expecting_trap("unordered-loads", unordered) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 538b0247d8..ca10af8fd0 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -10,6 +10,13 @@ from test_dynamic_segment import fixture from test_linux_dynamic_strings import dynamic_strings, file_offset from test_linux_load_alignment import loads +DEFAULT_BASE = 0x400000 + + +def rebased(relocation): + offset, info, addend = relocation + return (offset + DEFAULT_BASE, info, addend + DEFAULT_BASE if info & 0xffffffff == 8 else addend) + def image_with_relocations(relocations, sce, imported): image = fixture() @@ -75,7 +82,7 @@ def main(): if tags.get(8, 0): start = file_offset(data, loads(data), tags[7]) actual = [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] - assert actual == [relocation for relocation in relocations if relocation in (imported, relative)] + assert actual == [rebased(relocation) for relocation in relocations if relocation in (imported, relative)] if target == "windows" and os.name == "nt" and not has_import: run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (name, level, run.returncode, run.stderr) @@ -96,7 +103,7 @@ def main(): if target == "linux": tags, _ = dynamic_strings(data) start = file_offset(data, loads(data), tags[7]) - assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [relative] + assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [rebased(relative)] if target == "windows" and os.name == "nt": run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (sce, level, run.returncode, run.stderr) diff --git a/core/relinker/relinker/tests/test_null_symbol_relocations.py b/core/relinker/relinker/tests/test_null_symbol_relocations.py index 1ba9d01419..99f436b7b7 100644 --- a/core/relinker/relinker/tests/test_null_symbol_relocations.py +++ b/core/relinker/relinker/tests/test_null_symbol_relocations.py @@ -14,6 +14,7 @@ from test_linux_load_alignment import loads GLOB_DAT_SLOT = 0x308 ABSOLUTE_SLOT = 0x310 ADDEND = 0x1234 +DEFAULT_BASE = 0x400000 CHECK_SLOTS = bytes.fromhex( "488b0501010000" "4885c0" "751a" "488b0dfd000000" "4881f934120000" "750a" @@ -68,8 +69,8 @@ def main(): registry = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8")) assert registry == [], (index, target, level, registry) if target == "linux": - assert linux_imports(data) == [(GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0), - (ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], (index, level) + assert linux_imports(data) == [(DEFAULT_BASE + GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0), + (DEFAULT_BASE + ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], (index, level) if target == "windows" and os.name == "nt": run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (index, level, run.returncode, run.stderr) diff --git a/core/relinker/relinker/tests/test_optional_rela.py b/core/relinker/relinker/tests/test_optional_rela.py index 5dcc15edc5..45b16fe10d 100644 --- a/core/relinker/relinker/tests/test_optional_rela.py +++ b/core/relinker/relinker/tests/test_optional_rela.py @@ -15,6 +15,7 @@ from test_linux_load_alignment import loads from test_windows_import_modules import provider +DEFAULT_BASE = 0x400000 SCE_TAGS = {2: 0x6100002D, 3: 0x61000027, 5: 0x61000035, 6: 0x61000039, 7: 0x6100002F, 8: 0x61000031, 9: 0x61000033, 10: 0x61000037, 11: 0x6100003B, 20: 0x6100002B, 23: 0x61000029} @@ -102,8 +103,8 @@ def main(): if relative or plt: offset = file_offset(data, loads(data), dynamic[23 if plt else 7]) destination, info, addend = struct.unpack_from("<QQq", data, offset) - assert destination == 0x300 and info & 0xFFFFFFFF == (7 if plt else 8), case.name - assert addend == (0 if plt else 0x210), case.name + assert destination == 0x300 + DEFAULT_BASE and info & 0xFFFFFFFF == (7 if plt else 8), case.name + assert addend == (0 if plt else 0x210 + DEFAULT_BASE), case.name if target == "windows" and os.name == "nt": run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (case.name, run.returncode, run.stderr) diff --git a/core/relinker/relinker/tests/test_overlapping_relocations.py b/core/relinker/relinker/tests/test_overlapping_relocations.py index 84633cb6b0..492ca0f065 100644 --- a/core/relinker/relinker/tests/test_overlapping_relocations.py +++ b/core/relinker/relinker/tests/test_overlapping_relocations.py @@ -16,6 +16,7 @@ from test_linux_load_alignment import loads from test_macos_output import MachO +DEFAULT_BASE = 0x400000 SCE_TAGS = {2: 0x6100002D, 3: 0x61000027, 5: 0x61000035, 6: 0x61000039, 7: 0x6100002F, 8: 0x61000031, 9: 0x61000033, 10: 0x61000037, 11: 0x6100003B, 20: 0x6100002B, 23: 0x61000029} @@ -73,6 +74,7 @@ def fixture(layout, sce, guest, misaligned=False, conflicting=False): def check_output(data, target, guest, local, plt): if target == "linux": + base = 0 if guest else DEFAULT_BASE assert data.startswith(b"\x7fELF") assert all(load[2] % 0x1000 == load[3] % 0x1000 for load in loads(data)) tags = dynamic_tags(data) @@ -83,7 +85,8 @@ def check_output(data, target, guest, local, plt): actual = [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[size], 24)] assert [(offset, info & 0xFFFFFFFF, addend) for offset, info, addend in actual] == [ - (offset, info & 0xFFFFFFFF, addend) for offset, info, addend in expected], (actual, expected) + (offset + base, info & 0xFFFFFFFF, addend + base if info & 0xFFFFFFFF == 8 else addend) + for offset, info, addend in expected], (actual, expected) if address == 23: assert all(info >> 32 for _, info, _ in actual) return diff --git a/core/relinker/relinker/tests/test_sce_relro.py b/core/relinker/relinker/tests/test_sce_relro.py index 51de2a0953..42cbcb5bf8 100644 --- a/core/relinker/relinker/tests/test_sce_relro.py +++ b/core/relinker/relinker/tests/test_sce_relro.py @@ -12,6 +12,7 @@ from test_linux_load_alignment import loads PT_LOAD = 1 PT_DYNAMIC = 2 PT_SCE_RELRO = 0x61000010 +DEFAULT_BASE = 0x400000 PT_SCE_VERSION = 0x6FFFFF01 @@ -77,11 +78,11 @@ def main(): assert result.returncode == 0, (name, target, result.stdout, result.stderr) if target == "linux": data = output.read_bytes() - relro = [load for load in loads(data) if load[3] <= 0x1000 < load[3] + load[6]] - assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro) + relro = [load for load in loads(data) if load[3] <= DEFAULT_BASE + 0x1000 < load[3] + load[6]] + assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == DEFAULT_BASE + 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro) tags, _ = dynamic_strings(data) start = file_offset(data, loads(data), tags[7]) - assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [(0x1000, 8, 0x210)], name + assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [(DEFAULT_BASE + 0x1000, 8, DEFAULT_BASE + 0x210)], name if target == "windows" and os.name == "nt": run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (name, run.returncode, run.stderr) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 43ff7062d0..7096954ef8 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -11,6 +11,7 @@ - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. +- Converted Linux main executables are [placed at `0x400000`](../../../core/relinker/relinker/src/pipeline/Conversion.cpp) by default, as `ET_EXEC` without ASLR, not only with `--linux-fixed-base`: the console executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). The relinker refuses an image that spans 2 GiB or more, so the executable ends below the guest arena at `0x200000000`. With `--lazy-binding` the executable stays position independent. - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. - [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. Linux and macOS resolve these imports through the host loader by name and are unchanged. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 672e35bbbf..191adfd77d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -64,7 +64,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base <address>` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | | `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | -| `--linux-fixed-base <address>` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | +| `--linux-fixed-base <address>` | Load the Linux executable at this fixed address instead of the default `0x400000`, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base <address>` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | | `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | @@ -85,7 +85,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--exclude-sce-module <file>` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | -| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | +| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. @@ -120,11 +120,11 @@ align = 0x1000 description = "pool size" ``` -`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `settings` and `settings_sha256` are accepted in any file; only `main_target` is used, by `--verify-output`. Any other top-level key is an error. +`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `linux_base`, `settings` and `settings_sha256` are accepted in any file; only `main_target` and `linux_base` are used, by `--verify-output`. Any other top-level key is an error. | Entry key | Meaning | |-----------|---------| -| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the seven keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | +| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the eight keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | | `target` | Required. The main executable's file name, or for `--executable` files and bundled modules the path relative to the module parent directory, for example `sce_module/libc.prx`. | | `va` | Required. The virtual address of the ELF as a disassembler shows it. | | `type` | Optional: `u8 u16 u32 u64 i8 i16 i32 i64`. Then `expected`, `value`, `min`, `max` and `align` are integers, range-checked and written little-endian. Without it, `expected` and `value` are hexadecimal byte strings of the same length. | @@ -137,11 +137,11 @@ x86 `mov r/m64, imm32` and `add`/`sub`/`cmp r/m64, imm32` sign-extend their imme Entries apply in file order. A patch range must lie in the file bytes of one `PT_LOAD` and outside the ELF header and program header table. The conversion fails before writing any output if bytes differ from `expected`, a range is not valid, two entries overlap, a target matches no file or more than one file, or a range overlaps bytes the relinker itself rewrites (`--to-intel` replacements and trampolines, relinker patches, syscall traps) or the 8-byte target of a relocation of the file. -When at least one patch is applied, `<output-stem>.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target` and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. +When at least one patch is applied, `<output-stem>.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target`, `linux_base` (the address a Linux `ET_EXEC` output is placed at) and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. `--verify-patches <file> <path>` writes nothing and exits `0` when no entry is a mismatch. Without `--verify-output`, `<path>` is the input executable of any type, modules are found as in a conversion (module options and `--executable` apply), and each entry is `applicable` (the input holds `expected`) or `mismatch`. A target that matches nothing is an error that suggests `--verify-output`. -With `--verify-output`, `<path>` is a converted Linux ELF (`ET_DYN`); `ET_EXEC` output from `--linux-fixed-base`, PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. The entry whose target is `main_target` is checked against `<path>`, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. +With `--verify-output`, `<path>` is a converted Linux ELF; PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. Entries are checked at their `va` plus the manifest's `linux_base` in an `ET_EXEC` output, which requires `linux_base`; an `ET_DYN` output, from `--lazy-binding`, requires a manifest without it. The entry whose target is `main_target` is checked against `<path>`, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel`, `--rpath`, `--patches`, `--no-patches` and the like) and a second path. @@ -149,7 +149,7 @@ Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel` For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. -For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux, or `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use these options only when the executable requires a fixed address. Without them, Linux output is a position-independent executable as before, and macOS output places the lowest ELF load page at `0x140010000`. +For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use this option only when the executable requires a fixed address; without it, macOS output places the lowest ELF load page at `0x140010000`. Linux output places virtual address 0 at `0x400000` without an option, inside the 40-bit window that a title's own `sceLibcMspaceCreate` checks; `--linux-fixed-base` selects another address, and with `--lazy-binding` the executable stays position independent. The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased. From 69b21adf3279c782cb9776ac056e08fb061fcbc2 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 20:31:46 +0200 Subject: [PATCH 0497/1360] fix(libSceNgs2.native): parse ATRAC9 smpl loops as the module does --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 48 +++++--------- .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 64 +++++++++++++------ docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 63 insertions(+), 51 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 5aecd3530b..171e79004c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -305,44 +305,28 @@ static int ReadChunks(std::size_t size, TRead&& read, RiffChunks& chunks) { return SCE_NGS2_OK; } -static bool AddAtrac9Range(Ngs2WaveformInfo& info, std::uint64_t begin, std::uint32_t samples, std::uint32_t repeats) { - if (samples == 0) return true; - const auto preroll = std::min<std::uint64_t>(begin, info.num_audio_unit_samples); - const auto firstFrame = (begin - preroll) / info.num_audio_frame_samples; - const auto skip = begin - firstFrame * info.num_audio_frame_samples; - const auto frames = (skip + samples + info.num_audio_frame_samples - 1) / info.num_audio_frame_samples; - const auto offset = firstFrame * info.audio_frame_size; - const auto bytes = frames * info.audio_frame_size; - if (offset > info.data_size || bytes > info.data_size - offset) return false; +static void AddAtrac9Block(Ngs2WaveformInfo& info, std::uint32_t begin, std::uint32_t samples, std::uint32_t repeats) { auto& block = info.block[info.num_blocks++]; - block = {info.data_offset + offset, bytes, repeats, static_cast<std::uint32_t>(skip), samples, 0, 0}; - return true; + block = Atrac9Block(info.format.config_data, begin, samples); + block.data_offset += info.data_offset; + block.num_repeats = repeats; } static int ParseAtrac9Loop(const RiffChunks& chunks, Ngs2WaveformInfo& info) { if (chunks.sampler == nullptr) return SCE_NGS2_OK; - if (chunks.samplerSize < 36) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; - const auto count = ReadLe32(chunks.sampler + 28); - if (count > (chunks.samplerSize - 36) / 24) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; - if (count == 0) return SCE_NGS2_OK; - if (count != 1) throw std::runtime_error("NGS2: multiple waveform loops are not implemented"); - const auto* loop = chunks.sampler + 36; - if (ReadLe32(loop + 4) != 0 || ReadLe32(loop + 16) != 0) - throw std::runtime_error("NGS2: non-forward or fractional waveform loops are not implemented"); - const std::uint64_t begin = ReadLe32(loop + 8); - const std::uint64_t end = static_cast<std::uint64_t>(ReadLe32(loop + 12)) + 1; - const auto skip = info.block[0].num_skip_samples; - const std::uint64_t waveformEnd = static_cast<std::uint64_t>(skip) + info.num_samples; - if (begin < skip || begin >= end || end > waveformEnd) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; - const auto plays = ReadLe32(loop + 20); - info.loop_begin_position = static_cast<std::uint32_t>(begin - skip); - info.loop_end_position = static_cast<std::uint32_t>(end - skip); + if (chunks.samplerSize < 32) throw std::runtime_error("NGS2: a smpl chunk too short for its loop count is not implemented"); + if (ReadLe32(chunks.sampler + 28) == 0) return SCE_NGS2_OK; + if (chunks.samplerSize < 52) throw std::runtime_error("NGS2: a smpl chunk too short for its first loop is not implemented"); + const std::uint32_t begin = ReadLe32(chunks.sampler + 44) - info.num_delay_samples; + const std::uint32_t end = ReadLe32(chunks.sampler + 48) - info.num_delay_samples + 1u; + info.loop_begin_position = begin; + info.loop_end_position = end; + if (end <= begin) return SCE_NGS2_OK; + if (end > info.num_samples) throw std::runtime_error("NGS2: a waveform loop ending past the last sample is not implemented"); info.num_blocks = 0; - info.block[0] = {}; - if (!AddAtrac9Range(info, skip, info.loop_begin_position, 0) || - !AddAtrac9Range(info, begin, static_cast<std::uint32_t>(end - begin), plays == 0 ? UINT32_MAX : plays - 1) || - !AddAtrac9Range(info, end, static_cast<std::uint32_t>(waveformEnd - end), 0)) - return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + if (begin != 0) AddAtrac9Block(info, 0, begin, 0); + AddAtrac9Block(info, begin, end - begin, UINT32_MAX); + if (end != info.num_samples) AddAtrac9Block(info, end, info.num_samples - end, 0); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 15b7b0d42a..39736292e8 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -9,6 +9,7 @@ #include <iterator> #include <random> #include <stdexcept> +#include <tuple> #include <utility> #include <vector> @@ -314,7 +315,7 @@ static void TestParseLoops() { Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); Require(info.loop_begin_position == begin && info.loop_end_position == end && info.num_samples == samples); Require(info.num_blocks == 1 + (begin != 0) + (end != samples)); - Require(info.block[begin != 0].num_repeats == 1); + Require(info.block[begin != 0].num_repeats == UINT32_MAX); const auto system = CreateSystem(); const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, info.format}); @@ -322,12 +323,12 @@ static void TestParseLoops() { Ngs2SamplerVoiceWaveformBlocksParam{{}, file.data(), 0, info.num_blocks, info.block}); Patch(voice, Mastering(system, 1)); Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); - const auto rendered = Render(system, samples + end - begin + Grain); + const auto loop = end - begin; + const auto rendered = Render(system, end + 2 * loop); for (std::uint32_t i = 0; i < rendered.size(); ++i) { - const auto index = i < end ? i : i - (end - begin); - Require(rendered[i] == (i < samples + end - begin ? reference[256 + index] : 0.0f)); + const auto index = i < end ? i : begin + (i - end) % loop; + Require(rendered[i] == reference[256 + index]); } - Require(Flags(voice) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } } @@ -355,28 +356,55 @@ static void TestParseLoops() { Require(sceNgs2SystemDestroy(system, nullptr) == 0); } +static bool ParseThrows(const std::vector<std::uint8_t>& file) { + Ngs2WaveformInfo info{}; + try { + sceNgs2ParseWaveformData(file.data(), file.size(), &info); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static std::vector<std::uint8_t> ShortSampler(std::vector<std::uint8_t> file, std::uint32_t size) { + file.erase(file.begin() + 100 + size, file.begin() + 160); + Set32(file, 96, size); + Set32(file, 4, static_cast<std::uint32_t>(file.size() - 8)); + return file; +} + static void TestMalformedLoops() { const auto original = LoopedFile(100, 700, 0); - Ngs2WaveformInfo info{}; - for (const auto [offset, value] : {std::pair<std::size_t, std::uint32_t>{128, 2}, {144, 0}, {148, 255}, {148, 5000}}) { + Ngs2WaveformInfo expected{}; + Require(sceNgs2ParseWaveformData(original.data(), original.size(), &expected) == SCE_NGS2_OK && expected.num_blocks == 3); + for (const auto [offset, value] : {std::pair<std::size_t, std::uint32_t>{128, 2}, {140, 1}, {140, 2}, {152, 0x80000000u}, {156, 5}}) { auto file = original; Set32(file, offset, value); - Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.loop_begin_position == 100 && info.loop_end_position == 700 && info.num_blocks == 3); + for (std::uint32_t block = 0; block < 3; ++block) Require(std::memcmp(&info.block[block], &expected.block[block], sizeof(Ngs2WaveformBlock)) == 0); } - for (const auto offset : {140u, 152u}) { + for (const auto [start, last, begin, end] : {std::tuple<std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t>{0, 955, 0xffffff00u, 700}, {356, 255, 100, 0}, {956, 955, 700, 700}, {956, 700, 700, 445}}) { auto file = original; - Set32(file, offset, 1); - bool rejected = false; - try { sceNgs2ParseWaveformData(file.data(), file.size(), &info); } catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + Set32(file, 144, start); + Set32(file, 148, last); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.loop_begin_position == begin && info.loop_end_position == end && info.num_blocks == 1); + Require(info.block[0].num_repeats == 0 && info.block[0].num_samples == info.num_samples); } + for (const std::uint32_t last : {4096u, 5000u, 0xffffffffu}) { + auto file = original; + Set32(file, 148, last); + Require(ParseThrows(file)); + } + Ngs2WaveformInfo info{}; for (std::size_t size = 92; size < 168; ++size) Require(sceNgs2ParseWaveformData(original.data(), size, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - auto shortSampler = original; - shortSampler.erase(shortSampler.begin() + 100 + 35, shortSampler.begin() + 160); - Set32(shortSampler, 96, 35); - Set32(shortSampler, 4, static_cast<std::uint32_t>(shortSampler.size() - 8)); - Require(sceNgs2ParseWaveformData(shortSampler.data(), shortSampler.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + for (const std::uint32_t size : {28u, 36u, 50u}) Require(ParseThrows(ShortSampler(original, size))); + const auto minimal = ShortSampler(original, 52); + Require(sceNgs2ParseWaveformData(minimal.data(), minimal.size(), &info) == SCE_NGS2_OK && info.num_blocks == 3); } static std::uint8_t* SuperframeBeforeGuardPage() { diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 44c7a955fb..fd60051d79 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -7,7 +7,7 @@ - [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. -- [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. +- [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - a loop ending past the last sample (the module's tail count wraps), a `smpl` chunk too short for its count or first loop, and a looped file at a rate the module has no frame size for throw; unmeasured on a console. As [prosper reads the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L249-L257), only the first loop's start and inclusive end are read, less the one-frame decoder delay; type, fraction, play count and other loops are ignored. As in [its block list](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L297-L346), an end not after the start keeps one block, else an intro, the loop repeating forever and a tail. PPSA03747's chunk (start 256, inclusive end 299224, 298969 samples) gives one block repeating forever - [NGS2 ATRAC9 parsed delay](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reports one codec frame as `num_delay_samples` whatever the `fact` chunk's delay word holds, as [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) sets it for ATRAC9 (its [test](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/tests/hle/test_ngs2_waveform.cpp#L326-L342) expects 256 for a `fact` of 900 and 0). Not measured on a console. The block skip still comes from the `fact` chunk's third word, where prosper uses the same one-frame delay - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. From c11623c211420d47bed53c26133979e2bbf7547d Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:26:12 +0200 Subject: [PATCH 0498/1360] feat(libSceFreeType): implement FT_Set_Transform --- core/libs/prx/libSceFreeType/Export.cpp | 16 ++++ .../libSceFreeType/include/GuestFreeType.hpp | 8 ++ core/libs/tests/GuestFreeType.cpp | 90 +++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 116 insertions(+) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0c4f8d79b1..cb2d953700 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -205,4 +205,20 @@ std::uint64_t APS5_VABI FT_Get_Next_Char_nid_postfix(GuestFaceRec* face, std::ui return FT_Get_Next_Char(owner ? owner->host : nullptr, static_cast<FT_ULong>(static_cast<std::uint32_t>(charcode)), glyphIndex); } +void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec* face, const GuestMatrix* matrix, const GuestVector* delta) { + GuestFace* owner = FaceOf(face); + if (!owner) return; + const auto component = [](std::int64_t value) { + if (value < std::numeric_limits<FT_Long>::min() || value > std::numeric_limits<FT_Long>::max()) { + throw std::runtime_error("FT_Set_Transform: a component outside the host FT_Long range is not implemented"); + } + return static_cast<FT_Long>(value); + }; + FT_Matrix nativeMatrix{}; + FT_Vector nativeDelta{}; + if (matrix) nativeMatrix = {component(matrix->xx), component(matrix->xy), component(matrix->yx), component(matrix->yy)}; + if (delta) nativeDelta = {component(delta->x), component(delta->y)}; + FT_Set_Transform(owner->host, matrix ? &nativeMatrix : nullptr, delta ? &nativeDelta : nullptr); +} + } diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 1a2a074378..d9134a33e6 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -18,6 +18,13 @@ struct GuestVector { std::int64_t y; }; +struct GuestMatrix { + std::int64_t xx; + std::int64_t xy; + std::int64_t yx; + std::int64_t yy; +}; + struct GuestBBox { std::int64_t xMin; std::int64_t yMin; @@ -149,6 +156,7 @@ struct GuestFaceRec { void* internal; }; +static_assert(sizeof(GuestMatrix) == 32 && offsetof(GuestMatrix, xy) == 8 && offsetof(GuestMatrix, yx) == 16 && offsetof(GuestMatrix, yy) == 24); static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 8e5c7e7d0f..52a1a3ee51 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -14,6 +14,7 @@ #include <filesystem> #include <fstream> #include <initializer_list> +#include <limits> #include <random> #include <stdexcept> #include <string> @@ -23,6 +24,7 @@ using namespace FreeType; extern "C" { +void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec*, const GuestMatrix*, const GuestVector*); int APS5_VABI FT_Init_FreeType_nid_postfix(void**); int APS5_VABI FT_Done_FreeType_nid_postfix(void*); int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); @@ -141,6 +143,93 @@ static void SetFinalizers(GuestFaceRec* face, Finalizers& events) { face->size->generic = {&events, GuestSizeFinalizer}; } +static void CheckTransforms(const std::vector<unsigned char>& font) { + FT_Library native = nullptr; + FT_Face host = nullptr; + FT_Face nativeOther = nullptr; + void* library = nullptr; + GuestFaceRec* face = nullptr; + GuestFaceRec* other = nullptr; + Require(FT_Init_FreeType(&native) == 0 && FT_Init_FreeType_nid_postfix(&library) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast<FT_Long>(font.size()), 0, &host) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast<FT_Long>(font.size()), 0, &nativeOther) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &other) == 0); + Require(FT_Set_Pixel_Sizes(host, 0, 100) == 0 && FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0); + Require(FT_Set_Pixel_Sizes(nativeOther, 0, 100) == 0 && FT_Set_Pixel_Sizes_nid_postfix(other, 0, 100) == 0); + const std::array matrices{ + GuestMatrix{65536, 0, 0, 65536}, GuestMatrix{0, -65536, 65536, 0}, GuestMatrix{0, 65536, -65536, 0}, + GuestMatrix{-65536, 0, 0, 65536}, GuestMatrix{131072, 0, 0, 32768}, GuestMatrix{65536, 16384, -8192, 65536}, + GuestMatrix{0, 0, 0, 0} + }; + const std::array deltas{GuestVector{0, 0}, GuestVector{64, -128}, GuestVector{31, -63}, GuestVector{-96, 192}}; + std::size_t comparisons = 0; + for (std::size_t m = 0; m <= matrices.size(); ++m) { + for (std::size_t d = 0; d <= deltas.size(); ++d) { + GuestMatrix input = m < matrices.size() ? matrices[m] : matrices[0]; + GuestVector delta = d < deltas.size() ? deltas[d] : deltas[0]; + FT_Matrix matrix{static_cast<FT_Fixed>(input.xx), static_cast<FT_Fixed>(input.xy), static_cast<FT_Fixed>(input.yx), static_cast<FT_Fixed>(input.yy)}; + FT_Vector translation{static_cast<FT_Pos>(delta.x), static_cast<FT_Pos>(delta.y)}; + std::array<unsigned char, sizeof(GuestGlyphSlotRec)> savedSlot{}; + std::array<unsigned char, sizeof(GuestSizeRec)> savedSize{}; + std::memcpy(savedSlot.data(), face->glyph, savedSlot.size()); + std::memcpy(savedSize.data(), face->size, savedSize.size()); + FT_Set_Transform_nid_postfix(face, m < matrices.size() ? &input : nullptr, d < deltas.size() ? &delta : nullptr); + FT_Set_Transform(host, m < matrices.size() ? &matrix : nullptr, d < deltas.size() ? &translation : nullptr); + Require(std::memcmp(savedSlot.data(), face->glyph, savedSlot.size()) == 0); + Require(std::memcmp(savedSize.data(), face->size, savedSize.size()) == 0); + Require(input.xx == matrix.xx && input.xy == matrix.xy && input.yx == matrix.yx && input.yy == matrix.yy); + Require(delta.x == translation.x && delta.y == translation.y); + input = matrices[0]; + delta = deltas[0]; + for (const auto flags : std::array<std::int32_t, 3>{FT_LOAD_DEFAULT, FT_LOAD_IGNORE_TRANSFORM, FT_LOAD_RENDER}) { + for (const std::uint32_t glyph : {0u, 1u}) { + Require(FT_Load_Glyph_nid_postfix(face, glyph, flags) == FT_Load_Glyph(host, glyph, flags)); + SlotMatches(face->glyph, host->glyph); + if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(host->glyph, FT_RENDER_MODE_NORMAL)); + SlotMatches(face->glyph, host->glyph); + } + ++comparisons; + } + } + Require(FT_Load_Glyph_nid_postfix(other, 1, FT_LOAD_DEFAULT) == FT_Load_Glyph(nativeOther, 1, FT_LOAD_DEFAULT)); + SlotMatches(other->glyph, nativeOther->glyph); + } + } + GuestMatrix rejected{65536, 0, 0, 65536}; + GuestVector rejectedDelta{}; + const auto loadUnchanged = [&] { + Require(FT_Load_Glyph_nid_postfix(face, 1, FT_LOAD_DEFAULT) == FT_Load_Glyph(host, 1, FT_LOAD_DEFAULT)); + SlotMatches(face->glyph, host->glyph); + }; + if constexpr (sizeof(FT_Long) < sizeof(std::int64_t)) { + for (const auto value : std::array<std::int64_t, 5>{-0x80000001ll, 0x80000000ll, 0x100000000ll, + std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max()}) { + for (auto member : {&GuestMatrix::xx, &GuestMatrix::xy, &GuestMatrix::yx, &GuestMatrix::yy}) { + rejected = matrices[0]; + rejected.*member = value; + Require(ThrowsWith([&] { FT_Set_Transform_nid_postfix(face, &rejected, nullptr); }, + "FT_Set_Transform: a component outside the host FT_Long range is not implemented")); + loadUnchanged(); + } + for (auto member : {&GuestVector::x, &GuestVector::y}) { + rejectedDelta = {}; + rejectedDelta.*member = value; + Require(ThrowsWith([&] { FT_Set_Transform_nid_postfix(face, &matrices[1], &rejectedDelta); }, + "FT_Set_Transform: a component outside the host FT_Long range is not implemented")); + loadUnchanged(); + } + } + } + rejected.xx = std::numeric_limits<std::int64_t>::max(); + rejectedDelta.x = std::numeric_limits<std::int64_t>::min(); + FT_Set_Transform_nid_postfix(nullptr, &rejected, &rejectedDelta); + FT_Set_Transform_nid_postfix(nullptr, nullptr, nullptr); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(native) == 0); + std::printf("FT_Set_Transform: %zu native glyph and bitmap comparisons\n", comparisons); +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); @@ -339,6 +428,7 @@ int main() { void* empty = nullptr; Require(FT_Init_FreeType_nid_postfix(&empty) == 0 && FT_Done_FreeType_nid_postfix(empty) == 0); + CheckTransforms(font); std::filesystem::current_path(previous); std::error_code ignored; std::filesystem::remove_all(root, ignored); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..2f2bdcbe7a 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -10,6 +10,8 @@ ### Functional +- [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. + - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read From 032344e05a4059147339fc7379a8dd9e1208c7f3 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:35:18 +0300 Subject: [PATCH 0499/1360] fix(tools): wrap entrypoint in main with stream reconfigure in unwrap_self --- tools/tests/test_unwrap_self.py | 10 ++++++++++ tools/unwrap_self.py | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 6ee6d2095e..49f1344d3a 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -106,6 +106,16 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(source.read_bytes(), container) self.assertEqual(alias.read_bytes(), container) + def test_main_with_explicit_argv(self): + container = build_container(default_segments(), default_records(), default_payloads()) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + with contextlib.redirect_stdout(io.StringIO()): + code = unwrap_self.main([str(source), str(output)]) + self.assertEqual(code, 0) + self.assertTrue(output.exists()) + def convert(self, container, name="input.self"): directory = Path(self.directory) source = directory / name diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 1ce81f2e12..78a35a5a36 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -328,12 +328,15 @@ def unwrap(path, output, verbose=False): return len(result) -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="unwrap a decrypted SELF or fSELF container into a clean ELF") parser.add_argument("input", help="SELF container to read") parser.add_argument("output", help="ELF executable to write") parser.add_argument("--verbose", action="store_true", help="print the program header mapping") - arguments = parser.parse_args() + arguments = parser.parse_args(argv) try: size = unwrap(arguments.input, arguments.output, arguments.verbose) except Refused as error: @@ -347,6 +350,4 @@ def main(): if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") sys.exit(main()) From 8cded11b59e765ce27280176e189e66a36aa1f9d Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:41:47 +0300 Subject: [PATCH 0500/1360] fix(tools): decode compile commands JSON with utf-8-sig in check_compile --- tools/check_compile.py | 2 +- tools/tests/test_check_compile.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index a251673abf..9b66a1cd16 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -45,7 +45,7 @@ def entries(build): raise ValueError(f"{build} is not a directory; configure the build first") manifest = build / "compile_commands.json" if manifest.is_file(): - return json.loads(manifest.read_text(encoding="utf-8")) + return json.loads(manifest.read_text(encoding="utf-8-sig")) ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index 3dcc4b1d43..ffd20390ab 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -216,6 +216,15 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + def test_compile_commands_accepts_utf8_bom(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') + (self.build / "compile_commands.json").write_text("\ufeff" + json.dumps([entry]), encoding="utf-8") + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + def test_every_configuration_is_checked_and_each_source_is_counted_once(self): source = self.source("unit.cpp") script = self.compiler( From a42182ef499531a3737307ff2b407a2c2eaadd7e Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Wed, 21 Oct 2026 19:08:36 +0700 Subject: [PATCH 0501/1360] fix(libSceNpTrophy2): return INVALID_TROPHY_ID for a trophy that is not in the title's data sceNpTrophy2GetTrophyInfo threw std::invalid_argument for a trophy id that is not in the title's trophyNN.ucp, so a title that asks for one stopped (PPSA14363 asks for trophy 19 of ids 0 to 18). KytyPS5 returns NP_TROPHY2_ERROR_INVALID_TROPHY_ID (0x8055390A) there without writing either output; return the same. A group that is not in the data still throws. Co-Authored-By: Claude <noreply@anthropic.com> --- .../include/NpTrophy2Constants.hpp | 1 + core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp | 4 +--- core/libs/tests/GuestNpTrophyData.cpp | 16 ++++++++++++++-- docs/dev/debt/libSceNpTrophy2.md | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 6a853b4c28..58ec7f5e33 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -4,6 +4,7 @@ #include <cstdint> static constexpr int SCE_NP_TROPHY2_OK = 0; +static constexpr int SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID = -2141898486; static constexpr int SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND = -2141898479; static constexpr int NP_TROPHY2_CONTEXT_DEFAULT = 1; diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index ce29da1513..cc32a75483 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -82,9 +82,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, return SCE_NP_TROPHY2_OK; } const auto trophy = package->trophies.find(trophy_id); - if (trophy == package->trophies.end()) { - throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); - } + if (trophy == package->trophies.end()) return SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID; if (details != nullptr) fillDetails(trophy->second, details); if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index 5c4eaf025a..8bf4e7ad00 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -2,6 +2,7 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include <algorithm> #include <cstddef> #include <cstdint> #include <cstdio> @@ -36,6 +37,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTro } static constexpr int InitialUser = 0x10000000; +static constexpr int InvalidTrophyId = static_cast<int>(0x8055390Au); static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ {"id":"0","grade":"P"}, @@ -168,8 +170,18 @@ static void ReportTrophyData(int context) { Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported"); const auto steps = Trophy(context, 5, &details); Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target"); - NpTrophy2Data unknown{}; - Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported"); + const auto untouched = [](const void* bytes, std::size_t size) { + const auto* first = static_cast<const unsigned char*>(bytes); + return std::all_of(first, first + size, [](unsigned char byte) { return byte == 0x5a; }); + }; + for (const int unknownId : {8, 99, -1}) { + NpTrophy2Details unknownDetails{}; + NpTrophy2Data unknown{}; + std::memset(&unknownDetails, 0x5a, sizeof(unknownDetails)); + std::memset(&unknown, 0x5a, sizeof(unknown)); + Require(sceNpTrophy2GetTrophyInfo(context, 1, unknownId, &unknownDetails, &unknown) == InvalidTrophyId, "a trophy that is not in the package did not return the invalid trophy id error"); + Require(untouched(&unknownDetails, sizeof(unknownDetails)) && untouched(&unknown, sizeof(unknown)), "a trophy that is not in the package wrote its details or data"); + } std::vector<NpTrophy2Details> list(10); std::vector<NpTrophy2Data> listed(10); diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 4576f12de6..e6cb5cd197 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -14,7 +14,7 @@ - [libSceNpTrophy2 pending exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 23 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy2.c). ### Functional -- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. +- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) - not confirmed on the console: `sceNpTrophy2GetTrophyInfo` of a trophy id that is not in the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) returns `0x8055390A` (KytyPS5's `NP_TROPHY2_ERROR_INVALID_TROPHY_ID`) and writes nothing; a context not created or destroyed reports placeholders and no unlocks and returns success; hidden trophies report their names and descriptions before the unlock; `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points). A group that is not in the data throws. Without the file, or when it is malformed, the queries report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Unlocks come from the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_<tag>.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. From 5266e7f770ac22a0e28e1602605a7aea06d3e1b8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:16:25 +0300 Subject: [PATCH 0502/1360] fix(http): recover cache and request option stub signatures verify argument widths against the local system module, including the four-register auth cache export and four-byte option outputs. keep every throwing stub body unchanged. --- core/libs/prx/libSceHttp/Export.cpp | 24 ++++++++++++------------ docs/dev/debt/libSceHttp.md | 12 ------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 5c537baf62..a8d7d7eeec 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -634,12 +634,12 @@ int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { return 0; } -int APS5_VABI sceHttpAuthCacheImport(void) { +int APS5_VABI sceHttpAuthCacheImport(int contextId, const void* buffer, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpAuthCacheExport(void) { +int APS5_VABI sceHttpAuthCacheExport(int contextId, void* buffer, std::size_t size, std::size_t* requiredSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -687,17 +687,17 @@ int APS5_VABI sceHttpDbgGetRequestStat(void) { return 0; } -int APS5_VABI sceHttpAddRequestHeaderRaw(void) { +int APS5_VABI sceHttpAddRequestHeaderRaw(int id, const char* header, int mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpRemoveRequestHeader(void) { +int APS5_VABI sceHttpRemoveRequestHeader(int id, const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpGetAuthEnabled(void) { +int APS5_VABI sceHttpGetAuthEnabled(int id, int* enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -717,7 +717,7 @@ int APS5_VABI sceHttpGetCookie(void) { return 0; } -int APS5_VABI sceHttpGetCookieEnabled(void) { +int APS5_VABI sceHttpGetCookieEnabled(int id, int* enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -767,12 +767,12 @@ int APS5_VABI sceHttpGetCookieStats(void) { return 0; } -int APS5_VABI sceHttpSetPriorityOption(void) { +int APS5_VABI sceHttpSetPriorityOption(int id, int priority) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpSetPolicyOption(void) { +int APS5_VABI sceHttpSetPolicyOption(int id, int policy) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -788,7 +788,7 @@ int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int http_ctx_id, int en return SetHttpContextAcceptEncodingGzip(http_ctx_id, enable != 0) ? 0 : ERROR_INVALID_ID; } -int APS5_VABI sceHttpSetProxy(void) { +int APS5_VABI sceHttpSetProxy(int id, int mode, int authentication, const char* hostname, std::uint16_t port) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -798,7 +798,7 @@ int APS5_VABI sceHttpSetSocketCreationCallback(void) { return 0; } -int APS5_VABI sceHttpSetHttp09Enabled(void) { +int APS5_VABI sceHttpSetHttp09Enabled(int id, int enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -813,12 +813,12 @@ int APS5_VABI sceHttpGetConnectionStat(void) { return 0; } -int APS5_VABI sceHttpAddQuery(void) { +int APS5_VABI sceHttpAddQuery(int id, const char* query) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpSetDelayBuildRequestEnabled(void) { +int APS5_VABI sceHttpSetDelayBuildRequestEnabled(int id, int enabled) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 7119fff249..e743811644 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -21,8 +21,6 @@ - [sceHttpSetAuthInfoCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - read from the disassembly of libSceHttp.sprx on firmware 13.60, not measured on a console and not compared with other emulators. The arguments are (out, src, pool, prepare, require), `prepare` before `require`, the reverse of `sceHttpUriParse`. A null source returns `ERROR_INVALID_URL`; without an output or a pool, a null `require` returns `ERROR_INVALID_VALUE`, otherwise the size is only reported. Only the scheme, hostname, path, username and password are copied, in that order, each with its terminator (a missing one becomes an empty string and takes one byte), and the port; the query, the fragment and the opaque flag stay 0. A pool that is too small clears the output and the pool and returns `ERROR_OUT_OF_MEMORY` (0x80431022, the console's out-of-size error) -- [sceHttpAuthCacheImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpAuthCacheExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature @@ -32,13 +30,9 @@ - [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgGetRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpAddRequestHeaderRaw](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpRemoveRequestHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetAuthEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpAddCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpCookieExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature @@ -48,16 +42,10 @@ - [sceHttpSetCookieMaxNum](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetCookieMaxNumPerDomain](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetCookieStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetPriorityOption](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetPolicyOption](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpRequestGetAllHeaders](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetProxy](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetHttp09Enabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetRegisteredCtxIds](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpAddQuery](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetDelayBuildRequestEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature ### Functional From 9f39eb44da1407ab6ef6b6af53279a72c1fa7e9f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:18:17 +0300 Subject: [PATCH 0503/1360] fix(json): correct the object erase return contract the system export leaves the return register unspecified on missing keys and tail-calls node removal on a match. declare void and check object contents instead of a fabricated removal count. guest_json passes on windows. --- core/libs/prx/libSceJson2/Export.cpp | 5 ++--- core/libs/tests/GuestJson.cpp | 7 ++++--- docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 6231181390..ff9da15cec 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -730,14 +730,13 @@ ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(ObjectIterator* } bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } -std::size_t APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object* self, const String* key) { +void APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object* self, const String* key) { if (key == nullptr || key->text == nullptr) throw std::invalid_argument("sce::Json::Object::erase: null key"); auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); - if (it == self->items->end()) return 0; + if (it == self->items->end()) return; Destroy(it->key); Destroy(it->value); self->items->erase(it); - return 1; } void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value* self) { Construct(*self); } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 16de775009..f6569900e7 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -145,7 +145,7 @@ void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(Iterator*, const Iterator*); Iterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(Iterator*, std::size_t); void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(Iterator*, const Iterator*); Iterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(Iterator*, std::size_t); -std::size_t APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object*, const String*); +void APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object*, const String*); } static void Check(bool value, int line) { @@ -697,9 +697,10 @@ static void ObjectOperations() { _ZN3sce4Json6Object8iteratorD1Ev(©); Require(_ZNK3sce4Json6Object4sizeEv(&object) == 3); - Require(_ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2) == 1); + _ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + _ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2); Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); - Require(_ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2) == 0); Iterator findResult{}, end{}; _ZNK3sce4Json6Object4findERKNS0_6StringE(&findResult, &object, &k2); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 3a89284e01..015e476dac 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -10,7 +10,7 @@ - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - member of `Value` returning `bool` as a conversion operator, with no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: evaluates to `false` for `TypeNull` and boolean `false`, and `true` for boolean `true`. For all other value types it currently returns `true`; whether the console converts them, throws, or checks container emptiness is unverified -- [sce::Json::Object::insert and sce::Json::Object::erase](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - signatures and behaviors have no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: `insert` returns an iterator through a result pointer before `this` as `Object::begin` does, and takes a const Pair reference. If the key exists, it updates the existing value and returns an iterator to it; otherwise it appends a deep copy of the pair and returns an iterator to the new element; whether the console updates existing keys, appends duplicates, or ignores duplicate insertion is unverified. `erase` removes the item with matching key and returns 1 if found and removed, or 0 if not; whether the console returns size_t, bool, or an iterator is unverified +- [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - signatures and behaviors have no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: `insert` returns an iterator through a result pointer before `this` as `Object::begin` does, and takes a const Pair reference. If the key exists, it updates the existing value and returns an iterator to it; otherwise it appends a deep copy of the pair and returns an iterator to the new element; whether the console updates existing keys, appends duplicates, or ignores duplicate insertion is unverified. ### Functional From 6f1e594cebbaeeb93fc683f37aa5243c7e74243a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:20:15 +0300 Subject: [PATCH 0504/1360] fix(np): recover session signaling query argument lists follow five exported dispatchers in the local system module. retain opaque payload pointers and document unresolved layouts instead of guessing their fields. throwing stub bodies remain unchanged. --- core/libs/prx/libSceNpSessionSignaling/Export.cpp | 10 +++++----- docs/dev/debt/libSceNpSessionSignaling.md | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 7dfa95ff73..8c60a0db57 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -79,7 +79,7 @@ int APS5_VABI sceNpSessionSignalingRequestPrepare(uint32_t contextId, uint32_t* return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingGetMemoryInfo(void) { +int APS5_VABI sceNpSessionSignalingGetMemoryInfo(void* memoryInfo) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -89,22 +89,22 @@ int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(void) { return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(void) { +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(std::uint32_t contextId, const void* peerAddress, void* connectionId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(std::uint32_t contextId, const void* sessionId, void* groupId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetGroupInfo() { +int APS5_VABI sceNpSessionSignalingGetGroupInfo(std::uint32_t contextId, std::uint32_t groupId, void* groupInfo) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2(std::uint32_t contextId, std::uint32_t addressType, std::uint32_t address, std::uint16_t port, void* connectionId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index f5f1396a51..d37e80e5ca 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -2,9 +2,8 @@ ### Unknown function info -- [sceNpSessionSignalingGetConnectionFromPeerAddress](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [sceNpSessionSignalingGetMemoryInfo](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified -- PPSA31327 imports declared without parameters, signatures unknown: [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling), imported by Yakuza 0 Director's Cut and not called before the main menu. - [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. + +- [sceNpSessionSignalingGetMemoryInfo, sceNpSessionSignalingGetConnectionFromPeerAddress, sceNpSessionSignalingGetGroupFromSessionId, sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - argument registers and scalar widths follow the system module exports `lbXTXRG5nyM`, `dJJ0UPrrsok`, `UGcXquhVYrY`, `WtOs6cUbrx8` and `hbNcE4fnBDs`. Pointer payloads remain opaque: their layouts and the address-type values require tracing the virtual callees. All five exports remain throwing stubs. From 7e8fede88c44d668e1f259b1060414db13ca6857 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:21:24 +0300 Subject: [PATCH 0505/1360] fix(rtc): restore the complete system timezone table decode all 70 entries at 0x4b80 used by the lookup at 0x15d0. preserve lookup order and the parser's uppercase second-letter check. cover missing zones, case handling and prefix matching in guest_rtc on windows. --- core/libs/prx/libSceRtc/Export.cpp | 22 ++++++++++++++-------- core/libs/tests/GuestRtc.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceRtc.md | 1 - 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 43d049c976..7364077dff 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -270,14 +270,20 @@ bool parseZoneName(const char* cursor, std::int64_t& offsetMinutes) { const char* name; std::int64_t minutes; } zones[] = { - {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, {"SWT", 60}, {"MEZ", 60}, {"AESST", 660}, {"ACSST", 630}, - {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, - {"GMT", 0}, {"WET", 0}, {"BST", 60}, {"WAT", -60}, {"AST", -240}, {"ADT", -180}, {"EST", -300}, {"EDT", -240}, - {"CST", -360}, {"CDT", -300}, {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"YST", -540}, {"YDT", -480}, - {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NFT", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, - {"DNT", 60}, {"NOR", 60}, {"SET", 60}, {"MEWT", 60}, {"MEST", 120}, {"CEST", 120}, {"SST", 120}, {"FWT", 120}, {"FST", 60}, {"EET", 120}, {"IST", 120}, - {"WAST", 420}, {"WADT", 480}, {"CCT", 480}, {"AWST", 480}, {"JST", 540}, {"KST", 540}, {"ACST", 570}, {"EAST", 600}, - {"GST", 600}, {"AEST", 600}, {"NZT", 720}, {"NZST", 720}, {"NZDT", 780}, {"IDLE", 720}, {"JT", 450}, + {"GMT", 0}, {"EST", -300}, {"EDT", -240}, {"CST", -360}, {"CDT", -300}, + {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"NZDT", 780}, + {"NZST", 720}, {"IDLE", 720}, {"NZT", 720}, {"AESST", 660}, {"ACSST", 630}, + {"CADT", 630}, {"SADT", 630}, {"AEST", 600}, {"EAST", 600}, {"GST", 600}, + {"LIGT", 600}, {"ACST", 570}, {"SAST", 570}, {"CAST", 570}, {"AWSST", 540}, + {"JST", 540}, {"KST", 540}, {"WDT", 540}, {"MT", 510}, {"AWST", 480}, + {"CCT", 480}, {"WADT", 480}, {"WST", 480}, {"JT", 450}, {"WAST", 420}, + {"IT", 210}, {"BT", 180}, {"EETDST", 180}, {"EET", 120}, {"CETDST", 120}, {"CEST", 120}, + {"FWT", 120}, {"IST", 120}, {"MEST", 120}, {"METDST", 120}, {"SST", 120}, + {"BST", 60}, {"CET", 60}, {"DNT", 60}, {"FST", 60}, {"MET", 60}, + {"MEWT", 60}, {"MEZ", 60}, {"NOR", 60}, {"SET", 60}, {"SWT", 60}, + {"WETDST", 60}, {"WET", 0}, {"WAT", -60}, {"NDT", -90}, {"ADT", -180}, + {"NFT", -150}, {"NST", -150}, {"AST", -240}, {"YDT", -480}, {"HDT", -540}, + {"YST", -540}, {"AHST", -600}, {"CAT", -600}, {"NT", -660}, {"IDLW", -720}, }; for (const auto& zone : zones) { const char* name = cursor; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a5585f843d..53e55fd327 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -438,6 +438,8 @@ int main() { const char* name; std::int64_t minutes; } additionalZones[] = { + {"LIGT", 600}, {"AWSST", 540}, {"CETDST", 120}, + {"DNT", 60}, {"NOR", 60}, {"SET", 60}, {"NFT", -150}, {"SWT", 60}, {"MEZ", 60}, {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, {"AESST", 660}, {"ACSST", 630}, {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, }; @@ -463,6 +465,20 @@ int main() { Require(tick.tick == at(2000, 3, 1, 8, 9, 10) - static_cast<std::uint64_t>(zone.minutes * 60000000)); } + const struct { + const char* name; + std::int64_t minutes; + } secondLetterZones[] = { + {"MT", 0}, {"mt", 510}, {"IT", 0}, {"it", 210}, + {"BT", 0}, {"bt", 180}, {"NT", 0}, {"nt", -660}, + }; + for (const auto& zone : secondLetterZones) { + char text[80]; + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", zone.name); + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast<std::uint64_t>(zone.minutes * 60000000)); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index cf5399b601..8b683a05c6 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -6,7 +6,6 @@ ### Unknown function info -- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the zone name table matches every name of 1 to 3 uppercase letters as a payload process on PS5 firmware 13.60 parsed them in the RFC 2822 form; of the longer names only real ones were tried (ACDT, AEST, CEST, NZDT, MESZ and others). Any other name of 4 or more letters is assumed to take the military letter offset of its first character, so a longer name the console's table holds but nobody measured gets the wrong offset - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick formats the current time, as shadPS4 does and as these three and `sceRtcFormatRFC3339` did in a payload process on a PS5 (firmware 13.60); offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`, also with a null tick there; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date From 2a9e6d8defc722166c4aba2b23730b4aebbb68c8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:23:02 +0300 Subject: [PATCH 0506/1360] fix(screenshot): recover overlay image stub signatures verify the path and two 32-bit coordinates in the overlay loader and the fourth origin argument in the forwarding export. retain both throwing bodies and remove the resolved signature debt. --- core/libs/prx/libSceScreenShot/Export.cpp | 4 ++-- docs/dev/debt/libSceScreenShot.md | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp index ae99fefcf6..3f898a1f74 100644 --- a/core/libs/prx/libSceScreenShot/Export.cpp +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -5,11 +5,11 @@ extern "C" { -int APS5_VABI sceScreenShotSetOverlayImage() { +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int x, int y) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceScreenShotSetOverlayImageWithOrigin() { +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int x, int y, int origin) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index 568cb899b6..2e1f886b38 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -3,4 +3,3 @@ ### Unknown function info - [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c). The PS4 compatibility implementation also exposes the names but declares no parameters; no PS5 prototype was found. The declarations have no parameters solely to resolve known imports and make no ABI claim From d402d576fb1dbf266467554573806b2500f1e5a1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:24:54 +0300 Subject: [PATCH 0507/1360] fix(voiceqos): recover packet and attribute stub signatures restore packet size pointers, attribute buffers, debug query arguments and hq initialization arguments from the system module. correct the writable remote attribute output and align existing stub tests with the recovered declarations. both guest_voice_qos tests pass on windows. --- core/libs/prx/libSceVoiceQoS/Export.cpp | 8 ++++---- core/libs/tests/GuestVoiceQoS.cpp | 10 +++++----- docs/dev/debt/libSceVoiceQoS.md | 3 +-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 96053a9e69..3e02a10932 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -148,7 +148,7 @@ int APS5_VABI sceVoiceQoSDisconnect(int32_t connectionId) { return Locked([&](QoS& qos) { return qos.connections.erase(connectionId) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; }); } -int APS5_VABI sceVoiceQoSSetConnectionAttribute() { +int APS5_VABI sceVoiceQoSSetConnectionAttribute(std::int32_t connectionId, std::int32_t attributeId, const void* value, std::int32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -180,7 +180,7 @@ int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attr return Locked([&](QoS& qos) { return GetAttribute(qos.locals, localId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, void* value, int32_t size) { (void)remoteId; (void)attributeId; (void)value; @@ -237,12 +237,12 @@ int APS5_VABI sceVoiceQoSGetStatus(int32_t connectionId, int32_t statusId, void* return 0; } -int APS5_VABI sceVoiceQoSDebugGetStatus() { +int APS5_VABI sceVoiceQoSDebugGetStatus(std::int32_t connectionId, std::int32_t statusId, void* value, std::int32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVoiceQoSInitHQ() { +int APS5_VABI sceVoiceQoSInitHQ(void* memory, std::uint32_t memorySize, std::int32_t appType) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 49d2671371..9be6bbae28 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -16,15 +16,15 @@ int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); -int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); int APS5_VABI sceVoiceQoSGetConnectionAttribute(std::int32_t, std::int32_t, void*, std::int32_t); int APS5_VABI sceVoiceQoSGetLocalEndpoint(std::int32_t, std::int32_t*); int APS5_VABI sceVoiceQoSGetRemoteEndpoint(std::int32_t, std::int32_t*); int APS5_VABI sceVoiceQoSEnableChat(); int APS5_VABI sceVoiceQoSDisableChat(); int APS5_VABI sceVoiceQoSGetStatus(std::int32_t, std::int32_t, void*, std::int32_t); -int APS5_VABI sceVoiceQoSDebugGetStatus(); -int APS5_VABI sceVoiceQoSInitHQ(); +int APS5_VABI sceVoiceQoSDebugGetStatus(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSInitHQ(void*, std::uint32_t, std::int32_t); int APS5_VABI sceVoiceQoSSetMode(); int APS5_VABI sceVoiceQoSTerminate(); int APS5_VABI sceVoiceQoSReadPacket(std::int32_t, void*, std::uint32_t*); @@ -157,8 +157,8 @@ int main(int argc, char** argv) { Require(NotImplemented([] { sceVoiceQoSEnableChat(); })); Require(NotImplemented([] { sceVoiceQoSDisableChat(); })); Require(NotImplemented([] { sceVoiceQoSGetStatus(1, 1, nullptr, 0); })); - Require(NotImplemented([] { sceVoiceQoSDebugGetStatus(); })); - Require(NotImplemented([] { sceVoiceQoSInitHQ(); })); + Require(NotImplemented([] { sceVoiceQoSDebugGetStatus(0, 200, nullptr, 4); })); + Require(NotImplemented([] { sceVoiceQoSInitHQ(nullptr, 0xc0000, 0x80000); })); Require(NotImplemented([] { sceVoiceQoSSetMode(); })); Require(NotImplemented([] { sceVoiceQoSTerminate(); })); } diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index fa6732187f..ba43885880 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -7,9 +7,8 @@ ### Unknown function info -- [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) - [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, and `GetLocalEndpointAttribute(id, attribute, value, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws -- [libSceVoiceQoS firmware exports](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `sceVoiceQoSGetRemoteEndpointAttribute`, `sceVoiceQoSGetConnectionAttribute` and `sceVoiceQoSGetStatus` follow public PS4 SDK prototypes in [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2436-L2441) and remain throwing stubs as endpoint and connection attribute storage is unverified on hardware. `sceVoiceQoSEnableChat`, `sceVoiceQoSDisableChat`, `sceVoiceQoSDebugGetStatus`, `sceVoiceQoSInitHQ`, `sceVoiceQoSSetMode`, and `sceVoiceQoSTerminate` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceVoiceQoS firmware exports](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `sceVoiceQoSGetRemoteEndpointAttribute`, `sceVoiceQoSGetConnectionAttribute` and `sceVoiceQoSGetStatus` follow public PS4 SDK prototypes in [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2436-L2441) and remain throwing stubs as endpoint and connection attribute storage is unverified on hardware. `sceVoiceQoSSetMode` and `sceVoiceQoSTerminate` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws ### Functional From b078879d07078d469ae27d744f36fb3ced957809 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:36:16 +0300 Subject: [PATCH 0508/1360] fix(np): recover session signaling query structures --- .../prx/libSceNpSessionSignaling/Export.cpp | 34 ++++++++++++++++--- docs/dev/debt/libSceNpSessionSignaling.md | 2 -- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 8c60a0db57..4266f0fcf4 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -10,6 +10,30 @@ static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast<int>(0x80552D06); static std::atomic<uint32_t> g_nextContext{1}; +struct SessionSignalingMemoryInfo { + std::uint64_t poolSize; + std::uint64_t usedSize; + std::uint64_t peakUsedSize; +}; + +struct SessionSignalingPeerAddress { + std::uint64_t accountId; + std::uint32_t platform; +}; + +struct SessionSignalingGroupInfo { + std::uint32_t connectionIds[64]; + std::uint64_t connectionCount; +}; + +static_assert(sizeof(SessionSignalingMemoryInfo) == 24); +static_assert(offsetof(SessionSignalingMemoryInfo, usedSize) == 8); +static_assert(offsetof(SessionSignalingMemoryInfo, peakUsedSize) == 16); +static_assert(sizeof(SessionSignalingPeerAddress) == 16); +static_assert(offsetof(SessionSignalingPeerAddress, platform) == 8); +static_assert(sizeof(SessionSignalingGroupInfo) == 264); +static_assert(offsetof(SessionSignalingGroupInfo, connectionCount) == 256); + extern "C" { int APS5_VABI sceNpSessionSignalingInitialize(void* param) { @@ -79,7 +103,7 @@ int APS5_VABI sceNpSessionSignalingRequestPrepare(uint32_t contextId, uint32_t* return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingGetMemoryInfo(void* memoryInfo) { +int APS5_VABI sceNpSessionSignalingGetMemoryInfo(SessionSignalingMemoryInfo* memoryInfo) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -89,22 +113,22 @@ int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(void) { return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(std::uint32_t contextId, const void* peerAddress, void* connectionId) { +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(std::uint32_t contextId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(std::uint32_t contextId, const void* sessionId, void* groupId) { +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(std::uint32_t contextId, const char* sessionId, std::uint32_t* groupId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetGroupInfo(std::uint32_t contextId, std::uint32_t groupId, void* groupInfo) { +int APS5_VABI sceNpSessionSignalingGetGroupInfo(std::uint32_t contextId, std::uint32_t groupId, SessionSignalingGroupInfo* groupInfo) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2(std::uint32_t contextId, std::uint32_t addressType, std::uint32_t address, std::uint16_t port, void* connectionId) { +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2(std::uint32_t contextId, std::uint32_t groupId, std::uint32_t address, std::uint16_t port, SessionSignalingGroupInfo* groupInfo) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index d37e80e5ca..b797d16a65 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -5,5 +5,3 @@ - [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified - [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. - -- [sceNpSessionSignalingGetMemoryInfo, sceNpSessionSignalingGetConnectionFromPeerAddress, sceNpSessionSignalingGetGroupFromSessionId, sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - argument registers and scalar widths follow the system module exports `lbXTXRG5nyM`, `dJJ0UPrrsok`, `UGcXquhVYrY`, `WtOs6cUbrx8` and `hbNcE4fnBDs`. Pointer payloads remain opaque: their layouts and the address-type values require tracing the virtual callees. All five exports remain throwing stubs. From 39cfd3e2ad73d354c8968e4f27d339ef9aba0f4c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:37:52 +0300 Subject: [PATCH 0509/1360] fix(np): recover connection statistics output layout --- core/libs/prx/libSceNpSessionSignaling/Export.cpp | 13 ++++++++++++- docs/dev/debt/libSceNpSessionSignaling.md | 1 - 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 4266f0fcf4..290ef13d06 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -21,6 +21,13 @@ struct SessionSignalingPeerAddress { std::uint32_t platform; }; +struct SessionSignalingConnectionStatistics { + std::uint32_t peakConnectionCount; + std::uint32_t connectionCount; + std::uint32_t connectingCount; + std::uint32_t connectedCount; +}; + struct SessionSignalingGroupInfo { std::uint32_t connectionIds[64]; std::uint64_t connectionCount; @@ -31,6 +38,10 @@ static_assert(offsetof(SessionSignalingMemoryInfo, usedSize) == 8); static_assert(offsetof(SessionSignalingMemoryInfo, peakUsedSize) == 16); static_assert(sizeof(SessionSignalingPeerAddress) == 16); static_assert(offsetof(SessionSignalingPeerAddress, platform) == 8); +static_assert(sizeof(SessionSignalingConnectionStatistics) == 16); +static_assert(offsetof(SessionSignalingConnectionStatistics, connectionCount) == 4); +static_assert(offsetof(SessionSignalingConnectionStatistics, connectingCount) == 8); +static_assert(offsetof(SessionSignalingConnectionStatistics, connectedCount) == 12); static_assert(sizeof(SessionSignalingGroupInfo) == 264); static_assert(offsetof(SessionSignalingGroupInfo, connectionCount) == 256); @@ -108,7 +119,7 @@ int APS5_VABI sceNpSessionSignalingGetMemoryInfo(SessionSignalingMemoryInfo* mem return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(void) { +int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(SessionSignalingConnectionStatistics* statistics) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index b797d16a65..80cb116b9a 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -2,6 +2,5 @@ ### Unknown function info -- [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified - [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 35943b3e7b5e08242e1489a8bb98ba53dc5a5851 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:39:36 +0300 Subject: [PATCH 0510/1360] fix(http): recover cookie buffer and limit signatures --- core/libs/prx/libSceHttp/Export.cpp | 16 ++++++++-------- docs/dev/debt/libSceHttp.md | 8 -------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index a8d7d7eeec..24ae6e9d30 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -712,7 +712,7 @@ int APS5_VABI sceHttpGetEpollId(void) { return 0; } -int APS5_VABI sceHttpGetCookie(void) { +int APS5_VABI sceHttpGetCookie(int contextId, const char* url, char* cookie, std::size_t* requiredSize, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -722,7 +722,7 @@ int APS5_VABI sceHttpGetCookieEnabled(int id, int* enabled) { return 0; } -int APS5_VABI sceHttpAddCookie(void) { +int APS5_VABI sceHttpAddCookie(int contextId, const char* url, const char* cookie, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -732,32 +732,32 @@ int APS5_VABI sceHttpSetCookieSendCallback(void) { return 0; } -int APS5_VABI sceHttpCookieExport(void) { +int APS5_VABI sceHttpCookieExport(int contextId, void* buffer, std::size_t size, std::size_t* requiredSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCookieImport(void) { +int APS5_VABI sceHttpCookieImport(int contextId, const void* buffer, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpSetCookieTotalMaxSize(void) { +int APS5_VABI sceHttpSetCookieTotalMaxSize(int contextId, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpSetCookieMaxSize(void) { +int APS5_VABI sceHttpSetCookieMaxSize(int contextId, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpSetCookieMaxNum(void) { +int APS5_VABI sceHttpSetCookieMaxNum(int contextId, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpSetCookieMaxNumPerDomain(void) { +int APS5_VABI sceHttpSetCookieMaxNumPerDomain(int contextId, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index e743811644..3e1bf029fa 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -32,15 +32,7 @@ - [sceHttpDbgGetRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpAddCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpCookieExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpCookieImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetCookieTotalMaxSize](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetCookieMaxSize](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetCookieMaxNum](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetCookieMaxNumPerDomain](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetCookieStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpRequestGetAllHeaders](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature From 2239c0b717a6a9311aff526fca780aaf11116e1c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:41:19 +0300 Subject: [PATCH 0511/1360] fix(http): recover request metadata and memory stats signatures --- core/libs/prx/libSceHttp/Export.cpp | 23 ++++++++++++++++++++--- docs/dev/debt/libSceHttp.md | 3 --- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 24ae6e9d30..b923302dac 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -31,6 +31,23 @@ struct RequestStatusRegistration { }; static std::mutex g_statusMutex; static std::map<int, RequestStatusRegistration> g_statusCallbacks; +struct HttpMemoryPoolStats { + std::size_t poolSize; + std::size_t peakUsedSize; + std::size_t usedSize; +}; + +struct HttpRequestHeader { + const char* name; + const char* value; +}; + +static_assert(sizeof(HttpMemoryPoolStats) == 24); +static_assert(offsetof(HttpMemoryPoolStats, peakUsedSize) == 8); +static_assert(offsetof(HttpMemoryPoolStats, usedSize) == 16); +static_assert(sizeof(HttpRequestHeader) == 16); +static_assert(offsetof(HttpRequestHeader, value) == 8); + struct HttpsCaList { void* certs; int certsNum; @@ -657,7 +674,7 @@ int APS5_VABI sceHttpDbgShowStat(void) { return 0; } -int APS5_VABI sceHttpGetMemoryPoolStats(void) { +int APS5_VABI sceHttpGetMemoryPoolStats(int contextId, HttpMemoryPoolStats* stats) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -777,7 +794,7 @@ int APS5_VABI sceHttpSetPolicyOption(int id, int policy) { return 0; } -int APS5_VABI sceHttpRequestGetAllHeaders(void) { +int APS5_VABI sceHttpRequestGetAllHeaders(int requestId, HttpRequestHeader* headers, std::uint32_t* headerCount, char* contentLength, std::size_t* contentLengthSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -803,7 +820,7 @@ int APS5_VABI sceHttpSetHttp09Enabled(int id, int enabled) { return 0; } -int APS5_VABI sceHttpGetRegisteredCtxIds(void) { +int APS5_VABI sceHttpGetRegisteredCtxIds(int contextId, int* netPoolId, int* sslContextId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 3e1bf029fa..d34df8365e 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -24,7 +24,6 @@ - [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetMemoryPoolStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature @@ -34,9 +33,7 @@ - [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetCookieStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpRequestGetAllHeaders](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetRegisteredCtxIds](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature ### Functional From 96c0eeb46b1033719ec84703bf7d3a38702080c1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:43:25 +0300 Subject: [PATCH 0512/1360] fix(keycode): recover keyboard data conversion signature --- core/libs/prx/libSceConvertKeycode/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 9b9e92908f..6d6a3f0048 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -38,7 +38,7 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint64_t a0, std::uint64_t return kErrInvalidAddress; } -int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { +int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* keyData, std::int32_t index, std::uint32_t state, std::uint32_t keyboardType, char16_t* character) { NotImplemented_nid_no_patch(__func__); return 0; } From 91c3ff3041a282e00e4264091dfec8f9ba5df674 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:45:15 +0300 Subject: [PATCH 0513/1360] fix(hmd): recover device setting and scalar query signatures --- core/libs/prx/libSceHmd/Export.cpp | 48 +++++++++++++++--------------- docs/dev/debt/libSceHmd.md | 24 --------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index ba66e13820..b1b86f3bcc 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -69,7 +69,7 @@ std::int32_t APS5_VABI sceHmdInternalMmapIsConnect() { return 0; } -int APS5_VABI sceHmdInternalGetPUVersion(void) { +int APS5_VABI sceHmdInternalGetPUVersion(std::uint32_t* version) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -79,7 +79,7 @@ int APS5_VABI sceHmdInternalEventInitialize(void) { return 0; } -int APS5_VABI sceHmdInternalSetVirtualDisplaySize(void) { +int APS5_VABI sceHmdInternalSetVirtualDisplaySize(std::uint8_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -89,7 +89,7 @@ int APS5_VABI sceHmdInternalSocialScreenSetOutput(void) { return 0; } -int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(void) { +int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(std::uint8_t* depth) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -109,12 +109,12 @@ int APS5_VABI sceHmdReprojectionUnsetCallback(void) { return 0; } -int APS5_VABI sceHmdInternalSetCrashReportCommand(void) { +int APS5_VABI sceHmdInternalSetCrashReportCommand(std::uint8_t command) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalGetDebugTextMode(void) { +int APS5_VABI sceHmdInternalGetDebugTextMode(std::uint8_t* mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -137,12 +137,12 @@ int APS5_VABI sceHmdReprojectionStartWideNear(void) { return 0; } -int APS5_VABI sceHmdInternalGetPUSerialNumber(void) { +int APS5_VABI sceHmdInternalGetPUSerialNumber(char* serialNumber) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(void) { +int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(std::uint8_t* mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -182,7 +182,7 @@ int APS5_VABI sceHmdReprojectionSetCallback(void) { return 0; } -int APS5_VABI sceHmdInternalDfuCheckAfterPvt(void) { +int APS5_VABI sceHmdInternalDfuCheckAfterPvt(std::uint8_t* afterPvt) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -202,7 +202,7 @@ int APS5_VABI sceHmdDistortionSetOutputMinColor(void) { return 0; } -int APS5_VABI sceHmdInternalSetVirtualDisplayDepth(void) { +int APS5_VABI sceHmdInternalSetVirtualDisplayDepth(std::uint8_t depth) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -268,7 +268,7 @@ int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, void* leftOffset, void* return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } -int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(void) { +int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -386,7 +386,7 @@ int APS5_VABI sceHmdReprojectionAddDisplayBuffer(void) { return 0; } -int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(void) { +int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(std::uint8_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -515,7 +515,7 @@ int APS5_VABI sceHmdInternalDfuStart(void) { return 0; } -int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(void) { +int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(std::uint8_t height) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -545,7 +545,7 @@ int APS5_VABI sceHmdInternalSeparateModeClose(void) { return 0; } -int APS5_VABI sceHmdInternalSetDebugTextMode(void) { +int APS5_VABI sceHmdInternalSetDebugTextMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -560,7 +560,7 @@ int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(void) { return 0; } -int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(void) { +int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(std::uint8_t* height) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -650,7 +650,7 @@ int APS5_VABI sceHmdInternalSetDeviceConnection(void) { return 0; } -int APS5_VABI sceHmdInternalGetVirtualDisplaySize(void) { +int APS5_VABI sceHmdInternalGetVirtualDisplaySize(std::uint8_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -694,7 +694,7 @@ int APS5_VABI sceHmdInternalCrashReportOpen(void) { return 0; } -int APS5_VABI sceHmdInternalSetDemoMode(void) { +int APS5_VABI sceHmdInternalSetDemoMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -719,12 +719,12 @@ int APS5_VABI sceHmdReprojectionClearUserEventStart(void) { return 0; } -int APS5_VABI sceHmdInternalSetUserType(void) { +int APS5_VABI sceHmdInternalSetUserType(std::uint8_t userType) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalGetDemoMode(void) { +int APS5_VABI sceHmdInternalGetDemoMode(std::uint8_t* mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -739,7 +739,7 @@ int APS5_VABI sceHmdInternalGetCrashDumpInfo(void) { return 0; } -int APS5_VABI sceHmdInternalGetTv4kCapability(void) { +int APS5_VABI sceHmdInternalGetTv4kCapability(std::uint8_t* capability) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -776,7 +776,7 @@ int APS5_VABI sceHmdInternal3dAudioClose(void) { return 0; } -int APS5_VABI sceHmdInternalSetBrightness(void) { +int APS5_VABI sceHmdInternalSetBrightness(std::uint8_t brightness) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -796,7 +796,7 @@ int APS5_VABI sceHmdInternalGetStatusReport(void) { return 0; } -int APS5_VABI sceHmdInternalGetBrightness(void) { +int APS5_VABI sceHmdInternalGetBrightness(std::uint8_t* brightness) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -831,12 +831,12 @@ int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(void) { return 0; } -int APS5_VABI sceHmdInternalSetDebugGpo(void) { +int APS5_VABI sceHmdInternalSetDebugGpo(std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(void) { +int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(std::uint8_t* enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -846,7 +846,7 @@ int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(void) { return 0; } -int APS5_VABI sceHmdInternalGetPuRevision(void) { +int APS5_VABI sceHmdInternalGetPuRevision(std::uint32_t* revision) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index ee1012aafa..ce4cc7fc2b 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -7,20 +7,13 @@ ### Unknown function info - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count -- [sceHmdInternalGetPUVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalEventInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetVirtualDisplaySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetVirtualDisplayDepth](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetEyeStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionUnsetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetCrashReportCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDebugTextMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetPUSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDebugSocialScreenMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetM2LedBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -28,11 +21,9 @@ - [sceHmdInternalAnotherScreenSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetRequiredPUPVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuCheckAfterPvt](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetInertialSensor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetVirtualDisplayDepth](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -44,7 +35,6 @@ - [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDebugSocialScreenMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuPowerStatusForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -66,7 +56,6 @@ - [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetM2LedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetIpdSettingEnableForSystemService](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetLedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioSendData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -90,16 +79,13 @@ - [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetVirtualDisplayHeight](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuSetMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetForcedCrash](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuCheckPartialUpdateAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDebugTextMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetHmuPowerControlForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetVirtualDisplayHeight](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetLedForVsh](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -117,7 +103,6 @@ - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDeviceConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetVirtualDisplaySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionUnsetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -125,34 +110,25 @@ - [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDemoMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuSend](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionClearUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetUserType](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDemoMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetTv4kCapability](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuReset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMmapGetCount](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetPortStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStop](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDebugGpo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetIpdSettingEnableForSystemService](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetPuRevision](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 0dd86fcff760af24434ac023112453b795d9223f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:46:27 +0300 Subject: [PATCH 0514/1360] fix(hmd): recover reprojection event and color signatures --- core/libs/prx/libSceHmd/Export.cpp | 8 ++++---- docs/dev/debt/libSceHmd.md | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index b1b86f3bcc..1d529744da 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -192,12 +192,12 @@ int APS5_VABI sceHmdInternalResetInertialSensor(void) { return 0; } -int APS5_VABI sceHmdReprojectionSetUserEventStart(void) { +int APS5_VABI sceHmdReprojectionSetUserEventStart(std::int64_t eventQueue, std::int32_t eventId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdDistortionSetOutputMinColor(void) { +int APS5_VABI sceHmdDistortionSetOutputMinColor(float red, float green, float blue) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -355,7 +355,7 @@ int APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { return 0; } -int APS5_VABI sceHmdReprojectionSetOutputMinColor(void) { +int APS5_VABI sceHmdReprojectionSetOutputMinColor(float red, float green, float blue) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -684,7 +684,7 @@ int APS5_VABI sceHmdReprojectionStartWithOverlay(void) { return 0; } -int APS5_VABI sceHmdReprojectionSetUserEventEnd(void) { +int APS5_VABI sceHmdReprojectionSetUserEventEnd(std::int64_t eventQueue, std::int32_t eventId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index ce4cc7fc2b..c6d6c7bb75 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -22,8 +22,6 @@ - [sceHmdInternalGetRequiredPUPVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetInertialSensor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionSetUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdDistortionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -51,7 +49,6 @@ - [sceHmdInternalGetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetPortConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetM2LedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -108,7 +105,6 @@ - [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionSetUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 419a34cadb409255ee13ae90f623acc2ba619aa9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:47:13 +0300 Subject: [PATCH 0515/1360] fix(netctl): recover callback and event stub signatures --- core/libs/prx/libSceNetCtl/Export.cpp | 28 +++++++++++++-------------- docs/dev/debt/libSceNetCtl.md | 14 -------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index c8e3080177..0170ff2ead 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -522,32 +522,32 @@ int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { return UnregisterCallback(g_callbacksV6, cid); } -int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(void) { +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(NetCtlCallback callback, void* argument, int* callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlRegisterCallbackIpcInt(void) { +int APS5_VABI sceNetCtlRegisterCallbackIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(void) { +int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(void) { +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(int callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(void) { +int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(int callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(void) { +int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(int callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -562,17 +562,17 @@ int APS5_VABI sceNetCtlCheckCallbackForLibIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlClearEventIpcInt(void) { +int APS5_VABI sceNetCtlClearEventIpcInt(int event) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlClearEventForNpToolkit(void) { +int APS5_VABI sceNetCtlClearEventForNpToolkit(int event) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlClearEventForLibIpcInt(void) { +int APS5_VABI sceNetCtlClearEventForLibIpcInt(int event) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -662,12 +662,12 @@ int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(void) { +int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(NetCtlCallback callback, void* argument, int* callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(void) { +int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(int callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -825,17 +825,17 @@ int APS5_VABI sceNetBweCheckCallbackIpcInt(void) { return 0; } -int APS5_VABI sceNetBweClearEventIpcInt(void) { +int APS5_VABI sceNetBweClearEventIpcInt(int event) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetBweRegisterCallbackIpcInt(void) { +int APS5_VABI sceNetBweRegisterCallbackIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetBweUnregisterCallbackIpcInt(void) { +int APS5_VABI sceNetBweUnregisterCallbackIpcInt(int callbackId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index f9fb92ef5c..0c6fe50b9f 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -8,17 +8,8 @@ - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware -- [sceNetCtlRegisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlRegisterCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlUnregisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlUnregisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlUnregisterCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlCheckCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlClearEventIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlClearEventForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlClearEventForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetStateIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetState2IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -36,8 +27,6 @@ - [sceNetCtlGetScanInfoBssidForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetScanInfoForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlSetErrorNotificationEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlRegisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlUnregisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown - [sceNetCtlGetResultV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetStateV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -70,9 +59,6 @@ - [sceNetCtlApAppInitWpaKeyForQa](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRestart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweCheckCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweClearEventIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweUnregisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweStartInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweFinishInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature From e5a43c86bca6b8aa67a1894f21349e0ff8d0ae4b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:47:46 +0300 Subject: [PATCH 0516/1360] fix(netctl): recover state and result query signatures --- core/libs/prx/libSceNetCtl/Export.cpp | 18 +++++++++--------- docs/dev/debt/libSceNetCtl.md | 9 --------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 0170ff2ead..1819d066c5 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -577,17 +577,17 @@ int APS5_VABI sceNetCtlClearEventForLibIpcInt(int event) { return 0; } -int APS5_VABI sceNetCtlGetResultIpcInt(void) { +int APS5_VABI sceNetCtlGetResultIpcInt(int event, int* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetStateIpcInt(void) { +int APS5_VABI sceNetCtlGetStateIpcInt(int* state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetState2IpcInt(void) { +int APS5_VABI sceNetCtlGetState2IpcInt(int* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -597,12 +597,12 @@ int APS5_VABI sceNetCtlGetInfoIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlGetEtherLinkMode(void) { +int APS5_VABI sceNetCtlGetEtherLinkMode(int* mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetWifiType(void) { +int APS5_VABI sceNetCtlGetWifiType(int* type) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -657,7 +657,7 @@ int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(void) { +int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(int enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -681,12 +681,12 @@ int APS5_VABI sceNetCtlGetResultV6(int event_type, int* error_code) { return 0; } -int APS5_VABI sceNetCtlGetResultV6IpcInt(void) { +int APS5_VABI sceNetCtlGetResultV6IpcInt(int event, int* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetStateV6IpcInt(void) { +int APS5_VABI sceNetCtlGetStateV6IpcInt(int* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -736,7 +736,7 @@ int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlGetConnectionElapsedTimeIpcInt(void) { +int APS5_VABI sceNetCtlGetConnectionElapsedTimeIpcInt(std::uint64_t* elapsedTime) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 0c6fe50b9f..1c0958c89b 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -10,12 +10,7 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware - [sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlCheckCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetStateIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetState2IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetEtherLinkMode](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetWifiType](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlConnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlConnectWithRetryIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlConnectConfIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -26,10 +21,7 @@ - [sceNetCtlGetScanInfoForSsidScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetScanInfoBssidForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetScanInfoForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlSetErrorNotificationEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown -- [sceNetCtlGetResultV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetStateV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetInfoV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetNatInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlSetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -39,7 +31,6 @@ - [sceNetCtlEnableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetBandwidthInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetNetEvConfigInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetConnectionElapsedTimeIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpClearEvent](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpRegisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature From 9ccfc42192df3c27d969ddfe4a7759086f758880 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:51:16 +0300 Subject: [PATCH 0517/1360] fix(net): recover socket and interface control signatures --- core/libs/prx/libSceNet/Export.cpp | 46 +++++++++++++++--------------- docs/dev/debt/libSceNet.md | 23 --------------- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d6a9bd0b15..d3fecf8937 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2207,7 +2207,7 @@ int APS5_VABI sceNetGetRouteInfo(void) { return 0; } -int APS5_VABI sceNetGetsockoptEx(void) { +int APS5_VABI sceNetGetsockoptEx(int socketId, int level, int option, void* value, std::uint32_t* valueSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2243,7 +2243,7 @@ int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { return 1; } -int APS5_VABI sceNetIoctl(void) { +int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2274,12 +2274,12 @@ int APS5_VABI sceNetSetDnsInfoToKernel(void) { return 0; } -int APS5_VABI sceNetSetsockoptEx(void) { +int APS5_VABI sceNetSetsockoptEx(int socketId, int level, int option, const void* value, std::uint32_t valueSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetSocketInternal(void) { +int APS5_VABI sceNetSocketInternal(const char* name, int domain, int type, int protocol) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2309,7 +2309,7 @@ int APS5_VABI sceNetSyncWait(void) { return 0; } -int APS5_VABI sceNetSysctl(void) { +int APS5_VABI sceNetSysctl(const int* name, std::uint32_t nameLength, void* oldValue, std::size_t* oldSize, const void* newValue, std::size_t newSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2384,27 +2384,27 @@ int APS5_VABI sceNetShowRoute6ForBuffer(void) { return 0; } -int APS5_VABI sceNetConfigGetIfFlags(void) { +int APS5_VABI sceNetConfigGetIfFlags(std::uint32_t interfaceId, std::uint32_t* flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigSetIfFlags(void) { +int APS5_VABI sceNetConfigSetIfFlags(std::uint32_t interfaceId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigUnsetIfFlags(void) { +int APS5_VABI sceNetConfigUnsetIfFlags(std::uint32_t interfaceId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigDelIfaddr(void) { +int APS5_VABI sceNetConfigDelIfaddr(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigDownInterface(void) { +int APS5_VABI sceNetConfigDownInterface(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2429,12 +2429,12 @@ int APS5_VABI sceNetConfigSetIfLinkLocalAddr6(void) { return 0; } -int APS5_VABI sceNetConfigUpInterface(void) { +int APS5_VABI sceNetConfigUpInterface(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigUpInterfaceWithFlags(void) { +int APS5_VABI sceNetConfigUpInterfaceWithFlags(std::uint32_t interfaceId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2464,7 +2464,7 @@ int APS5_VABI sceNetConfigGetIfaddr(void) { return 0; } -int APS5_VABI sceNetConfigSetIfmtu(void) { +int APS5_VABI sceNetConfigSetIfmtu(std::uint32_t interfaceId, int mtu) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2549,7 +2549,7 @@ int APS5_VABI sceNetDhcpStart(void) { return 0; } -int APS5_VABI sceNetDhcpStop(void) { +int APS5_VABI sceNetDhcpStop(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2609,7 +2609,7 @@ int APS5_VABI sceNetAddrConfig6Start(void) { return 0; } -int APS5_VABI sceNetAddrConfig6Stop(void) { +int APS5_VABI sceNetAddrConfig6Stop(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2634,7 +2634,7 @@ int APS5_VABI sceNetDhcpdStart(void) { return 0; } -int APS5_VABI sceNetDhcpdStop(void) { +int APS5_VABI sceNetDhcpdStop(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2644,7 +2644,7 @@ int APS5_VABI sceNetDuplicateIpStart(void) { return 0; } -int APS5_VABI sceNetDuplicateIpStop(void) { +int APS5_VABI sceNetDuplicateIpStop(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2669,7 +2669,7 @@ int APS5_VABI sceNetConfigWlanAdhocJoin(void) { return 0; } -int APS5_VABI sceNetConfigWlanAdhocLeave(void) { +int APS5_VABI sceNetConfigWlanAdhocLeave(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2709,7 +2709,7 @@ int APS5_VABI sceNetConfigWlanApStart(void) { return 0; } -int APS5_VABI sceNetConfigWlanApStop(void) { +int APS5_VABI sceNetConfigWlanApStop(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2724,7 +2724,7 @@ int APS5_VABI sceNetConfigWlanBackgroundScanStart(void) { return 0; } -int APS5_VABI sceNetConfigWlanBackgroundScanStop(void) { +int APS5_VABI sceNetConfigWlanBackgroundScanStop(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2754,7 +2754,7 @@ int APS5_VABI sceNetConfigWlanInfraGetRssiInfo(void) { return 0; } -int APS5_VABI sceNetConfigWlanInfraLeave(void) { +int APS5_VABI sceNetConfigWlanInfraLeave(int interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2774,7 +2774,7 @@ int APS5_VABI sceNetConfigWlanSetDeviceConfig(void) { return 0; } -int APS5_VABI sceNetConfigEtherGetLinkMode(void) { +int APS5_VABI sceNetConfigEtherGetLinkMode(std::uint32_t interfaceId, int* mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2784,7 +2784,7 @@ int APS5_VABI sceNetConfigEtherPostPlugInOutEvent(void) { return 0; } -int APS5_VABI sceNetConfigEtherSetLinkMode(void) { +int APS5_VABI sceNetConfigEtherSetLinkMode(std::uint32_t interfaceId, int mode) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fa35cc1780..eb9de70d66 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -43,23 +43,18 @@ - [sceNetGetNameToIndex](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` - [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. -- [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware - [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSocketInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSyncCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSyncDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSyncGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSyncSignal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSyncWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSysctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -74,23 +69,15 @@ - [sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDns6InfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRoute6ForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigGetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigUnsetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDownInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetDefaultRouteDev](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetIfLinkLocalAddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigUpInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigUpInterfaceWithFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigFlushRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigFlushRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetIfmtu](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddArp](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddArpWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddMRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -107,7 +94,6 @@ - [sceNetDhcpGetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetInfoEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDhcpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigDelIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigDelDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -119,19 +105,15 @@ - [sceNetConfigDelRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetAddrConfig6Stop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6GetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDhcpdStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDuplicateIpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocLeave](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocPspEmuClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocPspEmuSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -139,22 +121,17 @@ - [sceNetConfigWlanAdhocSetExtInfoElement](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanApStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanApStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanBackgroundScanQuery](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanBackgroundScanStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanBackgroundScanStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanDiagGetDeviceInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanDiagSetAntenna](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanDiagSetTxFixedRate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanGetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanInfraGetRssiInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanInfraLeave](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanInfraScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanScan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanSetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigEtherGetLinkMode](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigEtherPostPlugInOutEvent](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigEtherSetLinkMode](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetBandwidthControlSetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 45997cbfe91cbb60b9bcf95ff476d02964958fa6 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:54:50 +0300 Subject: [PATCH 0518/1360] fix(net): recover address and route configuration signatures --- core/libs/prx/libSceNet/Export.cpp | 44 +++++++++++++++++------------- docs/dev/debt/libSceNet.md | 19 ------------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d3fecf8937..029f0b6255 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,12 @@ // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetIpv6Address { + std::uint8_t bytes[16]; +}; + +static_assert(sizeof(NetIpv6Address) == 16); + namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; @@ -2409,17 +2415,17 @@ int APS5_VABI sceNetConfigDownInterface(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigSetDefaultRoute(void) { +int APS5_VABI sceNetConfigSetDefaultRoute(const std::uint32_t* gateway) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigSetDefaultRouteDev(void) { +int APS5_VABI sceNetConfigSetDefaultRouteDev(const std::uint32_t* gateway) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigSetIfaddr(void) { +int APS5_VABI sceNetConfigSetIfaddr(int interfaceId, const std::uint32_t* address, const std::uint32_t* broadcastAddress, const std::uint32_t* netmask) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2469,37 +2475,37 @@ int APS5_VABI sceNetConfigSetIfmtu(std::uint32_t interfaceId, int mtu) { return 0; } -int APS5_VABI sceNetConfigAddArp(void) { +int APS5_VABI sceNetConfigAddArp(const NetEtherAddr* etherAddress, const std::uint32_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigAddArpWithInterface(void) { +int APS5_VABI sceNetConfigAddArpWithInterface(int interfaceId, const NetEtherAddr* etherAddress, const std::uint32_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigAddMRoute(void) { +int APS5_VABI sceNetConfigAddMRoute(const std::uint32_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigAddRoute(void) { +int APS5_VABI sceNetConfigAddRoute(const std::uint32_t* destination, const std::uint32_t* gateway, const std::uint32_t* netmask, int hostRoute) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigAddRouteWithInterface(void) { +int APS5_VABI sceNetConfigAddRouteWithInterface(int interfaceId, const std::uint32_t* destination, const std::uint32_t* gateway, const std::uint32_t* netmask, int hostRoute) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigDelArp(void) { +int APS5_VABI sceNetConfigDelArp(const NetEtherAddr* etherAddress, const std::uint32_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigDelArpWithInterface(void) { +int APS5_VABI sceNetConfigDelArpWithInterface(int interfaceId, const NetEtherAddr* etherAddress, const std::uint32_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2514,17 +2520,17 @@ int APS5_VABI sceNetConfigDelDefaultRouteDev(void) { return 0; } -int APS5_VABI sceNetConfigDelMRoute(void) { +int APS5_VABI sceNetConfigDelMRoute(const std::uint32_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigDelRoute(void) { +int APS5_VABI sceNetConfigDelRoute(const std::uint32_t* destination, const std::uint32_t* gateway, const std::uint32_t* netmask) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigGetDefaultRoute(void) { +int APS5_VABI sceNetConfigGetDefaultRoute(std::uint32_t* gateway) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2559,7 +2565,7 @@ int APS5_VABI sceNetConfigAddIfaddr6(void) { return 0; } -int APS5_VABI sceNetConfigDelIfaddr6(void) { +int APS5_VABI sceNetConfigDelIfaddr6(int interfaceId, const NetIpv6Address* address) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2569,7 +2575,7 @@ int APS5_VABI sceNetConfigDelDefaultRoute6(void) { return 0; } -int APS5_VABI sceNetConfigSetIfaddr6(void) { +int APS5_VABI sceNetConfigSetIfaddr6(int interfaceId, const NetIpv6Address* address, std::uint8_t prefixLength) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2579,7 +2585,7 @@ int APS5_VABI sceNetConfigSetIfaddr6WithFlags(void) { return 0; } -int APS5_VABI sceNetConfigSetDefaultRoute6(void) { +int APS5_VABI sceNetConfigSetDefaultRoute6(int interfaceId, const NetIpv6Address* gateway) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2589,17 +2595,17 @@ int APS5_VABI sceNetConfigGetIfaddr6(void) { return 0; } -int APS5_VABI sceNetConfigGetDefaultRoute6(void) { +int APS5_VABI sceNetConfigGetDefaultRoute6(NetIpv6Address* gateway) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigDelRoute6(void) { +int APS5_VABI sceNetConfigDelRoute6(const NetIpv6Address* destination, std::uint8_t prefixLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigAddRoute6(void) { +int APS5_VABI sceNetConfigAddRoute6(const NetIpv6Address* destination, const NetIpv6Address* gateway, std::uint8_t prefixLength) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index eb9de70d66..5894d4eb0e 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -69,41 +69,22 @@ - [sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDns6InfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRoute6ForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetDefaultRouteDev](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetIfLinkLocalAddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigFlushRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigFlushRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddArp](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddArpWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddMRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddRouteWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelArp](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelArpWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigDelDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigDelDefaultRouteDev](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelMRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigGetDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetAutoipInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetInfoEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigDelDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetIfaddr6WithFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigGetDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6GetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 6d2aebd4698138cbd9fa89d080b594895e22f200 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:56:01 +0300 Subject: [PATCH 0519/1360] fix(json): restore the default special float format --- core/libs/prx/libSceJson2/Export.cpp | 1 + core/libs/tests/GuestJson.cpp | 2 +- docs/dev/debt/libSceJson2.md | 6 +----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index ff9da15cec..be2706ad6a 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -981,6 +981,7 @@ static_assert(sizeof(InitParameter2) == 0x28 && offsetof(InitParameter2, special void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { *self = {}; + self->specialFloatFormatType = 2; } void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index f6569900e7..9bbb99f743 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -877,7 +877,7 @@ static void InitParameters() { construct(parameter); StoredInitParameter2 stored = Stored(parameter); Require(stored.allocator == nullptr && stored.userData == nullptr && stored.fileBufferSize == 0); - Require(stored.specialFloatFormatType == 0); + Require(stored.specialFloatFormatType == 2); Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); int allocParam = 0; diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 015e476dac..3877f521fc 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -4,14 +4,10 @@ - [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown -- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown +- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - `InitParameterRtti` and RTTI allocation remain unverified. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - member of `Value` returning `bool` as a conversion operator, with no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: evaluates to `false` for `TypeNull` and boolean `false`, and `true` for boolean `true`. For all other value types it currently returns `true`; whether the console converts them, throws, or checks container emptiness is unverified - [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - signatures and behaviors have no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: `insert` returns an iterator through a result pointer before `this` as `Object::begin` does, and takes a const Pair reference. If the key exists, it updates the existing value and returns an iterator to it; otherwise it appends a deep copy of the pair and returns an iterator to the new element; whether the console updates existing keys, appends duplicates, or ignores duplicate insertion is unverified. - -### Functional - -- [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console From fc3db1540261f91aa0c2925bfee8c01ac76f65f0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:56:54 +0300 Subject: [PATCH 0520/1360] fix(json): preserve existing values on duplicate insertion --- core/libs/prx/libSceJson2/Export.cpp | 1 - core/libs/tests/GuestJson.cpp | 8 ++++++++ docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index be2706ad6a..9ca8b9d125 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1083,7 +1083,6 @@ void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair* self) { ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator* result, Object* self, const Pair* pair) { auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& p) { return *p.key.text == *pair->key.text; }); if (it != self->items->end()) { - AssignNode(NodeOf(it->value), NodeOf(pair->value)); result->it = it; return result; } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 9bbb99f743..c29cbc39b3 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -552,6 +552,14 @@ static void ObjectInsert() { Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&p->key), "score") == 0); Require(*_ZNK3sce4Json5Value10getIntegerEv(&p->value) == 100); + Require(_ZN3sce4Json5Value3setEl(&pair.value, 200) == 0); + Iterator duplicate{}; + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&duplicate, &object, &pair) == &duplicate); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&duplicate, &inserted)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 1); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&_ZNK3sce4Json6Object8iteratordeEv(&duplicate)->value) == 100); + _ZN3sce4Json6Object8iteratorD1Ev(&duplicate); + String otherKey{}; _ZN3sce4Json6StringC1EPKc(&otherKey, "other"); Value otherValue{}; diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 3877f521fc..6db5c83e5b 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -10,4 +10,4 @@ - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - member of `Value` returning `bool` as a conversion operator, with no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: evaluates to `false` for `TypeNull` and boolean `false`, and `true` for boolean `true`. For all other value types it currently returns `true`; whether the console converts them, throws, or checks container emptiness is unverified -- [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - signatures and behaviors have no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: `insert` returns an iterator through a result pointer before `this` as `Object::begin` does, and takes a const Pair reference. If the key exists, it updates the existing value and returns an iterator to it; otherwise it appends a deep copy of the pair and returns an iterator to the new element; whether the console updates existing keys, appends duplicates, or ignores duplicate insertion is unverified. +- [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - new keys are appended in insertion order; the system library uses key order. Duplicate insertion preserves the existing value. From 2b94ddfa7e43db56e8dc65239baf015ab4982ad2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:58:03 +0300 Subject: [PATCH 0521/1360] fix(net): recover ipv6 interface configuration signatures --- core/libs/prx/libSceNet/Export.cpp | 14 +++++++++++--- docs/dev/debt/libSceNet.md | 6 ------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 029f0b6255..7cddc02c41 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -52,6 +52,14 @@ struct NetIpv6Address { static_assert(sizeof(NetIpv6Address) == 16); +struct NetIpv6InterfaceAddress { + NetIpv6Address address; + std::uint8_t prefixLength; + std::uint8_t reserved[3]; +}; + +static_assert(sizeof(NetIpv6InterfaceAddress) == 20); + namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; @@ -2560,7 +2568,7 @@ int APS5_VABI sceNetDhcpStop(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigAddIfaddr6(void) { +int APS5_VABI sceNetConfigAddIfaddr6(int interfaceId, const NetIpv6Address* address, std::uint8_t prefixLength) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2580,7 +2588,7 @@ int APS5_VABI sceNetConfigSetIfaddr6(int interfaceId, const NetIpv6Address* addr return 0; } -int APS5_VABI sceNetConfigSetIfaddr6WithFlags(void) { +int APS5_VABI sceNetConfigSetIfaddr6WithFlags(int interfaceId, const NetIpv6Address* address, std::uint8_t prefixLength, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2590,7 +2598,7 @@ int APS5_VABI sceNetConfigSetDefaultRoute6(int interfaceId, const NetIpv6Address return 0; } -int APS5_VABI sceNetConfigGetIfaddr6(void) { +int APS5_VABI sceNetConfigGetIfaddr6(int interfaceId, NetIpv6InterfaceAddress* addresses, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 5894d4eb0e..93f860806d 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -75,16 +75,10 @@ - [sceNetConfigFlushRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelDefaultRouteDev](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetAutoipInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetInfoEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigDelDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetIfaddr6WithFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigGetIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6GetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 7b1451b99be7f5ea2f285c2f9253db4460fea38a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:00:02 +0300 Subject: [PATCH 0522/1360] fix(ajm): recover vorbis packet parameter layouts --- core/libs/prx/libSceAjm.native/Export.cpp | 35 +++++++++++++++++++++-- docs/dev/debt/libSceAjm.native.md | 1 - 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 2df65a236d..2a7d6a4f1f 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -12,6 +12,37 @@ static void AjmStub(const char* name) { NotImplemented_nid_no_patch(name); } +struct WVorbisHeaderPacketParameter { + std::size_t size; + std::int32_t channels; + std::uint32_t sampleRate; + std::int32_t bytesPerSecond; + std::int32_t shortBlockExponent; + std::int32_t longBlockExponent; + std::uint32_t reserved; + const void* setupPacket; + std::size_t setupPacketSize; + void* output; + std::size_t outputCapacity; + std::size_t outputSize; +}; + +struct WVorbisSetupPacketParameter { + std::size_t size; + std::int32_t channels; + std::uint32_t reserved; + const void* input; + std::size_t inputSize; + void* output; + std::size_t outputCapacity; + std::size_t outputSize; +}; + +static_assert(sizeof(WVorbisHeaderPacketParameter) == 72); +static_assert(offsetof(WVorbisHeaderPacketParameter, setupPacket) == 32); +static_assert(sizeof(WVorbisSetupPacketParameter) == 56); +static_assert(offsetof(WVorbisSetupPacketParameter, input) == 16); + extern "C" { int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const void* pcm_input, size_t pcm_input_size, void* bitstream_output, size_t bitstream_output_size, void* result) { @@ -32,12 +63,12 @@ const char* APS5_VABI sceAjmStrError(int error) { return nullptr; } -int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(void) { +int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) { +int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(WVorbisSetupPacketParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index f8982027f0..2f7798c679 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,6 @@ - [sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ajm/ajm.h#L19-L20) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L100-L104), not measured on a console; these throwing declarations only resolve imports. - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. -- [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [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 8d3314c8240b1ce17ef583cf4df72e88dbd22657 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:00:27 +0300 Subject: [PATCH 0523/1360] fix(rudp): recover remote address and event pump signatures --- core/libs/prx/libSceRudp/Export.cpp | 6 +++--- core/libs/tests/GuestRudpPump.cpp | 15 ++++++++------- docs/dev/debt/libSceRudp.md | 5 ++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 56df07b0ab..923b4e9a76 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -289,7 +289,7 @@ int APS5_VABI sceRudpGetContextStatus(int ctx_id, void* status, uint32_t size) { return 0; } -int APS5_VABI sceRudpGetRemoteInfo() { +int APS5_VABI sceRudpGetRemoteInfo(int contextId, void* address, std::uint32_t* addressSize, std::uint16_t* virtualPort) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -364,12 +364,12 @@ int APS5_VABI sceRudpNetFlush(void) { return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpProcessEvents(void) { +int APS5_VABI sceRudpProcessEvents(std::uint64_t timeout) { std::lock_guard<std::mutex> lk(g_mutex); return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpNetReceived(void) { +int APS5_VABI sceRudpNetReceived(int socketId, const void* data, std::size_t dataSize, const void* sourceAddress, std::uint32_t sourceAddressSize) { std::lock_guard<std::mutex> lk(g_mutex); return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index d9e350ae23..2cc7f5586f 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> +#include <cstddef> #include <cstdio> #include <cstdlib> #include <string> @@ -8,10 +9,10 @@ extern "C" { int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpEnd(); -int APS5_VABI sceRudpProcessEvents(); +int APS5_VABI sceRudpProcessEvents(std::uint64_t timeout); int APS5_VABI sceRudpNetFlush(); int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); -int APS5_VABI sceRudpNetReceived(); +int APS5_VABI sceRudpNetReceived(int socketId, const void* data, std::size_t dataSize, const void* sourceAddress, std::uint32_t sourceAddressSize); int APS5_VABI sceRudpEnableInternalIOThread2(std::uint32_t, std::uint32_t, std::uint64_t); } @@ -27,9 +28,9 @@ int main() { constexpr int notInitialized = static_cast<int>(0x80770001); constexpr int invalidArgument = static_cast<int>(0x80770004); std::uint16_t segmentSize = 0x5a5a; - CHECK(sceRudpProcessEvents() == notInitialized); + CHECK(sceRudpProcessEvents(0) == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); - CHECK(sceRudpNetReceived() == notInitialized); + CHECK(sceRudpNetReceived(0, nullptr, 0, nullptr, 0) == notInitialized); CHECK(sceRudpEnableInternalIOThread2(0, 0, 0) == notInitialized); CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); @@ -37,9 +38,9 @@ int main() { alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); - CHECK(sceRudpProcessEvents() == 0); + CHECK(sceRudpProcessEvents(0) == 0); CHECK(sceRudpNetFlush() == 0); - CHECK(sceRudpNetReceived() == 0); + CHECK(sceRudpNetReceived(0, nullptr, 0, nullptr, 0) == 0); CHECK(sceRudpEnableInternalIOThread2(0x4000, 0, 0x3) == 0); CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); @@ -53,6 +54,6 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); - CHECK(sceRudpNetReceived() == notInitialized); + CHECK(sceRudpNetReceived(0, nullptr, 0, nullptr, 0) == notInitialized); return 0; } diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index f8abf0989f..d6337281b5 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -2,15 +2,14 @@ ### Silent stubs -- [sceRudpProcessEvents, sceRudpNetFlush and sceRudpNetReceived](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process, send or receive; the three calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. What the console's `sceRudpNetReceived` returns is unverified. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown +- [sceRudpProcessEvents, sceRudpNetFlush and sceRudpNetReceived](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process, send or receive; the three calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. What the console's `sceRudpNetReceived` returns is unverified. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result. - [sceRudpEnableInternalIOThread2](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(uint32_t stack_size, uint32_t priority, uint64_t affinity)`, taken from the managed `SceRudp.Rudp.EnableInternalIOThread(uint stackSize, uint priority, ulong affinity)` in PPSA06888's Unity metadata; the first two match `sceRudpEnableInternalIOThread`. It returns success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as `sceRudpEnableInternalIOThread` does, without starting any thread. Whether the console reports other errors, and whether it applies the affinity, is unverified. - [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified ### Unknown function info - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. -- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written +- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. -- PPSA06888 imports with unknown signatures: [sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. The third argument is a pointer to a 4-byte buffer seeded from the byte its wrapper reads through the second argument, which does not show what the arguments mean. The managed `SceRudp.Context.GetRemoteInfo()` in the title's Unity metadata takes no parameters and returns a `SceRudp.AddrAndVport`; its native arguments are not visible there. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too - [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. The signatures, the 8-byte `SceRudpPollEvent`, the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From fbb28d6d62f68ef06b3275c7c1eb13f8892319bd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:00:28 +0300 Subject: [PATCH 0524/1360] docs(debt): remove the resolved keyboard conversion signature --- docs/dev/debt/libSceAudioOut.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index f6e6901f1f..d86f3e1808 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -4,7 +4,7 @@ - [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. -- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 import, [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), remains declared without parameters, with an unknown signature. Names from the shadPS4 aerolib NID list +- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, output)`: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) reads `(user_id, output, latency_us)`, but the known calls set just edi and esi. The validation follows KytyPS5: an output above 2 returns `0x80268001` and every user id is accepted, where before only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) were; the latency value of the three-argument reading is never received, and there is no 3D rendering stage to apply anything to From d64e3c4ea73010f4c35a2e14184e34e87cf68460 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:02:16 +0300 Subject: [PATCH 0525/1360] fix(tts): recover status and speech parameter signatures --- core/libs/prx/libSceTextToSpeech2/Export.cpp | 37 +++++++++++++++++--- core/libs/tests/GuestTextToSpeech2.cpp | 18 +++++----- docs/dev/debt/libSceTextToSpeech2.md | 3 +- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceTextToSpeech2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp index 09950022bc..375bf561f4 100644 --- a/core/libs/prx/libSceTextToSpeech2/Export.cpp +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -9,13 +9,40 @@ constexpr std::int32_t SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast<std::int32_t>(0 } +struct TextToSpeechInitParameter { + std::uint32_t version; + std::int32_t threadPriority; + std::uint64_t cpuAffinity; + std::uint8_t reserved[48]; +}; + +struct TextToSpeechOpenParameter { + std::uint32_t language; + std::uint32_t voiceType; + std::uint8_t reserved[56]; +}; + +struct TextToSpeechSpeakParameter { + const char16_t* text; + std::uint32_t length; + std::uint32_t mode; + std::uint32_t volume; + std::int32_t speed; + std::uint8_t reserved[40]; +}; + +static_assert(sizeof(TextToSpeechInitParameter) == 64); +static_assert(sizeof(TextToSpeechOpenParameter) == 64); +static_assert(sizeof(TextToSpeechSpeakParameter) == 64); +static_assert(offsetof(TextToSpeechSpeakParameter, reserved) == 24); + extern "C" { -int APS5_VABI sceTextToSpeech2GetSystemStatus() { +int APS5_VABI sceTextToSpeech2GetSystemStatus(std::int32_t* status) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { +std::int32_t APS5_VABI sceTextToSpeech2Initialize(const TextToSpeechInitParameter* param) { (void)param; return SCE_KERNEL_ERROR_EOPNOTSUPP; } @@ -28,15 +55,15 @@ int APS5_VABI sceTextToSpeech2Close() { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -int APS5_VABI sceTextToSpeech2GetSpeechStatus() { +int APS5_VABI sceTextToSpeech2GetSpeechStatus(std::int32_t* status) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -int APS5_VABI sceTextToSpeech2Open() { +int APS5_VABI sceTextToSpeech2Open(const TextToSpeechOpenParameter* parameter) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -int APS5_VABI sceTextToSpeech2Speak() { +int APS5_VABI sceTextToSpeech2Speak(const TextToSpeechSpeakParameter* parameter) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } diff --git a/core/libs/tests/GuestTextToSpeech2.cpp b/core/libs/tests/GuestTextToSpeech2.cpp index 9bac0096e0..93de9dc755 100644 --- a/core/libs/tests/GuestTextToSpeech2.cpp +++ b/core/libs/tests/GuestTextToSpeech2.cpp @@ -6,26 +6,26 @@ extern "C" { std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param); -int APS5_VABI sceTextToSpeech2Open(); +int APS5_VABI sceTextToSpeech2Open(const void* parameter); int APS5_VABI sceTextToSpeech2Cancel(); int APS5_VABI sceTextToSpeech2Close(); -int APS5_VABI sceTextToSpeech2GetSpeechStatus(); -int APS5_VABI sceTextToSpeech2Speak(); +int APS5_VABI sceTextToSpeech2GetSpeechStatus(std::int32_t* status); +int APS5_VABI sceTextToSpeech2Speak(const void* parameter); int APS5_VABI sceTextToSpeech2Terminate(); -int APS5_VABI sceTextToSpeech2GetSystemStatus(); +int APS5_VABI sceTextToSpeech2GetSystemStatus(std::int32_t* status); } int main() { constexpr std::int32_t unsupported = static_cast<std::int32_t>(0x8002002D); - const std::uint32_t param[12]{0x2000000, 0x26c}; + const std::uint32_t param[16]{0x2000000, 0x26c}; if (sceTextToSpeech2Initialize(param) != unsupported) { std::puts("TextToSpeech2: initialization did not report the unsupported operation"); std::abort(); } - if (sceTextToSpeech2Open() != unsupported || sceTextToSpeech2Cancel() != unsupported || - sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus() != unsupported || - sceTextToSpeech2Speak() != unsupported || sceTextToSpeech2Terminate() != unsupported || - sceTextToSpeech2GetSystemStatus() != unsupported) { + if (sceTextToSpeech2Open(nullptr) != unsupported || sceTextToSpeech2Cancel() != unsupported || + sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus(nullptr) != unsupported || + sceTextToSpeech2Speak(nullptr) != unsupported || sceTextToSpeech2Terminate() != unsupported || + sceTextToSpeech2GetSystemStatus(nullptr) != unsupported) { std::puts("TextToSpeech2: later calls did not report the unsupported operation"); std::abort(); } diff --git a/docs/dev/debt/libSceTextToSpeech2.md b/docs/dev/debt/libSceTextToSpeech2.md index fb8dc582dc..58ba1298f6 100644 --- a/docs/dev/debt/libSceTextToSpeech2.md +++ b/docs/dev/debt/libSceTextToSpeech2.md @@ -6,5 +6,4 @@ ### Unknown function info -- [sceTextToSpeech2Initialize](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters and return the same error -- [sceTextToSpeech2GetSystemStatus](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize` +- [sceTextToSpeech2Initialize](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero. From fe0540a4e52a31864871e246f2f35c65186c9f39 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:03:20 +0300 Subject: [PATCH 0526/1360] fix(hmd): recover eye geometry and led control signatures --- core/libs/prx/libSceHmd/Export.cpp | 30 ++++++++++++++++++++++-------- docs/dev/debt/libSceHmd.md | 7 ------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 1d529744da..493e492595 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -16,6 +16,20 @@ std::int32_t Initialize(const Hmd::InitializeParam* param) { } +struct HmdEyeDistances { + std::uint32_t left; + std::uint32_t right; +}; + +struct HmdEyeOffset { + float x; + float y; + float z; +}; + +static_assert(sizeof(HmdEyeDistances) == 8); +static_assert(sizeof(HmdEyeOffset) == 12); + extern "C" { std::int32_t APS5_VABI sceHmdInitialize(const Hmd::InitializeParam* param) { @@ -147,12 +161,12 @@ int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(std::uint8_t* mode) { return 0; } -int APS5_VABI sceHmdInternalSetM2LedBrightness(void) { +int APS5_VABI sceHmdInternalSetM2LedBrightness(std::int32_t handle, std::uint32_t ledMask, const std::uint8_t* brightness) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalSetIPD(void) { +int APS5_VABI sceHmdInternalSetIPD(std::int32_t mode, HmdEyeDistances distances) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -226,12 +240,12 @@ int APS5_VABI sceHmdReprojectionClearUserEventEnd(void) { return 0; } -int APS5_VABI sceHmdInternalGetIPD(void) { +int APS5_VABI sceHmdInternalGetIPD(HmdEyeDistances* distances) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalSetVRMode(void) { +int APS5_VABI sceHmdInternalSetVRMode(std::int32_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -261,7 +275,7 @@ int APS5_VABI sceHmdReprojectionInitializeCapture(void) { return 0; } -int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, void* leftOffset, void* rightOffset) { +int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, HmdEyeOffset* leftOffset, HmdEyeOffset* rightOffset) { (void)handle; (void)leftOffset; (void)rightOffset; @@ -302,7 +316,7 @@ int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(void) { return 0; } -int APS5_VABI sceHmdInternalGetPuBuildNumber(void) { +int APS5_VABI sceHmdInternalGetPuBuildNumber(std::uint32_t* buildNumber) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -311,7 +325,7 @@ int APS5_VABI sceHmdDistortionGetCompoundEyeCorrectionCommand(void) { return static_cast<int>(0x81110009); } -int APS5_VABI sceHmdInternalSetS3dPassMode(void) { +int APS5_VABI sceHmdInternalSetS3dPassMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -340,7 +354,7 @@ int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { return 0x100; } -int APS5_VABI sceHmdInternalGetDebugMode(void) { +int APS5_VABI sceHmdInternalGetDebugMode(std::uint8_t* mode) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index c6d6c7bb75..2e04c6b1e6 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -14,8 +14,6 @@ - [sceHmdReprojectionUnsetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetM2LedBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -26,8 +24,6 @@ - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionClearUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetVRMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportReadDataSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -39,14 +35,11 @@ - [sceHmdInternalAnotherScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCreateSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetPuBuildNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetS3dPassMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStopCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalIsCommonDlgMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMmapGetSensorCalibrationData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetPortConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 51ef9e8111e6cc1dea90e5aba2dad4fb15540b3b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:03:40 +0300 Subject: [PATCH 0527/1360] docs(netctl): close verified parameterless ipc signatures --- docs/dev/debt/libSceNetCtl.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 1c0958c89b..4f3c132d59 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -8,13 +8,8 @@ - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware -- [sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlCheckCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlConnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlConnectWithRetryIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlConnectConfIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlDisconnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetScanInfoBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetScanInfoByBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -24,11 +19,6 @@ - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown - [sceNetCtlGetInfoV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetNatInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlSetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlUnsetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlIsBandwidthManagementEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlDisableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlEnableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetBandwidthInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetNetEvConfigInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -45,15 +35,10 @@ - [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApCpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApAppInitWpaKey](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApAppInitWpaKeyForQa](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRestart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweCheckCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweStartInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweFinishInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweStartInternetConnectionTestBandwidthTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweSetInternetConnectionTestResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetWifiInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature From 5c9322b1e01f04693074105616a27972149d45c2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:06:47 +0300 Subject: [PATCH 0528/1360] fix(net): recover synchronization and event callback contracts --- core/libs/prx/libSceNet/Export.cpp | 31 ++++++++++++++++++++---------- docs/dev/debt/libSceNet.md | 10 ---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 7cddc02c41..11e606de4a 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,17 @@ // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetSyncObject { + void* mutex; + void* condition; + std::uint32_t signals; + std::uint32_t reserved; +}; + +static_assert(sizeof(NetSyncObject) == 24); + +using NetEventCallback = std::uint64_t (APS5_VABI*)(std::uint32_t interfaceId, std::uint64_t events, void* userData, void* waitData); + struct NetIpv6Address { std::uint8_t bytes[16]; }; @@ -2128,22 +2139,22 @@ int APS5_VABI sceNetDbgInit(void) { return 0; } -int APS5_VABI sceNetEventCallbackCreate(void) { +int APS5_VABI sceNetEventCallbackCreate(std::uint32_t interfaceId, NetEventCallback callback, void* userData, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetEventCallbackDestroy(void) { +int APS5_VABI sceNetEventCallbackDestroy(std::uint32_t interfaceId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetEventCallbackGetError(void) { +int APS5_VABI sceNetEventCallbackGetError(std::uint32_t interfaceId, std::int32_t* error, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetEventCallbackWaitCB(void) { +int APS5_VABI sceNetEventCallbackWaitCB(std::int32_t timeout, void* waitData, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2202,7 +2213,7 @@ int APS5_VABI sceNetGetIfnameNumList(void) { return 0; } -int APS5_VABI sceNetGetNameToIndex(void) { +int APS5_VABI sceNetGetNameToIndex(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2298,27 +2309,27 @@ int APS5_VABI sceNetSocketInternal(const char* name, int domain, int type, int p return 0; } -int APS5_VABI sceNetSyncCreate(void) { +int APS5_VABI sceNetSyncCreate(NetSyncObject* sync, std::uint32_t signals, const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetSyncDestroy(void) { +int APS5_VABI sceNetSyncDestroy(NetSyncObject* sync) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetSyncGet(void) { +NetSyncObject* APS5_VABI sceNetSyncGet(std::uint32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetSyncSignal(void) { +int APS5_VABI sceNetSyncSignal(NetSyncObject* sync, std::uint32_t signals) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetSyncWait(void) { +int APS5_VABI sceNetSyncWait(NetSyncObject* sync, std::uint32_t* timeout) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 93f860806d..8e063352ab 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -27,10 +27,6 @@ - [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetControl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDbgInit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEventCallbackCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEventCallbackDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEventCallbackGetError](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEventCallbackWaitCB](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -40,7 +36,6 @@ - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware - [sceNetGetIfnameNumList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetNameToIndex](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` @@ -50,11 +45,6 @@ - [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware - [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSyncCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSyncDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSyncGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSyncSignal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSyncWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 3812ecbc08fb71a3272de23c3326167895e1c2fa Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:06:48 +0300 Subject: [PATCH 0529/1360] fix(json): recover scalar boolean conversion semantics --- core/libs/prx/libSceJson2/Export.cpp | 14 +++++++++++--- core/libs/tests/GuestJson.cpp | 27 +++++++++++++++++++++++++++ docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 9ca8b9d125..93e460c734 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1057,9 +1057,17 @@ int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEP bool APS5_VABI _ZNK3sce4Json5ValuecvbEv(const Value* self) { const Node& n = NodeOf(*self); - if (n.type == TypeNull) return false; - if (n.type == TypeBoolean) return n.boolean; - return true; + switch (n.type) { + case TypeNull: return false; + case TypeBoolean: return n.boolean; + case TypeInteger: return n.integer != 0; + case TypeUInteger: return n.uinteger != 0; + case TypeReal: return n.real != 0.0; + case TypeString: return n.string.text && !n.string.text->empty(); + case TypeArray: + case TypeObject: return true; + default: throw std::runtime_error("sce::Json::Value: invalid value type"); + } } void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair* self, const String* key, const Value* value) { diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index c29cbc39b3..1645862b04 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -98,6 +98,7 @@ void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(Value*, const Object*); void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value*); void APS5_VABI _ZN3sce4Json5Value5clearEv(Value*); int APS5_VABI _ZN3sce4Json5Value3setEl(Value*, std::int64_t); +int APS5_VABI _ZN3sce4Json5Value3setEm(Value*, std::uint64_t); int APS5_VABI _ZN3sce4Json5Value3setEPKc(Value*, const char*); std::int32_t APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value*); const bool* APS5_VABI _ZNK3sce4Json5Value10getBooleanEv(const Value*); @@ -1190,6 +1191,32 @@ static void ValueOperatorBool() { Require(_ZNK3sce4Json5ValuecvbEv(&boolTrue)); _ZN3sce4Json5ValueD1Ev(&boolTrue); + Value scalar{}; + _ZN3sce4Json5ValueC1Ev(&scalar); + for (std::int64_t value : {std::int64_t{0}, std::int64_t{1}, std::int64_t{-1}}) { + Require(_ZN3sce4Json5Value3setEl(&scalar, value) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (value != 0)); + } + for (std::uint64_t value : {std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0xffffffffffffffff}}) { + Require(_ZN3sce4Json5Value3setEm(&scalar, value) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (value != 0)); + } + for (double value : {0.0, -0.0, 1.0, -1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) { + Require(_ZN3sce4Json5Value3setEd(&scalar, value) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (value != 0.0)); + } + for (const char* text : {"", "0", "false", " "}) { + Require(_ZN3sce4Json5Value3setEPKc(&scalar, text) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (*text != 0)); + } + _ZN3sce4Json5ValueD1Ev(&scalar); + for (std::int32_t type : {TypeArray, TypeObject}) { + Value container{}; + _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&container, type); + Require(_ZNK3sce4Json5ValuecvbEv(&container)); + _ZN3sce4Json5ValueD1Ev(&container); + } + Value cleared{}; _ZN3sce4Json5ValueC1Eb(&cleared, true); Require(_ZNK3sce4Json5ValuecvbEv(&cleared)); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 6db5c83e5b..082a4f641b 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -9,5 +9,5 @@ - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified -- [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - member of `Value` returning `bool` as a conversion operator, with no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: evaluates to `false` for `TypeNull` and boolean `false`, and `true` for boolean `true`. For all other value types it currently returns `true`; whether the console converts them, throws, or checks container emptiness is unverified +- [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. - [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - new keys are appended in insertion order; the system library uses key order. Duplicate insertion preserves the existing value. From 4a7d2251d30562f99c8adf445080476db4617336 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:11:37 +0300 Subject: [PATCH 0530/1360] fix(ime): recover candidate keyboard and panel signatures --- core/libs/prx/libSceIme/Export.cpp | 55 ++++++++++++++++++++---------- docs/dev/debt/libSceIme.md | 18 ---------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 6eaf4cbbe8..671a4833b0 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -15,6 +15,25 @@ #include "prx/libSceIme/include/ImeFilterText.hpp" #include "prx/libSceIme/include/ImeInput.hpp" +struct ImeVshContext; + +struct ImeVshCaretGeometry { + std::int32_t x; + std::int32_t y; + std::uint32_t height; + std::uint32_t index; +}; + +struct ImeVshTextGeometry { + std::int32_t x; + std::int32_t y; + std::uint32_t width; + std::uint32_t height; +}; + +static_assert(sizeof(ImeVshCaretGeometry) == 16); +static_assert(sizeof(ImeVshTextGeometry) == 16); + namespace { constexpr int ErrorBusy = static_cast<int>(0x80bc0001u); @@ -586,7 +605,7 @@ int APS5_VABI sceImeVshOpen(void) { return 0; } -int APS5_VABI sceImeKeyboardUpdate(void) { +int APS5_VABI sceImeKeyboardUpdate(EventHandler handler) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -601,7 +620,7 @@ int APS5_VABI sceImeVshUpdateContext(void) { return 0; } -int APS5_VABI sceImeVshClearPreedit(void) { +int APS5_VABI sceImeVshClearPreedit(ImeVshContext* context) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -611,47 +630,47 @@ int APS5_VABI sceImeVshNotifySetText(void) { return 0; } -int APS5_VABI sceImeVshSetCaretGeometry(void) { +int APS5_VABI sceImeVshSetCaretGeometry(ImeVshContext* context, const ImeVshCaretGeometry* geometry) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshConfirmPreedit(void) { +int APS5_VABI sceImeVshConfirmPreedit(ImeVshContext* context) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshSetCaretIndexInPreedit(void) { +int APS5_VABI sceImeVshSetCaretIndexInPreedit(ImeVshContext* context, std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshSetPreeditGeometry(void) { +int APS5_VABI sceImeVshSetPreeditGeometry(ImeVshContext* context, const ImeVshTextGeometry* geometry) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshSetSelectGeometry(void) { +int APS5_VABI sceImeVshSetSelectGeometry(ImeVshContext* context, const ImeVshTextGeometry* geometry) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshClose(void) { +int APS5_VABI sceImeVshClose(ImeVshContext* context) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshGetPanelPositionAndForm(void) { +int APS5_VABI sceImeVshGetPanelPositionAndForm(ImeVshContext* context, PositionAndForm* form) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeSetCandidateIndex(void) { +int APS5_VABI sceImeSetCandidateIndex(std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeConfirmCandidate(void) { +int APS5_VABI sceImeConfirmCandidate(std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -661,12 +680,12 @@ int APS5_VABI sceImeDisableController(void) { return 0; } -int APS5_VABI sceImeSetVoiceDictationEnabled(void) { +int APS5_VABI sceImeSetVoiceDictationEnabled(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeSetEditEventWithRemotePlayEnabled(void) { +int APS5_VABI sceImeSetEditEventWithRemotePlayEnabled(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -680,7 +699,7 @@ int APS5_VABI sceImeVshInformConfirmdString2(void) { return 0; } -int APS5_VABI sceImeVshSetSelectionText(void) { +int APS5_VABI sceImeVshSetSelectionText(ImeVshContext* context, const char16_t* text, std::uint32_t length) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -695,7 +714,7 @@ int APS5_VABI sceImeCheckRemoteEventParam(void) { return 0; } -int APS5_VABI sceImeKeyboardOpenInternal(void) { +int APS5_VABI sceImeKeyboardOpenInternal(std::int32_t userId, EventHandler handler, const KeyboardParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -745,7 +764,7 @@ int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(void) { return 0; } -int APS5_VABI sceImeVshSetPanelPosition(void) { +int APS5_VABI sceImeVshSetPanelPosition(ImeVshContext* context, float x, float y, std::int32_t horizontalAlignment, std::int32_t verticalAlignment) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -785,12 +804,12 @@ int APS5_VABI sceImeDicReplaceWord(void) { return 0; } -int APS5_VABI sceImeDicDeleteLearnDics(void) { +int APS5_VABI sceImeDicDeleteLearnDics(std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeDicDeleteUserDics(void) { +int APS5_VABI sceImeDicDeleteUserDics(std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 465e86b085..2d0e987979 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -11,29 +11,14 @@ - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdateContext](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshClearPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifySetText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetCaretGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshConfirmPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetCaretIndexInPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetPreeditGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetSelectGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeSetCandidateIndex](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeConfirmCandidate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeDisableController](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeSetVoiceDictationEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeSetEditEventWithRemotePlayEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshInformConfirmdString2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetSelectionText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeCheckUpdateTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeCheckRemoteEventParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeKeyboardOpenInternal](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshSetParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdateContext2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshSendTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature @@ -43,7 +28,6 @@ - [sceImeVshNotifyDeleteText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyUpdateCaretText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshCheckAutoReConvertingPreeditText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetPanelPosition](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyPreeditMetaInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature @@ -51,8 +35,6 @@ - [sceImeDicAddWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeDicDeleteWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeDicReplaceWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDicDeleteLearnDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDicDeleteUserDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [libSceIme pending exports](../../../core/libs/prx/libSceIme/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIme.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From cf0544a93bf52ae407fb278d80b87c03a11a52cd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:11:37 +0300 Subject: [PATCH 0531/1360] docs(json): narrow the remaining initialization debt --- docs/dev/debt/libSceJson2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 082a4f641b..12d1655e14 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -4,7 +4,7 @@ - [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown -- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - `InitParameterRtti` and RTTI allocation remain unverified. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown +- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown From 9e87228eafb5c31966958ec0946283a7d86d59af Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:14:38 +0300 Subject: [PATCH 0532/1360] docs(debt): close verified hmd and pad parameterless signatures --- docs/dev/debt/libSceHmd.md | 6 ------ docs/dev/debt/libScePad.md | 2 -- 2 files changed, 8 deletions(-) diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 2e04c6b1e6..6305a8d945 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -23,7 +23,6 @@ - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionClearUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportReadDataSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -51,7 +50,6 @@ - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStopLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportReadData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -73,7 +71,6 @@ - [sceHmdInternalDfuSetMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetForcedCrash](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuCheckPartialUpdateAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSeparateModeClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetHmuPowerControlForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -93,7 +90,6 @@ - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDeviceConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionUnsetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -102,7 +98,6 @@ - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuSend](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionClearUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuReset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -115,7 +110,6 @@ - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetPortStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStop](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index c52607d8ee..5a085d20d1 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -115,8 +115,6 @@ - [scePadIsMoveReproductionModel](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetAstonModel](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadMbusInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadMbusTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadConnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadDisconnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadDisconnectDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From 5f1d9bafa2872407c119e20c07293e78ef7de49e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:16:40 +0300 Subject: [PATCH 0533/1360] fix(pad): recover controller settings signatures --- core/libs/prx/libScePad/Export.cpp | 28 ++++++++++++++-------------- docs/dev/debt/libScePad.md | 14 -------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 362519358f..ef58f94386 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -385,17 +385,17 @@ int APS5_VABI scePadSetUserColor(void) { return 0; } -int APS5_VABI scePadDisableVibration(void) { +int APS5_VABI scePadDisableVibration(std::int32_t handle, bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetVibrationStrength(void) { +int APS5_VABI scePadSetVibrationStrength(std::int32_t handle, std::uint32_t strength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetLightBarBaseBrightness(void) { +int APS5_VABI scePadSetLightBarBaseBrightness(std::int32_t handle, std::uint32_t brightness) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -415,37 +415,37 @@ int APS5_VABI scePadGetControllerIdleCount(void) { return 0; } -int APS5_VABI scePadSetPowerSavingState(void) { +int APS5_VABI scePadSetPowerSavingState(std::int32_t handle, std::uint8_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetAutoPowerOffCount(void) { +int APS5_VABI scePadSetAutoPowerOffCount(std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadEnableUsbConnection(void) { +int APS5_VABI scePadEnableUsbConnection(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadEnableAutoDetect(void) { +int APS5_VABI scePadEnableAutoDetect(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadEnablePlayPauseEmulation(void) { +int APS5_VABI scePadEnablePlayPauseEmulation(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadDisableTrackingLedBatterySaver(void) { +int APS5_VABI scePadDisableTrackingLedBatterySaver(bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetLoginUserNumber(void) { +int APS5_VABI scePadSetLoginUserNumber(std::int32_t number) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -510,12 +510,12 @@ int APS5_VABI scePadSwitchConnection(void) { return 0; } -int APS5_VABI scePadDisableTriggerEffect(void) { +int APS5_VABI scePadDisableTriggerEffect(std::int32_t handle, bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetTriggerEffectStrength(void) { +int APS5_VABI scePadSetTriggerEffectStrength(std::int32_t handle, std::uint32_t strength) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -525,12 +525,12 @@ int APS5_VABI scePadGetTriggerEffectInternalState(void) { return 0; } -int APS5_VABI scePadSetUserNumber(void) { +int APS5_VABI scePadSetUserNumber(std::int32_t handle, std::int32_t number) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadDisableTransportButtons(void) { +int APS5_VABI scePadDisableTransportButtons(bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 5a085d20d1..c6403b4866 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -22,19 +22,9 @@ - [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetUserColor](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableVibration](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetVibrationStrength](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLightBarBaseBrightness](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLightBarBlinking](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetIdleCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetControllerIdleCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetPowerSavingState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetAutoPowerOffCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnableUsbConnection](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnableAutoDetect](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnablePlayPauseEmulation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableTrackingLedBatterySaver](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLoginUserNumber](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetCapability](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadShareOutputData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadShareOutputDataWithFunctionType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -47,11 +37,7 @@ - [scePadSetForceIntercepted](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadIsLightBarBaseBrightnessControllable](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSwitchConnection](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableTriggerEffect](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetTriggerEffectStrength](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTriggerEffectInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetUserNumber](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableTransportButtons](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetEnd](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From adb500a3c1f3e5ec92f5498fa3abfb05fbeffb2a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:17:02 +0300 Subject: [PATCH 0534/1360] fix(http): recover the cookie statistics layout --- core/libs/prx/libSceHttp/Export.cpp | 14 +++++++++++++- docs/dev/debt/libSceHttp.md | 1 - 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index b923302dac..092e6d3fb5 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -31,6 +31,18 @@ struct RequestStatusRegistration { }; static std::mutex g_statusMutex; static std::map<int, RequestStatusRegistration> g_statusCallbacks; +struct HttpCookieStats { + std::size_t usedBytes; + std::uint32_t cookieCount; + std::uint32_t reserved; + std::size_t peakUsedBytes; + std::uint32_t peakCookieCount; + std::uint32_t evictedCookieCount; +}; + +static_assert(sizeof(HttpCookieStats) == 32); +static_assert(offsetof(HttpCookieStats, peakUsedBytes) == 16); + struct HttpMemoryPoolStats { std::size_t poolSize; std::size_t peakUsedSize; @@ -779,7 +791,7 @@ int APS5_VABI sceHttpSetCookieMaxNumPerDomain(int contextId, std::uint32_t count return 0; } -int APS5_VABI sceHttpGetCookieStats(void) { +int APS5_VABI sceHttpGetCookieStats(int id, HttpCookieStats* stats) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index d34df8365e..8163e0c50f 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -32,7 +32,6 @@ - [sceHttpSetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetCookieStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature From 891d790b6ab756851ede833210d51e31a1db01d3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:17:39 +0300 Subject: [PATCH 0535/1360] fix(pad): recover idle counters and device query signatures --- core/libs/prx/libScePad/Export.cpp | 24 ++++++++++++------------ docs/dev/debt/libScePad.md | 12 ------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index ef58f94386..9ad0c2c049 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -356,11 +356,11 @@ int APS5_VABI scePadGetExtensionUnitInfo(void) { return 0; } -int APS5_VABI scePadEnableExtensionPort(void) { +int APS5_VABI scePadEnableExtensionPort(bool enabled) { return 0; } -int APS5_VABI scePadEnableSpecificDeviceClass(void) { +int APS5_VABI scePadEnableSpecificDeviceClass(std::int32_t deviceClass) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -380,7 +380,7 @@ int APS5_VABI scePadGetDataInternal(void) { return 0; } -int APS5_VABI scePadSetUserColor(void) { +int APS5_VABI scePadSetUserColor(std::int32_t handle, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -405,12 +405,12 @@ int APS5_VABI scePadSetLightBarBlinking(void) { return 0; } -int APS5_VABI scePadGetIdleCount(void) { +std::uint64_t APS5_VABI scePadGetIdleCount(std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetControllerIdleCount(void) { +int APS5_VABI scePadGetControllerIdleCount(std::int32_t handle, std::uint64_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -465,7 +465,7 @@ int APS5_VABI scePadShareOutputDataWithFunctionType(void) { return 0; } -int APS5_VABI scePadSetVibrationForce(void) { +int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -475,12 +475,12 @@ int APS5_VABI scePadGetVersionInfo(void) { return 0; } -int APS5_VABI scePadGetControllerType(void) { +int APS5_VABI scePadGetControllerType(std::int32_t handle, std::int32_t* type) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadDisableDataConversion(void) { +int APS5_VABI scePadDisableDataConversion(std::int32_t handle, bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -490,22 +490,22 @@ int APS5_VABI scePadSetButtonRemappingInfo(void) { return 0; } -int APS5_VABI scePadSetProcessPrivilegeOfButtonRemapping(void) { +int APS5_VABI scePadSetProcessPrivilegeOfButtonRemapping(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetForceIntercepted(void) { +int APS5_VABI scePadSetForceIntercepted(bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadIsLightBarBaseBrightnessControllable(void) { +int APS5_VABI scePadIsLightBarBaseBrightnessControllable(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSwitchConnection(void) { +int APS5_VABI scePadSwitchConnection(std::int32_t handle, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index c6403b4866..a1fe12fd15 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -16,27 +16,15 @@ - [scePadReadStateExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetExtensionReport](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetExtensionUnitInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnableSpecificDeviceClass](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadOpenExt2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetUserColor](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLightBarBlinking](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetIdleCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetControllerIdleCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetCapability](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadShareOutputData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadShareOutputDataWithFunctionType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableDataConversion](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetButtonRemappingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetProcessPrivilegeOfButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetForceIntercepted](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsLightBarBaseBrightnessControllable](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSwitchConnection](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTriggerEffectInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From 482c0e92d4db4312dd6d51b2965d6f3c872dbd44 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:18:48 +0300 Subject: [PATCH 0536/1360] fix(pad): recover profile and copilot control signatures --- core/libs/prx/libScePad/Export.cpp | 26 +++++++++++++------------- docs/dev/debt/libScePad.md | 13 ------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 9ad0c2c049..3332bcc0f5 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -680,57 +680,57 @@ int APS5_VABI scePadResetLeonardo(void) { return 0; } -int APS5_VABI scePadSetProfileEditMode(void) { +int APS5_VABI scePadSetProfileEditMode(std::int32_t handle, bool enabled, std::uint32_t profile) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetProfileEditMode(void) { +int APS5_VABI scePadGetProfileEditMode(std::int32_t handle, std::int32_t* mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetProfileEditModeLeonardo(void) { +int APS5_VABI scePadGetProfileEditModeLeonardo(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetSelectButtonCustomizeMode(void) { +int APS5_VABI scePadSetSelectButtonCustomizeMode(std::int32_t handle, bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetSelectButtonCustomizeMode(void) { +int APS5_VABI scePadGetSelectButtonCustomizeMode(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetToggleAllReleaseLeonardo(void) { +int APS5_VABI scePadSetToggleAllReleaseLeonardo(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadEnableControllerIndicator(void) { +int APS5_VABI scePadEnableControllerIndicator(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadEnableProfileIndicator(void) { +int APS5_VABI scePadEnableProfileIndicator(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetProfileIndicatorAnimation(void) { +int APS5_VABI scePadSetProfileIndicatorAnimation(std::int32_t handle, std::uint8_t animation) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadIsCoPilot(void) { +int APS5_VABI scePadIsCoPilot(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetIsCoPilot(void) { +int APS5_VABI scePadSetIsCoPilot(std::int32_t handle, bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -745,12 +745,12 @@ int APS5_VABI scePadGetTelemetryInfo(void) { return 0; } -int APS5_VABI scePadGetActiveTime(void) { +int APS5_VABI scePadGetActiveTime(std::int32_t handle, std::int32_t type, std::uint64_t* activeTime) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetDeviceId(void) { +int APS5_VABI scePadGetDeviceId(std::int32_t handle, std::int32_t* deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index a1fe12fd15..8bb42751b0 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -55,21 +55,8 @@ - [scePadGetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadResetLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetProfileEditMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetProfileEditMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetProfileEditModeLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetSelectButtonCustomizeMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetSelectButtonCustomizeMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetToggleAllReleaseLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnableControllerIndicator](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnableProfileIndicator](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetProfileIndicatorAnimation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsCoPilot](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetIsCoPilot](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRegisterCollaboInputMgrProcess](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetActiveTime](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetDeviceId](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadResetLightBarAll](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From b878641d266a798248df58c08d9e0762219c37d4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:19:41 +0300 Subject: [PATCH 0537/1360] fix(pad): recover tracker and connection query signatures --- core/libs/prx/libScePad/Export.cpp | 32 +++++++++++++++++++----------- docs/dev/debt/libScePad.md | 13 ------------ 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 3332bcc0f5..237a434d01 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -27,6 +27,14 @@ bool ValidPort(int userId, int type, int index) { } +struct PadMotionSensorPosition { + float x; + float y; + float z; +}; + +static_assert(sizeof(PadMotionSensorPosition) == 12); + extern "C" { int APS5_VABI scePadClose_nid_postfix(int handle) { @@ -765,7 +773,7 @@ int APS5_VABI scePadResetLightBarAll(void) { return 0; } -int APS5_VABI scePadResetLightBarAllByPortType(void) { +int APS5_VABI scePadResetLightBarAllByPortType(std::uint32_t portType) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -785,47 +793,47 @@ int APS5_VABI scePadReadBlasterForTracker(void) { return 0; } -int APS5_VABI scePadResetOrientationForTracker(void) { +int APS5_VABI scePadResetOrientationForTracker(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetMotionTimerUnit(void) { +int APS5_VABI scePadGetMotionTimerUnit(std::int32_t handle, float* unit) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetVrTrackingMode(void) { +int APS5_VABI scePadSetVrTrackingMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetMotionSensorPosition(void) { +int APS5_VABI scePadGetMotionSensorPosition(std::int32_t handle, PadMotionSensorPosition* position) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetSphereRadius(void) { +int APS5_VABI scePadGetSphereRadius(std::int32_t handle, float* radius) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadIsValidHandle(void) { +bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadIsDS4Connected(void) { +bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadIsBlasterConnected(void) { +bool APS5_VABI scePadIsBlasterConnected(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadIsMoveConnected(void) { +bool APS5_VABI scePadIsMoveConnected(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -835,7 +843,7 @@ int APS5_VABI scePadGetBluetoothAddress(void) { return 0; } -int APS5_VABI scePadIsMoveReproductionModel(void) { +int APS5_VABI scePadIsMoveReproductionModel(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -845,7 +853,7 @@ int APS5_VABI scePadReadAstonForTracker(void) { return 0; } -int APS5_VABI scePadGetAstonModel(void) { +int APS5_VABI scePadGetAstonModel(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 8bb42751b0..d39050b7cb 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -58,24 +58,11 @@ - [scePadRegisterCollaboInputMgrProcess](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadResetLightBarAll](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLightBarForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadBlasterForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadResetOrientationForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetMotionTimerUnit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetVrTrackingMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetMotionSensorPosition](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetSphereRadius](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsDS4Connected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsBlasterConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetBluetoothAddress](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadIsMoveReproductionModel](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetAstonModel](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadConnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadDisconnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadDisconnectDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From ae33ab4bb1b2be17e4e4c2f73410a3fb842a9897 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:20:31 +0300 Subject: [PATCH 0538/1360] fix(pad): recover feature report and compatibility signatures --- core/libs/prx/libScePad/Export.cpp | 20 ++++++++++---------- docs/dev/debt/libScePad.md | 12 ------------ 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 237a434d01..12193b5057 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -873,12 +873,12 @@ int APS5_VABI scePadConnectPort(void) { return 0; } -int APS5_VABI scePadDisconnectPort(void) { +int APS5_VABI scePadDisconnectPort(std::int32_t userId, std::int32_t portType) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadDisconnectDevice(void) { +int APS5_VABI scePadDisconnectDevice(std::int32_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -903,7 +903,7 @@ int APS5_VABI scePadVirtualDeviceAddDevice(void) { return 0; } -int APS5_VABI scePadVirtualDeviceDeleteDevice(void) { +int APS5_VABI scePadVirtualDeviceDeleteDevice(std::int32_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -928,17 +928,17 @@ int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(void) { return 0; } -int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(void) { +int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetShareButtonMaskForRemotePlay(void) { +int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetPS4BcVibrationMode(void) { +int APS5_VABI scePadSetPS4BcVibrationMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -948,7 +948,7 @@ int APS5_VABI scePadSetVirtualDeviceComponentInformation(void) { return 0; } -int APS5_VABI scePadEnablePs4CompatibleMode(void) { +int APS5_VABI scePadEnablePs4CompatibleMode(std::int32_t handle, bool enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -983,17 +983,17 @@ int APS5_VABI scePadRemoteControlInsertDataForAston(void) { return 0; } -int APS5_VABI scePadGetFeatureReport2(void) { +int APS5_VABI scePadGetFeatureReport2(std::int32_t handle, std::uint8_t reportId, void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetFeatureReport2(void) { +int APS5_VABI scePadSetFeatureReport2(std::int32_t handle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetMuteLed(void) { +int APS5_VABI scePadSetMuteLed(std::int32_t handle, std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index d39050b7cb..9cadbed787 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -64,31 +64,19 @@ - [scePadGetBluetoothAddress](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadConnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisconnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisconnectDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDeviceInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProcessFocus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceDeleteDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceInsertTriggerInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceSetTriggerEffectSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceDisableButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetShareButtonMaskForRemotePlay](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetPS4BcVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetVirtualDeviceComponentInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadEnablePs4CompatibleMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetLicenseControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadHistory](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadStartRecording](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadStopRecording](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertDataForAston](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetFeatureReport2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetFeatureReport2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetMuteLed](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadBondUpdateInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadBondUpdateIsReady](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadBondUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From 7c741db5efc918a0a74ee3ef69ba1524721dfabb Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:24:24 +0300 Subject: [PATCH 0539/1360] fix(pad): recover controller update status and data signatures --- core/libs/prx/libScePad/Export.cpp | 28 ++++++++++++++++++---------- docs/dev/debt/libScePad.md | 14 -------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 12193b5057..7a4390c091 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -27,6 +27,14 @@ bool ValidPort(int userId, int type, int index) { } +struct PadUpdateData { + std::size_t size; + std::uint8_t data[32768]; +}; + +static_assert(sizeof(PadUpdateData) == 32776); +static_assert(offsetof(PadUpdateData, data) == 8); + struct PadMotionSensorPosition { float x; float y; @@ -1003,7 +1011,7 @@ int APS5_VABI scePadBondUpdateInit(void) { return 0; } -int APS5_VABI scePadBondUpdateIsReady(void) { +int APS5_VABI scePadBondUpdateIsReady(std::int32_t deviceId, std::int32_t* ready) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1013,22 +1021,22 @@ int APS5_VABI scePadBondUpdateStart(void) { return 0; } -int APS5_VABI scePadBondUpdateGetStatus(void) { +int APS5_VABI scePadBondUpdateGetStatus(std::int32_t deviceId, std::int32_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadBondUpdateSendData(void) { +int APS5_VABI scePadBondUpdateSendData(std::int32_t deviceId, const PadUpdateData* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadBondUpdateResetBond(void) { +int APS5_VABI scePadBondUpdateResetBond(std::int32_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadBondUpdateGetReconnectStatus(void) { +int APS5_VABI scePadBondUpdateGetReconnectStatus(std::int32_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1048,7 +1056,7 @@ int APS5_VABI scePadControllerUpdateIsUpdateNeeded2(void) { return 0; } -int APS5_VABI scePadControllerUpdateIsReady(void) { +int APS5_VABI scePadControllerUpdateIsReady(std::int32_t deviceId, std::int32_t* ready) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1058,22 +1066,22 @@ int APS5_VABI scePadControllerUpdateStart(void) { return 0; } -int APS5_VABI scePadControllerUpdateGetStatus(void) { +int APS5_VABI scePadControllerUpdateGetStatus(std::int32_t deviceId, std::int32_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadControllerUpdateSendData(void) { +int APS5_VABI scePadControllerUpdateSendData(std::int32_t deviceId, const PadUpdateData* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadControllerUpdateResetController(void) { +int APS5_VABI scePadControllerUpdateResetController(std::int32_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadControllerUpdateGetReconnectStatus(void) { +int APS5_VABI scePadControllerUpdateGetReconnectStatus(std::int32_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 9cadbed787..7040a5d940 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -77,23 +77,9 @@ - [scePadReadHistory](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertDataForAston](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateIsReady](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadBondUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateGetStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateSendData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateResetBond](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateIsUpdateNeeded2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateIsReady](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateGetStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateSendData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateResetController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature ### Functional From fa38f4e2afdb5345e31ed22e691fbac6f8abf937 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:25:55 +0300 Subject: [PATCH 0540/1360] fix(pad): recover profile selection and infrared status signatures --- core/libs/prx/libScePad/Export.cpp | 16 ++++++++-------- docs/dev/debt/libScePad.md | 9 --------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 7a4390c091..f8565dd031 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -561,12 +561,12 @@ int APS5_VABI scePadSetLotusIrCodeSetData(void) { return 0; } -int APS5_VABI scePadSetLotusIrCodeSetEnd(void) { +int APS5_VABI scePadSetLotusIrCodeSetEnd(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetLotusIrCodeSetResult(void) { +int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* result, std::uint8_t* deviceResult) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -596,7 +596,7 @@ int APS5_VABI scePadUiReadStateForTuning(void) { return 0; } -int APS5_VABI scePadSetStickTriggerTuningMode(void) { +int APS5_VABI scePadSetStickTriggerTuningMode(std::int32_t handle, std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -636,17 +636,17 @@ int APS5_VABI scePadGetActiveProfileInfo(void) { return 0; } -int APS5_VABI scePadDisableActiveProfileChange(void) { +int APS5_VABI scePadDisableActiveProfileChange(std::int32_t handle, bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadDisableActiveProfileChangeByDeviceId(void) { +int APS5_VABI scePadDisableActiveProfileChangeByDeviceId(std::int32_t deviceId, bool disabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetActiveProfileSlot(void) { +int APS5_VABI scePadSetActiveProfileSlot(std::int32_t deviceId, std::uint32_t slot) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -671,12 +671,12 @@ int APS5_VABI scePadParseUniqueDataLeonardo(void) { return 0; } -int APS5_VABI scePadGetControllerTypeLeonardo(void) { +int APS5_VABI scePadGetControllerTypeLeonardo(std::int32_t deviceId, std::int32_t* controllerType) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetControllerTypeLeonardo(void) { +int APS5_VABI scePadSetControllerTypeLeonardo(std::int32_t deviceId, std::int32_t controllerType) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 7040a5d940..661d3608d2 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -28,14 +28,11 @@ - [scePadGetTriggerEffectInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusIrCodeSetData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLotusIrCodeSetEnd](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetLotusIrCodeSetResult](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusEnableIrKey](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetUsbDetectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadUiReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadUiReadStateForTuning](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetStickTriggerTuningMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetStickTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetTriggerTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -43,19 +40,13 @@ - [scePadResetHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetProfileInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetActiveProfileInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableActiveProfileChange](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDisableActiveProfileChangeByDeviceId](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetActiveProfileSlot](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetSavedSettingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetSavedSettingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVrControllerGetPhysicalUsbConnectionStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadParseUniqueDataLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetControllerTypeLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetControllerTypeLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadResetLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadRegisterCollaboInputMgrProcess](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLightBarForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From 4e067caa65605740a961908cd2e8a3377ab62a03 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:26:54 +0300 Subject: [PATCH 0541/1360] fix(pad): recover infrared transfer signatures --- core/libs/prx/libScePad/Export.cpp | 8 ++++---- docs/dev/debt/libScePad.md | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index f8565dd031..3411748f9d 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -551,12 +551,12 @@ int APS5_VABI scePadDisableTransportButtons(bool disabled) { return 0; } -int APS5_VABI scePadSetLotusIrCodeSetStart(void) { +int APS5_VABI scePadSetLotusIrCodeSetStart(std::int32_t handle, std::uint8_t codeSet, std::uint16_t code, std::size_t dataSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetLotusIrCodeSetData(void) { +int APS5_VABI scePadSetLotusIrCodeSetData(std::int32_t handle, std::uint8_t packetIndex, const void* data, std::size_t dataSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -571,12 +571,12 @@ int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* r return 0; } -int APS5_VABI scePadSetLotusEnableIrKey(void) { +int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetLotusDisableIrKey(void) { +int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 661d3608d2..3f570f61dd 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -26,10 +26,6 @@ - [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetButtonRemappingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTriggerEffectInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLotusIrCodeSetStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLotusIrCodeSetData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLotusEnableIrKey](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetUsbDetectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadUiReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadUiReadStateForTuning](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From d25257b90becd72f545d22a0694bdca6f201a798 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:28:04 +0300 Subject: [PATCH 0542/1360] fix(pad): recover capability and tracker color signatures --- core/libs/prx/libScePad/Export.cpp | 6 +++--- docs/dev/debt/libScePad.md | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 3411748f9d..61298f177b 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -466,7 +466,7 @@ int APS5_VABI scePadSetLoginUserNumber(std::int32_t number) { return 0; } -int APS5_VABI scePadGetCapability(void) { +int APS5_VABI scePadGetCapability(std::int32_t deviceId, std::uint32_t* capability) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -786,7 +786,7 @@ int APS5_VABI scePadResetLightBarAllByPortType(std::uint32_t portType) { return 0; } -int APS5_VABI scePadSetLightBarForTracker(void) { +int APS5_VABI scePadSetLightBarForTracker(std::int32_t handle, const PadLightBarParam* color) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -846,7 +846,7 @@ bool APS5_VABI scePadIsMoveConnected(std::int32_t handle) { return 0; } -int APS5_VABI scePadGetBluetoothAddress(void) { +int APS5_VABI scePadGetBluetoothAddress(std::int32_t handle, std::uint64_t* address) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 3f570f61dd..c4912cb4fd 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -20,7 +20,6 @@ - [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLightBarBlinking](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetCapability](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadShareOutputData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadShareOutputDataWithFunctionType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -45,10 +44,8 @@ - [scePadResetLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLightBarForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadBlasterForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetBluetoothAddress](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadConnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDeviceInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From a1818c1ed0aec0419f9ade16a93e97bd32d38925 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:30:22 +0300 Subject: [PATCH 0543/1360] fix(json): preserve system object key ordering --- core/libs/prx/libSceJson2/Export.cpp | 38 ++++++++++++-------- core/libs/tests/GuestJson.cpp | 52 +++++++++++++++++++++++++--- docs/dev/debt/libSceJson2.md | 1 - 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 93e460c734..e4d7e67624 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -333,6 +333,10 @@ void Serialize(std::string& out, const Node& n) { throw std::runtime_error("sce::Json: invalid value type"); } +std::list<Pair>::iterator ObjectPosition(const Object& object, const std::string& key) { + return std::find_if(object.items->begin(), object.items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key.c_str()) >= 0; }); +} + class Parser { public: Parser(const char* text, std::size_t size, const Handlers& handlers) : _text(text), _end(text + size), _handlers(handlers) {} @@ -502,11 +506,10 @@ private: if (_text >= _end || *_text != ':') return false; ++_text; skip(); - Pair pair{}; + auto& pair = *out.object.items->emplace(ObjectPosition(out.object, key)); Construct(pair.key, key); Construct(pair.value); pair.value.node->handlers = _handlers; - out.object.items->push_back(pair); if (!value(*pair.value.node)) return false; skip(); if (_text < _end && *_text == ',') { ++_text; continue; } @@ -532,13 +535,18 @@ void* const JsonMemAllocatorVtable[] = { }; Value& ObjectEntry(Object& object, const std::string& key) { - for (auto& pair : *object.items) - if (*pair.key.text == key) return pair.value; + auto position = ObjectPosition(object, key); + if (position != object.items->end() && std::strcmp(position->key.text->c_str(), key.c_str()) == 0) return position->value; Pair pair{}; Construct(pair.key, key); - Construct(pair.value); - object.items->push_back(pair); - return object.items->back().value; + try { + Construct(pair.value); + return object.items->insert(position, pair)->value; + } catch (...) { + Destroy(pair.value); + Destroy(pair.key); + throw; + } } } @@ -718,7 +726,7 @@ ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return &*self->it; } ObjectIterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(ObjectIterator* result, const Object* self, const String* key) { - result->it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); + result->it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0; }); return result; } bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } @@ -732,7 +740,7 @@ bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->i std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } void APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object* self, const String* key) { if (key == nullptr || key->text == nullptr) throw std::invalid_argument("sce::Json::Object::erase: null key"); - auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); + auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0; }); if (it == self->items->end()) return; Destroy(it->key); Destroy(it->value); @@ -857,7 +865,7 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEPKc(const Value* self, const char* const Node& n = NodeOf(*self); if (key != nullptr && n.type == TypeObject) for (const auto& pair : *n.object.items) - if (*pair.key.text == key) return &pair.value; + if (std::strcmp(pair.key.text->c_str(), key) == 0) return &pair.value; return &MissingElement(self); } const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t index) { @@ -877,7 +885,7 @@ const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value* 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; + if (std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0) return &pair.value; return &MissingElement(self); } Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value* self, std::size_t index) { @@ -891,7 +899,7 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value* self, cons Node& n = NodeOf(*self); if (key == nullptr || n.type != TypeObject) return nullptr; for (auto& pair : *n.object.items) - if (*pair.key.text == *key->text) return &pair.value; + if (std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0) return &pair.value; return nullptr; } Array* APS5_VABI _ZN3sce4Json5Value10referArrayEv(Value* self) { @@ -1089,8 +1097,8 @@ void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair* self) { } ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator* result, Object* self, const Pair* pair) { - auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& p) { return *p.key.text == *pair->key.text; }); - if (it != self->items->end()) { + auto it = ObjectPosition(*self, *pair->key.text); + if (it != self->items->end() && std::strcmp(it->key.text->c_str(), pair->key.text->c_str()) == 0) { result->it = it; return result; } @@ -1099,7 +1107,7 @@ ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator Construct(copy.key, *pair->key.text); try { CopyInto(copy.value, pair->value); - result->it = self->items->insert(self->items->end(), copy); + result->it = self->items->insert(it, copy); return result; } catch (...) { Destroy(copy.value); diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 1645862b04..f18fbcdec8 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -232,8 +232,8 @@ static void ParseAndRoundTrip() { Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5ValueixEm(_ZNK3sce4Json5ValueixEm(&list, 2), 0)) == 3); const std::string serialized = Serialize(root); - Require(serialized == "{\"name\":\"x\xc3\xa9\\n\",\"count\":3,\"big\":18446744073709551615,\"neg\":-5," - "\"pi\":1.5,\"ok\":true,\"none\":null,\"list\":[1,\"two\",[3]]}"); + Require(serialized == "{\"big\":18446744073709551615,\"count\":3,\"list\":[1,\"two\",[3]]," + "\"name\":\"x\xc3\xa9\\n\",\"neg\":-5,\"none\":null,\"ok\":true,\"pi\":1.5}"); Value reparsed{}; _ZN3sce4Json5ValueC1Ev(&reparsed); Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&reparsed, serialized.c_str(), serialized.size()) == 0); @@ -571,6 +571,11 @@ static void ObjectInsert() { Iterator otherInserted{}; Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&otherInserted, &object, &otherPair) == &otherInserted); Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + Iterator first{}; + _ZNK3sce4Json6Object5beginEv(&first, &object); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&first, &otherInserted)); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&_ZNK3sce4Json6Object8iteratordeEv(&inserted)->value) == 100); + _ZN3sce4Json6Object8iteratorD1Ev(&first); _ZN3sce4Json6Object8iteratorD1Ev(&otherInserted); _ZN3sce4Json6Object4PairD1Ev(&otherPair); @@ -584,6 +589,37 @@ static void ObjectInsert() { _ZN3sce4Json6ObjectD1Ev(&object); } +static void ObjectKeyOrder() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + for (const char* name : {"z", "\xc3\xa9", "a", "A", ""}) { + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, name); + _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &key); + _ZN3sce4Json6StringD1Ev(&key); + } + Iterator member{}; + _ZNK3sce4Json6Object5beginEv(&member, &object); + for (const char* expected : {"", "A", "a", "z", "\xc3\xa9"}) { + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&member)->key), expected) == 0); + _ZN3sce4Json6Object8iteratorppEv(&member); + } + _ZN3sce4Json6Object8iteratorD1Ev(&member); + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "a"); + Value* existing = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &key); + _ZN3sce4Json6String6appendEPKcm(&key, "\0suffix", 7); + Require(_ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &key) == existing); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&member, &object, &key); + Require(&_ZNK3sce4Json6Object8iteratordeEv(&member)->value == existing); + _ZN3sce4Json6Object8iteratorD1Ev(&member); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 5); + _ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &key); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 4); + _ZN3sce4Json6StringD1Ev(&key); + _ZN3sce4Json6ObjectD1Ev(&object); +} + static void StringMethods() { Require(_ZN3sce4Json6String4nposE == static_cast<std::size_t>(-1)); const auto text = [](const String& string) { return std::string(_ZNK3sce4Json6String5c_strEv(&string), _ZNK3sce4Json6String6lengthEv(&string)); }; @@ -689,11 +725,11 @@ static void ObjectOperations() { _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p2, &k2, &v2); _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p3, &k3, &v3); Iterator ins{}; - _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p1); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p3); _ZN3sce4Json6Object8iteratorD1Ev(&ins); _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p2); _ZN3sce4Json6Object8iteratorD1Ev(&ins); - _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p3); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p1); _ZN3sce4Json6Object8iteratorD1Ev(&ins); Iterator begin{}; @@ -779,6 +815,13 @@ static void ValueAccess() { 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); + Iterator member{}; + _ZNK3sce4Json6Object5beginEv(&member, _ZNK3sce4Json5Value9getObjectEv(&root)); + for (const char* expected : {"flag", "list", "name"}) { + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&member)->key), expected) == 0); + _ZN3sce4Json6Object8iteratorppEv(&member); + } + _ZN3sce4Json6Object8iteratorD1Ev(&member); String key{}; _ZN3sce4Json6StringC1EPKc(&key, "list"); @@ -1232,6 +1275,7 @@ int main() { ObjectFind(); ObjectPair(); ObjectInsert(); + ObjectKeyOrder(); ArrayIteratorCopies(); ObjectIteratorCopies(); AllocatorNotifyError(); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 12d1655e14..86b6444e91 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -10,4 +10,3 @@ - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. -- [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - new keys are appended in insertion order; the system library uses key order. Duplicate insertion preserves the existing value. From 787318c3d86c514b480fbaca607849038fe24e3e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:32:15 +0300 Subject: [PATCH 0544/1360] fix(pad): recover vr vibration and update query signatures --- core/libs/prx/libScePad/Export.cpp | 8 ++++---- docs/dev/debt/libScePad.md | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 61298f177b..c97a1e3c23 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -53,12 +53,12 @@ int APS5_VABI scePadClose_nid_postfix(int handle) { return PAD_OK; } -int APS5_VABI scePadVrControllerSetVibration() { +int APS5_VABI scePadVrControllerSetVibration(std::int32_t handle, std::uint8_t intensity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerSetVibrationMode() { +int APS5_VABI scePadVrControllerSetVibrationMode(std::int32_t handle, std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -876,7 +876,7 @@ int APS5_VABI scePadMbusTerm(void) { return 0; } -int APS5_VABI scePadConnectPort(void) { +int APS5_VABI scePadConnectPort(std::int32_t userId, std::int32_t portType) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1051,7 +1051,7 @@ int APS5_VABI scePadControllerUpdateInit(void) { return 0; } -int APS5_VABI scePadControllerUpdateIsUpdateNeeded2(void) { +int APS5_VABI scePadControllerUpdateIsUpdateNeeded2(std::int32_t deviceId, const std::uint32_t* version, std::uint16_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index c4912cb4fd..738e21a092 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -6,7 +6,7 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown -- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled @@ -47,7 +47,6 @@ - [scePadReadForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadBlasterForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadConnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDeviceInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProcessFocus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -62,7 +61,6 @@ - [scePadRemoteControlInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertDataForAston](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadBondUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateIsUpdateNeeded2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature ### Functional From 1d9ba432df0e80f873e187cbdeae48ff6107ca93 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:34:15 +0300 Subject: [PATCH 0545/1360] fix(net): recover diagnostic buffer and pool signatures --- core/libs/prx/libSceNet/Export.cpp | 20 ++++++++++---------- docs/dev/debt/libSceNet.md | 11 ----------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 11e606de4a..a6c3965f80 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2056,7 +2056,7 @@ int APS5_VABI sceNetSetDnsInfo(void) { return 0; } -int APS5_VABI sceNetShowIfconfigWithMemory(void) { +int APS5_VABI sceNetShowIfconfigWithMemory(std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2066,7 +2066,7 @@ int APS5_VABI sceNetShowPolicy(void) { return 0; } -int APS5_VABI sceNetShowPolicyWithMemory(void) { +int APS5_VABI sceNetShowPolicyWithMemory(std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2075,7 +2075,7 @@ int APS5_VABI sceNetShowRoute(void) { return 0; } -int APS5_VABI sceNetShowRouteWithMemory(void) { +int APS5_VABI sceNetShowRouteWithMemory(std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2114,7 +2114,7 @@ int APS5_VABI sceNetShowRoute6(void) { return 0; } -int APS5_VABI sceNetShowRoute6WithMemory(void) { +int APS5_VABI sceNetShowRoute6WithMemory(std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2359,27 +2359,27 @@ int APS5_VABI sceNetUsleep(int microseconds) { return sceKernelUsleep_nid_postfix(static_cast<KernelUseconds>(microseconds)); } -int APS5_VABI sceNetShowIfconfigForBuffer(void) { +int APS5_VABI sceNetShowIfconfigForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetShowRouteForBuffer(void) { +int APS5_VABI sceNetShowRouteForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetShowNetstatForBuffer(void) { +int APS5_VABI sceNetShowNetstatForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetShowNetstatEx(void) { +int APS5_VABI sceNetShowNetstatEx(std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetShowNetstatExForBuffer(void) { +int APS5_VABI sceNetShowNetstatExForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2404,7 +2404,7 @@ int APS5_VABI sceNetSetDns6InfoToKernel(void) { return 0; } -int APS5_VABI sceNetShowRoute6ForBuffer(void) { +int APS5_VABI sceNetShowRoute6ForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 8e063352ab..88add20018 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -12,17 +12,12 @@ - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowIfconfigWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowPolicy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowPolicyWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowRouteWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnect](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowRoute6WithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetControl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -49,16 +44,10 @@ - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware -- [sceNetShowIfconfigForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowRouteForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowNetstatForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowNetstatEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowNetstatExForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetNtopWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDns6InfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetShowRoute6ForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetIfLinkLocalAddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigFlushRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 9a09fc0a78948157f68291ae994a7885b6fa2172 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:35:11 +0300 Subject: [PATCH 0546/1360] fix(net): restore dns cache argument validation --- core/libs/prx/libSceNet/Export.cpp | 5 ++++- core/libs/tests/GuestSceNet.cpp | 11 +++++++---- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index a6c3965f80..1719fc4787 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -100,6 +100,7 @@ constexpr int NET_EMSGSIZE = 40; constexpr int NET_ETIMEDOUT = 60; constexpr int NET_ECONNREFUSED = 61; constexpr int NET_EHOSTUNREACH = 65; +constexpr int NET_ENOTINIT = 200; constexpr int NET_ERROR_BASE = static_cast<int>(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast<int>(0x804101E1u); constexpr int NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION = 0x1; @@ -2037,7 +2038,9 @@ extern const std::uint8_t in6addr_any_nid_postfix[16] = {}; extern const std::uint8_t in6addr_loopback_nid_postfix[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; int APS5_VABI sceNetClearDnsCache(int flags) { - if (flags != 0) throw std::runtime_error("sceNetClearDnsCache: flags " + std::to_string(flags) + " are not supported"); + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0) return fail(NET_EINVAL); return 0; } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3f00cfe6d4..52b2c027b0 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1439,6 +1439,10 @@ int main() { Require(in6addr_any_nid_postfix[i] == 0); Require(in6addr_loopback_nid_postfix[i] == (i == 15 ? 1 : 0)); } + for (const int flags : {0, 1, -1}) { + Require(sceNetClearDnsCache(flags) == static_cast<int>(0x804101C8)); + Require(*sceNetErrnoLoc() == 200); + } Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); CheckSocketHostErrors(); @@ -1823,10 +1827,9 @@ int main() { ipv4.fill(0xA5); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0 && ipv4[0] == 127); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); - for (const int flags : {0x1, -1}) { - bool threw = false; - try { sceNetClearDnsCache(flags); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + for (const int flags : {1, 2, 0x10000, -1}) { + Require(sceNetClearDnsCache(flags) == static_cast<int>(0x80410116)); + Require(*sceNetErrnoLoc() == 22); } Require(sceNetResolverDestroy(resolver) == 0); resolver_error = -1; diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 88add20018..6d37567f4c 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -8,7 +8,7 @@ - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator -- [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) returns 0 without doing anything: every `sceNetResolverStart*` lookup goes to the host resolver, so the library keeps no DNS cache to drop, and the host's own cache is left alone. The `(int flags)` signature and its meaning come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L928-L930); nonzero flags throw because their meaning is unknown +- [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 1ef8e7beab4359f6b932ada4330352af1a4fbbde Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:36:07 +0300 Subject: [PATCH 0547/1360] fix(net): recover ipv4 and ipv6 dns parameter layouts --- core/libs/prx/libSceNet/Export.cpp | 25 +++++++++++++++++++------ docs/dev/debt/libSceNet.md | 5 ----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 1719fc4787..93e8f30629 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -63,6 +63,19 @@ struct NetIpv6Address { static_assert(sizeof(NetIpv6Address) == 16); +struct NetDnsInfo { + std::uint32_t primaryAddress; + std::uint32_t secondaryAddress; +}; + +struct NetDns6Info { + NetIpv6Address primaryAddress; + NetIpv6Address secondaryAddress; +}; + +static_assert(sizeof(NetDnsInfo) == 8); +static_assert(sizeof(NetDns6Info) == 32); + struct NetIpv6InterfaceAddress { NetIpv6Address address; std::uint8_t prefixLength; @@ -2054,7 +2067,7 @@ int APS5_VABI sceNetGetInterfaceStats(void) { return 0; } -int APS5_VABI sceNetSetDnsInfo(void) { +int APS5_VABI sceNetSetDnsInfo(const NetDnsInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2108,7 +2121,7 @@ int APS5_VABI sceNetGetSockInfo6(void) { return 0; } -int APS5_VABI sceNetSetDns6Info(void) { +int APS5_VABI sceNetSetDns6Info(const NetDns6Info* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2177,7 +2190,7 @@ int APS5_VABI sceNetGetAppMemInfo(void) { return 0; } -int APS5_VABI sceNetGetDnsInfo(std::uint32_t* info, int flags) { +int APS5_VABI sceNetGetDnsInfo(NetDnsInfo* info, std::uint32_t flags) { if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); std::array<std::uint32_t, 2> servers{}; @@ -2297,7 +2310,7 @@ void APS5_VABI sceNetMemoryFree(void* memory) { GuestHeap::GuestHeapFree_nid_postfix(memory); } -int APS5_VABI sceNetSetDnsInfoToKernel(void) { +int APS5_VABI sceNetSetDnsInfoToKernel(const NetDnsInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2397,12 +2410,12 @@ int APS5_VABI sceNetInetPtonWithScopeId(void) { return 0; } -int APS5_VABI sceNetGetDns6Info(void) { +int APS5_VABI sceNetGetDns6Info(NetDns6Info* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetSetDns6InfoToKernel(void) { +int APS5_VABI sceNetSetDns6InfoToKernel(const NetDns6Info* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 6d37567f4c..9c27c860fb 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -11,13 +11,11 @@ - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnect](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetControl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -39,15 +37,12 @@ - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware - [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware -- [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware - [sceNetInetNtopWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetSetDns6InfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigSetIfLinkLocalAddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigFlushRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 0e9839e53c0b85f84b04c8a5b783896be219fdd9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:37:24 +0300 Subject: [PATCH 0548/1360] fix(net): recover packet dump and control signatures --- core/libs/prx/libSceNet/Export.cpp | 14 +++++++------- docs/dev/debt/libSceNet.md | 10 ---------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 93e8f30629..cbc9df76fd 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2145,7 +2145,7 @@ int APS5_VABI sceNetAllocateAllRouteInfo6(void) { return 0; } -int APS5_VABI sceNetControl(void) { +int APS5_VABI sceNetControl(std::uint32_t interfaceId, std::int32_t interfaceIndex, std::uint32_t command, void* data, std::uint32_t dataSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2855,7 +2855,7 @@ int APS5_VABI sceNetBandwidthControlGetDataTraffic(void) { return 0; } -int APS5_VABI sceNetConfigSetDefaultScope(void) { +int APS5_VABI sceNetConfigSetDefaultScope(std::uint32_t interfaceId, const char* interfaceName, std::int32_t scopeId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2875,7 +2875,7 @@ int APS5_VABI sceNetConfigRoutingShowRoutingConfig(void) { return 0; } -int APS5_VABI sceNetConfigRoutingShowtCtlVar(void) { +int APS5_VABI sceNetConfigRoutingShowtCtlVar(const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2885,12 +2885,12 @@ int APS5_VABI sceNetGetStatisticsInfoInternal(void) { return 0; } -int APS5_VABI sceNetDumpAbort(void) { +int APS5_VABI sceNetDumpAbort(std::int32_t dumpId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetDumpCreate(void) { +int APS5_VABI sceNetDumpCreate(const char* name, std::int32_t bufferSize, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2900,12 +2900,12 @@ int APS5_VABI sceNetDumpSetFilterParam(void) { return 0; } -int APS5_VABI sceNetDumpDestroy(void) { +int APS5_VABI sceNetDumpDestroy(std::int32_t dumpId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetDumpRead(void) { +int APS5_VABI sceNetDumpRead(std::int32_t dumpId, void* buffer, std::int32_t bufferSize, std::uint32_t* flags) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 9c27c860fb..ca82d1ac9b 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -18,7 +18,6 @@ - [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetControl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDbgInit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -56,7 +55,6 @@ - [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6GetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetInfoDumpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -82,21 +80,13 @@ - [sceNetConfigWlanSetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigEtherPostPlugInOutEvent](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetPppoeStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetBandwidthControlSetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetBandwidthControlGetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetBandwidthControlGetDataTraffic](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetDefaultScope](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigRoutingStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigRoutingShowtCtlVar](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetStatisticsInfoInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDumpAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDumpCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDumpSetFilterParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDumpDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDumpRead](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetEmulationDebugSettingsSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetEmulationDebugSettingsGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetEmulationSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 726359fadda131e02518571b255f8dc9bb48f90b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:39:07 +0300 Subject: [PATCH 0549/1360] fix(ime): recover text notification signatures --- core/libs/prx/libSceIme/Export.cpp | 12 ++++++------ docs/dev/debt/libSceIme.md | 7 ------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 671a4833b0..3d8735dc88 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -610,7 +610,7 @@ int APS5_VABI sceImeKeyboardUpdate(EventHandler handler) { return 0; } -int APS5_VABI sceImeVshUpdate(void) { +int APS5_VABI sceImeVshUpdate(ImeVshContext* context) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -694,7 +694,7 @@ int APS5_VABI sceImeForTestFunction(void) { return ErrorInternal; } -int APS5_VABI sceImeVshInformConfirmdString2(void) { +int APS5_VABI sceImeVshInformConfirmdString2(ImeVshContext* context, const char16_t* text, std::int32_t firstParameter, std::int32_t secondParameter, std::int32_t thirdParameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -734,7 +734,7 @@ int APS5_VABI sceImeVshSendTextInfo(void) { return 0; } -int APS5_VABI sceImeVshInformConfirmdString(void) { +int APS5_VABI sceImeVshInformConfirmdString(ImeVshContext* context, const char16_t* text, std::int32_t parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -749,17 +749,17 @@ int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(void) { return 0; } -int APS5_VABI sceImeVshNotifyDeleteText(void) { +int APS5_VABI sceImeVshNotifyDeleteText(ImeVshContext* context, const char16_t* text, std::int32_t length, std::int32_t parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshNotifyUpdateCaretText(void) { +int APS5_VABI sceImeVshNotifyUpdateCaretText(ImeVshContext* context, const char16_t* text, std::int32_t length, std::int32_t parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(void) { +int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(ImeVshContext* context, const char16_t* text) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 2d0e987979..16d8f84e8e 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -11,23 +11,16 @@ - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdateContext](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifySetText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDisableController](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshInformConfirmdString2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeCheckUpdateTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeCheckRemoteEventParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshSetParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdateContext2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshSendTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshInformConfirmdString](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyCurrentTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshNotifyDeleteText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshNotifyUpdateCaretText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshCheckAutoReConvertingPreeditText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyPreeditMetaInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature From 763dcfe88da354a4ec63917ebd6d22d42dc053e1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:40:02 +0300 Subject: [PATCH 0550/1360] fix(hmd): recover led and device status signatures --- core/libs/prx/libSceHmd/Export.cpp | 18 +++++++++--------- docs/dev/debt/libSceHmd.md | 12 ------------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 493e492595..82ed0eedc9 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -186,7 +186,7 @@ int APS5_VABI sceHmdInternalAnotherScreenSendVideo(void) { return 0; } -int APS5_VABI sceHmdInternalGetRequiredPUPVersion(void) { +int APS5_VABI sceHmdInternalGetRequiredPUPVersion(std::uint32_t* firstVersion, std::uint32_t* secondVersion) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -250,7 +250,7 @@ int APS5_VABI sceHmdInternalSetVRMode(std::int32_t enabled) { return 0; } -int APS5_VABI sceHmdInternalCrashReportReadDataSize(void) { +int APS5_VABI sceHmdInternalCrashReportReadDataSize(std::uint32_t* dataSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -287,7 +287,7 @@ int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(std::uint8_t mode) { return 0; } -int APS5_VABI sceHmdInternalGetHmuPowerStatusForDebug(void) { +int APS5_VABI sceHmdInternalGetHmuPowerStatusForDebug(std::uint8_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -335,7 +335,7 @@ int APS5_VABI sceHmdReprojectionStopCapture(void) { return 0; } -int APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { +bool APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -359,12 +359,12 @@ int APS5_VABI sceHmdInternalGetDebugMode(std::uint8_t* mode) { return 0; } -int APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { +bool APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { +bool APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -374,7 +374,7 @@ int APS5_VABI sceHmdReprojectionSetOutputMinColor(float red, float green, float return 0; } -int APS5_VABI sceHmdInternalSetPortConnection(void) { +int APS5_VABI sceHmdInternalSetPortConnection(std::uint64_t connection) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -384,7 +384,7 @@ int APS5_VABI sceHmdReprojectionDebugGetLastInfo(void) { return 0; } -int APS5_VABI sceHmdInternalSetM2LedOn(void) { +int APS5_VABI sceHmdInternalSetM2LedOn(std::int32_t handle, std::uint32_t ledMask, std::uint32_t brightness) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -405,7 +405,7 @@ int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(std::uint8_t ena return 0; } -int APS5_VABI sceHmdInternalSetLedOn(void) { +int APS5_VABI sceHmdInternalSetLedOn(std::int32_t handle, std::uint32_t ledMask) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 6305a8d945..7e1c4ee2d7 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -11,41 +11,29 @@ - [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetEyeStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionUnsetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetRequiredPUPVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalResetInertialSensor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCrashReportReadDataSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetHmuPowerStatusForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCreateSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStopCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalIsCommonDlgMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMmapGetSensorCalibrationData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetPortConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetM2LedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetLedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioSendData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 47f6af24a46cc0de340645d2984addbfe6f17056 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:42:05 +0300 Subject: [PATCH 0551/1360] fix(json): restore the parser error code --- core/libs/prx/libSceJson2/Export.cpp | 5 +++-- core/libs/tests/GuestJson.cpp | 12 +++++++++--- docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index e4d7e67624..2d1ea2907f 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -25,7 +25,7 @@ enum ValueType : std::int32_t { TypeObject = 7, }; -constexpr int JsonErrorParse = static_cast<int>(0x80920101); +constexpr int JsonErrorParse = static_cast<int>(0x80848101); struct Node; struct Value { Node* node; }; @@ -945,7 +945,8 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value* out, Pars } int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { - if (text == nullptr || size == 0) return JsonErrorParse; + if (size == 0) return JsonErrorParse; + if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); Node parsed{}; parsed.handlers = NodeOf(*out).handlers; Parser parser(text, size, parsed.handlers); diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index f18fbcdec8..1c6ca64872 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -158,7 +158,7 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) enum : std::int32_t { TypeNull, TypeBoolean, TypeInteger, TypeUInteger, TypeReal, TypeString, TypeArray, TypeObject }; -constexpr int JsonErrorParse = static_cast<int>(0x80920101); +constexpr int JsonErrorParse = static_cast<int>(0x80848101); static Value fallback{}; static int callbackCalls = 0; @@ -241,11 +241,17 @@ static void ParseAndRoundTrip() { _ZN3sce4Json5ValueD1Ev(&reparsed); for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru", "{} x", "01"}) { - Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) == JsonErrorParse); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 0) == JsonErrorParse); - Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 10) == JsonErrorParse); + bool nullTextRejected = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 10); + } catch (const std::invalid_argument&) { + nullTextRejected = true; + } + Require(nullTextRejected); Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "", 0) == JsonErrorParse); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); for (const char* overflow : {"1e309", "[-1e309]", "{\"a\":[1,1e400]}"}) { diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 86b6444e91..b290b68e93 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -3,7 +3,7 @@ ### Unknown function info - [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown -- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown +- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types From 17a1c4d291631706b5575f0e7a558a9d5ff31bec Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:43:48 +0300 Subject: [PATCH 0552/1360] fix(json): restore bytewise string comparison --- core/libs/prx/libSceJson2/Export.cpp | 16 ++++++++++++---- core/libs/tests/GuestJson.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 2d1ea2907f..4a6b1247b7 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -333,6 +333,14 @@ void Serialize(std::string& out, const Node& n) { throw std::runtime_error("sce::Json: invalid value type"); } +int CompareText(const char* left, const char* right) { + while (*left != '\0' && *left == *right) { + ++left; + ++right; + } + return static_cast<unsigned char>(*left) - static_cast<unsigned char>(*right); +} + std::list<Pair>::iterator ObjectPosition(const Object& object, const std::string& key) { return std::find_if(object.items->begin(), object.items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key.c_str()) >= 0; }); } @@ -617,8 +625,8 @@ bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String* self) { return self->t std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return self->text->size(); } std::size_t APS5_VABI _ZNK3sce4Json6String4sizeEv(const String* self) { return self->text->size(); } std::size_t APS5_VABI _ZNK3sce4Json6String8capacityEv(const String* self) { return self->text->capacity(); } -bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } -bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String* self, const String* other) { return other != nullptr && other->text != nullptr && *self->text == *other->text; } +bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && CompareText(self->text->c_str(), text) == 0; } +bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String* self, const String* other) { return other != nullptr && other->text != nullptr && CompareText(self->text->c_str(), other->text->c_str()) == 0; } void APS5_VABI _ZN3sce4Json6String5clearEv(String* self) { self->text->clear(); } void APS5_VABI _ZN3sce4Json6String6resizeEm(String* self, std::size_t count) { self->text->resize(count); } void APS5_VABI _ZN3sce4Json6String7reserveEm(String* self, std::size_t capacity) { self->text->reserve(capacity); } @@ -653,11 +661,11 @@ std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String* self, char char std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String* self, char character, std::size_t position) { return self->text->rfind(character, position); } int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null text"); - return self->text->compare(text); + return CompareText(self->text->c_str(), text); } int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String* self, const String* other) { if (other == nullptr || other->text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null other"); - return self->text->compare(*other->text); + return CompareText(self->text->c_str(), other->text->c_str()); } void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 1c6ca64872..f8c8c0f121 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -626,6 +626,22 @@ static void ObjectKeyOrder() { _ZN3sce4Json6ObjectD1Ev(&object); } +static void StringComparison() { + String left{}, right{}; + _ZN3sce4Json6StringC1EPKc(&left, "a"); + _ZN3sce4Json6StringC1EPKc(&right, "a"); + _ZN3sce4Json6String6appendEPKcm(&left, "\0suffix", 7); + Require(_ZNK3sce4Json6StringeqERKS1_(&left, &right)); + Require(_ZNK3sce4Json6StringeqEPKc(&left, "a")); + Require(_ZNK3sce4Json6String7compareERKS1_(&left, &right) == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "a") == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "z") == -25); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "") == 97); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "\xff") == -158); + _ZN3sce4Json6StringD1Ev(&right); + _ZN3sce4Json6StringD1Ev(&left); +} + static void StringMethods() { Require(_ZN3sce4Json6String4nposE == static_cast<std::size_t>(-1)); const auto text = [](const String& string) { return std::string(_ZNK3sce4Json6String5c_strEv(&string), _ZNK3sce4Json6String6lengthEv(&string)); }; @@ -1286,6 +1302,7 @@ int main() { ObjectIteratorCopies(); AllocatorNotifyError(); StringMethods(); + StringComparison(); ValueSwap(); ArrayIteratorAdvance(); ObjectOperations(); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index b290b68e93..eba56c389a 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -8,5 +8,5 @@ - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown -- [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified +- [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. From f487c3699c13f253e54f8669533ca79a6fffaf0b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:45:18 +0300 Subject: [PATCH 0553/1360] fix(json): replace duplicate members while parsing --- core/libs/prx/libSceJson2/Export.cpp | 38 ++++++++++++++-------------- core/libs/tests/GuestJson.cpp | 12 +++++++++ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 4a6b1247b7..6e6a475608 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -345,6 +345,21 @@ std::list<Pair>::iterator ObjectPosition(const Object& object, const std::string return std::find_if(object.items->begin(), object.items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key.c_str()) >= 0; }); } +Value& ObjectEntry(Object& object, const std::string& key) { + auto position = ObjectPosition(object, key); + if (position != object.items->end() && std::strcmp(position->key.text->c_str(), key.c_str()) == 0) return position->value; + Pair pair{}; + Construct(pair.key, key); + try { + Construct(pair.value); + return object.items->insert(position, pair)->value; + } catch (...) { + Destroy(pair.value); + Destroy(pair.key); + throw; + } +} + class Parser { public: Parser(const char* text, std::size_t size, const Handlers& handlers) : _text(text), _end(text + size), _handlers(handlers) {} @@ -514,11 +529,10 @@ private: if (_text >= _end || *_text != ':') return false; ++_text; skip(); - auto& pair = *out.object.items->emplace(ObjectPosition(out.object, key)); - Construct(pair.key, key); - Construct(pair.value); - pair.value.node->handlers = _handlers; - if (!value(*pair.value.node)) return false; + Value& entry = ObjectEntry(out.object, key); + Clear(*entry.node); + entry.node->handlers = _handlers; + if (!value(*entry.node)) return false; skip(); if (_text < _end && *_text == ',') { ++_text; continue; } if (_text < _end && *_text == '}') { ++_text; --_depth; return true; } @@ -542,20 +556,6 @@ void* const JsonMemAllocatorVtable[] = { reinterpret_cast<void*>(&JsonMemAllocatorPureVirtual), reinterpret_cast<void*>(&JsonMemAllocatorPureVirtual), }; -Value& ObjectEntry(Object& object, const std::string& key) { - auto position = ObjectPosition(object, key); - if (position != object.items->end() && std::strcmp(position->key.text->c_str(), key.c_str()) == 0) return position->value; - Pair pair{}; - Construct(pair.key, key); - try { - Construct(pair.value); - return object.items->insert(position, pair)->value; - } catch (...) { - Destroy(pair.value); - Destroy(pair.key); - throw; - } -} } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index f8c8c0f121..fac453fa0e 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -626,6 +626,17 @@ static void ObjectKeyOrder() { _ZN3sce4Json6ObjectD1Ev(&object); } +static void ParseDuplicateKeys() { + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + for (const char* text : {R"({"a":1,"a":2})", R"({"a":[1,2],"a":2})", R"({"a":{"old":true},"a":2})", R"({"a":1,"a\u0000suffix":2})"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, std::strlen(text)) == 0); + Require(_ZNK3sce4Json5Value5countEv(&root) == 1); + Require(Serialize(root) == R"({"a":2})"); + } + _ZN3sce4Json5ValueD1Ev(&root); +} + static void StringComparison() { String left{}, right{}; _ZN3sce4Json6StringC1EPKc(&left, "a"); @@ -1303,6 +1314,7 @@ int main() { AllocatorNotifyError(); StringMethods(); StringComparison(); + ParseDuplicateKeys(); ValueSwap(); ArrayIteratorAdvance(); ObjectOperations(); From 03e47f84fb671cac565c1f87c2b6842afaa69deb Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:47:16 +0300 Subject: [PATCH 0554/1360] fix(json): restore incremental parser callback semantics --- core/libs/prx/libSceJson2/Export.cpp | 182 +++++++++++++++------------ core/libs/tests/GuestJson.cpp | 27 +++- docs/dev/debt/libSceJson2.md | 3 +- 3 files changed, 126 insertions(+), 86 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 6e6a475608..b14ad5ccd7 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -360,30 +360,57 @@ Value& ObjectEntry(Object& object, const std::string& key) { } } +using ParserReadCallback = int (APS5_VABI*)(char&, void*); + class Parser { public: - Parser(const char* text, std::size_t size, const Handlers& handlers) : _text(text), _end(text + size), _handlers(handlers) {} + Parser(const char* text, std::size_t size, const Handlers& handlers) : text(text), end(text + size), handlers(handlers) {} + Parser(ParserReadCallback read, void* context, const Handlers& handlers) : read(read), context(context), handlers(handlers) {} bool Parse(Node& out) { skip(); - if (!value(out)) return false; - skip(); - return _text == _end || *_text == '\0'; + return value(out); } + int Error() const { return readError < 0 ? readError : JsonErrorParse; } + private: static constexpr std::size_t MaxNestingDepth = 512; - const char* _text; - const char* _end; - Handlers _handlers; - std::size_t _depth = 0; + const char* text = nullptr; + const char* end = nullptr; + ParserReadCallback read = nullptr; + void* context = nullptr; + Handlers handlers; + std::size_t depth = 0; + int current = -2; + int readError = 0; - void skip() { while (_text < _end && (*_text == ' ' || *_text == '\t' || *_text == '\n' || *_text == '\r')) ++_text; } + int peek() { + if (current != -2) return current; + if (read != nullptr) { + char character = 0; + readError = read(character, context); + current = readError < 0 ? -1 : static_cast<unsigned char>(character); + } else if (text == end) { + readError = JsonErrorParse; + current = -1; + } else { + current = static_cast<unsigned char>(*text++); + } + return current; + } + int take() { + const int character = peek(); + if (character >= 0) current = -2; + return character; + } + void skip() { while (peek() == ' ' || peek() == '\t' || peek() == '\n' || peek() == '\r') take(); } bool literal(const char* word) { - const auto length = std::strlen(word); - if (static_cast<std::size_t>(_end - _text) < length || std::memcmp(_text, word, length) != 0) return false; - _text += length; + for (; *word != '\0'; ++word) { + if (peek() != static_cast<unsigned char>(*word)) return false; + take(); + } return true; } static void appendUtf8(std::string& out, std::uint32_t code) { @@ -393,10 +420,9 @@ private: else { out += static_cast<char>(0xf0 | (code >> 18)); out += static_cast<char>(0x80 | ((code >> 12) & 0x3f)); out += static_cast<char>(0x80 | ((code >> 6) & 0x3f)); out += static_cast<char>(0x80 | (code & 0x3f)); } } bool hex4(std::uint32_t& code) { - if (_end - _text < 4) return false; code = 0; for (int i = 0; i < 4; ++i) { - const char c = *_text++; + const int c = take(); code <<= 4; if (c >= '0' && c <= '9') code |= static_cast<std::uint32_t>(c - '0'); else if (c >= 'a' && c <= 'f') code |= static_cast<std::uint32_t>(c - 'a' + 10); @@ -406,14 +432,14 @@ private: return true; } bool string(std::string& out) { - if (_text >= _end || *_text != '"') return false; - ++_text; - while (_text < _end && *_text != '"') { - const char c = *_text++; + if (peek() < 0 || peek() != '"') return false; + take(); + while (peek() >= 0 && peek() != '"') { + const int c = take(); if (static_cast<unsigned char>(c) < 0x20) return false; if (c != '\\') { out += c; continue; } - if (_text >= _end) return false; - switch (*_text++) { + if (peek() < 0) return false; + switch (take()) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; @@ -436,31 +462,30 @@ private: default: return false; } } - if (_text >= _end) return false; - ++_text; + if (peek() < 0) return false; + take(); return true; } bool number(Node& out) { - const char* start = _text; - if (_text < _end && *_text == '-') ++_text; - if (_text >= _end || *_text < '0' || *_text > '9') return false; - if (*_text == '0') ++_text; - else while (_text < _end && *_text >= '0' && *_text <= '9') ++_text; + std::string token; + if (peek() >= 0 && peek() == '-') token.push_back(static_cast<char>(take())); + if (peek() < 0 || peek() < '0' || peek() > '9') return false; + while (peek() >= '0' && peek() <= '9') token.push_back(static_cast<char>(take())); bool integral = true; - if (_text < _end && *_text == '.') { + if (peek() >= 0 && peek() == '.') { integral = false; - ++_text; - if (_text >= _end || *_text < '0' || *_text > '9') return false; - while (_text < _end && *_text >= '0' && *_text <= '9') ++_text; + token.push_back(static_cast<char>(take())); + if (peek() < 0 || peek() < '0' || peek() > '9') return false; + while (peek() >= 0 && peek() >= '0' && peek() <= '9') token.push_back(static_cast<char>(take())); } - if (_text < _end && (*_text == 'e' || *_text == 'E')) { + if (peek() >= 0 && (peek() == 'e' || peek() == 'E')) { integral = false; - ++_text; - if (_text < _end && (*_text == '+' || *_text == '-')) ++_text; - if (_text >= _end || *_text < '0' || *_text > '9') return false; - while (_text < _end && *_text >= '0' && *_text <= '9') ++_text; + token.push_back(static_cast<char>(take())); + if (peek() >= 0 && (peek() == '+' || peek() == '-')) token.push_back(static_cast<char>(take())); + if (peek() < 0 || peek() < '0' || peek() > '9') return false; + while (peek() >= 0 && peek() >= '0' && peek() <= '9') token.push_back(static_cast<char>(take())); } - const std::string text(start, _text); + const std::string& text = token; if (integral) { errno = 0; if (text[0] == '-') { @@ -482,8 +507,8 @@ private: return true; } bool value(Node& out) { - if (_text >= _end) return false; - switch (*_text) { + if (peek() < 0) return false; + switch (peek()) { case 'n': if (!literal("null")) return false; SetType(out, TypeNull); return true; case 't': if (!literal("true")) return false; SetType(out, TypeBoolean); out.boolean = true; return true; case 'f': if (!literal("false")) return false; SetType(out, TypeBoolean); return true; @@ -495,47 +520,47 @@ private: return true; } case '[': { - if (_depth == MaxNestingDepth) return false; - ++_text; - ++_depth; + if (depth == MaxNestingDepth) return false; + take(); + ++depth; SetType(out, TypeArray); skip(); - if (_text < _end && *_text == ']') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == ']') { take(); --depth; return true; } for (;;) { Value item{}; Construct(item); - item.node->handlers = _handlers; + item.node->handlers = handlers; out.array.items->push_back(item); skip(); if (!value(*item.node)) return false; skip(); - if (_text < _end && *_text == ',') { ++_text; continue; } - if (_text < _end && *_text == ']') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == ',') { take(); continue; } + if (peek() >= 0 && peek() == ']') { take(); --depth; return true; } return false; } } case '{': { - if (_depth == MaxNestingDepth) return false; - ++_text; - ++_depth; + if (depth == MaxNestingDepth) return false; + take(); + ++depth; SetType(out, TypeObject); skip(); - if (_text < _end && *_text == '}') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == '}') { take(); --depth; return true; } for (;;) { skip(); std::string key; if (!string(key)) return false; skip(); - if (_text >= _end || *_text != ':') return false; - ++_text; + if (peek() < 0 || peek() != ':') return false; + take(); skip(); Value& entry = ObjectEntry(out.object, key); Clear(*entry.node); - entry.node->handlers = _handlers; + entry.node->handlers = handlers; if (!value(*entry.node)) return false; skip(); - if (_text < _end && *_text == ',') { ++_text; continue; } - if (_text < _end && *_text == '}') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == ',') { take(); continue; } + if (peek() >= 0 && peek() == '}') { take(); --depth; return true; } return false; } } @@ -545,6 +570,25 @@ private: } }; +int ParseValue(Value* out, Parser& parser) { + Node parsed{}; + parsed.handlers = NodeOf(*out).handlers; + bool parsedOk = false; + try { + parsedOk = parser.Parse(parsed); + } catch (...) { + Clear(parsed); + throw; + } + if (!parsedOk) { + Clear(parsed); + return parser.Error(); + } + Clear(NodeOf(*out)); + *out->node = parsed; + return 0; +} + [[noreturn]] void APS5_VABI JsonMemAllocatorPureVirtual() { throw std::runtime_error("sce::Json::MemAllocator: pure virtual function called"); } @@ -940,38 +984,18 @@ int APS5_VABI _ZNK3sce4Json5Value8toStringERNS0_6StringE(const Value* self, Stri return 0; } -using ParserReadCallback = int (APS5_VABI*)(char&, void*); - -int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value* out, ParserReadCallback read, void* context) { if (read == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null read callback"); - std::string text; - char ch = 0; - while (read(ch, context) == 0 && ch != '\0') text.push_back(ch); - return _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(out, text.data(), text.size()); + Parser parser(read, context, NodeOf(*out).handlers); + return ParseValue(out, parser); } int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { if (size == 0) return JsonErrorParse; if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); - Node parsed{}; - parsed.handlers = NodeOf(*out).handlers; - Parser parser(text, size, parsed.handlers); - bool parsedOk = false; - try { - parsedOk = parser.Parse(parsed); - } catch (...) { - Clear(parsed); - throw; - } - if (!parsedOk) { - Clear(parsed); - return JsonErrorParse; - } - Clear(NodeOf(*out)); - *out->node = parsed; - return 0; + Parser parser(text, size, NodeOf(*out).handlers); + return ParseValue(out, parser); } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index fac453fa0e..9193c726c7 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -240,7 +240,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"}) { Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) == JsonErrorParse); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } @@ -1038,6 +1038,8 @@ struct ReadState { const char* text; std::size_t position; int calls; + int successStatus = 0; + int failureStatus = -1; }; static int APS5_VABI ReadNext(char& ch, void* context) { @@ -1045,10 +1047,10 @@ static int APS5_VABI ReadNext(char& ch, void* context) { ++state->calls; if (state->text[state->position] == '\0') { ch = '\0'; - return -1; + return state->failureStatus; } ch = state->text[state->position++]; - return 0; + return state->successStatus; } static void StreamParse() { @@ -1056,12 +1058,27 @@ static void StreamParse() { _ZN3sce4Json5ValueC1Ev(&root); ReadState valid{"{\"list\": [1, 2.5, true]}", 0, 0}; Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &valid) == 0); - Require(valid.calls == static_cast<int>(std::strlen(valid.text)) + 1); + Require(valid.calls == static_cast<int>(std::strlen(valid.text))); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject); ReadState invalid{"{\"list\": [1, 2.5", 0, 0}; Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &invalid) < 0); ReadState trailing{"[1] 2", 0, 0}; - Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) < 0); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) == 0); + Require(trailing.position == 3); + Require(Serialize(root) == "[1]"); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) == 0); + Require(Serialize(root) == "2"); + ReadState positive{"true false", 0, 0, 7}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &positive) == 0); + Require(positive.position == 4 && positive.calls == 4); + Require(Serialize(root) == "true"); + ReadState failed{"tr", 0, 0, 0, -123456}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &failed) == -123456); + Require(failed.calls == 3); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "{} trailing", 11) == 0); + Require(Serialize(root) == "{}"); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "01", 2) == 0); + Require(Serialize(root) == "1"); _ZN3sce4Json5ValueD1Ev(&root); } diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index eba56c389a..897a310ef2 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -2,8 +2,7 @@ ### Unknown function info -- [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown -- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown +- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Positive integer tokens use the signed type when they fit; the system parser uses the unsigned type. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types From 10c25e1dac30144bc56ee4fec61155931e0531b4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:51:07 +0300 Subject: [PATCH 0555/1360] fix(http2): recover websocket callbacks and async signatures --- core/libs/prx/libSceHttp2/Export.cpp | 13 +++++++++---- docs/dev/debt/libSceHttp2.md | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 3d681f4ede..bc1682e4fe 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -101,6 +101,11 @@ static int CompleteAsync(const char* function, int req_id, const Http2AsyncOptio return 0; } +using Http2WebSocketTextCallback = void (APS5_VABI*)(std::int32_t requestId, const char* text, std::uint32_t flags, void* userData); +using Http2WebSocketDataCallback = void (APS5_VABI*)(std::int32_t requestId, const void* data, std::size_t dataSize, std::uint32_t flags, void* userData); +using Http2WebSocketCloseCallback = void (APS5_VABI*)(std::int32_t requestId, const std::uint16_t* status, const char* reason, std::uint32_t flags, void* userData); +using Http2WebSocketErrorCallback = void (APS5_VABI*)(std::int32_t requestId, std::int32_t error, void* userData); + extern "C" { int APS5_VABI sceHttp2AddRequestHeader(int id, const char* name, const char* value, uint32_t mode) { @@ -381,22 +386,22 @@ int APS5_VABI sceHttp2SetResolveRetry(int id, int32_t retry) { return 0; } -int APS5_VABI sceHttp2WebSocketCreateRequest() { +int APS5_VABI sceHttp2WebSocketCreateRequest(std::int32_t templateId, const char* url, Http2WebSocketTextCallback textCallback, Http2WebSocketDataCallback dataCallback, Http2WebSocketCloseCallback closeCallback, Http2WebSocketErrorCallback errorCallback, void* receiveBuffer, std::size_t receiveBufferSize, void* userData, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketCloseAsync() { +int APS5_VABI sceHttp2WebSocketCloseAsync(std::int32_t requestId, const std::uint16_t* status, const char* reason, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketSendTextMessageAsync() { +int APS5_VABI sceHttp2WebSocketSendTextMessageAsync(std::int32_t requestId, const char* text, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketSendDataMessageAsync() { +int APS5_VABI sceHttp2WebSocketSendDataMessageAsync(std::int32_t requestId, const void* data, std::size_t dataSize, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 2fa52cfbc5..f3db1ce9df 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -10,7 +10,6 @@ - [sceHttp2WebSocketSetPingTimeout](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `-MU9L6S3Fj4`) - imported by PPSA27836 01.000.001, whose loader stopped on the missing export; not called during its start-up. The parameters and the console errors are unknown. Every call throws. - [sceHttp2DeleteCookieBox](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console - [sceHttp2SetResolveRetry](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - signature `(int id, int32_t retry)` from libSceHttp's `sceHttpSetResolveRetry`; the value is accepted and ignored like the other Http2 settings, since no request reaches the network -- [sceHttp2WebSocketCreateRequest, sceHttp2WebSocketCloseAsync, sceHttp2WebSocketSendTextMessageAsync, sceHttp2WebSocketSendDataMessageAsync](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA16064 and not called during its start-up; unknown signatures - [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 From 7525726c14dd102490ae0b1936af9870a599d910 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:55:27 +0300 Subject: [PATCH 0556/1360] fix(netctl): recover access point retry signatures --- core/libs/prx/libSceNetCtl/Export.cpp | 6 +++--- docs/dev/debt/libSceNetCtl.md | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 1819d066c5..6c6663967f 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -778,12 +778,12 @@ int APS5_VABI sceNetCtlApCpStart(void) { return 0; } -int APS5_VABI sceNetCtlApAppStartWithRetryPid(void) { +int APS5_VABI sceNetCtlApAppStartWithRetryPid(std::int32_t processId, const char* passcode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlApAppStartWithRetry(void) { +int APS5_VABI sceNetCtlApAppStartWithRetry(const char* passcode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -810,7 +810,7 @@ int APS5_VABI sceNetCtlApAppInitWpaKey(void) { return 0; } -int APS5_VABI sceNetCtlApAppInitWpaKeyForQa(void) { +int APS5_VABI sceNetCtlApAppInitWpaKeyForQa(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 4f3c132d59..3f5f367476 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -29,14 +29,9 @@ - [sceNetCtlApRpGetState](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApCpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApAppStartWithRetryPid](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApAppStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApAppInitWpaKey](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApAppInitWpaKeyForQa](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRestart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweSetInternetConnectionTestResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature From 0b9b261a7aa1e9f6520bbb239d1634c69fc8d100 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:56:23 +0300 Subject: [PATCH 0557/1360] fix(hmd): recover screen transfer and eye status signatures --- core/libs/prx/libSceHmd/Export.cpp | 15 +++++++++++---- docs/dev/debt/libSceHmd.md | 6 ------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 82ed0eedc9..0a5e7009e1 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -27,6 +27,13 @@ struct HmdEyeOffset { float z; }; +struct HmdScreenData { + const void* data; + std::int32_t size; +}; + +static_assert(sizeof(HmdScreenData) == 16); +static_assert(offsetof(HmdScreenData, size) == 8); static_assert(sizeof(HmdEyeDistances) == 8); static_assert(sizeof(HmdEyeOffset) == 12); @@ -108,7 +115,7 @@ int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(std::uint8_t* depth) { return 0; } -int APS5_VABI sceHmdInternalGetEyeStatus(void) { +int APS5_VABI sceHmdInternalGetEyeStatus(std::int32_t handle, HmdEyeOffset* left, HmdEyeOffset* right) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -181,7 +188,7 @@ int APS5_VABI sceHmdInternalDfuGetStatus(void) { return 0; } -int APS5_VABI sceHmdInternalAnotherScreenSendVideo(void) { +int APS5_VABI sceHmdInternalAnotherScreenSendVideo(const HmdScreenData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -260,7 +267,7 @@ int APS5_VABI sceHmdInternalGetVr2dData(void) { return 0; } -int APS5_VABI sceHmdInternalAnotherScreenSendAudio(void) { +int APS5_VABI sceHmdInternalAnotherScreenSendAudio(const HmdScreenData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -840,7 +847,7 @@ int APS5_VABI sceHmdInternal3dAudioOpen(void) { return 0; } -int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(void) { +int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(std::int32_t* state) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 7e1c4ee2d7..40c6b40792 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -7,21 +7,16 @@ ### Unknown function info - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count -- [sceHmdInternalEventInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetEyeStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -100,6 +95,5 @@ - [sceHmdInternalSetPortStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 09d851b3155826d82c54ef3157cf7f4288400830 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:57:23 +0300 Subject: [PATCH 0558/1360] fix(hmd): recover device update transfer signatures --- core/libs/prx/libSceHmd/Export.cpp | 18 ++++++++++++++---- docs/dev/debt/libSceHmd.md | 8 -------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 0a5e7009e1..cd2eaa264e 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -27,6 +27,16 @@ struct HmdEyeOffset { float z; }; +struct HmdDfuData { + const void* data; + std::uint32_t size; + std::uint32_t mode; +}; + +static_assert(sizeof(HmdDfuData) == 16); +static_assert(offsetof(HmdDfuData, size) == 8); +static_assert(offsetof(HmdDfuData, mode) == 12); + struct HmdScreenData { const void* data; std::int32_t size; @@ -546,7 +556,7 @@ int APS5_VABI sceHmdDistortionInitialize(void) { return 0; } -int APS5_VABI sceHmdInternalDfuSetMode(void) { +int APS5_VABI sceHmdInternalDfuSetMode(std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -556,7 +566,7 @@ int APS5_VABI sceHmdInternalSetForcedCrash(void) { return 0; } -int APS5_VABI sceHmdInternalDfuCheckPartialUpdateAvailable(void) { +int APS5_VABI sceHmdInternalDfuCheckPartialUpdateAvailable(bool* available) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -646,7 +656,7 @@ int APS5_VABI sceHmdDistortionGet2dVrCommand(void) { return 0; } -int APS5_VABI sceHmdInternalDfuSendSize(void) { +int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -730,7 +740,7 @@ int APS5_VABI sceHmdGetDistortionParams(void) { return 0; } -int APS5_VABI sceHmdInternalDfuSend(void) { +int APS5_VABI sceHmdInternalDfuSend(const HmdDfuData* data) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 40c6b40792..e41acf1fcd 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -37,7 +37,6 @@ - [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetHmuPowerControl](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalBindDeviceWithUserId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -49,16 +48,12 @@ - [sceHmdInternalIsGameVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuSetMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetForcedCrash](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuCheckPartialUpdateAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetHmuPowerControlForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetLedForVsh](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -68,7 +63,6 @@ - [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMmapGetModeId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuSendSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -80,10 +74,8 @@ - [sceHmdInternalCrashReportOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuSend](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuReset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMmapGetCount](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 4e3f7647e8ff456b65ec414480758d9566e57098 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:57:56 +0300 Subject: [PATCH 0559/1360] fix(hmd): recover device control and capability signatures --- core/libs/prx/libSceHmd/Export.cpp | 22 +++++++++++----------- docs/dev/debt/libSceHmd.md | 11 ----------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index cd2eaa264e..c7cd7529b0 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -481,7 +481,7 @@ int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void) { return 0x100; } -int APS5_VABI sceHmdInternalSetHmuPowerControl(void) { +int APS5_VABI sceHmdInternalSetHmuPowerControl(std::uint8_t control) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -501,7 +501,7 @@ int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(void) { return 0; } -int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(void) { +int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(bool* available) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -521,7 +521,7 @@ int APS5_VABI sceHmdReprojectionStartLiveCapture(void) { return 0; } -int APS5_VABI sceHmdInternalIsGameVr2d(void) { +bool APS5_VABI sceHmdInternalIsGameVr2d(void) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -561,7 +561,7 @@ int APS5_VABI sceHmdInternalDfuSetMode(std::uint32_t mode) { return 0; } -int APS5_VABI sceHmdInternalSetForcedCrash(void) { +int APS5_VABI sceHmdInternalSetForcedCrash(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -586,7 +586,7 @@ int APS5_VABI sceHmdInternalCrashReportClose(void) { return 0; } -int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(void) { +int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(std::uint8_t control) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -611,7 +611,7 @@ int APS5_VABI sceHmdInternalDfuOpen(void) { return 0; } -int APS5_VABI sceHmdInternalSetDebugMode(void) { +int APS5_VABI sceHmdInternalSetDebugMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -626,17 +626,17 @@ int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(void) { return 0; } -int APS5_VABI sceHmdInternalCheckDeviceModelMk3(void) { +int APS5_VABI sceHmdInternalCheckDeviceModelMk3(bool* isMk3) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdSetVr2dEnable(void) { +int APS5_VABI sceHmdSetVr2dEnable(std::int32_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalSetSidetone(void) { +int APS5_VABI sceHmdInternalSetSidetone(std::uint8_t level) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -676,7 +676,7 @@ int APS5_VABI sceHmdGetDistortionCorrectionCommand(void) { return 0; } -int APS5_VABI sceHmdInternalSetDeviceConnection(void) { +int APS5_VABI sceHmdInternalSetDeviceConnection(std::uint64_t connection) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -837,7 +837,7 @@ int APS5_VABI sceHmdReprojectionFinalizeCapture(void) { return 0; } -int APS5_VABI sceHmdInternalSetPortStatus(void) { +int APS5_VABI sceHmdInternalSetPortStatus(std::uint64_t status) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index e41acf1fcd..6e2bc07f80 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -37,36 +37,26 @@ - [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetHmuPowerControl](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalBindDeviceWithUserId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCheckS3dPassModeAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalIsGameVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetForcedCrash](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetHmuPowerControlForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetLedForVsh](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCheckDeviceModelMk3](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdSetVr2dEnable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetSidetone](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMmapGetModeId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDeviceConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -84,7 +74,6 @@ - [sceHmdInternalSeparateModeSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetPortStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 027ec5453ca0c8bf577b900d3b23e140d78e1754 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:58:38 +0300 Subject: [PATCH 0560/1360] fix(hmd): recover crash report and separate screen signatures --- core/libs/prx/libSceHmd/Export.cpp | 8 ++++---- docs/dev/debt/libSceHmd.md | 8 -------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index c7cd7529b0..8d24c62b73 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -452,7 +452,7 @@ int APS5_VABI sceHmdInternalAnotherScreenClose(void) { return 0; } -int APS5_VABI sceHmdInternalCrashReportReadData(void) { +int APS5_VABI sceHmdInternalCrashReportReadData(void* buffer, std::uint32_t* bytesRead, std::uint32_t bufferSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -696,7 +696,7 @@ int APS5_VABI sceHmdInternalGetHmuOpticalParam(void) { return 0; } -int APS5_VABI sceHmdInternalSeparateModeSendVideo(void) { +int APS5_VABI sceHmdInternalSeparateModeSendVideo(const HmdScreenData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -812,12 +812,12 @@ int APS5_VABI sceHmdInternalSetBrightness(std::uint8_t brightness) { return 0; } -int APS5_VABI sceHmdInternalSocialScreenGetFadeState(void) { +int APS5_VABI sceHmdInternalSocialScreenGetFadeState(std::int32_t* state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalSeparateModeSendAudio(void) { +int APS5_VABI sceHmdInternalSeparateModeSendAudio(const HmdScreenData* data) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 6e2bc07f80..44528ec2d5 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -33,7 +33,6 @@ - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStopLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCrashReportReadData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -46,9 +45,7 @@ - [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalResetLedForVsh](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -58,10 +55,7 @@ - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCrashReportOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -70,8 +64,6 @@ - [sceHmdInternalMmapGetCount](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternal3dAudioClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSocialScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSeparateModeSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From bea25c3189e92d260cc0a8d835671d4743b887db Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:59:35 +0300 Subject: [PATCH 0561/1360] fix(hmd): recover audio port and event callback signatures --- core/libs/prx/libSceHmd/Export.cpp | 24 ++++++++++++++++++------ docs/dev/debt/libSceHmd.md | 7 ------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 8d24c62b73..eb46e0aeef 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -27,6 +27,18 @@ struct HmdEyeOffset { float z; }; +struct HmdMirroringAspect { + std::uint16_t reserved0; + std::int16_t aspect; + std::uint16_t reserved1; + std::uint16_t reserved2; +}; + +static_assert(sizeof(HmdMirroringAspect) == 8); +static_assert(offsetof(HmdMirroringAspect, aspect) == 2); + +using HmdEventCallback = void (APS5_VABI *)(std::uint32_t events); + struct HmdDfuData { const void* data; std::uint32_t size; @@ -154,7 +166,7 @@ int APS5_VABI sceHmdDistortionGetWorkMemoryAlign(void) { return 0x400; } -int APS5_VABI sceHmdInternalRegisterEventCallback(void) { +int APS5_VABI sceHmdInternalRegisterEventCallback(HmdEventCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -188,7 +200,7 @@ int APS5_VABI sceHmdInternalSetIPD(std::int32_t mode, HmdEyeDistances distances) return 0; } -int APS5_VABI sceHmdInternalMirroringModeSetAspect(void) { +int APS5_VABI sceHmdInternalMirroringModeSetAspect(const HmdMirroringAspect* aspect) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -427,7 +439,7 @@ int APS5_VABI sceHmdInternalSetLedOn(std::int32_t handle, std::uint32_t ledMask) return 0; } -int APS5_VABI sceHmdInternal3dAudioSendData(void) { +int APS5_VABI sceHmdInternal3dAudioSendData(std::int32_t port, const void* data, std::int32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -646,7 +658,7 @@ int APS5_VABI sceHmdInternalSeparateModeOpen(void) { return 0; } -int APS5_VABI sceHmdInternalMmapGetModeId(void) { +int APS5_VABI sceHmdInternalMmapGetModeId(std::uint32_t* modeId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -802,7 +814,7 @@ int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, void* data, std:: return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } -int APS5_VABI sceHmdInternal3dAudioClose(void) { +int APS5_VABI sceHmdInternal3dAudioClose(std::int32_t port) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -852,7 +864,7 @@ int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(void) { return 0; } -int APS5_VABI sceHmdInternal3dAudioOpen(void) { +int APS5_VABI sceHmdInternal3dAudioOpen(std::int32_t port) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 44528ec2d5..f632e1a788 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -8,9 +8,7 @@ - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count - [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -29,7 +27,6 @@ - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternal3dAudioSendData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStopLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -49,7 +46,6 @@ - [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalMmapGetModeId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -61,12 +57,9 @@ - [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalMmapGetCount](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternal3dAudioClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 9773ed9724892b23e6b6eb030b7afd32ee97b1f6 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 12:59:51 +0300 Subject: [PATCH 0562/1360] docs(hmd): close verified capture cleanup signatures --- docs/dev/debt/libSceHmd.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index f632e1a788..937a332046 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -29,7 +29,6 @@ - [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStopLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -39,7 +38,6 @@ - [sceHmdInternalAnotherScreenGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature @@ -54,12 +52,10 @@ - [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 60e09c873db55a5046bff5c3a8efa120c51d20dd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:01:45 +0300 Subject: [PATCH 0563/1360] fix(net): recover scoped address and interface control signatures --- core/libs/prx/libSceNet/Export.cpp | 16 ++++++++-------- docs/dev/debt/libSceNet.md | 8 -------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index cbc9df76fd..80d5b41db6 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2400,12 +2400,12 @@ int APS5_VABI sceNetShowNetstatExForBuffer(char* buffer, std::int32_t bufferSize return 0; } -int APS5_VABI sceNetInetNtopWithScopeId(void) { +const char* APS5_VABI sceNetInetNtopWithScopeId(std::int32_t family, const void* address, char* text, std::uint32_t size, std::uint32_t scopeId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetInetPtonWithScopeId(void) { +int APS5_VABI sceNetInetPtonWithScopeId(std::int32_t family, const char* text, void* address, std::uint32_t* scopeId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2465,7 +2465,7 @@ int APS5_VABI sceNetConfigSetIfaddr(int interfaceId, const std::uint32_t* addres return 0; } -int APS5_VABI sceNetConfigSetIfLinkLocalAddr6(void) { +int APS5_VABI sceNetConfigSetIfLinkLocalAddr6(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2485,12 +2485,12 @@ int APS5_VABI sceNetConfigCleanUpAllInterfaces(void) { return 0; } -int APS5_VABI sceNetConfigFlushRoute(void) { +int APS5_VABI sceNetConfigFlushRoute(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigFlushRoute6(void) { +int APS5_VABI sceNetConfigFlushRoute6(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2690,7 +2690,7 @@ int APS5_VABI sceNetDuplicateIpStop(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigWlanAdhocClearWakeOnWlan(void) { +int APS5_VABI sceNetConfigWlanAdhocClearWakeOnWlan(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2715,7 +2715,7 @@ int APS5_VABI sceNetConfigWlanAdhocLeave(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigWlanAdhocPspEmuClearWakeOnWlan(void) { +int APS5_VABI sceNetConfigWlanAdhocPspEmuClearWakeOnWlan(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2820,7 +2820,7 @@ int APS5_VABI sceNetConfigEtherGetLinkMode(std::uint32_t interfaceId, int* mode) return 0; } -int APS5_VABI sceNetConfigEtherPostPlugInOutEvent(void) { +int APS5_VABI sceNetConfigEtherPostPlugInOutEvent(std::uint32_t interfaceId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index ca82d1ac9b..a4c93d98ec 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -40,12 +40,7 @@ - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware -- [sceNetInetNtopWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetInetPtonWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigSetIfLinkLocalAddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigFlushRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigFlushRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpGetAutoipInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -57,11 +52,9 @@ - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocPspEmuClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocPspEmuSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -78,7 +71,6 @@ - [sceNetConfigWlanInfraScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanScan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanSetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigEtherPostPlugInOutEvent](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetBandwidthControlSetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetBandwidthControlGetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 85b192580a6211336c970853849cc4dd50c1244d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:03:06 +0300 Subject: [PATCH 0564/1360] fix(net): recover thread join and interface list signatures --- core/libs/prx/libSceNet/Export.cpp | 4 ++-- docs/dev/debt/libSceNet.md | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 80d5b41db6..5214a1b339 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2224,7 +2224,7 @@ const char* APS5_VABI sceNetGetIfName(std::uint32_t index) { return names[index]; } -int APS5_VABI sceNetGetIfnameNumList(void) { +int APS5_VABI sceNetGetIfnameNumList(std::uint32_t* interfaceIds, std::int32_t capacity, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2365,7 +2365,7 @@ int APS5_VABI sceNetThreadExit(void) { return 0; } -int APS5_VABI sceNetThreadJoin(void) { +int APS5_VABI sceNetThreadJoin(void* thread) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a4c93d98ec..6d3285577b 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -27,7 +27,6 @@ - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware -- [sceNetGetIfnameNumList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` @@ -38,7 +37,6 @@ - [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 843185a354542e187b46aabf1335316767a3f729 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:05:18 +0300 Subject: [PATCH 0565/1360] fix(json): restore integer token types and bit interpretation --- core/libs/prx/libSceJson2/Export.cpp | 11 ++++++----- core/libs/tests/GuestJson.cpp | 27 +++++++++++++++++++++++++-- docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index b14ad5ccd7..2593044dad 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,4 +1,5 @@ #include <algorithm> +#include <bit> #include <cerrno> #include <cmath> #include <cstdint> @@ -494,8 +495,8 @@ private: } else { const unsigned long long value = std::strtoull(text.c_str(), nullptr, 10); if (errno == 0) { - if (value <= static_cast<unsigned long long>(INT64_MAX)) { out.type = TypeInteger; out.integer = static_cast<std::int64_t>(value); } - else { out.type = TypeUInteger; out.uinteger = value; } + out.type = TypeUInteger; + out.uinteger = value; return true; } } @@ -882,15 +883,15 @@ ValueType APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value* self) { return No const bool* APS5_VABI _ZNK3sce4Json5Value10getBooleanEv(const Value* self) { return &Typed(self, TypeBoolean).boolean; } const std::int64_t* APS5_VABI _ZNK3sce4Json5Value10getIntegerEv(const Value* self) { Node& n = const_cast<Node&>(NodeOf(*self)); - if (n.type == TypeUInteger && n.uinteger <= static_cast<std::uint64_t>(INT64_MAX)) { - n.integer = static_cast<std::int64_t>(n.uinteger); + if (n.type == TypeUInteger) { + n.integer = std::bit_cast<std::int64_t>(n.uinteger); return &n.integer; } return &Typed(self, TypeInteger).integer; } const std::uint64_t* APS5_VABI _ZNK3sce4Json5Value11getUIntegerEv(const Value* self) { Node& n = const_cast<Node&>(NodeOf(*self)); - if (n.type == TypeInteger && n.integer >= 0) { + if (n.type == TypeInteger) { n.uinteger = static_cast<std::uint64_t>(n.integer); return &n.uinteger; } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 9193c726c7..f73d4bbe14 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -197,7 +197,7 @@ static void ParseAndRoundTrip() { Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject); Require(_ZNK3sce4Json5Value5countEv(&root) == 8); Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&Member(root, "name"))), "x\xc3\xa9\n") == 0); - Require(_ZNK3sce4Json5Value7getTypeEv(&Member(root, "count")) == TypeInteger); + Require(_ZNK3sce4Json5Value7getTypeEv(&Member(root, "count")) == TypeUInteger); Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(root, "count")) == 3); Require(*_ZNK3sce4Json5Value11getUIntegerEv(&Member(root, "count")) == 3); Require(*_ZNK3sce4Json5Value7getRealEv(&Member(root, "count")) == 3.0); @@ -228,7 +228,7 @@ static void ParseAndRoundTrip() { types.push_back(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Array8iteratordeEv(&it))); _ZN3sce4Json5Array8iteratorD1Ev(&it); _ZN3sce4Json5Array8iteratorD1Ev(&end); - Require((types == std::vector<std::int32_t>{TypeInteger, TypeString, TypeArray})); + Require((types == std::vector<std::int32_t>{TypeUInteger, TypeString, TypeArray})); Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5ValueixEm(_ZNK3sce4Json5ValueixEm(&list, 2), 0)) == 3); const std::string serialized = Serialize(root); @@ -1053,6 +1053,28 @@ static int APS5_VABI ReadNext(char& ch, void* context) { return state->successStatus; } +static void IntegerRepresentation() { + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + for (const char* text : {"0", "1", "9223372036854775807", "9223372036854775808", "18446744073709551615"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)) == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeUInteger); + Require(Serialize(value) == text); + } + Require(*_ZNK3sce4Json5Value10getIntegerEv(&value) == -1); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeUInteger); + Require(_ZN3sce4Json5Value3setEl(&value, -1) == 0); + Require(*_ZNK3sce4Json5Value11getUIntegerEv(&value) == std::numeric_limits<std::uint64_t>::max()); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeInteger); + for (const char* text : {"-0", "-1", "-9223372036854775808"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)) == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeInteger); + } + Require(*_ZNK3sce4Json5Value11getUIntegerEv(&value) == (std::uint64_t{1} << 63)); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeInteger); + _ZN3sce4Json5ValueD1Ev(&value); +} + static void StreamParse() { Value root{}; _ZN3sce4Json5ValueC1Ev(&root); @@ -1346,5 +1368,6 @@ int main() { InitParameters(); ValueClear(); ValueOperatorBool(); + IntegerRepresentation(); StreamParse(); } diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 897a310ef2..6aef517355 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Positive integer tokens use the signed type when they fit; the system parser uses the unsigned type. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser +- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types From 5cd83484897a6b02a78e16d05565b6a776f9ed8c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:08:00 +0300 Subject: [PATCH 0566/1360] fix(ime): recover clause conversion argument width --- core/libs/prx/libSceIme/Export.cpp | 2 +- docs/dev/debt/libSceIme.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 3d8735dc88..5ab0ae9f52 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -514,7 +514,7 @@ int APS5_VABI sceImeSetCaret(const Caret* caret) { return 0; } -int APS5_VABI sceImeSetConsecutiveClauseConversionEnabled(bool enabled) { +int APS5_VABI sceImeSetConsecutiveClauseConversionEnabled(std::uint8_t enabled) { (void)enabled; NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 16d8f84e8e..4e6a62319d 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -6,8 +6,7 @@ ### Unknown function info -- [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown -- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title +- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature From b1bcbf7995743af5bab57852df1af5d07d16100d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:10:36 +0300 Subject: [PATCH 0567/1360] fix(psml): recover shared memory requirements and release signatures --- core/libs/prx/libScePsml_debug/Export.cpp | 17 +++++++++-------- core/libs/tests/GuestPsml.cpp | 9 +++++---- docs/dev/debt/libScePsml_debug.md | 5 ++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 448d0c8a60..1b8babf0fc 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -19,6 +19,7 @@ constexpr std::int32_t SCE_PSML_ERROR_NULL_OUTPUT = static_cast<std::int32_t>(0x constexpr std::uint64_t MfsrBlockBytes = 0x200000; constexpr std::uint64_t MfsrBlockCount = 4; constexpr std::uint32_t MfsrDispatchPacketDwords = 0x600; +constexpr std::uint64_t MfsrSharedBlockCount = 52; struct MfsrRequirement { std::uint64_t blockBytes; @@ -78,10 +79,10 @@ void RequireMfsr(bool condition, const char* function, const char* what) { if (!condition) throw std::runtime_error(std::string(function) + ": " + what); } -void RequireBlocks(const MfsrBlock* blocks, std::uint64_t blockCount, const char* function) { - RequireMfsr(blockCount >= MfsrBlockCount, function, "fewer memory blocks than the requirement"); +void RequireBlocks(const MfsrBlock* blocks, std::uint64_t blockCount, std::uint64_t requiredCount, const char* function) { + RequireMfsr(blockCount >= requiredCount, function, "fewer memory blocks than the requirement"); RequireMfsr(blocks != nullptr, function, "null memory block list"); - for (std::uint64_t i = 0; i < MfsrBlockCount; ++i) { + for (std::uint64_t i = 0; i < requiredCount; ++i) { RequireMfsr(blocks[i].bytes >= MfsrBlockBytes && blocks[i].address % MfsrBlockBytes == 0, function, "memory block smaller or less aligned than the requirement"); } } @@ -162,7 +163,7 @@ std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(MfsrRequirem if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); - *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrSharedBlockCount}; return 0; } @@ -170,7 +171,7 @@ std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; RequireMfsr(sharedResources != nullptr && param != nullptr, __func__, "null handle or parameter"); RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); - RequireBlocks(param->blocks, param->blockCount, __func__); + RequireBlocks(param->blocks, param->blockCount, MfsrSharedBlockCount, __func__); *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr, __func__); return 0; } @@ -191,7 +192,7 @@ std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(MfsrRequire std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const MfsrContextParam* param) { if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; RequireMfsr(context != nullptr && param != nullptr, __func__, "null handle or parameter"); - RequireBlocks(param->blocks, param->blockCount, __func__); + RequireBlocks(param->blocks, param->blockCount, MfsrBlockCount, __func__); const auto [inputWidth, inputHeight] = MfsrInputDimensions(param); *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources, __func__, inputWidth, inputHeight); return 0; @@ -274,7 +275,7 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsr2ReleaseSharedResources() { +int APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -289,7 +290,7 @@ int APS5_VABI scePsmlMfsr2GetContextInitRequirement() { return 0; } -int APS5_VABI scePsmlMfsr2ReleaseContext() { +int APS5_VABI scePsmlMfsr2ReleaseContext(void* context) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index f82ec1744a..882a4b5d04 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -5,6 +5,7 @@ #include <cstring> #include <stdexcept> #include <string> +#include <vector> extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param); @@ -116,8 +117,8 @@ struct ContextParam { } }; -std::array<Block, 4> Blocks(const Requirement& requirement, std::uint64_t first) { - std::array<Block, 4> blocks{}; +std::vector<Block> Blocks(const Requirement& requirement, std::uint64_t first) { + std::vector<Block> blocks(requirement.blockCount); for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = {first + i * requirement.blockBytes, requirement.blockBytes}; return blocks; } @@ -153,7 +154,7 @@ void SetupAfterInit() { SharedParam sharedParam{}; Requirement shared{}; Require(scePsmlMfsrGetSharedResourcesInitRequirement(&shared, &sharedParam) == 0); - Require(shared.blockBytes == 0x200000 && shared.alignment == 0x200000 && shared.blockCount == 4); + Require(shared.blockBytes == 0x200000 && shared.alignment == 0x200000 && shared.blockCount == 52); const auto sharedBlocks = Blocks(shared, 0x10000000); sharedParam.blocks = sharedBlocks.data(); @@ -239,7 +240,7 @@ void RefusedAfterInit() { Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == 0); const auto blocks = Blocks(requirement, 0x10000000); sharedParam.blocks = blocks.data(); - sharedParam.blockCount = 3; + sharedParam.blockCount = 51; void* sharedResources = nullptr; Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "fewer memory blocks")); auto small = blocks; diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index a4cd5f3070..af02ca6976 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,12 +2,11 @@ ### Unknown function info -- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The descriptor pointer at 0x28 is not read. The requirement of four 2 MiB blocks, 2 MiB aligned, is what PPSA08330 v06.006 allocates regardless of the returned count; Sony's values are unknown ([KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libPsml.cpp) derives 52 blocks for type 0; its layout differs from these calls: it writes a 0x30-byte object through the first argument, where PPSA08330 passes one of two adjacent 8-byte handle globals, and reads the shared resources at context parameter offset 8, where both titles pass the block list). Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw -- [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The descriptor pointer at 0x28 is not read. The context requirement still reports four 2 MiB blocks regardless of its parameters; the system calculation depends on the context configuration. Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw +- [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) -- [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K) - imported by PPSA01317, which fails to load without them; signatures unknown, so they throw. The names are not in the aerolib NID list; they hash to the imported NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw` (`tools/nid_names.py`) ### Functional From 69095fd9107cc514be7f3aa824cbd96ca08d1ece Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:12:28 +0300 Subject: [PATCH 0568/1360] fix(psml): recover shared resource parameter layouts --- core/libs/prx/libScePsml_debug/Export.cpp | 20 ++++++++++++++++++-- docs/dev/debt/libScePsml_debug.md | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 1b8babf0fc..524e34549c 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -40,6 +40,22 @@ struct MfsrSharedResourcesParam { std::uint64_t reserved2; }; +struct Mfsr2SharedResourcesParam { + std::uint32_t type; + std::uint32_t reserved0; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint16_t width; + std::uint16_t height; + std::uint32_t reserved1; +}; + +static_assert(sizeof(Mfsr2SharedResourcesParam) == 32); +static_assert(offsetof(Mfsr2SharedResourcesParam, blocks) == 8); +static_assert(offsetof(Mfsr2SharedResourcesParam, blockCount) == 16); +static_assert(offsetof(Mfsr2SharedResourcesParam, width) == 24); +static_assert(offsetof(Mfsr2SharedResourcesParam, reserved1) == 28); + struct MfsrContextParam { std::uint32_t reserved0[2]; const MfsrBlock* blocks; @@ -280,7 +296,7 @@ int APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { return 0; } -int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement() { +int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -300,7 +316,7 @@ int APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords() { return 0; } -int APS5_VABI scePsmlMfsr2CreateSharedResources() { +int APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index af02ca6976..e7c6c48729 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -3,7 +3,7 @@ ### Unknown function info - [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The descriptor pointer at 0x28 is not read. The context requirement still reports four 2 MiB blocks regardless of its parameters; the system calculation depends on the context configuration. Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw -- [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters +- [scePsmlMfsr2CreateContext, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) From 979fa6208639d4e977b494a082d7cabb0cc167ef Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:12:58 +0300 Subject: [PATCH 0569/1360] docs(psml): narrow the remaining resource setup debt --- docs/dev/debt/libScePsml_debug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index e7c6c48729..5b0a539687 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,7 +2,7 @@ ### Unknown function info -- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The descriptor pointer at 0x28 is not read. The context requirement still reports four 2 MiB blocks regardless of its parameters; the system calculation depends on the context configuration. Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only resource type 0 is supported. The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. - [scePsmlMfsr2CreateContext, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source From 7ede27cc9c88f6466dfd8b96ba60c819aa20507d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:16:57 +0300 Subject: [PATCH 0570/1360] fix(hmd): recover sensor calibration buffer signature --- core/libs/prx/libSceHmd/Export.cpp | 2 +- docs/dev/debt/libSceHmd.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index eb46e0aeef..fbdfeacca6 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -369,7 +369,7 @@ bool APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { return 0; } -int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void) { +int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void* buffer, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 937a332046..73cc19fd6c 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -23,7 +23,6 @@ - [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStopCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalMmapGetSensorCalibrationData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 4cb4905c29bb461b27492fcc8cdc0a4acf682827 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:17:02 +0300 Subject: [PATCH 0571/1360] fix(netctl): recover connection test result signature --- core/libs/prx/libSceNetCtl/Export.cpp | 2 +- docs/dev/debt/libSceNetCtl.md | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 6c6663967f..03167d8863 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -860,7 +860,7 @@ int APS5_VABI sceNetBweStartInternetConnectionTestBandwidthTestIpcInt(void) { return 0; } -int APS5_VABI sceNetBweSetInternetConnectionTestResultIpcInt(void) { +int APS5_VABI sceNetBweSetInternetConnectionTestResultIpcInt(std::int32_t firstResult, std::int32_t secondResult) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 3f5f367476..dbb3c48613 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -32,9 +32,7 @@ - [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRestart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweSetInternetConnectionTestResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlGetWifiInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature ### Functional From 0e1ddf3bdf97c053ce10638790ed54b9cae02f94 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:17:06 +0300 Subject: [PATCH 0572/1360] fix(json): preserve value storage when swapping contents --- core/libs/prx/libSceJson2/Export.cpp | 11 ++++++++++- core/libs/tests/GuestJson.cpp | 8 ++++++++ core/libs/tests/JsonCopyFailure.cpp | 29 +++++++++++++++++++++++++++- docs/dev/debt/libSceJson2.md | 2 +- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 2593044dad..7d0dc664c6 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -837,7 +837,16 @@ Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value* self, const Value* other) { return self; } void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value* self, Value* other) { - std::swap(self->node, other->node); + Node temporary{}; + try { + AssignNode(temporary, NodeOf(*self)); + AssignNode(NodeOf(*self), NodeOf(*other)); + AssignNode(NodeOf(*other), temporary); + } catch (...) { + Clear(temporary); + throw; + } + Clear(temporary); } int APS5_VABI _ZN3sce4Json5Value3setENS0_9ValueTypeE(Value* self, ValueType type) { SetType(NodeOf(*self), type); return 0; } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index f73d4bbe14..da59aa435e 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -714,6 +714,14 @@ static void ValueSwap() { _ZN3sce4Json5Value4swapERS1_(&v1, &v2); Require(*_ZNK3sce4Json5Value10getIntegerEv(&v2) == 123); Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&v1)), "hello") == 0); + Require(_ZN3sce4Json5Value3setEl(&v1, -456) == 0); + const auto* firstStorage = _ZNK3sce4Json5Value10getIntegerEv(&v1); + const auto* secondStorage = _ZNK3sce4Json5Value10getIntegerEv(&v2); + _ZN3sce4Json5Value4swapERS1_(&v1, &v2); + Require(_ZNK3sce4Json5Value10getIntegerEv(&v1) == firstStorage && *firstStorage == 123); + Require(_ZNK3sce4Json5Value10getIntegerEv(&v2) == secondStorage && *secondStorage == -456); + _ZN3sce4Json5Value4swapERS1_(&v1, &v1); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&v1) == 123); _ZN3sce4Json5ValueD1Ev(&v1); _ZN3sce4Json5ValueD1Ev(&v2); } diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp index 0fa28bad98..9707f85913 100644 --- a/core/libs/tests/JsonCopyFailure.cpp +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -13,6 +13,7 @@ struct Object { void* items; }; struct Pair { String key; std::uint64_t reserved; Value value; }; extern "C" { +void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value*, Value*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1EPKc(Value*, const char*); void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6StringE(Value*, const String*); @@ -212,9 +213,35 @@ int main() { } Require(succeeded, "copy never succeeded"); } + activeCase = "value swap"; + bool swapSucceeded = false; + for (activeBudget = 0; activeBudget < 256 && !swapSucceeded; ++activeBudget) { + const auto before = liveAllocations; + Value first{}, second{}; + _ZN3sce4Json5ValueC1ERKS1_(&first, &objectValue); + _ZN3sce4Json5ValueC1ERKS1_(&second, &arrayValue); + allocationBudget = activeBudget; + try { + _ZN3sce4Json5Value4swapERS1_(&first, &second); + swapSucceeded = true; + } catch (const std::bad_alloc&) { + ++failures; + } + allocationBudget = -1; + if (swapSucceeded) { + CheckValue(first, arrayJson); + CheckValue(second, objectJson); + } + _ZN3sce4Json5ValueD1Ev(&first); + _ZN3sce4Json5ValueD1Ev(&second); + Require(liveAllocations == before, "swap leaked allocations"); + CheckValue(objectValue, objectJson); + CheckValue(arrayValue, arrayJson); + } + Require(swapSucceeded, "swap never succeeded"); _ZN3sce4Json6StringD1Ev(&text); _ZN3sce4Json5ValueD1Ev(&arrayValue); _ZN3sce4Json5ValueD1Ev(&objectValue); Require(liveAllocations == initial, "source cleanup leaked allocations"); - std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]), failures); + std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]) + 1, failures); } diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 6aef517355..40f236efab 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -7,5 +7,5 @@ - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown -- [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified +- [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. From ce03b9c68a75eb356112564e113b0b2889211127 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:22:09 +0300 Subject: [PATCH 0573/1360] fix(json): restore array iterator lifetime and end handling --- core/libs/prx/libSceJson2/Export.cpp | 57 +++++++++++++++++++++------- core/libs/tests/GuestJson.cpp | 16 +++++++- docs/dev/debt/libSceJson2.md | 5 ++- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 7d0dc664c6..369597ce8f 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -34,7 +34,11 @@ struct String { std::string* text; }; struct Array { std::list<Value>* items; }; struct Pair; struct Object { std::list<Pair>* items; }; -struct ArrayIterator { std::list<Value>::iterator it; }; +struct ArrayIteratorState { + std::list<Value>::iterator position; + std::list<Value>::iterator end; +}; +struct ArrayIterator { ArrayIteratorState* state; }; struct ObjectIterator { std::list<Pair>::iterator it; }; struct Pair { String key; @@ -726,8 +730,8 @@ void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Val throw; } } -ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, const Array* self) { result->it = self->items->begin(); return result; } -ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->it = self->items->end(); return result; } +ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, const Array* self) { result->state = new ArrayIteratorState{self->items->begin(), self->items->end()}; return result; } +ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->state = new ArrayIteratorState{self->items->end(), self->items->end()}; return result; } bool APS5_VABI _ZNK3sce4Json5Array5emptyEv(const Array* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array* self) { return self->items->size(); } void APS5_VABI _ZN3sce4Json5Array5clearEv(Array* self) { @@ -742,14 +746,32 @@ const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { if (self->items->empty()) throw std::out_of_range("sce::Json::Array::back: empty array"); return &self->items->back(); } -void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { (void)self; } -ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { ++self->it; return self; } -Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { return &*self->it; } -Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return &*self->it; } -bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { return self->it != other->it; } -void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(ArrayIterator* self, const ArrayIterator* other) { self->it = other->it; } +void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { delete self->state; self->state = nullptr; } +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { + if (self->state && self->state->position != self->state->end) ++self->state->position; + return self; +} +Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { + if (!self->state || self->state->position == self->state->end) throw std::out_of_range("sce::Json::Array::iterator: no current element"); + return &*self->state->position; +} +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return _ZNK3sce4Json5Array8iteratordeEv(self); } +bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { + if (!self->state || !other->state) return false; + const Value* left = self->state && self->state->position != self->state->end ? &*self->state->position : nullptr; + const Value* right = other->state && other->state->position != other->state->end ? &*other->state->position : nullptr; + return left != right; +} +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(ArrayIterator* self, const ArrayIterator* other) { + self->state = other->state ? new ArrayIteratorState(*other->state) : nullptr; +} ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(ArrayIterator* self, std::size_t count) { - std::advance(self->it, count); + if (self->state) { + while (count && self->state->position != self->state->end) { + ++self->state->position; + --count; + } + } return self; } @@ -1160,18 +1182,25 @@ ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator } ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEi(ArrayIterator* result, ArrayIterator* self, int) { - result->it = self->it; - ++self->it; + _ZN3sce4Json5Array8iteratorC1ERKS2_(result, self); + _ZN3sce4Json5Array8iteratorppEv(self); return result; } ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_(ArrayIterator* self, const ArrayIterator* other) { - self->it = other->it; + if (self->state) { + if (other->state && other->state->position != other->state->end) { + *self->state = *other->state; + } else { + delete self->state; + self->state = nullptr; + } + } return self; } void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(ArrayIterator* self) { - self->it = {}; + self->state = new ArrayIteratorState{}; } ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(ObjectIterator* result, ObjectIterator* self, int) { diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index da59aa435e..3baa092693 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -400,7 +400,7 @@ static void ArrayIteratorCopies() { Iterator it{}; std::memset(&it, 0x5a, sizeof(it)); _ZN3sce4Json5Array8iteratorC1Ev(&it); - Require(it.position == nullptr); + Require(it.position != nullptr); Iterator begin{}, end{}; _ZNK3sce4Json5Array5beginEv(&begin, &array); @@ -411,6 +411,7 @@ static void ArrayIteratorCopies() { std::vector<std::int64_t> seen; Iterator previous{}; while (_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end)) { + _ZN3sce4Json5Array8iteratorD1Ev(&previous); Require(_ZN3sce4Json5Array8iteratorppEi(&previous, &it, 0) == &previous); seen.push_back(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(&previous))); Require(_ZNK3sce4Json5Array8iteratorneERKS2_(&previous, &it)); @@ -418,6 +419,11 @@ static void ArrayIteratorCopies() { Require((seen == std::vector<std::int64_t>{1, 2, 3})); Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end)); Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(&previous)) == 3); + Require(_ZN3sce4Json5Array8iteratoraSERKS2_(&it, &end) == &it); + Require(it.position == nullptr); + Require(_ZN3sce4Json5Array8iteratoraSERKS2_(&it, &begin) == &it); + Require(it.position == nullptr); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &begin)); _ZN3sce4Json5Array8iteratorD1Ev(&previous); _ZN3sce4Json5Array8iteratorD1Ev(&it); @@ -742,6 +748,14 @@ static void ArrayIteratorAdvance() { _ZN3sce4Json5Array8iteratorC1ERKS2_(©, &begin); Require(_ZN3sce4Json5Array8iterator7advanceEm(©, 3) == ©); Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(©)) == 30); + Iterator end{}; + _ZNK3sce4Json5Array3endEv(&end, &array); + _ZN3sce4Json5Array8iterator7advanceEm(©, std::numeric_limits<std::size_t>::max()); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(©, &end)); + _ZN3sce4Json5Array8iteratorppEv(©); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(©, &end)); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(&begin)) == 0); + _ZN3sce4Json5Array8iteratorD1Ev(&end); _ZN3sce4Json5Array8iteratorD1Ev(&begin); _ZN3sce4Json5Array8iteratorD1Ev(©); _ZN3sce4Json5ArrayD1Ev(&array); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 40f236efab..cadf462424 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -4,8 +4,9 @@ - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. -- [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular +- [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed object iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown -- [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified +- [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - storage uses `std::string`; capacity and allocation growth do not match the system 32-bit capacity and allocator settings. Appending null text with a nonzero length throws instead of dereferencing it. +- [sce::Json::Array::iterator::advance and Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return type of `advance` remains unverified. Object iterator advancement has no end check, while the system asserts on advancing past the end. Dereferencing an empty array iterator throws instead of returning the system null-value pointer or an invalid address. Object iterators still use host iterator storage and lifetime semantics. - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. From b19f2227df8f0a49d2e146fea482a2608e5d5fc7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:29:41 +0300 Subject: [PATCH 0574/1360] fix(json): restore object iterator ownership and bounds --- core/libs/prx/libSceJson2/Export.cpp | 74 +++++++++++++++++++++------- core/libs/tests/GuestJson.cpp | 14 +++++- core/libs/tests/JsonCopyFailure.cpp | 37 +++++++++++++- docs/dev/debt/libSceJson2.md | 4 +- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 369597ce8f..5b92c48d68 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -8,6 +8,7 @@ #include <cstring> #include <list> #include <mutex> +#include <memory> #include <stdexcept> #include <string> #include "SceTypes.hpp" @@ -39,7 +40,11 @@ struct ArrayIteratorState { std::list<Value>::iterator end; }; struct ArrayIterator { ArrayIteratorState* state; }; -struct ObjectIterator { std::list<Pair>::iterator it; }; +struct ObjectIteratorState { + std::list<Pair>* owner = nullptr; + std::list<Pair>::iterator position{}; +}; +struct ObjectIterator { ObjectIteratorState* state; }; struct Pair { String key; std::uint64_t reserved; @@ -794,21 +799,44 @@ void APS5_VABI _ZN3sce4Json6Object5clearEv(Object* self) { self->items->clear(); } Value* APS5_VABI _ZN3sce4Json6ObjectixERKNS0_6StringE(Object* self, const String* key) { return &ObjectEntry(*self, *key->text); } -ObjectIterator* APS5_VABI _ZNK3sce4Json6Object5beginEv(ObjectIterator* result, const Object* self) { result->it = self->items->begin(); return result; } -ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, const Object* self) { result->it = self->items->end(); return result; } -void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { (void)self; } -ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { ++self->it; return self; } -Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } -Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return &*self->it; } +ObjectIterator* APS5_VABI _ZNK3sce4Json6Object5beginEv(ObjectIterator* result, const Object* self) { result->state = new ObjectIteratorState{self->items, self->items->begin()}; return result; } +ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, const Object* self) { result->state = new ObjectIteratorState{self->items, self->items->end()}; return result; } +void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { delete self->state; self->state = nullptr; } +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { + if (self->state && self->state->owner) { + if (self->state->position == self->state->owner->end()) throw std::out_of_range("sce::Json::Object::iterator: advance past the end"); + ++self->state->position; + } + return self; +} +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { + if (!self->state || !self->state->owner || self->state->position == self->state->owner->end()) throw std::out_of_range("sce::Json::Object::iterator: no current element"); + return &*self->state->position; +} +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return _ZNK3sce4Json6Object8iteratordeEv(self); } ObjectIterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(ObjectIterator* result, const Object* self, const String* key) { - result->it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0; }); + const auto position = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0; }); + result->state = new ObjectIteratorState{self->items, position}; return result; } -bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } -bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it == other->it; } -void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(ObjectIterator* self, const ObjectIterator* other) { self->it = other->it; } +bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const ObjectIterator* self, const ObjectIterator* other) { + if (!self->state || !other->state) return false; + return self->state->owner == other->state->owner && self->state->position == other->state->position; +} +bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { + if (!self->state || !other->state) return false; + return !_ZNK3sce4Json6Object8iteratoreqERKS2_(self, other); +} +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(ObjectIterator* self, const ObjectIterator* other) { + self->state = other->state ? new ObjectIteratorState(*other->state) : nullptr; +} ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(ObjectIterator* self, std::size_t count) { - std::advance(self->it, count); + if (self->state && self->state->owner) { + while (count) { + _ZN3sce4Json6Object8iteratorppEv(self); + --count; + } + } return self; } bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->items->empty(); } @@ -1163,8 +1191,9 @@ void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair* self) { ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator* result, Object* self, const Pair* pair) { auto it = ObjectPosition(*self, *pair->key.text); + auto state = std::make_unique<ObjectIteratorState>(ObjectIteratorState{self->items, it}); if (it != self->items->end() && std::strcmp(it->key.text->c_str(), pair->key.text->c_str()) == 0) { - result->it = it; + result->state = state.release(); return result; } Pair copy{}; @@ -1172,7 +1201,8 @@ ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator Construct(copy.key, *pair->key.text); try { CopyInto(copy.value, pair->value); - result->it = self->items->insert(it, copy); + state->position = self->items->insert(it, copy); + result->state = state.release(); return result; } catch (...) { Destroy(copy.value); @@ -1204,18 +1234,26 @@ void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(ArrayIterator* self) { } ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(ObjectIterator* result, ObjectIterator* self, int) { - result->it = self->it; - ++self->it; + auto state = self->state ? std::make_unique<ObjectIteratorState>(*self->state) : nullptr; + _ZN3sce4Json6Object8iteratorppEv(self); + result->state = state.release(); return result; } ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratoraSERKS2_(ObjectIterator* self, const ObjectIterator* other) { - self->it = other->it; + if (self->state) { + if (other->state && other->state->owner) { + *self->state = *other->state; + } else { + delete self->state; + self->state = nullptr; + } + } return self; } void APS5_VABI _ZN3sce4Json6Object8iteratorC1Ev(ObjectIterator* self) { - self->it = {}; + self->state = new ObjectIteratorState{}; } void APS5_VABI _ZN3sce4Json6Object4PairC1Ev(Pair* self) { diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 3baa092693..c20ffcbd83 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -457,6 +457,7 @@ static void ObjectFind() { Value* second = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &beta); Require(_ZN3sce4Json5Value3setEPKc(second, "b") == 0); _ZNK3sce4Json6Object3endEv(&end, &object); + _ZN3sce4Json6Object8iteratorD1Ev(&found); Iterator* returned = _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &beta); Require(returned == &found); Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); @@ -466,6 +467,7 @@ static void ObjectFind() { Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair->key), "beta") == 0); _ZN3sce4Json6Object8iteratorppEv(&found); Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&found); _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &gamma); Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); @@ -512,7 +514,8 @@ static void ObjectIteratorCopies() { Iterator it{}; std::memset(&it, 0x5a, sizeof(it)); _ZN3sce4Json6Object8iteratorC1Ev(&it); - Require(it.position == nullptr); + Require(it.position != nullptr); + _ZN3sce4Json6Object8iteratorD1Ev(&it); Iterator begin{}, end{}; _ZNK3sce4Json6Object5beginEv(&begin, &object); @@ -528,6 +531,7 @@ static void ObjectIteratorCopies() { _ZN3sce4Json6Object8iteratorD1Ev(&post); Iterator assigned{}; + _ZN3sce4Json6Object8iteratorC1Ev(&assigned); Require(_ZN3sce4Json6Object8iteratoraSERKS2_(&assigned, &begin) == &assigned); Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&assigned, &begin)); Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&assigned, &begin)); @@ -793,6 +797,14 @@ static void ObjectOperations() { _ZN3sce4Json6Object8iteratorC1ERKS2_(©, &begin); Require(_ZN3sce4Json6Object8iterator7advanceEm(©, 2) == ©); Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(©)->key), "gamma") == 0); + _ZN3sce4Json6Object8iterator7advanceEm(©, 1); + bool endRejected = false; + try { + _ZN3sce4Json6Object8iterator7advanceEm(©, 1); + } catch (const std::out_of_range&) { + endRejected = true; + } + Require(endRejected); _ZN3sce4Json6Object8iteratorD1Ev(&begin); _ZN3sce4Json6Object8iteratorD1Ev(©); diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp index 9707f85913..6f95b142e3 100644 --- a/core/libs/tests/JsonCopyFailure.cpp +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -5,14 +5,22 @@ #include <cstdlib> #include <cstring> #include <new> +#include <initializer_list> struct Value { void* node; }; struct String { void* text; }; struct Array { void* items; }; struct Object { void* items; }; +struct Iterator { void* state; }; struct Pair { String key; std::uint64_t reserved; Value value; }; extern "C" { +Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); +Iterator* APS5_VABI _ZNK3sce4Json6Object5beginEv(Iterator*, const Object*); +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(Iterator*, const Iterator*); +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(Iterator*, const Iterator*); +void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(Iterator*); +void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(Iterator*); void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value*, Value*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1EPKc(Value*, const char*); @@ -213,6 +221,33 @@ int main() { } Require(succeeded, "copy never succeeded"); } + for (bool objectIterator : {false, true}) { + activeCase = objectIterator ? "object iterator copy" : "array iterator copy"; + const auto before = liveAllocations; + Iterator source{}; + if (objectIterator) _ZNK3sce4Json6Object5beginEv(&source, object); + else _ZNK3sce4Json5Array5beginEv(&source, array); + for (activeBudget = 0; activeBudget <= 1; ++activeBudget) { + Iterator copy{}; + bool succeeded = false; + allocationBudget = activeBudget; + try { + if (objectIterator) _ZN3sce4Json6Object8iteratorC1ERKS2_(©, &source); + else _ZN3sce4Json5Array8iteratorC1ERKS2_(©, &source); + succeeded = true; + } catch (const std::bad_alloc&) { + ++failures; + } + allocationBudget = -1; + Require(succeeded == (activeBudget == 1), "iterator allocation failure ignored"); + if (succeeded) Require(copy.state != source.state, "iterator copy shares owned storage"); + if (objectIterator) _ZN3sce4Json6Object8iteratorD1Ev(©); + else _ZN3sce4Json5Array8iteratorD1Ev(©); + } + if (objectIterator) _ZN3sce4Json6Object8iteratorD1Ev(&source); + else _ZN3sce4Json5Array8iteratorD1Ev(&source); + Require(liveAllocations == before, "iterator copy leaked allocations"); + } activeCase = "value swap"; bool swapSucceeded = false; for (activeBudget = 0; activeBudget < 256 && !swapSucceeded; ++activeBudget) { @@ -243,5 +278,5 @@ int main() { _ZN3sce4Json5ValueD1Ev(&arrayValue); _ZN3sce4Json5ValueD1Ev(&objectValue); Require(liveAllocations == initial, "source cleanup leaked allocations"); - std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]) + 1, failures); + std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]) + 3, failures); } diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index cadf462424..4868db14d0 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -4,9 +4,9 @@ - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. -- [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed object iterators are singular +- [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - storage uses `std::string`; capacity and allocation growth do not match the system 32-bit capacity and allocator settings. Appending null text with a nonzero length throws instead of dereferencing it. -- [sce::Json::Array::iterator::advance and Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return type of `advance` remains unverified. Object iterator advancement has no end check, while the system asserts on advancing past the end. Dereferencing an empty array iterator throws instead of returning the system null-value pointer or an invalid address. Object iterators still use host iterator storage and lifetime semantics. +- [sce::Json::Array::iterator::advance and Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return type of `advance` remains unverified. Object iterator advancement past the end throws instead of asserting. Dereferencing an empty array iterator throws instead of returning the system null-value pointer or an invalid address. - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. From daaa00c99140578048562642f33033030f12d7ba Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:33:40 +0300 Subject: [PATCH 0575/1360] fix(psml): restore mfsr2 context and dispatch signatures --- core/libs/prx/libScePsml_debug/Export.cpp | 67 ++++++++++++++++++++++- docs/dev/debt/libScePsml_debug.md | 2 +- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 524e34549c..44021762c2 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -56,6 +56,67 @@ static_assert(offsetof(Mfsr2SharedResourcesParam, blockCount) == 16); static_assert(offsetof(Mfsr2SharedResourcesParam, width) == 24); static_assert(offsetof(Mfsr2SharedResourcesParam, reserved1) == 28); +struct Mfsr2ContextParam { + std::uint32_t profile; + std::uint32_t reserved0; + void* sharedResources; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint64_t reserved1; + std::uint16_t outputWidth; + std::uint16_t outputHeight; + std::uint32_t inputColorSpace; +}; + +struct MfsrCommandBuffer { + std::uint32_t* cursor; + std::uint32_t sizeInDwords; +}; + +struct MfsrTextureDescriptor { + std::uint32_t words[8]; +}; + +struct Mfsr2DispatchParam { + const MfsrTextureDescriptor* outputColor; + const MfsrTextureDescriptor* color; + const MfsrTextureDescriptor* motionVectors; + const MfsrTextureDescriptor* depth; + const MfsrTextureDescriptor* exposure; + std::uint64_t reserved0; + float preExposure; + std::uint32_t renderX; + std::uint32_t renderY; + std::uint32_t renderWidth; + std::uint32_t renderHeight; + float jitter[2]; + float motionVectorScale[2]; + float motionVectorOffset[2]; + float depthRange[2]; + std::uint8_t resetHistory; + std::uint8_t reserved1[3]; + std::uint32_t optionFlags; + float sharpness; +}; + +static_assert(sizeof(Mfsr2ContextParam) == 48); +static_assert(offsetof(Mfsr2ContextParam, sharedResources) == 8); +static_assert(offsetof(Mfsr2ContextParam, blocks) == 16); +static_assert(offsetof(Mfsr2ContextParam, blockCount) == 24); +static_assert(offsetof(Mfsr2ContextParam, outputWidth) == 40); +static_assert(offsetof(Mfsr2ContextParam, inputColorSpace) == 44); +static_assert(sizeof(MfsrCommandBuffer) == 16); +static_assert(offsetof(MfsrCommandBuffer, sizeInDwords) == 8); +static_assert(sizeof(MfsrTextureDescriptor) == 32); +static_assert(sizeof(Mfsr2DispatchParam) == 112); +static_assert(offsetof(Mfsr2DispatchParam, preExposure) == 48); +static_assert(offsetof(Mfsr2DispatchParam, renderX) == 52); +static_assert(offsetof(Mfsr2DispatchParam, jitter) == 68); +static_assert(offsetof(Mfsr2DispatchParam, depthRange) == 92); +static_assert(offsetof(Mfsr2DispatchParam, resetHistory) == 100); +static_assert(offsetof(Mfsr2DispatchParam, optionFlags) == 104); +static_assert(offsetof(Mfsr2DispatchParam, sharpness) == 108); + struct MfsrContextParam { std::uint32_t reserved0[2]; const MfsrBlock* blocks; @@ -301,7 +362,7 @@ int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* req return 0; } -int APS5_VABI scePsmlMfsr2GetContextInitRequirement() { +int APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -321,12 +382,12 @@ int APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mf return 0; } -int APS5_VABI scePsmlMfsr2GetDispatchPackets() { +int APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsr2CreateContext() { +int APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 5b0a539687..f77c32d0b1 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -3,7 +3,7 @@ ### Unknown function info - [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only resource type 0 is supported. The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. -- [scePsmlMfsr2CreateContext, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters +- [scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - writes a 32-bit size through the second argument; the unused first argument still needs caller verification. - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) From 0f6161f885f3728f614dfc2ba9861e47fa53fdd0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:35:13 +0300 Subject: [PATCH 0576/1360] fix(net): enforce initialization for sleep and random calls --- core/libs/prx/libSceNet/Export.cpp | 6 +++++- core/libs/tests/GuestSceNet.cpp | 6 ++++++ docs/dev/debt/libSceNet.md | 5 +---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 5214a1b339..36704406eb 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2235,6 +2235,7 @@ int APS5_VABI sceNetGetNameToIndex(std::uint32_t interfaceId) { } int APS5_VABI sceNetGetRandom(std::uint32_t* output) { + if (!g_net_inited) return fail(NET_ENOTINIT); if (output == nullptr) return fail(NET_EINVAL); static std::mutex mutex; static std::random_device device; @@ -2371,8 +2372,11 @@ int APS5_VABI sceNetThreadJoin(void* thread) { } int APS5_VABI sceNetUsleep(int microseconds) { + if (!g_net_inited) return fail(NET_ENOTINIT); if (microseconds < 0) return fail(NET_EINVAL); - return sceKernelUsleep_nid_postfix(static_cast<KernelUseconds>(microseconds)); + const int result = sceKernelUsleep_nid_postfix(static_cast<KernelUseconds>(microseconds)); + if (result < 0) set_errno((result & 0xFF00) == 0x100 ? result & 0xFF : 205); + return result; } int APS5_VABI sceNetShowIfconfigForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 52b2c027b0..713a2c0186 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1443,6 +1443,12 @@ int main() { Require(sceNetClearDnsCache(flags) == static_cast<int>(0x804101C8)); Require(*sceNetErrnoLoc() == 200); } + std::uint32_t randomOutput = 0x12345678; + Require(Failed(sceNetGetRandom(&randomOutput), 200)); + Require(randomOutput == 0x12345678); + Require(Failed(sceNetGetRandom(nullptr), 200)); + Require(Failed(sceNetUsleep(0), 200)); + Require(Failed(sceNetUsleep(-1), 200)); Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); CheckSocketHostErrors(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 6d3285577b..7f908bd380 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -26,10 +26,8 @@ - [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware -- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware +- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` - [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. @@ -37,7 +35,6 @@ - [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 1a214787cea00f9025c38364dd09b11a9fcac3c3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:37:26 +0300 Subject: [PATCH 0577/1360] fix(net): correct allocation size and flag handling --- core/libs/prx/libSceNet/Export.cpp | 8 ++++---- core/libs/tests/GuestSceNet.cpp | 12 ++++++++++-- docs/dev/debt/libSceNet.md | 4 +--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 36704406eb..39a828ebe0 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2175,7 +2175,7 @@ int APS5_VABI sceNetEventCallbackWaitCB(std::int32_t timeout, void* waitData, st return 0; } -int APS5_VABI sceNetFreeAllRouteInfo(void) { +int APS5_VABI sceNetFreeAllRouteInfo(void* routes) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2290,11 +2290,11 @@ int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { return 0; } -void* APS5_VABI sceNetMemoryAllocate(std::int64_t size, int flags) { +void* APS5_VABI sceNetMemoryAllocate(std::int32_t size, int flags) { constexpr int zeroFill = 2; - if ((flags & ~zeroFill) != 0) - throw std::runtime_error("sceNetMemoryAllocate: flags " + std::to_string(flags) + " are not supported"); if (size <= 0) return nullptr; + if ((flags & 4) != 0) + throw std::runtime_error("sceNetMemoryAllocate: pool reserve allocation is not implemented"); void* memory = nullptr; try { memory = GuestHeap::GuestHeapAllocate_nid_postfix(static_cast<std::size_t>(size)); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 713a2c0186..6db4c99913 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -135,7 +135,7 @@ int APS5_VABI sceNetPoolDestroy(int); int APS5_VABI sceNetGetMemoryPoolStats(int, NetMemoryPoolStats*); const char* APS5_VABI sceNetGetIfName(std::uint32_t); int APS5_VABI sceNetGetRandom(std::uint32_t*); -void* APS5_VABI sceNetMemoryAllocate(std::int64_t, int); +void* APS5_VABI sceNetMemoryAllocate(std::int32_t, int); void APS5_VABI sceNetMemoryFree(void*); } @@ -303,6 +303,8 @@ static void CheckRandom() { static void CheckNetMemory() { Require(sceNetMemoryAllocate(0, 0) == nullptr); Require(sceNetMemoryAllocate(-1, 2) == nullptr); + Require(sceNetMemoryAllocate(0, 4) == nullptr); + Require(sceNetMemoryAllocate(INT32_MIN, -1) == nullptr); auto* zeroed = static_cast<std::uint8_t*>(sceNetMemoryAllocate(4096, 2)); Require(zeroed != nullptr); Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); @@ -314,9 +316,15 @@ static void CheckNetMemory() { sceNetMemoryFree(plain); sceNetMemoryFree(zeroed); sceNetMemoryFree(nullptr); + for (const int flags : {1, 0x40000001, -5}) { + auto* allocation = static_cast<std::uint8_t*>(sceNetMemoryAllocate(16, flags)); + Require(allocation != nullptr); + if ((flags & 2) != 0) Require(std::all_of(allocation, allocation + 16, [](std::uint8_t byte) { return byte == 0; })); + sceNetMemoryFree(allocation); + } bool threw = false; try { - sceNetMemoryAllocate(16, 1); + sceNetMemoryAllocate(16, 4); } catch (const std::runtime_error&) { threw = true; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 7f908bd380..391136c6b2 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -19,7 +19,6 @@ - [sceNetAllocateAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDbgInit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown @@ -31,8 +30,7 @@ - [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. -- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware -- [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware +- [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 5862c362f49236a7a86e41d9e4acdf8d73d0ade2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:38:44 +0300 Subject: [PATCH 0578/1360] fix(user-service): restore initialization and accessibility signatures --- core/libs/prx/libSceUserService/Export.cpp | 12 +++++------- core/libs/prx/libSceUserService/UserService.hpp | 6 ++++++ core/libs/tests/GuestUserServiceLifecycle.cpp | 15 ++++++++------- docs/dev/debt/libSceUserService.md | 3 +-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 7a4c11d33e..a12d5c3263 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -26,7 +26,7 @@ int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32 return ReportAccessibilitySetting(user_id, chat_transcription, 0); } -int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable() { +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(std::int32_t userId, std::int32_t* enabled) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -43,7 +43,7 @@ int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibr return ReportAccessibilitySetting(user_id, vibration, 1); } -int APS5_VABI sceUserServiceGetAccessibilityZoom() { +int APS5_VABI sceUserServiceGetAccessibilityZoom(std::int32_t userId, std::int32_t* zoom) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -160,18 +160,16 @@ static int MarkInitialized() { return USER_SERVICE_OK; } -int APS5_VABI sceUserServiceInitialize(const void* params) { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* params) { if (params != nullptr) { - int priority = 0; - std::memcpy(&priority, params, sizeof(priority)); - if (!ValidThreadPriority(priority)) { + if (!ValidThreadPriority(params->priority)) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } } return MarkInitialized(); } -int APS5_VABI sceUserServiceInitialize2(int priority) { +int APS5_VABI sceUserServiceInitialize2(std::int32_t priority, KernelCpumask affinityMask) { if (!ValidThreadPriority(priority)) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index 1e0f926aa3..eeb192f8aa 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -4,6 +4,12 @@ #include <cstddef> #include <cstdint> +struct UserServiceInitParam { + std::int32_t priority; +}; + +static_assert(sizeof(UserServiceInitParam) == 4); + constexpr int USER_SERVICE_OK = 0; constexpr int USER_SERVICE_ERROR_NOT_INITIALIZED = static_cast<int>(0x80960002u); constexpr int USER_SERVICE_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80960003u); diff --git a/core/libs/tests/GuestUserServiceLifecycle.cpp b/core/libs/tests/GuestUserServiceLifecycle.cpp index c169e42d26..550acb5353 100644 --- a/core/libs/tests/GuestUserServiceLifecycle.cpp +++ b/core/libs/tests/GuestUserServiceLifecycle.cpp @@ -5,8 +5,8 @@ #include <initializer_list> extern "C" { -int APS5_VABI sceUserServiceInitialize(const void* params); -int APS5_VABI sceUserServiceInitialize2(int priority); +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* params); +int APS5_VABI sceUserServiceInitialize2(std::int32_t priority, KernelCpumask affinityMask); int APS5_VABI sceUserServiceTerminate(void); int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_id_list); int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event); @@ -34,25 +34,26 @@ void RequireLoginOnce() { } int InitializeWithPriority(int priority) { - return sceUserServiceInitialize(&priority); + const UserServiceInitParam param{priority}; + return sceUserServiceInitialize(¶m); } void TestThreadPriority() { for (int priority : {0, 0xFF, 0x300, -1}) { Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); - Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority, 1) == USER_SERVICE_ERROR_INVALID_ARGUMENT); } RequireNotInitialized(); - Require(sceUserServiceInitialize2(0x100) == USER_SERVICE_OK); + Require(sceUserServiceInitialize2(0x100, 1) == USER_SERVICE_OK); RequireLoginOnce(); for (int priority : {0x100, 700, 0x2FF}) { Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); - Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + Require(sceUserServiceInitialize2(priority, 1) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); } for (int priority : {0, 0xFF, 0x300, -1}) { Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); - Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority, 1) == USER_SERVICE_ERROR_INVALID_ARGUMENT); } Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); Require(sceUserServiceTerminate() == USER_SERVICE_OK); diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 4fca560df4..e320add914 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -3,9 +3,8 @@ ### Unknown function info - [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` -- [sceUserServiceInitialize, sceUserServiceTerminate](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - not measured: whether an invalid thread priority leaves an uninitialized service uninitialized (it does here), the parameters after their first `int`, and any second argument of `sceUserServiceInitialize2`. Measured on a PS5: before initialization and after `sceUserServiceTerminate`, `GetLoginUserIdList`, `GetEvent` and `Terminate` return `0x80960002`; a second initialization returns `0x80960003` and a new one reports the initial user's login again; a priority outside 0x100 to 0x2FF, from the first `int` of the parameters or the `sceUserServiceInitialize2` argument, returns `0x80960005` before the initialized check. The other calls do not check initialization +- [sceUserServiceInitialize and sceUserServiceInitialize2](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - event delivery has no background service thread, so the confirmed priority and Initialize2 CPU affinity mask do not affect host scheduling. - [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output -- [sceUserServiceGetAccessibilityKeyremapEnable, sceUserServiceGetAccessibilityZoom](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - declared without parameters as throwing stubs calling `NotImplemented_nid_no_patch`. Their signatures and values remain unverified on hardware From dd2a009f1efef001301e7a6c6ef6fda824902077 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:41:40 +0300 Subject: [PATCH 0579/1360] fix: restore batch priority and vr trigger query signatures --- core/libs/prx/libSceAcm/Export.cpp | 2 +- core/libs/prx/libScePad/Export.cpp | 4 ++-- docs/dev/debt/libSceAcm.md | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 325afaa575..abc5dc83bc 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -91,7 +91,7 @@ int APS5_VABI sceAcm_Panner(void) { return 0; } -int APS5_VABI sceAcmBatchJobPriority(void) { +int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index c97a1e3c23..ab3e74552d 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -293,7 +293,7 @@ int APS5_VABI scePadVrControllerRead() { return 0; } -int APS5_VABI scePadVrControllerGetTriggerEffectState() { +int APS5_VABI scePadVrControllerGetTriggerEffectState(std::int32_t leftHandle, std::int32_t rightHandle, PadTriggerEffectStateInformation* states) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -334,7 +334,7 @@ int APS5_VABI scePadUnknown_n3kSX62fgNo() { return g_initialized.load(std::memory_order_acquire) ? PAD_OK : PAD_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { +int APS5_VABI scePadVrControllerGetTriggerEffectStates(std::int32_t leftHandle, std::int32_t rightHandle, PadTriggerEffectStateInformation* states) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index 59682c3221..7bdccfcf99 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -8,6 +8,5 @@ - Context validation error codes (0x81940002 for bad context and 0x81940006 for bad pointer) for [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) follow prosper's `hle_audio_spatial.cpp`. KytyPS5 (`acm.cpp`) and prosper do not track individual batch identifiers, so `sceAcmBatchWait` returns success for valid contexts without inspecting batch IDs. Hardware ACM DSP coprocessor scheduling and console error verification remain unmeasured. - [sceAcm_ConvReverb_SharedIr, sceAcm_IFFT](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAcm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim -- PPSA02530 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceAcmBatchJobPriority](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) and [scePadVrControllerGetTriggerEffectStates](../../../core/libs/prx/libScePad/Export.cpp) (libScePad). - [`sceAcmBatchJobNotification`](../../../core/libs/prx/libSceAcm/Export.cpp) is a parameterless pending declaration for import resolution. Its signature and behavior are unverified; it throws through `NotImplemented_nid_no_patch`. From 11c5d585e6e52be0059735cec8fa7fc4555c115b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:41:51 +0300 Subject: [PATCH 0580/1360] fix(audio): correct mix level and 3d latency validation --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 9 +++------ core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp | 2 +- core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp | 7 +++++-- .../prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp | 6 +++--- docs/dev/debt/libSceAudioOut.md | 5 ++--- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 0eda307e26..c6a9930bb6 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -656,20 +656,17 @@ int APS5_VABI sceAudioOutSysOpen(int userId, std::uint32_t mode) { return allocatePort(PORT_TYPE_SYS, SYS_PORT_SAMPLES, SYS_PORT_FRAME_RATE, Format::S16_8Ch); } -int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, std::uint32_t mixLevel) { std::lock_guard<std::mutex> lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } - if (port->type != PORT_TYPE_PADSPK) { - return -2144993270; - } if (mixLevel > DEFAULT_VOLUME) { return -2144993260; } - if (mixLevel < 0) { - throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported"); + if (port->type != PORT_TYPE_PADSPK) { + return -2144993270; } port->mixLevel = mixLevel; return 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index c647f607ea..b9d9d152cf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -16,8 +16,8 @@ int APS5_VABI sceAudioOut2Initialize(void) { } int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { - (void)userId; if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; + if (userId != 0 && (userId != 0xFF || output == 0)) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 9a09ec4fba..3ab1cd4a0f 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -53,11 +53,14 @@ constexpr int errorMasteringInvalidApiParam = static_cast<int>(0x80268201); constexpr int errorMasteringInvalidStatesId = static_cast<int>(0x80268204); void TestSet3DLatency() { - Require(sceAudioOut2Set3DLatency(systemUser, 0) == 0, "output 0 for the system user must be accepted"); + Require(sceAudioOut2Set3DLatency(systemUser, 0) == errorInvalidParam, "output 0 for selector 255 must be refused"); + for (std::uint32_t output = 0; output <= 2; ++output) { + Require(sceAudioOut2Set3DLatency(0, output) == 0, "selector 0 must accept each supported output"); + } Require(sceAudioOut2Set3DLatency(systemUser, 1) == 0, "output 1 for the system user must be accepted"); Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "output 2 for the system user must be accepted"); Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "output 2 must be accepted again"); - Require(sceAudioOut2Set3DLatency(user, 1) == 0, "output 1 for another user must be accepted"); + Require(sceAudioOut2Set3DLatency(user, 1) == errorInvalidParam, "a user id is not a valid latency selector"); Require(sceAudioOut2Set3DLatency(systemUser, 3) == errorInvalidParam, "output 3 must be refused"); Require(sceAudioOut2Set3DLatency(systemUser, 0xFFFFFFFFu) == errorInvalidParam, "a large output must be refused"); } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp index 9c7ac4e9af..ca25acb711 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp @@ -16,7 +16,7 @@ int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std:: int APS5_VABI sceAudioOutClose(int); int APS5_VABI sceAudioOutOutput(int, const void*); int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); -int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int); +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, std::uint32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -153,7 +153,7 @@ void TestOtherPortType() { Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block); Require(Play(portTypeMain, formatS16Mono, block, [](int handle) { Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType); - Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); }) == block); } @@ -164,7 +164,7 @@ void TestRejectedLevels() { closed = handle; Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); - Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); })); + Require(sceAudioOutSetMixLevelPadSpk(handle, UINT32_MAX) == invalidMixLevel); Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort); Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort); }) == Scaled(block, unity / 2)); diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index d86f3e1808..80f284769c 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -7,12 +7,11 @@ - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured -- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, output)`: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) reads `(user_id, output, latency_us)`, but the known calls set just edi and esi. The validation follows KytyPS5: an output above 2 returns `0x80268001` and every user id is accepted, where before only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) were; the latency value of the three-argument reading is never received, and there is no 3D rendering stage to apply anything to +- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result - [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the errors `0x80268201` and `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does; a null parameter returns `0x80268201`, the mastering invalid-API-parameter error KytyPS5 returns for a null state, which is an inference rather than a measurement. The meaning of `output` and `user` is unknown and both are ignored - [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. The debug state ids are unknown, so every id is accepted, and a null param returns `0x80260502` like the other AudioOut2 calls. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured -- [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 - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device - [sceAudioOutSysOpen, sceAudioOutSysConfigureOutput, sceAudioOutSysClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; the signatures follow [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L24-L29), whose audio code both titles share, and are not confirmed on a console. `sceAudioOutSysOpen(user, mode)` opens a port only for mode 5, the TrueHD MAT route: [the same project's carrier table](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/iec61937.cpp#L125-L140), read from sceAudioOutExOpen's own tables in firmware 12.70, describes it as a 768 kHz port of 1024 S16 8-channel frames whose 16-byte frames are consumed at 192 kHz; every other mode throws. Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so like an Ex port a Sys port has no host device: `sceAudioOutOutput` paces it at its own rate and drops the MAT bursts. `sceAudioOutSysConfigureOutput(type, flags, mode, target, option)` accepts only the observed `(1, 0, 5, 1, 0)` HDMI MAT switch and changes nothing; other arguments throw. `sceAudioOutSysClose` closes a Sys port and throws for any other handle. The restore path EVO-PLAYER-PS5 takes after a Sys port calls `sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0)`, so no Sys reset combination is known @@ -112,7 +111,7 @@ ### Functional - [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. -- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. SetParam accepts the parameter set, and SetSystemDebugState records the debug state id, without anything to apply them to. +- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. - [sceAudioOut2LoInit, sceAudioOut2LoContext* and sceAudioOut2LoPort* calls](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) have no low-latency path: each forwards to its `sceAudioOut2` counterpart, so grain and queue depth are the context's `num_grains` and `queue_depth`, and playback keeps the 40 ms cushion. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L489-L545) only says each call takes the same handles and parameter blocks as its counterpart and drives the low-latency clock with a shallow queue and a small grain ([docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/audio.md#L662-L668)). Unverified: the console's grain and depth limits, its error for a handle of the other service (accepted here) and what `sceAudioOut2LoInit` returns when repeated (0 here). From d004e7a6f9f6e295832e31ea0fe45c38245e8782 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:46:05 +0300 Subject: [PATCH 0581/1360] fix(np): restore signaling initialization and peer lookup types --- .../prx/libSceNpSessionSignaling/Export.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 290ef13d06..164bd13a78 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -10,6 +10,14 @@ static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast<int>(0x80552D06); static std::atomic<uint32_t> g_nextContext{1}; +struct SessionSignalingInitParam { + std::int32_t userId; + std::uint64_t poolSize; + std::uint64_t affinityMask; + std::int32_t threadPriority; + std::uint64_t stackSize; +}; + struct SessionSignalingMemoryInfo { std::uint64_t poolSize; std::uint64_t usedSize; @@ -34,6 +42,11 @@ struct SessionSignalingGroupInfo { }; static_assert(sizeof(SessionSignalingMemoryInfo) == 24); +static_assert(sizeof(SessionSignalingInitParam) == 40); +static_assert(offsetof(SessionSignalingInitParam, poolSize) == 8); +static_assert(offsetof(SessionSignalingInitParam, affinityMask) == 16); +static_assert(offsetof(SessionSignalingInitParam, threadPriority) == 24); +static_assert(offsetof(SessionSignalingInitParam, stackSize) == 32); static_assert(offsetof(SessionSignalingMemoryInfo, usedSize) == 8); static_assert(offsetof(SessionSignalingMemoryInfo, peakUsedSize) == 16); static_assert(sizeof(SessionSignalingPeerAddress) == 16); @@ -47,7 +60,7 @@ static_assert(offsetof(SessionSignalingGroupInfo, connectionCount) == 256); extern "C" { -int APS5_VABI sceNpSessionSignalingInitialize(void* param) { +int APS5_VABI sceNpSessionSignalingInitialize(const SessionSignalingInitParam* param) { (void)param; return 0; } @@ -81,11 +94,11 @@ int APS5_VABI sceNpSessionSignalingGetConnectionInfo(void) { return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingActivateUser(void) { +int APS5_VABI sceNpSessionSignalingActivateUser(std::uint32_t contextId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(void) { +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(std::uint32_t contextId, std::uint32_t groupId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } From c203dbb302d8324c04e6f3f546cab50ef00502a0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:48:53 +0300 Subject: [PATCH 0582/1360] fix(np): restore signaling context callbacks and session arguments --- .../prx/libSceNpSessionSignaling/Export.cpp | 48 ++++++++++++++++--- core/libs/tests/GuestNpSessionSignaling.cpp | 11 ++++- docs/dev/debt/libSceNpSessionSignaling.md | 2 + 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 164bd13a78..cf7657d9e6 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -29,6 +29,30 @@ struct SessionSignalingPeerAddress { std::uint32_t platform; }; +struct SessionSignalingSessionParam { + std::uint32_t mode; + std::uint32_t option; +}; + +using SessionSignalingEventCallback = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::int32_t, const SessionSignalingPeerAddress*, std::int32_t, void*); +using SessionSignalingConnectionCallback = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::int32_t, std::int32_t, void*); +using SessionSignalingConnectionCallback2 = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::uint32_t, std::int32_t, std::int32_t, void*); + +template<typename TConnectionCallback> +struct SessionSignalingContextParam { + std::int32_t userId; + std::uint32_t serviceLabel; + SessionSignalingEventCallback prepareCallback; + void* prepareUserData; + SessionSignalingEventCallback sessionCallback; + void* sessionUserData; + TConnectionCallback connectionCallback; + void* connectionUserData; +}; + +using SessionSignalingContextParam1 = SessionSignalingContextParam<SessionSignalingConnectionCallback>; +using SessionSignalingContextParam2 = SessionSignalingContextParam<SessionSignalingConnectionCallback2>; + struct SessionSignalingConnectionStatistics { std::uint32_t peakConnectionCount; std::uint32_t connectionCount; @@ -50,6 +74,15 @@ static_assert(offsetof(SessionSignalingInitParam, stackSize) == 32); static_assert(offsetof(SessionSignalingMemoryInfo, usedSize) == 8); static_assert(offsetof(SessionSignalingMemoryInfo, peakUsedSize) == 16); static_assert(sizeof(SessionSignalingPeerAddress) == 16); +static_assert(sizeof(SessionSignalingSessionParam) == 8); +static_assert(offsetof(SessionSignalingSessionParam, option) == 4); +static_assert(sizeof(SessionSignalingContextParam1) == 56); +static_assert(sizeof(SessionSignalingContextParam2) == 56); +static_assert(offsetof(SessionSignalingContextParam1, prepareCallback) == 8); +static_assert(offsetof(SessionSignalingContextParam1, sessionCallback) == 24); +static_assert(offsetof(SessionSignalingContextParam1, connectionCallback) == 40); +static_assert(offsetof(SessionSignalingContextParam2, connectionCallback) == 40); +static_assert(offsetof(SessionSignalingContextParam2, connectionUserData) == 48); static_assert(offsetof(SessionSignalingPeerAddress, platform) == 8); static_assert(sizeof(SessionSignalingConnectionStatistics) == 16); static_assert(offsetof(SessionSignalingConnectionStatistics, connectionCount) == 4); @@ -65,19 +98,20 @@ int APS5_VABI sceNpSessionSignalingInitialize(const SessionSignalingInitParam* p return 0; } -int APS5_VABI sceNpSessionSignalingActivateSession(void) { +int APS5_VABI sceNpSessionSignalingActivateSession(std::uint32_t contextId, const char* sessionId, std::uint32_t sessionType, const SessionSignalingSessionParam* param, std::uint32_t* groupId) { return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, uint32_t* context_id) { - (void)param; - if (!context_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; - *context_id = g_nextContext.fetch_add(1, std::memory_order_relaxed); +int APS5_VABI sceNpSessionSignalingCreateContext2(const SessionSignalingContextParam2* param, std::uint32_t* contextId) { + if (!param || !contextId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + *contextId = g_nextContext.fetch_add(1, std::memory_order_relaxed); return 0; } -int APS5_VABI sceNpSessionSignalingCreateContext(const void* param, uint32_t* context_id) { - return sceNpSessionSignalingCreateContext2(param, context_id); +int APS5_VABI sceNpSessionSignalingCreateContext(const SessionSignalingContextParam1* param, std::uint32_t* contextId) { + if (!param || !contextId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + *contextId = g_nextContext.fetch_add(1, std::memory_order_relaxed); + return 0; } int APS5_VABI sceNpSessionSignalingDeactivate(uint32_t context_id) { diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 27bc077144..39b90f44f1 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI sceNpSessionSignalingInitialize(void* param); +int APS5_VABI sceNpSessionSignalingCreateContext(const void* param, std::uint32_t* contextId); int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, std::uint32_t* contextId); std::int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::int32_t contextId, void* info); int APS5_VABI sceNpSessionSignalingRequestPrepare(std::uint32_t contextId, std::uint32_t* requestId); @@ -29,10 +30,16 @@ void Require(bool condition, const char* message) { } int main() { - unsigned char initializeParam[32] = {}; + alignas(8) unsigned char initializeParam[40] = {}; Require(sceNpSessionSignalingInitialize(initializeParam) == 0, "initialize failed"); - unsigned char contextParam[64] = {}; + alignas(8) unsigned char contextParam[56] = {}; std::uint32_t contextId = 0; + Require(sceNpSessionSignalingCreateContext(nullptr, &contextId) == InvalidArgument, "null context parameter"); + Require(contextId == 0, "invalid context parameter wrote an id"); + Require(sceNpSessionSignalingCreateContext2(nullptr, &contextId) == InvalidArgument, "null context2 parameter"); + Require(contextId == 0, "invalid context2 parameter wrote an id"); + Require(sceNpSessionSignalingCreateContext(contextParam, nullptr) == InvalidArgument, "null context id"); + Require(sceNpSessionSignalingCreateContext2(contextParam, nullptr) == InvalidArgument, "null context2 id"); Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == 0, "context creation failed"); std::uint32_t requestId = 0xa5a5a5a5u; diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 80cb116b9a..7b26d4e439 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -2,5 +2,7 @@ ### Unknown function info +- [sceNpSessionSignalingInitialize, sceNpSessionSignalingCreateContext and sceNpSessionSignalingCreateContext2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - the initialization layout is 40 bytes; both context layouts are 56 bytes with callback and user-data pairs. The second context variant adds a group identifier to its connection callback. Pool allocation, initialization state, per-user context limits, context lookup, callback delivery and underlying NP account validation are not implemented. + - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified - [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 5a0a494e120a0d71717cc94ddeed9baaf558e1f7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:51:06 +0300 Subject: [PATCH 0583/1360] fix(np): restore signaling connection query layouts --- .../prx/libSceNpSessionSignaling/Export.cpp | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index cf7657d9e6..99f4bf2412 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -34,6 +34,24 @@ struct SessionSignalingSessionParam { std::uint32_t option; }; +struct SessionSignalingNetAddress { + std::uint32_t address; + std::uint16_t port; +}; + +union SessionSignalingConnectionInfo { + std::uint32_t value; + SessionSignalingNetAddress netAddress; + SessionSignalingPeerAddress peerAddress; +}; + +struct SessionSignalingLocalNetInfo { + std::uint32_t localAddress; + std::uint32_t mappedAddress; + std::uint32_t mappedPort; + std::uint32_t mappingStatus; +}; + using SessionSignalingEventCallback = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::int32_t, const SessionSignalingPeerAddress*, std::int32_t, void*); using SessionSignalingConnectionCallback = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::int32_t, std::int32_t, void*); using SessionSignalingConnectionCallback2 = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::uint32_t, std::int32_t, std::int32_t, void*); @@ -76,6 +94,13 @@ static_assert(offsetof(SessionSignalingMemoryInfo, peakUsedSize) == 16); static_assert(sizeof(SessionSignalingPeerAddress) == 16); static_assert(sizeof(SessionSignalingSessionParam) == 8); static_assert(offsetof(SessionSignalingSessionParam, option) == 4); +static_assert(sizeof(SessionSignalingNetAddress) == 8); +static_assert(offsetof(SessionSignalingNetAddress, port) == 4); +static_assert(sizeof(SessionSignalingConnectionInfo) == 16); +static_assert(sizeof(SessionSignalingLocalNetInfo) == 16); +static_assert(offsetof(SessionSignalingLocalNetInfo, mappedAddress) == 4); +static_assert(offsetof(SessionSignalingLocalNetInfo, mappedPort) == 8); +static_assert(offsetof(SessionSignalingLocalNetInfo, mappingStatus) == 12); static_assert(sizeof(SessionSignalingContextParam1) == 56); static_assert(sizeof(SessionSignalingContextParam2) == 56); static_assert(offsetof(SessionSignalingContextParam1, prepareCallback) == 8); @@ -124,7 +149,7 @@ int APS5_VABI sceNpSessionSignalingDestroyContext(uint32_t context_id) { return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionInfo(void) { +int APS5_VABI sceNpSessionSignalingGetConnectionInfo(std::uint32_t contextId, std::uint32_t connectionId, std::uint32_t code, SessionSignalingConnectionInfo* info) { return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } @@ -140,17 +165,17 @@ int APS5_VABI sceNpSessionSignalingTerminate(void) { return 0; } -int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(int32_t context_id, int32_t connection_id, int32_t* status, void* peer_address, uint16_t* peer_port) { - (void)context_id; - (void)connection_id; - (void)peer_address; - (void)peer_port; +int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::uint32_t contextId, std::uint32_t connectionId, std::int32_t* status, std::uint32_t* peerAddress, std::uint16_t* peerPort) { + (void)contextId; + (void)connectionId; + (void)peerAddress; + (void)peerPort; if (!status) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } -int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(int32_t context_id, void* info) { - (void)context_id; +int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::uint32_t contextId, SessionSignalingLocalNetInfo* info) { + (void)contextId; if (!info) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } From 9ff2cd1d18955ceb3e8b98ebdb2f355cc3b3b8b9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:54:02 +0300 Subject: [PATCH 0584/1360] fix(np): validate signaling context lifecycle and stale handles --- .../prx/libSceNpSessionSignaling/Export.cpp | 112 +++++++++++++++--- core/libs/tests/GuestNpSessionSignaling.cpp | 42 +++++++ docs/dev/debt/libSceNpSessionSignaling.md | 4 +- 3 files changed, 138 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index 99f4bf2412..79969f86ed 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -2,13 +2,28 @@ #include <cstddef> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include <atomic> +#include <array> +#include <mutex> // 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<int>(0x80553303); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast<int>(0x8055330C); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast<int>(0x80552D06); -static std::atomic<uint32_t> g_nextContext{1}; +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED = static_cast<int>(0x80553301); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80553302); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_LIMIT = static_cast<int>(0x80553307); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND = static_cast<int>(0x80553308); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND = static_cast<int>(0x8055330A); + +struct SessionSignalingContext { + std::uint32_t id; + std::int32_t userId; + bool active; +}; + +static std::mutex g_mutex; +static bool g_initialized = false; +static std::array<SessionSignalingContext, 10> g_contexts{}; struct SessionSignalingInitParam { std::int32_t userId; @@ -116,73 +131,134 @@ static_assert(offsetof(SessionSignalingConnectionStatistics, connectedCount) == static_assert(sizeof(SessionSignalingGroupInfo) == 264); static_assert(offsetof(SessionSignalingGroupInfo, connectionCount) == 256); +static SessionSignalingContext* FindContext(std::uint32_t contextId) { + auto& context = g_contexts[contextId % g_contexts.size()]; + return context.active && context.id == contextId ? &context : nullptr; +} + +template<typename TConnectionCallback> +static int CreateContext(const SessionSignalingContextParam<TConnectionCallback>* param, std::uint32_t* contextId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!param || !contextId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + for (const auto& context : g_contexts) { + if (context.active && context.userId == param->userId) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_LIMIT; + } + for (auto& context : g_contexts) { + if (!context.active) { + context.userId = param->userId; + context.active = true; + *contextId = context.id; + return 0; + } + } + return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_LIMIT; +} + extern "C" { int APS5_VABI sceNpSessionSignalingInitialize(const SessionSignalingInitParam* param) { - (void)param; + std::lock_guard lock(g_mutex); + if (g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_ALREADY_INITIALIZED; + if (!param || !param->poolSize) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + for (std::uint32_t index = 0; index < g_contexts.size(); ++index) { + g_contexts[index] = {1000 + index, 0, false}; + } + g_initialized = true; return 0; } int APS5_VABI sceNpSessionSignalingActivateSession(std::uint32_t contextId, const char* sessionId, std::uint32_t sessionType, const SessionSignalingSessionParam* param, std::uint32_t* groupId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!sessionId || !param || !groupId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + if ((param->mode == 1 && param->option != 0) || (param->mode == 2 && param->option == 0)) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } int APS5_VABI sceNpSessionSignalingCreateContext2(const SessionSignalingContextParam2* param, std::uint32_t* contextId) { - if (!param || !contextId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; - *contextId = g_nextContext.fetch_add(1, std::memory_order_relaxed); - return 0; + return CreateContext(param, contextId); } int APS5_VABI sceNpSessionSignalingCreateContext(const SessionSignalingContextParam1* param, std::uint32_t* contextId) { - if (!param || !contextId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; - *contextId = g_nextContext.fetch_add(1, std::memory_order_relaxed); - return 0; + return CreateContext(param, contextId); } -int APS5_VABI sceNpSessionSignalingDeactivate(uint32_t context_id) { - (void)context_id; - return 0; +int APS5_VABI sceNpSessionSignalingDeactivate(std::uint32_t contextId, std::uint32_t connectionId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } -int APS5_VABI sceNpSessionSignalingDestroyContext(uint32_t context_id) { - (void)context_id; +int APS5_VABI sceNpSessionSignalingDestroyContext(std::uint32_t contextId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + auto* context = FindContext(contextId); + if (!context) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + context->active = false; + const auto nextId = context->id + static_cast<std::uint32_t>(g_contexts.size()); + context->id = nextId < 100001 ? nextId : 1000 + contextId % static_cast<std::uint32_t>(g_contexts.size()); return 0; } int APS5_VABI sceNpSessionSignalingGetConnectionInfo(std::uint32_t contextId, std::uint32_t connectionId, std::uint32_t code, SessionSignalingConnectionInfo* info) { - return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!info) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } int APS5_VABI sceNpSessionSignalingActivateUser(std::uint32_t contextId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!peerAddress || !connectionId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(std::uint32_t contextId, std::uint32_t groupId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { - return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!peerAddress || !connectionId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } int APS5_VABI sceNpSessionSignalingTerminate(void) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + g_initialized = false; + for (auto& context : g_contexts) context.active = false; return 0; } int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::uint32_t contextId, std::uint32_t connectionId, std::int32_t* status, std::uint32_t* peerAddress, std::uint16_t* peerPort) { - (void)contextId; (void)connectionId; (void)peerAddress; (void)peerPort; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; if (!status) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::uint32_t contextId, SessionSignalingLocalNetInfo* info) { (void)contextId; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; if (!info) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } int APS5_VABI sceNpSessionSignalingRequestPrepare(uint32_t contextId, uint32_t* requestId) { - (void)contextId; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; if (!requestId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 39b90f44f1..982664e90c 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -3,11 +3,14 @@ #include <cstdio> #include <cstdlib> #include <cstring> +#include <array> extern "C" { int APS5_VABI sceNpSessionSignalingInitialize(void* param); int APS5_VABI sceNpSessionSignalingCreateContext(const void* param, std::uint32_t* contextId); int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, std::uint32_t* contextId); +int APS5_VABI sceNpSessionSignalingDestroyContext(std::uint32_t contextId); +int APS5_VABI sceNpSessionSignalingDeactivate(std::uint32_t contextId, std::uint32_t connectionId); std::int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::int32_t contextId, void* info); int APS5_VABI sceNpSessionSignalingRequestPrepare(std::uint32_t contextId, std::uint32_t* requestId); std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::int32_t contextId, std::int32_t connectionId, std::int32_t* status, void* peerAddress, std::uint16_t* peerPort); @@ -19,6 +22,11 @@ namespace { constexpr int InvalidArgument = static_cast<int>(0x80553303u); constexpr int Unavailable = static_cast<int>(0x80552D06u); constexpr int ConnectionNotFound = static_cast<int>(0x8055330Cu); +constexpr int NotInitialized = static_cast<int>(0x80553301u); +constexpr int AlreadyInitialized = static_cast<int>(0x80553302u); +constexpr int ContextLimit = static_cast<int>(0x80553307u); +constexpr int ContextNotFound = static_cast<int>(0x80553308u); +constexpr int GroupNotFound = static_cast<int>(0x8055330Au); void Require(bool condition, const char* message) { if (!condition) { @@ -31,7 +39,15 @@ void Require(bool condition, const char* message) { int main() { alignas(8) unsigned char initializeParam[40] = {}; + Require(sceNpSessionSignalingTerminate() == NotInitialized, "terminate before initialize"); + Require(sceNpSessionSignalingCreateContext2(nullptr, nullptr) == NotInitialized, "initialization check precedes context arguments"); + Require(sceNpSessionSignalingGetConnectionStatus(0, 0, nullptr, nullptr, nullptr) == NotInitialized, "status before initialize"); + Require(sceNpSessionSignalingInitialize(nullptr) == InvalidArgument, "null initialize parameter"); + Require(sceNpSessionSignalingInitialize(initializeParam) == InvalidArgument, "zero pool size"); + const std::uint64_t poolSize = 4 * 1024 * 1024; + std::memcpy(initializeParam + 8, &poolSize, sizeof(poolSize)); Require(sceNpSessionSignalingInitialize(initializeParam) == 0, "initialize failed"); + Require(sceNpSessionSignalingInitialize(nullptr) == AlreadyInitialized, "initialization check precedes initialize arguments"); alignas(8) unsigned char contextParam[56] = {}; std::uint32_t contextId = 0; Require(sceNpSessionSignalingCreateContext(nullptr, &contextId) == InvalidArgument, "null context parameter"); @@ -41,8 +57,26 @@ int main() { Require(sceNpSessionSignalingCreateContext(contextParam, nullptr) == InvalidArgument, "null context id"); Require(sceNpSessionSignalingCreateContext2(contextParam, nullptr) == InvalidArgument, "null context2 id"); Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == 0, "context creation failed"); + const auto firstId = contextId; + Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == ContextLimit, "duplicate user context"); + Require(contextId == firstId, "duplicate context modified output"); + std::array<std::uint32_t, 9> otherContexts{}; + for (std::int32_t userId = 1; userId <= 9; ++userId) { + std::memcpy(contextParam, &userId, sizeof(userId)); + Require(sceNpSessionSignalingCreateContext2(contextParam, &otherContexts[userId - 1]) == 0, "create remaining contexts"); + } + const std::int32_t overflowUser = 10; + std::memcpy(contextParam, &overflowUser, sizeof(overflowUser)); + Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == ContextLimit, "context limit"); + Require(contextId == firstId, "context limit modified output"); + Require(sceNpSessionSignalingDestroyContext(firstId) == 0, "destroy first context"); + Require(sceNpSessionSignalingDestroyContext(firstId) == ContextNotFound, "destroy stale context"); + Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == 0, "reuse context slot"); + Require(contextId != firstId, "reused context kept stale id"); std::uint32_t requestId = 0xa5a5a5a5u; + Require(sceNpSessionSignalingRequestPrepare(firstId, &requestId) == ContextNotFound, "prepare stale context"); + Require(sceNpSessionSignalingRequestPrepare(firstId, nullptr) == InvalidArgument, "prepare argument precedence"); Require(sceNpSessionSignalingRequestPrepare(contextId, nullptr) == InvalidArgument, "null request id"); Require(sceNpSessionSignalingRequestPrepare(contextId, &requestId) == Unavailable, "prepare without network"); Require(requestId == 0xa5a5a5a5u, "failed prepare wrote a request id"); @@ -59,10 +93,18 @@ int main() { std::int32_t status = 0x5a5a5a5a; std::uint32_t peerAddress = 0xa5a5a5a5u; std::uint16_t peerPort = 0xa5a5; + Require(sceNpSessionSignalingGetConnectionStatus(firstId, 1, &status, &peerAddress, &peerPort) == ContextNotFound, "status stale context"); Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, nullptr, &peerAddress, &peerPort) == InvalidArgument, "null connection status"); Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, &peerAddress, &peerPort) == ConnectionNotFound, "connection without network"); Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, nullptr, nullptr) == ConnectionNotFound, "connection without peer outputs"); Require(status == 0x5a5a5a5a && peerAddress == 0xa5a5a5a5u && peerPort == 0xa5a5, "failed status query wrote an output"); + Require(sceNpSessionSignalingDeactivate(firstId, 1) == ContextNotFound, "deactivate stale context"); + Require(sceNpSessionSignalingDeactivate(contextId, 1) == GroupNotFound, "deactivate nonexistent group"); Require(sceNpSessionSignalingTerminate() == 0, "terminate failed"); + Require(sceNpSessionSignalingDestroyContext(contextId) == NotInitialized, "destroy after terminate"); + Require(sceNpSessionSignalingTerminate() == NotInitialized, "repeated terminate"); + Require(sceNpSessionSignalingInitialize(initializeParam) == 0, "reinitialize"); + Require(sceNpSessionSignalingRequestPrepare(contextId, &requestId) == ContextNotFound, "reinitialize retained a context"); + Require(sceNpSessionSignalingTerminate() == 0, "final terminate"); } diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 7b26d4e439..f6a89b9950 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceNpSessionSignalingInitialize, sceNpSessionSignalingCreateContext and sceNpSessionSignalingCreateContext2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - the initialization layout is 40 bytes; both context layouts are 56 bytes with callback and user-data pairs. The second context variant adds a group identifier to its connection callback. Pool allocation, initialization state, per-user context limits, context lookup, callback delivery and underlying NP account validation are not implemented. +- [sceNpSessionSignalingInitialize, sceNpSessionSignalingCreateContext and sceNpSessionSignalingCreateContext2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - the initialization layout is 40 bytes; both context layouts are 56 bytes with callback and user-data pairs. The second context variant adds a group identifier to its connection callback. Pool allocation, callback delivery and underlying NP account validation are not implemented; priority and affinity settings do not affect host scheduling. -- [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified +- [sceNpSessionSignalingGetConnectionStatus, sceNpSessionSignalingActivateSession and sceNpSessionSignalingActivateUser](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - initialization, context lookup and output validation follow the system library. Connections never activate in the offline runtime; activation returns unavailable and a connection query on a valid context returns connection-not-found without writing outputs. SDK-dependent peer-address validation and local-network discovery remain incomplete. - [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From e2ead10b22b15f4051a691f0b5cff6c7e3f65c99 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 13:57:22 +0300 Subject: [PATCH 0585/1360] fix(regmgr): restore binary access signatures --- core/libs/prx/libSceRegMgr/Export.cpp | 4 ++-- docs/dev/debt/libSceRegMgr.md | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceRegMgr/Export.cpp b/core/libs/prx/libSceRegMgr/Export.cpp index e1e4aad2c1..9d71463de6 100644 --- a/core/libs/prx/libSceRegMgr/Export.cpp +++ b/core/libs/prx/libSceRegMgr/Export.cpp @@ -5,7 +5,7 @@ extern "C" { -int APS5_VABI sceRegMgrGetBin() { +int APS5_VABI sceRegMgrGetBin(std::uint32_t optionId, void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -17,7 +17,7 @@ int APS5_VABI sceRegMgrGetStr(int32_t optionId, char* outString, std::size_t str NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceRegMgrSetBin() { +int APS5_VABI sceRegMgrSetBin(std::uint32_t optionId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceRegMgr.md b/docs/dev/debt/libSceRegMgr.md index 9b6dbd6441..24cd287ce7 100644 --- a/docs/dev/debt/libSceRegMgr.md +++ b/docs/dev/debt/libSceRegMgr.md @@ -2,8 +2,6 @@ ### Unknown function info -- [sceRegMgrNonSysGetInt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr, NID `dKeshzt29G4`) - observed as an import needed for module loading (issue 4720); its `(int64_t key, int32_t unk, int32_t* data)` signature follows [ChonkyStation4](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/SceRegMgr/SceRegMgr.cpp#L38-L47), not measured on a console, so every call throws -- [sceRegMgrGetBin and sceRegMgrSetBin](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRegMgr.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. The string and integer prototypes follow [SharpProspero's SystemSettings bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/SystemSettings.cs) - [sceRegMgrCheckError](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature - [sceRegMgrCheckIntVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature - [sceRegMgrGetBinInitVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature From 99e5cb5c1a87ab6cb0243db562d9655d8b4b484e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:01:31 +0300 Subject: [PATCH 0586/1360] fix(appinst): restore verified installation query arguments --- core/libs/prx/libSceAppInstUtil/Export.cpp | 18 ++++++++++-------- docs/dev/debt/libSceAppInstUtil.md | 4 +++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAppInstUtil/Export.cpp b/core/libs/prx/libSceAppInstUtil/Export.cpp index 845fd759b0..c3485fbe05 100644 --- a/core/libs/prx/libSceAppInstUtil/Export.cpp +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -3,6 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct AppInstallProgressInfo; + extern "C" { int APS5_VABI sceAppInstUtilInitialize(void) { @@ -25,7 +27,7 @@ int APS5_VABI sceAppInstUtilAppIsInInstalling(int32_t* installing) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilAppIsInUpdating() { +int APS5_VABI sceAppInstUtilAppIsInUpdating(const char* titleId, bool* updating) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -33,7 +35,7 @@ int APS5_VABI sceAppInstUtilAppExists(const char* titleId, int32_t* exists) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilAppGetSize() { +int APS5_VABI sceAppInstUtilAppGetSize(const char* titleId, std::uint64_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -53,11 +55,11 @@ int APS5_VABI sceAppInstUtilAppUnInstall(const char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilAppUnInstallAddcont() { +int APS5_VABI sceAppInstUtilAppUnInstallAddcont(const char* titleId, const char* contentLabel) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilAppUnInstallPat() { +int APS5_VABI sceAppInstUtilAppUnInstallPat(const char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -65,19 +67,19 @@ int APS5_VABI sceAppInstUtilAppUnInstallTheme() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilAppUnInstallTypes() { +int APS5_VABI sceAppInstUtilAppUnInstallTypes(const char* titleId, std::uint32_t types) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilGetInstallProgressInfo() { +int APS5_VABI sceAppInstUtilGetInstallProgressInfo(const char* titleId, AppInstallProgressInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilGetInstallProgress() { +int APS5_VABI sceAppInstUtilGetInstallProgress(const char* titleId, void* progress) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilGetPrimaryAppSlot() { +int APS5_VABI sceAppInstUtilGetPrimaryAppSlot(const char* titleId, void* slot) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAppInstUtil.md b/docs/dev/debt/libSceAppInstUtil.md index ecf5fb2a82..92d88fb4a6 100644 --- a/docs/dev/debt/libSceAppInstUtil.md +++ b/docs/dev/debt/libSceAppInstUtil.md @@ -2,4 +2,6 @@ ### Unknown function info -- [sceAppInstUtilAppIsInUpdating, sceAppInstUtilAppGetSize, sceAppInstUtilAppInstallPkg, sceAppInstUtilAppPrepareOverwritePkg, sceAppInstUtilAppRecoverApp, sceAppInstUtilAppUnInstallAddcont, sceAppInstUtilAppUnInstallPat, sceAppInstUtilAppUnInstallTheme, sceAppInstUtilAppUnInstallTypes, sceAppInstUtilGetInstallProgressInfo, sceAppInstUtilGetInstallProgress and sceAppInstUtilGetPrimaryAppSlot](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c), but no credible prototype was found. They are declared without parameters only to resolve the known imports; this is not an ABI claim and callers must not call them until a prototype is verified. The other seven declarations follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppInstUtil/AppInstUtil.cs) +- [sceAppInstUtilAppInstallPkg](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - the package path is followed by an opaque 64-bit value sent to the install service; its meaning and pointer-versus-integer type remain unresolved. +- [sceAppInstUtilGetInstallProgressInfo, sceAppInstUtilGetInstallProgress and sceAppInstUtilGetPrimaryAppSlot](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - take a title-id string and an output pointer. The IPC replies contain 48, 4 and 4 bytes respectively; callers and reply producers are needed to establish the field types. +- [sceAppInstUtilAppPrepareOverwritePkg, sceAppInstUtilAppRecoverApp and sceAppInstUtilAppUnInstallTheme](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - absent from the supplied system module and from the supplied decompiled callers; the old export inventory establishes names but not prototypes. From 8add4fe2580eeecb72f386a26dd68b8cafc8f3fa Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:03:45 +0300 Subject: [PATCH 0587/1360] fix(bgft): restore task control and progress signatures --- core/libs/prx/libSceBgft/Export.cpp | 43 ++++++++++++++++++----------- docs/dev/debt/libSceBgft.md | 4 ++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp index bb89f27861..8480641566 100644 --- a/core/libs/prx/libSceBgft/Export.cpp +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -3,9 +3,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct BgftInitParam { + void* heap; + uint64_t heapSize; +}; + +struct BgftProgress; +struct BgftStorageTaskParam; + +static_assert(sizeof(BgftInitParam) == 16); +static_assert(offsetof(BgftInitParam, heapSize) == 8); + extern "C" { -int APS5_VABI sceBgftServiceInit(void* params) { +int APS5_VABI sceBgftServiceInit(const BgftInitParam* params) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -29,11 +40,11 @@ int APS5_VABI sceBgftServiceDownloadStopTaskAll() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceDownloadFindActivePatchTask() { +int APS5_VABI sceBgftServiceDownloadFindActivePatchTask(const char* contentId, int32_t* taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceDownloadFindActivePupTask() { +int APS5_VABI sceBgftServiceDownloadFindActivePupTask(int32_t* taskId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -41,7 +52,7 @@ int APS5_VABI sceBgftServiceDownloadFindTaskByContentId(const char* contentId, i NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceDownloadGetProgress(int32_t taskId, void* progress) { +int APS5_VABI sceBgftServiceDownloadGetProgress(int32_t taskId, BgftProgress* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -61,15 +72,15 @@ int APS5_VABI sceBgftServiceDownloadStopTask(int32_t taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask() { +int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask(const char* contentId, int32_t* taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadFindActiveTask() { +int APS5_VABI sceBgftServiceIntDownloadFindActiveTask(const char* contentId, int32_t subType, int32_t* taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress() { +int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* contentId, BgftProgress* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -77,11 +88,11 @@ int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress() { +int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress(const char* contentId, BgftProgress* progress) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetProgress() { +int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -89,31 +100,31 @@ int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadPauseTask() { +int APS5_VABI sceBgftServiceIntDownloadPauseTask(int32_t taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx() { +int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx(const BgftStorageTaskParam* params, int32_t* taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadResumeTask() { +int APS5_VABI sceBgftServiceIntDownloadResumeTask(int32_t taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadStopTask() { +int APS5_VABI sceBgftServiceIntDownloadStopTask(int32_t taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadStartTask() { +int APS5_VABI sceBgftServiceIntDownloadStartTask(int32_t taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadUnregisterTask() { +int APS5_VABI sceBgftServiceIntDownloadUnregisterTask(int32_t taskId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntInit() { +int APS5_VABI sceBgftServiceIntInit(const BgftInitParam* params) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 8d8e3ccbe7..3da7019e92 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -2,4 +2,6 @@ ### Unknown function info -- [sceBgftServiceDownloadPauseTaskAll, sceBgftServiceDownloadResumeTaskAll, sceBgftServiceDownloadStartTaskAll, sceBgftServiceDownloadStopTaskAll, sceBgftServiceDownloadFindActivePatchTask, sceBgftServiceDownloadFindActivePupTask, sceBgftServiceIntDownloadFindActiveGameAndGameAcTask, sceBgftServiceIntDownloadFindActiveTask, sceBgftServiceIntDownloadGetGameAndGameAcProgress, sceBgftServiceIntDownloadGetPatchProgress, sceBgftServiceIntDownloadGetPlayGoProgress, sceBgftServiceIntDownloadGetProgress, sceBgftServiceIntDownloadGetTaskInfo, sceBgftServiceIntDownloadPauseTask, sceBgftServiceIntDownloadRegisterTaskByStorageEx, sceBgftServiceIntDownloadResumeTask, sceBgftServiceIntDownloadStopTask, sceBgftServiceIntDownloadStartTask, sceBgftServiceIntDownloadUnregisterTask and sceBgftServiceIntInit](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. The eight task-control declarations not listed here follow [SharpProspero's DownloadService bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/DownloadService.cs) +- `sceBgftServiceDownloadGetProgress`, `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. +- `sceBgftServiceIntDownloadRegisterTaskByStorageEx`: confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. Scalar meanings and the field at offset 24 remain unresolved; the parameter type is forward-declared. +- `sceBgftServiceIntDownloadGetPatchProgress` and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. From 0d237011c704520b815486cbc35b15a2ce590bb6 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:07:58 +0300 Subject: [PATCH 0588/1360] fix(keycode): restore virtual key mappings and argument types --- core/libs/prx/libSceConvertKeycode/Export.cpp | 226 +++++++++++++++++- core/libs/tests/GuestConvertKeycode.cpp | 23 ++ docs/dev/debt/libSceConvertKeycode.md | 2 + 3 files changed, 246 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 6d6a3f0048..7eb667382d 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,13 +1,225 @@ +#include <array> #include <cstdint> #include <stdexcept> #include <string> #include "SceTypes.hpp" -#include "HitLog.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr int kErrInvalidAddress = static_cast<int>(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast<int>(0x80BC0010u); +constexpr std::array<std::uint16_t, 256> BaseVirtualKeys = { + 255, 255, 255, 255, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, + 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 48, 13, 27, 8, 9, 32, 187, 219, 221, + 186, 191, 191, 192, 222, 220, 188, 190, 189, 20, 112, 113, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 44, 145, 19, 45, 36, 33, 46, 35, 34, 39, + 37, 40, 38, 144, 111, 106, 109, 107, 13, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 96, 110, 226, 93, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 12, 255, 255, 255, 255, 255, 255, 255, + 162, 160, 164, 91, 163, 161, 165, 92, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +}; + +constexpr auto VirtualKeys = [] { + std::array<std::array<std::uint16_t, 256>, 39> layouts{}; + for (auto& layout : layouts) layout = BaseVirtualKeys; + layouts[1][28] = 90; + layouts[1][29] = 89; + layouts[1][45] = 189; + layouts[1][46] = 221; + layouts[1][47] = 186; + layouts[1][48] = 187; + layouts[2][28] = 90; + layouts[2][29] = 89; + layouts[2][45] = 219; + layouts[2][46] = 221; + layouts[2][47] = 186; + layouts[2][48] = 192; + layouts[2][49] = 223; + layouts[2][50] = 223; + layouts[2][51] = 222; + layouts[2][52] = 220; + layouts[2][53] = 191; + layouts[3][45] = 189; + layouts[3][46] = 187; + layouts[3][47] = 219; + layouts[3][48] = 221; + layouts[3][49] = 220; + layouts[3][50] = 220; + layouts[3][51] = 186; + layouts[3][53] = 192; + layouts[3][56] = 191; + layouts[3][100] = 255; + layouts[4][45] = 189; + layouts[4][46] = 187; + layouts[4][47] = 219; + layouts[4][48] = 221; + layouts[4][49] = 222; + layouts[4][50] = 222; + layouts[4][51] = 186; + layouts[4][52] = 192; + layouts[4][53] = 223; + layouts[4][56] = 191; + layouts[5][45] = 219; + layouts[5][46] = 221; + layouts[5][47] = 186; + layouts[5][48] = 187; + layouts[6][45] = 219; + layouts[6][46] = 221; + layouts[6][47] = 186; + layouts[6][48] = 187; + layouts[6][100] = 255; + layouts[8][4] = 81; + layouts[8][16] = 188; + layouts[8][20] = 65; + layouts[8][26] = 90; + layouts[8][29] = 87; + layouts[8][45] = 219; + layouts[8][46] = 187; + layouts[8][49] = 220; + layouts[8][50] = 220; + layouts[8][51] = 77; + layouts[8][52] = 192; + layouts[8][53] = 222; + layouts[8][54] = 190; + layouts[8][55] = 191; + layouts[8][56] = 223; + layouts[9][4] = 81; + layouts[9][16] = 188; + layouts[9][20] = 65; + layouts[9][26] = 90; + layouts[9][29] = 87; + layouts[9][45] = 219; + layouts[9][46] = 189; + layouts[9][49] = 220; + layouts[9][50] = 220; + layouts[9][51] = 77; + layouts[9][52] = 192; + layouts[9][53] = 222; + layouts[9][54] = 190; + layouts[9][55] = 191; + layouts[9][56] = 187; + layouts[10][45] = 189; + layouts[10][46] = 187; + layouts[10][47] = 219; + layouts[10][48] = 221; + layouts[10][49] = 220; + layouts[10][50] = 220; + layouts[10][51] = 186; + layouts[10][52] = 192; + layouts[10][53] = 222; + layouts[10][56] = 191; + layouts[11] = layouts[2]; + layouts[12] = layouts[5]; + layouts[13][45] = 219; + layouts[13][46] = 191; + layouts[13][49] = 220; + layouts[13][50] = 220; + layouts[13][51] = 187; + layouts[13][52] = 192; + layouts[13][53] = 222; + layouts[15][46] = 191; + layouts[15][47] = 219; + layouts[15][48] = 221; + layouts[15][49] = 220; + layouts[15][50] = 220; + layouts[15][51] = 186; + layouts[15][53] = 192; + layouts[16][45] = 189; + layouts[16][46] = 187; + layouts[16][47] = 219; + layouts[16][48] = 221; + layouts[16][49] = 220; + layouts[16][50] = 220; + layouts[16][51] = 186; + layouts[16][53] = 192; + layouts[16][56] = 191; + layouts[17] = layouts[5]; + layouts[18] = layouts[3]; + layouts[20][45] = 223; + layouts[20][46] = 189; + layouts[20][47] = 219; + layouts[20][48] = 221; + layouts[20][49] = 188; + layouts[20][50] = 188; + layouts[20][51] = 186; + layouts[20][52] = 190; + layouts[20][53] = 192; + layouts[20][54] = 191; + layouts[20][55] = 220; + layouts[20][56] = 190; + layouts[21][45] = 189; + layouts[21][46] = 222; + layouts[21][47] = 192; + layouts[21][48] = 219; + layouts[21][49] = 221; + layouts[21][50] = 221; + layouts[21][51] = 187; + layouts[21][52] = 186; + layouts[21][53] = 243; + layouts[21][56] = 191; + layouts[21][100] = 255; + layouts[21][135] = 226; + layouts[21][136] = 242; + layouts[21][137] = 220; + layouts[21][138] = 28; + layouts[21][139] = 247; + layouts[22] = layouts[21]; + layouts[23][45] = 189; + layouts[23][46] = 187; + layouts[23][47] = 219; + layouts[23][48] = 221; + layouts[23][49] = 220; + layouts[23][50] = 220; + layouts[23][51] = 186; + layouts[23][53] = 192; + layouts[23][56] = 191; + layouts[23][136] = 21; + layouts[23][138] = 25; + layouts[24] = layouts[3]; + layouts[25] = layouts[3]; + layouts[26] = layouts[3]; + layouts[27] = layouts[3]; + layouts[28] = layouts[3]; + layouts[29] = layouts[16]; + layouts[30] = layouts[16]; + layouts[31][28] = 90; + layouts[31][29] = 89; + layouts[31][46] = 191; + layouts[31][47] = 219; + layouts[31][48] = 221; + layouts[31][49] = 220; + layouts[31][50] = 220; + layouts[31][51] = 186; + layouts[31][53] = 192; + layouts[32] = layouts[16]; + layouts[33] = layouts[16]; + layouts[34] = layouts[16]; + layouts[35] = layouts[16]; + layouts[36][28] = 90; + layouts[36][29] = 89; + layouts[36][39] = 192; + layouts[36][45] = 191; + layouts[36][46] = 187; + layouts[36][47] = 219; + layouts[36][48] = 221; + layouts[36][49] = 220; + layouts[36][50] = 220; + layouts[36][51] = 186; + layouts[36][53] = 48; + layouts[37] = layouts[3]; + layouts[38] = layouts[3]; + return layouts; +}(); + +constexpr int kErrInvalidKeyboardType = static_cast<int>(0x80BC001Du); constexpr std::uint32_t kIgnoredStateBits = 0x4000000u; char16_t UsCharacter(std::uint16_t keycode) { @@ -32,10 +244,14 @@ int APS5_VABI sceConvertKeycodeGetImeKeyboardType(int32_t userId, uint32_t* type *type = 0; return 0; } -int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint64_t a0, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3) { - APS5_HIT("CONVERTKEYCODE", "sceConvertKeycodeGetVirtualKeycode args 0x%llx 0x%llx 0x%llx 0x%llx -> error (signature unknown)", - static_cast<unsigned long long>(a0), static_cast<unsigned long long>(a1), static_cast<unsigned long long>(a2), static_cast<unsigned long long>(a3)); - return kErrInvalidAddress; +int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uint32_t keyboardType, std::uint16_t* virtualKeycode) { + if (virtualKeycode == nullptr) return kErrInvalidAddress; + if (keyboardType == 0 || keyboardType > VirtualKeys.size()) return kErrInvalidKeyboardType; + if (keyboardType >= 31) throw std::runtime_error("sceConvertKeycodeGetVirtualKeycode: guest sdk version is unavailable"); + auto value = keycode < BaseVirtualKeys.size() ? VirtualKeys[keyboardType - 1][keycode] : 255; + if ((keyboardType == 2 || keyboardType == 3 || keyboardType == 12) && (value == 89 || value == 90)) throw std::runtime_error("sceConvertKeycodeGetVirtualKeycode: guest sdk version is unavailable"); + *virtualKeycode = static_cast<std::uint16_t>(value); + return 0; } int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* keyData, std::int32_t index, std::uint32_t state, std::uint32_t keyboardType, char16_t* character) { diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 0e7095e097..1f2ec31789 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -3,8 +3,10 @@ #include <cstdint> #include <cstdlib> #include <initializer_list> +#include <stdexcept> extern "C" { +int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t, std::uint32_t, std::uint16_t*); int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::uint32_t, char16_t*); } @@ -18,6 +20,27 @@ int main() { Require(sceConvertKeycodeGetImeKeyboardType(0, nullptr) == invalidAddress); Require(sceConvertKeycodeGetImeKeyboardType(-1, nullptr) == invalidAddress); + std::uint16_t virtualKey = 0xABCD; + Require(sceConvertKeycodeGetVirtualKeycode(4, 0, nullptr) == invalidAddress); + Require(sceConvertKeycodeGetVirtualKeycode(4, 0, &virtualKey) == static_cast<int>(0x80BC001Du)); + Require(virtualKey == 0xABCD); + Require(sceConvertKeycodeGetVirtualKeycode(4, 40, &virtualKey) == static_cast<int>(0x80BC001Du)); + Require(sceConvertKeycodeGetVirtualKeycode(4, 4, &virtualKey) == 0 && virtualKey == 65); + Require(sceConvertKeycodeGetVirtualKeycode(4, 9, &virtualKey) == 0 && virtualKey == 81); + Require(sceConvertKeycodeGetVirtualKeycode(100, 4, &virtualKey) == 0 && virtualKey == 255); + Require(sceConvertKeycodeGetVirtualKeycode(136, 24, &virtualKey) == 0 && virtualKey == 21); + Require(sceConvertKeycodeGetVirtualKeycode(65535, 4, &virtualKey) == 0 && virtualKey == 255); + for (std::uint32_t layout : {2u, 3u, 12u, 31u, 39u}) { + virtualKey = 0xABCD; + bool rejected = false; + try { + sceConvertKeycodeGetVirtualKeycode(28, layout, &virtualKey); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && virtualKey == 0xABCD); + } + std::uint32_t type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); Require(type == 0xFFFFFFFFu); diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 706e31a66e..fbf5f9d6cd 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -7,3 +7,5 @@ ### Functional - [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) takes the keycode as a USB HID keyboard usage and returns the unshifted character of the US layout for letters, digits, punctuation, space and the keypad. A keycode without a character returns 0 with the character 0; whether the console returns an error instead is not measured. + +- `sceConvertKeycodeGetVirtualKeycode`: HID-to-virtual-key mappings and the three-argument signature are confirmed. Layouts 31–39 and version-dependent Y/Z mappings for layouts 2, 3 and 12 throw until the guest platform and compiled SDK version are exposed; no version is guessed. From b5dbd377ed62b5b7f1bcc57ed8d69895dbb2eaa8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:12:35 +0300 Subject: [PATCH 0589/1360] fix(keycode): restore layout modifiers and character conversion --- core/libs/prx/libSceConvertKeycode/Export.cpp | 2194 ++++++++++++++++- core/libs/tests/GuestConvertKeycode.cpp | 70 +- docs/dev/debt/libSceConvertKeycode.md | 9 +- 3 files changed, 2218 insertions(+), 55 deletions(-) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 7eb667382d..a38cb0293c 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,7 +1,6 @@ #include <array> #include <cstdint> #include <stdexcept> -#include <string> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -220,19 +219,2179 @@ constexpr auto VirtualKeys = [] { }(); constexpr int kErrInvalidKeyboardType = static_cast<int>(0x80BC001Du); -constexpr std::uint32_t kIgnoredStateBits = 0x4000000u; +constexpr auto CharacterTables = [] { + std::array<std::array<std::uint32_t, 256>, 111> tables{}; + tables[1][4] = 327777; + tables[1][5] = 327778; + tables[1][6] = 327779; + tables[1][7] = 327780; + tables[1][8] = 327781; + tables[1][9] = 327782; + tables[1][10] = 327783; + tables[1][11] = 327784; + tables[1][12] = 327785; + tables[1][13] = 327786; + tables[1][14] = 327787; + tables[1][15] = 327788; + tables[1][16] = 327789; + tables[1][17] = 327790; + tables[1][18] = 327791; + tables[1][19] = 327792; + tables[1][20] = 327793; + tables[1][21] = 327794; + tables[1][22] = 327795; + tables[1][23] = 327796; + tables[1][24] = 327797; + tables[1][25] = 327798; + tables[1][26] = 327799; + tables[1][27] = 327800; + tables[1][28] = 327802; + tables[1][29] = 327801; + tables[1][30] = 262193; + tables[1][31] = 262194; + tables[1][32] = 262195; + tables[1][33] = 262196; + tables[1][34] = 262197; + tables[1][35] = 262198; + tables[1][36] = 262199; + tables[1][37] = 262200; + tables[1][38] = 262201; + tables[1][39] = 262192; + tables[1][40] = 262154; + tables[1][42] = 262152; + tables[1][43] = 262153; + tables[1][44] = 262176; + tables[1][45] = 262183; + tables[1][46] = 262238; + tables[1][47] = 262396; + tables[1][48] = 262312; + tables[1][49] = 262180; + tables[1][50] = 262180; + tables[1][51] = 262390; + tables[1][52] = 262372; + tables[1][53] = 262311; + tables[1][54] = 262188; + tables[1][55] = 262190; + tables[1][56] = 262189; + tables[1][83] = 393299; + tables[1][84] = 393263; + tables[1][85] = 393258; + tables[1][86] = 393261; + tables[1][87] = 393259; + tables[1][88] = 393226; + tables[1][89] = 393293; + tables[1][90] = 393297; + tables[1][91] = 393294; + tables[1][92] = 393296; + tables[1][93] = 393309; + tables[1][94] = 393295; + tables[1][95] = 393290; + tables[1][96] = 393298; + tables[1][97] = 393291; + tables[1][98] = 393289; + tables[1][99] = 393292; + tables[1][100] = 262204; + tables[1][103] = 393277; + tables[1][133] = 393260; + tables[1][134] = 393277; + tables[1][140] = 393260; + tables[2] = tables[1]; + tables[2][4] = 327745; + tables[2][5] = 327746; + tables[2][6] = 327747; + tables[2][7] = 327748; + tables[2][8] = 327749; + tables[2][9] = 327750; + tables[2][10] = 327751; + tables[2][11] = 327752; + tables[2][12] = 327753; + tables[2][13] = 327754; + tables[2][14] = 327755; + tables[2][15] = 327756; + tables[2][16] = 327757; + tables[2][17] = 327758; + tables[2][18] = 327759; + tables[2][19] = 327760; + tables[2][20] = 327761; + tables[2][21] = 327762; + tables[2][22] = 327763; + tables[2][23] = 327764; + tables[2][24] = 327765; + tables[2][25] = 327766; + tables[2][26] = 327767; + tables[2][27] = 327768; + tables[2][28] = 327770; + tables[2][29] = 327769; + tables[2][30] = 262187; + tables[2][31] = 262178; + tables[2][32] = 262186; + tables[2][33] = 262375; + tables[2][34] = 262181; + tables[2][35] = 262182; + tables[2][36] = 262191; + tables[2][37] = 262184; + tables[2][38] = 262185; + tables[2][39] = 262205; + tables[2][45] = 262207; + tables[2][46] = 262240; + tables[2][47] = 262376; + tables[2][48] = 262177; + tables[2][49] = 262307; + tables[2][50] = 262307; + tables[2][51] = 262377; + tables[2][52] = 262368; + tables[2][53] = 262320; + tables[2][54] = 262203; + tables[2][55] = 262202; + tables[2][56] = 262239; + tables[2][89] = 393265; + tables[2][90] = 393266; + tables[2][91] = 393267; + tables[2][92] = 393268; + tables[2][93] = 393269; + tables[2][94] = 393270; + tables[2][95] = 393271; + tables[2][96] = 393272; + tables[2][97] = 393273; + tables[2][98] = 393264; + tables[2][99] = 393262; + tables[2][100] = 262206; + tables[3][8] = 336044; + tables[3][30] = 262310; + tables[3][31] = 262208; + tables[3][32] = 262179; + tables[3][33] = 262320; + tables[3][34] = 262311; + tables[3][35] = 262316; + tables[3][36] = 262268; + tables[3][37] = 262306; + tables[3][45] = 262324; + tables[3][46] = 262270; + tables[3][47] = 262235; + tables[3][48] = 262237; + tables[3][49] = 262269; + tables[3][50] = 262269; + tables[3][52] = 262267; + tables[3][100] = 262236; + tables[3][136] = 262324; + tables[3][137] = 262324; + tables[3][138] = 262324; + tables[3][146] = 262324; + tables[3][147] = 262324; + tables[4] = tables[1]; + tables[4][28] = 327801; + tables[4][29] = 327802; + tables[4][45] = 262189; + tables[4][46] = 262205; + tables[4][47] = 262235; + tables[4][48] = 262237; + tables[4][49] = 262236; + tables[4][50] = 262236; + tables[4][51] = 262203; + tables[4][52] = 262183; + tables[4][53] = 262240; + tables[4][56] = 262191; + tables[4][100] = 262236; + tables[5] = tables[2]; + tables[5][28] = 327769; + tables[5][29] = 327770; + tables[5][30] = 262177; + tables[5][31] = 262208; + tables[5][32] = 262179; + tables[5][33] = 262180; + tables[5][35] = 262238; + tables[5][36] = 262182; + tables[5][37] = 262186; + tables[5][38] = 262184; + tables[5][39] = 262185; + tables[5][45] = 262239; + tables[5][46] = 262187; + tables[5][47] = 262267; + tables[5][48] = 262269; + tables[5][49] = 262268; + tables[5][50] = 262268; + tables[5][51] = 262202; + tables[5][52] = 262178; + tables[5][53] = 262270; + tables[5][54] = 262204; + tables[5][55] = 262206; + tables[5][56] = 262207; + tables[5][100] = 262268; + tables[6][34] = 270508; + tables[7] = tables[1]; + tables[7][4] = 393025; + tables[7][5] = 393026; + tables[7][6] = 393027; + tables[7][7] = 393028; + tables[7][8] = 393029; + tables[7][9] = 393030; + tables[7][10] = 393031; + tables[7][11] = 393032; + tables[7][12] = 393033; + tables[7][13] = 393034; + tables[7][14] = 393035; + tables[7][15] = 393036; + tables[7][16] = 393037; + tables[7][17] = 393038; + tables[7][18] = 393039; + tables[7][19] = 393040; + tables[7][20] = 393041; + tables[7][21] = 393042; + tables[7][22] = 393043; + tables[7][23] = 393044; + tables[7][24] = 393045; + tables[7][25] = 393046; + tables[7][26] = 393047; + tables[7][27] = 393048; + tables[7][28] = 393049; + tables[7][29] = 393050; + tables[7][30] = 327441; + tables[7][31] = 327442; + tables[7][32] = 327443; + tables[7][33] = 327444; + tables[7][34] = 327445; + tables[7][35] = 327446; + tables[7][36] = 327447; + tables[7][37] = 327448; + tables[7][38] = 327449; + tables[7][39] = 327440; + tables[7][44] = 274432; + tables[7][45] = 327437; + tables[7][46] = 327486; + tables[7][47] = 327456; + tables[7][48] = 327483; + tables[7][49] = 327485; + tables[7][50] = 327485; + tables[7][51] = 327451; + tables[7][52] = 327450; + tables[7][53] = 0; + tables[7][54] = 327436; + tables[7][55] = 327438; + tables[7][56] = 327439; + tables[7][100] = 0; + tables[7][135] = 327653; + tables[8] = tables[2]; + tables[8][4] = 392993; + tables[8][5] = 392994; + tables[8][6] = 392995; + tables[8][7] = 392996; + tables[8][8] = 392997; + tables[8][9] = 392998; + tables[8][10] = 392999; + tables[8][11] = 393000; + tables[8][12] = 393001; + tables[8][13] = 393002; + tables[8][14] = 393003; + tables[8][15] = 393004; + tables[8][16] = 393005; + tables[8][17] = 393006; + tables[8][18] = 393007; + tables[8][19] = 393008; + tables[8][20] = 393009; + tables[8][21] = 393010; + tables[8][22] = 393011; + tables[8][23] = 393012; + tables[8][24] = 393013; + tables[8][25] = 393014; + tables[8][26] = 393015; + tables[8][27] = 393016; + tables[8][28] = 393017; + tables[8][29] = 393018; + tables[8][30] = 327425; + tables[8][31] = 270364; + tables[8][32] = 327427; + tables[8][33] = 327428; + tables[8][34] = 327429; + tables[8][35] = 327430; + tables[8][36] = 270360; + tables[8][37] = 327432; + tables[8][38] = 327433; + tables[8][39] = 0; + tables[8][45] = 327453; + tables[8][46] = 327518; + tables[8][47] = 327488; + tables[8][48] = 327515; + tables[8][49] = 327517; + tables[8][50] = 327517; + tables[8][51] = 327435; + tables[8][52] = 327434; + tables[8][53] = 0; + tables[8][54] = 327452; + tables[8][55] = 327454; + tables[8][56] = 327455; + tables[8][100] = 0; + tables[8][135] = 327487; + tables[8][137] = 327516; + tables[9] = tables[4]; + tables[9][46] = 262238; + tables[9][47] = 262208; + tables[9][48] = 262235; + tables[9][49] = 262237; + tables[9][50] = 262237; + tables[9][52] = 262202; + tables[9][53] = 0; + tables[9][100] = 0; + tables[9][135] = 262236; + tables[9][137] = 262309; + tables[10] = tables[5]; + tables[10][31] = 262178; + tables[10][35] = 262182; + tables[10][36] = 262183; + tables[10][37] = 262184; + tables[10][38] = 262185; + tables[10][39] = 0; + tables[10][45] = 262205; + tables[10][46] = 262270; + tables[10][47] = 262240; + tables[10][48] = 262267; + tables[10][49] = 262269; + tables[10][50] = 262269; + tables[10][51] = 262187; + tables[10][52] = 262186; + tables[10][53] = 0; + tables[10][100] = 0; + tables[10][135] = 262239; + tables[10][137] = 262268; + tables[11] = tables[9]; + tables[11][4] = 274625; + tables[11][5] = 274611; + tables[11][6] = 274621; + tables[11][7] = 274615; + tables[11][8] = 274596; + tables[11][9] = 274639; + tables[11][10] = 274605; + tables[11][11] = 274607; + tables[11][12] = 274635; + tables[11][13] = 274654; + tables[11][14] = 274638; + tables[11][15] = 274666; + tables[11][16] = 274658; + tables[11][17] = 274655; + tables[11][18] = 274665; + tables[11][19] = 274619; + tables[11][20] = 274623; + tables[11][21] = 274617; + tables[11][22] = 274632; + tables[11][23] = 274603; + tables[11][24] = 274634; + tables[11][25] = 274642; + tables[11][26] = 274630; + tables[11][27] = 274613; + tables[11][28] = 274675; + tables[11][29] = 274628; + tables[11][30] = 274636; + tables[11][31] = 274645; + tables[11][32] = 274594; + tables[11][33] = 274598; + tables[11][34] = 274600; + tables[11][35] = 274602; + tables[11][36] = 274660; + tables[11][37] = 274662; + tables[11][38] = 274664; + tables[11][39] = 274671; + tables[11][45] = 274651; + tables[11][46] = 274648; + tables[11][47] = 274587; + tables[11][48] = 274588; + tables[11][49] = 274656; + tables[11][50] = 274656; + tables[11][51] = 274668; + tables[11][52] = 274609; + tables[11][54] = 274637; + tables[11][55] = 274667; + tables[11][56] = 274657; + tables[11][135] = 274669; + tables[11][137] = 274684; + tables[12] = tables[11]; + tables[12][8] = 274595; + tables[12][29] = 274627; + tables[12][32] = 274593; + tables[12][33] = 274597; + tables[12][34] = 274599; + tables[12][35] = 274601; + tables[12][36] = 274659; + tables[12][37] = 274661; + tables[12][38] = 274663; + tables[12][39] = 274674; + tables[12][48] = 274444; + tables[12][49] = 274445; + tables[12][50] = 274445; + tables[12][54] = 274433; + tables[12][55] = 274434; + tables[12][56] = 274683; + tables[12][89] = 393265; + tables[12][90] = 393266; + tables[12][91] = 393267; + tables[12][92] = 393268; + tables[12][93] = 393269; + tables[12][94] = 393270; + tables[12][95] = 393271; + tables[12][96] = 393272; + tables[12][97] = 393273; + tables[12][98] = 393264; + tables[12][99] = 393262; + tables[13] = tables[9]; + tables[13][4] = 327553; + tables[13][5] = 327546; + tables[13][6] = 327551; + tables[13][7] = 327548; + tables[13][8] = 327538; + tables[13][9] = 327562; + tables[13][10] = 327543; + tables[13][11] = 327544; + tables[13][12] = 327558; + tables[13][13] = 327567; + tables[13][14] = 327561; + tables[13][15] = 327576; + tables[13][16] = 327571; + tables[13][17] = 327568; + tables[13][18] = 327575; + tables[13][19] = 327550; + tables[13][20] = 327552; + tables[13][21] = 327549; + tables[13][22] = 327556; + tables[13][23] = 327542; + tables[13][24] = 327557; + tables[13][25] = 327563; + tables[13][26] = 327555; + tables[13][27] = 327547; + tables[13][28] = 327581; + tables[13][29] = 327554; + tables[13][30] = 327559; + tables[13][31] = 327564; + tables[13][32] = 327537; + tables[13][33] = 327539; + tables[13][34] = 327540; + tables[13][35] = 327541; + tables[13][36] = 327572; + tables[13][37] = 327573; + tables[13][38] = 327574; + tables[13][39] = 327580; + tables[13][45] = 327566; + tables[13][46] = 327565; + tables[13][47] = 327582; + tables[13][48] = 327583; + tables[13][49] = 327569; + tables[13][50] = 327569; + tables[13][51] = 327578; + tables[13][52] = 327545; + tables[13][54] = 327560; + tables[13][55] = 327577; + tables[13][56] = 327570; + tables[13][135] = 327579; + tables[13][137] = 327536; + tables[14] = tables[13]; + tables[14][8] = 327528; + tables[14][29] = 327535; + tables[14][32] = 327527; + tables[14][33] = 327529; + tables[14][34] = 327530; + tables[14][35] = 327531; + tables[14][36] = 327532; + tables[14][37] = 327533; + tables[14][38] = 327534; + tables[14][39] = 327526; + tables[14][48] = 327522; + tables[14][49] = 327523; + tables[14][50] = 327523; + tables[14][54] = 327524; + tables[14][55] = 327521; + tables[14][56] = 327525; + tables[14][89] = 393265; + tables[14][90] = 393266; + tables[14][91] = 393267; + tables[14][92] = 393268; + tables[14][93] = 393269; + tables[14][94] = 393270; + tables[14][95] = 393271; + tables[14][96] = 393272; + tables[14][97] = 393273; + tables[14][98] = 393264; + tables[14][99] = 393262; + tables[15] = tables[11]; + tables[15][4] = 274529; + tables[15][5] = 274515; + tables[15][6] = 274525; + tables[15][7] = 274519; + tables[15][8] = 274500; + tables[15][9] = 274543; + tables[15][10] = 274509; + tables[15][11] = 274511; + tables[15][12] = 274539; + tables[15][13] = 274558; + tables[15][14] = 274542; + tables[15][15] = 274570; + tables[15][16] = 274562; + tables[15][17] = 274559; + tables[15][18] = 274569; + tables[15][19] = 274523; + tables[15][20] = 274527; + tables[15][21] = 274521; + tables[15][22] = 274536; + tables[15][23] = 274507; + tables[15][24] = 274538; + tables[15][25] = 274546; + tables[15][26] = 274534; + tables[15][27] = 274517; + tables[15][28] = 274579; + tables[15][29] = 274532; + tables[15][30] = 274540; + tables[15][31] = 274549; + tables[15][32] = 274498; + tables[15][33] = 274502; + tables[15][34] = 274504; + tables[15][35] = 274506; + tables[15][36] = 274564; + tables[15][37] = 274566; + tables[15][38] = 274568; + tables[15][39] = 274575; + tables[15][45] = 274555; + tables[15][46] = 274552; + tables[15][49] = 274560; + tables[15][50] = 274560; + tables[15][51] = 274572; + tables[15][52] = 274513; + tables[15][54] = 274541; + tables[15][55] = 274571; + tables[15][56] = 274561; + tables[15][135] = 274573; + tables[16] = tables[15]; + tables[16][8] = 274499; + tables[16][29] = 274531; + tables[16][32] = 274497; + tables[16][33] = 274501; + tables[16][34] = 274503; + tables[16][35] = 274505; + tables[16][36] = 274563; + tables[16][37] = 274565; + tables[16][38] = 274567; + tables[16][39] = 274578; + tables[16][48] = 274444; + tables[16][49] = 274445; + tables[16][50] = 274445; + tables[16][54] = 274433; + tables[16][55] = 274434; + tables[16][56] = 274683; + tables[16][89] = 393265; + tables[16][90] = 393266; + tables[16][91] = 393267; + tables[16][92] = 393268; + tables[16][93] = 393269; + tables[16][94] = 393270; + tables[16][95] = 393271; + tables[16][96] = 393272; + tables[16][97] = 393273; + tables[16][98] = 393264; + tables[16][99] = 393262; + tables[17] = tables[1]; + tables[17][30] = 327729; + tables[17][31] = 327730; + tables[17][32] = 327731; + tables[17][33] = 327732; + tables[17][34] = 327733; + tables[17][35] = 327734; + tables[17][36] = 327735; + tables[17][37] = 327736; + tables[17][38] = 327737; + tables[17][39] = 327728; + tables[17][45] = 327903; + tables[17][46] = 262324; + tables[17][47] = 327932; + tables[17][48] = 327723; + tables[17][49] = 327715; + tables[17][50] = 327715; + tables[17][51] = 327926; + tables[17][52] = 327908; + tables[17][53] = 262238; + tables[17][54] = 327724; + tables[17][55] = 327726; + tables[17][136] = 262238; + tables[17][137] = 262238; + tables[17][138] = 262238; + tables[17][146] = 262238; + tables[17][147] = 262238; + tables[18] = tables[2]; + tables[18][30] = 327713; + tables[18][31] = 327714; + tables[18][32] = 327847; + tables[18][33] = 327716; + tables[18][34] = 327717; + tables[18][35] = 327718; + tables[18][36] = 327727; + tables[18][37] = 327720; + tables[18][38] = 327721; + tables[18][39] = 327741; + tables[18][45] = 327743; + tables[18][47] = 327900; + tables[18][48] = 327722; + tables[18][49] = 327719; + tables[18][50] = 327719; + tables[18][51] = 327894; + tables[18][52] = 327876; + tables[18][54] = 327739; + tables[18][55] = 327738; + tables[19][8] = 336044; + tables[19][16] = 327861; + tables[19][20] = 327744; + tables[19][31] = 327858; + tables[19][32] = 327859; + tables[19][36] = 327803; + tables[19][37] = 327771; + tables[19][38] = 327773; + tables[19][39] = 327805; + tables[19][45] = 327772; + tables[19][48] = 327806; + tables[19][100] = 262268; + tables[20] = tables[1]; + tables[20][28] = 327801; + tables[20][29] = 327802; + tables[20][46] = 262305; + tables[20][47] = 262240; + tables[20][48] = 262187; + tables[20][49] = 327911; + tables[20][50] = 327911; + tables[20][51] = 327921; + tables[20][52] = 262324; + tables[20][53] = 262330; + tables[21] = tables[2]; + tables[21][28] = 327769; + tables[21][29] = 327770; + tables[21][30] = 262177; + tables[21][32] = 262327; + tables[21][33] = 262180; + tables[21][46] = 262335; + tables[21][47] = 262238; + tables[21][48] = 262186; + tables[21][49] = 327879; + tables[21][50] = 327879; + tables[21][51] = 327889; + tables[21][52] = 262312; + tables[21][53] = 262314; + tables[22] = tables[3]; + tables[22][30] = 262268; + tables[22][33] = 262270; + tables[22][34] = 262272; + tables[22][36] = 0; + tables[22][37] = 0; + tables[22][45] = 0; + tables[22][46] = 0; + tables[22][49] = 327805; + tables[22][50] = 327805; + tables[22][53] = 262236; + tables[22][100] = 0; + tables[22][136] = 262270; + tables[22][137] = 262270; + tables[22][138] = 262270; + tables[22][146] = 262270; + tables[22][147] = 262270; + tables[23] = tables[17]; + tables[23][4] = 327793; + tables[23][16] = 327724; + tables[23][20] = 327777; + tables[23][26] = 327802; + tables[23][28] = 327801; + tables[23][29] = 327799; + tables[23][30] = 327718; + tables[23][31] = 327913; + tables[23][32] = 327714; + tables[23][33] = 327719; + tables[23][34] = 327720; + tables[23][35] = 327725; + tables[23][36] = 327912; + tables[23][37] = 327775; + tables[23][38] = 327911; + tables[23][39] = 327904; + tables[23][45] = 327721; + tables[23][46] = 327741; + tables[23][47] = 327774; + tables[23][48] = 327716; + tables[23][49] = 327722; + tables[23][50] = 327722; + tables[23][51] = 327789; + tables[23][52] = 327929; + tables[23][53] = 262322; + tables[23][54] = 327739; + tables[23][55] = 327738; + tables[23][56] = 327713; + tables[24] = tables[21]; + tables[24][4] = 327761; + tables[24][16] = 327743; + tables[24][20] = 327745; + tables[24][26] = 327770; + tables[24][29] = 327767; + tables[24][30] = 327729; + tables[24][31] = 327730; + tables[24][32] = 327731; + tables[24][33] = 327732; + tables[24][34] = 327733; + tables[24][35] = 327734; + tables[24][36] = 327735; + tables[24][37] = 327736; + tables[24][38] = 327737; + tables[24][39] = 327728; + tables[24][45] = 327856; + tables[24][46] = 327723; + tables[24][47] = 327848; + tables[24][48] = 327843; + tables[24][49] = 327861; + tables[24][50] = 327861; + tables[24][51] = 327757; + tables[24][52] = 327717; + tables[24][53] = 0; + tables[24][54] = 327726; + tables[24][55] = 327727; + tables[24][56] = 327847; + tables[24][136] = 262312; + tables[24][137] = 262312; + tables[24][138] = 262312; + tables[24][146] = 262312; + tables[24][147] = 262312; + tables[25][8] = 336044; + tables[25][31] = 262270; + tables[25][32] = 327715; + tables[25][33] = 327803; + tables[25][34] = 327771; + tables[25][35] = 327804; + tables[25][36] = 262240; + tables[25][37] = 327772; + tables[25][38] = 327774; + tables[25][39] = 327744; + tables[25][45] = 327773; + tables[25][46] = 327805; + tables[25][48] = 327844; + tables[25][136] = 262270; + tables[25][137] = 262270; + tables[25][138] = 262270; + tables[25][146] = 262270; + tables[25][147] = 262270; + tables[26] = tables[20]; + tables[26][46] = 262380; + tables[26][47] = 262376; + tables[26][49] = 262393; + tables[26][50] = 262393; + tables[26][51] = 262386; + tables[26][52] = 262368; + tables[26][53] = 262236; + tables[27] = tables[21]; + tables[27][32] = 262307; + tables[27][46] = 262238; + tables[27][47] = 262377; + tables[27][49] = 262311; + tables[27][50] = 262311; + tables[27][51] = 262375; + tables[27][52] = 262320; + tables[27][53] = 262268; + tables[28] = tables[6]; + tables[28][8] = 336044; + tables[28][47] = 262235; + tables[28][48] = 262237; + tables[28][51] = 262208; + tables[28][52] = 262179; + tables[29][47] = 262267; + tables[29][48] = 262269; + tables[30] = tables[20]; + tables[30][45] = 262191; + tables[30][46] = 262320; + tables[30][47] = 262312; + tables[30][48] = 262186; + tables[30][49] = 262204; + tables[30][50] = 262204; + tables[30][51] = 262187; + tables[30][53] = 262208; + tables[30][100] = 262237; + tables[31] = tables[21]; + tables[31][32] = 262179; + tables[31][36] = 262239; + tables[31][39] = 262183; + tables[31][46] = 262270; + tables[31][48] = 262268; + tables[31][49] = 262206; + tables[31][50] = 262206; + tables[31][51] = 262321; + tables[31][52] = 262240; + tables[31][53] = 262311; + tables[31][56] = 262205; + tables[31][100] = 262235; + tables[31][136] = 262270; + tables[31][137] = 262270; + tables[31][138] = 262270; + tables[31][146] = 262270; + tables[31][147] = 262270; + tables[32][6] = 327842; + tables[32][8] = 336044; + tables[32][16] = 327861; + tables[32][21] = 327862; + tables[32][22] = 327903; + tables[32][27] = 327867; + tables[32][29] = 327851; + tables[32][30] = 262329; + tables[32][31] = 262322; + tables[32][32] = 262323; + tables[32][33] = 262332; + tables[32][34] = 262333; + tables[32][35] = 262334; + tables[32][36] = 262307; + tables[32][37] = 262267; + tables[32][38] = 262269; + tables[32][45] = 262236; + tables[32][46] = 262328; + tables[32][53] = 262316; + tables[32][55] = 262327; + tables[32][100] = 262310; + tables[32][136] = 262328; + tables[32][137] = 262328; + tables[32][138] = 262328; + tables[32][146] = 262328; + tables[32][147] = 262328; + tables[33] = tables[26]; + tables[33][46] = 262315; + tables[33][47] = 262187; + tables[33][48] = 262324; + tables[33][49] = 262270; + tables[33][50] = 262270; + tables[33][51] = 327911; + tables[33][52] = 262330; + tables[34] = tables[27]; + tables[34][32] = 262179; + tables[34][46] = 262331; + tables[34][47] = 262186; + tables[34][48] = 262240; + tables[34][49] = 262238; + tables[34][50] = 262238; + tables[34][51] = 327879; + tables[34][52] = 262314; + tables[35] = tables[6]; + tables[35][8] = 336044; + tables[35][31] = 262208; + tables[35][32] = 262307; + tables[35][33] = 262311; + tables[35][36] = 262267; + tables[35][37] = 262235; + tables[35][38] = 262237; + tables[35][39] = 262269; + tables[35][47] = 262312; + tables[35][48] = 262237; + tables[35][136] = 262312; + tables[35][137] = 262312; + tables[35][138] = 262312; + tables[35][146] = 262312; + tables[35][147] = 262312; + tables[36] = tables[4]; + tables[36][4] = 328772; + tables[36][5] = 328760; + tables[36][6] = 328769; + tables[36][7] = 328754; + tables[36][8] = 328771; + tables[36][9] = 328752; + tables[36][10] = 328767; + tables[36][11] = 328768; + tables[36][12] = 328776; + tables[36][13] = 328766; + tables[36][14] = 328763; + tables[36][15] = 328756; + tables[36][16] = 328780; + tables[36][17] = 328770; + tables[36][18] = 328777; + tables[36][19] = 328759; + tables[36][20] = 328761; + tables[36][21] = 328762; + tables[36][22] = 328779; + tables[36][23] = 328757; + tables[36][24] = 328755; + tables[36][25] = 328764; + tables[36][26] = 328774; + tables[36][27] = 328775; + tables[36][28] = 328765; + tables[36][29] = 328783; + tables[36][47] = 328773; + tables[36][48] = 328778; + tables[36][51] = 328758; + tables[36][52] = 328781; + tables[36][53] = 328785; + tables[36][54] = 328753; + tables[36][55] = 328782; + tables[36][56] = 262190; + tables[37] = tables[5]; + tables[37][4] = 328740; + tables[37][5] = 328728; + tables[37][6] = 328737; + tables[37][7] = 328722; + tables[37][8] = 328739; + tables[37][9] = 328720; + tables[37][10] = 328735; + tables[37][11] = 328736; + tables[37][12] = 328744; + tables[37][13] = 328734; + tables[37][14] = 328731; + tables[37][15] = 328724; + tables[37][16] = 328748; + tables[37][17] = 328738; + tables[37][18] = 328745; + tables[37][19] = 328727; + tables[37][20] = 328729; + tables[37][21] = 328730; + tables[37][22] = 328747; + tables[37][23] = 328725; + tables[37][24] = 328723; + tables[37][25] = 328732; + tables[37][26] = 328742; + tables[37][27] = 328743; + tables[37][28] = 328733; + tables[37][29] = 328751; + tables[37][31] = 262178; + tables[37][32] = 270614; + tables[37][33] = 262203; + tables[37][35] = 262202; + tables[37][36] = 262207; + tables[37][47] = 328741; + tables[37][48] = 328746; + tables[37][49] = 262191; + tables[37][50] = 262191; + tables[37][51] = 328726; + tables[37][52] = 328749; + tables[37][53] = 328705; + tables[37][54] = 328721; + tables[37][55] = 328750; + tables[37][56] = 262188; + tables[37][100] = 262191; + tables[38] = tables[4]; + tables[38][49] = 262179; + tables[38][50] = 262179; + tables[39] = tables[5]; + tables[39][31] = 262178; + tables[39][32] = 262307; + tables[39][49] = 262270; + tables[39][50] = 262270; + tables[39][52] = 262208; + tables[39][53] = 262316; + tables[40][4] = 327905; + tables[40][8] = 327913; + tables[40][12] = 327917; + tables[40][18] = 327923; + tables[40][24] = 327930; + tables[40][33] = 270508; + tables[40][53] = 262310; + tables[41][4] = 327873; + tables[41][8] = 327881; + tables[41][12] = 327885; + tables[41][18] = 327891; + tables[41][24] = 327898; + tables[42] = tables[4]; + tables[42][4] = 332038; + tables[42][5] = 332146; + tables[42][6] = 332046; + tables[42][7] = 332043; + tables[42][8] = 332035; + tables[42][9] = 332037; + tables[42][10] = 332050; + tables[42][11] = 332137; + tables[42][12] = 332131; + tables[42][13] = 332133; + tables[42][14] = 332129; + tables[42][15] = 332149; + tables[42][16] = 332147; + tables[42][17] = 332142; + tables[42][18] = 332130; + tables[42][19] = 332134; + tables[42][20] = 332039; + tables[42][21] = 332032; + tables[42][22] = 332034; + tables[42][23] = 332041; + tables[42][24] = 332135; + tables[42][25] = 332049; + tables[42][26] = 332044; + tables[42][27] = 332048; + tables[42][28] = 332141; + tables[42][29] = 332047; + tables[42][50] = 262237; + tables[42][52] = 270360; + tables[43] = tables[5]; + tables[43][4] = 332038; + tables[43][5] = 332146; + tables[43][6] = 332046; + tables[43][7] = 332043; + tables[43][8] = 332036; + tables[43][9] = 332037; + tables[43][10] = 332050; + tables[43][11] = 332137; + tables[43][12] = 332131; + tables[43][13] = 332133; + tables[43][14] = 332129; + tables[43][15] = 332149; + tables[43][16] = 332147; + tables[43][17] = 332142; + tables[43][18] = 332132; + tables[43][19] = 332136; + tables[43][20] = 332040; + tables[43][21] = 332033; + tables[43][22] = 332034; + tables[43][23] = 332042; + tables[43][24] = 332135; + tables[43][25] = 332049; + tables[43][26] = 332045; + tables[43][27] = 332048; + tables[43][28] = 332141; + tables[43][29] = 332047; + tables[43][50] = 262269; + tables[43][52] = 270364; + tables[44] = tables[20]; + tables[44][45] = 262187; + tables[44][46] = 262236; + tables[44][47] = 327909; + tables[44][48] = 262312; + tables[44][49] = 262183; + tables[44][50] = 262183; + tables[44][51] = 327928; + tables[44][52] = 327910; + tables[44][53] = 262268; + tables[45] = tables[34]; + tables[45][33] = 262308; + tables[45][46] = 262240; + tables[45][47] = 327877; + tables[45][48] = 262238; + tables[45][49] = 262186; + tables[45][50] = 262186; + tables[45][51] = 327896; + tables[45][52] = 327878; + tables[45][53] = 262311; + tables[45][136] = 262240; + tables[45][137] = 262240; + tables[45][138] = 262240; + tables[45][146] = 262240; + tables[45][147] = 262240; + tables[46] = tables[35]; + tables[46][16] = 327861; + tables[46][33] = 262180; + tables[46][46] = 262324; + tables[46][47] = 0; + tables[46][48] = 262270; + tables[46][136] = 262324; + tables[46][137] = 262324; + tables[46][138] = 262324; + tables[46][146] = 262324; + tables[46][147] = 262324; + tables[47] = tables[44]; + tables[47][46] = 262324; + tables[47][51] = 327926; + tables[47][52] = 327908; + tables[47][53] = 262311; + tables[47][136] = 262312; + tables[47][137] = 262312; + tables[47][138] = 262312; + tables[47][146] = 262312; + tables[47][147] = 262312; + tables[48] = tables[45]; + tables[48][51] = 327894; + tables[48][52] = 327876; + tables[48][53] = 262333; + tables[48][136] = 262238; + tables[48][137] = 262238; + tables[48][138] = 262238; + tables[48][146] = 262238; + tables[48][147] = 262238; + tables[49] = tables[46]; + tables[49][45] = 262236; + tables[49][46] = 0; + tables[49][100] = 262268; + tables[49][136] = 262270; + tables[49][137] = 262270; + tables[49][138] = 262270; + tables[49][146] = 262270; + tables[49][147] = 262270; + tables[50] = tables[47]; + tables[50][51] = 327910; + tables[50][52] = 327928; + tables[50][53] = 262333; + tables[50][136] = 262324; + tables[50][137] = 262324; + tables[50][138] = 262324; + tables[50][146] = 262324; + tables[50][147] = 262324; + tables[51] = tables[45]; + tables[51][51] = 327878; + tables[51][52] = 327896; + tables[52] = tables[46]; + tables[52][34] = 262272; + tables[52][46] = 262268; + tables[52][100] = 262236; + tables[52][136] = 0; + tables[52][137] = 0; + tables[52][138] = 0; + tables[52][146] = 0; + tables[52][147] = 0; + tables[53] = tables[38]; + tables[53][4] = 340231; + tables[53][5] = 340246; + tables[53][6] = 340239; + tables[53][7] = 340238; + tables[53][8] = 340237; + tables[53][9] = 340241; + tables[53][10] = 340245; + tables[53][11] = 340248; + tables[53][12] = 340251; + tables[53][13] = 340264; + tables[53][14] = 340252; + tables[53][15] = 340256; + tables[53][16] = 340265; + tables[53][17] = 340249; + tables[53][18] = 340255; + tables[53][19] = 340259; + tables[53][20] = 340230; + tables[53][21] = 340240; + tables[53][22] = 340235; + tables[53][23] = 340244; + tables[53][24] = 340263; + tables[53][25] = 340242; + tables[53][26] = 340234; + tables[53][27] = 340236; + tables[53][28] = 340247; + tables[53][29] = 340232; + tables[53][30] = 274693; + tables[53][31] = 274697; + tables[53][32] = 262855; + tables[53][33] = 262859; + tables[53][34] = 274707; + tables[53][35] = 262858; + tables[53][36] = 262873; + tables[53][37] = 274714; + tables[53][38] = 274718; + tables[53][39] = 274722; + tables[53][45] = 274726; + tables[53][51] = 274724; + tables[53][52] = 270360; + tables[53][54] = 274717; + tables[53][55] = 274721; + tables[53][56] = 274725; + tables[54] = tables[43]; + tables[54][4] = 327777; + tables[54][5] = 327778; + tables[54][6] = 327779; + tables[54][7] = 327780; + tables[54][8] = 327781; + tables[54][9] = 327782; + tables[54][10] = 327783; + tables[54][11] = 327784; + tables[54][12] = 327785; + tables[54][13] = 327786; + tables[54][14] = 327787; + tables[54][15] = 327788; + tables[54][16] = 327789; + tables[54][17] = 327790; + tables[54][18] = 327791; + tables[54][19] = 327792; + tables[54][20] = 327793; + tables[54][21] = 327794; + tables[54][22] = 327795; + tables[54][23] = 327796; + tables[54][24] = 327797; + tables[54][25] = 327798; + tables[54][26] = 327799; + tables[54][27] = 327800; + tables[54][28] = 327801; + tables[54][29] = 327802; + tables[54][50] = 262179; + tables[54][100] = 262236; + tables[55] = tables[5]; + tables[55][52] = 270364; + tables[56] = tables[7]; + tables[56][46] = 327453; + tables[56][47] = 327483; + tables[56][48] = 327485; + tables[56][49] = 327484; + tables[56][50] = 327484; + tables[56][52] = 270360; + tables[56][53] = 270360; + tables[56][100] = 327653; + tables[56][135] = 0; + tables[57] = tables[8]; + tables[57][31] = 327456; + tables[57][35] = 327486; + tables[57][36] = 327430; + tables[57][37] = 327434; + tables[57][38] = 327432; + tables[57][39] = 327433; + tables[57][45] = 327487; + tables[57][46] = 327435; + tables[57][47] = 327515; + tables[57][48] = 327517; + tables[57][49] = 327516; + tables[57][50] = 327516; + tables[57][51] = 327450; + tables[57][52] = 270364; + tables[57][53] = 327518; + tables[57][100] = 327516; + tables[57][135] = 0; + tables[57][137] = 0; + tables[58] = tables[42]; + tables[58][4] = 353765; + tables[58][5] = 354056; + tables[58][6] = 365009; + tables[58][7] = 354088; + tables[58][8] = 355380; + tables[58][9] = 356459; + tables[58][10] = 349983; + tables[58][11] = 359161; + tables[58][12] = 352776; + tables[58][13] = 348993; + tables[58][14] = 350503; + tables[58][15] = 347693; + tables[58][16] = 347648; + tables[58][17] = 352019; + tables[58][18] = 347834; + tables[58][19] = 352195; + tables[58][20] = 352843; + tables[58][21] = 349155; + tables[58][22] = 351288; + tables[58][23] = 351999; + tables[58][24] = 351345; + tables[58][25] = 350579; + tables[58][26] = 357680; + tables[58][27] = 366307; + tables[58][28] = 349020; + tables[58][29] = 365005; + tables[58][50] = 262179; + tables[59] = tables[1]; + tables[59][47] = 262376; + tables[59][51] = 262377; + tables[59][52] = 262368; + tables[60] = tables[2]; + tables[60][47] = 262396; + tables[60][51] = 262390; + tables[60][52] = 262372; + tables[61] = tables[4]; + tables[61][47] = 262238; + tables[61][48] = 262328; + tables[61][49] = 262204; + tables[61][50] = 262204; + tables[61][52] = 262240; + tables[61][53] = 262179; + tables[61][56] = 327913; + tables[61][100] = 262315; + tables[61][136] = 262240; + tables[61][137] = 262240; + tables[61][138] = 262240; + tables[61][146] = 262240; + tables[61][147] = 262240; + tables[62] = tables[39]; + tables[62][32] = 262191; + tables[62][35] = 262207; + tables[62][47] = 262238; + tables[62][48] = 262312; + tables[62][49] = 262206; + tables[62][50] = 262206; + tables[62][52] = 262240; + tables[62][53] = 262268; + tables[62][54] = 262183; + tables[62][55] = 262190; + tables[62][56] = 327881; + tables[62][100] = 262331; + tables[62][136] = 262240; + tables[62][137] = 262240; + tables[62][138] = 262240; + tables[62][146] = 262240; + tables[62][147] = 262240; + tables[63] = tables[52]; + tables[63][8] = 0; + tables[63][18] = 327847; + tables[63][19] = 327862; + tables[63][30] = 262321; + tables[63][33] = 262306; + tables[63][34] = 262308; + tables[63][35] = 262316; + tables[63][36] = 262310; + tables[63][37] = 262322; + tables[63][38] = 262323; + tables[63][39] = 262332; + tables[63][45] = 262333; + tables[63][46] = 262334; + tables[63][47] = 262235; + tables[63][48] = 262237; + tables[63][49] = 262269; + tables[63][50] = 262269; + tables[63][51] = 262270; + tables[63][52] = 262267; + tables[63][53] = 262236; + tables[63][54] = 262319; + tables[63][55] = 262317; + tables[63][56] = 262324; + tables[63][100] = 262320; + tables[64] = tables[23]; + tables[64][35] = 327847; + tables[64][37] = 327713; + tables[64][46] = 327725; + tables[64][49] = 327861; + tables[64][50] = 327861; + tables[64][56] = 327741; + tables[65] = tables[24]; + tables[65][46] = 327775; + tables[65][48] = 327722; + tables[65][49] = 327843; + tables[65][50] = 327843; + tables[65][53] = 262323; + tables[65][56] = 327723; + tables[66][8] = 336044; + tables[66][30] = 327804; + tables[66][31] = 327744; + tables[66][32] = 327715; + tables[66][33] = 327803; + tables[66][34] = 327771; + tables[66][35] = 327774; + tables[66][38] = 327803; + tables[66][39] = 327805; + tables[66][47] = 327771; + tables[66][48] = 327773; + tables[66][49] = 262240; + tables[66][50] = 262240; + tables[66][52] = 262324; + tables[66][56] = 262270; + tables[66][100] = 262236; + tables[67][49] = 262240; + tables[67][50] = 262240; + tables[67][52] = 262324; + tables[67][56] = 262270; + tables[67][136] = 262240; + tables[67][137] = 262240; + tables[67][138] = 262240; + tables[67][146] = 262240; + tables[67][147] = 262240; + tables[68] = tables[4]; + tables[68][49] = 262179; + tables[68][100] = 0; + tables[69] = tables[5]; + tables[69][49] = 262270; + tables[69][100] = 0; + tables[70][4] = 327941; + tables[70][6] = 327943; + tables[70][8] = 327961; + tables[70][15] = 328002; + tables[70][17] = 328004; + tables[70][18] = 327923; + tables[70][22] = 328027; + tables[70][24] = 270508; + tables[70][27] = 328058; + tables[70][29] = 328060; + tables[71][4] = 327940; + tables[71][6] = 327942; + tables[71][8] = 327960; + tables[71][15] = 328001; + tables[71][17] = 328003; + tables[71][18] = 327891; + tables[71][22] = 328026; + tables[71][27] = 328057; + tables[71][29] = 328059; + tables[72] = tables[4]; + tables[72][47] = 262324; + tables[72][48] = 262235; + tables[72][49] = 0; + tables[72][50] = 262237; + tables[72][51] = 327911; + tables[72][52] = 262270; + tables[72][53] = 262183; + tables[72][56] = 262203; + tables[72][135] = 262191; + tables[73] = tables[5]; + tables[73][35] = 262312; + tables[73][47] = 262240; + tables[73][48] = 262267; + tables[73][49] = 0; + tables[73][50] = 262269; + tables[73][51] = 327879; + tables[73][52] = 262238; + tables[73][53] = 262178; + tables[73][56] = 262202; + tables[73][135] = 262207; + tables[74][8] = 262320; + tables[74][20] = 262191; + tables[74][26] = 262207; + tables[74][30] = 262329; + tables[74][31] = 262322; + tables[74][32] = 262323; + tables[74][33] = 262307; + tables[74][34] = 262306; + tables[74][35] = 262316; + tables[74][46] = 262311; + tables[74][48] = 262314; + tables[74][50] = 262330; + tables[74][135] = 262320; + tables[75] = tables[20]; + tables[75][12] = 327985; + tables[75][45] = 262186; + tables[75][46] = 262189; + tables[75][47] = 327967; + tables[75][48] = 327932; + tables[75][49] = 0; + tables[75][50] = 262188; + tables[75][51] = 328031; + tables[75][52] = 327785; + tables[75][53] = 262178; + tables[75][54] = 327926; + tables[75][55] = 327911; + tables[75][56] = 262190; + tables[76] = tables[21]; + tables[76][31] = 262183; + tables[76][32] = 262238; + tables[76][33] = 262187; + tables[76][46] = 262239; + tables[76][47] = 327966; + tables[76][48] = 262364; + tables[76][49] = 0; + tables[76][50] = 262203; + tables[76][51] = 328030; + tables[76][52] = 327984; + tables[76][53] = 262377; + tables[76][54] = 327894; + tables[76][55] = 327879; + tables[76][56] = 262202; + tables[77] = tables[52]; + tables[77][4] = 327910; + tables[77][8] = 270508; + tables[77][12] = 327785; + tables[77][16] = 0; + tables[77][20] = 262208; + tables[77][22] = 262367; + tables[77][30] = 262206; + tables[77][31] = 262307; + tables[77][32] = 262179; + tables[77][34] = 262333; + tables[77][45] = 262236; + tables[77][47] = 262312; + tables[77][50] = 262240; + tables[77][51] = 262324; + tables[77][53] = 262204; + tables[77][100] = 262268; + tables[78][4] = 327878; + tables[78][12] = 327984; + tables[79] = tables[20]; + tables[79][41] = 262268; + tables[79][46] = 262335; + tables[79][47] = 262324; + tables[79][50] = 327805; + tables[79][52] = 262267; + tables[79][53] = 262268; + tables[80] = tables[21]; + tables[80][32] = 262179; + tables[80][46] = 262305; + tables[80][47] = 262312; + tables[80][50] = 327773; + tables[80][52] = 262235; + tables[80][53] = 262320; + tables[81][20] = 262208; + tables[81][45] = 262236; + tables[81][48] = 262270; + tables[81][50] = 262240; + tables[81][52] = 262238; + tables[81][53] = 262316; + tables[82] = tables[4]; + tables[82][4] = 263732; + tables[82][5] = 263748; + tables[82][6] = 263716; + tables[82][7] = 263754; + tables[82][8] = 263723; + tables[82][9] = 263720; + tables[82][10] = 263748; + tables[82][11] = 263719; + tables[82][12] = 263751; + tables[82][13] = 263722; + tables[82][14] = 263750; + tables[82][15] = 263749; + tables[82][16] = 263721; + tables[82][17] = 263753; + tables[82][18] = 263726; + tables[82][19] = 263725; + tables[82][20] = 263734; + tables[82][21] = 263746; + tables[82][22] = 263731; + tables[82][23] = 263745; + tables[82][24] = 263737; + tables[82][25] = 263729; + tables[82][26] = 263733; + tables[82][27] = 263713; + tables[82][28] = 263738; + tables[82][29] = 263718; + tables[82][47] = 263724; + tables[82][48] = 263727; + tables[82][51] = 263747; + tables[82][52] = 263735; + tables[82][53] = 263728; + tables[82][54] = 263752; + tables[82][55] = 263730; + tables[82][56] = 263736; + tables[83] = tables[5]; + tables[83][4] = 263760; + tables[83][5] = 263748; + tables[83][6] = 262269; + tables[83][7] = 262237; + tables[83][8] = 263759; + tables[83][9] = 262235; + tables[83][10] = 263748; + tables[83][11] = 263715; + tables[83][12] = 262391; + tables[83][13] = 263744; + tables[83][14] = 263692; + tables[83][15] = 262191; + tables[83][16] = 270361; + tables[83][17] = 263714; + tables[83][18] = 262359; + tables[83][19] = 263707; + tables[83][20] = 263758; + tables[83][21] = 263756; + tables[83][22] = 263757; + tables[83][23] = 263748; + tables[83][24] = 270360; + tables[83][25] = 262267; + tables[83][26] = 263755; + tables[83][27] = 263762; + tables[83][28] = 263717; + tables[83][29] = 262270; + tables[83][38] = 262185; + tables[83][39] = 262184; + tables[83][47] = 262204; + tables[83][48] = 262206; + tables[83][53] = 263761; + tables[83][54] = 262188; + tables[83][55] = 262190; + tables[83][56] = 263711; + tables[84] = tables[1]; + tables[84][4] = 331295; + tables[84][5] = 331316; + tables[84][6] = 331329; + tables[84][7] = 331265; + tables[84][8] = 331315; + tables[84][9] = 331284; + tables[84][10] = 331328; + tables[84][11] = 331337; + tables[84][12] = 331299; + tables[84][13] = 331336; + tables[84][14] = 331314; + tables[84][15] = 331306; + tables[84][16] = 331287; + tables[84][17] = 331319; + tables[84][18] = 331289; + tables[84][19] = 331298; + tables[84][20] = 331334; + tables[84][21] = 331294; + tables[84][22] = 331307; + tables[84][23] = 331312; + tables[84][24] = 331317; + tables[84][25] = 331309; + tables[84][26] = 331332; + tables[84][27] = 331291; + tables[84][28] = 331313; + tables[84][29] = 331292; + tables[84][30] = 331333; + tables[84][31] = 327727; + tables[84][32] = 327725; + tables[84][33] = 331296; + tables[84][34] = 331286; + tables[84][35] = 331320; + tables[84][36] = 331318; + tables[84][37] = 331268; + tables[84][38] = 331285; + tables[84][39] = 331272; + tables[84][45] = 331266; + tables[84][46] = 331274; + tables[84][47] = 331290; + tables[84][48] = 331301; + tables[84][49] = 331267; + tables[84][50] = 331267; + tables[84][51] = 331303; + tables[84][52] = 331271; + tables[84][53] = 327775; + tables[84][54] = 331297; + tables[84][55] = 331331; + tables[84][56] = 331293; + tables[84][100] = 331267; + tables[84][103] = 0; + tables[84][133] = 0; + tables[84][134] = 0; + tables[84][140] = 0; + tables[85] = tables[2]; + tables[85][4] = 331300; + tables[85][5] = 331322; + tables[85][6] = 331273; + tables[85][7] = 331279; + tables[85][8] = 331278; + tables[85][9] = 331330; + tables[85][10] = 331276; + tables[85][11] = 331335; + tables[85][12] = 331283; + tables[85][13] = 331339; + tables[85][14] = 331305; + tables[85][15] = 331304; + tables[85][16] = 327743; + tables[85][17] = 331340; + tables[85][18] = 331311; + tables[85][19] = 331277; + tables[85][20] = 331344; + tables[85][21] = 331281; + tables[85][22] = 331270; + tables[85][23] = 331288; + tables[85][24] = 331338; + tables[85][25] = 331310; + tables[85][26] = 327714; + tables[85][27] = 327721; + tables[85][28] = 331341; + tables[85][29] = 327720; + tables[85][30] = 327723; + tables[85][31] = 331345; + tables[85][32] = 331346; + tables[85][33] = 331347; + tables[85][34] = 331348; + tables[85][35] = 331321; + tables[85][36] = 331327; + tables[85][37] = 331349; + tables[85][38] = 331350; + tables[85][39] = 331351; + tables[85][45] = 331352; + tables[85][46] = 331353; + tables[85][47] = 331280; + tables[85][48] = 327724; + tables[85][49] = 331269; + tables[85][50] = 331269; + tables[85][51] = 331275; + tables[85][52] = 327726; + tables[85][53] = 327717; + tables[85][54] = 331282; + tables[85][55] = 331308; + tables[85][56] = 331302; + tables[85][100] = 331269; + tables[85][103] = 0; + tables[85][133] = 0; + tables[85][134] = 0; + tables[85][140] = 0; + tables[86] = tables[1]; + tables[86][30] = 262187; + tables[86][31] = 327963; + tables[86][32] = 328033; + tables[86][33] = 327949; + tables[86][34] = 328025; + tables[86][35] = 328062; + tables[86][36] = 327933; + tables[86][37] = 327905; + tables[86][38] = 327917; + tables[86][39] = 327913; + tables[86][45] = 262205; + tables[86][46] = 262324; + tables[86][47] = 327930; + tables[86][48] = 262185; + tables[86][49] = 262312; + tables[86][50] = 262312; + tables[86][51] = 328047; + tables[86][52] = 262311; + tables[86][53] = 262203; + tables[86][100] = 262236; + tables[86][103] = 0; + tables[86][133] = 0; + tables[86][134] = 0; + tables[86][140] = 0; + tables[87] = tables[2]; + tables[87][30] = 262193; + tables[87][31] = 262194; + tables[87][32] = 262195; + tables[87][33] = 262196; + tables[87][34] = 262197; + tables[87][35] = 262198; + tables[87][36] = 262199; + tables[87][37] = 262200; + tables[87][38] = 262201; + tables[87][39] = 262192; + tables[87][45] = 262181; + tables[87][46] = 262855; + tables[87][47] = 262191; + tables[87][48] = 262184; + tables[87][49] = 262183; + tables[87][50] = 262183; + tables[87][51] = 262178; + tables[87][52] = 262177; + tables[87][54] = 262207; + tables[87][100] = 262268; + tables[87][103] = 0; + tables[87][133] = 0; + tables[87][134] = 0; + tables[87][140] = 0; + tables[88][5] = 262267; + tables[88][6] = 262182; + tables[88][7] = 262416; + tables[88][8] = 270508; + tables[88][9] = 262235; + tables[88][10] = 262237; + tables[88][14] = 262466; + tables[88][15] = 262465; + tables[88][17] = 262269; + tables[88][20] = 262236; + tables[88][22] = 262417; + tables[88][25] = 262208; + tables[88][26] = 262268; + tables[88][27] = 262179; + tables[88][30] = 262270; + tables[88][31] = 262855; + tables[88][32] = 262238; + tables[88][33] = 262872; + tables[88][34] = 262320; + tables[88][35] = 262875; + tables[88][36] = 262240; + tables[88][37] = 262873; + tables[88][38] = 262324; + tables[88][39] = 262877; + tables[88][45] = 262312; + tables[88][46] = 262328; + tables[88][47] = 262391; + tables[88][48] = 262359; + tables[88][49] = 262308; + tables[88][50] = 262308; + tables[88][51] = 262180; + tables[88][52] = 262367; + tables[88][54] = 262204; + tables[88][55] = 262206; + tables[88][56] = 262186; + tables[89] = tables[4]; + tables[89][4] = 328625; + tables[89][5] = 328626; + tables[89][6] = 328648; + tables[89][7] = 328628; + tables[89][8] = 328629; + tables[89][9] = 328646; + tables[89][10] = 328627; + tables[89][11] = 328631; + tables[89][12] = 328633; + tables[89][13] = 328638; + tables[89][14] = 328634; + tables[89][15] = 328635; + tables[89][16] = 328636; + tables[89][17] = 328637; + tables[89][18] = 328639; + tables[89][19] = 328640; + tables[89][20] = 262203; + tables[89][21] = 328641; + tables[89][22] = 328643; + tables[89][23] = 328644; + tables[89][24] = 328632; + tables[89][25] = 328649; + tables[89][26] = 263106; + tables[89][27] = 328647; + tables[89][28] = 328645; + tables[89][29] = 328630; + tables[89][51] = 263044; + tables[89][100] = 262204; + tables[89][103] = 0; + tables[89][133] = 0; + tables[89][134] = 0; + tables[89][140] = 0; + tables[90] = tables[5]; + tables[90][4] = 328593; + tables[90][5] = 328594; + tables[90][6] = 328616; + tables[90][7] = 328596; + tables[90][8] = 328597; + tables[90][9] = 328614; + tables[90][10] = 328595; + tables[90][11] = 328599; + tables[90][12] = 328601; + tables[90][13] = 328606; + tables[90][14] = 328602; + tables[90][15] = 328603; + tables[90][16] = 328604; + tables[90][17] = 328605; + tables[90][18] = 328607; + tables[90][19] = 328608; + tables[90][20] = 262202; + tables[90][21] = 328609; + tables[90][22] = 328611; + tables[90][23] = 328612; + tables[90][24] = 328600; + tables[90][25] = 328617; + tables[90][26] = 263045; + tables[90][27] = 328615; + tables[90][28] = 328613; + tables[90][29] = 328598; + tables[90][51] = 262312; + tables[90][100] = 262206; + tables[90][103] = 0; + tables[90][133] = 0; + tables[90][134] = 0; + tables[90][140] = 0; + tables[91][6] = 262313; + tables[91][8] = 270508; + tables[91][21] = 262318; + tables[91][28] = 262309; + tables[91][31] = 262322; + tables[91][32] = 262323; + tables[91][33] = 262307; + tables[91][34] = 262311; + tables[91][35] = 262326; + tables[91][37] = 262308; + tables[91][38] = 262310; + tables[91][39] = 262320; + tables[91][45] = 262321; + tables[91][46] = 262333; + tables[91][47] = 262315; + tables[91][48] = 262331; + tables[91][50] = 262316; + tables[91][51] = 263045; + tables[92] = tables[4]; + tables[92][103] = 0; + tables[92][133] = 0; + tables[92][134] = 0; + tables[92][140] = 0; + tables[93] = tables[5]; + tables[93][103] = 0; + tables[93][133] = 0; + tables[93][134] = 0; + tables[93][140] = 0; + tables[94] = tables[92]; + tables[94][30] = 327939; + tables[94][31] = 327906; + tables[94][32] = 327914; + tables[94][33] = 327924; + tables[94][34] = 262912; + tables[94][35] = 262921; + tables[94][36] = 262915; + tables[94][37] = 262913; + tables[94][38] = 262947; + tables[94][39] = 327953; + tables[94][46] = 270507; + tables[94][47] = 328112; + tables[94][48] = 328097; + tables[95] = tables[93]; + tables[95][30] = 327938; + tables[95][31] = 327874; + tables[95][32] = 327882; + tables[95][33] = 327892; + tables[95][34] = 262912; + tables[95][35] = 262921; + tables[95][36] = 262915; + tables[95][37] = 262913; + tables[95][38] = 262947; + tables[95][39] = 327952; + tables[95][47] = 328111; + tables[95][48] = 328096; + tables[96] = tables[92]; + tables[96][40] = 0; + tables[96][42] = 0; + tables[96][43] = 0; + tables[96][44] = 0; + tables[96][83] = 0; + tables[96][84] = 0; + tables[96][85] = 0; + tables[96][86] = 0; + tables[96][87] = 0; + tables[96][88] = 0; + tables[96][89] = 0; + tables[96][90] = 0; + tables[96][91] = 0; + tables[96][92] = 0; + tables[96][93] = 0; + tables[96][94] = 0; + tables[96][95] = 0; + tables[96][96] = 0; + tables[96][97] = 0; + tables[96][98] = 0; + tables[96][99] = 0; + tables[97] = tables[93]; + tables[97][40] = 0; + tables[97][42] = 0; + tables[97][43] = 0; + tables[97][44] = 0; + tables[97][83] = 0; + tables[97][84] = 0; + tables[97][85] = 0; + tables[97][86] = 0; + tables[97][87] = 0; + tables[97][88] = 0; + tables[97][89] = 0; + tables[97][90] = 0; + tables[97][91] = 0; + tables[97][92] = 0; + tables[97][93] = 0; + tables[97][94] = 0; + tables[97][95] = 0; + tables[97][96] = 0; + tables[97][97] = 0; + tables[97][98] = 0; + tables[97][99] = 0; + tables[98] = tables[92]; + tables[98][47] = 327939; + tables[98][48] = 327918; + tables[98][49] = 327906; + tables[98][50] = 327906; + tables[98][51] = 328217; + tables[98][52] = 328219; + tables[98][53] = 270366; + tables[99] = tables[93]; + tables[99][47] = 327938; + tables[99][48] = 327886; + tables[99][49] = 327874; + tables[99][50] = 327874; + tables[99][51] = 328216; + tables[99][52] = 328218; + tables[99][53] = 270365; + tables[99][54] = 262203; + tables[99][55] = 262202; + tables[100] = tables[88]; + tables[100][5] = 0; + tables[100][6] = 262313; + tables[100][7] = 327953; + tables[100][9] = 0; + tables[100][10] = 0; + tables[100][14] = 0; + tables[100][15] = 328002; + tables[100][17] = 0; + tables[100][19] = 262311; + tables[100][20] = 0; + tables[100][22] = 262367; + tables[100][25] = 0; + tables[100][26] = 0; + tables[100][27] = 0; + tables[100][47] = 262235; + tables[100][48] = 262237; + tables[100][49] = 262236; + tables[100][50] = 262236; + tables[100][51] = 262203; + tables[100][52] = 262183; + tables[100][53] = 262240; + tables[100][56] = 0; + tables[101] = tables[29]; + tables[101][7] = 327952; + tables[101][15] = 328001; + tables[101][45] = 270355; + tables[101][46] = 262321; + tables[101][49] = 262268; + tables[101][50] = 262268; + tables[101][51] = 262202; + tables[101][52] = 262178; + tables[101][53] = 262270; + tables[101][54] = 262315; + tables[101][55] = 262331; + tables[102] = tables[92]; + tables[102][28] = 327802; + tables[102][29] = 327801; + tables[102][39] = 327926; + tables[102][45] = 327932; + tables[102][46] = 327923; + tables[102][47] = 328017; + tables[102][48] = 327930; + tables[102][49] = 328049; + tables[102][50] = 328049; + tables[102][51] = 327913; + tables[102][52] = 327905; + tables[102][53] = 262192; + tables[102][56] = 262189; + tables[102][100] = 327917; + tables[103] = tables[87]; + tables[103][30] = 262183; + tables[103][31] = 262178; + tables[103][32] = 262187; + tables[103][33] = 262177; + tables[103][34] = 262181; + tables[103][35] = 262191; + tables[103][36] = 262205; + tables[103][37] = 262184; + tables[103][38] = 262185; + tables[103][39] = 327894; + tables[103][45] = 327900; + tables[103][46] = 327891; + tables[103][47] = 328016; + tables[103][48] = 327898; + tables[103][49] = 328048; + tables[103][50] = 328048; + tables[103][51] = 327881; + tables[103][52] = 327873; + tables[103][53] = 262311; + tables[103][100] = 327885; + tables[104] = tables[88]; + tables[104][4] = 262372; + tables[104][8] = 262340; + tables[104][12] = 262349; + tables[104][13] = 262381; + tables[104][16] = 262204; + tables[104][24] = 270508; + tables[104][29] = 262206; + tables[104][54] = 262203; + tables[104][100] = 262204; + tables[105] = tables[36]; + tables[105][22] = 328790; + tables[105][48] = 328791; + tables[105][52] = 328788; + tables[105][53] = 327719; + tables[105][83] = 0; + tables[105][100] = 263313; + tables[106] = tables[37]; + tables[106][22] = 328710; + tables[106][48] = 328711; + tables[106][52] = 328708; + tables[106][53] = 0; + tables[106][100] = 263312; + tables[107][24] = 263313; + tables[108][24] = 263312; + tables[109][40] = 262154; + tables[109][42] = 262152; + tables[109][43] = 262153; + tables[109][88] = 262154; + tables[109][186] = 262152; + tables[109][187] = 262153; + tables[110][31] = 327962; + tables[110][32] = 328032; + tables[110][33] = 327948; + tables[110][34] = 328024; + tables[110][35] = 328061; + tables[110][36] = 327901; + tables[110][37] = 327873; + tables[110][38] = 327885; + tables[110][39] = 327881; + tables[110][47] = 327898; + tables[110][51] = 328046; + return tables; +}(); -char16_t UsCharacter(std::uint16_t keycode) { - static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; - static constexpr char16_t kKeypad[] = u"/*-+\0" u"1234567890."; - if (keycode >= 0x04 && keycode <= 0x1D) return static_cast<char16_t>(u'a' + (keycode - 0x04)); - if (keycode >= 0x1E && keycode <= 0x26) return static_cast<char16_t>(u'1' + (keycode - 0x1E)); - if (keycode == 0x27) return u'0'; - if (keycode == 0x2C) return u' '; - if (keycode >= 0x2D && keycode <= 0x38) return kPunctuation[keycode - 0x2D]; - if (keycode == 0x64) return u'\\'; - if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; - return 0; +constexpr std::array<std::array<std::uint8_t, 4>, 41> CharacterLayouts = {{ + {1, 2, 3, 0}, + {4, 5, 6, 0}, + {7, 8, 0, 0}, + {9, 10, 0, 0}, + {11, 12, 0, 0}, + {13, 14, 0, 0}, + {15, 16, 0, 0}, + {17, 18, 19, 0}, + {20, 21, 22, 0}, + {23, 24, 25, 0}, + {26, 27, 28, 29}, + {30, 31, 32, 0}, + {33, 34, 35, 0}, + {36, 37, 0, 0}, + {38, 39, 40, 41}, + {42, 43, 0, 0}, + {44, 45, 46, 0}, + {47, 48, 49, 0}, + {50, 51, 52, 0}, + {47, 48, 49, 0}, + {53, 54, 0, 55}, + {56, 57, 0, 0}, + {58, 54, 0, 55}, + {59, 60, 3, 0}, + {61, 62, 63, 0}, + {64, 65, 66, 67}, + {68, 69, 70, 71}, + {72, 73, 74, 0}, + {75, 76, 77, 78}, + {79, 80, 81, 0}, + {82, 83, 0, 0}, + {84, 85, 0, 0}, + {86, 87, 88, 0}, + {89, 90, 91, 0}, + {92, 93, 0, 0}, + {94, 95, 96, 97}, + {98, 99, 100, 101}, + {102, 103, 104, 0}, + {105, 106, 107, 108}, + {109, 109, 109, 109}, + {110, 110, 110, 110}, +}}; + +constexpr std::array<char16_t, 19> KoreanInitial = { + 12593, 12594, 12596, 12599, 12600, 12601, 12609, 12610, 12611, 12613, 12614, 12615, 12616, 12617, 12618, 12619, 12620, 12621, 12622 +}; + +constexpr std::array<char16_t, 27> KoreanFinal = { + 12593, 12594, 12595, 12596, 12597, 12598, 12599, 12601, 12602, 12603, 12604, 12605, 12606, 12607, 12608, 12609, 12610, 12612, 12613, 12614, 12615, 12616, 12618, 12619, 12620, 12621, 12622 +}; + + +std::size_t CharacterLayout(std::uint32_t type, std::uint32_t state) { + constexpr std::array<std::uint8_t, 39> layouts = {18, 7, 0, 1, 14, 8, 29, 17, 9, 25, 24, 23, 10, 11, 16, 26, 27, 12, 1, 19, 28, 3, 3, 1, 1, 1, 1, 1, 1, 1, 14, 32, 14, 34, 35, 36, 37, 1, 1}; + const bool imeMode = (state & 0x4000000u) != 0; + const bool alternateMode = (state & 0x10000000u) != 0; + switch (type) { + case 19: return imeMode ? 13 : 1; + case 22: return alternateMode ? 2 : 3; + case 23: + if (!imeMode) return alternateMode ? 2 : 3; + if ((state & 0x8000000u) == 0) return 6; + return alternateMode ? 4 : 5; + case 24: return imeMode ? 15 : 1; + case 26: + if (imeMode) return 20; + return alternateMode ? 21 : 1; + case 29: + case 39: + if (imeMode) return 22; + return alternateMode ? 21 : 1; + case 25: + case 27: + case 28: return alternateMode ? 21 : 1; + case 30: return imeMode ? 30 : 1; + case 31: return imeMode ? 31 : 14; + case 33: return imeMode ? 33 : 14; + case 38: return imeMode ? 38 : 1; + default: return layouts[type - 1]; + } +} + +std::uint32_t CharacterEntry(std::size_t layout, std::size_t layer, std::uint16_t keycode) { + if (keycode >= 256) return 0; + return CharacterTables[CharacterLayouts[layout][layer]][keycode]; +} + +char16_t ConvertCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type) { + if (keycode == 83 || (keycode & 0xFFF8u) == 224) return 0; + const auto layout = CharacterLayout(type, state); + const bool shifted = (state & 0x2200u) != 0; + const bool caps = (state & 0x20000u) != 0; + if (layout == 0 && caps) { + if (keycode == 47) return shifted ? 200 : 220; + if (keycode == 52) return shifted ? 192 : 196; + if (keycode == 51) return shifted ? 201 : 214; + } + const auto& layers = CharacterLayouts[layout]; + if ((state & 0x4000u) != 0 && (layers[2] != 0 || layers[3] != 0)) { + auto entry = CharacterEntry(layout, shifted ? 3 : 2, keycode); + if (entry == 0) return static_cast<char16_t>(CharacterEntry(39, 0, keycode)); + if (caps && (entry & 0x10000u) != 0) { + const auto opposite = CharacterEntry(layout, shifted ? 2 : 3, keycode); + if (opposite != 0) entry = opposite; + } + return static_cast<char16_t>(entry); + } + auto entry = CharacterEntry(layout, shifted ? 1 : 0, keycode); + if (entry == 0) return 0; + if (caps) { + if ((type == 26 || type == 29 || type == 39) && (state & 0x4000000u) != 0) { + entry = CharacterEntry(layout, shifted ? 3 : 0, keycode); + } else if (type == 32 && !shifted && ((keycode >= 31 && keycode <= 39) || keycode == 47 || keycode == 51)) { + entry = CharacterEntry(40, 0, keycode); + } else if ((entry & 0x10000u) != 0) { + entry = CharacterEntry(layout, shifted ? 0 : 1, keycode); + } + if (entry == 0) return 0; + } + if ((entry & 0x20000u) != 0) { + const bool numLock = (state & 0x10000u) != 0; + if (!numLock && keycode >= 89 && keycode <= 99) return 0; + entry = CharacterEntry(layout, numLock ? 1 : 0, keycode); + if (entry == 0) return 0; + } + auto character = static_cast<char16_t>(entry); + if ((state & 0x4000000u) != 0 && type == 24) { + if ((character >= 12592 && character < 12688) || (character >= 44032 && character < 55216) || (character & 0xFF00u) == 0x1100) { + if (character >= 4352 && character <= 4370) return KoreanInitial[character - 4352]; + if (character >= 4449 && character <= 4469) return static_cast<char16_t>(character + 8174); + if (character >= 4520 && character <= 4546) return KoreanFinal[character - 4520]; + return 0; + } + } + if ((state & 0x4000000u) != 0 && type == 30) { + if ((state & 0x80u) == 0) { + if (keycode == 5) character = shifted ? 1570 : 1575; + if (shifted && keycode == 10) character = 1571; + if (shifted && keycode == 23) character = 1573; + } + const auto modifiers = state & 0xFFF8FFFFu; + if (modifiers == 67117312 || modifiers == 67121152 || modifiers == 67109632 || modifiers == 67113472) { + constexpr std::array<char16_t, 4> formatting = {0x200D, 0x200C, 0x200E, 0x200F}; + return keycode >= 30 && keycode <= 33 ? formatting[keycode - 30] : 0; + } + if ((state & 0x1100u) != 0 && shifted) return 0; + } + return character; } } // namespace @@ -241,8 +2400,7 @@ extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(int32_t userId, uint32_t* type) { if (type == nullptr) return kErrInvalidAddress; if (userId < 0) return kErrInvalidUserId; - *type = 0; - return 0; + throw std::runtime_error("sceConvertKeycodeGetImeKeyboardType: user keyboard language and variant are unavailable"); } int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uint32_t keyboardType, std::uint16_t* virtualKeycode) { if (virtualKeycode == nullptr) return kErrInvalidAddress; @@ -261,9 +2419,9 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { if (character == nullptr) return kErrInvalidAddress; - if ((state & ~kIgnoredStateBits) != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); - if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); - *character = UsCharacter(keycode); + if (type == 0 || type > 39) return kErrInvalidKeyboardType; + if (type >= 31) throw std::runtime_error("sceConvertKeycodeGetCharacter: guest sdk version is unavailable"); + *character = ConvertCharacter(keycode, state, type); return 0; } diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 1f2ec31789..cce08ee498 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -45,38 +45,48 @@ int main() { Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); Require(type == 0xFFFFFFFFu); - Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == 0); - Require(type == 0); + bool rejected = false; + try { + sceConvertKeycodeGetImeKeyboardType(0, &type); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && type == 0xFFFFFFFFu); - type = 0xFFFFFFFFu; - Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); - Require(type == 0); - - Require(sceConvertKeycodeGetCharacter(0x04, 0, 0, nullptr) == invalidAddress); + Require(sceConvertKeycodeGetCharacter(4, 0, 0, nullptr) == invalidAddress); + char16_t character = u'?'; + Require(sceConvertKeycodeGetCharacter(4, 0, 0, &character) == static_cast<int>(0x80BC001Du)); + Require(character == u'?'); + Require(sceConvertKeycodeGetCharacter(4, 0, 40, &character) == static_cast<int>(0x80BC001Du)); for (std::uint32_t state : {0u, 0x4000000u}) { - char16_t character[3] = {u'?', 0, 0}; - Require(sceConvertKeycodeGetCharacter(0x04, state, 0, character) == 0); - Require(character[0] == u'a'); - Require(sceConvertKeycodeGetCharacter(0x1E, state, 0, character) == 0); - Require(character[0] == u'1'); - Require(sceConvertKeycodeGetCharacter(0x3A, state, 0, character) == 0); - Require(character[0] == 0); - Require(sceConvertKeycodeGetCharacter(0x64, state, 0, character) == 0); - Require(character[0] == u'\\'); - Require(sceConvertKeycodeGetCharacter(0x32, state, 0, character) == 0); - Require(character[0] == 0); - Require(sceConvertKeycodeGetCharacter(0x27, state, 0, character) == 0); - Require(character[0] == u'0'); - Require(sceConvertKeycodeGetCharacter(0x2C, state, 0, character) == 0); - Require(character[0] == u' '); - Require(sceConvertKeycodeGetCharacter(0x2D, state, 0, character) == 0); - Require(character[0] == u'-'); - Require(sceConvertKeycodeGetCharacter(0x54, state, 0, character) == 0); - Require(character[0] == u'/'); - Require(sceConvertKeycodeGetCharacter(0x59, state, 0, character) == 0); - Require(character[0] == u'1'); - Require(sceConvertKeycodeGetCharacter(0x63, state, 0, character) == 0); - Require(character[0] == u'.'); + Require(sceConvertKeycodeGetCharacter(4, state, 4, &character) == 0 && character == u'a'); + Require(sceConvertKeycodeGetCharacter(30, state, 4, &character) == 0 && character == u'1'); + Require(sceConvertKeycodeGetCharacter(58, state, 4, &character) == 0 && character == 0); + Require(sceConvertKeycodeGetCharacter(65535, state, 4, &character) == 0 && character == 0); + } + Require(sceConvertKeycodeGetCharacter(4, 0x200, 4, &character) == 0 && character == u'A'); + Require(sceConvertKeycodeGetCharacter(4, 0x20000, 4, &character) == 0 && character == u'A'); + Require(sceConvertKeycodeGetCharacter(4, 0x20200, 4, &character) == 0 && character == u'a'); + Require(sceConvertKeycodeGetCharacter(30, 0x200, 4, &character) == 0 && character == u'!'); + Require(sceConvertKeycodeGetCharacter(89, 0, 4, &character) == 0 && character == 0); + Require(sceConvertKeycodeGetCharacter(89, 0x10000, 4, &character) == 0 && character == u'1'); + Require(sceConvertKeycodeGetCharacter(89, 0x10200, 4, &character) == 0 && character == u'1'); + Require(sceConvertKeycodeGetCharacter(4, 0, 9, &character) == 0 && character == u'q'); + Require(sceConvertKeycodeGetCharacter(28, 0, 2, &character) == 0 && character == u'z'); + Require(sceConvertKeycodeGetCharacter(20, 0x4000, 2, &character) == 0 && character == u'@'); + Require(sceConvertKeycodeGetCharacter(47, 0x20000, 3, &character) == 0 && character == 220); + Require(sceConvertKeycodeGetCharacter(47, 0x20200, 3, &character) == 0 && character == 200); + Require(sceConvertKeycodeGetCharacter(4, 0x4000000, 24, &character) == 0 && character == 0x3141); + Require(sceConvertKeycodeGetCharacter(5, 0x4000000, 30, &character) == 0 && character == 1575); + Require(sceConvertKeycodeGetCharacter(5, 0x4000200, 30, &character) == 0 && character == 1570); + Require(sceConvertKeycodeGetCharacter(30, 0x4000300, 30, &character) == 0 && character == 0x200D); + Require(sceConvertKeycodeGetCharacter(31, 0x4000300, 30, &character) == 0 && character == 0x200C); + Require(sceConvertKeycodeGetCharacter(32, 0x4000300, 30, &character) == 0 && character == 0x200E); + Require(sceConvertKeycodeGetCharacter(33, 0x4000300, 30, &character) == 0 && character == 0x200F); + for (std::uint32_t layout = 1; layout <= 30; ++layout) { + for (std::uint16_t key : {83, 224, 225, 226, 227, 228, 229, 230, 231}) { + Require(sceConvertKeycodeGetCharacter(key, 0xFFFFFFFFu, layout, &character) == 0 && character == 0); + } } return 0; } diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index fbf5f9d6cd..d220da5136 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -1,11 +1,6 @@ # libSceConvertKeycode technical debt -### Unknown function info - -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PPSA02530 (#5726) and PPSA27836 pass `0x4000000` as a constant: PPSA27836 calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. The returned character for HID usage `0x64` has not been measured on a console; `\` is an unverified US-layout guess based on the Windows VK mapping, which does not establish the console result. - ### Functional -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) takes the keycode as a USB HID keyboard usage and returns the unshifted character of the US layout for letters, digits, punctuation, space and the keypad. A keycode without a character returns 0 with the character 0; whether the console returns an error instead is not measured. - -- `sceConvertKeycodeGetVirtualKeycode`: HID-to-virtual-key mappings and the three-argument signature are confirmed. Layouts 31–39 and version-dependent Y/Z mappings for layouts 2, 3 and 12 throw until the guest platform and compiled SDK version are exposed; no version is guessed. +- `sceConvertKeycodeGetVirtualKeycode` and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. +- `sceConvertKeycodeGetImeKeyboardType`: user keyboard language and Japanese/Chinese keyboard variants are not exposed by the current UserService implementation. Valid-user queries throw instead of returning a fabricated keyboard type. The confirmed language/variant mapping and SDK compatibility rules still need integration with those settings. From 286efdc9123fd93ddafadce5e5d1eae51fb8cea8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:14:30 +0300 Subject: [PATCH 0590/1360] fix(discid): restore output pointer arguments --- core/libs/prx/libSceDiscId/Export.cpp | 7 +++++-- docs/dev/debt/libSceDiscId.md | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceDiscId/Export.cpp b/core/libs/prx/libSceDiscId/Export.cpp index 055e970b29..ef0373a67b 100644 --- a/core/libs/prx/libSceDiscId/Export.cpp +++ b/core/libs/prx/libSceDiscId/Export.cpp @@ -3,14 +3,17 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct DiscId; +struct DiscId2; + extern "C" { -int APS5_VABI sceDiscIdGet() { +int APS5_VABI sceDiscIdGet(DiscId* discId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceDiscIdGet2() { +int APS5_VABI sceDiscIdGet2(DiscId2* discId, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md index fb26a7a344..92a0e6f465 100644 --- a/docs/dev/debt/libSceDiscId.md +++ b/docs/dev/debt/libSceDiscId.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceDiscIdGet and sceDiscIdGet2](../../../core/libs/prx/libSceDiscId/Export.cpp) (libSceDiscId) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDiscId.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- `sceDiscIdGet` takes one output pointer; `sceDiscIdGet2` takes an output pointer and a 32-bit argument that must be zero. Both pass the pointer to a device ioctl without accessing its contents. Output structure sizes and fields require the device-side implementation or a caller and remain unresolved. From 5536d6175666cd74346ed1d657cc97aeabbf8b59 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:14:33 +0300 Subject: [PATCH 0591/1360] fix(contentdelete): restore lifecycle errors and list arguments --- core/libs/prx/libSceContentDelete/Export.cpp | 21 ++++++++++--------- core/libs/tests/GuestContentDelete.cpp | 22 ++++++++++++-------- docs/dev/debt/libSceContentDelete.md | 5 +++-- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 3a9fe2a4c8..5f886b22a4 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -9,32 +9,33 @@ namespace { std::atomic<bool> g_initialized{false}; -void RequireInitialized(const char* function) { - if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); -} +constexpr int InvalidArgument = static_cast<int>(0x809D5001u); +constexpr int NotInitialized = static_cast<int>(0x809D5002u); +constexpr int AlreadyInitialized = static_cast<int>(0x809D5003u); } extern "C" { -int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param) { - if (init_param == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* initParam) { + if (initParam == nullptr || initParam->heap_size != 0x4000) return InvalidArgument; bool expected = false; - if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + if (!g_initialized.compare_exchange_strong(expected, true)) return AlreadyInitialized; return 0; } int APS5_VABI sceContentDeleteByPath(const char* contentPath) { + if (!g_initialized.load()) return NotInitialized; if (contentPath == nullptr) APS5_INVALID_ARG_EX; - RequireInitialized(__func__); + if (*contentPath == 0) return InvalidArgument; throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } int APS5_VABI sceContentDeleteById(std::int64_t contentId) { - RequireInitialized(__func__); + if (!g_initialized.load()) return NotInitialized; throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); } -int APS5_VABI sceContentDeleteByIdListForSf(void) { +int APS5_VABI sceContentDeleteByIdListForSf(const std::int64_t* contentIds, std::size_t count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -56,7 +57,7 @@ int APS5_VABI sceContentDeleteMemoryTerminate(void) { int APS5_VABI sceContentDeleteTerminate(void) { bool expected = true; - if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (!g_initialized.compare_exchange_strong(expected, false)) return NotInitialized; return 0; } diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp index 9353f3a578..06cebf5b38 100644 --- a/core/libs/tests/GuestContentDelete.cpp +++ b/core/libs/tests/GuestContentDelete.cpp @@ -4,7 +4,7 @@ #include <stdexcept> #include <string> -extern "C" int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param); +extern "C" int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* initParam); extern "C" int APS5_VABI sceContentDeleteTerminate(void); extern "C" int APS5_VABI sceContentDeleteById(std::int64_t contentId); extern "C" int APS5_VABI sceContentDeleteByPath(const char* contentPath); @@ -30,16 +30,20 @@ bool Contains(const std::string& text, const std::string& part) { return text.fi } int main() { - Require(LogicErrorMessage([] { sceContentDeleteTerminate(); }) == "sceContentDeleteTerminate: not initialized"); - Require(LogicErrorMessage([] { sceContentDeleteInitialize(nullptr); }) == "invalid argument"); - Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); - Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath: not initialized"); - Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + Require(sceContentDeleteTerminate() == static_cast<int>(0x809D5002u)); + Require(sceContentDeleteInitialize(nullptr) == static_cast<int>(0x809D5001u)); + Require(sceContentDeleteById(1) == static_cast<int>(0x809D5002u)); + Require(sceContentDeleteByPath("/user/av_contents/photo/a.png") == static_cast<int>(0x809D5002u)); + Require(sceContentDeleteByPath(nullptr) == static_cast<int>(0x809D5002u)); ContentDeleteInitParam param{}; param.heap_size = 0x10000; + Require(sceContentDeleteInitialize(¶m) == static_cast<int>(0x809D5001u)); + param.heap_size = 0x4000; Require(sceContentDeleteInitialize(¶m) == 0); - Require(LogicErrorMessage([&] { sceContentDeleteInitialize(¶m); }) == "sceContentDeleteInitialize: already initialized"); + Require(sceContentDeleteInitialize(¶m) == static_cast<int>(0x809D5003u)); + Require(sceContentDeleteInitialize(nullptr) == static_cast<int>(0x809D5001u)); + Require(sceContentDeleteByPath("") == static_cast<int>(0x809D5001u)); Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); @@ -47,7 +51,7 @@ int main() { Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); Require(sceContentDeleteTerminate() == 0); - Require(LogicErrorMessage([] { sceContentDeleteTerminate(); }) == "sceContentDeleteTerminate: not initialized"); - Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + Require(sceContentDeleteTerminate() == static_cast<int>(0x809D5002u)); + Require(sceContentDeleteById(1) == static_cast<int>(0x809D5002u)); return 0; } diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index a0dacf89d0..da7501c081 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -2,8 +2,9 @@ ### Functional -- [sceContentDeleteById and sceContentDeleteByPath](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) delete from a content library that is empty on the host, as nothing is exported to it, so both throw for every content id and path once the arguments and the initialization are checked: no reference gives the console's error for content that is not in the library ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentDelete.cs#L25-L42) names only `0x809D5006`, no permission). The signatures, a 64-bit content id and a UTF-8 path, follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L541-L542) and SharpProspero, which agree. A null path and a call outside `sceContentDeleteInitialize` and `sceContentDeleteTerminate` throw, as the console's errors for them are unknown, and the order of the two checks is unverified. +- `sceContentDeleteById` and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization throw; the backend result cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. +- `sceContentDeleteInitialize`: validates the confirmed 16 KiB heap requirement and lifecycle errors, but does not create the original allocator, mutex and database client. Initialization-parameter fields other than the 64-bit heap size at offset 8 remain unresolved. ### Unknown function info -- [sceContentDeleteByIdListForSf, sceContentDeleteGlobalMstat, sceContentDeleteMemoryInitialize, sceContentDeleteMemoryTerminate](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) - exported symbols from decrypted PS5 firmware 1.00–3.20 libSceContentDelete.sprx; parameter lists and return values are unknown/unverified and no reference (KytyPS5, shadPS4, fpPS4, SharpProspero) documents their signatures. Declared as stubs that throw via `NotImplemented_nid_no_patch` +- `sceContentDeleteGlobalMstat`, `sceContentDeleteMemoryInitialize` and `sceContentDeleteMemoryTerminate`: neither exports nor callers were found in the supplied decompiled library corpus; their signatures remain unresolved. From 0b09b0c26225341d4cb99f6e6961e3ef27b49120 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:14:48 +0300 Subject: [PATCH 0592/1360] docs: link remaining abi debt to library sources --- docs/dev/debt/libSceBgft.md | 6 +++--- docs/dev/debt/libSceContentDelete.md | 6 +++--- docs/dev/debt/libSceConvertKeycode.md | 4 ++-- docs/dev/debt/libSceDiscId.md | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 3da7019e92..839539717c 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -2,6 +2,6 @@ ### Unknown function info -- `sceBgftServiceDownloadGetProgress`, `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. -- `sceBgftServiceIntDownloadRegisterTaskByStorageEx`: confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. Scalar meanings and the field at offset 24 remain unresolved; the parameter type is forward-declared. -- `sceBgftServiceIntDownloadGetPatchProgress` and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. +- [sceBgftServiceDownloadGetProgress](../../../core/libs/prx/libSceBgft/Export.cpp), `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. +- [sceBgftServiceIntDownloadRegisterTaskByStorageEx](../../../core/libs/prx/libSceBgft/Export.cpp): confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. Scalar meanings and the field at offset 24 remain unresolved; the parameter type is forward-declared. +- [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index da7501c081..f7365210d5 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -2,9 +2,9 @@ ### Functional -- `sceContentDeleteById` and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization throw; the backend result cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. -- `sceContentDeleteInitialize`: validates the confirmed 16 KiB heap requirement and lifecycle errors, but does not create the original allocator, mutex and database client. Initialization-parameter fields other than the 64-bit heap size at offset 8 remain unresolved. +- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization throw; the backend result cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. +- [sceContentDeleteInitialize](../../../core/libs/prx/libSceContentDelete/Export.cpp): validates the confirmed 16 KiB heap requirement and lifecycle errors, but does not create the original allocator, mutex and database client. Initialization-parameter fields other than the 64-bit heap size at offset 8 remain unresolved. ### Unknown function info -- `sceContentDeleteGlobalMstat`, `sceContentDeleteMemoryInitialize` and `sceContentDeleteMemoryTerminate`: neither exports nor callers were found in the supplied decompiled library corpus; their signatures remain unresolved. +- [sceContentDeleteGlobalMstat](../../../core/libs/prx/libSceContentDelete/Export.cpp), `sceContentDeleteMemoryInitialize` and `sceContentDeleteMemoryTerminate`: neither exports nor callers were found in the supplied decompiled library corpus; their signatures remain unresolved. diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index d220da5136..5c01a3cabc 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,5 +2,5 @@ ### Functional -- `sceConvertKeycodeGetVirtualKeycode` and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. -- `sceConvertKeycodeGetImeKeyboardType`: user keyboard language and Japanese/Chinese keyboard variants are not exposed by the current UserService implementation. Valid-user queries throw instead of returning a fabricated keyboard type. The confirmed language/variant mapping and SDK compatibility rules still need integration with those settings. +- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. +- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): user keyboard language and Japanese/Chinese keyboard variants are not exposed by the current UserService implementation. Valid-user queries throw instead of returning a fabricated keyboard type. The confirmed language/variant mapping and SDK compatibility rules still need integration with those settings. diff --git a/docs/dev/debt/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md index 92a0e6f465..17eb02c445 100644 --- a/docs/dev/debt/libSceDiscId.md +++ b/docs/dev/debt/libSceDiscId.md @@ -2,4 +2,4 @@ ### Unknown function info -- `sceDiscIdGet` takes one output pointer; `sceDiscIdGet2` takes an output pointer and a 32-bit argument that must be zero. Both pass the pointer to a device ioctl without accessing its contents. Output structure sizes and fields require the device-side implementation or a caller and remain unresolved. +- [sceDiscIdGet](../../../core/libs/prx/libSceDiscId/Export.cpp) takes one output pointer; `sceDiscIdGet2` takes an output pointer and a 32-bit argument that must be zero. Both pass the pointer to a device ioctl without accessing its contents. Output structure sizes and fields require the device-side implementation or a caller and remain unresolved. From dd1478f4af311e2bc7a34747731a01713dd14681 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:18:28 +0300 Subject: [PATCH 0593/1360] fix(httpcache): restore request and memory pool argument layouts --- core/libs/prx/libSceHttpCache/Export.cpp | 87 ++++++++++++++++++------ docs/dev/debt/libSceHttpCache.md | 3 +- 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index 6f5d37ccb1..12a2497b87 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -3,94 +3,143 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct HttpCacheHeader { + const char* name; + const char* value; +}; + +struct HttpCacheRequest { + std::uint32_t method; + std::uint32_t reserved; + const char* url; + std::size_t headerCount; + const HttpCacheHeader* headers; +}; + +struct HttpCacheResponseHeader { + std::uint32_t statusCode; + std::uint32_t reserved; + std::size_t headerCount; + const HttpCacheHeader* headers; +}; + +struct HttpCacheResponse { + std::uint32_t statusCode; + std::uint32_t reserved; + std::size_t headerCount; + const HttpCacheHeader* headers; + std::uint64_t contentLength; +}; + +struct HttpCacheQuotaEntry { + const char* name; + std::uint64_t size; +}; + +struct HttpCacheQuota { + std::size_t count; + const HttpCacheQuotaEntry* entries; +}; + +static_assert(sizeof(HttpCacheHeader) == 16); +static_assert(sizeof(HttpCacheRequest) == 32); +static_assert(offsetof(HttpCacheRequest, url) == 8); +static_assert(offsetof(HttpCacheRequest, headers) == 24); +static_assert(sizeof(HttpCacheResponseHeader) == 24); +static_assert(offsetof(HttpCacheResponseHeader, headers) == 16); +static_assert(sizeof(HttpCacheResponse) == 32); +static_assert(offsetof(HttpCacheResponse, contentLength) == 24); +static_assert(sizeof(HttpCacheQuotaEntry) == 16); +static_assert(sizeof(HttpCacheQuota) == 16); + extern "C" { -int APS5_VABI sceHttpCacheClear() { +int APS5_VABI sceHttpCacheClear(std::int32_t cacheId, std::int32_t userId, const char* tag) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheClearAll() { +int APS5_VABI sceHttpCacheClearAll(std::int32_t cacheId, std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheCompleteRequest() { +int APS5_VABI sceHttpCacheCompleteRequest(std::int32_t requestId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheCreateRequest() { +int APS5_VABI sceHttpCacheCreateRequest(std::int32_t cacheId, std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheCreateRequestWithTag() { +int APS5_VABI sceHttpCacheCreateRequestWithTag(std::int32_t cacheId, std::int32_t userId, const char* tag) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheDeleteRequest() { +int APS5_VABI sceHttpCacheDeleteRequest(std::int32_t requestId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheInit() { +int APS5_VABI sceHttpCacheInit(std::size_t poolSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheReadData() { +int APS5_VABI sceHttpCacheReadData(std::int32_t requestId, std::size_t size, void* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheRetrieve() { +int APS5_VABI sceHttpCacheRetrieve(std::int32_t requestId, const HttpCacheRequest* request, HttpCacheResponse* response, HttpCacheRequest* nextRequest, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheRetrieveWithMemoryPool() { +int APS5_VABI sceHttpCacheRetrieveWithMemoryPool(std::int32_t requestId, const HttpCacheRequest* request, HttpCacheResponse* response, HttpCacheRequest* nextRequest, std::int32_t* result, void* pool, std::size_t* requiredSize, std::size_t poolSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheRevalidate() { +int APS5_VABI sceHttpCacheRevalidate(std::int32_t requestId, const HttpCacheResponseHeader* header, HttpCacheResponse* response, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheRevalidateWithMemoryPool() { +int APS5_VABI sceHttpCacheRevalidateWithMemoryPool(std::int32_t requestId, const HttpCacheResponseHeader* header, HttpCacheResponse* response, std::int32_t* result, void* pool, std::size_t* requiredSize, std::size_t poolSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheSetCacheSharing() { +int APS5_VABI sceHttpCacheSetCacheSharing(std::uint8_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheSetData() { +int APS5_VABI sceHttpCacheSetData(std::int32_t requestId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheSetQuota() { +int APS5_VABI sceHttpCacheSetQuota(std::int32_t cacheId, const HttpCacheQuota* quota) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheSetResponseHeader() { +int APS5_VABI sceHttpCacheSetResponseHeader(std::int32_t requestId, const HttpCacheResponseHeader* header) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheSystemClearAll() { +int APS5_VABI sceHttpCacheSystemClearAll(const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpCacheSystemInit() { +int APS5_VABI sceHttpCacheSystemInit(const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -108,7 +157,7 @@ int APS5_VABI sceHttpCacheSystemTerm() { return 0; } -int APS5_VABI sceHttpCacheTerm() { +int APS5_VABI sceHttpCacheTerm(std::int32_t cacheId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md index 9ee1af9ece..a2d7b748a6 100644 --- a/docs/dev/debt/libSceHttpCache.md +++ b/docs/dev/debt/libSceHttpCache.md @@ -2,6 +2,5 @@ ### Unknown function info -- [sceHttpCacheClear, sceHttpCacheClearAll, sceHttpCacheCompleteRequest, sceHttpCacheCreateRequest, sceHttpCacheCreateRequestWithTag, sceHttpCacheDeleteRequest, sceHttpCacheInit, sceHttpCacheReadData, sceHttpCacheRetrieve, sceHttpCacheRetrieveWithMemoryPool, sceHttpCacheRevalidate, sceHttpCacheRevalidateWithMemoryPool, sceHttpCacheSetCacheSharing, sceHttpCacheSetData, sceHttpCacheSetQuota, sceHttpCacheSetResponseHeader, sceHttpCacheSystemClearAll, sceHttpCacheSystemInit, sceHttpCacheSystemSendStatistics, sceHttpCacheSystemShutdown, sceHttpCacheSystemTerm and sceHttpCacheTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp) (libSceHttpCache) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttpCache.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - +- [sceHttpCacheSystemSendStatistics and sceHttpCacheSystemTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp): the supplied implementations return zero without reading arguments. No callers have yet established whether their public prototypes include unused arguments. - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheClearCacheInfo`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheDebugSetUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). From aaf286859beb47dd0ccfe5a1718bb4a4f58d0c35 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:21:42 +0300 Subject: [PATCH 0594/1360] fix(bluetooth): restore hid callbacks and registration checks --- core/libs/prx/libSceBluetoothHid/Export.cpp | 98 +++++++++++++++++---- core/libs/tests/GuestBluetoothHid.cpp | 70 ++++++++++----- docs/dev/debt/libSceBluetoothHid.md | 6 +- 3 files changed, 136 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index 5b7a222827..63c76ddd55 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -4,47 +4,115 @@ #include "prx/libc/include/General.hpp" #include <string> +#include <mutex> + +struct BluetoothHidThreadParam { + std::int32_t priority; + std::uint32_t padding; + std::uint64_t affinityMask; + std::uint32_t reserved; + std::uint32_t tailPadding; +}; + +struct BluetoothHidParam { + const BluetoothHidThreadParam* threadParam; + std::uint32_t reserved; + std::uint32_t padding; +}; + +struct BluetoothHidEvent { + std::uint64_t deviceHandle; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint8_t type; + std::uint8_t status; + std::uint16_t reportSize; + std::uint8_t reserved[8]; + std::uint8_t report[78]; + std::uint8_t padding[2]; +}; + +struct BluetoothHidDeviceInfo; +struct BluetoothHidDeviceName; +using BluetoothHidCallback = void (APS5_VABI *)(const BluetoothHidEvent*, std::int32_t, void*); + +static_assert(sizeof(BluetoothHidThreadParam) == 24); +static_assert(offsetof(BluetoothHidThreadParam, affinityMask) == 8); +static_assert(offsetof(BluetoothHidThreadParam, reserved) == 16); +static_assert(sizeof(BluetoothHidParam) == 16); +static_assert(sizeof(BluetoothHidEvent) == 104); +static_assert(offsetof(BluetoothHidEvent, reportSize) == 14); +static_assert(offsetof(BluetoothHidEvent, report) == 24); + +namespace { +std::mutex stateMutex; +bool initialized = false; +bool callbackRegistered = false; +constexpr std::int32_t InvalidArgument = static_cast<std::int32_t>(0x81540001u); +constexpr std::int32_t NotInitialized = static_cast<std::int32_t>(0x81540004u); +constexpr std::int32_t NoCallback = static_cast<std::int32_t>(0x81540006u); +} extern "C" { std::int32_t APS5_VABI sceBluetoothHidInit() { + std::lock_guard lock(stateMutex); + initialized = true; return 0; } -std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(void* callback, std::int32_t flags, void* param) { - (void)flags; - (void)param; - if (callback == nullptr) throw std::invalid_argument(std::string(__func__) + ": null callback"); +std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(BluetoothHidCallback callback, void* userData, const BluetoothHidParam* param) { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; + if (callback == nullptr) return InvalidArgument; + if (param != nullptr) { + if (param->reserved != 0) return InvalidArgument; + if (param->threadParam != nullptr) { + if (param->threadParam->reserved != 0) return InvalidArgument; + throw std::runtime_error("sceBluetoothHidRegisterCallback: custom worker scheduling is not implemented"); + } + } + (void)userData; + callbackRegistered = true; return 0; } std::int32_t APS5_VABI sceBluetoothHidUnregisterCallback() { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; + if (!callbackRegistered) return NoCallback; + callbackRegistered = false; return 0; } std::int32_t APS5_VABI sceBluetoothHidRegisterDevice(std::uint16_t vendorId, std::uint16_t productId) { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; + if (!callbackRegistered) return NoCallback; (void)vendorId; (void)productId; return 0; } std::int32_t APS5_VABI sceBluetoothHidUnregisterDevice(std::uint16_t vendorId, std::uint16_t productId) { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; (void)vendorId; (void)productId; return 0; } -int APS5_VABI sceBluetoothHidGetDeviceName() { +int APS5_VABI sceBluetoothHidGetDeviceName(std::uint64_t deviceHandle, BluetoothHidDeviceName* name) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidGetInputReport() { +int APS5_VABI sceBluetoothHidGetInputReport(std::uint64_t deviceHandle, std::uint8_t reportId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidSetOutputReport() { +int APS5_VABI sceBluetoothHidSetOutputReport(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -53,42 +121,42 @@ int APS5_VABI sceBluetoothHidDebugGetVersion() { return 0x1e; } -int APS5_VABI sceBluetoothHidDisconnectDevice() { +int APS5_VABI sceBluetoothHidDisconnectDevice(std::int32_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidGetDeviceInfo() { +int APS5_VABI sceBluetoothHidGetDeviceInfo(std::int32_t deviceId, BluetoothHidDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidGetFeatureReport() { +int APS5_VABI sceBluetoothHidGetFeatureReport(std::uint64_t deviceHandle, std::uint8_t reportId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidGetReportDescriptor() { +int APS5_VABI sceBluetoothHidGetReportDescriptor(std::uint64_t deviceHandle, void* descriptor, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidInterruptOutput() { +int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidParamInitialize() { +int APS5_VABI sceBluetoothHidParamInitialize(BluetoothHidParam* param, const BluetoothHidThreadParam* threadParam) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidSetFeatureReport() { +int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidThreadParamInitialize() { +int APS5_VABI sceBluetoothHidThreadParamInitialize(BluetoothHidThreadParam* param, std::int32_t priority, std::uint64_t affinityMask) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index e118532944..fb37aa3c23 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -1,29 +1,46 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> +#include <cstddef> #include <cstdio> #include <cstdlib> #include <stdexcept> +struct BluetoothHidEvent; +struct BluetoothHidDeviceInfo; +struct BluetoothHidThreadParam { + std::int32_t priority; + std::uint32_t padding; + std::uint64_t affinityMask; + std::uint32_t reserved; + std::uint32_t tailPadding; +}; +struct BluetoothHidParam { + const BluetoothHidThreadParam* threadParam; + std::uint32_t reserved; + std::uint32_t padding; +}; +using BluetoothHidCallback = void (APS5_VABI *)(const BluetoothHidEvent*, std::int32_t, void*); + extern "C" { std::int32_t APS5_VABI sceBluetoothHidInit(); -std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(void* callback, std::int32_t flags, void* param); +std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(BluetoothHidCallback callback, void* userData, const BluetoothHidParam* param); std::int32_t APS5_VABI sceBluetoothHidUnregisterCallback(); std::int32_t APS5_VABI sceBluetoothHidRegisterDevice(std::uint16_t vendorId, std::uint16_t productId); std::int32_t APS5_VABI sceBluetoothHidUnregisterDevice(std::uint16_t vendorId, std::uint16_t productId); -int APS5_VABI sceBluetoothHidGetInputReport(); +int APS5_VABI sceBluetoothHidGetInputReport(std::uint64_t, std::uint8_t); int APS5_VABI sceBluetoothHidDebugGetVersion(); -int APS5_VABI sceBluetoothHidGetDeviceInfo(); -int APS5_VABI sceBluetoothHidDisconnectDevice(); -int APS5_VABI sceBluetoothHidGetFeatureReport(); -int APS5_VABI sceBluetoothHidSetFeatureReport(); +int APS5_VABI sceBluetoothHidGetDeviceInfo(std::int32_t, BluetoothHidDeviceInfo*); +int APS5_VABI sceBluetoothHidDisconnectDevice(std::int32_t); +int APS5_VABI sceBluetoothHidGetFeatureReport(std::uint64_t, std::uint8_t); +int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t, std::uint8_t, const void*, std::size_t); } namespace { int callbackCalls = 0; -void APS5_VABI Callback() { +void APS5_VABI Callback(const BluetoothHidEvent*, std::int32_t, void*) { ++callbackCalls; } @@ -37,21 +54,34 @@ void Require(bool condition, const char* message) { } int main() { + constexpr auto notInitialized = static_cast<std::int32_t>(0x81540004u); + constexpr auto invalidArgument = static_cast<std::int32_t>(0x81540001u); + constexpr auto noCallback = static_cast<std::int32_t>(0x81540006u); + Require(sceBluetoothHidRegisterCallback(nullptr, nullptr, nullptr) == notInitialized, "initialization check order"); + Require(sceBluetoothHidRegisterDevice(1, 2) == notInitialized, "registration before initialization"); + Require(sceBluetoothHidUnregisterDevice(1, 2) == notInitialized, "unregistration before initialization"); + Require(sceBluetoothHidUnregisterCallback() == notInitialized, "callback removal before initialization"); Require(sceBluetoothHidInit() == 0, "initialization failed"); - std::uint8_t param[16]{}; - Require(sceBluetoothHidRegisterCallback(reinterpret_cast<void*>(&Callback), 0, param) == 0, "callback registration failed"); + Require(sceBluetoothHidInit() == 0, "repeated initialization failed"); + Require(sceBluetoothHidRegisterDevice(1, 2) == noCallback, "registration without callback"); + Require(sceBluetoothHidUnregisterCallback() == noCallback, "removing absent callback"); + BluetoothHidParam param{}; + Require(sceBluetoothHidRegisterCallback(&Callback, &callbackCalls, ¶m) == 0, "callback registration failed"); Require(sceBluetoothHidRegisterDevice(0x44f, 0x1000) == 0, "device registration failed"); Require(callbackCalls == 0, "a device connection was reported"); + Require(sceBluetoothHidRegisterCallback(nullptr, nullptr, ¶m) == invalidArgument, "null callback validation"); + param.reserved = 1; + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, ¶m) == invalidArgument, "reserved field validation"); + param.reserved = 0; + BluetoothHidThreadParam threadParam{}; + threadParam.reserved = 1; + param.threadParam = &threadParam; + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, ¶m) == invalidArgument, "thread reserved field validation"); + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, nullptr) == 0, "optional parameters rejected"); bool threw = false; - try { - sceBluetoothHidRegisterCallback(nullptr, 0, param); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw, "a null callback was accepted"); threw = false; try { - sceBluetoothHidGetInputReport(); + sceBluetoothHidGetInputReport(0, 0); } catch (const std::runtime_error&) { threw = true; } @@ -59,28 +89,28 @@ int main() { Require(sceBluetoothHidDebugGetVersion() == 0x1e, "debug get version did not return the firmware version"); threw = false; try { - sceBluetoothHidGetDeviceInfo(); + sceBluetoothHidGetDeviceInfo(0, nullptr); } catch (const std::runtime_error&) { threw = true; } Require(threw, "get device info did not throw"); threw = false; try { - sceBluetoothHidDisconnectDevice(); + sceBluetoothHidDisconnectDevice(0); } catch (const std::runtime_error&) { threw = true; } Require(threw, "disconnect device did not throw"); threw = false; try { - sceBluetoothHidGetFeatureReport(); + sceBluetoothHidGetFeatureReport(0, 0); } catch (const std::runtime_error&) { threw = true; } Require(threw, "get feature report did not throw"); threw = false; try { - sceBluetoothHidSetFeatureReport(); + sceBluetoothHidSetFeatureReport(0, 0, nullptr, 0); } catch (const std::runtime_error&) { threw = true; } diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index b8ec7bf2ab..faf41e09da 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -2,9 +2,9 @@ ### Silent stubs -- [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) implements only the no-device path: initialization and callback and device registration succeed, the callback is never called because no device connects, and calls that need a connected device throw +- [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): the host still has no device backend or event worker. Registration retains only lifecycle state and never delivers the recovered 104-byte callback events. Custom worker scheduling throws; device ioctl failures and resource exhaustion are not modeled. ### Unknown function info -- [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - signatures taken from PPSA01317: `sceBluetoothHidInit()` and `sceBluetoothHidUnregisterCallback()` without arguments, `sceBluetoothHidRegisterCallback(callback, 0, param)` with a 16-byte param, and `sceBluetoothHidRegisterDevice` / `sceBluetoothHidUnregisterDevice(vendorId, productId)`; the report and device name exports are declared without parameters -- [sceBluetoothHidDebugGetVersion, sceBluetoothHidDisconnectDevice, sceBluetoothHidGetDeviceInfo, sceBluetoothHidGetFeatureReport, sceBluetoothHidGetReportDescriptor, sceBluetoothHidInterruptOutput, sceBluetoothHidParamInitialize, sceBluetoothHidSetFeatureReport, sceBluetoothHidThreadParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [sceBluetoothHidGetDeviceName and sceBluetoothHidGetDeviceInfo](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): output pointers are forwarded to device ioctls without accessing the buffers. Their output sizes and layouts remain unresolved. Report APIs use 64-bit device handles; device-info and disconnect APIs use 32-bit device IDs. +- [sceBluetoothHidDebugGetVersion](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): returns a constant without reading arguments, so the supplied implementation does not establish whether the public prototype has unused parameters. From 8c411544ebeb54109464977fca7bcf273feb10a1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:27:05 +0300 Subject: [PATCH 0595/1360] fix(keyboard): restore device arguments and verified key mappings --- core/libs/prx/libSceKeyboard/Export.cpp | 27 ++++++++++--------- .../prx/libSceKeyboard/src/keyboard_impl.cpp | 7 ++--- .../prx/libSceKeyboard/tests/Keyboard.cpp | 8 +++--- docs/dev/debt/libSceKeyboard.md | 6 +++-- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index d8d408237a..c7be6fe290 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceKeyboard/include/KeyboardState.hpp" +struct KeyboardDeviceInfo; +struct KeyboardProcessFocus; + extern "C" { int APS5_VABI sceKeyboardInit(void) { @@ -24,21 +27,21 @@ int APS5_VABI sceKeyboardReadState(int32_t handle, KeyboardData* data) { return Keyboard::ReadState(handle, data); } -int APS5_VABI sceKeyboardGetKey2Char(int32_t handle, int32_t arrange, uint32_t led, uint32_t modifier_key, uint16_t key_code, KeyboardCharData* char_data) { - return Keyboard::GetKey2Char(handle, arrange, led, modifier_key, key_code, char_data); +int APS5_VABI sceKeyboardGetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint8_t led, std::uint8_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { + return Keyboard::GetKey2Char(handle, arrange, led, modifierKey, keyCode, charData); } -int APS5_VABI sceKeyboardConnectPort(void) { +int APS5_VABI sceKeyboardConnectPort(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardDebugGetDeviceId(void) { +int APS5_VABI sceKeyboardDebugGetDeviceId(std::int32_t handle, std::int32_t* deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardDeviceOpen(void) { +int APS5_VABI sceKeyboardDeviceOpen(std::uint64_t deviceId, std::int32_t index, std::uint32_t options) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -48,27 +51,27 @@ int APS5_VABI sceKeyboardDisconnectAll(void) { return 0; } -int APS5_VABI sceKeyboardDisconnectDevice(void) { +int APS5_VABI sceKeyboardDisconnectDevice(std::int32_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardDisconnectPort(void) { +int APS5_VABI sceKeyboardDisconnectPort(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardGetConnection(void) { +int APS5_VABI sceKeyboardGetConnection(std::int32_t handle, bool* connected) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardGetDeviceInfo(void) { +int APS5_VABI sceKeyboardGetDeviceInfo(std::int32_t deviceId, KeyboardDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardGetHandle(void) { +int APS5_VABI sceKeyboardGetHandle(std::int32_t userId, std::int32_t type, std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -88,12 +91,12 @@ int APS5_VABI sceKeyboardPadEmulateOpen(void) { return 0; } -int APS5_VABI sceKeyboardSetProcessFocus(void) { +int APS5_VABI sceKeyboardSetProcessFocus(const KeyboardProcessFocus* focus) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKeyboardSetProcessPrivilege(void) { +int APS5_VABI sceKeyboardSetProcessPrivilege(std::uint8_t privilege) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 0a16e2d75e..bd9cf7d8f8 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -19,8 +19,8 @@ std::uint32_t modifiers = 0; std::array<std::uint16_t, KEYBOARD_MAX_KEYCODES> keys{}; int keyCount = 0; -constexpr char US_NORMAL[] = "1234567890\n\0\b\t -=[]\\\0;'`,./"; -constexpr char US_SHIFTED[] = "!@#$%^&*()\n\0\b\t _+{}|\0:\"~<>?"; +constexpr char US_NORMAL[] = "1234567890\n\0\b\t -=[]\\\\;'`,./"; +constexpr char US_SHIFTED[] = "!@#$%^&*()\n\0\b\t _+{}||:\"~<>?"; 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."; @@ -81,8 +81,9 @@ std::uint16_t character(bool jis, std::uint32_t ledState, std::uint32_t modifier const char* table = jis ? (shift ? JIS_SHIFTED : JIS_NORMAL) : (shift ? US_SHIFTED : US_NORMAL); return static_cast<unsigned char>(table[keyCode - 0x1e]); } + if (!jis && keyCode == 0x64) return shift ? '|' : '\\'; if (jis && keyCode == 0x87) return shift ? '_' : '\\'; - if (jis && keyCode == 0x89) return shift ? '|' : '\\'; + if (jis && keyCode == 0x89) return shift ? '|' : 0x00A5; switch (keyCode) { case 0x54: return '/'; case 0x55: return '*'; diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 97e1dd5129..7c97932bff 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -13,7 +13,7 @@ int APS5_VABI sceKeyboardOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceKeyboardClose(std::int32_t); int APS5_VABI sceKeyboardRead(std::int32_t, KeyboardData*, std::int32_t); int APS5_VABI sceKeyboardReadState(std::int32_t, KeyboardData*); -int APS5_VABI sceKeyboardGetKey2Char(std::int32_t, std::int32_t, std::uint32_t, std::uint32_t, std::uint16_t, KeyboardCharData*); +int APS5_VABI sceKeyboardGetKey2Char(std::int32_t, std::int32_t, std::uint8_t, std::uint8_t, std::uint16_t, KeyboardCharData*); int APS5_VABI sceKeyboardConnectPort(void); int APS5_VABI sceKeyboardDisconnectAll(void); } @@ -115,13 +115,15 @@ int main() { Require(Char(KEYBOARD_LED_CAPS_LOCK, 0, 0x1e) == '1' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x1e) == '!'); Require(Char(0, 0, 0x27) == '0' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x27) == ')'); Require(Char(0, 0, 0x28) == '\n' && Char(0, 0, 0x2a) == '\b' && Char(0, 0, 0x2b) == '\t' && Char(0, 0, 0x2c) == ' '); - Require(Char(0, 0, 0x29) == 0 && Char(0, 0, 0x32) == 0 && Char(0, 0, 0x3a) == 0 && Char(0, 0, 0xe1) == 0); + Require(Char(0, 0, 0x29) == 0 && Char(0, 0, 0x3a) == 0 && Char(0, 0, 0xe1) == 0); Require(Char(0, 0, 0x31) == '\\' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x31) == '|'); Require(Char(0, 0, 0x34) == '\'' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x34) == '"'); Require(Char(0, 0, 0x38) == '/' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x38) == '?'); 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, 0x32) == '\\' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x32) == '|'); + Require(Char(0, 0, 0x64) == '\\' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x64) == '|'); Require(Char(0, 0, 0x87) == 0 && Char(0, 0, 0x89) == 0); constexpr std::uint32_t shift = KEYBOARD_MOD_LEFT_SHIFT; @@ -134,7 +136,7 @@ int main() { 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, 0x87) == '\\' && Jis(shift, 0x87) == '_' && Jis(0, 0x89) == 0x00A5 && 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); diff --git a/docs/dev/debt/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md index a0a894689e..60edc36b24 100644 --- a/docs/dev/debt/libSceKeyboard.md +++ b/docs/dev/debt/libSceKeyboard.md @@ -2,9 +2,11 @@ ### Functional -- [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. +- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp): the original validates HID usages 4 through 231 before the handle and preserves unconverted HID usages in the output with `processed == 0`. The host instead accepts out-of-range usages and writes zero for unconverted keys. The original ignores the Kana LED, while the host throws for it. This partially implemented function contains `NotImplemented_nid_no_patch(__func__)`; its body is preserved under the requested restriction. ### Unknown function info - [sceDbgKeyboardClose, sceDbgKeyboardGetKey2Char, sceDbgKeyboardInit, sceDbgKeyboardOpen, sceDbgKeyboardRead, sceDbgKeyboardReadState](../../../core/libs/prx/libSceKeyboard/Export.cpp) (libSceKeyboard) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceKeyboard.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. -- [sceKeyboardConnectPort, sceKeyboardDebugGetDeviceId, sceKeyboardDeviceOpen, sceKeyboardDisconnectAll, sceKeyboardDisconnectDevice, sceKeyboardDisconnectPort, sceKeyboardGetConnection, sceKeyboardGetDeviceInfo, sceKeyboardGetHandle, sceKeyboardMbusInit, sceKeyboardPadEmulateClose, sceKeyboardPadEmulateOpen, sceKeyboardSetProcessFocus, sceKeyboardSetProcessPrivilege](../../../core/libs/prx/libSceKeyboard/Export.cpp) (libSceKeyboard) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [sceKeyboardGetDeviceInfo and sceKeyboardSetProcessFocus](../../../core/libs/prx/libSceKeyboard/Export.cpp): pointer arguments are passed to device ioctls. Device-info storage is 168 bytes in the connection-query caller, but its fields and the process-focus layout remain unresolved. +- [sceKeyboardConnectPort, sceKeyboardDisconnectPort and sceKeyboardDeviceOpen](../../../core/libs/prx/libSceKeyboard/Export.cpp): argument widths are confirmed. The meaning/order of the two 32-bit port arguments and the final 32-bit device-open argument still require the device-service backend. +- [sceKeyboardPadEmulateClose and sceKeyboardPadEmulateOpen](../../../core/libs/prx/libSceKeyboard/Export.cpp): the supplied keyboard library has neither export; their signatures remain unresolved. From 7b44b3c8d171afec9d395a01b72be18abe4c7dc4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:29:36 +0300 Subject: [PATCH 0596/1360] fix(mbus): restore device service parameter pointers --- core/libs/prx/libSceMbus/Export.cpp | 42 ++++++++++++++++++--- core/libs/prx/libSceMbus/PendingExports.cpp | 4 +- core/libs/tests/GuestMbus.cpp | 22 +++++++++-- docs/dev/debt/libSceMbus.md | 6 ++- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceMbus/Export.cpp b/core/libs/prx/libSceMbus/Export.cpp index 10a101f22c..6d67b46184 100644 --- a/core/libs/prx/libSceMbus/Export.cpp +++ b/core/libs/prx/libSceMbus/Export.cpp @@ -5,11 +5,41 @@ #include <string> +struct DeviceServiceThreadParam { + std::uint64_t affinityMask; + std::int32_t priority; + std::uint32_t reserved; +}; + +struct DeviceServiceInitParam { + std::uint32_t size; + std::uint32_t reserved; + const DeviceServiceThreadParam* threadParam; +}; + +struct DeviceServiceUsageFilter { + std::uint16_t usagePage; + std::uint16_t minimumUsage; + std::uint16_t maximumUsage; +}; + +static_assert(sizeof(DeviceServiceThreadParam) == 16); +static_assert(offsetof(DeviceServiceThreadParam, priority) == 8); +static_assert(sizeof(DeviceServiceInitParam) == 16); +static_assert(offsetof(DeviceServiceInitParam, threadParam) == 8); +static_assert(sizeof(DeviceServiceUsageFilter) == 6); + extern "C" { -std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, std::int32_t flags) { +std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, const DeviceServiceInitParam* param) { (void)mode; - (void)flags; + if (param != nullptr) { + if (param->size != sizeof(DeviceServiceInitParam) || param->reserved != 0) return static_cast<std::int32_t>(0x809B0080u); + if (param->threadParam != nullptr) { + if (param->threadParam->reserved != 0) return static_cast<std::int32_t>(0x809B0080u); + throw std::runtime_error("sceDeviceServiceInitialize: custom worker scheduling is not implemented"); + } + } return 0; } @@ -17,14 +47,14 @@ std::int32_t APS5_VABI sceDeviceServiceTerminate() { return 0; } -std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t deviceType) { - (void)deviceType; +std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t clear) { + (void)clear; return 0; } -std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, std::int32_t arg1, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint64_t infoSize) { +std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, const DeviceServiceUsageFilter* filter, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint32_t infoSize) { (void)deviceClass; - (void)arg1; + (void)filter; (void)arg2; (void)infos; (void)reserved; diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index 907eeb12c0..deb64a2463 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -31,7 +31,7 @@ int APS5_VABI sceMbusAddHandle() { return 0; } -int APS5_VABI sceMbusAddHandleByDeviceId() { +int APS5_VABI sceMbusAddHandleByDeviceId(std::uint32_t deviceClass, std::int32_t handle, std::uint64_t deviceId, std::uint32_t first, std::uint32_t second, std::uint32_t third) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -516,7 +516,7 @@ int APS5_VABI sceMbusSetTestFlag() { return 0; } -int APS5_VABI sceMbusSetThreadParam() { +int APS5_VABI sceMbusSetThreadParam(std::int32_t priority, std::uint64_t affinityMask, std::uint64_t reserved) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestMbus.cpp b/core/libs/tests/GuestMbus.cpp index c4f97b1141..d1c7c85258 100644 --- a/core/libs/tests/GuestMbus.cpp +++ b/core/libs/tests/GuestMbus.cpp @@ -6,12 +6,20 @@ #include <cstdlib> #include <stdexcept> +struct DeviceServiceThreadParam; +struct DeviceServiceInitParam { + std::uint32_t size; + std::uint32_t reserved; + const DeviceServiceThreadParam* threadParam; +}; +struct DeviceServiceUsageFilter; + extern "C" { -std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, std::int32_t flags); +std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, const DeviceServiceInitParam* param); std::int32_t APS5_VABI sceDeviceServiceTerminate(); -std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t deviceType); +std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t clear); +std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, const DeviceServiceUsageFilter* filter, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint32_t infoSize); std::int32_t APS5_VABI sceDeviceServiceGetGeneration(); -std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, std::int32_t arg1, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint64_t infoSize); } namespace { @@ -26,6 +34,14 @@ void Require(bool condition, const char* message) { } int main() { + DeviceServiceInitParam param{}; + constexpr auto invalidArgument = static_cast<std::int32_t>(0x809B0080u); + Require(sceDeviceServiceInitialize(3, ¶m) == invalidArgument, "invalid parameter size accepted"); + param.size = 16; + param.reserved = 1; + Require(sceDeviceServiceInitialize(3, ¶m) == invalidArgument, "reserved field accepted"); + param.reserved = 0; + Require(sceDeviceServiceInitialize(3, ¶m) == 0, "default parameter initialization failed"); Require(sceDeviceServiceInitialize(3, 0) == 0, "initialization failed"); Require(sceDeviceServiceGetGeneration() == 0, "the device set started at another generation"); Require(sceDeviceServiceGetEventState(1) == 0, "a device event was reported"); diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 1ae13da251..c19904c941 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -6,6 +6,8 @@ ### Unknown function info -- [libSceMbus](../../../core/libs/prx/libSceMbus/Export.cpp) (`libSceDeviceService`) - signatures taken from PPSA01317, which calls `sceDeviceServiceInitialize(3, 0)`, polls `sceDeviceServiceGetEventState(1)` and queries again when bit 0 or 1 is set, and calls `sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, infos, 1, &count, &unknown, 0x70)`, treating a negative result as an error and `count` as the number of entries. The host has no devices: the event state is 0 and a query returns 0 with `count` 0 +- [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 115 symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 113 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. + +- [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. From 223cc1c45695569e55c7668c7fbf159db7b0d315 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:32:50 +0300 Subject: [PATCH 0597/1360] fix(opuscelt): restore encoder and control signatures --- core/libs/prx/libSceOpusCeltEnc/Export.cpp | 24 ++++++++++++---------- docs/dev/debt/libSceOpusCeltEnc.md | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index df4e7d1854..bcbe6e9029 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -3,59 +3,61 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct OpusCeltEncoder; + extern "C" { -int APS5_VABI sceOpusCeltEncCreate() { +int APS5_VABI sceOpusCeltEncCreate(OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncCreateEx() { +int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t* contextId, OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncCtl() { +int APS5_VABI sceOpusCeltEncCtl(OpusCeltEncoder* encoder, std::int32_t request, ...) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncDestroy() { +int APS5_VABI sceOpusCeltEncDestroy(OpusCeltEncoder* encoder) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncEncode() { +int APS5_VABI sceOpusCeltEncEncode(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncEncodeFloat() { +int APS5_VABI sceOpusCeltEncEncodeFloat(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority() { +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncEncodeWithPriority() { +int APS5_VABI sceOpusCeltEncEncodeWithPriority(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncGetSize() { +int APS5_VABI sceOpusCeltEncGetSize(std::int32_t channels) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncInitialize() { +int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t* contextId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncTerminate() { +int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t* contextId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index 6f232c9cee..6ef2fceb48 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceOpusCeltEncCreate, sceOpusCeltEncCreateEx, sceOpusCeltEncCtl, sceOpusCeltEncDestroy, sceOpusCeltEncEncode, sceOpusCeltEncEncodeFloat, sceOpusCeltEncEncodeFloatWithPriority, sceOpusCeltEncEncodeWithPriority, sceOpusCeltEncGetSize, sceOpusCeltEncInitialize and sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. From 45555f165f08272906c410a0b51e71bf23941c87 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:32:51 +0300 Subject: [PATCH 0598/1360] fix(opussilk): restore encoding and batch control signatures --- core/libs/prx/libSceOpusSilkEnc/Export.cpp | 30 ++++++++++++---------- docs/dev/debt/libSceOpusSilkEnc.md | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 201dab2e4b..4e741a3bf9 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -3,74 +3,76 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct OpusSilkEncoder; + extern "C" { -int APS5_VABI sceOpusSilkEncCancel() { +int APS5_VABI sceOpusSilkEncCancel(OpusSilkEncoder* encoder, std::uint32_t batchId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCreate() { +int APS5_VABI sceOpusSilkEncCreate(OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCreateEx() { +int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t* contextId, OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCtl() { +int APS5_VABI sceOpusSilkEncCtl(OpusSilkEncoder* encoder, std::int32_t request, ...) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncDestroy() { +int APS5_VABI sceOpusSilkEncDestroy(OpusSilkEncoder* encoder) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncode() { +int APS5_VABI sceOpusSilkEncEncode(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeFloat() { +int APS5_VABI sceOpusSilkEncEncodeFloat(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority() { +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout() { +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeWithPriority() { +int APS5_VABI sceOpusSilkEncEncodeWithPriority(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout() { +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncGetSize() { +int APS5_VABI sceOpusSilkEncGetSize(std::int32_t channels) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncInitialize() { +int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t* contextId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncTerminate() { +int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t* contextId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 3e129c1e1f..266dfe90fd 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceOpusSilkEncCancel, sceOpusSilkEncCreate, sceOpusSilkEncCreateEx, sceOpusSilkEncCtl, sceOpusSilkEncDestroy, sceOpusSilkEncEncode, sceOpusSilkEncEncodeFloat, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority, sceOpusSilkEncEncodeWithPriorityAndTimeout, sceOpusSilkEncGetSize, sceOpusSilkEncInitialize and sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. From d356e6535b2a8bbf3a93431f4cd8270ee9967430 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:40:33 +0300 Subject: [PATCH 0599/1360] fix(automounter): restore usb and thread attribute arguments --- core/libs/prx/libSceAutoMounterClient/Export.cpp | 8 +++++--- docs/dev/debt/libSceAutoMounterClient.md | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAutoMounterClient/Export.cpp b/core/libs/prx/libSceAutoMounterClient/Export.cpp index 3b3e49adb8..e4845005e9 100644 --- a/core/libs/prx/libSceAutoMounterClient/Export.cpp +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -3,17 +3,19 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct AutoMounterUsbDeviceInfo; + extern "C" { -int APS5_VABI sceAutoMounterClientGetUsbDeviceInfo() { +int APS5_VABI sceAutoMounterClientGetUsbDeviceInfo(const char* deviceName, AutoMounterUsbDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAutoMounterClientGetUsbDeviceList() { +int APS5_VABI sceAutoMounterClientGetUsbDeviceList(char (*deviceNames)[64], std::int32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAutoMounterClientInit() { +int APS5_VABI sceAutoMounterClientInit(PthreadAttr* threadAttr) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAutoMounterClient.md b/docs/dev/debt/libSceAutoMounterClient.md index b7200004e3..1517732495 100644 --- a/docs/dev/debt/libSceAutoMounterClient.md +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -3,3 +3,4 @@ ### Unknown function info - [sceAutoMounterClientRegisterCallback, sceAutoMounterClientTerm and sceAutoMounterClientUnregisterCallback](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAutoMounterClient.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceAutoMounterClientGetUsbDeviceInfo](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp): the name string and output pointer are confirmed. The output occupies 944 bytes, including a 128-byte device name and three variable-length byte arrays with 64-bit lengths, but the metadata field meanings remain unresolved. From 7149edc821b28a42b699a978ad62084d19ee388c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:40:39 +0300 Subject: [PATCH 0600/1360] fix(razorcpu): restore job event arguments and flush output behavior --- core/libs/prx/libSceRazorCpu/Export.cpp | 30 +++++++++++++++++-------- core/libs/tests/GuestRazorCpu.cpp | 16 ++++++------- docs/dev/debt/libSceRazorCpu.md | 12 +++------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 751f920cd1..defd9d798b 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -9,18 +9,30 @@ uint32_t APS5_VABI sceRazorCpuIsCapturing(void) { return 0; } -int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args) { - (void)args; +int APS5_VABI sceRazorCpuJobManagerDispatch(const char* label, uint16_t firstId, uint16_t secondId, uint16_t firstCount, uint16_t secondCount, uint64_t firstValue, uint64_t secondValue) { + (void)label; + (void)firstId; + (void)secondId; + (void)firstCount; + (void)secondCount; + (void)firstValue; + (void)secondValue; return 0; } -int APS5_VABI sceRazorCpuJobManagerJob(const void* args) { - (void)args; +int APS5_VABI sceRazorCpuJobManagerJob(uint16_t firstId, uint16_t secondId, uint16_t thirdId, uint16_t event) { + (void)firstId; + (void)secondId; + (void)thirdId; + (void)event; return 0; } -int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { - (void)args; +int APS5_VABI sceRazorCpuJobManagerSequence(const char* label, uint16_t firstId, uint16_t secondId, uint16_t event) { + (void)label; + (void)firstId; + (void)secondId; + (void)event; return 0; } @@ -47,9 +59,9 @@ int APS5_VABI sceRazorCpuPopMarker(void) { return 0; } -int APS5_VABI sceRazorCpuFlushOccurred(uint64_t* timeSpentInFlush) { - if (timeSpentInFlush != nullptr) *timeSpentInFlush = 0; - return 0; +bool APS5_VABI sceRazorCpuFlushOccurred(uint64_t* timeSpentInFlush) { + (void)timeSpentInFlush; + return false; } int APS5_VABI sceRazorCpuPlotValue(const char* series, float value) { diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 13572d5feb..69f35dbfa0 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -7,14 +7,14 @@ extern "C" { std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); -int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); -int APS5_VABI sceRazorCpuJobManagerJob(const void* args); -int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); +int APS5_VABI sceRazorCpuJobManagerDispatch(const char* label, std::uint16_t firstId, std::uint16_t secondId, std::uint16_t firstCount, std::uint16_t secondCount, std::uint64_t firstValue, std::uint64_t secondValue); +int APS5_VABI sceRazorCpuJobManagerJob(std::uint16_t firstId, std::uint16_t secondId, std::uint16_t thirdId, std::uint16_t event); +int APS5_VABI sceRazorCpuJobManagerSequence(const char* label, std::uint16_t firstId, std::uint16_t secondId, std::uint16_t event); int APS5_VABI sceRazorCpuPushMarker(const char* label, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuNamedSync(const char* label); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); -int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); +bool APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); int APS5_VABI sceRazorCpuPlotValue(const char* series, float value); int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* description); int APS5_VABI sceRazorCpuSync(void); @@ -38,9 +38,9 @@ void Require(bool value) { if (!value) std::abort(); } int main() { Require(sceRazorCpuIsCapturing() == 0); - Require(sceRazorCpuJobManagerDispatch(nullptr) == 0); - Require(sceRazorCpuJobManagerJob(nullptr) == 0); - Require(sceRazorCpuJobManagerSequence(nullptr) == 0); + Require(sceRazorCpuJobManagerDispatch(nullptr, 1, 2, 3, 4, 5, 6) == 0); + Require(sceRazorCpuJobManagerJob(1, 2, 3, 16) == 0); + Require(sceRazorCpuJobManagerSequence(nullptr, 1, 2, 3) == 0); Require(sceRazorCpuNamedSync("frame") == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); @@ -52,7 +52,7 @@ int main() { Require(sceRazorCpuIsCapturing() == 0); std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; Require(sceRazorCpuFlushOccurred(&timeSpentInFlush) == 0); - Require(timeSpentInFlush == 0); + Require(timeSpentInFlush == 0x123456789abcdef0ull); Require(sceRazorCpuFlushOccurred(nullptr) == 0); Require(sceRazorCpuPlotValue("read time (ms)", 1.5f) == 0); Require(sceRazorCpuWriteBookmark("read timeout", "lba=0x10") == 0); diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index f6f5690d75..fed20752ee 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -1,18 +1,12 @@ # libSceRazorCpu technical debt -### Silent stubs +### Functional -- [sceRazorCpuPushMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 -- [sceRazorCpuSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the `int(void)` signature and its meaning, an unnamed frame-boundary event, come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L119-L122). Without a capture there is no event to record, so it does nothing and returns 0, like the other `sceRazorCpu*` markers here and the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/razor_cpu/razor_cpu.cpp#L143-L146) stub. PRAGMATA (PPSA02530) continues on 0; the console's result with no capture attached is unverified -- [sceRazorCpuDisableFiberUserMarkers, sceRazorCpuBeginLogicalFileAccess and sceRazorCpuEndLogicalFileAccess](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures and meanings from SharpProspero ([fiber markers](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L73-L79), [logical file access](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L189-L209)): the first makes later markers carry the thread instead of the fiber, the other two attribute the thread's file reads and writes to a named logical file. Both only change what a capture records, so without one they do nothing, and the two file-access calls return 0 for any arguments. Whether the console checks the operation (0 read, 1 write) or an unmatched end without a capture is unverified -- [sceRazorCpuInitDataTags, sceRazorCpuShutdownDataTags, sceRazorCpuTagBuffer, sceRazorCpuTagArray, sceRazorCpuResizeTaggedBuffer and sceRazorCpuUnTagBuffer](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L130-L187), which documents them as the data sampler's buffer-tag table and the tags in it. Without a capture there is no data sampler, so they neither read the caller's storage nor keep the tags, and return 0 for any arguments; the console's errors for a tag before `sceRazorCpuInitDataTags`, an unknown buffer or a full table are unverified +- [libSceRazorCpu](../../../core/libs/prx/libSceRazorCpu/Export.cpp): capture-controller registration, thread-local capture storage, marker serialization and flushing are not implemented. The host implements only the confirmed uninitialized-profiler path: markers return zero without reading labels, and `sceRazorCpuFlushOccurred` returns false without writing its optional output. ### Unknown function info - [sceRazorCpuFiberLogNameChange, sceRazorCpuFiberSwitch, sceRazorCpuGpuMarkerSync, sceRazorCpuInitializeGpuMarkerContext, sceRazorCpuInitializeInternal, sceRazorCpuSetPopMarkerCallback](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the list is the intersection of the export table of the console's [libSceRazorCpu](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRazorCpu.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. - [sceRazorCpuSetPushMarkerCallback, sceRazorCpuSetPushMarkerStaticCallback, sceRazorCpuStartCaptureInternal, sceRazorCpuStopCaptureInternal, sceRazorCpuWorkloadRunBegin, sceRazorCpuWorkloadRunEnd, sceRazorCpuWorkloadSubmit](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the list is the intersection of the export table of the console's [libSceRazorCpu](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRazorCpu.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. -- [sceRazorCpuFlushOccurred](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) -- [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture -- [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 -- [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified +- [sceRazorCpuJobManagerDispatch, sceRazorCpuJobManagerJob and sceRazorCpuJobManagerSequence](../../../core/libs/prx/libSceRazorCpu/Export.cpp): argument positions and serialized widths are recovered from machine instructions. Dispatch takes seven arguments, including two 64-bit payloads whose upper 16 bits are replaced by the count fields. The payloads are not dereferenced, so their original pointer-versus-integer types and the semantic names of the event identifiers remain unresolved. Job and Sequence serialize 16-bit identifiers and an event selector; their source enum names are unavailable. - [sceRazorCpuGetDataTagStorageSize](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - throws: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L142-L144) documents it as the byte size a tag table needs for a count of entries, but not the size of an entry From 57ee1e839e9971146232d142b67f58b02b277e15 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:43:56 +0300 Subject: [PATCH 0601/1360] fix(opuscelt): verify decoder abi and const context inputs --- core/libs/prx/libSceOpusCeltDec/Export.cpp | 4 ++-- core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp | 4 ++-- core/libs/tests/GuestOpusCeltDec.cpp | 4 ++-- docs/dev/debt/libSceOpusCeltDec.md | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceOpusCeltDec/Export.cpp b/core/libs/prx/libSceOpusCeltDec/Export.cpp index 2b04cca7be..5f40c01b3c 100644 --- a/core/libs/prx/libSceOpusCeltDec/Export.cpp +++ b/core/libs/prx/libSceOpusCeltDec/Export.cpp @@ -13,11 +13,11 @@ extern "C" { int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t* context) { return library.Initialize(context); } -int APS5_VABI sceOpusCeltDecTerminate(std::uint32_t* context) { return library.Terminate(context); } +int APS5_VABI sceOpusCeltDecTerminate(const std::uint32_t* context) { return library.Terminate(context); } int APS5_VABI sceOpusCeltDecGetSize(int channels) { return library.GetSize(channels); } -int APS5_VABI sceOpusCeltDecCreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { +int APS5_VABI sceOpusCeltDecCreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { return library.CreateEx(context, state, sampleRate, channels); } diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp index eef8d86d52..818381ea7c 100644 --- a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -38,7 +38,7 @@ public: return 0; } - int Terminate(std::uint32_t* context) { + int Terminate(const std::uint32_t* context) { if (!context) fail("null context"); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); @@ -53,7 +53,7 @@ public: return 640; } - int CreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { + int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { if (!context || !state) fail("null context or state"); GetSize(channels); if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) diff --git a/core/libs/tests/GuestOpusCeltDec.cpp b/core/libs/tests/GuestOpusCeltDec.cpp index aa3572b7e1..d6422e910b 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -7,9 +7,9 @@ extern "C" { int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*); -int APS5_VABI sceOpusCeltDecTerminate(std::uint32_t*); +int APS5_VABI sceOpusCeltDecTerminate(const std::uint32_t*); int APS5_VABI sceOpusCeltDecGetSize(int); -int APS5_VABI sceOpusCeltDecCreateEx(std::uint32_t*, void*, int, int); +int APS5_VABI sceOpusCeltDecCreateEx(const std::uint32_t*, void*, int, int); int APS5_VABI sceOpusCeltDecDecode(void*, const std::uint8_t*, int, std::int16_t*, int); int APS5_VABI sceOpusCeltDecDestroy(void*); } diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 66de7596b2..0c94aeb226 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -3,8 +3,9 @@ ### Unknown function info - [sceOpusCeltDecCreate, sceOpusCeltDecCtl, sceOpusCeltDecDecodeFloat, sceOpusCeltDecDecodeFloatWithPriority, sceOpusCeltDecDecodeWithPriority](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. -- [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001. Their signatures are assumed to match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp), and the 640-byte state size, context handling and byte-count return are taken from [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp); none of them was measured on a console. PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which libSceOpusDec here never returns, so the fallback is not reached +- [sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the non-null path forwards the AJM finalization result, but the null path returns without setting the return register. Its source return type cannot be established from this callee alone. The existing integer return is retained. ### Functional -- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) decodes with the FFmpeg decoder of [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) and shares its limits. Packets whose TOC configuration is below 16 (SILK and hybrid) throw: whether the console decodes them or returns an error code is unknown. Contexts and decoder states are separate from libSceOpusDec's +- [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. Original error codes, AJM resource ownership and termination with live decoders are not implemented. +- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; these inputs still throw. Packet loss concealment, asynchronous AJM execution, exact PCM conversion and resampler buffering remain outside the verified behavior of the FFmpeg implementation in [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp). From 31935cc859f8de8794706898dd40b3b13fd8056b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:43:59 +0300 Subject: [PATCH 0602/1360] fix(discmap): restore bitmap output widths --- core/libs/prx/libSceDiscMap/Export.cpp | 2 +- core/libs/tests/GuestDiscMap.cpp | 2 +- docs/dev/debt/libSceDiscMap.md | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceDiscMap/Export.cpp b/core/libs/prx/libSceDiscMap/Export.cpp index 80f2a899dc..8fedde6365 100644 --- a/core/libs/prx/libSceDiscMap/Export.cpp +++ b/core/libs/prx/libSceDiscMap/Export.cpp @@ -19,7 +19,7 @@ std::int32_t APS5_VABI sceDiscMapIsRequestOnHDD(const char* path, std::int64_t o } APS5_EXPORT("ioKMruft1ek", sceDiscMapUnknown_ioKMruft1ek); -std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int64_t offset, std::int64_t size, void* flags, void* out1, void* out2) { +std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int64_t offset, std::int64_t size, std::uint64_t* flags, std::uint64_t* out1, std::uint64_t* out2) { (void)path; (void)offset; (void)size; diff --git a/core/libs/tests/GuestDiscMap.cpp b/core/libs/tests/GuestDiscMap.cpp index 41f711baff..498dc7c443 100644 --- a/core/libs/tests/GuestDiscMap.cpp +++ b/core/libs/tests/GuestDiscMap.cpp @@ -6,7 +6,7 @@ extern "C" { std::int32_t APS5_VABI sceDiscMapIsRequestOnHDD(const char* path, std::int64_t offset, std::int64_t size, std::int32_t* result); -std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int64_t offset, std::int64_t size, void* flags, void* out1, void* out2); +std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int64_t offset, std::int64_t size, std::uint64_t* flags, std::uint64_t* out1, std::uint64_t* out2); } namespace { diff --git a/docs/dev/debt/libSceDiscMap.md b/docs/dev/debt/libSceDiscMap.md index 4fcd7700ec..b058be4747 100644 --- a/docs/dev/debt/libSceDiscMap.md +++ b/docs/dev/debt/libSceDiscMap.md @@ -1,10 +1,9 @@ # libSceDiscMap technical debt -### Silent stubs +### Functional -- [libSceDiscMap](../../../core/libs/prx/libSceDiscMap/Export.cpp) implements only the state with no disc bitmap: `sceDiscMapIsRequestOnHDD` and `ioKMruft1ek` return `SCE_DISC_MAP_ERROR_NO_BITMAP_INFO` (0x81100004) for every request and write nothing. CUSA02636's bundled `libSceFios2.prx` imports both; what it does with the error is not known (shadPS4 returns the same, and [shadps4-game-compatibility #617](https://github.com/shadps4-compatibility/shadps4-game-compatibility/issues/617) lists CUSA02636 as in-game) +- [libSceDiscMap](../../../core/libs/prx/libSceDiscMap/Export.cpp): loading disc bitmap metadata and resolving file byte ranges to bitmap blocks are not implemented. Both exports implement the confirmed no-bitmap path: return `SCE_DISC_MAP_ERROR_NO_BITMAP_INFO` before validating any argument and leave every output unchanged. The supplied module confirms signed 64-bit byte offset/size, a 32-bit `IsRequestOnHDD` result and three 64-bit outputs for `ioKMruft1ek`. The latter outputs describe the bitmap selector, first block and block count. Its exported source name remains unavailable. ### Unknown function info - [sceDiscMapGetPackageSize](../../../core/libs/prx/libSceDiscMap/Export.cpp) (libSceDiscMap) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDiscMap.c); the `(int64_t, int*, int*)` prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/disc_map/disc_map.cpp#L12), is not measured on a console, and every call throws -- [sceDiscMapIsRequestOnHDD, sceDiscMapUnknown_ioKMruft1ek](../../../core/libs/prx/libSceDiscMap/Export.cpp) (libSceDiscMap, NIDs `lbQKqsERhtE` and `ioKMruft1ek`) - `ioKMruft1ek` has no known name. The `(path, offset, size, result)` and `(path, offset, size, flags, out1, out2)` signatures follow [shadPS4 disc_map.cpp](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/disc_map/disc_map.cpp); the widths of the `ioKMruft1ek` outputs are unknown (shadPS4 `int*`, [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscediscmap.pas) `PInt64`), so they are `void*`. The error code is the one both return; [shadPS4 #2686](https://github.com/shadps4-emu/shadPS4/pull/2686) reads the console library as checking an initialisation global first in each call and returning it while the global is clear. Not measured on a console From 41035b4a0eeb9ac4255c21cb73e9544ff9f5a25d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:45:27 +0300 Subject: [PATCH 0603/1360] fix(gameupdate): reject unsupported service operations explicitly --- core/libs/prx/libSceGameUpdate/Export.cpp | 54 ++++++++++------------- core/libs/tests/GuestGameUpdate.cpp | 32 +++++++++----- docs/dev/debt/libSceGameUpdate.md | 7 +-- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 37c0901309..f8527db000 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -1,11 +1,10 @@ #include <atomic> #include <cstdint> #include <cstddef> +#include <stdexcept> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// Update checks need the network, which is not available; requests fail so the game skips the check. -static constexpr int SCE_GAME_UPDATE_ERROR_OFFLINE = static_cast<int>(0x80D50001); static constexpr int SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED = static_cast<int>(0x80412801); static constexpr int SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80412802); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80412803); @@ -15,19 +14,19 @@ static constexpr int SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND = static_cast<int>( static_assert(sizeof(GameUpdateCheckParam) == 0x30); static_assert(sizeof(GameUpdateCheckResult) == 0x30); -static std::atomic<bool> g_initialized{false}; +static std::atomic<bool> initialized{false}; -static int CheckRequestId(int request_id) { - if (request_id <= 0) { +static int checkRequestId(int requestId) { + if (requestId <= 0) { return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; } - if (!g_initialized.load()) { + if (!initialized.load()) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; } -static bool HasNonZeroReserved(const GameUpdateCheckParam* param) { +static bool hasNonZeroReserved(const GameUpdateCheckParam* param) { for (std::uint32_t word : param->reserved) { if (word != 0) { return true; @@ -38,12 +37,12 @@ static bool HasNonZeroReserved(const GameUpdateCheckParam* param) { extern "C" { -int APS5_VABI sceGameUpdateAbortRequest(int request_id) { - return CheckRequestId(request_id); +int APS5_VABI sceGameUpdateAbortRequest(int requestId) { + return checkRequestId(requestId); } -int APS5_VABI sceGameUpdateCheck(int request_id, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { - if (request_id <= 0) { +int APS5_VABI sceGameUpdateCheck(int requestId, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { + if (requestId <= 0) { return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; } if (param == nullptr || result == nullptr) { @@ -52,53 +51,46 @@ int APS5_VABI sceGameUpdateCheck(int request_id, const GameUpdateCheckParam* par if (param->size != sizeof(GameUpdateCheckParam) || result->size != sizeof(GameUpdateCheckResult)) { return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; } - if (param->option != 0 || HasNonZeroReserved(param)) { + if (param->option != 0 || hasNonZeroReserved(param)) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - return CheckRequestId(request_id); + return checkRequestId(requestId); } int APS5_VABI sceGameUpdateCreateRequest(void) { - if (!g_initialized.load()) { + if (!initialized.load()) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - return SCE_GAME_UPDATE_ERROR_OFFLINE; + throw std::runtime_error("sceGameUpdateCreateRequest: patch service request creation is not implemented"); } -int APS5_VABI sceGameUpdateDeleteRequest(int request_id) { - return CheckRequestId(request_id); +int APS5_VABI sceGameUpdateDeleteRequest(int requestId) { + return checkRequestId(requestId); } -int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const void* entitlement_label, GameUpdateAddcontVersionInfo* info) { - (void)service_label; +int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, GameUpdateAddcontVersionInfo* info) { + (void)serviceLabel; if (info == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - if (entitlement_label == nullptr) { + if (entitlementLabel == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - if (!g_initialized.load()) { + if (!initialized.load()) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { - return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; - } - const std::size_t size = info->size; - *info = {}; - info->size = size; - info->found = false; - return 0; + throw std::runtime_error("sceGameUpdateGetAddcontLatestVersion: entitlement version lookup is not implemented"); } int APS5_VABI sceGameUpdateInitialize(void) { - if (g_initialized.exchange(true)) { + if (initialized.exchange(true)) { return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; } return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - if (!g_initialized.exchange(false)) { + if (!initialized.exchange(false)) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } return 0; diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 13fcac85c2..c46d451c83 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -9,7 +9,7 @@ extern "C" { int APS5_VABI sceGameUpdateInitialize(void); int APS5_VABI sceGameUpdateTerminate(void); -int APS5_VABI sceGameUpdateGetAddcontLatestVersion(std::uint32_t, const void*, GameUpdateAddcontVersionInfo*); +int APS5_VABI sceGameUpdateGetAddcontLatestVersion(std::uint32_t, const NpUnifiedEntitlementLabel*, GameUpdateAddcontVersionInfo*); int APS5_VABI sceGameUpdateCreateRequest(void); int APS5_VABI sceGameUpdateCheck(int, const GameUpdateCheckParam*, GameUpdateCheckResult*); int APS5_VABI sceGameUpdateAbortRequest(int); @@ -18,6 +18,16 @@ int APS5_VABI sceGameUpdateDeleteRequest(int); static void Require(bool value) { if (!value) std::abort(); } +template <typename TFunction> +static void RequireThrows(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return; + } + std::abort(); +} + static void TestRequestArguments(int notInitialized, int invalidArgument, int invalidSize, int notFound) { GameUpdateCheckParam param{}; param.size = sizeof(param); @@ -63,7 +73,7 @@ int main() { constexpr int invalidSize = static_cast<int>(0x80412804); static_assert(sizeof(GameUpdateAddcontVersionInfo) == 0x30); - const std::uint8_t label[16] = {}; + const NpUnifiedEntitlementLabel label{}; GameUpdateAddcontVersionInfo info{}; info.size = sizeof(info); @@ -73,27 +83,27 @@ int main() { Require(sceGameUpdateTerminate() == notInitialized); TestRequestArguments(notInitialized, invalidArgument, invalidSize, notFound); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, &info) == notInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); Require(sceGameUpdateInitialize() == 0); Require(sceGameUpdateInitialize() == alreadyInitialized); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); info.size = sizeof(info) - 1; - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == invalidSize); + RequireThrows([&] { sceGameUpdateGetAddcontLatestVersion(0, &label, &info); }); std::memset(&info, 0xAB, sizeof(info)); info.size = sizeof(info) + 8; - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == 0); - Require(info.size == sizeof(info) + 8 && !info.found); - const auto* bytes = reinterpret_cast<const std::uint8_t*>(&info); - for (std::size_t i = sizeof(info.size); i < sizeof(info); ++i) Require(bytes[i] == 0); + const auto original = info; + RequireThrows([&] { sceGameUpdateGetAddcontLatestVersion(0, &label, &info); }); + Require(std::memcmp(&info, &original, sizeof(info)) == 0); + RequireThrows([] { sceGameUpdateCreateRequest(); }); Require(sceGameUpdateTerminate() == 0); Require(sceGameUpdateTerminate() == notInitialized); info.size = sizeof(info); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, &info) == notInitialized); } diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index c67a4a14a3..fd8f0f341a 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -1,6 +1,7 @@ # libSceGameUpdate technical debt -### Unknown function info +### Functional -- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` returns `0x80412803` as in prosper; KytyPS5 accepts it instead, and neither is confirmed on a console. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched -- [sceGameUpdateCreateRequest, sceGameUpdateCheck, sceGameUpdateAbortRequest, sceGameUpdateDeleteRequest](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - an initialized `sceGameUpdateCreateRequest` returns `0x80D50001` and creates no request, so `sceGameUpdateCheck`, `sceGameUpdateAbortRequest` and `sceGameUpdateDeleteRequest` answer `0x80412805` for every id that passes their argument checks. The PS5 3.20 module, as read by prosper, keeps 8 requests with ids from `0x20000000`, and answers "no update" when its patch-check service reports `0x80412883`; what that service returns on an offline console is unknown. Closes when: measured on a PS5 +- [libSceGameUpdate](../../../core/libs/prx/libSceGameUpdate/Export.cpp): initialization tracks host state but does not connect the patch service. Request creation and entitlement version lookup throw instead of fabricating an offline error or a successful no-update result. The original maintains eight service-backed requests with IDs starting at `0x20000000`; the service's offline response is still unknown. Check, Abort and Delete implement argument/state checks but have no live requests to operate on. +- [sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp): option zero is supported for argument validation. The original additionally permits option one when a kernel platform predicate succeeds; that platform mode is not modeled. +- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp): the supplied module confirms a 20-byte entitlement-label input and a 48-byte service output. Null pointers are rejected before initialization is checked. There is no local output-size validation or blanket clearing of the output; the host leaves it untouched when the missing service causes an exception. From b276137d2a05c53216b9eae3e008e1ccf79f9796 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:45:36 +0300 Subject: [PATCH 0604/1360] docs: classify razor cpu uninitialized capture stubs --- docs/dev/debt/libSceRazorCpu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index fed20752ee..092aa68738 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -1,6 +1,6 @@ # libSceRazorCpu technical debt -### Functional +### Silent stubs - [libSceRazorCpu](../../../core/libs/prx/libSceRazorCpu/Export.cpp): capture-controller registration, thread-local capture storage, marker serialization and flushing are not implemented. The host implements only the confirmed uninitialized-profiler path: markers return zero without reading labels, and `sceRazorCpuFlushOccurred` returns false without writing its optional output. From 0996b7523827c625383119070f43b7ceb68e0b03 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:48:32 +0300 Subject: [PATCH 0605/1360] fix(voiceqos): restore endpoint slots and attribute validation --- core/libs/prx/libSceVoiceQoS/Export.cpp | 133 ++++++++++++------------ core/libs/tests/GuestVoiceQoS.cpp | 53 +++++----- docs/dev/debt/libSceVoiceQoS.md | 9 +- 3 files changed, 98 insertions(+), 97 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 3e02a10932..6c52605b5e 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,12 +1,10 @@ #include <algorithm> #include <cstdint> #include <cstddef> -#include <cstring> -#include <limits> +#include <stdexcept> #include <map> #include <mutex> #include <utility> -#include <vector> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -17,9 +15,10 @@ constexpr std::int32_t VOICE_QOS_APP_TYPE_10000000 = 0x10000000; constexpr std::uint32_t VOICE_QOS_MEMORY_SIZE = 0x40000; constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast<int>(0x804E0901); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast<int>(0x804E0902); +constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT = static_cast<int>(0x804E0907); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast<int>(0x804E0905); -using Endpoint = std::map<std::int32_t, std::vector<std::uint8_t>>; +struct Endpoint {}; struct Connection { std::int32_t local; @@ -29,7 +28,7 @@ struct Connection { struct QoS { std::mutex mutex; bool initialized = false; - std::int32_t nextId = 1; + std::int32_t endpointLimit = 0; std::map<std::int32_t, Endpoint> locals; std::map<std::int32_t, Endpoint> remotes; std::map<std::int32_t, Connection> connections; @@ -40,53 +39,25 @@ QoS& State() { return qos; } -std::int32_t NewId(QoS& qos) { - const std::int32_t id = qos.nextId; - qos.nextId = qos.nextId == std::numeric_limits<std::int32_t>::max() ? 1 : qos.nextId + 1; - return id; -} - -int CreateEndpoint(std::int32_t* id, std::map<std::int32_t, Endpoint>& endpoints, QoS& qos) { +int CreateEndpoint(std::int32_t* id, std::map<std::int32_t, Endpoint>& endpoints, std::int32_t limit) { if (!id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - *id = NewId(qos); - endpoints.emplace(*id, Endpoint{}); - return 0; + for (std::int32_t candidate = 0; candidate < limit; ++candidate) { + if (!endpoints.contains(candidate)) { + endpoints.emplace(candidate, Endpoint{}); + *id = candidate; + return 0; + } + } + *id = -1; + return SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT; } int DeleteEndpoint(std::int32_t id, std::map<std::int32_t, Endpoint>& endpoints) { return endpoints.erase(id) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; } -int SetAttribute(std::map<std::int32_t, Endpoint>& endpoints, std::int32_t id, std::int32_t attribute, const void* value, std::int32_t size) { - const auto endpoint = endpoints.find(id); - if (endpoint == endpoints.end() || !value) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - if (size > 0) { - const auto* bytes = static_cast<const std::uint8_t*>(value); - endpoint->second[attribute].assign(bytes, bytes + size); - } else { - endpoint->second[attribute].clear(); - } - return 0; -} - -int GetAttribute(std::map<std::int32_t, Endpoint>& endpoints, std::int32_t id, std::int32_t attribute, void* value, std::int32_t size) { - const auto endpoint = endpoints.find(id); - if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - const auto attr = endpoint->second.find(attribute); - if (attr != endpoint->second.end() && !attr->second.empty()) { - const auto toCopy = std::min(static_cast<std::size_t>(size), attr->second.size()); - std::memcpy(value, attr->second.data(), toCopy); - if (static_cast<std::size_t>(size) > toCopy) { - std::memset(static_cast<std::uint8_t*>(value) + toCopy, 0, static_cast<std::size_t>(size) - toCopy); - } - } else { - std::memset(value, 0, static_cast<std::size_t>(size)); - } - return 0; -} - -template<typename Body> -int Locked(Body&& body) { +template<typename TBody> +int Locked(TBody&& body) { auto& qos = State(); std::lock_guard lock(qos.mutex); if (!qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; @@ -102,6 +73,7 @@ int APS5_VABI sceVoiceQoSInit(void* memBlock, uint32_t memSize, int32_t appType) std::lock_guard lock(qos.mutex); if (qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; if (!memBlock || memSize != VOICE_QOS_MEMORY_SIZE || (appType != VOICE_QOS_APP_TYPE_GAME && appType != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + qos.endpointLimit = appType == VOICE_QOS_APP_TYPE_10000000 ? 15 : 7; qos.initialized = true; return 0; } @@ -117,10 +89,15 @@ int APS5_VABI sceVoiceQoSEnd() { } int APS5_VABI sceVoiceQoSCreateLocalEndpoint(int32_t* localId, int32_t userId, int32_t deviceInId, int32_t deviceOutId) { - (void)userId; (void)deviceInId; (void)deviceOutId; - return Locked([&](QoS& qos) { return CreateEndpoint(localId, qos.locals, qos); }); + return Locked([&](QoS& qos) { + if (localId && userId == -1) { + *localId = -1; + return SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT; + } + return CreateEndpoint(localId, qos.locals, 1); + }); } int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(int32_t localId) { @@ -128,24 +105,45 @@ int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(int32_t localId) { } int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(int32_t* remoteId) { - return Locked([&](QoS& qos) { return CreateEndpoint(remoteId, qos.remotes, qos); }); + return Locked([&](QoS& qos) { return CreateEndpoint(remoteId, qos.remotes, qos.endpointLimit); }); } int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(int32_t remoteId) { - return Locked([&](QoS& qos) { return DeleteEndpoint(remoteId, qos.remotes); }); -} - -int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { return Locked([&](QoS& qos) { - if (!connectionId || !qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - *connectionId = NewId(qos); - qos.connections.emplace(*connectionId, Connection{localId, remoteId}); + const int result = DeleteEndpoint(remoteId, qos.remotes); + if (result != 0) return result; + for (auto it = qos.connections.begin(); it != qos.connections.end(); ++it) { + if (it->second.remote == remoteId) { + qos.connections.erase(it); + break; + } + } return 0; }); } +int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { + return Locked([&](QoS& qos) { + if (!connectionId || localId != 0 || remoteId < 0 || remoteId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (!qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) throw std::runtime_error("sceVoiceQoSConnect: missing voice port error behavior is not implemented"); + for (std::int32_t candidate = 0; candidate < qos.endpointLimit; ++candidate) { + if (!qos.connections.contains(candidate)) { + qos.connections.emplace(candidate, Connection{localId, remoteId}); + *connectionId = candidate; + return 0; + } + } + *connectionId = -1; + return SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT; + }); +} + int APS5_VABI sceVoiceQoSDisconnect(int32_t connectionId) { - return Locked([&](QoS& qos) { return qos.connections.erase(connectionId) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; }); + return Locked([&](QoS& qos) { + if (connectionId < 0 || connectionId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + qos.connections.erase(connectionId); + return 0; + }); } int APS5_VABI sceVoiceQoSSetConnectionAttribute(std::int32_t connectionId, std::int32_t attributeId, const void* value, std::int32_t size) { @@ -154,11 +152,17 @@ int APS5_VABI sceVoiceQoSSetConnectionAttribute(std::int32_t connectionId, std:: } int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(int32_t localId, int32_t attributeId, const void* value, int32_t size) { - return Locked([&](QoS& qos) { return SetAttribute(qos.locals, localId, attributeId, value, size); }); + return Locked([&](QoS&) -> int { + if (localId != 0 || !value || attributeId < 0 || attributeId > 3 || size != (attributeId % 2 == 0 ? 4 : 1)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + throw std::runtime_error("sceVoiceQoSSetLocalEndpointAttribute: voice port attributes are not implemented"); + }); } int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { - return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); + return Locked([&](QoS& qos) -> int { + if (remoteId < 0 || remoteId >= qos.endpointLimit || !value || attributeId != 7 || size != 1) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + throw std::runtime_error("sceVoiceQoSSetRemoteEndpointAttribute: voice port attributes are not implemented"); + }); } int APS5_VABI sceVoiceQoSReadPacket(int32_t connectionId, void* buffer, uint32_t* size) { @@ -177,7 +181,8 @@ int APS5_VABI sceVoiceQoSWritePacket(int32_t connectionId, const void* buffer, u } int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attributeId, void* value, int32_t size) { - return Locked([&](QoS& qos) { return GetAttribute(qos.locals, localId, attributeId, value, size); }); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, void* value, int32_t size) { @@ -200,20 +205,18 @@ int APS5_VABI sceVoiceQoSGetConnectionAttribute(int32_t connectionId, int32_t at int APS5_VABI sceVoiceQoSGetLocalEndpoint(int32_t connectionId, int32_t* localId) { return Locked([&](QoS& qos) { - if (!localId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (!localId || connectionId < 0 || connectionId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; const auto it = qos.connections.find(connectionId); - if (it == qos.connections.end()) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - *localId = it->second.local; + *localId = it == qos.connections.end() ? -1 : it->second.local; return 0; }); } int APS5_VABI sceVoiceQoSGetRemoteEndpoint(int32_t connectionId, int32_t* remoteId) { return Locked([&](QoS& qos) { - if (!remoteId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (!remoteId || connectionId < 0 || connectionId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; const auto it = qos.connections.find(connectionId); - if (it == qos.connections.end()) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - *remoteId = it->second.remote; + *remoteId = it == qos.connections.end() ? -1 : it->second.remote; return 0; }); } diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 9be6bbae28..8db920a44f 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -70,10 +70,10 @@ int main(int argc, char** argv) { std::int32_t remote = -1; std::int32_t connection = -1; Require(sceVoiceQoSCreateLocalEndpoint(nullptr, 1, 0, 0) == argumentInvalid); - Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == 0 && local > 0); - Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote > 0 && remote != local); - Require(sceVoiceQoSConnect(&connection, remote, local) == argumentInvalid); - Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection > 0); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == 0 && local == 0); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote == 0); + Require(sceVoiceQoSConnect(&connection, 1, remote) == argumentInvalid); + Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection == 0); std::int32_t queriedLocal = -1; std::int32_t queriedRemote = -1; Require(sceVoiceQoSGetLocalEndpoint(connection, nullptr) == argumentInvalid); @@ -86,32 +86,22 @@ int main(int argc, char** argv) { Require(sceVoiceQoSConnect(&duplicate, local, remote) == 0 && duplicate > 0 && duplicate != connection); Require(sceVoiceQoSDisconnect(duplicate) == 0); - std::int32_t otherLocal = -1; - std::int32_t otherConnection = -1; - Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 1, 0, 0) == 0); - Require(sceVoiceQoSConnect(&otherConnection, otherLocal, remote) == 0); - Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == 0); - Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == argumentInvalid); - Require(sceVoiceQoSDisconnect(otherConnection) == 0); + constexpr int limitReached = static_cast<int>(0x804E0907); + std::int32_t otherLocal = 12; + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 1, 0, 0) == limitReached && otherLocal == -1); + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 2, 0, 0) == limitReached && otherLocal == -1); const std::uint32_t level = 0x1234; - std::uint32_t readLevel = 0; - Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == 0); - Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); - Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == level); - Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, nullptr, sizeof(readLevel)) == argumentInvalid); - Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, 0) == argumentInvalid); - Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, -1) == argumentInvalid); - Require(sceVoiceQoSGetLocalEndpointAttribute(local + 100, 3, &readLevel, sizeof(readLevel)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); - Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); - Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); - Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == argumentInvalid); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, nullptr, sizeof(level)) == argumentInvalid); - Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, 0) == 0); - Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, 0) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == argumentInvalid); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); std::vector<std::uint8_t> packet(1024, 0); @@ -127,15 +117,21 @@ int main(int argc, char** argv) { Require(sceVoiceQoSWritePacket(connection, packet.data(), nullptr) == argumentInvalid); Require(sceVoiceQoSWritePacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + Require(NotImplemented([&] { sceVoiceQoSSetLocalEndpointAttribute(local, 0, &level, 4); })); + Require(NotImplemented([&] { sceVoiceQoSSetRemoteEndpointAttribute(remote, 7, &level, 1); })); Require(sceVoiceQoSDisconnect(connection) == 0); - Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); - Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == argumentInvalid); - Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == argumentInvalid); + Require(sceVoiceQoSDisconnect(connection) == 0); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == 0 && queriedLocal == -1); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == 0 && queriedRemote == -1); Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == 0); - Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0); + const int endpointLimit = appType == 0x10000000 ? 15 : 7; + for (int index = 0; index < endpointLimit; ++index) Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote == index); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == limitReached && remote == -1); + Require(sceVoiceQoSDeleteRemoteEndpoint(2) == 0); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote == 2); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == notInitialized); @@ -152,6 +148,7 @@ int main(int argc, char** argv) { Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); + Require(NotImplemented([] { sceVoiceQoSGetLocalEndpointAttribute(0, 0, nullptr, 0); })); Require(NotImplemented([] { sceVoiceQoSGetRemoteEndpointAttribute(1, 1, nullptr, 0); })); Require(NotImplemented([] { sceVoiceQoSGetConnectionAttribute(1, 1, nullptr, 0); })); Require(NotImplemented([] { sceVoiceQoSEnableChat(); })); diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index ba43885880..9b322456c0 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -7,10 +7,11 @@ ### Unknown function info -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, and `GetLocalEndpointAttribute(id, attribute, value, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws -- [libSceVoiceQoS firmware exports](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `sceVoiceQoSGetRemoteEndpointAttribute`, `sceVoiceQoSGetConnectionAttribute` and `sceVoiceQoSGetStatus` follow public PS4 SDK prototypes in [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2436-L2441) and remain throwing stubs as endpoint and connection attribute storage is unverified on hardware. `sceVoiceQoSSetMode` and `sceVoiceQoSTerminate` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [sceVoiceQoSSetMode and sceVoiceQoSTerminate](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): these exports are absent from the supplied module. Their argument declarations still require another firmware version or callers. ### Functional -- [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; here it drops every endpoint and connection -- [sceVoiceQoSGetLocalEndpoint, sceVoiceQoSGetRemoteEndpoint](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2437-L2439) (`GetLocalEndpoint(connectionId, int* localId)` and `GetRemoteEndpoint(connectionId, int* remoteId)`). Operations look up the connection by allocated id and return the connected local or remote endpoint id; an unknown connection id or null output pointer returns `ARGUMENT_INVALID` (0x804E0902), and calling before initialization returns `0x804E0901` +- [libSceVoiceQoS](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): initialization arguments, initialization errors, endpoint and connection argument lists, and the no-argument End path are confirmed by the supplied module. The host still tracks endpoints without initializing, starting or stopping libSceVoice or creating its ports. Device-dependent port creation, deletion and connection errors remain unimplemented; connecting an uncreated port throws. End clears host state but does not execute the confirmed voice stop/end sequence or propagate its result. +- [sceVoiceQoSCreateLocalEndpoint, sceVoiceQoSCreateRemoteEndpoint and sceVoiceQoSConnect](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): local ID zero and its single-slot limit, remote/connection slot allocation from zero, seven/fifteen slot capacities, exhaustion outputs, and reuse of released slots are recovered. Remote endpoint deletion clears the first matching connection. Shared encoder ownership, codec parameters and packet accounting are still missing. +- [sceVoiceQoSSetLocalEndpointAttribute and sceVoiceQoSSetRemoteEndpointAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): local selectors 0/2 accept four-byte values and 1/3 one-byte values; remote selector 7 accepts one byte. Unsupported selectors and sizes return the confirmed invalid-argument error. Valid requests throw until voice-port volume/mute forwarding is implemented; they no longer silently store arbitrary bytes. +- [sceVoiceQoSGetLocalEndpoint and sceVoiceQoSGetRemoteEndpoint](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): all in-range connection slots are queryable, including unused slots returning endpoint ID -1. Disconnecting an unused in-range slot succeeds. These state rules are confirmed; the packet, status and attribute getter bodies containing `NotImplemented_nid_no_patch` remain unchanged. From 555fd66a5cf6f15647a7325f68af0c4aba82c0fc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 14:50:18 +0300 Subject: [PATCH 0606/1360] fix(pngdec): restore work memory layout and validation errors --- core/libs/prx/libScePngDec/Export.cpp | 50 +++++++++++++++++---------- core/libs/tests/GuestPngDec.cpp | 24 +++++++------ docs/dev/debt/libScePngDec.md | 6 +++- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index 419c7966f0..8ff5827de0 100644 --- a/core/libs/prx/libScePngDec/Export.cpp +++ b/core/libs/prx/libScePngDec/Export.cpp @@ -28,8 +28,8 @@ constexpr std::int32_t SCE_PNG_DEC_ERROR_DECODE_ERROR = static_cast<std::int32_t constexpr std::uint32_t ATTRIBUTE_NONE = 0; constexpr std::uint32_t ATTRIBUTE_BIT_DEPTH_16 = 1; constexpr std::uint32_t MAX_IMAGE_WIDTH = 1000000; -constexpr std::uint32_t MEMORY_SIZE = 0x20; -constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +constexpr std::uint32_t WORK_MEMORY_OVERHEAD = 53392; +constexpr std::uintptr_t HANDLE_ALIGNMENT = 16; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; @@ -48,14 +48,20 @@ constexpr std::uint32_t MAX_PACKED_DIMENSION = 32767; struct Context { Context* self; + std::uint32_t size; std::uint32_t attribute; std::uint32_t maxImageWidth; + std::uint32_t memorySize; }; -static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); +static_assert(sizeof(Context) == 24); + +std::uint32_t requiredMemory(const PngDecCreateParam& param) { + return (((param.max_image_width + 7u) & ~7u) + 64u) * (8u << param.attribute) + WORK_MEMORY_OVERHEAD; +} std::int32_t validateCreateParam(const PngDecCreateParam* param) { - if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (!param) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (param->this_size != sizeof(PngDecCreateParam)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; if (param->attribute != ATTRIBUTE_NONE && param->attribute != ATTRIBUTE_BIT_DEPTH_16) return SCE_PNG_DEC_ERROR_INVALID_PARAM; if (param->max_image_width - 1 >= MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_PARAM; @@ -102,38 +108,42 @@ std::span<const std::uint8_t> toBytes(const void* data, std::uint32_t size) { return {static_cast<const std::uint8_t*>(data), size}; } -} // namespace +} // namespace extern "C" { -int32_t APS5_VABI scePngDecCreate(const PngDecCreateParam* param, void* memory_address, uint32_t memory_size, void** handle) { +int32_t APS5_VABI scePngDecCreate(const PngDecCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - if (!memory_address || !handle) return SCE_PNG_DEC_ERROR_INVALID_ADDR; - if (memory_size < MEMORY_SIZE) return SCE_PNG_DEC_ERROR_INVALID_WORK_MEMORY; - const auto address = reinterpret_cast<std::uintptr_t>(memory_address); + if (!memoryAddress) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (memorySize < requiredMemory(*param)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (!handle) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + const auto address = reinterpret_cast<std::uintptr_t>(memoryAddress); const std::uintptr_t aligned = (address + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); - auto* context = new (reinterpret_cast<void*>(aligned)) Context{}; + auto* context = new (reinterpret_cast<void*>(aligned)) Context; context->self = context; + context->size = sizeof(PngDecCreateParam); context->attribute = param->attribute; - context->maxImageWidth = param->max_image_width; + context->maxImageWidth = (param->max_image_width + 7u) & ~7u; + context->memorySize = requiredMemory(*param) - 144u; *handle = context; return 0; } -int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, PngDecImageInfo* image_info) { +int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, PngDecImageInfo* imageInfo) { const Context* context = toContext(handle); if (!context) return SCE_PNG_DEC_ERROR_INVALID_HANDLE; - if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (!param) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (!param->png_mem_addr || !param->image_mem_addr || (reinterpret_cast<std::uintptr_t>(param->image_mem_addr) & 3u) != 0) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (param->png_mem_size == 0 || param->image_mem_size == 0) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->pixel_format == 132) throw std::runtime_error("scePngDecDecode: pixel format 132 is not implemented"); if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_PNG_DEC_ERROR_INVALID_PARAM; if (param->alpha_value > 0xFF) return SCE_PNG_DEC_ERROR_INVALID_PARAM; const std::span<const std::uint8_t> png = toBytes(param->png_mem_addr, param->png_mem_size); const std::optional<Decoder::Png::Header> header = Decoder::Png::ParseHeader(png); if (!header) return SCE_PNG_DEC_ERROR_INVALID_DATA; - if (image_info) fillImageInfo(*header, image_info); + if (imageInfo) fillImageInfo(*header, imageInfo); if (header->bitDepth == 16 && context->attribute == ATTRIBUTE_BIT_DEPTH_16) { throw std::runtime_error("scePngDecDecode: 16-bit output is not implemented"); } @@ -179,20 +189,22 @@ int32_t APS5_VABI scePngDecDelete(void* handle) { return 0; } -int32_t APS5_VABI scePngDecParseHeader(const PngDecParseParam* param, PngDecImageInfo* image_info) { - if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (!param->png_mem_addr || !image_info) return SCE_PNG_DEC_ERROR_INVALID_ADDR; +int32_t APS5_VABI scePngDecParseHeader(const PngDecParseParam* param, PngDecImageInfo* imageInfo) { + if (!param) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (!param->png_mem_addr) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (param->png_mem_size == 0) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->reserved0 != 0) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (!imageInfo) return SCE_PNG_DEC_ERROR_INVALID_ADDR; const std::optional<Decoder::Png::Header> header = Decoder::Png::ParseHeader(toBytes(param->png_mem_addr, param->png_mem_size)); if (!header) return SCE_PNG_DEC_ERROR_INVALID_DATA; - fillImageInfo(*header, image_info); + fillImageInfo(*header, imageInfo); return 0; } int32_t APS5_VABI scePngDecQueryMemorySize(const PngDecCreateParam* param) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - return static_cast<std::int32_t>(MEMORY_SIZE); + return static_cast<std::int32_t>(requiredMemory(*param)); } int APS5_VABI scePngDecDecodeWithInputControl() { diff --git a/core/libs/tests/GuestPngDec.cpp b/core/libs/tests/GuestPngDec.cpp index 83281c1c1c..4ee4031ad2 100644 --- a/core/libs/tests/GuestPngDec.cpp +++ b/core/libs/tests/GuestPngDec.cpp @@ -81,8 +81,8 @@ int main() { const PngDecCreateParam param{sizeof(PngDecCreateParam), 0, 1920}; const std::int32_t memorySize = scePngDecQueryMemorySize(¶m); - Require(memorySize > 0); - Require(scePngDecQueryMemorySize(nullptr) == invalidParam); + Require(memorySize == 69264); + Require(scePngDecQueryMemorySize(nullptr) == invalidAddr); const PngDecCreateParam badSize{sizeof(PngDecCreateParam) - 1, 0, 1920}; Require(scePngDecQueryMemorySize(&badSize) == invalidSize); const PngDecCreateParam badAttribute{sizeof(PngDecCreateParam), 2, 1920}; @@ -94,21 +94,21 @@ int main() { const PngDecCreateParam overWidth{sizeof(PngDecCreateParam), 1, 1000001}; Require(scePngDecQueryMemorySize(&overWidth) == invalidParam); const PngDecCreateParam maxWidth{sizeof(PngDecCreateParam), 1, 1000000}; - Require(scePngDecQueryMemorySize(&maxWidth) == memorySize); + Require(scePngDecQueryMemorySize(&maxWidth) == 16054416); std::vector<unsigned char> memory(static_cast<std::size_t>(memorySize) + 1); unsigned char* unaligned = memory.data() + 1; void* handle = nullptr; - Require(scePngDecCreate(nullptr, unaligned, static_cast<std::uint32_t>(memorySize), &handle) == invalidParam); + Require(scePngDecCreate(nullptr, unaligned, static_cast<std::uint32_t>(memorySize), &handle) == invalidAddr); Require(scePngDecCreate(&badSize, unaligned, static_cast<std::uint32_t>(memorySize), &handle) == invalidSize); Require(scePngDecCreate(&overWidth, unaligned, static_cast<std::uint32_t>(memorySize), &handle) == invalidParam); Require(scePngDecCreate(¶m, nullptr, static_cast<std::uint32_t>(memorySize), &handle) == invalidAddr); Require(scePngDecCreate(¶m, unaligned, static_cast<std::uint32_t>(memorySize), nullptr) == invalidAddr); - Require(scePngDecCreate(¶m, unaligned, static_cast<std::uint32_t>(memorySize) - 1, &handle) == invalidWorkMemory); + Require(scePngDecCreate(¶m, unaligned, static_cast<std::uint32_t>(memorySize) - 1, &handle) == invalidSize); Require(handle == nullptr); Require(scePngDecCreate(¶m, unaligned, static_cast<std::uint32_t>(memorySize), &handle) == 0); - Require(reinterpret_cast<std::uintptr_t>(handle) % 8 == 0); - Require(static_cast<unsigned char*>(handle) >= unaligned && static_cast<unsigned char*>(handle) < unaligned + 8); + Require(reinterpret_cast<std::uintptr_t>(handle) % 16 == 0); + Require(static_cast<unsigned char*>(handle) >= unaligned && static_cast<unsigned char*>(handle) < unaligned + 16); const std::vector<std::uint8_t> rgbPng = RgbPng(); PngDecImageInfo info{}; @@ -125,7 +125,7 @@ int main() { } const PngDecParseParam completeTransparency{PALETTE_TRNS, 73, 0}; Require(scePngDecParseHeader(&completeTransparency, &info) == 0 && info.image_flag == 2); - Require(scePngDecParseHeader(nullptr, &info) == invalidParam); + Require(scePngDecParseHeader(nullptr, &info) == invalidAddr); Require(scePngDecParseHeader(&parse, nullptr) == invalidAddr); const PngDecParseParam parseNull{nullptr, 16, 0}; Require(scePngDecParseHeader(&parseNull, &info) == invalidAddr); @@ -213,7 +213,7 @@ int main() { decode = DecodeParam(rgbPng, image); Require(scePngDecDecode(nullptr, &decode, &info) == invalidHandle); - Require(scePngDecDecode(handle, nullptr, &info) == invalidParam); + Require(scePngDecDecode(handle, nullptr, &info) == invalidAddr); decode.png_mem_addr = nullptr; Require(scePngDecDecode(handle, &decode, &info) == invalidAddr); decode = DecodeParam(rgbPng, image); @@ -253,9 +253,11 @@ int main() { Require(scePngDecDecode(handle, &decode, &info) == decodeError); void* deepHandle = nullptr; - std::vector<unsigned char> deepMemory(static_cast<std::size_t>(memorySize)); const PngDecCreateParam deepParam{sizeof(PngDecCreateParam), 1, 1920}; - Require(scePngDecCreate(&deepParam, deepMemory.data(), static_cast<std::uint32_t>(memorySize), &deepHandle) == 0); + const auto deepSize = scePngDecQueryMemorySize(&deepParam); + Require(deepSize == 85136); + std::vector<unsigned char> deepMemory(static_cast<std::size_t>(deepSize)); + Require(scePngDecCreate(&deepParam, deepMemory.data(), static_cast<std::uint32_t>(deepSize), &deepHandle) == 0); decode = DecodeParam(gray16Png, gray16Image); Require(ThrowsRuntimeError([&] { scePngDecDecode(deepHandle, &decode, &info); })); decode = DecodeParam(rgbPng, image); diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md index 2eca53adb5..9449f5a2e9 100644 --- a/docs/dev/debt/libScePngDec.md +++ b/docs/dev/debt/libScePngDec.md @@ -1,6 +1,10 @@ # libScePngDec technical debt +### Functional + +- [libScePngDec](../../../core/libs/prx/libScePngDec/Export.cpp): the supplied module confirms the work-memory size formula, 16-byte handle alignment, the first 24 bytes of its context, and Create/QueryMemorySize validation order. The host now exposes these values but still allocates decoder working storage on the host instead of using the guest work region. +- [scePngDecDecode](../../../core/libs/prx/libScePngDec/Export.cpp): 16-bit output and the original's accepted pixel format 132 remain unsupported and throw. Malformed-image diagnostics and decoder scratch-memory behavior are not fully matched to the supplied module. + ### Unknown function info -- [scePngDecQueryMemorySize, scePngDecCreate](../../../core/libs/prx/libScePngDec/Export.cpp) (libScePngDec) - the work memory is a fixed 0x20 bytes, because decoding uses host memory; the PS5 module asks for `((max_image_width + 0x47) & ~7) * (8 << attribute)` plus a constant, as read from the 3.20 module by prosper. A null parameter returns `0x80690003`; prosper returns `0x80690001` when it cannot read the parameter, which was not measured. Closes when: measured on a PS5 - [scePngDecDecodeWithInputControl](../../../core/libs/prx/libScePngDec/Export.cpp) (libScePngDec) - its name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePngDec.c); the no-parameter prototype follows [shadPS4's pngdec.h](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/libpng/pngdec.h#L76) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim. From 7742fd5831fd05887c52b3e0d7aca020577d8902 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:13:28 +0300 Subject: [PATCH 0607/1360] fix(mbus): restore device registration and control arguments --- core/libs/prx/libSceMbus/PendingExports.cpp | 28 ++++++++++----------- docs/dev/debt/libSceMbus.md | 4 ++- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index deb64a2463..33c86a808b 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -2,7 +2,7 @@ extern "C" { -int APS5_VABI sceMbusAcquireControl() { +int APS5_VABI sceMbusAcquireControl(std::uint64_t control, std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -11,22 +11,22 @@ int APS5_VABI sceMbusAcquireControlList() { return static_cast<int>(0x809B00FF); } -int APS5_VABI sceMbusAcquireControlWithState() { +int APS5_VABI sceMbusAcquireControlWithState(std::uint32_t deviceClass, std::int32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusAcquireControlWithState2() { +int APS5_VABI sceMbusAcquireControlWithState2(std::uint32_t deviceClass, std::int32_t moduleId, std::int32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusAcquireControlWithStateFlag() { +int APS5_VABI sceMbusAcquireControlWithStateFlag(std::uint32_t deviceClass, std::uint32_t moduleId, std::uint32_t state, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusAddHandle() { +int APS5_VABI sceMbusAddHandle(std::uint32_t deviceClass, std::int32_t handle, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -36,32 +36,32 @@ int APS5_VABI sceMbusAddHandleByDeviceId(std::uint32_t deviceClass, std::int32_t return 0; } -int APS5_VABI sceMbusAddHandleByUserId() { +int APS5_VABI sceMbusAddHandleByUserId(std::uint32_t deviceClass, std::int32_t handle, std::int32_t userId, std::int32_t first, std::int32_t second, std::int32_t third) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusAddHandleByVidPid() { +int APS5_VABI sceMbusAddHandleByVidPid(std::uint32_t deviceClass, std::int32_t handle, std::int32_t first, std::uint16_t vendorId, std::uint16_t productId, std::int32_t second, std::int32_t third) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusAddHandleWithModuleId() { +int APS5_VABI sceMbusAddHandleWithModuleId(std::uint32_t deviceClass, std::int32_t handle, std::uint32_t flags, std::uint32_t moduleId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusAttachToApplicationByPid() { +int APS5_VABI sceMbusAttachToApplicationByPid(std::int32_t processId, std::int32_t deviceClass, std::int32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusBindDeviceWithUserId() { +int APS5_VABI sceMbusBindDeviceWithUserId(std::uint64_t deviceId, std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusChangeAppModuleState() { +int APS5_VABI sceMbusChangeAppModuleState(std::uint32_t deviceClass, std::int32_t moduleId, std::uint32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -71,17 +71,17 @@ int APS5_VABI sceMbusCheckDriverCallback() { return 0; } -int APS5_VABI sceMbusConvertToLocalDeviceId() { +int APS5_VABI sceMbusConvertToLocalDeviceId(std::uint64_t deviceId, std::uint32_t* localId, std::uint32_t* deviceClass) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusConvertToLocalDeviceId2() { +int APS5_VABI sceMbusConvertToLocalDeviceId2(std::uint64_t deviceId, std::uint32_t deviceClass, std::uint64_t* localId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusConvertToMbusDeviceId() { +int APS5_VABI sceMbusConvertToMbusDeviceId(std::uint64_t localId, std::uint32_t deviceClass, std::uint64_t* deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index c19904c941..335491f9c7 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,6 +8,8 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 113 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 99 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. + +- [sceMbus control and handle registration](../../../core/libs/prx/libSceMbus/PendingExports.cpp): argument positions and transmitted widths are recovered for control acquisition, handle registration, process attachment, user binding and device-ID conversion. Several scalar field meanings still depend on the service protocol; the 64-bit control value and unnamed registration fields must not be interpreted as pointers. AcquireControlList returns a constant error without reading its incoming arguments, so its source signature remains unresolved. From f5e1929f8b8657ecf7f0c8187aa2edec9b93f0b7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:14:33 +0300 Subject: [PATCH 0608/1360] fix(mbus): restore debug process and device query signatures --- core/libs/prx/libSceMbus/PendingExports.cpp | 54 ++++++++++++--------- docs/dev/debt/libSceMbus.md | 4 +- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index 33c86a808b..9822cc0453 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -1,5 +1,13 @@ #include "prx/libc/include/General.hpp" +struct MbusProcessInfo; +struct MbusStartupInfo; +struct MbusStartupParam; +struct MbusControlStatus; +struct MbusDebugDeviceInfo; +struct MbusInternalInfo; +struct MbusPriorityInfo; + extern "C" { int APS5_VABI sceMbusAcquireControl(std::uint64_t control, std::int32_t first, std::int32_t second) { @@ -86,117 +94,117 @@ int APS5_VABI sceMbusConvertToMbusDeviceId(std::uint64_t localId, std::uint32_t return 0; } -int APS5_VABI sceMbusDebugAcquireControl() { +int APS5_VABI sceMbusDebugAcquireControl(std::uint64_t control, std::int32_t first, std::int32_t second, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugAcquireControlWithState() { +int APS5_VABI sceMbusDebugAcquireControlWithState(std::uint32_t deviceClass, std::int32_t state, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugAcquireControlWithState2() { +int APS5_VABI sceMbusDebugAcquireControlWithState2(std::uint32_t deviceClass, std::int32_t moduleId, std::int32_t state, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugAcquireControlWithStateFlag() { +int APS5_VABI sceMbusDebugAcquireControlWithStateFlag(std::uint32_t deviceClass, std::uint32_t moduleId, std::uint32_t state, std::uint32_t flags, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugAddProcess() { +int APS5_VABI sceMbusDebugAddProcess(std::int32_t processId, std::int32_t flags, const MbusProcessInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugCheckProcessResume() { +int APS5_VABI sceMbusDebugCheckProcessResume(MbusProcessInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugDisableBgmForShellUi() { +int APS5_VABI sceMbusDebugDisableBgmForShellUi(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugGetApplicationStartupInfo() { +int APS5_VABI sceMbusDebugGetApplicationStartupInfo(std::uint32_t appId, const MbusProcessInfo* info, MbusStartupInfo* startupInfo) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugGetControlStatus() { +int APS5_VABI sceMbusDebugGetControlStatus(std::int32_t deviceClass, std::int32_t processId, MbusControlStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugGetDeviceInfo() { +int APS5_VABI sceMbusDebugGetDeviceInfo(std::uint64_t deviceClass, MbusDebugDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugGetInternalInfo() { +int APS5_VABI sceMbusDebugGetInternalInfo(MbusInternalInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugGetPriorityInfo() { +int APS5_VABI sceMbusDebugGetPriorityInfo(std::uint32_t deviceClass, MbusPriorityInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugReenableBgmForShellUi() { +int APS5_VABI sceMbusDebugReenableBgmForShellUi(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugReleaseControl() { +int APS5_VABI sceMbusDebugReleaseControl(std::uint64_t control, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugRemoveCameraAppModuleFocus() { +int APS5_VABI sceMbusDebugRemoveCameraAppModuleFocus(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugResumeApplication() { +int APS5_VABI sceMbusDebugResumeApplication(std::int32_t appId, std::uint8_t firstFlag, std::uint8_t secondFlag, MbusProcessInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugSetApplicationFocusByAppId() { +int APS5_VABI sceMbusDebugSetApplicationFocusByAppId(std::uint32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugSetCameraAppModuleFocus() { +int APS5_VABI sceMbusDebugSetCameraAppModuleFocus(std::int32_t processId, std::int32_t moduleId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugSetControllerFocusByAppId() { +int APS5_VABI sceMbusDebugSetControllerFocusByAppId(std::uint32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugSetOtherProcessExcludedAction() { +int APS5_VABI sceMbusDebugSetOtherProcessExcludedAction(std::uint64_t control, std::uint32_t action, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugSetPriority() { +int APS5_VABI sceMbusDebugSetPriority(std::uint64_t control, std::uint32_t priority, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugStartApplication() { +int APS5_VABI sceMbusDebugStartApplication(std::int32_t appId, std::int32_t first, std::int32_t second, std::uint8_t firstFlag, std::uint8_t secondFlag, const MbusStartupParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugStartApplication2() { +int APS5_VABI sceMbusDebugStartApplication2(std::int32_t appId, std::int32_t first, std::int32_t second, std::uint8_t firstFlag, std::uint8_t secondFlag, const MbusStartupParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 335491f9c7..457bd494bb 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,8 +8,10 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 99 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 76 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. - [sceMbus control and handle registration](../../../core/libs/prx/libSceMbus/PendingExports.cpp): argument positions and transmitted widths are recovered for control acquisition, handle registration, process attachment, user binding and device-ID conversion. Several scalar field meanings still depend on the service protocol; the 64-bit control value and unnamed registration fields must not be interpreted as pointers. AcquireControlList returns a constant error without reading its incoming arguments, so its source signature remains unresolved. + +- [sceMbus debug process and device queries](../../../core/libs/prx/libSceMbus/PendingExports.cpp): process information is transmitted as 16 bytes, startup input/output as eight bytes, control status as 24 bytes, internal information as 2832 bytes and priority information as 3088 bytes. Device enumeration writes a 32-bit count followed by 396-byte entries. These pointee fields and the semantics of unnamed process flags remain unresolved; declarations use distinct incomplete types instead of guessing their fields. From e6115d30f2f0fca61a7aaaefd2193317fd2cd8a6 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:15:42 +0300 Subject: [PATCH 0609/1360] fix(mbus): restore event queue and diagnostic arguments --- core/libs/prx/libSceMbus/PendingExports.cpp | 34 +++++++++++---------- docs/dev/debt/libSceMbus.md | 4 ++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index 9822cc0453..c1edb5c31a 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/General.hpp" +struct MbusEvent; +struct MbusEventCallbacks; struct MbusProcessInfo; struct MbusStartupInfo; struct MbusStartupParam; @@ -209,27 +211,27 @@ int APS5_VABI sceMbusDebugStartApplication2(std::int32_t appId, std::int32_t fir return 0; } -int APS5_VABI sceMbusDebugStartApplicationNull() { +int APS5_VABI sceMbusDebugStartApplicationNull(std::int32_t appId, std::int32_t first, std::int32_t second, std::uint8_t firstFlag, std::uint8_t secondFlag) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugSuspendApplication() { +int APS5_VABI sceMbusDebugSuspendApplication(std::uint32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugTerminateApplication() { +int APS5_VABI sceMbusDebugTerminateApplication(std::uint32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDebugTerminateProcess() { +int APS5_VABI sceMbusDebugTerminateProcess(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDetachFromApplicationByPid() { +int APS5_VABI sceMbusDetachFromApplicationByPid(std::int32_t processId, std::int32_t deviceClass) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -239,12 +241,12 @@ int APS5_VABI sceMbusDisableBgmForShellUi() { return 0; } -int APS5_VABI sceMbusDisableChat() { +int APS5_VABI sceMbusDisableChat(std::uint32_t deviceClass) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDisconnectDevice() { +int APS5_VABI sceMbusDisconnectDevice(std::uint64_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -259,17 +261,17 @@ int APS5_VABI sceMbusDumpDeviceInfo() { return 0; } -int APS5_VABI sceMbusDumpHandleStatusInfo() { +int APS5_VABI sceMbusDumpHandleStatusInfo(std::uint64_t control, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDumpHandleStatusInfoAll() { +int APS5_VABI sceMbusDumpHandleStatusInfoAll(std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusDumpModuleStateInfo() { +int APS5_VABI sceMbusDumpModuleStateInfo(std::uint64_t control) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -289,7 +291,7 @@ int APS5_VABI sceMbusDumpPortHandleInfo() { return 0; } -int APS5_VABI sceMbusDumpProcMediaInfo() { +int APS5_VABI sceMbusDumpProcMediaInfo(std::uint64_t control) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -304,17 +306,17 @@ int APS5_VABI sceMbusDumpUserMediaInfo() { return 0; } -int APS5_VABI sceMbusEnableChat() { +int APS5_VABI sceMbusEnableChat(std::uint32_t deviceClass) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusEnterAudioOutAutoAttenuation() { +int APS5_VABI sceMbusEnterAudioOutAutoAttenuation(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusEventBusStatusChangeSubscribe() { +int APS5_VABI sceMbusEventBusStatusChangeSubscribe(std::int32_t eventHandle, std::uint32_t first, std::uint32_t second, std::uint64_t firstValue, std::uint64_t secondValue) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -324,12 +326,12 @@ int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe() { return 0; } -int APS5_VABI sceMbusEventCallbackFuncsInit_() { +int APS5_VABI sceMbusEventCallbackFuncsInit_(MbusEventCallbacks* callbacks, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusEventCreate_() { +int APS5_VABI sceMbusEventCreate_(std::uint32_t flags, MbusEvent* buffer, std::uint32_t capacity, const MbusEventCallbacks* callbacks, void* userData) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 457bd494bb..5286392e2b 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,10 +8,12 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 76 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 52 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. - [sceMbus control and handle registration](../../../core/libs/prx/libSceMbus/PendingExports.cpp): argument positions and transmitted widths are recovered for control acquisition, handle registration, process attachment, user binding and device-ID conversion. Several scalar field meanings still depend on the service protocol; the 64-bit control value and unnamed registration fields must not be interpreted as pointers. AcquireControlList returns a constant error without reading its incoming arguments, so its source signature remains unresolved. - [sceMbus debug process and device queries](../../../core/libs/prx/libSceMbus/PendingExports.cpp): process information is transmitted as 16 bytes, startup input/output as eight bytes, control status as 24 bytes, internal information as 2832 bytes and priority information as 3088 bytes. Device enumeration writes a 32-bit count followed by 396-byte entries. These pointee fields and the semantics of unnamed process flags remain unresolved; declarations use distinct incomplete types instead of guessing their fields. + +- [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; callback signatures and event fields still require the dispatch path. EventCallbackFuncsInit only clears memory and has no defined return value; its protected stub retains the existing integer return. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. From 952b8ab0382fcb26ef599d64bf6add18e1e35c76 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:16:52 +0300 Subject: [PATCH 0610/1360] fix(mbus): restore device query and driver event signatures --- core/libs/prx/libSceMbus/PendingExports.cpp | 43 ++++++++++++--------- docs/dev/debt/libSceMbus.md | 4 +- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index c1edb5c31a..b78144821c 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -1,5 +1,10 @@ #include "prx/libc/include/General.hpp" +struct MbusDeviceDescription; +struct MbusDeviceInfo; +struct MbusSimulatedHandleStatus; +struct MbusSparkState; +struct MbusDriverCallbacks; struct MbusEvent; struct MbusEventCallbacks; struct MbusProcessInfo; @@ -336,27 +341,27 @@ int APS5_VABI sceMbusEventCreate_(std::uint32_t flags, MbusEvent* buffer, std::u return 0; } -int APS5_VABI sceMbusEventDestroy() { +int APS5_VABI sceMbusEventDestroy(std::int32_t eventHandle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusEventReceive() { +int APS5_VABI sceMbusEventReceive(std::int32_t eventHandle, MbusEvent* events, std::int32_t capacity, std::int32_t wait) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetControlStatus() { +int APS5_VABI sceMbusGetControlStatus(std::uint64_t control, MbusControlStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetDeviceDescription() { +int APS5_VABI sceMbusGetDeviceDescription(std::uint64_t deviceId, std::uint32_t selector, MbusDeviceDescription* description) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetDeviceInfoByBusId_() { +int APS5_VABI sceMbusGetDeviceInfoByBusId_(std::int32_t busId, MbusDeviceInfo* info, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -371,27 +376,27 @@ int APS5_VABI sceMbusGetDeviceInfoByCondition_() { return 0; } -int APS5_VABI sceMbusGetDeviceInfo_() { +int APS5_VABI sceMbusGetDeviceInfo_(std::uint64_t deviceId, MbusDeviceInfo* info, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetSimulatedBusUsableStatusByBusType() { +int APS5_VABI sceMbusGetSimulatedBusUsableStatusByBusType(std::uint32_t busType, std::int32_t first, std::uint64_t deviceId, std::uint32_t second, std::uint32_t third) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetSimulatedHandleStatusInfo() { +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo(std::int32_t deviceClass, std::int32_t first, std::int32_t second, MbusSimulatedHandleStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetSimulatedHandleStatusInfo2() { +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo2(std::uint32_t deviceClass, std::uint32_t first, std::uint32_t second, std::uint32_t third, MbusSimulatedHandleStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusGetSparkState() { +int APS5_VABI sceMbusGetSparkState(std::uint64_t deviceId, MbusSparkState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -406,22 +411,22 @@ int APS5_VABI sceMbusIsBgmProhibited() { return 0; } -int APS5_VABI sceMbusIsUsingDevice() { +int APS5_VABI sceMbusIsUsingDevice(std::int32_t deviceClass, std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusLeaveAudioOutAutoAttenuation() { +int APS5_VABI sceMbusLeaveAudioOutAutoAttenuation(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusNotifyBtDisconnection() { +int APS5_VABI sceMbusNotifyBtDisconnection(std::uint64_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusNotifyDriverEvent() { +int APS5_VABI sceMbusNotifyDriverEvent(std::int32_t driverId, std::int32_t eventId, std::uint64_t firstValue, std::int32_t flags, std::uint64_t secondValue) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -431,12 +436,12 @@ int APS5_VABI sceMbusReenableBgmForShellUi() { return 0; } -int APS5_VABI sceMbusRegisterDriver() { +int APS5_VABI sceMbusRegisterDriver(std::int32_t driverId, const MbusDriverCallbacks* callbacks, void* userData) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusReleaseControl() { +int APS5_VABI sceMbusReleaseControl(std::uint64_t control) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -446,17 +451,17 @@ int APS5_VABI sceMbusRemoveCameraAppModuleFocus() { return 0; } -int APS5_VABI sceMbusRemoveHandle() { +int APS5_VABI sceMbusRemoveHandle(std::int32_t deviceClass, std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusRemoveProxy() { +int APS5_VABI sceMbusRemoveProxy(std::int32_t processId, std::uint32_t deviceClass, std::uint64_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusResolveByDeviceId() { +int APS5_VABI sceMbusResolveByDeviceId(std::int32_t deviceClass, std::uint64_t deviceId, std::uint32_t first, std::uint32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 5286392e2b..e4313ab744 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,7 +8,7 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 52 remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. @@ -17,3 +17,5 @@ - [sceMbus debug process and device queries](../../../core/libs/prx/libSceMbus/PendingExports.cpp): process information is transmitted as 16 bytes, startup input/output as eight bytes, control status as 24 bytes, internal information as 2832 bytes and priority information as 3088 bytes. Device enumeration writes a 32-bit count followed by 396-byte entries. These pointee fields and the semantics of unnamed process flags remain unresolved; declarations use distinct incomplete types instead of guessing their fields. - [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; callback signatures and event fields still require the dispatch path. EventCallbackFuncsInit only clears memory and has no defined return value; its protected stub retains the existing integer return. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. + +- [sceMbus device and driver payloads](../../../core/libs/prx/libSceMbus/PendingExports.cpp): device-info records occupy 256 bytes, descriptions 264 bytes, simulated-handle status and Spark state 16 bytes each. Their service-side fields remain unresolved. Driver registration copies four callback pointers; driver-event notification transports two 64-bit values without dereferencing them. Init, IsBgmProhibited, ReenableBgmForShellUi and RemoveCameraAppModuleFocus consume no incoming arguments. From ab4a949f07f678d96d4d12d24c86b3a5a5ff8d00 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:18:05 +0300 Subject: [PATCH 0611/1360] fix(mbus): restore routing and user setting arguments --- core/libs/prx/libSceMbus/PendingExports.cpp | 39 +++++++++++---------- docs/dev/debt/libSceMbus.md | 4 ++- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index b78144821c..b69e0f5add 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -5,6 +5,7 @@ struct MbusDeviceInfo; struct MbusSimulatedHandleStatus; struct MbusSparkState; struct MbusDriverCallbacks; +struct MbusUsbPowerEntry; struct MbusEvent; struct MbusEventCallbacks; struct MbusProcessInfo; @@ -466,27 +467,27 @@ int APS5_VABI sceMbusResolveByDeviceId(std::int32_t deviceClass, std::uint64_t d return 0; } -int APS5_VABI sceMbusResolveByHandle() { +int APS5_VABI sceMbusResolveByHandle(std::int32_t deviceClass, std::int32_t handle, std::int32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusResolveByPlayerId() { +int APS5_VABI sceMbusResolveByPlayerId(std::uint32_t deviceClass, std::uint32_t playerId, std::uint32_t first, std::uint32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusResolveByUserId() { +int APS5_VABI sceMbusResolveByUserId(std::uint32_t deviceClass, std::int32_t userId, std::uint32_t first, std::uint32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetAutoLoginMode() { +int APS5_VABI sceMbusSetAutoLoginMode(std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetCameraAppModuleFocus() { +int APS5_VABI sceMbusSetCameraAppModuleFocus(std::int32_t moduleId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -496,37 +497,37 @@ int APS5_VABI sceMbusSetDeviceFunctionState() { return 0; } -int APS5_VABI sceMbusSetDuckingSetting() { +int APS5_VABI sceMbusSetDuckingSetting(std::int32_t setting) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetProxy() { +int APS5_VABI sceMbusSetProxy(std::int32_t processId, std::int32_t first, std::int32_t second, std::int32_t third, std::uint64_t deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetRoutingMode() { +int APS5_VABI sceMbusSetRoutingMode(std::int32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetScratchData() { +int APS5_VABI sceMbusSetScratchData(std::uint64_t deviceId, std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetScratchDataUpdatedEventMask() { +int APS5_VABI sceMbusSetScratchDataUpdatedEventMask(std::uint32_t mask) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetSocialScreenAudioMode() { +int APS5_VABI sceMbusSetSocialScreenAudioMode(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetTestFlag() { +int APS5_VABI sceMbusSetTestFlag(std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -536,17 +537,17 @@ int APS5_VABI sceMbusSetThreadParam(std::int32_t priority, std::uint64_t affinit return 0; } -int APS5_VABI sceMbusSetUsbPowerBlackList() { +int APS5_VABI sceMbusSetUsbPowerBlackList(std::int32_t first, std::uint8_t count, const MbusUsbPowerEntry* entries) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetUserMuteStatus() { +int APS5_VABI sceMbusSetUserMuteStatus(std::int32_t userId, std::int32_t status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusSetVolumeDown() { +int APS5_VABI sceMbusSetVolumeDown(std::uint32_t deviceClass, std::uint8_t volume) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -556,7 +557,7 @@ int APS5_VABI sceMbusStartAudioOutAuxBroadcast() { return 0; } -int APS5_VABI sceMbusStartAudioOutSharePlay() { +int APS5_VABI sceMbusStartAudioOutSharePlay(std::int32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -575,17 +576,17 @@ int APS5_VABI sceMbusTerm() { return 0; } -int APS5_VABI sceMbusUnregisterDriver() { +int APS5_VABI sceMbusUnregisterDriver(std::int32_t driverId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusUserLogin() { +int APS5_VABI sceMbusUserLogin(std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMbusUserLogout() { +int APS5_VABI sceMbusUserLogout(std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index e4313ab744..3329a8071c 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,7 +8,7 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - remaining symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): GetDeviceInfoByConditionForDeviceService, GetDeviceInfoByCondition_ and SetDeviceFunctionState still require by-value aggregate layouts. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. @@ -19,3 +19,5 @@ - [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; callback signatures and event fields still require the dispatch path. EventCallbackFuncsInit only clears memory and has no defined return value; its protected stub retains the existing integer return. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. - [sceMbus device and driver payloads](../../../core/libs/prx/libSceMbus/PendingExports.cpp): device-info records occupy 256 bytes, descriptions 264 bytes, simulated-handle status and Spark state 16 bytes each. Their service-side fields remain unresolved. Driver registration copies four callback pointers; driver-event notification transports two 64-bit values without dereferencing them. Init, IsBgmProhibited, ReenableBgmForShellUi and RemoveCameraAppModuleFocus consume no incoming arguments. + +- [sceMbus settings](../../../core/libs/prx/libSceMbus/PendingExports.cpp): USB power-list entries occupy 12 bytes, with an 8-bit count limited to 100; their fields remain unresolved. Several routing, scratch-data and proxy scalar meanings still require the service handler. Start/StopAudioOutAuxBroadcast and StopAudioOutSharePlay have confirmed zero-argument ABIs. From 0d4c2591a8dfaf607f682baf35908459a342e862 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:19:13 +0300 Subject: [PATCH 0612/1360] fix(mbus): restore event callbacks and condition query arguments --- core/libs/prx/libSceMbus/PendingExports.cpp | 10 ++++++++-- docs/dev/debt/libSceMbus.md | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index b69e0f5add..d0596729f0 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -7,7 +7,13 @@ struct MbusSparkState; struct MbusDriverCallbacks; struct MbusUsbPowerEntry; struct MbusEvent; -struct MbusEventCallbacks; +struct MbusDeviceCondition; +struct MbusEventCallbacks { + void (APS5_VABI *onOverflow)(std::int32_t eventHandle, void* userData); + void (APS5_VABI *onEvent)(std::int32_t eventHandle, void* userData, const MbusEvent* event); +}; + +static_assert(sizeof(MbusEventCallbacks) == 16); struct MbusProcessInfo; struct MbusStartupInfo; struct MbusStartupParam; @@ -372,7 +378,7 @@ int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService() { return 0; } -int APS5_VABI sceMbusGetDeviceInfoByCondition_() { +int APS5_VABI sceMbusGetDeviceInfoByCondition_(const MbusDeviceCondition* condition, std::int32_t flags, MbusDeviceInfo* infos, std::int32_t capacity, std::int32_t* first, std::int32_t* second, std::uint32_t infoSize) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 3329a8071c..a892a6c785 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,7 +8,7 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): GetDeviceInfoByConditionForDeviceService, GetDeviceInfoByCondition_ and SetDeviceFunctionState still require by-value aggregate layouts. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. +- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): GetDeviceInfoByConditionForDeviceService and SetDeviceFunctionState still require by-value aggregate layouts. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. @@ -16,8 +16,10 @@ - [sceMbus debug process and device queries](../../../core/libs/prx/libSceMbus/PendingExports.cpp): process information is transmitted as 16 bytes, startup input/output as eight bytes, control status as 24 bytes, internal information as 2832 bytes and priority information as 3088 bytes. Device enumeration writes a 32-bit count followed by 396-byte entries. These pointee fields and the semantics of unnamed process flags remain unresolved; declarations use distinct incomplete types instead of guessing their fields. -- [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; callback signatures and event fields still require the dispatch path. EventCallbackFuncsInit only clears memory and has no defined return value; its protected stub retains the existing integer return. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. +- [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; event fields still require the dispatch path; the overflow callback takes an event handle and user data, and the event callback additionally takes a pointer to the queued event. EventCallbackFuncsInit only clears memory and has no defined return value; its protected stub retains the existing integer return. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. - [sceMbus device and driver payloads](../../../core/libs/prx/libSceMbus/PendingExports.cpp): device-info records occupy 256 bytes, descriptions 264 bytes, simulated-handle status and Spark state 16 bytes each. Their service-side fields remain unresolved. Driver registration copies four callback pointers; driver-event notification transports two 64-bit values without dereferencing them. Init, IsBgmProhibited, ReenableBgmForShellUi and RemoveCameraAppModuleFocus consume no incoming arguments. - [sceMbus settings](../../../core/libs/prx/libSceMbus/PendingExports.cpp): USB power-list entries occupy 12 bytes, with an 8-bit count limited to 100; their fields remain unresolved. Several routing, scratch-data and proxy scalar meanings still require the service handler. Start/StopAudioOutAuxBroadcast and StopAudioOutSharePlay have confirmed zero-argument ABIs. + +- [sceMbusGetDeviceInfoByCondition_](../../../core/libs/prx/libSceMbus/PendingExports.cpp): its first parameter points to a 256-byte query condition; the second is a 32-bit scalar. Two optional 32-bit outputs and the sized 256-byte record array are confirmed, while condition fields and output meanings remain unresolved. From 5277d5b3385ca3c42bd02de44ec2bd4ce2047855 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:20:03 +0300 Subject: [PATCH 0613/1360] fix(mbus): validate device queries against the recovered abi --- core/libs/prx/libSceMbus/Export.cpp | 27 +++++++++++++++++++++------ core/libs/tests/GuestMbus.cpp | 24 ++++++++++++++++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceMbus/Export.cpp b/core/libs/prx/libSceMbus/Export.cpp index 6d67b46184..ca49b545be 100644 --- a/core/libs/prx/libSceMbus/Export.cpp +++ b/core/libs/prx/libSceMbus/Export.cpp @@ -53,14 +53,29 @@ std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t clear) { } std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, const DeviceServiceUsageFilter* filter, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint32_t infoSize) { - (void)deviceClass; - (void)filter; (void)arg2; - (void)infos; (void)reserved; - (void)infoSize; - if (count == nullptr || maxInfos < 0) throw std::invalid_argument(std::string(__func__) + ": invalid argument"); - *count = 0; + constexpr auto invalidArgument = static_cast<std::int32_t>(0x809B0080u); + switch (deviceClass) { + case 0x1001: + case 0x2001: + case 0x3001: + case 0x4001: + case 0x4002: + case 0x5001: + case 0x6001: + case 0x7001: + case 0x9001: + if (filter != nullptr) return invalidArgument; + break; + case 0x8001: + if (filter == nullptr || filter->minimumUsage > filter->maximumUsage) return invalidArgument; + break; + default: + return invalidArgument; + } + if (infoSize != 112 || maxInfos < 0 || (maxInfos != 0 && infos == nullptr)) return invalidArgument; + if (count != nullptr) *count = 0; return 0; } diff --git a/core/libs/tests/GuestMbus.cpp b/core/libs/tests/GuestMbus.cpp index d1c7c85258..e867068157 100644 --- a/core/libs/tests/GuestMbus.cpp +++ b/core/libs/tests/GuestMbus.cpp @@ -12,7 +12,11 @@ struct DeviceServiceInitParam { std::uint32_t reserved; const DeviceServiceThreadParam* threadParam; }; -struct DeviceServiceUsageFilter; +struct DeviceServiceUsageFilter { + std::uint16_t usagePage; + std::uint16_t minimumUsage; + std::uint16_t maximumUsage; +}; extern "C" { std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, const DeviceServiceInitParam* param); @@ -49,13 +53,17 @@ int main() { std::int32_t count = 7; std::int32_t reserved = 0; Require(sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, info.data(), 1, &count, &reserved, info.size()) == 0 && count == 0, "a device was listed"); - bool threw = false; - try { - sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, info.data(), 1, nullptr, &reserved, info.size()); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw, "a null count was accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, info.data(), 1, nullptr, &reserved, info.size()) == 0, "optional count output rejected"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, nullptr, 0, nullptr, nullptr, info.size()) == 0, "empty query rejected"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, nullptr, 1, &count, nullptr, info.size()) == invalidArgument, "null record buffer accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, info.data(), -1, &count, nullptr, info.size()) == invalidArgument, "negative capacity accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, info.data(), 1, &count, nullptr, info.size() - 1) == invalidArgument, "incorrect record size accepted"); + DeviceServiceUsageFilter filter{1, 2, 3}; + Require(sceDeviceServiceQueryDeviceInfo_(0x8001, &filter, 0, info.data(), 1, &count, nullptr, info.size()) == 0, "valid HID filter rejected"); + Require(sceDeviceServiceQueryDeviceInfo_(0x8001, nullptr, 0, info.data(), 1, &count, nullptr, info.size()) == invalidArgument, "missing HID filter accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, &filter, 0, info.data(), 1, &count, nullptr, info.size()) == invalidArgument, "unexpected HID filter accepted"); + filter.minimumUsage = 4; + Require(sceDeviceServiceQueryDeviceInfo_(0x8001, &filter, 0, info.data(), 1, &count, nullptr, info.size()) == invalidArgument, "reversed HID usage range accepted"); Require(sceDeviceServiceGetGeneration() == 0, "the generation advanced without a device change"); Require(sceDeviceServiceTerminate() == 0, "termination failed"); std::puts("DeviceService tests passed"); From ef071a8a633721b1bb917b2838b0dad8d2632a43 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:22:40 +0300 Subject: [PATCH 0614/1360] fix(systemservice): restore app messaging signatures --- .../prx/libSceSystemService/PendingExports.cpp | 18 ++++++++++-------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 9ac7176e02..c0b6fe5877 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,44 +7,46 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } +struct AppMessagingMessage; + extern "C" { -int APS5_VABI sceAppMessagingClearEventFlag() { +int APS5_VABI sceAppMessagingClearEventFlag(std::int32_t appId, std::uint64_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingReceiveMsg() { +int APS5_VABI sceAppMessagingReceiveMsg(AppMessagingMessage* message) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingSendMsg() { +int APS5_VABI sceAppMessagingSendMsg(std::uint32_t appId, std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingSendMsgToShellCore() { +int APS5_VABI sceAppMessagingSendMsgToShellCore(std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingSendMsgToShellUI() { +int APS5_VABI sceAppMessagingSendMsgToShellUI(std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingSetEventFlag() { +int APS5_VABI sceAppMessagingSetEventFlag(std::int32_t appId, std::uint64_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingTryGetEventFlag() { +int APS5_VABI sceAppMessagingTryGetEventFlag(std::uint64_t mask, std::uint64_t* flags, std::uint8_t clear) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppMessagingTryReceiveMsg() { +int APS5_VABI sceAppMessagingTryReceiveMsg(AppMessagingMessage* message) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 2e396200ca..ee2fc16c5e 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -105,3 +105,5 @@ - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. - [sceLncUtilKillApp, sceLncUtilKillAppWithReason](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the running title's app ID ends the title as `sceSystemServiceKillApp` does, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L219-L227) describes as a wrapper of `sceLncUtilKillApp`; the signatures follow its [launcher utility bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L262-L279). Another app ID throws, and so does a reason other than 0, whose meaning and effect on the console are not documented - [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2, and on macOS with `posix_spawn`, which closes them through `POSIX_SPAWN_CLOEXEC_DEFAULT`; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known + +- [AppMessaging receive buffers](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the supplied module confirms the eight messaging argument lists, 64-bit event masks, byte-sized clear flag and 8192-byte send limit. Receive writes an 8216-byte message whose field meanings still require the sender/service handler. The protected bodies remain unchanged. From 1c2ed0446c5095c39d59ecd8077b15bb506f4dcc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:23:57 +0300 Subject: [PATCH 0615/1360] fix(systemservice): restore launcher process and status signatures --- .../libSceSystemService/PendingExports.cpp | 26 +++++++++++-------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index c0b6fe5877..132a392cef 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,6 +7,10 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } + +struct LncLocalProcessParam; +struct LncAppStatus; +struct LncShellUiAppStatus; struct AppMessagingMessage; extern "C" { @@ -71,12 +75,12 @@ int APS5_VABI sceLncUtilActivateCdlg() { return 0; } -int APS5_VABI sceLncUtilAddLocalProcess() { +int APS5_VABI sceLncUtilAddLocalProcess(std::int32_t appId, const char* path, const char* const* argv, const LncLocalProcessParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilBlockAppSuspend() { +int APS5_VABI sceLncUtilBlockAppSuspend(const char* reason) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -121,22 +125,22 @@ int APS5_VABI sceLncUtilFinishSpecialResume() { return 0; } -int APS5_VABI sceLncUtilForceKillApp() { +int APS5_VABI sceLncUtilForceKillApp(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilForceKillLocalProcess() { +int APS5_VABI sceLncUtilForceKillLocalProcess(std::int32_t appId, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetAppCategory() { +int APS5_VABI sceLncUtilGetAppCategory(std::int32_t appId, std::uint32_t* category) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetAppCategoryType() { +int APS5_VABI sceLncUtilGetAppCategoryType(std::int32_t appId, std::uint32_t* categoryType) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -151,7 +155,7 @@ int APS5_VABI sceLncUtilGetAppId() { return 0; } -int APS5_VABI sceLncUtilGetAppIdListOfBigApp() { +int APS5_VABI sceLncUtilGetAppIdListOfBigApp(std::int32_t* appIds, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -161,7 +165,7 @@ int APS5_VABI sceLncUtilGetAppIdOfRunningBigApp() { return 0; } -int APS5_VABI sceLncUtilGetAppLaunchedUser() { +int APS5_VABI sceLncUtilGetAppLaunchedUser(std::int32_t appId, std::int32_t* userId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -171,17 +175,17 @@ int APS5_VABI sceLncUtilGetAppStatus() { return 0; } -int APS5_VABI sceLncUtilGetAppStatusList() { +int APS5_VABI sceLncUtilGetAppStatusList(LncAppStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetAppStatusListForShellUIReboot() { +int APS5_VABI sceLncUtilGetAppStatusListForShellUIReboot(LncShellUiAppStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetAppTitleId() { +int APS5_VABI sceLncUtilGetAppTitleId(std::int32_t appId, char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index ee2fc16c5e..5be32ae15e 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -107,3 +107,5 @@ - [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2, and on macOS with `posix_spawn`, which closes them through `POSIX_SPAWN_CLOEXEC_DEFAULT`; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known - [AppMessaging receive buffers](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the supplied module confirms the eight messaging argument lists, 64-bit event masks, byte-sized clear flag and 8192-byte send limit. Receive writes an 8216-byte message whose field meanings still require the sender/service handler. The protected bodies remain unchanged. + +- [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block; application-status entries occupy 64 bytes and shell-UI reboot entries 1336 bytes. Their fields remain unresolved. Title-ID output occupies 16 bytes; category, category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. From 85b972ee406b34158da4ff0b06075ca3856e6eb7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:25:16 +0300 Subject: [PATCH 0616/1360] fix(systemservice): restore launcher query output types --- .../libSceSystemService/PendingExports.cpp | 42 +++++++++++-------- docs/dev/debt/libSceSystemService.md | 2 + 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 132a392cef..d25650ca75 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -8,6 +8,12 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } + +struct LncDaemonEvent; +struct LncShellUiEvent; +struct LncLocalProcessStatus; +struct LncCoredumpState; +struct LncVideoFinalizeState; struct LncLocalProcessParam; struct LncAppStatus; struct LncShellUiAppStatus; @@ -190,37 +196,37 @@ int APS5_VABI sceLncUtilGetAppTitleId(std::int32_t appId, char* titleId) { return 0; } -int APS5_VABI sceLncUtilGetAppType() { +int APS5_VABI sceLncUtilGetAppType(std::int32_t appId, std::uint32_t* type) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetCdlgExec() { +int APS5_VABI sceLncUtilGetCdlgExec(std::int32_t appId, bool* executing) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetCdlgType() { +int APS5_VABI sceLncUtilGetCdlgType(std::int32_t appId, std::uint32_t* type) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetCoredumpState() { +int APS5_VABI sceLncUtilGetCoredumpState(std::int32_t appId, std::int32_t processId, LncCoredumpState* state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetDbgExecutablePath() { +int APS5_VABI sceLncUtilGetDbgExecutablePath(char* path, std::uint64_t selector) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetEventForDaemon() { +int APS5_VABI sceLncUtilGetEventForDaemon(LncDaemonEvent* event) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetEventForShellUI() { +int APS5_VABI sceLncUtilGetEventForShellUI(LncShellUiEvent* event) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -230,7 +236,7 @@ int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus() { return 0; } -int APS5_VABI sceLncUtilGetLocalProcessStatusList() { +int APS5_VABI sceLncUtilGetLocalProcessStatusList(LncLocalProcessStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -240,17 +246,17 @@ int APS5_VABI sceLncUtilGetParentSocket() { return 0; } -int APS5_VABI sceLncUtilGetResultKillApp() { +int APS5_VABI sceLncUtilGetResultKillApp(std::uint32_t requestId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetResultLaunchAppByTitleId() { +int APS5_VABI sceLncUtilGetResultLaunchAppByTitleId(std::uint32_t requestId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetVideoFinalizeState() { +int APS5_VABI sceLncUtilGetVideoFinalizeState(std::int32_t appId, std::int32_t processId, LncVideoFinalizeState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -260,17 +266,17 @@ int APS5_VABI sceLncUtilInitialize() { return 0; } -int APS5_VABI sceLncUtilIsActiveCdlg() { +int APS5_VABI sceLncUtilIsActiveCdlg(bool* active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilIsAppLaunched() { +int APS5_VABI sceLncUtilIsAppLaunched(const char* titleId, bool* launched) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilIsAppSuspended() { +int APS5_VABI sceLncUtilIsAppSuspended(std::int32_t appId, bool* suspended) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -280,22 +286,22 @@ int APS5_VABI sceLncUtilIsCdlgActivated() { return 0; } -int APS5_VABI sceLncUtilIsCpuBudgetOfExtraAudioDevicesAvailable() { +bool APS5_VABI sceLncUtilIsCpuBudgetOfExtraAudioDevicesAvailable() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilIsHostBoot() { +int APS5_VABI sceLncUtilIsHostBoot(std::int32_t appId, bool* hostBoot) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilIsPs2Emu() { +int APS5_VABI sceLncUtilIsPs2Emu(bool* emulated) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { +bool APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 5be32ae15e..70122d4518 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -109,3 +109,5 @@ - [AppMessaging receive buffers](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the supplied module confirms the eight messaging argument lists, 64-bit event masks, byte-sized clear flag and 8192-byte send limit. Receive writes an 8216-byte message whose field meanings still require the sender/service handler. The protected bodies remain unchanged. - [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block; application-status entries occupy 64 bytes and shell-UI reboot entries 1336 bytes. Their fields remain unresolved. Title-ID output occupies 16 bytes; category, category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. + +- [LncUtil event and diagnostic payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process status entries are 36 bytes, coredump state 12 bytes and video-finalization state eight bytes; their field meanings remain unresolved. Daemon and shell-UI events are distinct output structures. GetDbgExecutablePath writes up to a 1024-byte protocol output and forwards a 64-bit selector whose meaning is unresolved. Boolean outputs and the two direct boolean budget/mode queries are confirmed. From bad5fb14a2bacc22c83cbcc548df59aa446c51f5 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:26:10 +0300 Subject: [PATCH 0617/1360] fix(systemservice): restore launcher lifecycle arguments --- .../libSceSystemService/PendingExports.cpp | 46 ++++++++++--------- docs/dev/debt/libSceSystemService.md | 2 + 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index d25650ca75..b7d7760e88 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -9,6 +9,8 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); + +struct LncLaunchParam; struct LncDaemonEvent; struct LncShellUiEvent; struct LncLocalProcessStatus; @@ -306,37 +308,37 @@ bool APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { return 0; } -int APS5_VABI sceLncUtilIsSpeculativeLaunch() { +int APS5_VABI sceLncUtilIsSpeculativeLaunch(std::int32_t appId, bool* speculative) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilKickCoredumpOnlyProcMem() { +int APS5_VABI sceLncUtilKickCoredumpOnlyProcMem(std::int32_t appId, std::int32_t processId, const char* first, const char* second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilKillLocalProcess() { +int APS5_VABI sceLncUtilKillLocalProcess(std::int32_t appId, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilLaunchApp() { +int APS5_VABI sceLncUtilLaunchApp(const char* titleId, const char* const* argv, const LncLaunchParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilLoadExec() { +int APS5_VABI sceLncUtilLoadExec(const char* path, const char* const* argv) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilNotifyCoredumpRequestEnd() { +int APS5_VABI sceLncUtilNotifyCoredumpRequestEnd(std::int32_t appId, std::int32_t processId, std::int32_t result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress() { +int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress(std::int32_t appId, std::int32_t processId, std::int32_t progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -346,12 +348,12 @@ int APS5_VABI sceLncUtilNotifyVshReady() { return 0; } -int APS5_VABI sceLncUtilRaiseException() { +int APS5_VABI sceLncUtilRaiseException(std::int32_t appId, std::uint64_t exception) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilRaiseExceptionLocalProcess() { +int APS5_VABI sceLncUtilRaiseExceptionLocalProcess(std::int32_t appId, std::int32_t processId, std::uint64_t exception) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -386,57 +388,57 @@ int APS5_VABI sceLncUtilReleaseCpuBudgetOfInGameStore() { return 0; } -int APS5_VABI sceLncUtilResumeApp() { +int APS5_VABI sceLncUtilResumeApp(std::int32_t appId, std::uint64_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilResumeLocalProcess() { +int APS5_VABI sceLncUtilResumeLocalProcess(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSetAppFocus() { +int APS5_VABI sceLncUtilSetAppFocus(std::int32_t appId, std::uint64_t first, std::uint64_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSetCdlgExec() { +int APS5_VABI sceLncUtilSetCdlgExec(std::int32_t appId, std::uint8_t executing) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSetControllerFocus() { +int APS5_VABI sceLncUtilSetControllerFocus(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSetControllerFocusPermission() { +int APS5_VABI sceLncUtilSetControllerFocusPermission(std::uint8_t permission) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilStartKillApp() { +int APS5_VABI sceLncUtilStartKillApp(std::int32_t appId, std::uint32_t* requestId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilStartLaunchAppByTitleId() { +int APS5_VABI sceLncUtilStartLaunchAppByTitleId(const char* titleId, const char* const* argv, const LncLaunchParam* param, std::uint32_t* requestId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSuspendApp() { +int APS5_VABI sceLncUtilSuspendApp(std::int32_t appId, std::uint64_t flags, std::int32_t reason) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSuspendBackgroundApp() { +int APS5_VABI sceLncUtilSuspendBackgroundApp(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilSuspendLocalProcess() { +int APS5_VABI sceLncUtilSuspendLocalProcess(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -446,12 +448,12 @@ int APS5_VABI sceLncUtilTerminate() { return 0; } -int APS5_VABI sceLncUtilTryBlockAppSuspend() { +int APS5_VABI sceLncUtilTryBlockAppSuspend(const char* reason) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilUnblockAppSuspend() { +int APS5_VABI sceLncUtilUnblockAppSuspend(const char* reason) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 70122d4518..6178137fe0 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -111,3 +111,5 @@ - [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block; application-status entries occupy 64 bytes and shell-UI reboot entries 1336 bytes. Their fields remain unresolved. Title-ID output occupies 16 bytes; category, category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. - [LncUtil event and diagnostic payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process status entries are 36 bytes, coredump state 12 bytes and video-finalization state eight bytes; their field meanings remain unresolved. Daemon and shell-UI events are distinct output structures. GetDbgExecutablePath writes up to a 1024-byte protocol output and forwards a 64-bit selector whose meaning is unresolved. Boolean outputs and the two direct boolean budget/mode queries are confirmed. + +- [LncUtil launch and lifecycle payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 40-byte launch parameter fields and meanings of several 64-bit focus/resume/suspend/exception values remain unresolved. Launch and load-exec use null-terminated argument vectors; asynchronous launch/kill write a 32-bit request ID. The argument positions and transported widths are confirmed, but the service operations remain protected stubs. From 0cdc168bc3c2b69155114e6b29f8d41afc5a4b1f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:30:32 +0300 Subject: [PATCH 0618/1360] fix(systemservice): restore shell application data signatures --- .../libSceSystemService/PendingExports.cpp | 28 +++++++++---------- docs/dev/debt/libSceSystemService.md | 4 ++- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index b7d7760e88..5d978434c3 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,9 +7,7 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } - - - +struct ShellCoreAppLaunchTypeInfo; struct LncLaunchParam; struct LncDaemonEvent; struct LncShellUiEvent; @@ -533,7 +531,7 @@ int APS5_VABI sceShellCoreUtilActivateTerm() { return 0; } -int APS5_VABI sceShellCoreUtilChangeRunLevel() { +int APS5_VABI sceShellCoreUtilChangeRunLevel(std::int32_t runLevel) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -588,7 +586,7 @@ int APS5_VABI sceShellCoreUtilDeleteDiscInstalledTitleWorkaroundFile() { return 0; } -int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile() { +int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile(std::uint32_t deviceType) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -603,12 +601,12 @@ int APS5_VABI sceShellCoreUtilDeleteDownloadedTitleWorkaroundFile() { return 0; } -int APS5_VABI sceShellCoreUtilDoFsck() { +int APS5_VABI sceShellCoreUtilDoFsck(const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer() { +int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer(std::uint32_t deviceType) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -638,7 +636,7 @@ int APS5_VABI sceShellCoreUtilExecuteCrashReport() { return 0; } -int APS5_VABI sceShellCoreUtilExfatFormatExternalHdd() { +int APS5_VABI sceShellCoreUtilExfatFormatExternalHdd(const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -653,12 +651,12 @@ int APS5_VABI sceShellCoreUtilExitMiniApp() { return 0; } -int APS5_VABI sceShellCoreUtilExitMiniAppWithValue() { +int APS5_VABI sceShellCoreUtilExitMiniAppWithValue(const char* value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilFormatExternalHdd() { +int APS5_VABI sceShellCoreUtilFormatExternalHdd(const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -668,27 +666,27 @@ int APS5_VABI sceShellCoreUtilFormatHddForRestore() { return 0; } -int APS5_VABI sceShellCoreUtilGetAppData() { +int APS5_VABI sceShellCoreUtilGetAppData(const char* key, void* data, std::int32_t capacity, std::int32_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetAppEnableTTS() { +int APS5_VABI sceShellCoreUtilGetAppEnableTTS(std::int32_t appId, bool* enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetAppEnterButtonAssign() { +int APS5_VABI sceShellCoreUtilGetAppEnterButtonAssign(std::int32_t appId, std::int32_t* assignment) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetAppLaunchTypeInfo() { +int APS5_VABI sceShellCoreUtilGetAppLaunchTypeInfo(std::int32_t appId, ShellCoreAppLaunchTypeInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt() { +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt(std::int32_t appId, const char* key, std::int32_t* value) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 6178137fe0..21292cfdde 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -19,7 +19,7 @@ - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 -- [libSceSystemService firmware 3.20 pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - 475 symbol names are present in [the PS5 3.20 libSceSystemService export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemService.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceSystemService pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): unreviewed argument lists and service behavior remain unresolved. The supplied module defines 381 of the 477 pending names; the other 96 require another module/version or caller evidence. Confirmed argument lists are being restored independently of their protected throwing bodies; remaining empty declarations must not be treated as proof of a zero-argument ABI. - [sceSystemServiceLaunchSystemDeeplink](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceShellCoreUtilLaunchByUriParamInit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemStateMgrTickVideoPlayback](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature @@ -113,3 +113,5 @@ - [LncUtil event and diagnostic payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process status entries are 36 bytes, coredump state 12 bytes and video-finalization state eight bytes; their field meanings remain unresolved. Daemon and shell-UI events are distinct output structures. GetDbgExecutablePath writes up to a 1024-byte protocol output and forwards a 64-bit selector whose meaning is unresolved. Boolean outputs and the two direct boolean budget/mode queries are confirmed. - [LncUtil launch and lifecycle payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 40-byte launch parameter fields and meanings of several 64-bit focus/resume/suspend/exception values remain unresolved. Launch and load-exec use null-terminated argument vectors; asynchronous launch/kill write a 32-bit request ID. The argument positions and transported widths are confirmed, but the service operations remain protected stubs. + +- [ShellCoreUtil application data](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): GetAppLaunchTypeInfo writes eight bytes whose fields are unresolved. Application-data and launched-parameter keys are null-terminated strings, with 32-bit capacities/outputs. Format/fsck paths and mini-app return values are strings. File-formatting and service implementations remain protected stubs. From c13664d573c675defb9abfafbc6b40a171d7fd12 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:31:32 +0300 Subject: [PATCH 0619/1360] fix(systemservice): restore shell query outputs and string buffers --- .../libSceSystemService/PendingExports.cpp | 20 +++++++++---------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 5d978434c3..5969fc5468 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -691,17 +691,17 @@ int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt(std::int32_t appId, const c return 0; } -int APS5_VABI sceShellCoreUtilGetAppLaunchedParamIntByBudgetType() { +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamIntByBudgetType(std::int32_t budgetType, const char* key, std::int32_t* value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetAppLaunchedParamString() { +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamString(std::int32_t appId, const char* key, char* value, std::int32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetAppLaunchedParamStringByBudgetType() { +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamStringByBudgetType(std::int32_t budgetType, const char* key, char* value, std::int32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -716,17 +716,17 @@ int APS5_VABI sceShellCoreUtilGetBasicProductShape() { return 0; } -int APS5_VABI sceShellCoreUtilGetCheckerString() { +int APS5_VABI sceShellCoreUtilGetCheckerString(const char* key, char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetCheckerStringEx() { +int APS5_VABI sceShellCoreUtilGetCheckerStringEx(const char* firstKey, const char* secondKey, char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetCloudClientStatus() { +int APS5_VABI sceShellCoreUtilGetCloudClientStatus(std::uint32_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -771,22 +771,22 @@ int APS5_VABI sceShellCoreUtilGetCrashReportUploadStatus() { return 0; } -int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior() { +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior(std::uint32_t* first, std::uint32_t* second, std::uint32_t* third) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout() { +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout(std::uint32_t* first, std::uint32_t* second, std::uint32_t* third, std::uint8_t timeout) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition() { +int APS5_VABI sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition(std::uint64_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { +std::uint64_t APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 21292cfdde..5b13cb0bd8 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -115,3 +115,5 @@ - [LncUtil launch and lifecycle payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 40-byte launch parameter fields and meanings of several 64-bit focus/resume/suspend/exception values remain unresolved. Launch and load-exec use null-terminated argument vectors; asynchronous launch/kill write a 32-bit request ID. The argument positions and transported widths are confirmed, but the service operations remain protected stubs. - [ShellCoreUtil application data](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): GetAppLaunchTypeInfo writes eight bytes whose fields are unresolved. Application-data and launched-parameter keys are null-terminated strings, with 32-bit capacities/outputs. Format/fsck paths and mini-app return values are strings. File-formatting and service implementations remain protected stubs. + +- [ShellCoreUtil queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): launched-parameter strings have signed 32-bit capacities; checker strings use machine-sized capacities and one or two string keys. Cloud status and device-index outputs are 32-bit scalars; the timeout variant consumes one byte. Partition-size output and the direct temporary-content free-size return are 64-bit. Device-index field meanings and the service-side checker protocol remain unresolved. From 0d911b3aecaf7282e1b8dc0b0275edd81fd63503 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:32:46 +0300 Subject: [PATCH 0620/1360] fix(systemservice): restore storage and hid query signatures --- .../libSceSystemService/PendingExports.cpp | 24 +++++++++---------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 5969fc5468..420763bbe6 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -791,22 +791,22 @@ std::uint64_t APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { return 0; } -int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition() { +int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition(std::uint64_t* firstSize, std::uint64_t* secondSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetFsckProgress() { +int APS5_VABI sceShellCoreUtilGetFsckProgress(const char* path, std::uint32_t* progress) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetGameLiveStreamingStatus() { +int APS5_VABI sceShellCoreUtilGetGameLiveStreamingStatus(bool* active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetGnmCompositorOnScreenProfilerFlag() { +bool APS5_VABI sceShellCoreUtilGetGnmCompositorOnScreenProfilerFlag() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -816,32 +816,32 @@ int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationMode() { return 0; } -int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId() { +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString() { +int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString(char* value, std::size_t capacity, std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigFileString() { +int APS5_VABI sceShellCoreUtilGetHidConfigFileString(char* value, std::size_t capacity, std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigName() { +int APS5_VABI sceShellCoreUtilGetHidConfigName(char* value, std::size_t capacity, std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigNum() { +int APS5_VABI sceShellCoreUtilGetHidConfigNum(std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetIDUMode() { +int APS5_VABI sceShellCoreUtilGetIDUMode(std::uint8_t* mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -856,12 +856,12 @@ int APS5_VABI sceShellCoreUtilGetManifestFileStatus() { return 0; } -int APS5_VABI sceShellCoreUtilGetNeedSizeOfAppContent() { +int APS5_VABI sceShellCoreUtilGetNeedSizeOfAppContent(std::uint64_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetNetEvConfigFileInfoString() { +int APS5_VABI sceShellCoreUtilGetNetEvConfigFileInfoString(char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 5b13cb0bd8..3277bb053c 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -117,3 +117,5 @@ - [ShellCoreUtil application data](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): GetAppLaunchTypeInfo writes eight bytes whose fields are unresolved. Application-data and launched-parameter keys are null-terminated strings, with 32-bit capacities/outputs. Format/fsck paths and mini-app return values are strings. File-formatting and service implementations remain protected stubs. - [ShellCoreUtil queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): launched-parameter strings have signed 32-bit capacities; checker strings use machine-sized capacities and one or two string keys. Cloud status and device-index outputs are 32-bit scalars; the timeout variant consumes one byte. Partition-size output and the direct temporary-content free-size return are 64-bit. Device-index field meanings and the service-side checker protocol remain unresolved. + +- [ShellCoreUtil storage and HID queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): partition free sizes and required application-content size are 64-bit outputs; fsck takes a path and a 32-bit progress output. HID string queries transport a 64-bit capacity and 32-bit selector, while output-vector lengths sign-extend the capacity's low 32 bits. The selector meaning and IDU mode byte values remain unresolved. Streaming status and the compositor-profiler return are boolean; the compositor-profiler and current GPU-load queries consume no caller arguments. From 8b48df0b928de51d4d956822109432eae2cde7dd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:33:32 +0300 Subject: [PATCH 0621/1360] fix(systemservice): restore account and display query arguments --- .../libSceSystemService/PendingExports.cpp | 37 +++++++++++-------- docs/dev/debt/libSceSystemService.md | 2 + 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 420763bbe6..abdf06671c 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,6 +7,13 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } +struct ShellCorePbtcUserInfoList; +struct ShellCorePrivacyEventData; +struct ShellCorePsnAccountInfoFirst; +struct ShellCorePsnAccountInfoSecond; +struct ShellCorePsnAccountInfoThird; +struct ShellCorePsnAccountInfoFourth; +struct ShellCorePsnAccountInfoFifth; struct ShellCoreAppLaunchTypeInfo; struct LncLaunchParam; struct LncDaemonEvent; @@ -876,12 +883,12 @@ int APS5_VABI sceShellCoreUtilGetPapcGamePcl() { return 0; } -int APS5_VABI sceShellCoreUtilGetPbtcUserInfoList() { +int APS5_VABI sceShellCoreUtilGetPbtcUserInfoList(ShellCorePbtcUserInfoList* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData() { +int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData(const char* firstKey, const char* secondKey, ShellCorePrivacyEventData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -896,32 +903,32 @@ int APS5_VABI sceShellCoreUtilGetProgressOfFsck() { return 0; } -int APS5_VABI sceShellCoreUtilGetPs4AppCategoryDefaultsListId() { +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryDefaultsListId(void* data, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetPs4AppCategoryForTitleId() { +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryForTitleId(const char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetPsStoreIconLayout() { +int APS5_VABI sceShellCoreUtilGetPsStoreIconLayout(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetPsStoreIconState() { +int APS5_VABI sceShellCoreUtilGetPsStoreIconState(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetPsnAccountInfo() { +int APS5_VABI sceShellCoreUtilGetPsnAccountInfo(ShellCorePsnAccountInfoFirst* first, ShellCorePsnAccountInfoSecond* second, ShellCorePsnAccountInfoThird* third, ShellCorePsnAccountInfoFourth* fourth, ShellCorePsnAccountInfoFifth* fifth) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetRemotePlayStatus() { +int APS5_VABI sceShellCoreUtilGetRemotePlayStatus(bool* active) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -931,37 +938,37 @@ int APS5_VABI sceShellCoreUtilGetRunLevel() { return 0; } -int APS5_VABI sceShellCoreUtilGetSharePlayStatus() { +int APS5_VABI sceShellCoreUtilGetSharePlayStatus(bool* active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetSocialScreenStatus() { +int APS5_VABI sceShellCoreUtilGetSocialScreenStatus(bool* active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetSplashScreenState() { +int APS5_VABI sceShellCoreUtilGetSplashScreenState(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetSupportSiteURL() { +int APS5_VABI sceShellCoreUtilGetSupportSiteURL(std::uint32_t first, std::uint32_t second, char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag() { +int APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetSystemBGState() { +int APS5_VABI sceShellCoreUtilGetSystemBGState(std::int32_t appId, std::uint32_t* state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor() { +int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor(std::int32_t appId, std::uint32_t* color) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3277bb053c..4c9db52da8 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -119,3 +119,5 @@ - [ShellCoreUtil queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): launched-parameter strings have signed 32-bit capacities; checker strings use machine-sized capacities and one or two string keys. Cloud status and device-index outputs are 32-bit scalars; the timeout variant consumes one byte. Partition-size output and the direct temporary-content free-size return are 64-bit. Device-index field meanings and the service-side checker protocol remain unresolved. - [ShellCoreUtil storage and HID queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): partition free sizes and required application-content size are 64-bit outputs; fsck takes a path and a 32-bit progress output. HID string queries transport a 64-bit capacity and 32-bit selector, while output-vector lengths sign-extend the capacity's low 32 bits. The selector meaning and IDU mode byte values remain unresolved. Streaming status and the compositor-profiler return are boolean; the compositor-profiler and current GPU-load queries consume no caller arguments. + +- [ShellCoreUtil account and privacy payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Pbtc user information occupies 800 bytes; privacy-event data occupies eight bytes after two string keys, each limited to 32 characters. The five account-information outputs occupy 8, 104, 136, 1024 and 1032 bytes; their fields remain unresolved. Category-default list output has a byte capacity and unresolved element format. Remote-play/share-play/social-screen outputs are boolean; application display-state queries take a 32-bit application ID. Only the support-site query signature is restored; no endpoint content is included. From d34675bb212a671f434de9160f7925dce5ddd8b8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:34:28 +0300 Subject: [PATCH 0622/1360] fix(systemservice): restore telemetry and user query signatures --- .../libSceSystemService/PendingExports.cpp | 30 ++++++++++--------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index abdf06671c..748f5f12cf 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,6 +7,8 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } +struct ShellCoreTelemetryToken; +struct ShellCoreUmmStatus; struct ShellCorePbtcUserInfoList; struct ShellCorePrivacyEventData; struct ShellCorePsnAccountInfoFirst; @@ -973,37 +975,37 @@ int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor(std::int32_t appId, std::uint return 0; } -int APS5_VABI sceShellCoreUtilGetSystemBGWaveState() { +int APS5_VABI sceShellCoreUtilGetSystemBGWaveState(std::int32_t appId, std::uint32_t* state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetTelemetryToken() { +int APS5_VABI sceShellCoreUtilGetTelemetryToken(ShellCoreTelemetryToken* token) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileInfoString() { +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileInfoString(char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileString() { +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileString(char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetUIStatus() { +int APS5_VABI sceShellCoreUtilGetUIStatus(std::uint64_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetUmmStatus() { +int APS5_VABI sceShellCoreUtilGetUmmStatus(std::uint32_t id, ShellCoreUmmStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetUserFocus() { +int APS5_VABI sceShellCoreUtilGetUserFocus(std::int32_t userId, std::int32_t* appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1013,7 +1015,7 @@ int APS5_VABI sceShellCoreUtilGetUserIdOfMorpheusUser() { return 0; } -int APS5_VABI sceShellCoreUtilGetVMStatsForTelemetry() { +int APS5_VABI sceShellCoreUtilGetVMStatsForTelemetry(char* value, std::size_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1028,7 +1030,7 @@ int APS5_VABI sceShellCoreUtilGoBackToKratosCurrentSessionGame() { return 0; } -int APS5_VABI sceShellCoreUtilHideBlocksFromUser() { +int APS5_VABI sceShellCoreUtilHideBlocksFromUser(std::uint64_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1043,12 +1045,12 @@ int APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked() { return 0; } -int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAcquired() { +bool APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAcquired() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { +bool APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1058,17 +1060,17 @@ int APS5_VABI sceShellCoreUtilIsBgmPlaying() { return 0; } -int APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { +bool APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsEyeToEyeDistanceAdjusted() { +int APS5_VABI sceShellCoreUtilIsEyeToEyeDistanceAdjusted(std::uint32_t userId, std::uint8_t* adjusted) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsGameLiveStreamingOnAir() { +bool APS5_VABI sceShellCoreUtilIsGameLiveStreamingOnAir() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 4c9db52da8..3e9d921cd8 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -121,3 +121,5 @@ - [ShellCoreUtil storage and HID queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): partition free sizes and required application-content size are 64-bit outputs; fsck takes a path and a 32-bit progress output. HID string queries transport a 64-bit capacity and 32-bit selector, while output-vector lengths sign-extend the capacity's low 32 bits. The selector meaning and IDU mode byte values remain unresolved. Streaming status and the compositor-profiler return are boolean; the compositor-profiler and current GPU-load queries consume no caller arguments. - [ShellCoreUtil account and privacy payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Pbtc user information occupies 800 bytes; privacy-event data occupies eight bytes after two string keys, each limited to 32 characters. The five account-information outputs occupy 8, 104, 136, 1024 and 1032 bytes; their fields remain unresolved. Category-default list output has a byte capacity and unresolved element format. Remote-play/share-play/social-screen outputs are boolean; application display-state queries take a 32-bit application ID. Only the support-site query signature is restored; no endpoint content is included. + +- [ShellCoreUtil telemetry and user queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): telemetry-token output is 4112 bytes, including an eight-byte service result at offset 4104; its meaning remains unresolved. UMM status is an eight-byte output with unresolved fields. UI status is a 64-bit event mask; VM statistics and workaround-file strings use machine-sized capacities. HideBlocksFromUser transports a 64-bit value and a 32-bit value whose exact semantics remain unresolved. Eye-distance adjustment writes one byte. From 315c4b65a1760e85067063d0bc3f6cfbedf41b70 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:35:30 +0300 Subject: [PATCH 0623/1360] fix(systemservice): restore shell navigation and mount arguments --- .../libSceSystemService/PendingExports.cpp | 29 ++++++++++--------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 748f5f12cf..6801d3acc5 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,6 +7,7 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } +struct ShellCoreLaunchByUriParam; struct ShellCoreTelemetryToken; struct ShellCoreUmmStatus; struct ShellCorePbtcUserInfoList; @@ -1080,7 +1081,7 @@ int APS5_VABI sceShellCoreUtilIsIDUState() { return 0; } -int APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { +bool APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1095,7 +1096,7 @@ int APS5_VABI sceShellCoreUtilIsInternalKratosUser() { return 0; } -int APS5_VABI sceShellCoreUtilIsKilledOrSuspendedByLogout() { +int APS5_VABI sceShellCoreUtilIsKilledOrSuspendedByLogout(std::int32_t first, std::int32_t second, std::uint8_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1105,22 +1106,22 @@ int APS5_VABI sceShellCoreUtilIsNeededCrashReport() { return 0; } -int APS5_VABI sceShellCoreUtilIsPowerSaveAlertRequested() { +bool APS5_VABI sceShellCoreUtilIsPowerSaveAlertRequested() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsRemotePlayCpuBudgetAcquired() { +bool APS5_VABI sceShellCoreUtilIsRemotePlayCpuBudgetAcquired() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsScreenSaverOn() { +bool APS5_VABI sceShellCoreUtilIsScreenSaverOn() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsSharePlayCpuBudgetAcquired() { +bool APS5_VABI sceShellCoreUtilIsSharePlayCpuBudgetAcquired() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1135,17 +1136,17 @@ int APS5_VABI sceShellCoreUtilIsTemperatureDanger() { return 0; } -int APS5_VABI sceShellCoreUtilIsTitleWorkaroundEnabled() { +int APS5_VABI sceShellCoreUtilIsTitleWorkaroundEnabled(std::int32_t selector, const char* key) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilIsUsbMassStorageMounted() { +bool APS5_VABI sceShellCoreUtilIsUsbMassStorageMounted(std::uint32_t deviceIndex) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilLaunchByUri() { +int APS5_VABI sceShellCoreUtilLaunchByUri(const char* uri, const ShellCoreLaunchByUriParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1160,12 +1161,12 @@ int APS5_VABI sceShellCoreUtilMakeManifestFile() { return 0; } -int APS5_VABI sceShellCoreUtilMountAppRight() { +int APS5_VABI sceShellCoreUtilMountAppRight(const char* key) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilMountDownloadDataForShellUI() { +int APS5_VABI sceShellCoreUtilMountDownloadDataForShellUI(const char* key) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1180,17 +1181,17 @@ int APS5_VABI sceShellCoreUtilMountHddForRestore() { return 0; } -int APS5_VABI sceShellCoreUtilNavigateToAnotherApp() { +int APS5_VABI sceShellCoreUtilNavigateToAnotherApp(std::uint32_t target) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilNavigateToGoHome() { +int APS5_VABI sceShellCoreUtilNavigateToGoHome(std::uint32_t target) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp() { +int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp(std::uint32_t target) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3e9d921cd8..6cf0154ae6 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -123,3 +123,5 @@ - [ShellCoreUtil account and privacy payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Pbtc user information occupies 800 bytes; privacy-event data occupies eight bytes after two string keys, each limited to 32 characters. The five account-information outputs occupy 8, 104, 136, 1024 and 1032 bytes; their fields remain unresolved. Category-default list output has a byte capacity and unresolved element format. Remote-play/share-play/social-screen outputs are boolean; application display-state queries take a 32-bit application ID. Only the support-site query signature is restored; no endpoint content is included. - [ShellCoreUtil telemetry and user queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): telemetry-token output is 4112 bytes, including an eight-byte service result at offset 4104; its meaning remains unresolved. UMM status is an eight-byte output with unresolved fields. UI status is a 64-bit event mask; VM statistics and workaround-file strings use machine-sized capacities. HideBlocksFromUser transports a 64-bit value and a 32-bit value whose exact semantics remain unresolved. Eye-distance adjustment writes one byte. + +- [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): LaunchByUri reads an eight-byte parameter containing two 32-bit fields and swaps their order in its message; field meanings remain unresolved. Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. From c1843d13506d59acd68d2f5999ab340584583e25 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:36:12 +0300 Subject: [PATCH 0624/1360] fix(systemservice): restore notification and error report arguments --- .../libSceSystemService/PendingExports.cpp | 28 +++++++++---------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 6801d3acc5..8a26ee3e4d 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -1196,12 +1196,12 @@ int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp(std::uint32_t target) { return 0; } -int APS5_VABI sceShellCoreUtilNotificationCancelForIDU() { +int APS5_VABI sceShellCoreUtilNotificationCancelForIDU(std::int64_t notificationId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilNotificationRequestedForIDU() { +int APS5_VABI sceShellCoreUtilNotificationRequestedForIDU(std::int64_t notificationId, const char* text, std::uint32_t type) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1211,12 +1211,12 @@ int APS5_VABI sceShellCoreUtilNotifyBgmCoreTermination() { return 0; } -int APS5_VABI sceShellCoreUtilNotifyFsReadError() { +int APS5_VABI sceShellCoreUtilNotifyFsReadError(std::int32_t first, std::int32_t second, std::int32_t third, const char* detail) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged() { +int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged(std::int32_t appId, std::uint8_t status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1226,7 +1226,7 @@ int APS5_VABI sceShellCoreUtilNotifyPsnAccountInfoReceived() { return 0; } -int APS5_VABI sceShellCoreUtilNotifySystemSuspendTelemetryInfo() { +int APS5_VABI sceShellCoreUtilNotifySystemSuspendTelemetryInfo(std::uint32_t info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1236,22 +1236,22 @@ int APS5_VABI sceShellCoreUtilPfAuthClientConsoleTokenClearCache() { return 0; } -int APS5_VABI sceShellCoreUtilPostActivityForPsNow() { +int APS5_VABI sceShellCoreUtilPostActivityForPsNow(std::int32_t type, const char* activity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilPostErrorLog() { +int APS5_VABI sceShellCoreUtilPostErrorLog(const char* firstText, std::uint32_t firstValue, const char* secondText, const char* thirdText, std::uint32_t secondValue, const char* fourthText) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilPostLaunchConfirmResult() { +int APS5_VABI sceShellCoreUtilPostLaunchConfirmResult(std::uint32_t first, std::uint32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilPostPsmEventToShellUI() { +int APS5_VABI sceShellCoreUtilPostPsmEventToShellUI(std::int32_t eventId, const char* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1286,12 +1286,12 @@ int APS5_VABI sceShellCoreUtilReleaseSharePlayCpuBudget() { return 0; } -int APS5_VABI sceShellCoreUtilReportSessionErrorToGaikaiController() { +int APS5_VABI sceShellCoreUtilReportSessionErrorToGaikaiController(std::int32_t error, const char* detail) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilReportUnexpectedFatalErrorToSystemTelemetry() { +int APS5_VABI sceShellCoreUtilReportUnexpectedFatalErrorToSystemTelemetry(std::int32_t error, const char* detail) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1301,17 +1301,17 @@ int APS5_VABI sceShellCoreUtilRequestCameraCalibration() { return 0; } -int APS5_VABI sceShellCoreUtilRequestEjectDevice() { +int APS5_VABI sceShellCoreUtilRequestEjectDevice(const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilRequestRebootApp() { +int APS5_VABI sceShellCoreUtilRequestRebootApp(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilRequestShutdown() { +int APS5_VABI sceShellCoreUtilRequestShutdown(std::int32_t reason) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 6cf0154ae6..4479df83b0 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -125,3 +125,5 @@ - [ShellCoreUtil telemetry and user queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): telemetry-token output is 4112 bytes, including an eight-byte service result at offset 4104; its meaning remains unresolved. UMM status is an eight-byte output with unresolved fields. UI status is a 64-bit event mask; VM statistics and workaround-file strings use machine-sized capacities. HideBlocksFromUser transports a 64-bit value and a 32-bit value whose exact semantics remain unresolved. Eye-distance adjustment writes one byte. - [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): LaunchByUri reads an eight-byte parameter containing two 32-bit fields and swaps their order in its message; field meanings remain unresolved. Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. + +- [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. Error-log submission consumes four strings and two 32-bit values in the restored order; their semantic names remain unresolved. PSM events carry a 32-bit event ID and a string. IME notification consumes a 32-bit value and one byte; service meanings remain unresolved. From a1e95b92ed01666960dea76d938de9a3eb331893 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:37:06 +0300 Subject: [PATCH 0625/1360] fix(systemservice): restore shell state setter arguments --- .../libSceSystemService/PendingExports.cpp | 46 +++++++++---------- docs/dev/debt/libSceSystemService.md | 2 + 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 8a26ee3e4d..06d15bf183 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -1006,7 +1006,7 @@ int APS5_VABI sceShellCoreUtilGetUmmStatus(std::uint32_t id, ShellCoreUmmStatus* return 0; } -int APS5_VABI sceShellCoreUtilGetUserFocus(std::int32_t userId, std::int32_t* appId) { +int APS5_VABI sceShellCoreUtilGetUserFocus(std::int32_t appId, std::int32_t* userId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1326,112 +1326,112 @@ int APS5_VABI sceShellCoreUtilResetBgdcConfig() { return 0; } -int APS5_VABI sceShellCoreUtilSetAppData() { +int APS5_VABI sceShellCoreUtilSetAppData(const char* key, const char* value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetBgmProhibition() { +int APS5_VABI sceShellCoreUtilSetBgmProhibition(std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior() { +int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior(std::uint8_t first, std::uint32_t second, std::uint16_t third) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetGameLiveStreamingOnAirFlag() { +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingOnAirFlag(std::int32_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetGameLiveStreamingStatus() { +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingStatus(std::int32_t active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag() { +int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag(std::int32_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode() { +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode(std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId() { +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId(std::int32_t appId, std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetIDUMode() { +int APS5_VABI sceShellCoreUtilSetIDUMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetImposeStatusFlag() { +int APS5_VABI sceShellCoreUtilSetImposeStatusFlag(std::int32_t enabled) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetPsStoreIconLayout() { +int APS5_VABI sceShellCoreUtilSetPsStoreIconLayout(std::uint32_t layout) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetPsStoreIconState() { +int APS5_VABI sceShellCoreUtilSetPsStoreIconState(std::uint32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetRemotePlayStatus() { +int APS5_VABI sceShellCoreUtilSetRemotePlayStatus(std::int32_t active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSharePlayStatus() { +int APS5_VABI sceShellCoreUtilSetSharePlayStatus(std::int32_t active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSkipUpdateCheck() { +int APS5_VABI sceShellCoreUtilSetSkipUpdateCheck(std::uint8_t skip) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSocialScreenStatus() { +int APS5_VABI sceShellCoreUtilSetSocialScreenStatus(std::int32_t active) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSplashScreenState() { +int APS5_VABI sceShellCoreUtilSetSplashScreenState(std::uint32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSystemBGState() { +int APS5_VABI sceShellCoreUtilSetSystemBGState(std::uint32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSystemBGWaveColor() { +int APS5_VABI sceShellCoreUtilSetSystemBGWaveColor(std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetSystemBGWaveState() { +int APS5_VABI sceShellCoreUtilSetSystemBGWaveState(std::uint32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetUIStatus() { +int APS5_VABI sceShellCoreUtilSetUIStatus(std::uint64_t status, std::uint64_t mask) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilSetUserFocus() { +int APS5_VABI sceShellCoreUtilSetUserFocus(std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 4479df83b0..09b5471c3e 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -127,3 +127,5 @@ - [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): LaunchByUri reads an eight-byte parameter containing two 32-bit fields and swaps their order in its message; field meanings remain unresolved. Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. - [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. Error-log submission consumes four strings and two 32-bit values in the restored order; their semantic names remain unresolved. PSM events carry a 32-bit event ID and a string. IME notification consumes a 32-bit value and one byte; service meanings remain unresolved. + +- [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. Device-index field meanings remain unresolved. From 797fb303c5cad840bc148ed7a9c8df366175b8a4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:37:41 +0300 Subject: [PATCH 0626/1360] fix(systemservice): restore error dialog layout and event arguments --- .../libSceSystemService/PendingExports.cpp | 33 ++++++++++++------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 06d15bf183..fa19512d5e 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -7,6 +7,17 @@ constexpr int SystemServiceRejected = static_cast<int>(0x80A10005); } +struct ShellCoreErrorDialogParam { + const char* message; + const char* const* formatArgs; + std::uint8_t flag; +}; + +static_assert(sizeof(ShellCoreErrorDialogParam) == 24); +static_assert(offsetof(ShellCoreErrorDialogParam, message) == 0); +static_assert(offsetof(ShellCoreErrorDialogParam, formatArgs) == 8); +static_assert(offsetof(ShellCoreErrorDialogParam, flag) == 16); + struct ShellCoreLaunchByUriParam; struct ShellCoreTelemetryToken; struct ShellCoreUmmStatus; @@ -1436,22 +1447,22 @@ int APS5_VABI sceShellCoreUtilSetUserFocus(std::int32_t userId) { return 0; } -int APS5_VABI sceShellCoreUtilShowCriticalErrorDialog() { +int APS5_VABI sceShellCoreUtilShowCriticalErrorDialog(std::uint32_t error, const char* message) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilShowErrorDialog() { +int APS5_VABI sceShellCoreUtilShowErrorDialog(std::uint32_t error, const char* message) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilShowErrorDialogWithFormatArgs() { +int APS5_VABI sceShellCoreUtilShowErrorDialogWithFormatArgs(std::uint32_t error, const char* message, const char* const* formatArgs) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilShowErrorDialogWithParam() { +int APS5_VABI sceShellCoreUtilShowErrorDialogWithParam(std::uint32_t error, const ShellCoreErrorDialogParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1466,22 +1477,22 @@ int APS5_VABI sceShellCoreUtilSignalUserInput() { return 0; } -int APS5_VABI sceShellCoreUtilStartPsNowGame() { +int APS5_VABI sceShellCoreUtilStartPsNowGame(const char* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilStopPsNowGame() { +int APS5_VABI sceShellCoreUtilStopPsNowGame(const char* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilTalkKeyEventReceived() { +int APS5_VABI sceShellCoreUtilTalkKeyEventReceived(std::uint64_t first, std::uint32_t second, std::uint32_t third) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilTestBusTransferSpeed() { +int APS5_VABI sceShellCoreUtilTestBusTransferSpeed(const char* path, std::uint8_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1491,7 +1502,7 @@ int APS5_VABI sceShellCoreUtilTickHeartBeat() { return 0; } -int APS5_VABI sceShellCoreUtilTriggerPapcUpdate() { +int APS5_VABI sceShellCoreUtilTriggerPapcUpdate(std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1501,12 +1512,12 @@ int APS5_VABI sceShellCoreUtilTurnOffScreenSaver() { return 0; } -int APS5_VABI sceShellCoreUtilUnmountAppRight() { +int APS5_VABI sceShellCoreUtilUnmountAppRight(const char* key) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilUnmountDownloadDataForShellUI() { +int APS5_VABI sceShellCoreUtilUnmountDownloadDataForShellUI(const char* key) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 09b5471c3e..d757cac56d 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -129,3 +129,5 @@ - [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. Error-log submission consumes four strings and two 32-bit values in the restored order; their semantic names remain unresolved. PSM events carry a 32-bit event ID and a string. IME notification consumes a 32-bit value and one byte; service meanings remain unresolved. - [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. Device-index field meanings remain unresolved. + +- [ShellCoreUtil dialog parameters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 24-byte error-dialog parameter contains a message pointer at offset zero, a null-terminated vector of string arguments at offset eight and a byte flag at offset 16. Field offsets and size are asserted; the flag meaning remains unresolved. Start/stop streaming-game and unmount requests take strings. Talk-key events transport a 64-bit value and two 32-bit values whose meanings remain unresolved. Bus-transfer testing writes one byte after a string input. From 2386ab8e84b9a00835a72b43ddfcdf99737de64e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:38:54 +0300 Subject: [PATCH 0627/1360] fix(systemservice): restore activation handles and process signatures --- .../libSceSystemService/PendingExports.cpp | 63 ++++++++++--------- docs/dev/debt/libSceSystemService.md | 2 + 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index fa19512d5e..1ea502ba66 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -18,6 +18,11 @@ static_assert(offsetof(ShellCoreErrorDialogParam, message) == 0); static_assert(offsetof(ShellCoreErrorDialogParam, formatArgs) == 8); static_assert(offsetof(ShellCoreErrorDialogParam, flag) == 16); +struct SystemServiceLocalProcessParam; +struct SystemServiceJvmProcessParam; +struct SystemServicePs2ProcessParam; +struct SystemServiceAppInstallStatus; +struct SystemServiceAppStatus; struct ShellCoreLaunchByUriParam; struct ShellCoreTelemetryToken; struct ShellCoreUmmStatus; @@ -521,17 +526,17 @@ int APS5_VABI sceShellCoreUtilActivateAbort() { return 0; } -int APS5_VABI sceShellCoreUtilActivateGetStatus() { +int APS5_VABI sceShellCoreUtilActivateGetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilActivateInit() { +int APS5_VABI sceShellCoreUtilActivateInit(std::uint32_t type, std::uintptr_t* handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilActivateIsActivated() { +int APS5_VABI sceShellCoreUtilActivateIsActivated(std::uint32_t type, std::uint8_t* activated) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1561,22 +1566,22 @@ int APS5_VABI sceSystemServiceActivateHevc() { return 0; } -int APS5_VABI sceSystemServiceActivateHevcAbort() { +int APS5_VABI sceSystemServiceActivateHevcAbort(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcGetStatus() { +int APS5_VABI sceSystemServiceActivateHevcGetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcInit() { +int APS5_VABI sceSystemServiceActivateHevcInit(std::uintptr_t* handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcIsActivated() { +int APS5_VABI sceSystemServiceActivateHevcIsActivated(std::uint8_t* activated) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1586,92 +1591,92 @@ int APS5_VABI sceSystemServiceActivateHevcSoft() { return 0; } -int APS5_VABI sceSystemServiceActivateHevcSoftAbort() { +int APS5_VABI sceSystemServiceActivateHevcSoftAbort(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcSoftGetStatus() { +int APS5_VABI sceSystemServiceActivateHevcSoftGetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcSoftInit() { +int APS5_VABI sceSystemServiceActivateHevcSoftInit(std::uintptr_t* handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcSoftIsActivated() { +int APS5_VABI sceSystemServiceActivateHevcSoftIsActivated(std::uint8_t* activated) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcSoftStart() { +int APS5_VABI sceSystemServiceActivateHevcSoftStart(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcSoftTerm() { +int APS5_VABI sceSystemServiceActivateHevcSoftTerm(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcStart() { +int APS5_VABI sceSystemServiceActivateHevcStart(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateHevcTerm() { +int APS5_VABI sceSystemServiceActivateHevcTerm(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateMpeg2Abort() { +int APS5_VABI sceSystemServiceActivateMpeg2Abort(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateMpeg2GetStatus() { +int APS5_VABI sceSystemServiceActivateMpeg2GetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateMpeg2Init() { +int APS5_VABI sceSystemServiceActivateMpeg2Init(std::uintptr_t* handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateMpeg2IsActivated() { +int APS5_VABI sceSystemServiceActivateMpeg2IsActivated(std::uint8_t* activated) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateMpeg2Start() { +int APS5_VABI sceSystemServiceActivateMpeg2Start(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceActivateMpeg2Term() { +int APS5_VABI sceSystemServiceActivateMpeg2Term(std::uintptr_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceAddLocalProcess() { +int APS5_VABI sceSystemServiceAddLocalProcess(std::int32_t appId, const char* path, const char* const* argv, const SystemServiceLocalProcessParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceAddLocalProcessForJvm() { +int APS5_VABI sceSystemServiceAddLocalProcessForJvm(const char* path, const char* const* argv, const SystemServiceJvmProcessParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceAddLocalProcessForPs2Emu() { +int APS5_VABI sceSystemServiceAddLocalProcessForPs2Emu(const char* path, const char* const* argv, const SystemServicePs2ProcessParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceAppGetAppInstallStatus() { +int APS5_VABI sceSystemServiceAppGetAppInstallStatus(const char* titleId, SystemServiceAppInstallStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1778,12 +1783,12 @@ int APS5_VABI sceSystemServiceEnableSuspendNotification() { return 0; } -int APS5_VABI sceSystemServiceGetAppCategoryType() { +int APS5_VABI sceSystemServiceGetAppCategoryType(std::int32_t appId, std::uint32_t* categoryType) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceGetAppFocusedAppStatus() { +int APS5_VABI sceSystemServiceGetAppFocusedAppStatus(SystemServiceAppStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1798,12 +1803,12 @@ int APS5_VABI sceSystemServiceGetAppIdListOfBigApp() { return 0; } -int APS5_VABI sceSystemServiceGetAppLaunchedUser() { +int APS5_VABI sceSystemServiceGetAppLaunchedUser(std::int32_t appId, std::int32_t* userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceGetAppStatus() { +int APS5_VABI sceSystemServiceGetAppStatus(SystemServiceAppStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index d757cac56d..464ff980d3 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -131,3 +131,5 @@ - [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. Device-index field meanings remain unresolved. - [ShellCoreUtil dialog parameters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 24-byte error-dialog parameter contains a message pointer at offset zero, a null-terminated vector of string arguments at offset eight and a byte flag at offset 16. Field offsets and size are asserted; the flag meaning remains unresolved. Start/stop streaming-game and unmount requests take strings. Talk-key events transport a 64-bit value and two 32-bit values whose meanings remain unresolved. Bus-transfer testing writes one byte after a string input. + +- [SystemService activation and process signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): activation wrappers validate a machine-sized handle and expose 32-bit status/result outputs or a byte activation flag. Shell activation helpers also receive a type selector; constant-return activation exports still need caller evidence for unused parameters. SystemService local-process parameters differ from LncUtil parameters and are kept as distinct types; general creation reads through offset 71, JVM/PS2 creation reads two 32-bit fields at offsets four and eight. Install status writes 12 bytes and application status writes 16 bytes; their remaining field meanings are unresolved. From 46e9270dc4c1a41aaf61e862b21cf42d3a918e95 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:40:28 +0300 Subject: [PATCH 0628/1360] fix(systemservice): restore public query payload signatures --- .../libSceSystemService/PendingExports.cpp | 31 ++++++++++++------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 1ea502ba66..22ff539f47 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -18,6 +18,13 @@ static_assert(offsetof(ShellCoreErrorDialogParam, message) == 0); static_assert(offsetof(ShellCoreErrorDialogParam, formatArgs) == 8); static_assert(offsetof(ShellCoreErrorDialogParam, flag) == 16); +struct SystemServiceDaemonEvent; +struct SystemServiceLocalProcessStatus; +struct SystemServiceEvent; +struct SystemServicePSButtonEvent; +struct SystemServicePrivacyDefinitionData; +struct SystemServicePrivacyDefinitionVersion; +struct SystemServiceTitleWorkaroundInfo; struct SystemServiceLocalProcessParam; struct SystemServiceJvmProcessParam; struct SystemServicePs2ProcessParam; @@ -1813,22 +1820,22 @@ int APS5_VABI sceSystemServiceGetAppStatus(SystemServiceAppStatus* status) { return 0; } -int APS5_VABI sceSystemServiceGetAppTitleId() { +int APS5_VABI sceSystemServiceGetAppTitleId(std::int32_t appId, char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceGetAppType() { +int APS5_VABI sceSystemServiceGetAppType(std::int32_t appId, std::int32_t* type) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceGetDbgExecutablePath() { +int APS5_VABI sceSystemServiceGetDbgExecutablePath(char* path, std::uint64_t selector) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceGetEventForDaemon() { +int APS5_VABI sceSystemServiceGetEventForDaemon(SystemServiceDaemonEvent* event) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1838,7 +1845,7 @@ int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { return 0; } -int APS5_VABI sceSystemServiceGetLocalProcessStatusList() { +int APS5_VABI sceSystemServiceGetLocalProcessStatusList(SystemServiceLocalProcessStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1848,7 +1855,7 @@ int APS5_VABI sceSystemServiceGetMainAppTitleId() { return 0; } -int APS5_VABI sceSystemServiceGetPSButtonEvent() { +int APS5_VABI sceSystemServiceGetPSButtonEvent(const SystemServiceEvent* event, SystemServicePSButtonEvent* buttonEvent) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1873,12 +1880,12 @@ int APS5_VABI sceSystemServiceGetParentSocketForPsmKit() { return 0; } -int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionData() { +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionData(const char* key, SystemServicePrivacyDefinitionData* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion() { +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion(const char* key, SystemServicePrivacyDefinitionVersion* version) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1888,7 +1895,7 @@ int APS5_VABI sceSystemServiceGetPlatformPrivacySetting() { return 0; } -int APS5_VABI sceSystemServiceGetRecentLotusPushedButton() { +int APS5_VABI sceSystemServiceGetRecentLotusPushedButton(std::uint32_t timeout, std::int32_t* deviceId, std::uint32_t* buttons) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1913,7 +1920,7 @@ int APS5_VABI sceSystemServiceGetSubAppTitleIdForSP() { return 0; } -int APS5_VABI sceSystemServiceGetTitleWorkaroundInfo() { +int APS5_VABI sceSystemServiceGetTitleWorkaroundInfo(SystemServiceTitleWorkaroundInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1933,12 +1940,12 @@ int APS5_VABI sceSystemServiceInitializeForShellCore() { return 0; } -int APS5_VABI sceSystemServiceInvokeAppLaunchLink() { +int APS5_VABI sceSystemServiceInvokeAppLaunchLink(const char* uri) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceIsAppSuspended() { +int APS5_VABI sceSystemServiceIsAppSuspended(std::int32_t appId, bool* suspended) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 464ff980d3..1cf7485a3b 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -133,3 +133,5 @@ - [ShellCoreUtil dialog parameters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 24-byte error-dialog parameter contains a message pointer at offset zero, a null-terminated vector of string arguments at offset eight and a byte flag at offset 16. Field offsets and size are asserted; the flag meaning remains unresolved. Start/stop streaming-game and unmount requests take strings. Talk-key events transport a 64-bit value and two 32-bit values whose meanings remain unresolved. Bus-transfer testing writes one byte after a string input. - [SystemService activation and process signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): activation wrappers validate a machine-sized handle and expose 32-bit status/result outputs or a byte activation flag. Shell activation helpers also receive a type selector; constant-return activation exports still need caller evidence for unused parameters. SystemService local-process parameters differ from LncUtil parameters and are kept as distinct types; general creation reads through offset 71, JVM/PS2 creation reads two 32-bit fields at offsets four and eight. Install status writes 12 bytes and application status writes 16 bytes; their remaining field meanings are unresolved. + +- [SystemService query payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): daemon events write 12 bytes; local-process entries occupy 36 bytes, consisting of a 32-bit value and 32-byte name. PS-button extraction reads a 16-byte event and rearranges its fields into a distinct 16-byte output; semantic names remain unresolved. Privacy definition/version queries take string keys and distinct output types; their layouts remain unresolved. Workaround information writes four bytes at offset zero and 16 bytes at offset eight. From c9c8675b1d4c0f9d6c464e4ac60d83bc3c383fdf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:41:40 +0300 Subject: [PATCH 0629/1360] fix(systemservice): restore assembly alias signatures --- .../libSceSystemService/PendingExports.cpp | 22 +++++++++---------- docs/dev/debt/libSceSystemService.md | 4 +++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 22ff539f47..28bdc18d54 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -1805,7 +1805,7 @@ int APS5_VABI sceSystemServiceGetAppId() { return 0; } -int APS5_VABI sceSystemServiceGetAppIdListOfBigApp() { +int APS5_VABI sceSystemServiceGetAppIdListOfBigApp(std::int32_t* appIds, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1900,7 +1900,7 @@ int APS5_VABI sceSystemServiceGetRecentLotusPushedButton(std::uint32_t timeout, return 0; } -int APS5_VABI sceSystemServiceGetRemoteplayStatus() { +int APS5_VABI sceSystemServiceGetRemoteplayStatus(bool* active) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1925,7 +1925,7 @@ int APS5_VABI sceSystemServiceGetTitleWorkaroundInfo(SystemServiceTitleWorkaroun return 0; } -int APS5_VABI sceSystemServiceGetUIStatus() { +int APS5_VABI sceSystemServiceGetUIStatus(std::uint64_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1950,7 +1950,7 @@ int APS5_VABI sceSystemServiceIsAppSuspended(std::int32_t appId, bool* suspended return 0; } -int APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { +bool APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1965,17 +1965,17 @@ int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted() { return 0; } -int APS5_VABI sceSystemServiceIsGameLiveStreamingOnAir() { +bool APS5_VABI sceSystemServiceIsGameLiveStreamingOnAir() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceIsScreenSaverOn() { +bool APS5_VABI sceSystemServiceIsScreenSaverOn() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { +bool APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2028,7 +2028,7 @@ int APS5_VABI sceSystemServiceLoadExecVideoServiceWebApp() { return SystemServiceUnavailable; } -int APS5_VABI sceSystemServiceNavigateToAnotherApp() { +int APS5_VABI sceSystemServiceNavigateToAnotherApp(std::uint32_t target) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2038,12 +2038,12 @@ int APS5_VABI sceSystemServiceNavigateToGoBack() { return 0; } -int APS5_VABI sceSystemServiceNavigateToGoBackWithValue() { +int APS5_VABI sceSystemServiceNavigateToGoBackWithValue(const char* value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceNavigateToGoHome() { +int APS5_VABI sceSystemServiceNavigateToGoHome(std::uint32_t target) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2109,7 +2109,7 @@ int APS5_VABI sceSystemServiceSaveVideoToken() { return 0; } -int APS5_VABI sceSystemServiceSetControllerFocusPermission() { +int APS5_VABI sceSystemServiceSetControllerFocusPermission(std::uint8_t permission) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 1cf7485a3b..dbd19c0a01 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -19,7 +19,7 @@ - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 -- [libSceSystemService pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): unreviewed argument lists and service behavior remain unresolved. The supplied module defines 381 of the 477 pending names; the other 96 require another module/version or caller evidence. Confirmed argument lists are being restored independently of their protected throwing bodies; remaining empty declarations must not be treated as proof of a zero-argument ABI. +- [libSceSystemService pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): unreviewed argument lists and service behavior remain unresolved. The C decompilation contains 381 of the 477 pending names; additional exports exist as direct assembly jumps, so absence from that listing does not imply absence from the module. Confirmed argument lists are being restored independently of their protected throwing bodies; remaining empty declarations must not be treated as proof of a zero-argument ABI. - [sceSystemServiceLaunchSystemDeeplink](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceShellCoreUtilLaunchByUriParamInit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemStateMgrTickVideoPlayback](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature @@ -135,3 +135,5 @@ - [SystemService activation and process signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): activation wrappers validate a machine-sized handle and expose 32-bit status/result outputs or a byte activation flag. Shell activation helpers also receive a type selector; constant-return activation exports still need caller evidence for unused parameters. SystemService local-process parameters differ from LncUtil parameters and are kept as distinct types; general creation reads through offset 71, JVM/PS2 creation reads two 32-bit fields at offsets four and eight. Install status writes 12 bytes and application status writes 16 bytes; their remaining field meanings are unresolved. - [SystemService query payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): daemon events write 12 bytes; local-process entries occupy 36 bytes, consisting of a 32-bit value and 32-byte name. PS-button extraction reads a 16-byte event and rearranges its fields into a distinct 16-byte output; semantic names remain unresolved. Privacy definition/version queries take string keys and distinct output types; their layouts remain unresolved. Workaround information writes four bytes at offset zero and 16 bytes at offset eight. + +- [SystemService direct aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly-only jumps now share the confirmed signatures of their LncUtil/ShellCoreUtil targets for application-ID lists, remote-play/UI status, navigation, controller-focus permissions and boolean budget/status queries. Aliases of unresolved targets still require target or caller analysis. From d4f2911b74287939104ce1b22034d07858558ee8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:42:23 +0300 Subject: [PATCH 0630/1360] fix(systemservice): restore collapsed launcher dispatch arguments --- .../libSceSystemService/PendingExports.cpp | 21 ++++++++++--------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 28bdc18d54..1343f07d4e 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -41,6 +41,7 @@ struct ShellCorePsnAccountInfoThird; struct ShellCorePsnAccountInfoFourth; struct ShellCorePsnAccountInfoFifth; struct ShellCoreAppLaunchTypeInfo; +struct LncGpuCrashAppStatus; struct LncLaunchParam; struct LncDaemonEvent; struct LncShellUiEvent; @@ -129,7 +130,7 @@ int APS5_VABI sceLncUtilBlockingGetEventForDaemon() { return 0; } -int APS5_VABI sceLncUtilContinueApp() { +int APS5_VABI sceLncUtilContinueApp(std::uint32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -184,12 +185,12 @@ int APS5_VABI sceLncUtilGetAppCategoryType(std::int32_t appId, std::uint32_t* ca return 0; } -int APS5_VABI sceLncUtilGetAppFocusedAppStatus() { +int APS5_VABI sceLncUtilGetAppFocusedAppStatus(LncAppStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLncUtilGetAppId() { +int APS5_VABI sceLncUtilGetAppId(const char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -209,7 +210,7 @@ int APS5_VABI sceLncUtilGetAppLaunchedUser(std::int32_t appId, std::int32_t* use return 0; } -int APS5_VABI sceLncUtilGetAppStatus() { +int APS5_VABI sceLncUtilGetAppStatus(LncAppStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -264,7 +265,7 @@ int APS5_VABI sceLncUtilGetEventForShellUI(LncShellUiEvent* event) { return 0; } -int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus() { +int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus(LncGpuCrashAppStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -314,7 +315,7 @@ int APS5_VABI sceLncUtilIsAppSuspended(std::int32_t appId, bool* suspended) { return 0; } -int APS5_VABI sceLncUtilIsCdlgActivated() { +int APS5_VABI sceLncUtilIsCdlgActivated(std::uint8_t* activated) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -374,7 +375,7 @@ int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress(std::int32_t appId, std::i return 0; } -int APS5_VABI sceLncUtilNotifyVshReady() { +int APS5_VABI sceLncUtilNotifyVshReady(std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -389,7 +390,7 @@ int APS5_VABI sceLncUtilRaiseExceptionLocalProcess(std::int32_t appId, std::int3 return 0; } -int APS5_VABI sceLncUtilRegisterCdlgSharedMemoryName() { +int APS5_VABI sceLncUtilRegisterCdlgSharedMemoryName(const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -489,7 +490,7 @@ int APS5_VABI sceLncUtilUnblockAppSuspend(const char* reason) { return 0; } -int APS5_VABI sceLncUtilUnregisterCdlgSharedMemoryName() { +int APS5_VABI sceLncUtilUnregisterCdlgSharedMemoryName(const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1800,7 +1801,7 @@ int APS5_VABI sceSystemServiceGetAppFocusedAppStatus(SystemServiceAppStatus* sta return 0; } -int APS5_VABI sceSystemServiceGetAppId() { +int APS5_VABI sceSystemServiceGetAppId(const char* titleId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index dbd19c0a01..e9afdfd880 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -137,3 +137,5 @@ - [SystemService query payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): daemon events write 12 bytes; local-process entries occupy 36 bytes, consisting of a 32-bit value and 32-byte name. PS-button extraction reads a 16-byte event and rearranges its fields into a distinct 16-byte output; semantic names remain unresolved. Privacy definition/version queries take string keys and distinct output types; their layouts remain unresolved. Workaround information writes four bytes at offset zero and 16 bytes at offset eight. - [SystemService direct aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly-only jumps now share the confirmed signatures of their LncUtil/ShellCoreUtil targets for application-ID lists, remote-play/UI status, navigation, controller-focus permissions and boolean budget/status queries. Aliases of unresolved targets still require target or caller analysis. + +- [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. GPU-crash fields and the VSH-ready selector meaning remain unresolved; collapsed assembly procedures are not missing exports. From 32b7c50240ed15349d3ddbbba2fc983d146383cc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:43:39 +0300 Subject: [PATCH 0631/1360] fix(systemservice): restore process control and telemetry arguments --- .../libSceSystemService/PendingExports.cpp | 40 +++++++++++-------- docs/dev/debt/libSceSystemService.md | 2 + 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 1343f07d4e..e46b041d22 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -25,6 +25,12 @@ struct SystemServicePSButtonEvent; struct SystemServicePrivacyDefinitionData; struct SystemServicePrivacyDefinitionVersion; struct SystemServiceTitleWorkaroundInfo; +struct SystemServiceStoreParam { + std::uint32_t size; +}; + +static_assert(sizeof(SystemServiceStoreParam) == 4); + struct SystemServiceLocalProcessParam; struct SystemServiceJvmProcessParam; struct SystemServicePs2ProcessParam; @@ -1961,7 +1967,7 @@ int APS5_VABI sceSystemServiceIsBgmPlaying() { return 0; } -int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted() { +int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted(std::uint32_t userId, std::uint8_t* adjusted) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1981,17 +1987,17 @@ bool APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { return 0; } -int APS5_VABI sceSystemServiceKillLocalProcess() { +int APS5_VABI sceSystemServiceKillLocalProcess(std::int32_t appId, std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceKillLocalProcessForJvm() { +int APS5_VABI sceSystemServiceKillLocalProcessForJvm(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu() { +int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2001,7 +2007,7 @@ int APS5_VABI sceSystemServiceLaunchEventDetails() { return 0; } -int APS5_VABI sceSystemServiceLaunchStore() { +int APS5_VABI sceSystemServiceLaunchStore(const char* uri, const SystemServiceStoreParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2020,7 +2026,7 @@ int APS5_VABI sceSystemServiceLaunchUdsApp() { return SystemServiceRejected; } -int APS5_VABI sceSystemServiceLaunchWebApp() { +int APS5_VABI sceSystemServiceLaunchWebApp(const char* argument) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2054,7 +2060,7 @@ int APS5_VABI sceSystemServiceNotifyBgmCoreTermination() { return 0; } -int APS5_VABI sceSystemServiceRaiseExceptionLocalProcess() { +int APS5_VABI sceSystemServiceRaiseExceptionLocalProcess(std::int32_t appId, std::int32_t processId, std::uint64_t exception) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2100,12 +2106,12 @@ int APS5_VABI sceSystemServiceRequestToChangeRenderingMode() { return SystemServiceRejected; } -int APS5_VABI sceSystemServiceResumeLocalProcess() { +int APS5_VABI sceSystemServiceResumeLocalProcess(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceSaveVideoToken() { +int APS5_VABI sceSystemServiceSaveVideoToken(std::int32_t userId, const char* token) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2115,12 +2121,12 @@ int APS5_VABI sceSystemServiceSetControllerFocusPermission(std::uint8_t permissi return 0; } -int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode() { +int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode(std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag() { +int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag(std::uint8_t flag) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2129,7 +2135,7 @@ int APS5_VABI sceSystemServiceSetOutOfVrPlayZoneWarning() { return 0; } -int APS5_VABI sceSystemServiceSetPowerSaveLevel() { +int APS5_VABI sceSystemServiceSetPowerSaveLevel(std::uint32_t level) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2144,7 +2150,7 @@ int APS5_VABI sceSystemServiceShowClosedCaptionSettings() { return 0; } -int APS5_VABI sceSystemServiceShowErrorDialog() { +int APS5_VABI sceSystemServiceShowErrorDialog(std::uint32_t error, const char* message) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2159,22 +2165,22 @@ int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu() { return 0; } -int APS5_VABI sceSystemServiceSuspendBackgroundApp() { +int APS5_VABI sceSystemServiceSuspendBackgroundApp(std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceSuspendLocalProcess() { +int APS5_VABI sceSystemServiceSuspendLocalProcess(std::int32_t processId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceTelemetrySetData() { +int APS5_VABI sceSystemServiceTelemetrySetData(const char* key, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceTelemetrySetData2() { +int APS5_VABI sceSystemServiceTelemetrySetData2(const char* key, const char* data) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index e9afdfd880..5d427df1b0 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -139,3 +139,5 @@ - [SystemService direct aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly-only jumps now share the confirmed signatures of their LncUtil/ShellCoreUtil targets for application-ID lists, remote-play/UI status, navigation, controller-focus permissions and boolean budget/status queries. Aliases of unresolved targets still require target or caller analysis. - [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. GPU-crash fields and the VSH-ready selector meaning remain unresolved; collapsed assembly procedures are not missing exports. + +- [SystemService launch and telemetry arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): store-launch parameters contain only a four-byte size; web-app launch takes one string argument. Process lifecycle wrappers forward the verified LncUtil argument lists. TelemetrySetData takes a string key, binary payload and machine-sized length; TelemetrySetData2 takes two strings. Token storage consumes a 32-bit identifier and string; only its signature is restored, with no token values included. From 4e9520a9dbc2e7ec26658049d9c8aff5ffd6d577 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:44:57 +0300 Subject: [PATCH 0632/1360] fix(systemservice): restore state manager and modal arguments --- .../libSceSystemService/PendingExports.cpp | 20 +++++++++---------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index e46b041d22..7cb020b76e 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -1556,12 +1556,12 @@ int APS5_VABI sceShellCoreutilGetCrashReportProcessInformation() { return 0; } -int APS5_VABI sceSystemModalDialogClose() { +int APS5_VABI sceSystemModalDialogClose(std::uint32_t dialogId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemModalDialogOpen() { +int APS5_VABI sceSystemModalDialogOpen(const char* message) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1897,7 +1897,7 @@ int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion(const char* ke return 0; } -int APS5_VABI sceSystemServiceGetPlatformPrivacySetting() { +int APS5_VABI sceSystemServiceGetPlatformPrivacySetting(std::uint32_t* setting) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2315,7 +2315,7 @@ int APS5_VABI sceSystemStateMgrGetTriggerCode() { return 0; } -int APS5_VABI sceSystemStateMgrIsBdDriveReady() { +bool APS5_VABI sceSystemStateMgrIsBdDriveReady() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2325,12 +2325,12 @@ int APS5_VABI sceSystemStateMgrIsGpuPerformanceNormal() { return 0; } -int APS5_VABI sceSystemStateMgrIsShellUIShutdownInProgress() { +bool APS5_VABI sceSystemStateMgrIsShellUIShutdownInProgress() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemStateMgrIsStandbyModeEnabled() { +bool APS5_VABI sceSystemStateMgrIsStandbyModeEnabled() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2340,7 +2340,7 @@ int APS5_VABI sceSystemStateMgrLeaveMediaPlaybackMode() { return 0; } -int APS5_VABI sceSystemStateMgrNotifySystemSuspendResumeProgress() { +int APS5_VABI sceSystemStateMgrNotifySystemSuspendResumeProgress(const char* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2350,7 +2350,7 @@ int APS5_VABI sceSystemStateMgrReboot() { return 0; } -int APS5_VABI sceSystemStateMgrRequestToKeepMainOnStandby() { +int APS5_VABI sceSystemStateMgrRequestToKeepMainOnStandby(const char* reason) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2360,7 +2360,7 @@ int APS5_VABI sceSystemStateMgrSendCecOneTouchPlayCommand() { return 0; } -int APS5_VABI sceSystemStateMgrStartRebootTimer() { +int APS5_VABI sceSystemStateMgrStartRebootTimer(std::int32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2405,7 +2405,7 @@ int APS5_VABI sceSystemStateMgrWaitVshAutoUpdateVerifyDone() { return 0; } -int APS5_VABI sceSystemStateMgrWakeUp() { +int APS5_VABI sceSystemStateMgrWakeUp(std::uint16_t trigger) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 5d427df1b0..88483669b9 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -141,3 +141,5 @@ - [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. GPU-crash fields and the VSH-ready selector meaning remain unresolved; collapsed assembly procedures are not missing exports. - [SystemService launch and telemetry arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): store-launch parameters contain only a four-byte size; web-app launch takes one string argument. Process lifecycle wrappers forward the verified LncUtil argument lists. TelemetrySetData takes a string key, binary payload and machine-sized length; TelemetrySetData2 takes two strings. Token storage consumes a 32-bit identifier and string; only its signature is restored, with no token values included. + +- [SystemStateMgr and modal signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): keep-main-on-standby takes a string, reboot timer consumes a 32-bit mode and wake-up consumes a 16-bit trigger. Suspend/resume progress forwards a string to kernel logging. Disc readiness, shell shutdown progress and standby enablement return booleans. Modal open takes a string and close takes a 32-bit dialog ID. Platform privacy setting writes a 32-bit value; the kernel platform query it calls consumes no caller arguments. From 9b8c8e51d5b8c34fa07678c5b35e80c29b3254dd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:46:19 +0300 Subject: [PATCH 0633/1360] fix(usbstorage): restore mapping callbacks and system service aliases --- .../libSceSystemService/PendingExports.cpp | 36 ++++++++++--------- core/libs/prx/libSceUsbStorage/Export.cpp | 24 +++++++------ docs/dev/debt/libSceSystemService.md | 2 ++ 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 7cb020b76e..536ab51996 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -59,6 +59,10 @@ struct LncAppStatus; struct LncShellUiAppStatus; struct AppMessagingMessage; +struct UsbStorageEvent; +struct UsbStorageDeviceInfo; +using UsbStorageCallback = void (APS5_VABI *)(const UsbStorageEvent* event, void* userData); + extern "C" { int APS5_VABI sceAppMessagingClearEventFlag(std::int32_t appId, std::uint64_t flags) { @@ -2195,67 +2199,67 @@ int APS5_VABI sceSystemServiceTurnOffScreenSaver() { return 0; } -int APS5_VABI sceSystemServiceUsbStorageGetDeviceInfo() { +int APS5_VABI sceSystemServiceUsbStorageGetDeviceInfo(std::uint32_t device, UsbStorageDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageGetDeviceList() { +int APS5_VABI sceSystemServiceUsbStorageGetDeviceList(std::uint32_t* devices, std::int32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageGetdentsClose() { +int APS5_VABI sceSystemServiceUsbStorageGetdentsClose(std::int32_t fd) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen() { +int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen(std::uint32_t device, std::int32_t* fd, std::uint64_t* bufferSize) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageGetdentsRead() { +int APS5_VABI sceSystemServiceUsbStorageGetdentsRead(std::int32_t fd, void* buffer, std::uint64_t bufferSize, std::uint64_t* bytesRead) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageInit() { +int APS5_VABI sceSystemServiceUsbStorageInit(void* attr) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageIsExist() { +bool APS5_VABI sceSystemServiceUsbStorageIsExist(std::uint32_t device, const char* directory, std::uint8_t* exists) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageRegisterCallback() { +int APS5_VABI sceSystemServiceUsbStorageRegisterCallback(std::int32_t eventType, UsbStorageCallback callback, void* arg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable() { +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable(std::uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable() { +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable(std::uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageRequestMap() { +int APS5_VABI sceSystemServiceUsbStorageRequestMap(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB() { +int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageRequestUnmap() { +int APS5_VABI sceSystemServiceUsbStorageRequestUnmap(std::uint32_t device, const char* directory) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2265,17 +2269,17 @@ int APS5_VABI sceSystemServiceUsbStorageTerm() { return 0; } -int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback() { +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback(std::int32_t eventType, UsbStorageCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable() { +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable(std::uint32_t device, const char* directory, UsbStorageCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable() { +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable(std::uint32_t device, const char* directory, UsbStorageCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index 0704602439..96c447c943 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -3,6 +3,10 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct UsbStorageEvent; +struct UsbStorageDeviceInfo; +using UsbStorageCallback = void (APS5_VABI *)(const UsbStorageEvent* event, void* userData); + extern "C" { int APS5_VABI sceUsbStorageInit(void* attr) { @@ -13,31 +17,31 @@ int APS5_VABI sceUsbStorageTerm(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageRegisterCallback(int32_t eventType, void* callback, void* arg) { +int APS5_VABI sceUsbStorageRegisterCallback(int32_t eventType, UsbStorageCallback callback, void* arg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageUnregisterCallback(int32_t eventType, void* callback) { +int APS5_VABI sceUsbStorageUnregisterCallback(int32_t eventType, UsbStorageCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageRegisterCallbackForMapAvailable(uint32_t device, const char* directory, void* callback, void* arg) { +int APS5_VABI sceUsbStorageRegisterCallbackForMapAvailable(uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageUnregisterCallbackForMapAvailable(uint32_t device, const char* directory, void* callback) { +int APS5_VABI sceUsbStorageUnregisterCallbackForMapAvailable(uint32_t device, const char* directory, UsbStorageCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageRegisterCallbackForMapUnavailable(uint32_t device, const char* directory, void* callback, void* arg) { +int APS5_VABI sceUsbStorageRegisterCallbackForMapUnavailable(uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageUnregisterCallbackForMapUnavailable(uint32_t device, const char* directory, void* callback) { +int APS5_VABI sceUsbStorageUnregisterCallbackForMapUnavailable(uint32_t device, const char* directory, UsbStorageCallback callback) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageGetDeviceInfo(uint32_t device, void* info) { +int APS5_VABI sceUsbStorageGetDeviceInfo(uint32_t device, UsbStorageDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -49,13 +53,11 @@ int APS5_VABI sceUsbStorageGetMountPointOfShellCore(uint32_t device, char* mount NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageIsExist(uint32_t device) { +bool APS5_VABI sceUsbStorageIsExist(uint32_t device, const char* directory, std::uint8_t* exists) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbStorageRequestMap(uint32_t device, const char* directory, int32_t flags, - uint64_t volumeSize, char* mountPoint, uint64_t* mountId, - const void* reserved, size_t reservedSize) { +int APS5_VABI sceUsbStorageRequestMap(uint32_t device, const char* directory, int32_t flags, uint64_t volumeSize, char* mountPoint, uint64_t* mountId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 88483669b9..cf7798a4ed 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -143,3 +143,5 @@ - [SystemService launch and telemetry arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): store-launch parameters contain only a four-byte size; web-app launch takes one string argument. Process lifecycle wrappers forward the verified LncUtil argument lists. TelemetrySetData takes a string key, binary payload and machine-sized length; TelemetrySetData2 takes two strings. Token storage consumes a 32-bit identifier and string; only its signature is restored, with no token values included. - [SystemStateMgr and modal signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): keep-main-on-standby takes a string, reboot timer consumes a 32-bit mode and wake-up consumes a 16-bit trigger. Suspend/resume progress forwards a string to kernel logging. Disc readiness, shell shutdown progress and standby enablement return booleans. Modal open takes a string and close takes a 32-bit dialog ID. Platform privacy setting writes a 32-bit value; the kernel platform query it calls consumes no caller arguments. + +- [USB storage aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) and [USB exports](../../../core/libs/prx/libSceUsbStorage/Export.cpp): direct-jump signatures are matched to the supplied USB module. RequestMap/RequestMapWSB take six arguments; the two extra helper arguments are internal. IsExist takes a device, directory and byte output, with a separate boolean failure result. Callbacks receive an event pointer and user data. Device information writes 824 bytes, device lists hold eight 32-bit IDs, and mount-point outputs occupy 128 bytes. Event/device field meanings and initialization attributes remain unresolved. From 28fdabd59cfcc5b9b5c89921d23b5037f87f71d3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:48:57 +0300 Subject: [PATCH 0634/1360] fix(systemservice): restore float progress returns and clock arguments --- .../libSceSystemService/PendingExports.cpp | 19 ++++++++++--------- docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 536ab51996..face484405 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -31,6 +31,7 @@ struct SystemServiceStoreParam { static_assert(sizeof(SystemServiceStoreParam) == 4); +struct SystemServiceCpuPstateParam; struct SystemServiceLocalProcessParam; struct SystemServiceJvmProcessParam; struct SystemServicePs2ProcessParam; @@ -890,7 +891,7 @@ int APS5_VABI sceShellCoreUtilGetIDUMode(std::uint8_t* mode) { return 0; } -int APS5_VABI sceShellCoreUtilGetImposeMenuFlagForPs2Emu() { +bool APS5_VABI sceShellCoreUtilGetImposeMenuFlagForPs2Emu(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -930,12 +931,12 @@ int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData(const char* return 0; } -int APS5_VABI sceShellCoreUtilGetProgressOfFormatExternalHdd() { +float APS5_VABI sceShellCoreUtilGetProgressOfFormatExternalHdd() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetProgressOfFsck() { +float APS5_VABI sceShellCoreUtilGetProgressOfFsck() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -995,7 +996,7 @@ int APS5_VABI sceShellCoreUtilGetSupportSiteURL(std::uint32_t first, std::uint32 return 0; } -int APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag(std::int32_t appId) { +bool APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1075,7 +1076,7 @@ int APS5_VABI sceShellCoreUtilIncrementVersionNumberOfCameraCalibrationData() { return 0; } -int APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked() { +bool APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1120,7 +1121,7 @@ bool APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { return 0; } -int APS5_VABI sceShellCoreUtilIsInSystemSuspendBlackList() { +bool APS5_VABI sceShellCoreUtilIsInSystemSuspendBlackList(std::uint32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1703,7 +1704,7 @@ int APS5_VABI sceSystemServiceChangeAcpClock() { return 0; } -int APS5_VABI sceSystemServiceChangeClock() { +int APS5_VABI sceSystemServiceChangeClock(const char* clock, std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1712,7 +1713,7 @@ int APS5_VABI sceSystemServiceChangeCpuClock() { return 0; } -int APS5_VABI sceSystemServiceChangeCpuPstate() { +int APS5_VABI sceSystemServiceChangeCpuPstate(const SystemServiceCpuPstateParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1721,7 +1722,7 @@ int APS5_VABI sceSystemServiceChangeGpuClock() { return 0; } -int APS5_VABI sceSystemServiceChangeMemPstate() { +int APS5_VABI sceSystemServiceChangeMemPstate(std::uint32_t state) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index cf7798a4ed..5206f219ab 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -145,3 +145,5 @@ - [SystemStateMgr and modal signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): keep-main-on-standby takes a string, reboot timer consumes a 32-bit mode and wake-up consumes a 16-bit trigger. Suspend/resume progress forwards a string to kernel logging. Disc readiness, shell shutdown progress and standby enablement return booleans. Modal open takes a string and close takes a 32-bit dialog ID. Platform privacy setting writes a 32-bit value; the kernel platform query it calls consumes no caller arguments. - [USB storage aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) and [USB exports](../../../core/libs/prx/libSceUsbStorage/Export.cpp): direct-jump signatures are matched to the supplied USB module. RequestMap/RequestMapWSB take six arguments; the two extra helper arguments are internal. IsExist takes a device, directory and byte output, with a separate boolean failure result. Callbacks receive an event pointer and user data. Device information writes 824 bytes, device lists hold eight 32-bit IDs, and mount-point outputs occupy 128 bytes. Event/device field meanings and initialization attributes remain unresolved. + +- [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. From a8cb8345cd56b46d29e63c7a841431a6d901d34f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:57:21 +0300 Subject: [PATCH 0635/1360] fix(systemservice): validate system name buffers and termination --- core/libs/prx/libSceSystemService/Export.cpp | 1 + core/libs/tests/GuestSystemService.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 6c37c6863d..a923164119 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -323,6 +323,7 @@ int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, size_t bufS constexpr char SystemName[] = "PS5"; std::memset(buf, 0, bufSize); std::memcpy(buf, SystemName, sizeof(SystemName)); + buf[SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH - 1] = '\0'; return SYSTEM_SERVICE_OK; } diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 113cb6aa2d..3913b7dc1c 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -3,6 +3,7 @@ #include <cstddef> #include <cstdlib> #include <cstring> +#include <initializer_list> #include <stdexcept> extern "C" int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance); From 9e94b0cb712f1dd2109ffed769fbc1bd134e595c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:58:20 +0300 Subject: [PATCH 0636/1360] fix(systemservice): define launch and microphone status layouts --- .../libSceSystemService/PendingExports.cpp | 28 ++++++++++++++----- docs/dev/debt/libSceSystemService.md | 6 ++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index face484405..d17afe85ce 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -37,9 +37,23 @@ struct SystemServiceJvmProcessParam; struct SystemServicePs2ProcessParam; struct SystemServiceAppInstallStatus; struct SystemServiceAppStatus; -struct ShellCoreLaunchByUriParam; +struct ShellCoreLaunchByUriParam { + std::uint32_t option; + std::int32_t userId; +}; + +static_assert(sizeof(ShellCoreLaunchByUriParam) == 8); +static_assert(offsetof(ShellCoreLaunchByUriParam, option) == 0); +static_assert(offsetof(ShellCoreLaunchByUriParam, userId) == 4); struct ShellCoreTelemetryToken; -struct ShellCoreUmmStatus; +struct ShellCoreUmmStatus { + std::int32_t state; + std::int32_t inDeviceMuteState; +}; + +static_assert(sizeof(ShellCoreUmmStatus) == 8); +static_assert(offsetof(ShellCoreUmmStatus, state) == 0); +static_assert(offsetof(ShellCoreUmmStatus, inDeviceMuteState) == 4); struct ShellCorePbtcUserInfoList; struct ShellCorePrivacyEventData; struct ShellCorePsnAccountInfoFirst; @@ -836,7 +850,7 @@ std::uint64_t APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { return 0; } -int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition(std::uint64_t* firstSize, std::uint64_t* secondSize) { +int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition(std::uint64_t* freeSize, std::uint64_t* totalSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -881,7 +895,7 @@ int APS5_VABI sceShellCoreUtilGetHidConfigName(char* value, std::size_t capacity return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigNum(std::uint32_t* count) { +int APS5_VABI sceShellCoreUtilGetHidConfigNum(std::int32_t* configNum) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1036,7 +1050,7 @@ int APS5_VABI sceShellCoreUtilGetUIStatus(std::uint64_t* status) { return 0; } -int APS5_VABI sceShellCoreUtilGetUmmStatus(std::uint32_t id, ShellCoreUmmStatus* status) { +int APS5_VABI sceShellCoreUtilGetUmmStatus(std::int32_t userId, ShellCoreUmmStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1066,7 +1080,7 @@ int APS5_VABI sceShellCoreUtilGoBackToKratosCurrentSessionGame() { return 0; } -int APS5_VABI sceShellCoreUtilHideBlocksFromUser(std::uint64_t first, std::int32_t second) { +int APS5_VABI sceShellCoreUtilHideBlocksFromUser(std::int64_t bytes, std::int32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1276,7 +1290,7 @@ int APS5_VABI sceShellCoreUtilPostActivityForPsNow(std::int32_t type, const char return 0; } -int APS5_VABI sceShellCoreUtilPostErrorLog(const char* firstText, std::uint32_t firstValue, const char* secondText, const char* thirdText, std::uint32_t secondValue, const char* fourthText) { +int APS5_VABI sceShellCoreUtilPostErrorLog(const char* titleId, std::int32_t internalErrorCode, const char* shortError, const char* messageBuffer, std::int32_t suggestionType, const char* logReport) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 5206f219ab..20a5299373 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -122,11 +122,11 @@ - [ShellCoreUtil account and privacy payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Pbtc user information occupies 800 bytes; privacy-event data occupies eight bytes after two string keys, each limited to 32 characters. The five account-information outputs occupy 8, 104, 136, 1024 and 1032 bytes; their fields remain unresolved. Category-default list output has a byte capacity and unresolved element format. Remote-play/share-play/social-screen outputs are boolean; application display-state queries take a 32-bit application ID. Only the support-site query signature is restored; no endpoint content is included. -- [ShellCoreUtil telemetry and user queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): telemetry-token output is 4112 bytes, including an eight-byte service result at offset 4104; its meaning remains unresolved. UMM status is an eight-byte output with unresolved fields. UI status is a 64-bit event mask; VM statistics and workaround-file strings use machine-sized capacities. HideBlocksFromUser transports a 64-bit value and a 32-bit value whose exact semantics remain unresolved. Eye-distance adjustment writes one byte. +- [ShellCoreUtil telemetry and user queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): telemetry-token output is 4112 bytes, including an eight-byte service result at offset 4104; its meaning remains unresolved. UI status is a 64-bit event mask; VM statistics and workaround-file strings use machine-sized capacities. Eye-distance adjustment writes one byte. -- [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): LaunchByUri reads an eight-byte parameter containing two 32-bit fields and swaps their order in its message; field meanings remain unresolved. Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. +- [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. -- [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. Error-log submission consumes four strings and two 32-bit values in the restored order; their semantic names remain unresolved. PSM events carry a 32-bit event ID and a string. IME notification consumes a 32-bit value and one byte; service meanings remain unresolved. +- [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. PSM events carry a 32-bit event ID and a string. IME notification consumes a 32-bit value and one byte; service meanings remain unresolved. - [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. Device-index field meanings remain unresolved. From 1db52e3e4091719c9b6102e8e9ccaad16b14dd75 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 18:59:59 +0300 Subject: [PATCH 0637/1360] fix(systemservice): reject unsupported integer parameters --- core/libs/prx/libSceSystemService/Export.cpp | 6 ++++- .../libs/tests/GuestSystemServiceLanguage.cpp | 23 +++++++++++++++++++ docs/dev/debt/libSceSystemService.md | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index a923164119..d0b5a7623b 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -311,7 +311,11 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; case SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL: *value = SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF; break; case SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN: *value = SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS; break; - default: *value = 0; break; + default: + if ((paramId >= 100 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { + NotImplemented_nid_no_patch("sceSystemServiceParamGetInt: registry-backed parameter"); + } + return SYSTEM_SERVICE_ERROR_PARAMETER; } return SYSTEM_SERVICE_OK; } diff --git a/core/libs/tests/GuestSystemServiceLanguage.cpp b/core/libs/tests/GuestSystemServiceLanguage.cpp index efd37f499b..cf65b0d3c8 100644 --- a/core/libs/tests/GuestSystemServiceLanguage.cpp +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libSceSystemService/SystemService.hpp" #include <cstdlib> +#include <limits> #include <stdexcept> #include <string> @@ -45,6 +46,28 @@ bool Rejected(const char* value) { } int main() { + for (int paramId = -1; paramId <= 1001; ++paramId) { + Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + if ((paramId >= 1 && paramId <= 5) || paramId == 7 || paramId == 400 || paramId == 1000) continue; + int value = -12345; + if ((paramId >= 100 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { + bool threw = false; + try { + static_cast<void>(sceSystemServiceParamGetInt(paramId, &value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } else { + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + Require(value == -12345); + } + for (int paramId : {std::numeric_limits<int>::min(), std::numeric_limits<int>::max()}) { + int value = -12345; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(value == -12345); + } SetLanguage(nullptr); Require(Language() == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US); SetLanguage(""); diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 20a5299373..0033dc629e 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -147,3 +147,5 @@ - [USB storage aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) and [USB exports](../../../core/libs/prx/libSceUsbStorage/Export.cpp): direct-jump signatures are matched to the supplied USB module. RequestMap/RequestMapWSB take six arguments; the two extra helper arguments are internal. IsExist takes a device, directory and byte output, with a separate boolean failure result. Callbacks receive an event pointer and user data. Device information writes 824 bytes, device lists hold eight 32-bit IDs, and mount-point outputs occupy 128 bytes. Event/device field meanings and initialization attributes remain unresolved. - [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. + +- [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 100–111, 200–213 and 300 throw because their settings are not modeled. Language results omit the console SDK-version compatibility remapping, and parameters 7, 400 and 1000 do not reproduce platform-dependent availability. From 67ff7609c284a87b9abeb14f4b7ecf78d284142b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:00:52 +0300 Subject: [PATCH 0638/1360] fix(systemservice): define application message payload layout --- .../prx/libSceSystemService/PendingExports.cpp | 15 ++++++++++++++- docs/dev/debt/libSceSystemService.md | 1 - 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index d17afe85ce..c53f5c4acd 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -72,7 +72,20 @@ struct LncVideoFinalizeState; struct LncLocalProcessParam; struct LncAppStatus; struct LncShellUiAppStatus; -struct AppMessagingMessage; +struct AppMessagingMessage { + std::int32_t sender; + std::uint32_t messageType; + std::uint8_t payload[8192]; + std::uint32_t payloadSize; + std::uint64_t timestamp; +}; + +static_assert(sizeof(AppMessagingMessage) == 8216); +static_assert(offsetof(AppMessagingMessage, sender) == 0); +static_assert(offsetof(AppMessagingMessage, messageType) == 4); +static_assert(offsetof(AppMessagingMessage, payload) == 8); +static_assert(offsetof(AppMessagingMessage, payloadSize) == 8200); +static_assert(offsetof(AppMessagingMessage, timestamp) == 8208); struct UsbStorageEvent; struct UsbStorageDeviceInfo; diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 0033dc629e..3362d201bf 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -106,7 +106,6 @@ - [sceLncUtilKillApp, sceLncUtilKillAppWithReason](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the running title's app ID ends the title as `sceSystemServiceKillApp` does, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L219-L227) describes as a wrapper of `sceLncUtilKillApp`; the signatures follow its [launcher utility bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L262-L279). Another app ID throws, and so does a reason other than 0, whose meaning and effect on the console are not documented - [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2, and on macOS with `posix_spawn`, which closes them through `POSIX_SPAWN_CLOEXEC_DEFAULT`; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known -- [AppMessaging receive buffers](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the supplied module confirms the eight messaging argument lists, 64-bit event masks, byte-sized clear flag and 8192-byte send limit. Receive writes an 8216-byte message whose field meanings still require the sender/service handler. The protected bodies remain unchanged. - [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block; application-status entries occupy 64 bytes and shell-UI reboot entries 1336 bytes. Their fields remain unresolved. Title-ID output occupies 16 bytes; category, category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. From 50c8103af21f9b0ff2a1720aa4c0527ee5036be5 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:03:00 +0300 Subject: [PATCH 0639/1360] fix(systemservice): recover launch and process status structures --- .../libSceSystemService/PendingExports.cpp | 145 ++++++++++++++++-- docs/dev/debt/libSceSystemService.md | 8 +- 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index c53f5c4acd..64e265492b 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -62,16 +62,139 @@ struct ShellCorePsnAccountInfoThird; struct ShellCorePsnAccountInfoFourth; struct ShellCorePsnAccountInfoFifth; struct ShellCoreAppLaunchTypeInfo; -struct LncGpuCrashAppStatus; -struct LncLaunchParam; +struct LncGpuCrashAppStatus { + char titleId[16]; + std::int32_t appId; + std::int32_t appLocalPid; + char path[1024]; + std::int32_t appType; + std::int32_t coredumpReason; + char corefileName[256]; + std::uint32_t exceptionCode; + std::uint32_t appCategoryType; +}; + +static_assert(sizeof(LncGpuCrashAppStatus) == 1320); +static_assert(offsetof(LncGpuCrashAppStatus, titleId) == 0); +static_assert(offsetof(LncGpuCrashAppStatus, appId) == 16); +static_assert(offsetof(LncGpuCrashAppStatus, appLocalPid) == 20); +static_assert(offsetof(LncGpuCrashAppStatus, path) == 24); +static_assert(offsetof(LncGpuCrashAppStatus, appType) == 1048); +static_assert(offsetof(LncGpuCrashAppStatus, coredumpReason) == 1052); +static_assert(offsetof(LncGpuCrashAppStatus, corefileName) == 1056); +static_assert(offsetof(LncGpuCrashAppStatus, exceptionCode) == 1312); +static_assert(offsetof(LncGpuCrashAppStatus, appCategoryType) == 1316); +struct LncLaunchParam { + std::uint32_t size; + std::int32_t userId; + std::int32_t appAttr; + std::int32_t enableCrashReport; + std::uint64_t checkFlag; + std::uint64_t contextId; + bool isSpeculativeLaunch; +}; + +static_assert(sizeof(LncLaunchParam) == 40); +static_assert(offsetof(LncLaunchParam, size) == 0); +static_assert(offsetof(LncLaunchParam, userId) == 4); +static_assert(offsetof(LncLaunchParam, appAttr) == 8); +static_assert(offsetof(LncLaunchParam, enableCrashReport) == 12); +static_assert(offsetof(LncLaunchParam, checkFlag) == 16); +static_assert(offsetof(LncLaunchParam, contextId) == 24); +static_assert(offsetof(LncLaunchParam, isSpeculativeLaunch) == 32); struct LncDaemonEvent; struct LncShellUiEvent; -struct LncLocalProcessStatus; -struct LncCoredumpState; -struct LncVideoFinalizeState; +struct LncLocalProcessStatus { + std::int32_t appLocalPid; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(LncLocalProcessStatus) == 36); +static_assert(offsetof(LncLocalProcessStatus, appLocalPid) == 0); +static_assert(offsetof(LncLocalProcessStatus, reserved) == 4); +struct LncCoredumpState { + std::uint32_t progress; + std::uint32_t isFinished; + std::int32_t result; +}; + +static_assert(sizeof(LncCoredumpState) == 12); +static_assert(offsetof(LncCoredumpState, progress) == 0); +static_assert(offsetof(LncCoredumpState, isFinished) == 4); +static_assert(offsetof(LncCoredumpState, result) == 8); +struct LncVideoFinalizeState { + std::uint32_t progress; + std::int32_t result; +}; + +static_assert(sizeof(LncVideoFinalizeState) == 8); +static_assert(offsetof(LncVideoFinalizeState, progress) == 0); +static_assert(offsetof(LncVideoFinalizeState, result) == 4); struct LncLocalProcessParam; -struct LncAppStatus; -struct LncShellUiAppStatus; +struct LncAppStatus { + std::int32_t appId; + std::int32_t launchRequestAppId; + std::int32_t appType; + std::int32_t appAttr; + char titleId[16]; + std::uint32_t appCategoryType; + std::int32_t appBootMode; + std::int32_t appBootSubMode; + char appVer[6]; + char contentVersion[11]; + bool isSystemExApp; + bool isCdlgInitialized; +}; + +static_assert(sizeof(LncAppStatus) == 64); +static_assert(offsetof(LncAppStatus, appId) == 0); +static_assert(offsetof(LncAppStatus, launchRequestAppId) == 4); +static_assert(offsetof(LncAppStatus, appType) == 8); +static_assert(offsetof(LncAppStatus, appAttr) == 12); +static_assert(offsetof(LncAppStatus, titleId) == 16); +static_assert(offsetof(LncAppStatus, appCategoryType) == 32); +static_assert(offsetof(LncAppStatus, appBootMode) == 36); +static_assert(offsetof(LncAppStatus, appBootSubMode) == 40); +static_assert(offsetof(LncAppStatus, appVer) == 44); +static_assert(offsetof(LncAppStatus, contentVersion) == 50); +static_assert(offsetof(LncAppStatus, isSystemExApp) == 61); +static_assert(offsetof(LncAppStatus, isCdlgInitialized) == 62); +struct LncShellUiAppStatus { + std::int32_t appId; + std::int32_t appType; + std::int32_t appAttr; + std::int32_t launchRequestAppId; + std::int32_t userId; + std::int32_t isActiveCdlg; + char path[1024]; + char sceSysDir[256]; + std::int32_t isCoredumped; + std::int32_t isPrxModuleLoadFailed; + std::int32_t appLocalPid; + std::int32_t crashReportMode; + char category[4]; + std::uint32_t appCategoryType; + std::int32_t appBootMode; + std::int32_t appBootSubMode; +}; + +static_assert(sizeof(LncShellUiAppStatus) == 1336); +static_assert(offsetof(LncShellUiAppStatus, appId) == 0); +static_assert(offsetof(LncShellUiAppStatus, appType) == 4); +static_assert(offsetof(LncShellUiAppStatus, appAttr) == 8); +static_assert(offsetof(LncShellUiAppStatus, launchRequestAppId) == 12); +static_assert(offsetof(LncShellUiAppStatus, userId) == 16); +static_assert(offsetof(LncShellUiAppStatus, isActiveCdlg) == 20); +static_assert(offsetof(LncShellUiAppStatus, path) == 24); +static_assert(offsetof(LncShellUiAppStatus, sceSysDir) == 1048); +static_assert(offsetof(LncShellUiAppStatus, isCoredumped) == 1304); +static_assert(offsetof(LncShellUiAppStatus, isPrxModuleLoadFailed) == 1308); +static_assert(offsetof(LncShellUiAppStatus, appLocalPid) == 1312); +static_assert(offsetof(LncShellUiAppStatus, crashReportMode) == 1316); +static_assert(offsetof(LncShellUiAppStatus, category) == 1320); +static_assert(offsetof(LncShellUiAppStatus, appCategoryType) == 1324); +static_assert(offsetof(LncShellUiAppStatus, appBootMode) == 1328); +static_assert(offsetof(LncShellUiAppStatus, appBootSubMode) == 1332); struct AppMessagingMessage { std::int32_t sender; std::uint32_t messageType; @@ -168,7 +291,7 @@ int APS5_VABI sceLncUtilBlockingGetEventForDaemon() { return 0; } -int APS5_VABI sceLncUtilContinueApp(std::uint32_t appId) { +int APS5_VABI sceLncUtilContinueApp(std::int32_t appId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -213,7 +336,7 @@ int APS5_VABI sceLncUtilForceKillLocalProcess(std::int32_t appId, std::int32_t p return 0; } -int APS5_VABI sceLncUtilGetAppCategory(std::int32_t appId, std::uint32_t* category) { +int APS5_VABI sceLncUtilGetAppCategory(std::int32_t appId, char* category) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -413,7 +536,7 @@ int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress(std::int32_t appId, std::i return 0; } -int APS5_VABI sceLncUtilNotifyVshReady(std::uint32_t value) { +int APS5_VABI sceLncUtilNotifyVshReady(std::uint32_t errorCode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -468,7 +591,7 @@ int APS5_VABI sceLncUtilResumeLocalProcess(std::int32_t processId) { return 0; } -int APS5_VABI sceLncUtilSetAppFocus(std::int32_t appId, std::uint64_t first, std::uint64_t second) { +int APS5_VABI sceLncUtilSetAppFocus(std::int32_t appId, std::uint64_t flags, std::uint64_t contextId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3362d201bf..d0eccf7889 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -107,11 +107,11 @@ - [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2, and on macOS with `posix_spawn`, which closes them through `POSIX_SPAWN_CLOEXEC_DEFAULT`; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known -- [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block; application-status entries occupy 64 bytes and shell-UI reboot entries 1336 bytes. Their fields remain unresolved. Title-ID output occupies 16 bytes; category, category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. +- [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block whose fields remain unresolved. Title-ID output occupies 16 bytes; category output is a four-byte string, while category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. -- [LncUtil event and diagnostic payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process status entries are 36 bytes, coredump state 12 bytes and video-finalization state eight bytes; their field meanings remain unresolved. Daemon and shell-UI events are distinct output structures. GetDbgExecutablePath writes up to a 1024-byte protocol output and forwards a 64-bit selector whose meaning is unresolved. Boolean outputs and the two direct boolean budget/mode queries are confirmed. +- [LncUtil event and diagnostic payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Daemon and shell-UI events are distinct output structures. GetDbgExecutablePath writes up to a 1024-byte protocol output and forwards a 64-bit selector whose meaning is unresolved. Boolean outputs and the two direct boolean budget/mode queries are confirmed. -- [LncUtil launch and lifecycle payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 40-byte launch parameter fields and meanings of several 64-bit focus/resume/suspend/exception values remain unresolved. Launch and load-exec use null-terminated argument vectors; asynchronous launch/kill write a 32-bit request ID. The argument positions and transported widths are confirmed, but the service operations remain protected stubs. +- [LncUtil launch and lifecycle payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Flag values and service-side focus/resume/suspend/exception behavior remain unresolved. Launch and load-exec use null-terminated argument vectors; asynchronous launch/kill write a 32-bit request ID. The argument positions and transported widths are confirmed, but the service operations remain protected stubs. - [ShellCoreUtil application data](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): GetAppLaunchTypeInfo writes eight bytes whose fields are unresolved. Application-data and launched-parameter keys are null-terminated strings, with 32-bit capacities/outputs. Format/fsck paths and mini-app return values are strings. File-formatting and service implementations remain protected stubs. @@ -137,7 +137,7 @@ - [SystemService direct aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly-only jumps now share the confirmed signatures of their LncUtil/ShellCoreUtil targets for application-ID lists, remote-play/UI status, navigation, controller-focus permissions and boolean budget/status queries. Aliases of unresolved targets still require target or caller analysis. -- [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. GPU-crash fields and the VSH-ready selector meaning remain unresolved; collapsed assembly procedures are not missing exports. +- [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. The protected service implementations remain unchanged; collapsed assembly procedures are not missing exports. - [SystemService launch and telemetry arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): store-launch parameters contain only a four-byte size; web-app launch takes one string argument. Process lifecycle wrappers forward the verified LncUtil argument lists. TelemetrySetData takes a string key, binary payload and machine-sized length; TelemetrySetData2 takes two strings. Token storage consumes a 32-bit identifier and string; only its signature is restored, with no token values included. From b167045830c954df38a1c790c47d8347f3906ce0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:06:25 +0300 Subject: [PATCH 0640/1360] fix(systemservice): recover activation handles and device argument types --- .../libSceSystemService/PendingExports.cpp | 36 +++++++++---------- docs/dev/debt/libSceSystemService.md | 12 +++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 64e265492b..9841ee91cf 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -691,21 +691,21 @@ int APS5_VABI sceShellCoreUtilAcquireSharePlayCpuBudget() { return 0; } -int APS5_VABI sceShellCoreUtilActivateAbort() { +int APS5_VABI sceShellCoreUtilActivateAbort(std::intptr_t handle) { return 0; } -int APS5_VABI sceShellCoreUtilActivateGetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { +int APS5_VABI sceShellCoreUtilActivateGetStatus(std::intptr_t handle, std::int32_t* status, std::int32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilActivateInit(std::uint32_t type, std::uintptr_t* handle) { +int APS5_VABI sceShellCoreUtilActivateInit(std::int32_t type, std::intptr_t* handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilActivateIsActivated(std::uint32_t type, std::uint8_t* activated) { +int APS5_VABI sceShellCoreUtilActivateIsActivated(std::intptr_t handle, bool* activated) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -714,15 +714,15 @@ int APS5_VABI sceShellCoreUtilActivateRecordActivation() { return 0; } -int APS5_VABI sceShellCoreUtilActivateStart() { +int APS5_VABI sceShellCoreUtilActivateStart(std::intptr_t handle) { return 0; } -int APS5_VABI sceShellCoreUtilActivateStartAsync() { +int APS5_VABI sceShellCoreUtilActivateStartAsync(std::intptr_t handle) { return 0; } -int APS5_VABI sceShellCoreUtilActivateTerm() { +int APS5_VABI sceShellCoreUtilActivateTerm(std::intptr_t handle) { return 0; } @@ -781,7 +781,7 @@ int APS5_VABI sceShellCoreUtilDeleteDiscInstalledTitleWorkaroundFile() { return 0; } -int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile(std::uint32_t deviceType) { +int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile(std::int32_t configIndex) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -801,7 +801,7 @@ int APS5_VABI sceShellCoreUtilDoFsck(const char* path) { return 0; } -int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer(std::uint32_t deviceType) { +int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer(std::int32_t configIndex) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -966,12 +966,12 @@ int APS5_VABI sceShellCoreUtilGetCrashReportUploadStatus() { return 0; } -int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior(std::uint32_t* first, std::uint32_t* second, std::uint32_t* third) { +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior(std::uint32_t* behavior, std::int32_t* userId, std::uint32_t* scene) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout(std::uint32_t* first, std::uint32_t* second, std::uint32_t* third, std::uint8_t timeout) { +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout(std::uint32_t* behavior, std::int32_t* userId, std::uint32_t* scene, std::uint8_t timeout) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -991,7 +991,7 @@ int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition(std::uint64_t* freeSize return 0; } -int APS5_VABI sceShellCoreUtilGetFsckProgress(const char* path, std::uint32_t* progress) { +int APS5_VABI sceShellCoreUtilGetFsckProgress(const char* titleId, std::uint32_t* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1016,17 +1016,17 @@ int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId(std::int32_t appId) return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString(char* value, std::size_t capacity, std::int32_t index) { +int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString(char* value, std::size_t capacity, std::int32_t configIndex) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigFileString(char* value, std::size_t capacity, std::int32_t index) { +int APS5_VABI sceShellCoreUtilGetHidConfigFileString(char* value, std::size_t capacity, std::int32_t configIndex) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellCoreUtilGetHidConfigName(char* value, std::size_t capacity, std::int32_t index) { +int APS5_VABI sceShellCoreUtilGetHidConfigName(char* value, std::size_t capacity, std::int32_t configIndex) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1401,7 +1401,7 @@ int APS5_VABI sceShellCoreUtilNotifyFsReadError(std::int32_t first, std::int32_t return 0; } -int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged(std::int32_t appId, std::uint8_t status) { +int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged(std::int32_t imeStatus, std::uint8_t isCalledFromGame) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1521,7 +1521,7 @@ int APS5_VABI sceShellCoreUtilSetBgmProhibition(std::uint32_t value) { return 0; } -int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior(std::uint8_t first, std::uint32_t second, std::uint16_t third) { +int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior(std::uint32_t behavior, std::int32_t userId, std::uint8_t scene) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1661,7 +1661,7 @@ int APS5_VABI sceShellCoreUtilStopPsNowGame(const char* data) { return 0; } -int APS5_VABI sceShellCoreUtilTalkKeyEventReceived(std::uint64_t first, std::uint32_t second, std::uint32_t third) { +int APS5_VABI sceShellCoreUtilTalkKeyEventReceived(std::int64_t deviceId, std::int32_t userId, std::int32_t keyEventType) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index d0eccf7889..8d9548f41d 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -115,9 +115,9 @@ - [ShellCoreUtil application data](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): GetAppLaunchTypeInfo writes eight bytes whose fields are unresolved. Application-data and launched-parameter keys are null-terminated strings, with 32-bit capacities/outputs. Format/fsck paths and mini-app return values are strings. File-formatting and service implementations remain protected stubs. -- [ShellCoreUtil queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): launched-parameter strings have signed 32-bit capacities; checker strings use machine-sized capacities and one or two string keys. Cloud status and device-index outputs are 32-bit scalars; the timeout variant consumes one byte. Partition-size output and the direct temporary-content free-size return are 64-bit. Device-index field meanings and the service-side checker protocol remain unresolved. +- [ShellCoreUtil queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): launched-parameter strings have signed 32-bit capacities; checker strings use machine-sized capacities and one or two string keys. Cloud status and device-index outputs are 32-bit scalars; the timeout variant consumes one byte. Partition-size output and the direct temporary-content free-size return are 64-bit. The service-side checker protocol remains unresolved. Device-index scene output uses a 32-bit native store despite the managed wrapper using a byte-backed scene enum; the native output width is preserved. -- [ShellCoreUtil storage and HID queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): partition free sizes and required application-content size are 64-bit outputs; fsck takes a path and a 32-bit progress output. HID string queries transport a 64-bit capacity and 32-bit selector, while output-vector lengths sign-extend the capacity's low 32 bits. The selector meaning and IDU mode byte values remain unresolved. Streaming status and the compositor-profiler return are boolean; the compositor-profiler and current GPU-load queries consume no caller arguments. +- [ShellCoreUtil storage and HID queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): partition free sizes and required application-content size are 64-bit outputs; fsck progress takes a title ID and a 32-bit progress output. HID string queries transport a 64-bit capacity and 32-bit selector, while output-vector lengths sign-extend the capacity's low 32 bits. HID selectors are configuration indices; the wrapper declares 32-bit capacities while the native code transports 64 bits, so their declared width remains unresolved. IDU mode byte values remain unresolved. Streaming status and the compositor-profiler return are boolean; the compositor-profiler and current GPU-load queries consume no caller arguments. - [ShellCoreUtil account and privacy payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Pbtc user information occupies 800 bytes; privacy-event data occupies eight bytes after two string keys, each limited to 32 characters. The five account-information outputs occupy 8, 104, 136, 1024 and 1032 bytes; their fields remain unresolved. Category-default list output has a byte capacity and unresolved element format. Remote-play/share-play/social-screen outputs are boolean; application display-state queries take a 32-bit application ID. Only the support-site query signature is restored; no endpoint content is included. @@ -125,13 +125,13 @@ - [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. -- [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. PSM events carry a 32-bit event ID and a string. IME notification consumes a 32-bit value and one byte; service meanings remain unresolved. +- [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. PSM events carry a 32-bit event ID and a string. -- [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. Device-index field meanings remain unresolved. +- [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. -- [ShellCoreUtil dialog parameters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 24-byte error-dialog parameter contains a message pointer at offset zero, a null-terminated vector of string arguments at offset eight and a byte flag at offset 16. Field offsets and size are asserted; the flag meaning remains unresolved. Start/stop streaming-game and unmount requests take strings. Talk-key events transport a 64-bit value and two 32-bit values whose meanings remain unresolved. Bus-transfer testing writes one byte after a string input. +- [ShellCoreUtil dialog parameters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 24-byte error-dialog parameter contains a message pointer at offset zero, a null-terminated vector of string arguments at offset eight and a byte flag at offset 16. Field offsets and size are asserted; the flag meaning remains unresolved. Start/stop streaming-game and unmount requests take strings. Bus-transfer testing writes one byte after a string input. -- [SystemService activation and process signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): activation wrappers validate a machine-sized handle and expose 32-bit status/result outputs or a byte activation flag. Shell activation helpers also receive a type selector; constant-return activation exports still need caller evidence for unused parameters. SystemService local-process parameters differ from LncUtil parameters and are kept as distinct types; general creation reads through offset 71, JVM/PS2 creation reads two 32-bit fields at offsets four and eight. Install status writes 12 bytes and application status writes 16 bytes; their remaining field meanings are unresolved. +- [SystemService activation and process signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): activation wrappers validate a machine-sized handle and expose 32-bit status/result outputs or a byte activation flag. Shell activation initialization also receives a type selector; managed callers confirm the machine-sized handle consumed by constant-return start/abort/terminate exports. SystemService local-process parameters differ from LncUtil parameters and are kept as distinct types; general creation reads through offset 71, JVM/PS2 creation reads two 32-bit fields at offsets four and eight. Install status writes 12 bytes and application status writes 16 bytes; their remaining field meanings are unresolved. - [SystemService query payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): daemon events write 12 bytes; local-process entries occupy 36 bytes, consisting of a 32-bit value and 32-byte name. PS-button extraction reads a 16-byte event and rearranges its fields into a distinct 16-byte output; semantic names remain unresolved. Privacy definition/version queries take string keys and distinct output types; their layouts remain unresolved. Workaround information writes four bytes at offset zero and 16 bytes at offset eight. From abe9c62055be0f6177dbcd53987d2f6ee7f6c24e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:12:47 +0300 Subject: [PATCH 0641/1360] fix(contentexport): restore file and content operation signatures --- core/libs/SceTypes.hpp | 28 ++++++++++++++++ core/libs/prx/libSceContentExport/Export.cpp | 25 +++++++------- core/libs/tests/GuestContentExport.cpp | 35 ++++++++++---------- docs/dev/debt/libSceContentExport.md | 3 +- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 4f8d8e8fd7..f7b002c4d9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1690,6 +1690,34 @@ struct AppContentBootParam { struct AppContentMountPoint { char data[16]; }; +using ContentExportMallocCallback = void* (APS5_VABI *)(std::size_t size, void* userData); +using ContentExportFreeCallback = void (APS5_VABI *)(void* memory, void* userData); +using ContentExportDataCallback = std::int32_t (APS5_VABI *)(const void** data, std::size_t* size, void* userData); + +struct ContentExportInitParam { + ContentExportMallocCallback mallocFunc; + ContentExportFreeCallback freeFunc; + void* userData; +}; + +static_assert(sizeof(ContentExportInitParam) == 24); +static_assert(offsetof(ContentExportInitParam, freeFunc) == 8); +static_assert(offsetof(ContentExportInitParam, userData) == 16); + +struct ContentExportSfParam { + std::size_t size; + const char* metadata; + const std::int64_t* contentIds; + std::uint32_t contentCount; + std::uint32_t option; +}; + +static_assert(sizeof(ContentExportSfParam) == 32); +static_assert(offsetof(ContentExportSfParam, metadata) == 8); +static_assert(offsetof(ContentExportSfParam, contentIds) == 16); +static_assert(offsetof(ContentExportSfParam, contentCount) == 24); +static_assert(offsetof(ContentExportSfParam, option) == 28); + struct ContentExportInitParam2 { void* malloc_func; void* free_func; diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index b8a366cc65..eb87ef6971 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -40,22 +40,22 @@ int APS5_VABI sceContentExportFinish(std::int32_t expId) { return 0; } -int APS5_VABI sceContentExportFromData(void) { +int APS5_VABI sceContentExportFromData(std::int32_t expId, const ContentExportParam* param, std::size_t contentLength, ContentExportDataCallback callback, void* userData, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportFromFile(void) { +int APS5_VABI sceContentExportFromFile(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportFromFileWithThumbnail(void) { +int APS5_VABI sceContentExportFromFileWithThumbnail(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportAddToFavorites(void) { +int APS5_VABI sceContentExportAddToFavorites(std::int64_t contentId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -75,22 +75,22 @@ int APS5_VABI sceContentExportFrom(void) { return 0; } -int APS5_VABI sceContentExportFromFileForSf(void) { +int APS5_VABI sceContentExportFromFileForSf(std::int32_t expId, const ContentExportParam* param, const ContentExportSfParam* sfParam, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportFromFileWithContentIdList(void) { +int APS5_VABI sceContentExportFromFileWithContentIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const std::int64_t* contentIds, std::uint32_t contentCount, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportFromFileWithTitleIdList(void) { +int APS5_VABI sceContentExportFromFileWithTitleIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const char* titleIdList, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportInit(void) { +int APS5_VABI sceContentExportInit(const ContentExportInitParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -100,17 +100,17 @@ int APS5_VABI sceContentExportMalloc(void) { return 0; } -int APS5_VABI sceContentExportRemoveFromFavorites(void) { +int APS5_VABI sceContentExportRemoveFromFavorites(std::int64_t contentId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportRenameContent(void) { +int APS5_VABI sceContentExportRenameContent(std::int64_t contentId, const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportValidateContents(void) { +int APS5_VABI sceContentExportValidateContents(const std::int64_t* contentIds, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -136,8 +136,7 @@ int APS5_VABI sceContentExportCancel(std::int32_t expId) { throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); } -int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, - const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { +int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 40c6aa941d..21229622e9 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -4,18 +4,18 @@ #include <set> #include <stdexcept> -extern "C" int APS5_VABI sceContentExportAddToFavorites(void); +extern "C" int APS5_VABI sceContentExportAddToFavorites(std::int64_t contentId); extern "C" int APS5_VABI sceContentExportDataProvideFunction(void); extern "C" int APS5_VABI sceContentExportFree(void); extern "C" int APS5_VABI sceContentExportFrom(void); -extern "C" int APS5_VABI sceContentExportFromFileForSf(void); -extern "C" int APS5_VABI sceContentExportFromFileWithContentIdList(void); -extern "C" int APS5_VABI sceContentExportFromFileWithTitleIdList(void); -extern "C" int APS5_VABI sceContentExportInit(void); +extern "C" int APS5_VABI sceContentExportFromFileForSf(std::int32_t expId, const ContentExportParam* param, const ContentExportSfParam* sfParam, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportFromFileWithContentIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const std::int64_t* contentIds, std::uint32_t contentCount, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportFromFileWithTitleIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const char* titleIdList, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportInit(const ContentExportInitParam* param); extern "C" int APS5_VABI sceContentExportMalloc(void); -extern "C" int APS5_VABI sceContentExportRemoveFromFavorites(void); -extern "C" int APS5_VABI sceContentExportRenameContent(void); -extern "C" int APS5_VABI sceContentExportValidateContents(void); +extern "C" int APS5_VABI sceContentExportRemoveFromFavorites(std::int64_t contentId); +extern "C" int APS5_VABI sceContentExportRenameContent(std::int64_t contentId, const char* name); +extern "C" int APS5_VABI sceContentExportValidateContents(const std::int64_t* contentIds, std::uint32_t count); extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); @@ -23,8 +23,7 @@ extern "C" int APS5_VABI sceContentExportStart(void); extern "C" int APS5_VABI sceContentExportFinish(std::int32_t expId); extern "C" int APS5_VABI sceContentExportGetProgress(std::int32_t expId); extern "C" int APS5_VABI sceContentExportCancel(std::int32_t expId); -extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, - const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); namespace { @@ -98,17 +97,17 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportStart(); })); Require(Throws([&] { sceContentExportFinish(reopened); })); - Require(Throws<std::runtime_error>([] { sceContentExportAddToFavorites(); })); + Require(Throws<std::runtime_error>([] { sceContentExportAddToFavorites(0); })); Require(Throws<std::runtime_error>([] { sceContentExportDataProvideFunction(); })); Require(Throws<std::runtime_error>([] { sceContentExportFree(); })); Require(Throws<std::runtime_error>([] { sceContentExportFrom(); })); - Require(Throws<std::runtime_error>([] { sceContentExportFromFileForSf(); })); - Require(Throws<std::runtime_error>([] { sceContentExportFromFileWithContentIdList(); })); - Require(Throws<std::runtime_error>([] { sceContentExportFromFileWithTitleIdList(); })); - Require(Throws<std::runtime_error>([] { sceContentExportInit(); })); + Require(Throws<std::runtime_error>([] { sceContentExportFromFileForSf(0, nullptr, nullptr, nullptr, nullptr, nullptr, 0); })); + Require(Throws<std::runtime_error>([] { sceContentExportFromFileWithContentIdList(0, nullptr, nullptr, nullptr, nullptr, 0, nullptr, 0); })); + Require(Throws<std::runtime_error>([] { sceContentExportFromFileWithTitleIdList(0, nullptr, nullptr, nullptr, nullptr, nullptr, 0); })); + Require(Throws<std::runtime_error>([] { sceContentExportInit(nullptr); })); Require(Throws<std::runtime_error>([] { sceContentExportMalloc(); })); - Require(Throws<std::runtime_error>([] { sceContentExportRemoveFromFavorites(); })); - Require(Throws<std::runtime_error>([] { sceContentExportRenameContent(); })); - Require(Throws<std::runtime_error>([] { sceContentExportValidateContents(); })); + Require(Throws<std::runtime_error>([] { sceContentExportRemoveFromFavorites(0); })); + Require(Throws<std::runtime_error>([] { sceContentExportRenameContent(0, nullptr); })); + Require(Throws<std::runtime_error>([] { sceContentExportValidateContents(nullptr, 0); })); } diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 18355bf87b..d7d9e2f0d8 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -6,5 +6,6 @@ ### Unknown function info -- PlayStation 5 firmware exports declared without parameters, signatures unknown: [sceContentExportAddToFavorites, sceContentExportDataProvideFunction, sceContentExportFree, sceContentExportFrom, sceContentExportFromFileForSf, sceContentExportFromFileWithContentIdList, sceContentExportFromFileWithTitleIdList, sceContentExportInit, sceContentExportMalloc, sceContentExportRemoveFromFavorites, sceContentExportRenameContent, sceContentExportValidateContents](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport). Names from the firmware NID database; unimplemented and throw `NotImplemented_nid_no_patch`. +- [sceContentExportDataProvideFunction, sceContentExportFree, sceContentExportFrom and sceContentExportMalloc](../../../core/libs/prx/libSceContentExport/Export.cpp): their signatures remain unresolved; these names have no matching exports in the supplied module's C or assembly listing. +- [Content export arguments](../../../core/libs/prx/libSceContentExport/Export.cpp): file/data entry points and content-ID operations have restored argument lists. The Share Factory parameter contains a size, metadata string, content-ID list, count and option; metadata format and option values remain unresolved. Protected export bodies are unchanged. From 61b365f2d228b3192a84742ec841a46d25cb101f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:13:32 +0300 Subject: [PATCH 0642/1360] fix(contentexport): recover session slots and idle state errors --- core/libs/prx/libSceContentExport/Export.cpp | 26 +++++++++++--------- core/libs/tests/GuestContentExport.cpp | 23 ++++++++++------- docs/dev/debt/libSceContentExport.md | 2 +- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index eb87ef6971..0c5b205edf 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,18 +3,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include <atomic> -#include <limits> #include <mutex> #include <set> #include <stdexcept> #include <string> namespace { -constexpr std::size_t MaxExecution = 10; +constexpr std::int32_t MaxExecution = 10; +constexpr int ErrorInvalidState = static_cast<int>(0x809D3002u); +constexpr int ErrorBusy = static_cast<int>(0x809D3003u); +constexpr int ErrorNotInitialized = static_cast<int>(0x809D3004u); +constexpr int ErrorExecutionMax = static_cast<int>(0x809D3018u); std::atomic<bool> g_initialized{false}; std::mutex g_sessionMutex; std::set<std::int32_t> g_sessions; -std::int32_t g_lastSessionId = 0; void RequireSession(const char* function, std::int32_t expId) { if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); @@ -117,23 +119,23 @@ int APS5_VABI sceContentExportValidateContents(const std::int64_t* contentIds, s int APS5_VABI sceContentExportStart(void) { std::lock_guard lock(g_sessionMutex); - if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); - if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown"); - if (g_lastSessionId == std::numeric_limits<std::int32_t>::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted"); - g_sessions.insert(++g_lastSessionId); - return g_lastSessionId; + if (!g_initialized.load()) return ErrorNotInitialized; + for (std::int32_t expId = 0; expId < MaxExecution; ++expId) { + if (g_sessions.insert(expId).second) return expId; + } + return ErrorExecutionMax; } int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); - throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown"); + throw std::runtime_error(std::string(__func__) + ": progress requires the calling title SDK compatibility mode"); } int APS5_VABI sceContentExportCancel(std::int32_t expId) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); - throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); + return ErrorInvalidState; } int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { @@ -144,9 +146,9 @@ int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const Co int APS5_VABI sceContentExportTerm(void) { std::lock_guard lock(g_sessionMutex); - if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown"); + if (!g_sessions.empty()) return ErrorBusy; bool expected = true; - if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (!g_initialized.compare_exchange_strong(expected, false)) return ErrorNotInitialized; return 0; } diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 21229622e9..4f72b3a7c7 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -54,8 +54,8 @@ void Export(std::int32_t expId) { int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); - Require(Throws([] { sceContentExportTerm(); })); - Require(Throws([] { sceContentExportStart(); })); + Require(sceContentExportTerm() == static_cast<int>(0x809D3004u)); + Require(sceContentExportStart() == static_cast<int>(0x809D3004u)); Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; @@ -66,20 +66,20 @@ int main() { Require(sceContentExportInit2(¶m) == 0); Require(Throws([&] { sceContentExportInit2(¶m); })); Require(sceContentExportTerm() == 0); - Require(Throws([] { sceContentExportTerm(); })); + Require(sceContentExportTerm() == static_cast<int>(0x809D3004u)); Require(sceContentExportInit2(¶m) == 0); std::set<int> sessions; for (int i = 0; i < 10; ++i) { int session = sceContentExportStart(); - Require(session > 0); + Require(session == i); Require(sessions.insert(session).second); } - Require(Throws<std::runtime_error>([] { sceContentExportStart(); })); - Require(Throws<std::runtime_error>([] { sceContentExportTerm(); })); + Require(sceContentExportStart() == static_cast<int>(0x809D3018u)); + Require(sceContentExportTerm() == static_cast<int>(0x809D3003u)); int first = *sessions.begin(); Require(Throws<std::runtime_error>([&] { sceContentExportGetProgress(first); })); - Require(Throws<std::runtime_error>([&] { sceContentExportCancel(first); })); + Require(sceContentExportCancel(first) == static_cast<int>(0x809D3002u)); Require(Throws<std::runtime_error>([&] { Export(first); })); Require(path[0] == 0); Require(sceContentExportFinish(first) == 0); @@ -91,11 +91,16 @@ int main() { Require(Throws<std::invalid_argument>([] { sceContentExportFinish(-1); })); sessions.erase(first); int reopened = sceContentExportStart(); - Require(reopened > 0 && reopened != first); + Require(reopened == first); Require(sessions.insert(reopened).second); + Require(sceContentExportStart() == static_cast<int>(0x809D3018u)); + Require(sceContentExportFinish(7) == 0); + Require(sceContentExportFinish(3) == 0); + Require(sceContentExportStart() == 3); + Require(sceContentExportStart() == 7); for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); - Require(Throws([] { sceContentExportStart(); })); + Require(sceContentExportStart() == static_cast<int>(0x809D3004u)); Require(Throws([&] { sceContentExportFinish(reopened); })); Require(Throws<std::runtime_error>([] { sceContentExportAddToFavorites(0); })); Require(Throws<std::runtime_error>([] { sceContentExportDataProvideFunction(); })); diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index d7d9e2f0d8..01f0d64324 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -2,7 +2,7 @@ ### Functional -- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified +- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp): the host has no media library, so `sceContentExportFromDataWithThumbnail` throws for a valid session without invoking data callbacks. Session allocation does not reproduce guest allocator calls, worker creation or their failure paths. `sceContentExportGetProgress` throws because an idle session returns either zero or an invalid-state error according to the caller's SDK compatibility mode, which is not available here. Initialization and session lookup still throw for some invalid inputs instead of reproducing the console's error precedence and legacy SDK behavior. Session IDs now use the lowest free slot in 0–9; exhaustion, termination with live sessions and cancellation before an export return the confirmed errors. ### Unknown function info From 813b6f4362f24c96536eddf884a0bac8fde21f65 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:15:35 +0300 Subject: [PATCH 0643/1360] fix(contentsearch): recover initialization and query validation errors --- core/libs/prx/libSceContentSearch/Export.cpp | 30 +++++++++++--------- core/libs/tests/GuestContentSearch.cpp | 30 ++++++++++++++++---- docs/dev/debt/libSceContentSearch.md | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 2c9b8a5777..ac200672de 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -10,6 +10,8 @@ namespace { std::atomic<bool> g_initialized{false}; constexpr int ErrorNotInitialized = static_cast<int>(0x809D1001u); +constexpr int ErrorAlreadyInitialized = static_cast<int>(0x809D1002u); +constexpr int ErrorInvalidArgument = static_cast<int>(0x809D1003u); constexpr int ErrorLimitTooBig = static_cast<int>(0x809D100Au); constexpr std::uint32_t MaxSearchLimit = 92; constexpr std::int64_t LibraryUpdateId = 0; @@ -63,42 +65,44 @@ void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCond extern "C" { int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { - if (init_param == nullptr) APS5_INVALID_ARG_EX; + if (g_initialized.load()) return ErrorAlreadyInitialized; + if (init_param == nullptr || init_param->memory_size == 0 || init_param->memory_size % 16384 != 0) return ErrorInvalidArgument; bool expected = false; - if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + if (!g_initialized.compare_exchange_strong(expected, true)) return ErrorAlreadyInitialized; return 0; } int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; - ThrowUnknownMetadataId(__func__, metadataId); + return ErrorInvalidArgument; } int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size) { if (!g_initialized.load()) return ErrorNotInitialized; - if (field == nullptr || metadataType == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + if (field == nullptr || metadataType == nullptr || size == nullptr) return ErrorInvalidArgument; ThrowUnknownMetadataId(__func__, metadataId); } int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value) { if (!g_initialized.load()) return ErrorNotInitialized; - if (field == nullptr || value == nullptr) APS5_INVALID_ARG_EX; + if (field == nullptr || value == nullptr) return ErrorInvalidArgument; ThrowUnknownMetadataId(__func__, metadataId); } int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (filePath == nullptr || metadataId == nullptr) APS5_INVALID_ARG_EX; + if (filePath == nullptr || metadataId == nullptr) return ErrorInvalidArgument; throw std::logic_error(std::string(__func__) + ": " + filePath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId) { (void)offset; if (!g_initialized.load()) return ErrorNotInitialized; - if (numOfContent == nullptr || lastUpdateId == nullptr || (infos == nullptr && limit != 0)) APS5_INVALID_ARG_EX; + if ((columnSet == nullptr) != (columnSetLength == 0) || (orderByConditions == nullptr) != (orderByConditionsLength == 0) || limit == 0) return ErrorInvalidArgument; + if (limit > MaxSearchLimit) return ErrorLimitTooBig; + if (numOfContent == nullptr || lastUpdateId == nullptr || infos == nullptr) return ErrorInvalidArgument; ValidateColumnSets(__func__, columnSet, columnSetLength); ValidateOrderBy(__func__, orderByConditions, orderByConditionsLength); - if (limit > MaxSearchLimit) return ErrorLimitTooBig; *numOfContent = 0; *lastUpdateId = LibraryUpdateId; return 0; @@ -106,20 +110,20 @@ int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + if (lastUpdateId == nullptr) return ErrorInvalidArgument; *lastUpdateId = LibraryUpdateId; return 0; } int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (metadataId == nullptr) APS5_INVALID_ARG_EX; + if (metadataId == nullptr) return ErrorInvalidArgument; throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); } int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (size == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + if ((columnSet == nullptr) != (columnSetLength == 0) || size == nullptr || lastUpdateId == nullptr) return ErrorInvalidArgument; ValidateColumnSets(__func__, columnSet, columnSetLength); *size = 0; *lastUpdateId = LibraryUpdateId; @@ -128,7 +132,7 @@ int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColu int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (numOfContent == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + if ((columnSet == nullptr) != (columnSetLength == 0) || numOfContent == nullptr || lastUpdateId == nullptr) return ErrorInvalidArgument; ValidateColumnSets(__func__, columnSet, columnSetLength); *numOfContent = 0; *lastUpdateId = LibraryUpdateId; @@ -137,7 +141,7 @@ int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSe int APS5_VABI sceContentSearchTerm(void) { bool expected = true; - if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (!g_initialized.compare_exchange_strong(expected, false)) return ErrorNotInitialized; return 0; } diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index 2447383112..e9facbccc1 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -32,6 +32,8 @@ bool Throws(TAction action) { } constexpr int ErrorNotInitialized = static_cast<int>(0x809D1001u); +constexpr int ErrorInvalidArgument = static_cast<int>(0x809D1003u); +constexpr int ErrorAlreadyInitialized = static_cast<int>(0x809D1002u); constexpr int ErrorLimitTooBig = static_cast<int>(0x809D100Au); struct MetadataValue { @@ -66,7 +68,16 @@ int main() { Require(sceContentSearchCloseMetadata(1) == ErrorNotInitialized); Require(count == -1 && size == -1 && updateId == -1 && metadataId == -1); + Require(sceContentSearchTerm() == ErrorNotInitialized); + Require(sceContentSearchInit(nullptr) == ErrorInvalidArgument); + for (std::size_t memorySize : {std::size_t{0}, std::size_t{1}, std::size_t{16383}, param.memory_size - 1}) { + ContentSearchInitParam invalidParam{memorySize}; + Require(sceContentSearchInit(&invalidParam) == ErrorInvalidArgument); + Require(sceContentSearchTerm() == ErrorNotInitialized); + } Require(sceContentSearchInit(¶m) == 0); + Require(sceContentSearchInit(nullptr) == ErrorAlreadyInitialized); + Require(sceContentSearchInit(¶m) == ErrorAlreadyInitialized); Require(sceContentSearchGetContentLastUpdateId(&updateId) == 0); Require(updateId == 0); @@ -89,11 +100,11 @@ int main() { Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 93, &count, infos, &updateId) == ErrorLimitTooBig); Require(count == -1 && updateId == -1); - Require(Throws([&] { sceContentSearchGetContentLastUpdateId(nullptr); })); - Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId); })); - Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr); })); - Require(Throws([&] { sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId); })); - Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId); })); + Require(sceContentSearchGetContentLastUpdateId(nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId) == ErrorInvalidArgument); ContentSearchContentColumn unknownColumn{8, 0, &photo, 2}; ContentSearchContentColumnSet unknownColumnSet{&unknownColumn, 1, 2}; Require(Throws([&] { sceContentSearchGetNumOfContent(&unknownColumnSet, 1, &count, &updateId); })); @@ -106,11 +117,18 @@ int main() { ContentSearchContentOrderByCondition unknownSort{3, 0}; Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &unknownSort, 1, 0, 2, &count, infos, &updateId); })); Require(count == -1 && updateId == -1); + Require(sceContentSearchSearchContent(nullptr, 0, nullptr, 0, 0, 0, nullptr, nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchSearchContent(nullptr, 0, nullptr, 0, 0, 93, nullptr, nullptr, nullptr) == ErrorLimitTooBig); + Require(sceContentSearchSearchContent(&columnSet, 0, nullptr, 0, 0, 93, nullptr, nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchSearchContent(nullptr, 0, &orderBy, 0, 0, 92, &count, infos, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetNumOfContent(&columnSet, 0, &count, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetTotalContentSize(&columnSet, 0, &size, &updateId) == ErrorInvalidArgument); + Require(count == -1 && updateId == -1); Require(Throws([&] { sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId); })); Require(Throws([&] { sceContentSearchOpenMetadataByContentId(1, &metadataId); })); Require(metadataId == -1); - Require(Throws([] { sceContentSearchCloseMetadata(1); })); + Require(sceContentSearchCloseMetadata(1) == ErrorInvalidArgument); Require(Throws([&] { sceContentSearchGetMetadataFieldInfo(1, "title", &metadataType, &fieldSize); })); Require(Throws([&] { sceContentSearchGetMetadataValue(1, "title", &value); })); Require(metadataType == -1 && fieldSize == -1 && value.size == -1); diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index 56c59f62ad..8ef6373ca4 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -6,4 +6,4 @@ ### Functional -- [libSceContentSearch](../../../core/libs/prx/libSceContentSearch/Export.cpp) searches a content library that is empty on the host, as nothing is exported to it. The signatures follow [AlexAltea/orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L555-L569) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentSearch.cs#L255-L316), which agree; the filter and sort layouts and enums, the limit of 92 rows, `0x809D100A` for a larger limit and `0x809D1001` before `sceContentSearchInit` are SharpProspero's. `sceContentSearchGetNumOfContent`, `sceContentSearchGetTotalContentSize` and `sceContentSearchSearchContent` report 0 rows and 0 bytes without writing a content row, and the update id they and `sceContentSearchGetContentLastUpdateId` report is always 0; no reference gives the console's update id for a library that never changed. Filters and sort conditions are only checked against SharpProspero's enums, and a null output, a null array with a non-zero length, an empty column set, a null filter value or an unknown enum value throws, as the console's errors for them are unknown; a null column set with length 0 is taken as no filter. `sceContentSearchOpenMetadata` and `sceContentSearchOpenMetadataByContentId` throw, as no reference gives the error for a path or content id that is not in the library, so no metadata id is ever handed out and `sceContentSearchCloseMetadata`, `sceContentSearchGetMetadataFieldInfo` and `sceContentSearchGetMetadataValue` throw. Every title is taken to hold the content-search permission (SharpProspero's `0x809D1013` is never returned). The order of the initialization, argument and limit checks is unverified +- [libSceContentSearch](../../../core/libs/prx/libSceContentSearch/Export.cpp) searches a content library that is empty on the host, as nothing is exported to it. The signatures follow [AlexAltea/orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L555-L569) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentSearch.cs#L255-L316), which agree; the filter and sort layouts and enums, the limit of 92 rows, `0x809D100A` for a larger limit and `0x809D1001` before `sceContentSearchInit` are SharpProspero's. `sceContentSearchGetNumOfContent`, `sceContentSearchGetTotalContentSize` and `sceContentSearchSearchContent` report 0 rows and 0 bytes without writing a content row, and the update id they and `sceContentSearchGetContentLastUpdateId` report is always 0; no reference gives the console's update id for a library that never changed. Filters and sort conditions are only checked against SharpProspero's enums, and an empty nested column set, a null filter value or an unknown enum value throws, as their validation is not yet matched to the native query builder; a null column set with length 0 is taken as no filter. `sceContentSearchOpenMetadata` and `sceContentSearchOpenMetadataByContentId` throw, as no reference gives the error for a path or content id that is not in the library, so no metadata id is ever handed out; metadata-field and value queries with valid pointers still throw because field-dependent errors remain unresolved. Every title is taken to hold the content-search permission (SharpProspero's `0x809D1013` is never returned). Initialization checks and outer query pointer/count/limit precedence follow the supplied module. Guest heap allocation, platform-dependent minimum heap size and nested query validation remain unimplemented From 41e386a49541e8902f9d291ccea852c4bca33d5b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:19:33 +0300 Subject: [PATCH 0644/1360] fix(fontft): restore library arguments and validate format queries --- core/libs/prx/libSceFontFt/Export.cpp | 90 +++++++------------ .../prx/libSceFontFt/include/FontFtDriver.hpp | 3 + core/libs/prx/libSceFontFt/src/Driver.cpp | 29 ++++++ core/libs/tests/GuestFont.cpp | 44 ++++++--- docs/dev/debt/libSceFontFt.md | 6 +- 5 files changed, 101 insertions(+), 71 deletions(-) diff --git a/core/libs/prx/libSceFontFt/Export.cpp b/core/libs/prx/libSceFontFt/Export.cpp index 0dcd8bcf90..b0a213d62f 100644 --- a/core/libs/prx/libSceFontFt/Export.cpp +++ b/core/libs/prx/libSceFontFt/Export.cpp @@ -1,40 +1,14 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include <initializer_list> -#include <stdexcept> -#include <string> - -#include <ft2build.h> -#include FT_FREETYPE_H -#include FT_MODULE_H #include "prx/libSceFontFt/include/FontFtDriver.hpp" #include "prx/libc/include/General.hpp" #pragma GCC visibility push(default) -namespace { - -FT_Library ProbeLibrary() { - static const FT_Library library = [] { - FT_Library created = nullptr; - if (FT_Init_FreeType(&created) != 0) return static_cast<FT_Library>(nullptr); - return created; - }(); - return library; -} - -int SupportFormat(const char* function, std::initializer_list<const char*> modules) { - const FT_Library library = ProbeLibrary(); - if (library == nullptr) throw std::runtime_error(std::string(function) + ": FreeType failed to initialize"); - for (const char* module : modules) { - if (FT_Get_Module(library, module) == nullptr) throw std::runtime_error(std::string(function) + ": FreeType module " + module + " is not built in"); - } - return 0; -} - -} +struct FontFtAliases; +struct FontFtGlyphSelection; extern "C" { @@ -46,86 +20,86 @@ const Font::RendererSelection* APS5_VABI sceFontSelectRendererFt(int value) { return value == 0 ? FontFt::RendererTable() : nullptr; } -int APS5_VABI sceFontFtInitAliases() { +int APS5_VABI sceFontFtInitAliases(FontFtAliases** aliases) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSetAliasFont() { +int APS5_VABI sceFontFtSetAliasFont(const char* alias, std::uint32_t fontId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSetAliasPath() { +int APS5_VABI sceFontFtSetAliasPath(const char* alias, const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportBdf() { - return SupportFormat(__func__, {"bdf"}); +int APS5_VABI sceFontFtSupportBdf(FontLibrary library) { + return FontFt::SupportFormats(library, {"bdf"}); } -int APS5_VABI sceFontFtSupportCid() { - return SupportFormat(__func__, {"t1cid"}); +int APS5_VABI sceFontFtSupportCid(FontLibrary library) { + return FontFt::SupportFormats(library, {"t1cid"}); } -int APS5_VABI sceFontFtSupportFontFormats() { +int APS5_VABI sceFontFtSupportFontFormats(FontLibrary library, std::uint32_t formats) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportOpenType() { - return SupportFormat(__func__, {"cff", "truetype"}); +int APS5_VABI sceFontFtSupportOpenType(FontLibrary library) { + return FontFt::SupportFormats(library, {"cff", "truetype"}); } -int APS5_VABI sceFontFtSupportOpenTypeOtf() { - return SupportFormat(__func__, {"cff"}); +int APS5_VABI sceFontFtSupportOpenTypeOtf(FontLibrary library) { + return FontFt::SupportFormats(library, {"cff"}); } -int APS5_VABI sceFontFtSupportOpenTypeTtf() { - return SupportFormat(__func__, {"truetype"}); +int APS5_VABI sceFontFtSupportOpenTypeTtf(FontLibrary library) { + return FontFt::SupportFormats(library, {"truetype"}); } -int APS5_VABI sceFontFtSupportPcf() { - return SupportFormat(__func__, {"pcf"}); +int APS5_VABI sceFontFtSupportPcf(FontLibrary library) { + return FontFt::SupportFormats(library, {"pcf"}); } -int APS5_VABI sceFontFtSupportPfr() { - return SupportFormat(__func__, {"pfr"}); +int APS5_VABI sceFontFtSupportPfr(FontLibrary library) { + return FontFt::SupportFormats(library, {"pfr"}); } -int APS5_VABI sceFontFtSupportSystemFonts() { +int APS5_VABI sceFontFtSupportSystemFonts(FontLibrary library) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportTrueType() { - return SupportFormat(__func__, {"truetype"}); +int APS5_VABI sceFontFtSupportTrueType(FontLibrary library) { + return FontFt::SupportFormats(library, {"truetype"}); } -int APS5_VABI sceFontFtSupportTrueTypeGx() { +int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportType1() { - return SupportFormat(__func__, {"type1"}); +int APS5_VABI sceFontFtSupportType1(FontLibrary library) { + return FontFt::SupportFormats(library, {"type1"}); } -int APS5_VABI sceFontFtSupportType42() { - return SupportFormat(__func__, {"type42"}); +int APS5_VABI sceFontFtSupportType42(FontLibrary library) { + return FontFt::SupportFormats(library, {"type42"}); } -int APS5_VABI sceFontFtSupportWinFonts() { - return SupportFormat(__func__, {"winfonts"}); +int APS5_VABI sceFontFtSupportWinFonts(FontLibrary library) { + return FontFt::SupportFormats(library, {"winfonts"}); } -int APS5_VABI sceFontFtTermAliases() { +int APS5_VABI sceFontFtTermAliases(FontFtAliases** aliases) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontSelectGlyphsFt() { +const FontFtGlyphSelection* APS5_VABI sceFontSelectGlyphsFt(std::int32_t selector) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp b/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp index 87e7e92931..35ba74ff66 100644 --- a/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp +++ b/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp @@ -4,12 +4,15 @@ #ifndef CORE_LIBS_PRX_LIBSCEFONTFT_INCLUDE_FONTFTDRIVER_HPP #define CORE_LIBS_PRX_LIBSCEFONTFT_INCLUDE_FONTFTDRIVER_HPP +#include <initializer_list> + #include "prx/libSceFont/include/FontDriver.hpp" namespace FontFt { const Font::SysDriver* DriverTable(); const Font::RendererSelection* RendererTable(); +int SupportFormats(FontLibrary library, std::initializer_list<const char*> modules); } diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 7f49954e0a..152a72f8d7 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -2,6 +2,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> +#include <atomic> +#include <chrono> +#include <memory> +#include <stdexcept> +#include <thread> #include <cmath> #include <cstddef> #include <cstdint> @@ -768,3 +773,27 @@ const Font::SysDriver* FontFt::DriverTable() { const Font::RendererSelection* FontFt::RendererTable() { return &rendererTable; } + +int FontFt::SupportFormats(FontLibrary library, std::initializer_list<const char*> modules) { + auto* native = static_cast<Font::FontLibNative*>(library); + if (!native) return SCE_FONT_ERROR_INVALID_LIBRARY; + auto lockWord = std::atomic_ref<std::uint32_t>(native->lock_word); + std::uint32_t previous = lockWord.load(std::memory_order_acquire); + for (;;) { + if (native->magic != Font::LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + if ((previous & Font::LOCK_BIT) == 0 && lockWord.compare_exchange_weak(previous, previous | Font::LOCK_BIT, std::memory_order_acq_rel)) break; + std::this_thread::sleep_for(std::chrono::microseconds(30)); + previous = lockWord.load(std::memory_order_acquire); + } + const auto unlock = [previous](Font::FontLibNative* locked) { + std::atomic_ref<std::uint32_t>(locked->lock_word).store(previous, std::memory_order_release); + }; + const std::unique_ptr<Font::FontLibNative, decltype(unlock)> guard(native, unlock); + if (native->sys_driver != &driverTable) throw std::runtime_error("FontFt: unsupported library driver"); + const auto* context = static_cast<const FtLibraryContext*>(native->fontset_registry); + if (!context || !context->library) return SCE_FONT_ERROR_INVALID_LIBRARY; + for (const char* module : modules) { + if (!FT_Get_Module(context->library, module)) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; + } + return SCE_FONT_OK; +} diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 80184223b8..8db5efd303 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -61,17 +61,17 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting*); FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting*, const FontWritingLetterStep**); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); -int APS5_VABI sceFontFtSupportBdf(); -int APS5_VABI sceFontFtSupportCid(); -int APS5_VABI sceFontFtSupportOpenType(); -int APS5_VABI sceFontFtSupportOpenTypeOtf(); -int APS5_VABI sceFontFtSupportOpenTypeTtf(); -int APS5_VABI sceFontFtSupportPcf(); -int APS5_VABI sceFontFtSupportPfr(); -int APS5_VABI sceFontFtSupportTrueType(); -int APS5_VABI sceFontFtSupportType1(); -int APS5_VABI sceFontFtSupportType42(); -int APS5_VABI sceFontFtSupportWinFonts(); +int APS5_VABI sceFontFtSupportBdf(FontLibrary library); +int APS5_VABI sceFontFtSupportCid(FontLibrary library); +int APS5_VABI sceFontFtSupportOpenType(FontLibrary library); +int APS5_VABI sceFontFtSupportOpenTypeOtf(FontLibrary library); +int APS5_VABI sceFontFtSupportOpenTypeTtf(FontLibrary library); +int APS5_VABI sceFontFtSupportPcf(FontLibrary library); +int APS5_VABI sceFontFtSupportPfr(FontLibrary library); +int APS5_VABI sceFontFtSupportTrueType(FontLibrary library); +int APS5_VABI sceFontFtSupportType1(FontLibrary library); +int APS5_VABI sceFontFtSupportType42(FontLibrary library); +int APS5_VABI sceFontFtSupportWinFonts(FontLibrary library); int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter*, int*); FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(const FontTextCharacter*, const FontTextCharacter*, FontTextCodes*); FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes*); @@ -209,11 +209,31 @@ int main() { Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); Require(sceFontSelectLibraryFt(0) != nullptr && sceFontSelectLibraryFt(1) == nullptr); Require(sceFontSelectRendererFt(0) != nullptr && sceFontSelectRendererFt(1) == nullptr); - Require(sceFontFtSupportBdf() == 0 && sceFontFtSupportCid() == 0 && sceFontFtSupportOpenType() == 0 && sceFontFtSupportOpenTypeOtf() == 0 && sceFontFtSupportOpenTypeTtf() == 0 && sceFontFtSupportPcf() == 0 && sceFontFtSupportPfr() == 0 && sceFontFtSupportTrueType() == 0 && sceFontFtSupportType1() == 0 && sceFontFtSupportType42() == 0 && sceFontFtSupportWinFonts() == 0); FontLibrary library = nullptr; Require(sceFontCreateLibrary(&memory, nullptr, &library) == SCE_FONT_ERROR_INVALID_PARAMETER && library == nullptr); Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK && library != nullptr); + Require(sceFontFtSupportBdf(library) == 0 && sceFontFtSupportCid(library) == 0 && sceFontFtSupportOpenType(library) == 0 && sceFontFtSupportOpenTypeOtf(library) == 0 && sceFontFtSupportOpenTypeTtf(library) == 0 && sceFontFtSupportPcf(library) == 0 && sceFontFtSupportPfr(library) == 0 && sceFontFtSupportTrueType(library) == 0 && sceFontFtSupportType1(library) == 0 && sceFontFtSupportType42(library) == 0 && sceFontFtSupportWinFonts(library) == 0); + const auto* nativeLibrary = static_cast<const Font::FontLibNative*>(library); + const auto savedLock = nativeLibrary->lock_word; + for (auto support : {sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportType1, sceFontFtSupportType42, sceFontFtSupportWinFonts}) { + Font::FontLibNative invalidLibrary{}; + Require(support(nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(support(&invalidLibrary) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(support(library) == SCE_FONT_OK); + Require(nativeLibrary->lock_word == savedLock); + } + + Font::FontLibNative foreignLibrary{}; + foreignLibrary.magic = Font::LIBRARY_MAGIC; + foreignLibrary.lock_word = 5; + bool foreignDriverRejected = false; + try { + static_cast<void>(sceFontFtSupportTrueType(&foreignLibrary)); + } catch (const std::runtime_error&) { + foreignDriverRejected = true; + } + Require(foreignDriverRejected && foreignLibrary.lock_word == 5); std::uint32_t subPixelCount = 1; Require(sceFontGetPixelResolution(library, &subPixelCount) == SCE_FONT_OK && subPixelCount == 64); diff --git a/docs/dev/debt/libSceFontFt.md b/docs/dev/debt/libSceFontFt.md index cd35391b21..3aa05e385d 100644 --- a/docs/dev/debt/libSceFontFt.md +++ b/docs/dev/debt/libSceFontFt.md @@ -1,5 +1,9 @@ # libSceFontFt technical debt +### Functional + +- [Font format support](../../../core/libs/prx/libSceFontFt/Export.cpp): format queries now take the font library, validate its magic and lock it while checking its actual FreeType modules. The host registers its default modules at creation rather than enabling formats individually, so format-selection state and guest allocator failures during format registration are not reproduced. Unsupported foreign drivers throw. + ### Unknown function info -- [sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportType1, sceFontFtSupportType42 and sceFontFtSupportWinFonts](../../../core/libs/prx/libSceFontFt/Export.cpp) (libSceFontFt) are declared without parameters, signatures unknown; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/7da7a8e1f98eca9d8f01f84d20681f8faa0bd8fd/src/core/libraries/font/fontft.cpp), another emulator, declares them the same way and only logs. They are assumed to enable one font format in the FontFt library, which here always registers FreeType's default modules, so each call returns 0 when the host FreeType has the module (`bdf`, `t1cid`, `truetype`, `cff`, `pcf`, `pfr`, `type1`, `type42`, `winfonts`; OpenType needs `cff` and `truetype`) and throws when it does not. The mapping of Sony's format names to FreeType modules, the return value and the error codes of the console are unverified, and no title is known to call them. +- [Alias and glyph selection exports](../../../core/libs/prx/libSceFontFt/Export.cpp): alias initialization/termination take a handle pointer; alias registration takes a string and font ID or path. Glyph selection takes a 32-bit selector and returns a table pointer whose layout remains unresolved. Their protected implementations are unchanged. From 30736adb89ea4c974cbbb863b9c2cf9c6f2f2ab1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:21:48 +0300 Subject: [PATCH 0645/1360] fix(font): restore graphics lifecycle and metric signatures --- .../libs/prx/libSceFont/src/Unimplemented.cpp | 29 +++++++++++++------ docs/dev/debt/libSceFont.md | 2 ++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index bc79aed66e..3826e0dc48 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -2,6 +2,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "prx/libc/include/General.hpp" +#include "prx/libSceFont/include/FontTypes.hpp" + +struct FontGraphicsDevice; +struct FontGraphicsDeviceParam; +struct FontGraphicsService; +struct FontGraphicsServiceParam; +struct FontWords; +struct FontOutlineProfile; +struct FontMetrics; +struct FontStyleInformation; +struct FontGlyphExpandBufferState; #pragma GCC visibility push(default) @@ -12,12 +23,12 @@ int APS5_VABI sceFontControl() { return 0; } -int APS5_VABI sceFontCreateGraphicsDevice() { +int APS5_VABI sceFontCreateGraphicsDevice(const FontMemory* memory, const FontGraphicsDeviceParam* param, FontGraphicsDevice** device) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontCreateGraphicsService() { +int APS5_VABI sceFontCreateGraphicsService(const FontMemory* memory, const FontGraphicsServiceParam* param, FontGraphicsService** service) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -32,38 +43,38 @@ int APS5_VABI sceFontCreateWords() { return 0; } -int APS5_VABI sceFontDestroyGraphicsDevice() { +int APS5_VABI sceFontDestroyGraphicsDevice(FontGraphicsDevice** device) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontDestroyGraphicsService() { +int APS5_VABI sceFontDestroyGraphicsService(FontGraphicsService** service) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontDestroyWords() { +int APS5_VABI sceFontDestroyWords(FontWords** words) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { +int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlineProfile* profile) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGetFontMetrics() { +int APS5_VABI sceFontGetFontMetrics(FontHandle font, std::uint32_t mode, FontMetrics* metrics) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGetFontStyleInformation() { +int APS5_VABI sceFontGetFontStyleInformation(FontHandle font, FontStyleInformation* information) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGetGlyphExpandBufferState() { +int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandBufferState* state) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 5c3d5ad062..16c1c82488 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -22,3 +22,5 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - 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 Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; 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. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, 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 graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From e2f8adca1b42bde7a98a44eee80c0fc5730f3b3b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:27:05 +0300 Subject: [PATCH 0646/1360] fix(font): restore region and graphics transform signatures --- .../libs/prx/libSceFont/src/Unimplemented.cpp | 65 +++++++++++++------ docs/dev/debt/libSceFont.md | 2 + 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 3826e0dc48..6c1116ceef 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -13,6 +13,33 @@ struct FontOutlineProfile; struct FontMetrics; struct FontStyleInformation; struct FontGlyphExpandBufferState; +struct FontGraphicsResource; +struct FontGraphicsFillMethod; +struct FontGraphicsFillPlot; +struct FontGraphicsFillRates; +struct FontGraphicsFill; + +struct FontGraphicsRegion { + float x; + float y; + float width; + float height; + std::uint32_t flags; + std::uint32_t tag; + float outerRadius; + float innerRadius; +}; + +static_assert(sizeof(FontGraphicsRegion) == 32); +static_assert(offsetof(FontGraphicsRegion, x) == 0); +static_assert(offsetof(FontGraphicsRegion, y) == 4); +static_assert(offsetof(FontGraphicsRegion, width) == 8); +static_assert(offsetof(FontGraphicsRegion, height) == 12); +static_assert(offsetof(FontGraphicsRegion, flags) == 16); +static_assert(offsetof(FontGraphicsRegion, tag) == 20); +static_assert(offsetof(FontGraphicsRegion, outerRadius) == 24); +static_assert(offsetof(FontGraphicsRegion, innerRadius) == 28); + #pragma GCC visibility push(default) @@ -119,42 +146,42 @@ int APS5_VABI sceFontGraphicsExchangeResource() { return 0; } -int APS5_VABI sceFontGraphicsFillMethodInit() { +int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillPlotInit() { +int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillPlotSetLayout() { +int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillPlotSetMapping() { +int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesInit() { +int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesSetFillEffect() { +int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesSetLayout() { +int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesSetMapping() { +int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -164,17 +191,17 @@ int APS5_VABI sceFontGraphicsGetDeviceUsage() { return 0; } -int APS5_VABI sceFontGraphicsRegionInit() { +int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRegionInitCircular() { +int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRegionInitRoundish() { +int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -199,7 +226,7 @@ int APS5_VABI sceFontGraphicsSetupClipping() { return 0; } -int APS5_VABI sceFontGraphicsSetupColorRates() { +int APS5_VABI sceFontGraphicsSetupColorRates(FontGraphicsResource* resource, float red, float green, float blue, float alpha) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -229,7 +256,7 @@ int APS5_VABI sceFontGraphicsSetupHandleDefault() { return 0; } -int APS5_VABI sceFontGraphicsSetupLocation() { +int APS5_VABI sceFontGraphicsSetupLocation(FontGraphicsResource* resource, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -239,12 +266,12 @@ int APS5_VABI sceFontGraphicsSetupPositioning() { return 0; } -int APS5_VABI sceFontGraphicsSetupRotation() { +int APS5_VABI sceFontGraphicsSetupRotation(FontGraphicsResource* resource, float angle, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupScaling() { +int APS5_VABI sceFontGraphicsSetupScaling(FontGraphicsResource* resource, float scaleX, float scaleY, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -289,7 +316,7 @@ int APS5_VABI sceFontGraphicsUpdateClipping() { return 0; } -int APS5_VABI sceFontGraphicsUpdateColorRates() { +int APS5_VABI sceFontGraphicsUpdateColorRates(FontGraphicsResource* resource, float red, float green, float blue, float alpha) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -314,7 +341,7 @@ int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot() { return 0; } -int APS5_VABI sceFontGraphicsUpdateLocation() { +int APS5_VABI sceFontGraphicsUpdateLocation(FontGraphicsResource* resource, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -324,12 +351,12 @@ int APS5_VABI sceFontGraphicsUpdatePositioning() { return 0; } -int APS5_VABI sceFontGraphicsUpdateRotation() { +int APS5_VABI sceFontGraphicsUpdateRotation(FontGraphicsResource* resource, float angle, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateScaling() { +int APS5_VABI sceFontGraphicsUpdateScaling(FontGraphicsResource* resource, float scaleX, float scaleY, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 16c1c82488..02030240f1 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -24,3 +24,5 @@ - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, 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 graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. + +- [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. From 60281539f9a9ee026ff09c23e9e2aa57ff61e80c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:28:07 +0300 Subject: [PATCH 0647/1360] fix(font): restore drawing frame and fill argument types --- .../libs/prx/libSceFont/src/Unimplemented.cpp | 44 ++++++++++--------- docs/dev/debt/libSceFont.md | 2 + 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 6c1116ceef..e4bb1c699f 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -14,6 +14,8 @@ struct FontMetrics; struct FontStyleInformation; struct FontGlyphExpandBufferState; struct FontGraphicsResource; +struct FontGraphicsFrame; +struct FontGraphicsDeviceUsage; struct FontGraphicsFillMethod; struct FontGraphicsFillPlot; struct FontGraphicsFillRates; @@ -121,22 +123,22 @@ int APS5_VABI sceFontGlyphRenderImageVertical() { return 0; } -int APS5_VABI sceFontGraphicsBeginFrame() { +int APS5_VABI sceFontGraphicsBeginFrame(FontGraphicsFrame* frame, std::uint64_t flags, FontGraphicsResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsDrawingCancel() { +int APS5_VABI sceFontGraphicsDrawingCancel(FontGraphicsFrame** frame) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsDrawingFinish() { +int APS5_VABI sceFontGraphicsDrawingFinish(FontGraphicsFrame** frame, FontGraphicsDevice* device) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsEndFrame() { +int APS5_VABI sceFontGraphicsEndFrame(FontGraphicsFrame* frame) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -186,7 +188,7 @@ int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, f return 0; } -int APS5_VABI sceFontGraphicsGetDeviceUsage() { +int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGraphicsDeviceUsage* usage) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -216,12 +218,12 @@ int APS5_VABI sceFontGraphicsRenderResource() { return 0; } -int APS5_VABI sceFontGraphicsSetFramePolicy() { +int APS5_VABI sceFontGraphicsSetFramePolicy(FontGraphicsResource* resource, std::int32_t policy, std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupClipping() { +int APS5_VABI sceFontGraphicsSetupClipping(FontGraphicsResource* resource, const FontGraphicsRegion* region) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -231,27 +233,27 @@ int APS5_VABI sceFontGraphicsSetupColorRates(FontGraphicsResource* resource, flo return 0; } -int APS5_VABI sceFontGraphicsSetupFillMethod() { +int APS5_VABI sceFontGraphicsSetupFillMethod(FontGraphicsResource* resource, const FontGraphicsFillMethod* method) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupFillRates() { +int APS5_VABI sceFontGraphicsSetupFillRates(FontGraphicsResource* resource, const FontGraphicsFillRates* rates) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupGlyphFill() { +int APS5_VABI sceFontGraphicsSetupGlyphFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupGlyphFillPlot() { +int APS5_VABI sceFontGraphicsSetupGlyphFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupHandleDefault() { +int APS5_VABI sceFontGraphicsSetupHandleDefault(FontGraphicsResource* resource, std::uint64_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -276,12 +278,12 @@ int APS5_VABI sceFontGraphicsSetupScaling(FontGraphicsResource* resource, float return 0; } -int APS5_VABI sceFontGraphicsSetupShapeFill() { +int APS5_VABI sceFontGraphicsSetupShapeFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupShapeFillPlot() { +int APS5_VABI sceFontGraphicsSetupShapeFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -311,7 +313,7 @@ int APS5_VABI sceFontGraphicsStructureSurfaceTexture() { return 0; } -int APS5_VABI sceFontGraphicsUpdateClipping() { +int APS5_VABI sceFontGraphicsUpdateClipping(FontGraphicsResource* resource, const FontGraphicsRegion* region) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -321,22 +323,22 @@ int APS5_VABI sceFontGraphicsUpdateColorRates(FontGraphicsResource* resource, fl return 0; } -int APS5_VABI sceFontGraphicsUpdateFillMethod() { +int APS5_VABI sceFontGraphicsUpdateFillMethod(FontGraphicsResource* resource, const FontGraphicsFillMethod* method) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateFillRates() { +int APS5_VABI sceFontGraphicsUpdateFillRates(FontGraphicsResource* resource, const FontGraphicsFillRates* rates) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateGlyphFill() { +int APS5_VABI sceFontGraphicsUpdateGlyphFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot() { +int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -361,12 +363,12 @@ int APS5_VABI sceFontGraphicsUpdateScaling(FontGraphicsResource* resource, float return 0; } -int APS5_VABI sceFontGraphicsUpdateShapeFill() { +int APS5_VABI sceFontGraphicsUpdateShapeFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateShapeFillPlot() { +int APS5_VABI sceFontGraphicsUpdateShapeFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 02030240f1..2ba6558989 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -26,3 +26,5 @@ - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. - [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. + +Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Device usage, frame and resource layouts remain opaque; their runtime implementation is still unavailable. From 340328ed04811f5cfc0a186e55934e7c9077bc4e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:29:24 +0300 Subject: [PATCH 0648/1360] fix(font): recover glyph rendering and resource signatures --- .../libs/prx/libSceFont/src/Unimplemented.cpp | 21 ++++++++++--------- docs/dev/debt/libSceFont.md | 2 ++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index e4bb1c699f..bc6c9d98d0 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -15,6 +15,7 @@ struct FontStyleInformation; struct FontGlyphExpandBufferState; struct FontGraphicsResource; struct FontGraphicsFrame; +struct FontGraphicsDesignResource; struct FontGraphicsDeviceUsage; struct FontGraphicsFillMethod; struct FontGraphicsFillPlot; @@ -47,7 +48,7 @@ static_assert(offsetof(FontGraphicsRegion, innerRadius) == 28); extern "C" { -int APS5_VABI sceFontControl() { +int APS5_VABI sceFontControl(std::int32_t command, void* object, std::uintptr_t argument) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -108,17 +109,17 @@ int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandB return 0; } -int APS5_VABI sceFontGlyphRenderImage() { +int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGlyphRenderImageHorizontal() { +int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGlyphRenderImageVertical() { +int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -143,7 +144,7 @@ int APS5_VABI sceFontGraphicsEndFrame(FontGraphicsFrame* frame) { return 0; } -int APS5_VABI sceFontGraphicsExchangeResource() { +int APS5_VABI sceFontGraphicsExchangeResource(FontGraphicsResource* resource, FontGraphicsDesignResource* replacement, FontGraphicsDesignResource** previous) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -208,12 +209,12 @@ int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, floa return 0; } -int APS5_VABI sceFontGraphicsRelease() { +int APS5_VABI sceFontGraphicsRelease(void** object) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRenderResource() { +int APS5_VABI sceFontGraphicsRenderResource(FontGraphicsFrame* frame, FontGraphicsDesignResource* design, float x, float y, FontGraphicsResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -263,7 +264,7 @@ int APS5_VABI sceFontGraphicsSetupLocation(FontGraphicsResource* resource, float return 0; } -int APS5_VABI sceFontGraphicsSetupPositioning() { +int APS5_VABI sceFontGraphicsSetupPositioning(FontGraphicsResource* resource, std::uint32_t flags, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -348,7 +349,7 @@ int APS5_VABI sceFontGraphicsUpdateLocation(FontGraphicsResource* resource, floa return 0; } -int APS5_VABI sceFontGraphicsUpdatePositioning() { +int APS5_VABI sceFontGraphicsUpdatePositioning(FontGraphicsResource* resource, std::uint32_t flags, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -378,7 +379,7 @@ int APS5_VABI sceFontSupportGlyphs() { return 0; } -int APS5_VABI sceFontWordsFindWordCharacters() { +int APS5_VABI sceFontWordsFindWordCharacters(FontWords* words, FontTextCharacter* firstCharacter, FontTextCharacter* endCharacter, FontTextCharacter** lastCharacter, FontTextCharacter** nextCharacter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 2ba6558989..818b5183ab 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -28,3 +28,5 @@ - [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Device usage, frame and resource layouts remain opaque; their runtime implementation is still unavailable. + +Glyph image rendering uses six pointer arguments and two scalar coordinates, with existing style, surface, metrics and output layouts. Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. From 12eb05361afd24abda03d76d8a786ed8d2892fc1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:30:07 +0300 Subject: [PATCH 0649/1360] fix(font): recover canvas and texture construction signatures --- core/libs/prx/libSceFont/src/Unimplemented.cpp | 17 +++++++++++------ docs/dev/debt/libSceFont.md | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index bc6c9d98d0..1e3698f746 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -16,6 +16,11 @@ struct FontGlyphExpandBufferState; struct FontGraphicsResource; struct FontGraphicsFrame; struct FontGraphicsDesignResource; +struct FontGraphicsDesign; +struct FontGraphicsCanvas; +struct FontGraphicsCanvasSequence; +struct FontGraphicsSurface; +struct FontGraphicsSurfaceTexture; struct FontGraphicsDeviceUsage; struct FontGraphicsFillMethod; struct FontGraphicsFillPlot; @@ -289,27 +294,27 @@ int APS5_VABI sceFontGraphicsSetupShapeFillPlot(FontGraphicsResource* resource, return 0; } -int APS5_VABI sceFontGraphicsStructureCanvas() { +int APS5_VABI sceFontGraphicsStructureCanvas(FontGraphicsDevice* device, std::uint32_t width, std::uint32_t height, std::int32_t format, FontGraphicsCanvas** canvas) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureCanvasSequence() { +int APS5_VABI sceFontGraphicsStructureCanvasSequence(FontGraphicsDevice* device, FontGraphicsCanvas* canvas, const FontGraphicsSurface* surface, std::uint64_t flags, FontGraphicsCanvasSequence** sequence) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureDesign() { +int APS5_VABI sceFontGraphicsStructureDesign(FontGraphicsDevice* device, std::int32_t format, FontGraphicsDesign** design) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureDesignResource() { +int APS5_VABI sceFontGraphicsStructureDesignResource(FontGraphicsDevice* device, FontGraphicsDesign* design, FontGraphicsDesignResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureSurfaceTexture() { +int APS5_VABI sceFontGraphicsStructureSurfaceTexture(FontGraphicsDevice* device, const FontGraphicsSurface* surface, FontGraphicsSurfaceTexture** texture) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -389,7 +394,7 @@ int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { return 0; } -int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(void) { +int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, std::uint32_t firstMode, std::uint32_t secondMode, std::int32_t firstCount, std::int32_t firstLayout, std::int32_t secondLayout, std::int32_t secondCount, FontGraphicsFill** fill) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 818b5183ab..693d3c41e8 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -30,3 +30,5 @@ Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Device usage, frame and resource layouts remain opaque; their runtime implementation is still unavailable. Glyph image rendering uses six pointer arguments and two scalar coordinates, with existing style, surface, metrics and output layouts. Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. + +Canvas, design, surface texture and fill-pattern construction signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Pattern mode/count semantics and the graphics surface descriptor fields still require reconstruction; distinct opaque types preserve the confirmed object categories. From f775cecf378fff1ebb0c24c14993d0e96136c7e0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:30:49 +0300 Subject: [PATCH 0650/1360] fix(font): define fill descriptors and device usage layouts --- .../libs/prx/libSceFont/src/Unimplemented.cpp | 92 ++++++++++++++++++- docs/dev/debt/libSceFont.md | 4 +- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 1e3698f746..48854d5852 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -21,10 +21,6 @@ struct FontGraphicsCanvas; struct FontGraphicsCanvasSequence; struct FontGraphicsSurface; struct FontGraphicsSurfaceTexture; -struct FontGraphicsDeviceUsage; -struct FontGraphicsFillMethod; -struct FontGraphicsFillPlot; -struct FontGraphicsFillRates; struct FontGraphicsFill; struct FontGraphicsRegion { @@ -48,6 +44,94 @@ static_assert(offsetof(FontGraphicsRegion, tag) == 20); static_assert(offsetof(FontGraphicsRegion, outerRadius) == 24); static_assert(offsetof(FontGraphicsRegion, innerRadius) == 28); +struct FontGraphicsFillMethod { + std::int32_t mode; + std::int32_t type; + std::uint32_t flags; + std::uint32_t tag; + std::byte reserved[16]; +}; + +static_assert(sizeof(FontGraphicsFillMethod) == 32); +static_assert(offsetof(FontGraphicsFillMethod, mode) == 0); +static_assert(offsetof(FontGraphicsFillMethod, type) == 4); +static_assert(offsetof(FontGraphicsFillMethod, flags) == 8); +static_assert(offsetof(FontGraphicsFillMethod, tag) == 12); +static_assert(offsetof(FontGraphicsFillMethod, reserved) == 16); + +struct FontGraphicsFillPlot { + float mapping[4]; + const FontGraphicsRegion* region; + std::uint16_t tag; + std::uint16_t reserved26; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +static_assert(sizeof(FontGraphicsFillPlot) == 64); +static_assert(offsetof(FontGraphicsFillPlot, mapping) == 0); +static_assert(offsetof(FontGraphicsFillPlot, region) == 16); +static_assert(offsetof(FontGraphicsFillPlot, tag) == 24); +static_assert(offsetof(FontGraphicsFillPlot, reserved26) == 26); +static_assert(offsetof(FontGraphicsFillPlot, layout) == 28); +static_assert(offsetof(FontGraphicsFillPlot, flags) == 30); +static_assert(offsetof(FontGraphicsFillPlot, reserved) == 31); + +struct FontGraphicsFillRates { + float mapping[4]; + const FontGraphicsFill* fill; + std::uint16_t tag; + std::uint16_t effect; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +static_assert(sizeof(FontGraphicsFillRates) == 64); +static_assert(offsetof(FontGraphicsFillRates, mapping) == 0); +static_assert(offsetof(FontGraphicsFillRates, fill) == 16); +static_assert(offsetof(FontGraphicsFillRates, tag) == 24); +static_assert(offsetof(FontGraphicsFillRates, effect) == 26); +static_assert(offsetof(FontGraphicsFillRates, layout) == 28); +static_assert(offsetof(FontGraphicsFillRates, flags) == 30); +static_assert(offsetof(FontGraphicsFillRates, reserved) == 31); + +struct FontGraphicsPoolUsage { + std::uint32_t capacity; + std::uint32_t used; + std::uint32_t available; + std::uint32_t peak; + std::int32_t change; +}; + +static_assert(sizeof(FontGraphicsPoolUsage) == 20); +static_assert(offsetof(FontGraphicsPoolUsage, capacity) == 0); +static_assert(offsetof(FontGraphicsPoolUsage, used) == 4); +static_assert(offsetof(FontGraphicsPoolUsage, available) == 8); +static_assert(offsetof(FontGraphicsPoolUsage, peak) == 12); +static_assert(offsetof(FontGraphicsPoolUsage, change) == 16); + +struct FontGraphicsMemoryUsage { + FontGraphicsPoolUsage storage; + FontGraphicsPoolUsage allocations; + void* address; +}; + +static_assert(sizeof(FontGraphicsMemoryUsage) == 48); +static_assert(offsetof(FontGraphicsMemoryUsage, storage) == 0); +static_assert(offsetof(FontGraphicsMemoryUsage, allocations) == 20); +static_assert(offsetof(FontGraphicsMemoryUsage, address) == 40); + +struct FontGraphicsDeviceUsage { + FontGraphicsMemoryUsage first; + FontGraphicsMemoryUsage second; +}; + +static_assert(sizeof(FontGraphicsDeviceUsage) == 96); +static_assert(offsetof(FontGraphicsDeviceUsage, first) == 0); +static_assert(offsetof(FontGraphicsDeviceUsage, second) == 48); + #pragma GCC visibility push(default) diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 693d3c41e8..939b8a4dd2 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -27,8 +27,10 @@ - [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. -Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Device usage, frame and resource layouts remain opaque; their runtime implementation is still unavailable. +Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. Glyph image rendering uses six pointer arguments and two scalar coordinates, with existing style, surface, metrics and output layouts. Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. Canvas, design, surface texture and fill-pattern construction signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Pattern mode/count semantics and the graphics surface descriptor fields still require reconstruction; distinct opaque types preserve the confirmed object categories. + +[Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) now defines the 32-byte fill method, 64-byte fill plot/rates and 96-byte device usage layouts, with size and field-offset assertions. Uninterpreted descriptor tails remain reserved; pool statistics expose capacity, current usage, available bytes, peak and signed change. From add513ee50822c51154d580dd083d5d6db95a53c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:31:36 +0300 Subject: [PATCH 0651/1360] fix(font): restore glyph support and image fill arguments --- core/libs/prx/libSceFont/src/Unimplemented.cpp | 5 +++-- docs/dev/debt/libSceFont.md | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 48854d5852..49bdec210f 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -9,6 +9,7 @@ struct FontGraphicsDeviceParam; struct FontGraphicsService; struct FontGraphicsServiceParam; struct FontWords; +struct FontGlyphSupport; struct FontOutlineProfile; struct FontMetrics; struct FontStyleInformation; @@ -463,7 +464,7 @@ int APS5_VABI sceFontGraphicsUpdateShapeFillPlot(FontGraphicsResource* resource, return 0; } -int APS5_VABI sceFontSupportGlyphs() { +int APS5_VABI sceFontSupportGlyphs(const FontGlyphSupport* support) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -473,7 +474,7 @@ int APS5_VABI sceFontWordsFindWordCharacters(FontWords* words, FontTextCharacter return 0; } -int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { +int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, float x, float y, float width, float height, FontGraphicsFill** fill) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 939b8a4dd2..8d8ec0ff90 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -34,3 +34,5 @@ Glyph image rendering uses six pointer arguments and two scalar coordinates, wit Canvas, design, surface texture and fill-pattern construction signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Pattern mode/count semantics and the graphics surface descriptor fields still require reconstruction; distinct opaque types preserve the confirmed object categories. [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) now defines the 32-byte fill method, 64-byte fill plot/rates and 96-byte device usage layouts, with size and field-offset assertions. Uninterpreted descriptor tails remain reserved; pool statistics expose capacity, current usage, available bytes, peak and signed change. + +The [glyph support registration and texture-image fill signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered from native argument use. The image constructor takes a byte span and four scalar coordinates; the decompiler omits its successful path. `CreateWords` and `CreateGraphicsServiceWithEdition` consume the output pointer in the fourth general-purpose argument slot but do not read the third slot, whose source type is not established. From cfbff67da64abf2da5ae6e37dfcf4ec53ec39c86 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:32:14 +0300 Subject: [PATCH 0652/1360] fix(font): implement glyph attribute transitions and output --- core/libs/prx/libSceFont/src/Render.cpp | 19 +++++++--- core/libs/tests/GuestFont.cpp | 49 +++++++++++++------------ 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 3f78fa635a..7ed1af5165 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -420,11 +420,18 @@ int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, FontGlyph* pGlyph) { return SCE_FONT_OK; } -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, int attribute, int* oldAttribute) { - auto* generated = TryGetGeneratedGlyph(glyph); - if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; - if (oldAttribute) *oldAttribute = generated->attribute; - generated->attribute = attribute; +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, std::int32_t attribute, std::int32_t* previousAttribute) { + if (!glyph || glyph->magic != GLYPH_MAGIC) { + if (previousAttribute) *previousAttribute = 0; + return SCE_FONT_ERROR_INVALID_GLYPH; + } + if (attribute != 0x10 && attribute != 0x11) { + if (previousAttribute) *previousAttribute = 0; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + const std::int32_t previous = (glyph->flags & 2) ? 0x11 : 0x10; + glyph->flags = static_cast<std::uint16_t>((glyph->flags & ~2u) | ((attribute - 0x10) << 1)); + if (previousAttribute) *previousAttribute = previous; return SCE_FONT_OK; } @@ -433,7 +440,7 @@ int APS5_VABI sceFontGlyphGetAttribute(FontGlyph glyph, int attribute, int* nowA if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; auto* generated = TryGetGeneratedGlyph(glyph); if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; - *nowAttribute = generated->attribute; + *nowAttribute = (glyph->flags & 2) ? 0x11 : 0x10; return SCE_FONT_OK; } diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 8db5efd303..fc67507390 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -49,7 +49,7 @@ 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*); int APS5_VABI sceFontGenerateCharGlyph(FontHandle, std::uint32_t, const FontGenerateGlyphDetail*, FontGlyph*); -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, int, int*); +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::int32_t, std::int32_t*); int APS5_VABI sceFontGlyphGetAttribute(FontGlyph, int, int*); int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); int APS5_VABI sceFontTextSourceInit(FontTextSource*, const void*, std::uint32_t, FontTextParseFunction, void*); @@ -393,30 +393,31 @@ int main() { Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); FontGlyph glyph = nullptr; Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); - int glyphAttribute = -1; - Require(sceFontGlyphDefineAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); - Require(sceFontGlyphGetAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); - glyphAttribute = -1; - Require(sceFontGlyphDefineAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); - Require(sceFontGlyphDefineAttribute(glyph, 5, nullptr) == SCE_FONT_OK); - glyphAttribute = -1; - Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); - Require(sceFontGlyphGetAttribute(glyph, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); - glyphAttribute = -1; - Require(sceFontGlyphDefineAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); - Require(sceFontGlyphGetAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); + const auto originalFlags = glyph->flags; + std::int32_t previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0x11, &previousAttribute) == SCE_FONT_OK); + Require(previousAttribute == ((originalFlags & 2) ? 0x11 : 0x10)); + Require(glyph->flags == (originalFlags | 2)); + Require(sceFontGlyphDefineAttribute(glyph, 0x10, &previousAttribute) == SCE_FONT_OK && previousAttribute == 0x11); + Require(glyph->flags == (originalFlags & ~2u)); + Require(sceFontGlyphDefineAttribute(glyph, 0x10, &previousAttribute) == SCE_FONT_OK && previousAttribute == 0x10); + Require(sceFontGlyphDefineAttribute(glyph, 0x11, nullptr) == SCE_FONT_OK); + for (std::int32_t attribute = -1; attribute <= 256; ++attribute) { + if (attribute == 0x10 || attribute == 0x11) continue; + previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, attribute, &previousAttribute) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(previousAttribute == 0 && glyph->flags == (originalFlags | 2)); + } + previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, &previousAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && previousAttribute == 0); + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_GLYPH); FontGlyphOpaque notAGlyph{}; - Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); - Require(sceFontGlyphGetAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); - FontGlyph otherGlyph = nullptr; - Require(sceFontGenerateCharGlyph(font, 'V', nullptr, &otherGlyph) == SCE_FONT_OK && otherGlyph != nullptr); - glyphAttribute = -1; - Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); - Require(sceFontGlyphDefineAttribute(otherGlyph, 9, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); - glyphAttribute = -1; - Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 9); - Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); - Require(sceFontDeleteGlyph(&memory, &otherGlyph) == SCE_FONT_OK && otherGlyph == nullptr); + previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &previousAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && previousAttribute == 0); + int glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + Require(sceFontGlyphGetAttribute(glyph, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGlyphGetAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); Require(sceFontDeleteGlyph(&memory, &glyph) == SCE_FONT_OK && glyph == nullptr); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); From 86c1dadddeaf7cfcaef85bd820f3e9c7e0be3dc2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:33:19 +0300 Subject: [PATCH 0653/1360] fix(font): match pixel resolution validation and locking --- core/libs/prx/libSceFont/src/Library.cpp | 16 +++++++++++++--- core/libs/tests/GuestFont.cpp | 5 ++++- docs/dev/debt/libSceFont.md | 1 - 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Library.cpp b/core/libs/prx/libSceFont/src/Library.cpp index e872a3fa66..a3e01002c3 100644 --- a/core/libs/prx/libSceFont/src/Library.cpp +++ b/core/libs/prx/libSceFont/src/Library.cpp @@ -5,6 +5,7 @@ #include <atomic> #include <cstring> #include <limits> +#include <memory> #include "prx/libSceFont/include/FontInternal.hpp" @@ -298,10 +299,19 @@ int APS5_VABI sceFontRendererSetOutlineBufferPolicy(FontRenderer fontRenderer, s } int APS5_VABI sceFontGetPixelResolution(FontLibrary library, std::uint32_t* subPixelCount) { + auto* lib = static_cast<FontLibNative*>(library); + std::uint32_t previous = 0; + if (!lib || !AcquireLibraryLock(lib, previous)) { + if (subPixelCount) *subPixelCount = 0; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } + const auto unlock = [previous](FontLibNative* locked) { ReleaseLibraryLock(locked, previous); }; + const std::unique_ptr<FontLibNative, decltype(unlock)> lock(lib, unlock); + if (!lib->sys_driver || !lib->sys_driver->pixel_resolution) { + if (subPixelCount) *subPixelCount = 0; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } if (!subPixelCount) return SCE_FONT_ERROR_INVALID_PARAMETER; - *subPixelCount = 0; - const auto* lib = static_cast<const FontLibNative*>(library); - if (!lib || lib->magic != LIBRARY_MAGIC || !lib->sys_driver || !lib->sys_driver->pixel_resolution) return SCE_FONT_ERROR_INVALID_LIBRARY; *subPixelCount = lib->sys_driver->pixel_resolution(); return SCE_FONT_OK; } diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index fc67507390..ac87c2de70 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -238,7 +238,7 @@ int main() { std::uint32_t subPixelCount = 1; Require(sceFontGetPixelResolution(library, &subPixelCount) == SCE_FONT_OK && subPixelCount == 64); Require(sceFontGetPixelResolution(library, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); - Require(sceFontGetPixelResolution(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetPixelResolution(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); subPixelCount = 1; Require(sceFontGetPixelResolution(nullptr, &subPixelCount) == SCE_FONT_ERROR_INVALID_LIBRARY && subPixelCount == 0); FontHandleOpaque notALibrary{}; @@ -250,8 +250,11 @@ int main() { Require(sceFontCreateLibrary(&memory, &coarseDriver, &coarseLibrary) == SCE_FONT_OK && coarseLibrary != nullptr); Require(sceFontGetPixelResolution(coarseLibrary, &subPixelCount) == SCE_FONT_OK && subPixelCount == 16); Require(sceFontGetPixelResolution(library, &subPixelCount) == SCE_FONT_OK && subPixelCount == 64); + Require(static_cast<Font::FontLibNative*>(coarseLibrary)->lock_word == 0); coarseDriver.pixel_resolution = nullptr; Require(sceFontGetPixelResolution(coarseLibrary, &subPixelCount) == SCE_FONT_ERROR_INVALID_LIBRARY && subPixelCount == 0); + Require(sceFontGetPixelResolution(coarseLibrary, nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(static_cast<Font::FontLibNative*>(coarseLibrary)->lock_word == 0); Require(sceFontDestroyLibrary(&coarseLibrary) == SCE_FONT_OK && coarseLibrary == nullptr); void* cacheBuffer = nullptr; diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 8d8ec0ff90..919ecce40a 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -4,7 +4,6 @@ - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet - [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` - [sceFontDettachDeviceCacheBuffer](../../../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. It detaches the cache buffer attached by `sceFontAttachDeviceCacheBuffer`, returns the buffer and its size, and reports `SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER` when no cache buffer is attached - [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` From 581bbc18228dc6f37fe65961e6414ca75a2844d5 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 19:33:19 +0300 Subject: [PATCH 0654/1360] docs(contentsearch): condense remaining query and metadata debt --- docs/dev/debt/libSceContentSearch.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index 8ef6373ca4..6df6de62ff 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -6,4 +6,5 @@ ### Functional -- [libSceContentSearch](../../../core/libs/prx/libSceContentSearch/Export.cpp) searches a content library that is empty on the host, as nothing is exported to it. The signatures follow [AlexAltea/orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L555-L569) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentSearch.cs#L255-L316), which agree; the filter and sort layouts and enums, the limit of 92 rows, `0x809D100A` for a larger limit and `0x809D1001` before `sceContentSearchInit` are SharpProspero's. `sceContentSearchGetNumOfContent`, `sceContentSearchGetTotalContentSize` and `sceContentSearchSearchContent` report 0 rows and 0 bytes without writing a content row, and the update id they and `sceContentSearchGetContentLastUpdateId` report is always 0; no reference gives the console's update id for a library that never changed. Filters and sort conditions are only checked against SharpProspero's enums, and an empty nested column set, a null filter value or an unknown enum value throws, as their validation is not yet matched to the native query builder; a null column set with length 0 is taken as no filter. `sceContentSearchOpenMetadata` and `sceContentSearchOpenMetadataByContentId` throw, as no reference gives the error for a path or content id that is not in the library, so no metadata id is ever handed out; metadata-field and value queries with valid pointers still throw because field-dependent errors remain unresolved. Every title is taken to hold the content-search permission (SharpProspero's `0x809D1013` is never returned). Initialization checks and outer query pointer/count/limit precedence follow the supplied module. Guest heap allocation, platform-dependent minimum heap size and nested query validation remain unimplemented +- [Content queries](../../../core/libs/prx/libSceContentSearch/Export.cpp) use an empty host library and always report update id 0; the console's initial update id is unverified. No content rows are produced. Every title is assumed to have search permission. Nested filter/sort validation is incomplete: empty nested column sets, null filter values and unknown enum values throw. Guest heap allocation and the platform-dependent minimum heap size are unimplemented. Initialization and outer pointer/count/limit validation follow the supplied module. +- [Metadata access](../../../core/libs/prx/libSceContentSearch/Export.cpp) cannot open metadata by path or content id because missing-content errors remain unresolved. No metadata handles are issued. Queries reject invalid outer pointers but still throw for valid pointers because errors depend on the requested field; closing an unopened handle returns the confirmed invalid-argument error. From e544f181d1b4713a23951cf4a1594a19e5c147b7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:12:44 +0300 Subject: [PATCH 0655/1360] fix(ult): recover synchronization and ulthread signatures --- core/libs/prx/libSceUlt/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 10ba814b81..2d71ab6749 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -912,7 +912,7 @@ int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthrea return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); } -int APS5_VABI sceUltConditionVariableSignalAll() { +int APS5_VABI sceUltConditionVariableSignalAll(void* conditionVariable) { NotImplemented_nid_no_patch(__func__); return 0; } From 7d14ba91a49b75157c5149bc0cf62257d1cbb9aa Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:14:49 +0300 Subject: [PATCH 0656/1360] fix(ult): define diagnostic layouts and output arguments --- core/libs/prx/libSceUlt/Export.cpp | 18 +-- core/libs/prx/libSceUlt/SceUltTypes.hpp | 199 ++++++++++++++++++++++++ core/libs/tests/GuestUltThread.cpp | 9 +- docs/dev/debt/libSceUlt.md | 3 +- 4 files changed, 217 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 2d71ab6749..8f15feec36 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -917,47 +917,47 @@ int APS5_VABI sceUltConditionVariableSignalAll(void* conditionVariable) { return 0; } -int APS5_VABI sceUltGetConditionVariableInfo() { +int APS5_VABI sceUltGetConditionVariableInfo(void* object, UltConditionVariableInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetMutexInfo() { +int APS5_VABI sceUltGetMutexInfo(void* object, UltMutexInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetQueueDataResourcePoolInfo() { +int APS5_VABI sceUltGetQueueDataResourcePoolInfo(void* object, UltQueueDataResourcePoolInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetQueueInfo() { +int APS5_VABI sceUltGetQueueInfo(void* object, UltQueueInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetReaderWriterLockInfo() { +int APS5_VABI sceUltGetReaderWriterLockInfo(void* object, UltReaderWriterLockInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetSemaphoreInfo() { +int APS5_VABI sceUltGetSemaphoreInfo(void* object, UltSemaphoreInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetUlthreadInfo() { +int APS5_VABI sceUltGetUlthreadInfo(void* object, UltUlthreadInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetUlthreadRuntimeInfo() { +int APS5_VABI sceUltGetUlthreadRuntimeInfo(void* runtime, UltUlthreadRuntimeInfo* info, void** ulthreads, std::uint32_t ulthreadCapacity, Pthread* workerThreads, std::uint32_t workerCapacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { +int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo(void* object, UltWaitingQueueResourcePoolInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index efe53ec5a8..e02d67f2b5 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -5,6 +5,8 @@ #include <condition_variable> #include <cstdint> #include <cstring> + +#include <cstddef> #include <memory> #include <list> #include <mutex> @@ -33,6 +35,203 @@ inline int InitializeOptParam(UltOptParam* optParam) { return ULT_OK; } +struct UltWaitingThreadInfo { + std::uint64_t size; + void* thread; + std::uint32_t threadType; + std::int32_t waitValue; + std::byte reserved[40]; +}; + +static_assert(sizeof(UltWaitingThreadInfo) == 64); +static_assert(offsetof(UltWaitingThreadInfo, size) == 0); +static_assert(offsetof(UltWaitingThreadInfo, thread) == 8); +static_assert(offsetof(UltWaitingThreadInfo, threadType) == 16); +static_assert(offsetof(UltWaitingThreadInfo, waitValue) == 20); +static_assert(offsetof(UltWaitingThreadInfo, reserved) == 24); + +struct UltConditionVariableInfo { + std::uint64_t size; + void* object; + char name[32]; + void* mutex; + std::uint32_t waitingThreads; + std::byte reserved[68]; +}; + +static_assert(sizeof(UltConditionVariableInfo) == 128); +static_assert(offsetof(UltConditionVariableInfo, size) == 0); +static_assert(offsetof(UltConditionVariableInfo, object) == 8); +static_assert(offsetof(UltConditionVariableInfo, name) == 16); +static_assert(offsetof(UltConditionVariableInfo, mutex) == 48); +static_assert(offsetof(UltConditionVariableInfo, waitingThreads) == 56); +static_assert(offsetof(UltConditionVariableInfo, reserved) == 60); + +struct UltMutexInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + void* owner; + std::uint32_t ownerType; + std::uint32_t attributes; + std::uint32_t lockCount; + std::uint32_t waitingThreads; + std::byte reserved[48]; +}; + +static_assert(sizeof(UltMutexInfo) == 128); +static_assert(offsetof(UltMutexInfo, size) == 0); +static_assert(offsetof(UltMutexInfo, object) == 8); +static_assert(offsetof(UltMutexInfo, name) == 16); +static_assert(offsetof(UltMutexInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltMutexInfo, owner) == 56); +static_assert(offsetof(UltMutexInfo, ownerType) == 64); +static_assert(offsetof(UltMutexInfo, attributes) == 68); +static_assert(offsetof(UltMutexInfo, lockCount) == 72); +static_assert(offsetof(UltMutexInfo, waitingThreads) == 76); +static_assert(offsetof(UltMutexInfo, reserved) == 80); + +struct UltSemaphoreInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + std::uint32_t resources; + std::uint32_t waitingThreads; + std::byte reserved[64]; +}; + +static_assert(sizeof(UltSemaphoreInfo) == 128); +static_assert(offsetof(UltSemaphoreInfo, size) == 0); +static_assert(offsetof(UltSemaphoreInfo, object) == 8); +static_assert(offsetof(UltSemaphoreInfo, name) == 16); +static_assert(offsetof(UltSemaphoreInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltSemaphoreInfo, resources) == 56); +static_assert(offsetof(UltSemaphoreInfo, waitingThreads) == 60); +static_assert(offsetof(UltSemaphoreInfo, reserved) == 64); + +struct UltReaderWriterLockInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + std::uint32_t lockType; + std::uint32_t readers; + void* owner; + std::uint32_t ownerType; + std::uint32_t waitingThreads; + std::byte reserved[48]; +}; + +static_assert(sizeof(UltReaderWriterLockInfo) == 128); +static_assert(offsetof(UltReaderWriterLockInfo, size) == 0); +static_assert(offsetof(UltReaderWriterLockInfo, object) == 8); +static_assert(offsetof(UltReaderWriterLockInfo, name) == 16); +static_assert(offsetof(UltReaderWriterLockInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltReaderWriterLockInfo, lockType) == 56); +static_assert(offsetof(UltReaderWriterLockInfo, readers) == 60); +static_assert(offsetof(UltReaderWriterLockInfo, owner) == 64); +static_assert(offsetof(UltReaderWriterLockInfo, ownerType) == 72); +static_assert(offsetof(UltReaderWriterLockInfo, waitingThreads) == 76); +static_assert(offsetof(UltReaderWriterLockInfo, reserved) == 80); + +struct UltQueueInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + void* dataPool; + std::uint64_t dataSize; + std::uint32_t queuedItems; + std::uint32_t waitingPushers; + std::uint32_t waitingPoppers; + std::byte reserved[172]; +}; + +static_assert(sizeof(UltQueueInfo) == 256); +static_assert(offsetof(UltQueueInfo, size) == 0); +static_assert(offsetof(UltQueueInfo, object) == 8); +static_assert(offsetof(UltQueueInfo, name) == 16); +static_assert(offsetof(UltQueueInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltQueueInfo, dataPool) == 56); +static_assert(offsetof(UltQueueInfo, dataSize) == 64); +static_assert(offsetof(UltQueueInfo, queuedItems) == 72); +static_assert(offsetof(UltQueueInfo, waitingPushers) == 76); +static_assert(offsetof(UltQueueInfo, waitingPoppers) == 80); +static_assert(offsetof(UltQueueInfo, reserved) == 84); + +struct UltWaitingQueueResourcePoolInfo { + std::uint64_t size; + void* object; + char name[32]; + std::uint32_t availableThreads; + std::uint32_t maxThreads; + std::uint32_t availableSyncObjects; + std::uint32_t maxSyncObjects; + std::byte reserved[64]; +}; + +static_assert(sizeof(UltWaitingQueueResourcePoolInfo) == 128); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, size) == 0); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, object) == 8); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, name) == 16); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, availableThreads) == 48); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, maxThreads) == 52); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, availableSyncObjects) == 56); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, maxSyncObjects) == 60); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, reserved) == 64); + +struct UltQueueDataResourcePoolInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + std::uint64_t dataSize; + std::uint32_t availableItems; + std::uint32_t maxItems; + std::uint32_t availableQueues; + std::uint32_t maxQueues; + std::byte reserved[48]; +}; + +static_assert(sizeof(UltQueueDataResourcePoolInfo) == 128); +static_assert(offsetof(UltQueueDataResourcePoolInfo, size) == 0); +static_assert(offsetof(UltQueueDataResourcePoolInfo, object) == 8); +static_assert(offsetof(UltQueueDataResourcePoolInfo, name) == 16); +static_assert(offsetof(UltQueueDataResourcePoolInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltQueueDataResourcePoolInfo, dataSize) == 56); +static_assert(offsetof(UltQueueDataResourcePoolInfo, availableItems) == 64); +static_assert(offsetof(UltQueueDataResourcePoolInfo, maxItems) == 68); +static_assert(offsetof(UltQueueDataResourcePoolInfo, availableQueues) == 72); +static_assert(offsetof(UltQueueDataResourcePoolInfo, maxQueues) == 76); +static_assert(offsetof(UltQueueDataResourcePoolInfo, reserved) == 80); + +struct UltUlthreadRuntimeInfo { + std::uint64_t size; + void* object; + char name[32]; + std::uint32_t activeUlthreads; + std::uint32_t maxUlthreads; + std::uint32_t workerThreads; + std::uint32_t reserved60; + std::uint64_t workerStackSize; + std::byte reserved[56]; +}; + +static_assert(sizeof(UltUlthreadRuntimeInfo) == 128); +static_assert(offsetof(UltUlthreadRuntimeInfo, size) == 0); +static_assert(offsetof(UltUlthreadRuntimeInfo, object) == 8); +static_assert(offsetof(UltUlthreadRuntimeInfo, name) == 16); +static_assert(offsetof(UltUlthreadRuntimeInfo, maxUlthreads) == 52); +static_assert(offsetof(UltUlthreadRuntimeInfo, activeUlthreads) == 48); +static_assert(offsetof(UltUlthreadRuntimeInfo, workerThreads) == 56); +static_assert(offsetof(UltUlthreadRuntimeInfo, reserved60) == 60); +static_assert(offsetof(UltUlthreadRuntimeInfo, workerStackSize) == 64); +static_assert(offsetof(UltUlthreadRuntimeInfo, reserved) == 72); + +struct UltUlthreadInfo; + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 0d074c1d20..8a77a7e890 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -8,6 +8,9 @@ #include <thread> #include <stdexcept> +struct UltConditionVariableInfo; +struct UltWaitingThreadInfo; + extern "C" { int APS5_VABI sceUltInitialize(); int APS5_VABI sceUltFinalize(); @@ -38,7 +41,8 @@ int APS5_VABI sceUltSemaphoreDestroy(void*); int APS5_VABI sceUltQueueDestroy(void*); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); int APS5_VABI sceUltConditionVariableDestroy(void*); -int APS5_VABI sceUltGetConditionVariableInfo(); +int APS5_VABI sceUltReaderWriterLockDestroy(void*); +int APS5_VABI sceUltGetConditionVariableInfo(void*, UltConditionVariableInfo*, UltWaitingThreadInfo*, std::uint32_t); } static constexpr int Ok = 0; @@ -110,9 +114,10 @@ static void Validation() { CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); + CHECK(sceUltReaderWriterLockDestroy(nullptr) == ULT_ERROR_NULL); bool threwInfo = false; try { - sceUltGetConditionVariableInfo(); + sceUltGetConditionVariableInfo(nullptr, nullptr, nullptr, 0); } catch (const std::runtime_error&) { threwInfo = true; } diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index b55da9ee7f..7f17598f56 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -8,7 +8,8 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified - [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured -- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. The nested context in `UltUlthreadInfo` still requires reconstruction. + - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional From b469744334b5df035295fc14c59b9a3d2e031f00 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:15:19 +0300 Subject: [PATCH 0657/1360] fix(ult): match running thread and runtime destruction errors --- core/libs/prx/libSceUlt/Export.cpp | 4 ++-- core/libs/tests/GuestUltThread.cpp | 7 ++++--- docs/dev/debt/libSceUlt.md | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 8f15feec36..f449f06dba 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -824,7 +824,7 @@ int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { return ULT_ERROR_STATE; } if (!it->second->_exited.load(std::memory_order_acquire)) { - return ULT_ERROR_BUSY; + return ULT_ERROR_AGAIN; } } return sceUltUlthreadJoin(ulthread, status); @@ -869,7 +869,7 @@ int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { } for (const auto& entry : gUlthreads) { if (entry.second->_runtime == runtime) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } } gRuntimes.erase(it); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 8a77a7e890..9f4a146251 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -49,6 +49,7 @@ static constexpr int Ok = 0; static constexpr int Null = -2139029503; static constexpr int State = -2139029498; static constexpr int Busy = -2139029497; +static constexpr int Again = -2139029496; static void Require(bool value, int line) { if (!value) { @@ -139,9 +140,9 @@ static void TryJoin() { const int running = sceUltUlthreadTryJoin(&thread, &status); gRelease.store(true, std::memory_order_release); int joined; - while ((joined = sceUltUlthreadTryJoin(&thread, &status)) == Busy) std::this_thread::yield(); + while ((joined = sceUltUlthreadTryJoin(&thread, &status)) == Again) std::this_thread::yield(); - CHECK(running == Busy); + CHECK(running == Again); CHECK(joined == Ok && status == 0x3C); CHECK(sceUltUlthreadTryJoin(&thread, &status) == State); CHECK(sceUltUlthreadTryJoin(nullptr, &status) == Null); @@ -158,7 +159,7 @@ static void Destroy() { CHECK(_sceUltUlthreadRuntimeCreate(&other, "other", 1, 1, nullptr, nullptr, 0) == Ok); CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0x11, nullptr, 0, &runtime, nullptr, 0) == Ok); CHECK(sceUltUlthreadRuntimeDestroy(&other) == Ok); - CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Busy); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); std::int32_t status = 0; CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x11); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 7f17598f56..69ec383a4a 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -4,7 +4,7 @@ - [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. -- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware +- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified - [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured From eb0ff1e5de73e01716e9bef786d3eb7124f0dafd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:16:46 +0300 Subject: [PATCH 0658/1360] fix(audiopropagation): recover diagnostics and report callback abi --- .../AudioPropagation.hpp | 40 +++++++++++++++++++ .../prx/libSceAudioPropagation/Export.cpp | 20 +++++----- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 602d531cfa..d9351c0489 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -23,6 +23,46 @@ inline constexpr std::size_t PortalParamsSize = 0x60; inline constexpr std::uint32_t PathEntryId = 0x010107d9; inline constexpr std::size_t PathEntrySize = 0x28; +struct SourceInfo; +struct SystemInfo; +struct ReportRegistration; + +struct ReportArguments { + std::uint32_t generalOffset; + std::uint32_t floatingOffset; + void* overflowArguments; + void* savedRegisters; +}; + +struct ReportInfo { + std::uint64_t size; + AudioPropagationHandle handle; + std::uint32_t category; + std::int32_t result; + std::int32_t operation; + std::uint32_t reserved; + const char* function; + const char* format; + ReportArguments arguments; +}; + +using ReportHandler = void (APS5_VABI*)(const ReportInfo* report, void* userData); + +static_assert(sizeof(ReportArguments) == 24); +static_assert(offsetof(ReportArguments, generalOffset) == 0); +static_assert(offsetof(ReportArguments, floatingOffset) == 4); +static_assert(offsetof(ReportArguments, overflowArguments) == 8); +static_assert(offsetof(ReportArguments, savedRegisters) == 16); +static_assert(sizeof(ReportInfo) == 72); +static_assert(offsetof(ReportInfo, size) == 0); +static_assert(offsetof(ReportInfo, handle) == 8); +static_assert(offsetof(ReportInfo, category) == 16); +static_assert(offsetof(ReportInfo, result) == 20); +static_assert(offsetof(ReportInfo, operation) == 24); +static_assert(offsetof(ReportInfo, function) == 32); +static_assert(offsetof(ReportInfo, format) == 40); +static_assert(offsetof(ReportInfo, arguments) == 48); + struct Attribute { std::uint32_t id; std::uint32_t reserved0; diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 474117fe6f..a5534a4e45 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -315,32 +315,32 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy return 0; } -int APS5_VABI sceAudioPropagationPathGetNumPoints() { +int APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationReportApi() { +int APS5_VABI sceAudioPropagationReportApi(std::uint32_t category, AudioPropagationHandle handle, std::int32_t result, std::int32_t operation, const char* function, const char* format, ...) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationResetAttributes() { +int APS5_VABI sceAudioPropagationResetAttributes(Attribute* attributes, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationSourceQueryInfo() { +int APS5_VABI sceAudioPropagationSourceQueryInfo(AudioPropagationHandle source, SourceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationSystemLock() { +int APS5_VABI sceAudioPropagationSystemLock(AudioPropagationHandle system) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationSystemQueryInfo() { +int APS5_VABI sceAudioPropagationSystemQueryInfo(AudioPropagationHandle system, SystemInfo* info, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -395,22 +395,22 @@ int APS5_VABI sceAudioPropagationSystemOptionInit() { return 0; } -int APS5_VABI sceAudioPropagationReportRegisterHandler() { +int APS5_VABI sceAudioPropagationReportRegisterHandler(std::uint32_t category, ReportHandler handler, void* userData, ReportRegistration** registration) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationReportUnregisterHandler() { +int APS5_VABI sceAudioPropagationReportUnregisterHandler(ReportRegistration* registration) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationSystemEnumHandles() { +int APS5_VABI sceAudioPropagationSystemEnumHandles(AudioPropagationHandle* handles, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioPropagationSystemUnlock() { +int APS5_VABI sceAudioPropagationSystemUnlock(AudioPropagationHandle system) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 30802ce4c7..8fefea085b 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -8,4 +8,4 @@ - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. - [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 -- [libSceAudioPropagation firmware exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - `sceAudioPropagationPathGetNumPoints`, `sceAudioPropagationReportApi`, `sceAudioPropagationResetAttributes`, `sceAudioPropagationSourceQueryInfo`, `sceAudioPropagationSystemLock`, `sceAudioPropagationSystemQueryInfo`, `sceAudioPropagationAudioPathInit`, `sceAudioPropagationAudioPathPointInit`, `sceAudioPropagationMaterialInit`, `sceAudioPropagationPortalSettingsInit`, `sceAudioPropagationRayInit`, `sceAudioPropagationSourceRenderInfoInit`, `sceAudioPropagationSourceSetAudioPathsParamInit`, `sceAudioPropagationSystem`, `sceAudioPropagationSystemMemoryInit`, `sceAudioPropagationSystemOptionInit`, `sceAudioPropagationReportRegisterHandler`, `sceAudioPropagationReportUnregisterHandler`, `sceAudioPropagationSystemEnumHandles`, `sceAudioPropagationSystemUnlock` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From db3affa05b54989cae354b1bce510dbaaacb7e8a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:17:50 +0300 Subject: [PATCH 0659/1360] fix(audiopropagation): match audio path lookup errors --- core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp | 4 ++++ core/libs/prx/libSceAudioPropagation/Export.cpp | 8 +++++--- core/libs/tests/GuestAudioPropagation.cpp | 6 +++++- docs/dev/debt/libSceAudioPropagation.md | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index d9351c0489..6c5f9232f0 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -9,6 +9,10 @@ namespace AudioPropagation { +inline constexpr std::int32_t ErrorInvalidArgument = static_cast<std::int32_t>(0x8A700001); +inline constexpr std::int32_t ErrorInvalidHandle = static_cast<std::int32_t>(0x8A700002); +inline constexpr std::int32_t ErrorNullPointer = static_cast<std::int32_t>(0x8A700003); + inline constexpr std::uint32_t MaterialId = 0x010107d1; inline constexpr std::size_t MaterialSize = 0x40; inline constexpr std::uint32_t SystemMemoryId = 0x010107d4; diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index a5534a4e45..1a00395331 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -258,9 +258,11 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagati std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath) { auto& state = registry(); std::lock_guard lock(state.mutex); - requireObject(state, source, Kind::Source, __func__); - static_cast<void>(outPath); - throw std::invalid_argument(std::string(__func__) + ": index " + std::to_string(index) + " out of range (0 audio paths)"); + const auto found = state.objects.find(source); + if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; + if (!outPath) return ErrorNullPointer; + static_cast<void>(index); + return ErrorInvalidArgument; } std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count) { diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index c4d112d342..80cb31d799 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -122,7 +122,11 @@ int main() { std::uint32_t count = 7; Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == 0, "source reports audio paths"); AudioPropagationHandle path = 0; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceGetAudioPath(source, 0, &path); }, "audio path without paths"); + path = 0x1234; + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path without paths"); + Require(sceAudioPropagationSourceGetAudioPath(source, UINT32_MAX, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path index range"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, nullptr) == AudioPropagation::ErrorNullPointer, "audio path null output"); + Require(sceAudioPropagationSourceGetAudioPath(0, 0, nullptr) == AudioPropagation::ErrorInvalidHandle, "audio path handle precedence"); std::uint8_t rays[AudioPropagation::RaySize * 2]{}; count = 7; diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 8fefea085b..3e3dd69e2a 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. -- [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 +- [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` output width and invalid-handle/null-output/index errors are confirmed. 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. Other functions still throw for invalid input; their native validation precedence is not yet implemented - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From c0e20df906bb22a241e16ba8191254c3629e0423 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:20:35 +0300 Subject: [PATCH 0660/1360] fix(audiopropagation): reject invalid path calculation inputs --- .../prx/libSceAudioPropagation/AudioPropagation.hpp | 2 +- core/libs/prx/libSceAudioPropagation/Export.cpp | 13 ++++++------- core/libs/tests/GuestAudioPropagation.cpp | 8 ++++++-- docs/dev/debt/libSceAudioPropagation.md | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 6c5f9232f0..538b6a867f 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -123,7 +123,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAttributes(AudioPropagationHa std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagationHandle source, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath); std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count); -std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount); +std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain); std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const AudioPropagation::RenderInfo* infos, std::uint32_t count); diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 1a00395331..d1af51030e 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -274,15 +274,14 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle s return 0; } -std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount) { +std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount) { auto& state = registry(); std::lock_guard lock(state.mutex); - requireObject(state, source, Kind::Source, __func__); - static_cast<void>(rays); - static_cast<void>(flags); - static_cast<void>(paths); - requireNoPropagation(rayCount == 0 && pathCount == 0, __func__); - return 0; + const auto found = state.objects.find(source); + if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; + if (!rays || !paths) return ErrorNullPointer; + if (rayCount != 6 || reflectionOrder == 0 || pathCount == 0) return ErrorInvalidArgument; + throw std::runtime_error(__func__); } std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count) { diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 80cb31d799..ebb42f21d9 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -136,8 +136,12 @@ int main() { RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceGetRays(source, rays, nullptr); }, "rays without count"); Require(sceAudioPropagationSystemSetRays(system, rays, 0) == 0, "empty ray results failed"); RequireThrows<std::runtime_error>([&] { sceAudioPropagationSystemSetRays(system, rays, 1); }, "unrequested ray results"); - Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, nullptr, 0) == 0, "empty path calculation failed"); - RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceCalculateAudioPaths(source, rays, 1, 1, nullptr, 0); }, "path calculation with rays"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, nullptr, 0) == AudioPropagation::ErrorNullPointer, "path calculation null output"); + Require(sceAudioPropagationSourceCalculateAudioPaths(0, nullptr, 0, 0, nullptr, 0) == AudioPropagation::ErrorInvalidHandle, "path calculation handle precedence"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, rays, 6) == AudioPropagation::ErrorInvalidArgument, "path calculation empty rays"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 0, rays, 6) == AudioPropagation::ErrorInvalidArgument, "path calculation zero reflection order"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 1, rays, 0) == AudioPropagation::ErrorInvalidArgument, "path calculation zero output capacity"); + RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 1, rays, 6); }, "unsupported path calculation"); Require(sceAudioPropagationSourceSetAudioPaths(source, nullptr, 0) == 0, "empty audio paths failed"); RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceSetAudioPaths(source, rays, 1); }, "set audio paths"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 3e3dd69e2a..b1d100771e 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,10 +2,10 @@ ### Silent stubs -- [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` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no 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. +- [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 and audio paths are accepted only when empty and throw otherwise; path calculation returns the confirmed initial validation errors and throws for unsupported propagation; `sceAudioPropagationSourceRender` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no 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. ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. -- [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` output width and invalid-handle/null-output/index errors are confirmed. 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. Other functions still throw for invalid input; their native validation precedence is not yet implemented +- [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` selects reflection order; descriptor validation, ray ownership and path generation remain unimplemented; `sceAudioPropagationSourceGetAudioPath` output width and invalid-handle/null-output/index errors are confirmed. 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. Other functions still throw for invalid input; their native validation precedence is not yet implemented - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From 3251852138eb1027ca51a634336cb1a24671ac39 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:22:08 +0300 Subject: [PATCH 0661/1360] fix(rudp): recover poll signatures and event layout --- core/libs/SceTypes.hpp | 10 ++++++++++ core/libs/prx/libSceRudp/Export.cpp | 12 +++--------- docs/dev/debt/libSceRudp.md | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index f7b002c4d9..978123ba23 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1929,6 +1929,16 @@ struct PlayGoDialogResult { std::int32_t reserved[9]; }; +struct RudpPollEvent { + std::int32_t contextId; + std::uint16_t events; + std::uint16_t returnedEvents; +}; + +static_assert(sizeof(RudpPollEvent) == 8); +static_assert(offsetof(RudpPollEvent, events) == 4); +static_assert(offsetof(RudpPollEvent, returnedEvents) == 6); + using RudpEventHandler = void (*)(int ctx_id, int event_id, int error_code, void* arg); struct SystemServiceStatus { diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 923b4e9a76..2b17a8740b 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -89,16 +89,10 @@ GuestRudpEventHandler g_handler = nullptr; void* g_handler_arg = nullptr; struct RudpPoll { - uint32_t capacity; + std::size_t capacity; std::map<int, uint16_t> entries; }; -struct RudpPollEvent { - int ctxId; - uint16_t reqEvents; - uint16_t rtnEvents; -}; -static_assert(sizeof(RudpPollEvent) == 8); int g_next_poll = 1; std::map<int, RudpPoll> g_polls; @@ -386,7 +380,7 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } -int APS5_VABI sceRudpPollCreate(uint32_t size) { +int APS5_VABI sceRudpPollCreate(std::size_t size) { std::lock_guard<std::mutex> lk(g_mutex); if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; @@ -440,7 +434,7 @@ int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) return 0; } -int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, uint32_t eventLen, uint64_t timeout) { +int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eventLen, uint64_t timeout) { std::unique_lock<std::mutex> lk(g_mutex); if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index d6337281b5..48f8b5bc68 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -11,5 +11,5 @@ - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. -- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. The signatures, the 8-byte `SceRudpPollEvent`, the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. Signatures and the 8-byte event layout are recovered from the supplied module. The operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 08fa5d3578dd8a84c1d168ed0806582c2bcaf994 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:24:09 +0300 Subject: [PATCH 0662/1360] fix(rudp): validate activation and reject unsupported transport --- core/libs/prx/libSceRudp/Export.cpp | 8 ++++++-- core/libs/tests/GuestOfflineImports.cpp | 6 +++--- core/libs/tests/GuestRudpContextLifecycle.cpp | 17 ++++++++++++++--- docs/dev/debt/libSceRudp.md | 2 +- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 2b17a8740b..68afe5a022 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -126,8 +126,12 @@ int APS5_VABI sceRudpInit_nid_postfix(void* mem_pool, int mem_pool_size) { return 0; } -int APS5_VABI sceRudpActivate() { - return 0; +int APS5_VABI sceRudpActivate(std::int32_t contextId, const void* address, std::uint32_t addressLength) { + std::lock_guard lock(g_mutex); + if (!g_inited) return RUDP_ERROR_NOT_INITIALIZED; + if (contextId < 0 || !address) return RUDP_ERROR_INVALID_ARGUMENT; + static_cast<void>(addressLength); + throw std::runtime_error(__func__); } int APS5_VABI sceRudpGetStatus(void* status, std::size_t size) { diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 32dcdd9acf..fc8be8f7f4 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -24,7 +24,7 @@ int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(); int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(); int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(); int APS5_VABI sceRudpInit_nid_postfix(void*, int); -int APS5_VABI sceRudpActivate(); +int APS5_VABI sceRudpActivate(std::int32_t, const void*, std::uint32_t); int APS5_VABI sceRudpGetStatus(void*, std::size_t); int APS5_VABI sceRudpEnd(); } @@ -88,7 +88,7 @@ int main() { Require(sceNpEntitlementAccessRequestServiceEntitlementInfo() == signedOut); Require(sceNpEntitlementAccessPollServiceEntitlementInfo() == signedOut); - Require(sceRudpActivate() == 0); + Require(sceRudpActivate(-1, nullptr, 0) == static_cast<int>(0x80770001)); std::array<unsigned char, 248> status; status.fill(0x5a); const auto originalStatus = status; @@ -97,7 +97,7 @@ int main() { Require(status == originalStatus); alignas(8) static unsigned char rudpPool[0xC58]; Require(sceRudpInit_nid_postfix(rudpPool, sizeof(rudpPool)) == 0); - Require(sceRudpActivate() == 0); + Require(sceRudpActivate(-1, nullptr, 0) == static_cast<int>(0x80770004)); Require(sceRudpGetStatus(status.data(), status.size()) == 0); for (unsigned char byte : status) Require(byte == 0); Require(sceRudpGetStatus(nullptr, 0) == static_cast<int>(0x80770004)); diff --git a/core/libs/tests/GuestRudpContextLifecycle.cpp b/core/libs/tests/GuestRudpContextLifecycle.cpp index c3045181fb..51a119e0d8 100644 --- a/core/libs/tests/GuestRudpContextLifecycle.cpp +++ b/core/libs/tests/GuestRudpContextLifecycle.cpp @@ -13,7 +13,7 @@ extern "C" { using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpEnd(); -int APS5_VABI sceRudpActivate(); +int APS5_VABI sceRudpActivate(std::int32_t, const void*, std::uint32_t); int APS5_VABI sceRudpEnableInternalIOThread(std::uint32_t, std::uint32_t); int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler, void*); int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); @@ -113,7 +113,7 @@ int main() { constexpr int connReset = static_cast<int>(0x80770009u); alignas(16) static unsigned char pool[0x10000]; - Require(sceRudpActivate() == 0); + Require(sceRudpActivate(-1, nullptr, 0) == static_cast<int>(0x80770001)); Require(sceRudpEnableInternalIOThread(0, 0) == notInitialized); @@ -143,7 +143,7 @@ int main() { Require(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); - Require(sceRudpActivate() == 0); + Require(sceRudpActivate(-1, nullptr, 0) == static_cast<int>(0x80770004)); Require(sceRudpEnableInternalIOThread(0, 0) == 0); Require(sceRudpEnableInternalIOThread(1024, 1) == 0); @@ -170,6 +170,17 @@ int main() { Require(sceRudpCreateContext(Handler, nullptr, &ctx) == 0); Require(ctx >= 0); + Require(sceRudpActivate(ctx, nullptr, 0) == static_cast<int>(0x80770004)); + unsigned char address[34]{}; + Require(sceRudpActivate(-1, address, sizeof(address)) == static_cast<int>(0x80770004)); + bool activationUnsupported = false; + try { + sceRudpActivate(ctx, address, sizeof(address)); + } catch (const std::runtime_error&) { + activationUnsupported = true; + } + Require(activationUnsupported); + Require(sceRudpBind(ctx, 0, 0, 0) == 0); Require(sceRudpSetOption(ctx, 0, nullptr, 0) == invalidOption); diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 48f8b5bc68..6b667bd48d 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -4,7 +4,7 @@ - [sceRudpProcessEvents, sceRudpNetFlush and sceRudpNetReceived](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process, send or receive; the three calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. What the console's `sceRudpNetReceived` returns is unverified. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result. - [sceRudpEnableInternalIOThread2](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(uint32_t stack_size, uint32_t priority, uint64_t affinity)`, taken from the managed `SceRudp.Rudp.EnableInternalIOThread(uint stackSize, uint priority, ulong affinity)` in PPSA06888's Unity metadata; the first two match `sceRudpEnableInternalIOThread`. It returns success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as `sceRudpEnableInternalIOThread` does, without starting any thread. Whether the console reports other errors, and whether it applies the affinity, is unverified. -- [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified +- [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) takes a context ID, socket address and 32-bit address length. Initialization and initial argument checks match the native implementation; transport mode checks, address conversion and connection activation remain unimplemented and throw. ### Unknown function info From 932e8a6c75845b1677d1eedfd92f2b50a6f4b723 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:24:18 +0300 Subject: [PATCH 0663/1360] fix(npwebapi2): restore push context and trace signatures --- core/libs/prx/libSceNpWebApi2/Export.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 125c7fb846..9a2ebe62dd 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -12,6 +12,12 @@ static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT = static_cast<int>(0x static constexpr int SCE_NP_WEBAPI2_ERROR_UNAVAILABLE = static_cast<int>(0x80553406); static std::atomic<int> g_nextHandle{1}; +struct NpWebApi2PushContextId { + char value[37]; +}; + +static_assert(sizeof(NpWebApi2PushContextId) == 37); + struct NpWebApi2MemoryPoolStats { size_t pool_size; size_t max_inuse_size; @@ -43,8 +49,8 @@ int APS5_VABI sceNpWebApi2AddHttpRequestHeader(int64_t request_id, const char* f return 0; } -int APS5_VABI sceNpWebApi2AddWebTraceTag(int64_t request_id, const char* value) { - (void)request_id; +int APS5_VABI sceNpWebApi2AddWebTraceTag(std::int64_t requestId, const char* value) { + (void)requestId; if (!value) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; return 0; } @@ -171,11 +177,11 @@ int APS5_VABI sceNpWebApi2Terminate(int lib_ctx_id) { return 0; } -int APS5_VABI sceNpWebApi2PushEventCreatePushContext() { +int APS5_VABI sceNpWebApi2PushEventCreatePushContext(std::int32_t userContextId, NpWebApi2PushContextId* pushContextId) { return SCE_NP_WEBAPI2_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpWebApi2PushEventDeleteFilter() { +int APS5_VABI sceNpWebApi2PushEventDeleteFilter(std::int32_t libraryContextId, std::int32_t filterId) { return 0; } @@ -191,15 +197,15 @@ int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback() { return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } -int APS5_VABI sceNpWebApi2PushEventStartPushContextCallback() { +int APS5_VABI sceNpWebApi2PushEventStartPushContextCallback(std::int32_t userContextId, const NpWebApi2PushContextId* pushContextId) { return 0; } -int APS5_VABI sceNpWebApi2PushEventUnregisterCallback() { +int APS5_VABI sceNpWebApi2PushEventUnregisterCallback(std::int32_t userContextId, std::int32_t callbackId) { return 0; } -int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { +int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback(std::int32_t userContextId, std::int32_t callbackId) { return 0; } From 4c436012787dbc9e9efb50a12fbbcbca2e56d721 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:25:38 +0300 Subject: [PATCH 0664/1360] fix(vrtracker2): restore named export signatures and play area layouts --- core/libs/prx/libSceVrTracker2/Export.cpp | 43 +++++++++++++++++------ core/libs/tests/GuestVrTracker2.cpp | 5 +-- docs/dev/debt/libSceVrTracker2.md | 2 +- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index e323b3c719..b850d89f71 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -5,6 +5,29 @@ #include <string> #include "prx/libc/include/General.hpp" +struct VrTracker2InitializeParameter; +struct VrTracker2ResultParameter; +struct VrTracker2Result; +struct VrTracker2CoordinateLocation; + +struct VrTracker2BoundaryGeometry { + std::uint32_t size; + std::uint32_t pointCount; + float points[256][3]; +}; + +struct VrTracker2OrientedBoundingBox { + std::uint32_t size; + float transform[16]; + float extents[3]; +}; + +static_assert(sizeof(VrTracker2BoundaryGeometry) == 3080); +static_assert(offsetof(VrTracker2BoundaryGeometry, points) == 8); +static_assert(sizeof(VrTracker2OrientedBoundingBox) == 80); +static_assert(offsetof(VrTracker2OrientedBoundingBox, transform) == 4); +static_assert(offsetof(VrTracker2OrientedBoundingBox, extents) == 68); + namespace { constexpr std::int32_t SCE_OK = 0; @@ -25,39 +48,39 @@ std::int32_t APS5_VABI sceVrTracker2Finalize() { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2GetCoordinateSystem() { +std::int32_t APS5_VABI sceVrTracker2GetCoordinateSystem(std::uint32_t* coordinateSystem) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry(VrTracker2BoundaryGeometry* geometry) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox(VrTracker2OrientedBoundingBox* box) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2GetResult() { +std::int32_t APS5_VABI sceVrTracker2GetResult(const VrTracker2ResultParameter* parameter, VrTracker2Result* result) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2LocateCoordinateSystem() { +std::int32_t APS5_VABI sceVrTracker2LocateCoordinateSystem(std::uint32_t coordinateSystem, VrTracker2CoordinateLocation* location) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2RegisterDevice() { +std::int32_t APS5_VABI sceVrTracker2RegisterDevice(std::uint32_t deviceType, std::int32_t deviceHandle) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2ResetLocalCoordinate() { +std::int32_t APS5_VABI sceVrTracker2ResetLocalCoordinate(std::int32_t deviceHandle) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2SetCoordinateSystem() { +std::int32_t APS5_VABI sceVrTracker2SetCoordinateSystem(std::uint32_t coordinateSystem) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2UnregisterDevice() { +std::int32_t APS5_VABI sceVrTracker2UnregisterDevice(std::int32_t deviceHandle) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } @@ -75,7 +98,7 @@ std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* req return SCE_OK; } -std::int32_t APS5_VABI sceVrTracker2Initialize() { +std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index f8d5899a66..7b43bc31d0 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -17,7 +17,8 @@ std::int32_t APS5_VABI VrTracker2Accept(); std::int32_t APS5_VABI VrTracker2FillDefaults(void* output); std::int32_t APS5_VABI VrTracker2ClearWord(void* output); std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement); -std::int32_t APS5_VABI sceVrTracker2Initialize(); +struct VrTracker2InitializeParameter; +std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter); } namespace { @@ -110,6 +111,6 @@ int main() { parameter[0] = 0x21; Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); - Require(Throws([] { sceVrTracker2Initialize(); }), "unknown export"); + Require(Throws([] { sceVrTracker2Initialize(nullptr); }), "unknown export"); return 0; } diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 42938b9e61..0aa0c7b4d8 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes, so its exports answer as the console's module does before initialization, measured on a retail PS5 (firmware 13.60) with no PS VR2 headset by calling each of the 115 NIDs with null arguments, zeroed buffers and buffers filled with 0xAA and 0x01: 99 return 0x8A740001 and write nothing; `XknY0IDcOwU` and `ark13GkiskA` return 0x8A740006 for a null first argument and otherwise clear its first 24 and 16 bytes and return 0x8A740001; `-YvjOUph8B0` returns 0x8A740006 for a null second argument and otherwise 0x8A740001; `QyZvFyf-QEk` and `fTYZOZepkqE` return 0 and write nothing; `TZIUIk9id3c` writes 0xC000, 0, 0x40 and 0x40 as `uint32`, `uint32`, `uint16` and `uint16` at offsets 4 to 15 of its first argument and returns 0; `IB0qcDVu8vU` clears the `uint32` at its first argument and returns 0. The last two crash on the console for a null first argument and throw here. Signatures and what the tracker returns once initialized are unknown; the twelve exports with aerolib names keep them and the others carry this library's own names +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes, so its exports answer as the console's module does before initialization, measured on a retail PS5 (firmware 13.60) with no PS VR2 headset by calling each of the 115 NIDs with null arguments, zeroed buffers and buffers filled with 0xAA and 0x01: 99 return 0x8A740001 and write nothing; `XknY0IDcOwU` and `ark13GkiskA` return 0x8A740006 for a null first argument and otherwise clear its first 24 and 16 bytes and return 0x8A740001; `-YvjOUph8B0` returns 0x8A740006 for a null second argument and otherwise 0x8A740001; `QyZvFyf-QEk` and `fTYZOZepkqE` return 0 and write nothing; `TZIUIk9id3c` writes 0xC000, 0, 0x40 and 0x40 as `uint32`, `uint32`, `uint16` and `uint16` at offsets 4 to 15 of its first argument and returns 0; `IB0qcDVu8vU` clears the `uint32` at its first argument and returns 0. The last two crash on the console for a null first argument and throw here. The named export signatures and play-area output layouts are recovered; initialization, result parameter and result fields still need reconstruction. What the tracker returns once initialized is not implemented; the twelve exports with aerolib names keep them and the others carry this library's own names ### Unknown function info From a24a5524b0e191d97da4affc58b7b857366d14d8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:26:39 +0300 Subject: [PATCH 0665/1360] fix(tools): pass duplicate export patterns through stdin --- tools/tests/test_check_conventions_comments.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index f0d3d223fb..c86fcdd05a 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -706,6 +706,19 @@ class RepositoryChecksTests(unittest.TestCase): check.code() self.assertEqual(check.findings, [("comment", name, 2, "")]) + def test_large_export_set_checks_duplicates_without_command_line_limit(self): + first = self.root / "core/libs/prx/libSceA/Export.cpp" + first.parent.mkdir(parents=True) + first.write_text("int APS5_VABI sceLargeExportSet1999(int value) { return value; }\n", encoding="utf-8") + self.commit() + second = self.root / "core/libs/prx/libSceB/Export.cpp" + second.parent.mkdir(parents=True) + second.write_text("".join(f"int APS5_VABI sceLargeExportSet{index}(int value) {{ return value; }}\n" for index in range(2000)), encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + self.assertEqual(check.findings, [("duplicate-export", "core/libs/prx/libSceB/Export.cpp", 2000, "sceLargeExportSet1999 is also in libSceA")]) + def test_checks_from_a_subdirectory_see_the_whole_repository(self): definition = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" first = self.root / "core/libs/prx/libSceA/Export.cpp" From e2f139c5499da52968e8a395b1a8960c833887d4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:28:21 +0300 Subject: [PATCH 0666/1360] fix(s3da): restore port and transfer signatures --- core/libs/prx/libSceS3da/Export.cpp | 10 ++++++---- docs/dev/debt/libSceS3da.md | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceS3da/Export.cpp b/core/libs/prx/libSceS3da/Export.cpp index 34b8330de5..79ca9b7758 100644 --- a/core/libs/prx/libSceS3da/Export.cpp +++ b/core/libs/prx/libSceS3da/Export.cpp @@ -3,24 +3,26 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct S3daInfo; + extern "C" { -int APS5_VABI sceS3daClose() { +int APS5_VABI sceS3daClose(std::uint32_t port) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceS3daGetInfo() { +int APS5_VABI sceS3daGetInfo(std::uint32_t port, S3daInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceS3daOpen() { +int APS5_VABI sceS3daOpen(std::uint32_t port) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceS3daSendData() { +int APS5_VABI sceS3daSendData(std::uint32_t port, const void* data, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceS3da.md b/docs/dev/debt/libSceS3da.md index c941424735..cc6345ecaa 100644 --- a/docs/dev/debt/libSceS3da.md +++ b/docs/dev/debt/libSceS3da.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceS3daClose, sceS3daGetInfo, sceS3daOpen and sceS3daSendData](../../../core/libs/prx/libSceS3da/Export.cpp) (libSceS3da) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceS3da.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceS3daClose, sceS3daGetInfo, sceS3daOpen and sceS3daSendData](../../../core/libs/prx/libSceS3da/Export.cpp) remain unimplemented. Port arguments and transfer buffer/length are recovered from the native module. `GetInfo` forwards its output pointer to a kernel ioctl without accessing its fields; the information layout requires the kernel handler or a caller that interprets its output. From bbadd9bda400b2e0890b5842f70d32343faa5124 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:28:39 +0300 Subject: [PATCH 0667/1360] fix(shelluiutil): recover launch and output buffer signatures --- core/libs/prx/libSceShellUIUtil/Export.cpp | 8 +++++--- docs/dev/debt/libSceShellUIUtil.md | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceShellUIUtil/Export.cpp b/core/libs/prx/libSceShellUIUtil/Export.cpp index 4ff433cad7..2837093a4f 100644 --- a/core/libs/prx/libSceShellUIUtil/Export.cpp +++ b/core/libs/prx/libSceShellUIUtil/Export.cpp @@ -3,6 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct ShellUIUtilLaunchParameter; + extern "C" { int APS5_VABI sceShellUIUtilCreateTaskToken() { @@ -10,7 +12,7 @@ int APS5_VABI sceShellUIUtilCreateTaskToken() { return 0; } -int APS5_VABI sceShellUIUtilGetAppUrl() { +int APS5_VABI sceShellUIUtilGetAppUrl(const char* application, const char* parameter, char* output, std::uint32_t capacity, std::uint32_t* outputSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -20,12 +22,12 @@ int APS5_VABI sceShellUIUtilInitialize() { return 0; } -int APS5_VABI sceShellUIUtilLaunchByUri() { +int APS5_VABI sceShellUIUtilLaunchByUri(const char* uri, const ShellUIUtilLaunchParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellUIUtilLaunchTask() { +int APS5_VABI sceShellUIUtilLaunchTask(std::int32_t taskToken, const char* uri, char* output, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceShellUIUtil.md b/docs/dev/debt/libSceShellUIUtil.md index f07f584e14..12414a8ba7 100644 --- a/docs/dev/debt/libSceShellUIUtil.md +++ b/docs/dev/debt/libSceShellUIUtil.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceShellUIUtilCreateTaskToken, sceShellUIUtilGetAppUrl, sceShellUIUtilInitialize, sceShellUIUtilLaunchByUri, sceShellUIUtilLaunchTask and sceShellUIUtilTerminate](../../../core/libs/prx/libSceShellUIUtil/Export.cpp) (libSceShellUIUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceShellUIUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [libSceShellUIUtil](../../../core/libs/prx/libSceShellUIUtil/Export.cpp) remains unimplemented. Native wrappers confirm the export signatures, including two input strings, output buffer, 32-bit capacity and optional 32-bit output size for `GetAppUrl`. The precise string meanings and the second word of the 8-byte `LaunchByUri` parameter require the receiving IPC service; the wrapper only checks its leading size. Task completion, IPC errors and lifecycle behavior are not implemented. From 3025b1d393e451bbd2ad3ec8292c8a36e172fc8d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:28:40 +0300 Subject: [PATCH 0668/1360] docs(debt): condense remaining audio and tracker gaps --- docs/dev/debt/libSceAudioPropagation.md | 2 +- docs/dev/debt/libSceVrTracker2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index b1d100771e..1800ba874d 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,7 +2,7 @@ ### Silent stubs -- [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 and audio paths are accepted only when empty and throw otherwise; path calculation returns the confirmed initial validation errors and throws for unsupported propagation; `sceAudioPropagationSourceRender` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no 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. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose no paths or rays, rendering preserves the destination mix without adding path contributions, and memory queries return only a 16-byte CPU header. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. ### Unknown function info diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 0aa0c7b4d8..72d3209092 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes, so its exports answer as the console's module does before initialization, measured on a retail PS5 (firmware 13.60) with no PS VR2 headset by calling each of the 115 NIDs with null arguments, zeroed buffers and buffers filled with 0xAA and 0x01: 99 return 0x8A740001 and write nothing; `XknY0IDcOwU` and `ark13GkiskA` return 0x8A740006 for a null first argument and otherwise clear its first 24 and 16 bytes and return 0x8A740001; `-YvjOUph8B0` returns 0x8A740006 for a null second argument and otherwise 0x8A740001; `QyZvFyf-QEk` and `fTYZOZepkqE` return 0 and write nothing; `TZIUIk9id3c` writes 0xC000, 0, 0x40 and 0x40 as `uint32`, `uint32`, `uint16` and `uint16` at offsets 4 to 15 of its first argument and returns 0; `IB0qcDVu8vU` clears the `uint32` at its first argument and returns 0. The last two crash on the console for a null first argument and throw here. The named export signatures and play-area output layouts are recovered; initialization, result parameter and result fields still need reconstruction. What the tracker returns once initialized is not implemented; the twelve exports with aerolib names keep them and the others carry this library's own names +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured on firmware 13.60 without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. ### Unknown function info From 25d8f40d885cb5cff3c60763f517313874f9d157 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:30:35 +0300 Subject: [PATCH 0669/1360] fix(movetracker): restore calibration capture and state signatures --- core/libs/prx/libSceMoveTracker/Export.cpp | 21 +++++++++++++-------- docs/dev/debt/libSceMoveTracker.md | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceMoveTracker/Export.cpp b/core/libs/prx/libSceMoveTracker/Export.cpp index 8b2c497e09..fb002273b5 100644 --- a/core/libs/prx/libSceMoveTracker/Export.cpp +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -3,6 +3,11 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct MoveTrackerCameraFrame; +struct MoveTrackerControllerInput; +struct MoveTrackerState; +struct MoveTrackerPlaybackInfo; + extern "C" { int APS5_VABI sceMoveTrackerCalibrateReset() { @@ -10,17 +15,17 @@ int APS5_VABI sceMoveTrackerCalibrateReset() { return 0; } -int APS5_VABI sceMoveTrackerCalibrateYaw() { +int APS5_VABI sceMoveTrackerCalibrateYaw(std::int32_t handle, const float* direction) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMoveTrackerCameraUpdate() { +int APS5_VABI sceMoveTrackerCameraUpdate(const MoveTrackerCameraFrame* frames) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMoveTrackerCaptureInit() { +int APS5_VABI sceMoveTrackerCaptureInit(void* imageMemory, std::int32_t capacity, std::int32_t interval, const char* path, std::int32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -30,12 +35,12 @@ int APS5_VABI sceMoveTrackerCaptureTerm() { return 0; } -int APS5_VABI sceMoveTrackerControllersUpdate() { +int APS5_VABI sceMoveTrackerControllersUpdate(const MoveTrackerControllerInput* controllers) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMoveTrackerGetState() { +int APS5_VABI sceMoveTrackerGetState(std::int32_t handle, std::uint64_t timestamp, MoveTrackerState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -44,7 +49,7 @@ int APS5_VABI sceMoveTrackerGetVersion() { return 2; } -int APS5_VABI sceMoveTrackerGetWorkingMemorySize() { +int APS5_VABI sceMoveTrackerGetWorkingMemorySize(std::uint32_t* totalSize, std::uint32_t* processingSize) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -54,12 +59,12 @@ int APS5_VABI sceMoveTrackerInit() { return 0; } -int APS5_VABI sceMoveTrackerPlayGetImages() { +int APS5_VABI sceMoveTrackerPlayGetImages(MoveTrackerCameraFrame* frames) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceMoveTrackerPlayGetStates() { +int APS5_VABI sceMoveTrackerPlayGetStates(MoveTrackerPlaybackInfo* info, MoveTrackerState* states) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMoveTracker.md b/docs/dev/debt/libSceMoveTracker.md index 156c1dfb0a..b6ddbd43a0 100644 --- a/docs/dev/debt/libSceMoveTracker.md +++ b/docs/dev/debt/libSceMoveTracker.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceMoveTrackerCalibrateReset, sceMoveTrackerCalibrateYaw, sceMoveTrackerCameraUpdate, sceMoveTrackerCaptureInit, sceMoveTrackerCaptureTerm, sceMoveTrackerControllersUpdate, sceMoveTrackerGetState, sceMoveTrackerGetVersion, sceMoveTrackerGetWorkingMemorySize, sceMoveTrackerInit, sceMoveTrackerPlayGetImages, sceMoveTrackerPlayGetStates, sceMoveTrackerPlayStart, sceMoveTrackerPlayStep, sceMoveTrackerRecordSave, sceMoveTrackerRecordStart, sceMoveTrackerRecordStop and sceMoveTrackerTerm](../../../core/libs/prx/libSceMoveTracker/Export.cpp) (libSceMoveTracker) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMoveTracker.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [libSceMoveTracker](../../../core/libs/prx/libSceMoveTracker/Export.cpp) remains unimplemented. Calibration direction, camera/controller input, capture initialization, state query, memory-size query and playback output signatures are recovered. Camera and controller records occupy 24 bytes, state records 160 bytes, and playback metadata 16 bytes; their complete field meanings still require reconstruction. Initialization arguments and capture/playback helper signatures need further checking against caller register use. From b24f69664cc8e782e88138bcf388b64ee670ef45 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:32:26 +0300 Subject: [PATCH 0670/1360] fix(net): recover resolver connection and interface query signatures --- core/libs/prx/libSceNet/Export.cpp | 36 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 39a828ebe0..594a8dcbcb 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,24 @@ // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetResolverConnection; +struct NetThreadParameter; +struct NetInitializeParameter; +struct NetInterfaceInfo; +struct NetInterfaceStatistics; + +struct NetResolverConnectOption { + std::int32_t connectionTimeout; + std::int32_t resolverTimeout; + std::int32_t resolverRetryCount; + std::uint32_t reserved; +}; + +static_assert(sizeof(NetResolverConnectOption) == 16); +static_assert(offsetof(NetResolverConnectOption, resolverTimeout) == 4); +static_assert(offsetof(NetResolverConnectOption, resolverRetryCount) == 8); +static_assert(offsetof(NetResolverConnectOption, reserved) == 12); + struct NetSyncObject { void* mutex; void* condition; @@ -2096,22 +2114,22 @@ int APS5_VABI sceNetShowRouteWithMemory(std::int32_t poolId) { return 0; } -int APS5_VABI sceNetResolverConnectAbort(void) { +int APS5_VABI sceNetResolverConnectAbort(NetResolverConnection* connection, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetResolverConnectCreate(void) { +int APS5_VABI sceNetResolverConnectCreate(const char* name, std::int32_t socketType, NetResolverConnection* connection, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetResolverConnect(void) { +int APS5_VABI sceNetResolverConnect(NetResolverConnection* connection, const char* hostname, std::uint16_t port, const NetResolverConnectOption* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetResolverConnectDestroy(void) { +int APS5_VABI sceNetResolverConnectDestroy(NetResolverConnection* connection) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2199,17 +2217,17 @@ int APS5_VABI sceNetGetDnsInfo(NetDnsInfo* info, std::uint32_t flags) { return static_cast<int>(count); } -int APS5_VABI sceNetGetIfList(void) { +int APS5_VABI sceNetGetIfList(std::int32_t interfaceId, NetInterfaceInfo* interfaces, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetGetIfListEx(void) { +int APS5_VABI sceNetGetIfListEx(std::int32_t interfaceId, NetInterfaceInfo* interfaces, std::uint32_t capacity, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetGetIfListOnce(void) { +int APS5_VABI sceNetGetIfListOnce(std::int32_t interfaceId, NetInterfaceStatistics* statistics, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2263,7 +2281,7 @@ int APS5_VABI sceNetGetSystemMemInfo(void) { return 0; } -int APS5_VABI sceNetInitParam(void) { +int APS5_VABI sceNetInitParam(const NetInitializeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2356,7 +2374,7 @@ int APS5_VABI sceNetSysctl(const int* name, std::uint32_t nameLength, void* oldV return 0; } -int APS5_VABI sceNetThreadCreate(void) { +int APS5_VABI sceNetThreadCreate(Pthread* thread, const NetThreadParameter* parameter, const char* name) { NotImplemented_nid_no_patch(__func__); return 0; } From c90fa12732d294067e53530d0920b5d422b63573 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:34:08 +0300 Subject: [PATCH 0671/1360] fix(net): restore diagnostic buffers and route pointer returns --- core/libs/prx/libSceNet/Export.cpp | 44 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 594a8dcbcb..ad52371f2b 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,18 @@ // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetSockInfo6; +struct NetStatisticsInfo; +struct NetInterfaceStats; +struct NetRouteInfo; +struct NetDebugParameter; +struct NetArpInfo; +struct NetAppMemoryInfo; +struct NetSystemMemoryInfo; +struct NetBandwidthParameter; +struct NetDataTraffic; +struct NetEmulationParameter; + struct NetResolverConnection; struct NetThreadParameter; struct NetInitializeParameter; @@ -2075,12 +2087,12 @@ int APS5_VABI sceNetClearDnsCache(int flags) { return 0; } -int APS5_VABI sceNetGetStatisticsInfo(void) { +int APS5_VABI sceNetGetStatisticsInfo(NetStatisticsInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetGetInterfaceStats(void) { +int APS5_VABI sceNetGetInterfaceStats(NetInterfaceStats* stats, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2134,7 +2146,7 @@ int APS5_VABI sceNetResolverConnectDestroy(NetResolverConnection* connection) { return 0; } -int APS5_VABI sceNetGetSockInfo6(void) { +int APS5_VABI sceNetGetSockInfo6(std::int32_t socket, NetSockInfo6* info, std::uint32_t capacity, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2153,12 +2165,12 @@ int APS5_VABI sceNetShowRoute6WithMemory(std::int32_t poolId) { return 0; } -int APS5_VABI sceNetAllocateAllRouteInfo(void) { +NetRouteInfo* APS5_VABI sceNetAllocateAllRouteInfo(std::int32_t interfaceId, std::int32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetAllocateAllRouteInfo6(void) { +NetRouteInfo* APS5_VABI sceNetAllocateAllRouteInfo6(std::int32_t interfaceId, std::int32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2168,7 +2180,7 @@ int APS5_VABI sceNetControl(std::uint32_t interfaceId, std::int32_t interfaceInd return 0; } -int APS5_VABI sceNetDbgInit(void) { +int APS5_VABI sceNetDbgInit(NetDebugParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2198,12 +2210,12 @@ int APS5_VABI sceNetFreeAllRouteInfo(void* routes) { return 0; } -int APS5_VABI sceNetGetArpInfo(void) { +int APS5_VABI sceNetGetArpInfo(NetArpInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetGetAppMemInfo(void) { +int APS5_VABI sceNetGetAppMemInfo(NetAppMemoryInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2276,7 +2288,7 @@ void APS5_VABI sceNetGetSystemTime(std::uint64_t* output) { if (output != nullptr) *output = sceKernelGetProcessTime(); } -int APS5_VABI sceNetGetSystemMemInfo(void) { +int APS5_VABI sceNetGetSystemMemInfo(NetSystemMemoryInfo* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2862,17 +2874,17 @@ int APS5_VABI sceNetPppoeStop(void) { return 0; } -int APS5_VABI sceNetBandwidthControlSetDefaultParam(void) { +int APS5_VABI sceNetBandwidthControlSetDefaultParam(const NetBandwidthParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetBandwidthControlGetDefaultParam(void) { +int APS5_VABI sceNetBandwidthControlGetDefaultParam(NetBandwidthParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetBandwidthControlGetDataTraffic(void) { +int APS5_VABI sceNetBandwidthControlGetDataTraffic(std::int32_t interfaceId, NetDataTraffic* traffic, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2932,22 +2944,22 @@ int APS5_VABI sceNetDumpRead(std::int32_t dumpId, void* buffer, std::int32_t buf return 0; } -int APS5_VABI sceNetEmulationDebugSettingsSet(void) { +int APS5_VABI sceNetEmulationDebugSettingsSet(const NetEmulationParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetEmulationDebugSettingsGet(void) { +int APS5_VABI sceNetEmulationDebugSettingsGet(NetEmulationParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetEmulationSet(void) { +int APS5_VABI sceNetEmulationSet(const NetEmulationParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetEmulationGet(void) { +int APS5_VABI sceNetEmulationGet(NetEmulationParameter* parameter, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } From 77f3c97478b20bc6491aa07a08f40f3654a08a8b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:35:22 +0300 Subject: [PATCH 0672/1360] fix(net): restore wlan and dhcp query signatures --- core/libs/prx/libSceNet/Export.cpp | 59 ++++++++++++++++++++++-------- docs/dev/debt/libSceNet.md | 44 +--------------------- 2 files changed, 45 insertions(+), 58 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index ad52371f2b..931deb1696 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,33 @@ // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetWlanAdhocParameter; +struct NetWlanWakeInfo; +struct NetWlanPspWakeInfo; +struct NetWlanPspWakeParameter; +struct NetWlanApParameter; +struct NetWlanDeviceInfo; +struct NetWlanAntennaParameter; +struct NetWlanRateParameter; +struct NetWlanDeviceConfiguration; +struct NetWlanRssiInfo; +struct NetDhcpInfo; +struct NetAutoIpInfo; +struct NetAddress6Info; + +struct NetDhcpOptionInfo { + std::uint32_t option; + std::uint32_t capacity; + void* data; + std::uint32_t size; + std::uint32_t reserved; +}; + +static_assert(sizeof(NetDhcpOptionInfo) == 24); +static_assert(offsetof(NetDhcpOptionInfo, capacity) == 4); +static_assert(offsetof(NetDhcpOptionInfo, data) == 8); +static_assert(offsetof(NetDhcpOptionInfo, size) == 16); + struct NetSockInfo6; struct NetStatisticsInfo; struct NetInterfaceStats; @@ -2604,17 +2631,17 @@ int APS5_VABI sceNetConfigGetDefaultRoute(std::uint32_t* gateway) { return 0; } -int APS5_VABI sceNetDhcpGetAutoipInfo(void) { +int APS5_VABI sceNetDhcpGetAutoipInfo(std::int32_t interfaceId, NetAutoIpInfo* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetDhcpGetInfo(void) { +int APS5_VABI sceNetDhcpGetInfo(std::int32_t interfaceId, NetDhcpInfo* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetDhcpGetInfoEx(void) { +int APS5_VABI sceNetDhcpGetInfoEx(std::int32_t interfaceId, std::int32_t type, NetDhcpOptionInfo* info, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2689,7 +2716,7 @@ int APS5_VABI sceNetAddrConfig6Stop(int interfaceId) { return 0; } -int APS5_VABI sceNetAddrConfig6GetInfo(void) { +int APS5_VABI sceNetAddrConfig6GetInfo(std::int32_t interfaceId, NetAddress6Info* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2729,17 +2756,17 @@ int APS5_VABI sceNetConfigWlanAdhocClearWakeOnWlan(std::uint32_t interfaceId) { return 0; } -int APS5_VABI sceNetConfigWlanAdhocCreate(void) { +int APS5_VABI sceNetConfigWlanAdhocCreate(std::int32_t interfaceId, NetWlanAdhocParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanAdhocGetWakeOnWlanInfo(void) { +int APS5_VABI sceNetConfigWlanAdhocGetWakeOnWlanInfo(std::int32_t interfaceId, NetWlanWakeInfo* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanAdhocJoin(void) { +int APS5_VABI sceNetConfigWlanAdhocJoin(std::int32_t interfaceId, NetWlanAdhocParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2754,12 +2781,12 @@ int APS5_VABI sceNetConfigWlanAdhocPspEmuClearWakeOnWlan(std::uint32_t interface return 0; } -int APS5_VABI sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo(void) { +int APS5_VABI sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo(std::int32_t interfaceId, NetWlanPspWakeInfo* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanAdhocPspEmuSetWakeOnWlan(void) { +int APS5_VABI sceNetConfigWlanAdhocPspEmuSetWakeOnWlan(std::int32_t interfaceId, NetWlanPspWakeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2779,7 +2806,7 @@ int APS5_VABI sceNetConfigWlanAdhocSetWakeOnWlan(void) { return 0; } -int APS5_VABI sceNetConfigWlanApStart(void) { +int APS5_VABI sceNetConfigWlanApStart(std::int32_t interfaceId, NetWlanApParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2804,27 +2831,27 @@ int APS5_VABI sceNetConfigWlanBackgroundScanStop(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigWlanDiagGetDeviceInfo(void) { +int APS5_VABI sceNetConfigWlanDiagGetDeviceInfo(std::int32_t interfaceId, NetWlanDeviceInfo* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanDiagSetAntenna(void) { +int APS5_VABI sceNetConfigWlanDiagSetAntenna(std::int32_t interfaceId, NetWlanAntennaParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanDiagSetTxFixedRate(void) { +int APS5_VABI sceNetConfigWlanDiagSetTxFixedRate(std::int32_t interfaceId, NetWlanRateParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanGetDeviceConfig(void) { +int APS5_VABI sceNetConfigWlanGetDeviceConfig(std::int32_t interfaceId, NetWlanDeviceConfiguration* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanInfraGetRssiInfo(void) { +int APS5_VABI sceNetConfigWlanInfraGetRssiInfo(std::int32_t interfaceId, NetWlanRssiInfo* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2844,7 +2871,7 @@ int APS5_VABI sceNetConfigWlanScan(void) { return 0; } -int APS5_VABI sceNetConfigWlanSetDeviceConfig(void) { +int APS5_VABI sceNetConfigWlanSetDeviceConfig(std::int32_t interfaceId, NetWlanDeviceConfiguration* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 391136c6b2..d64641d764 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,76 +6,36 @@ ### Unknown function info -- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. +- [Resolver-connect, thread creation, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, thread, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. -- [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetResolverConnectAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetResolverConnectCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetResolverConnect](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetResolverConnectDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetAllocateAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDbgInit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown -- [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. -- [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDhcpGetAutoipInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDhcpGetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDhcpGetInfoEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetAddrConfig6GetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocPspEmuSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocSetExtInfoElement](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanAdhocSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanApStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanBackgroundScanQuery](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanBackgroundScanStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanDiagGetDeviceInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanDiagSetAntenna](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanDiagSetTxFixedRate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanGetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanInfraGetRssiInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanInfraScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigWlanScan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanSetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetBandwidthControlSetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetBandwidthControlGetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetBandwidthControlGetDataTraffic](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetStatisticsInfoInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDumpSetFilterParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEmulationDebugSettingsSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEmulationDebugSettingsGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEmulationSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetEmulationGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature ### Functional From 60ca71bd1f8916905cd3169e3cf2c594c661841e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:36:44 +0300 Subject: [PATCH 0673/1360] fix(net): restore wlan scan buffers and result outputs --- core/libs/prx/libSceNet/Export.cpp | 25 +++++++++++++++++-------- docs/dev/debt/libSceNet.md | 8 -------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 931deb1696..fe8faee481 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,15 @@ // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetWlanInformationElement; +struct NetWlanWakeParameter; +struct NetWlanScanParameter; +struct NetWlanScanResult; +struct NetWlanAdhocScanParameter; +struct NetWlanInfraScanParameter; +struct NetWlanJoinResult; +struct NetDumpFilterParameter; + struct NetWlanAdhocParameter; struct NetWlanWakeInfo; struct NetWlanPspWakeInfo; @@ -2791,17 +2800,17 @@ int APS5_VABI sceNetConfigWlanAdhocPspEmuSetWakeOnWlan(std::int32_t interfaceId, return 0; } -int APS5_VABI sceNetConfigWlanAdhocScanJoin(void) { +int APS5_VABI sceNetConfigWlanAdhocScanJoin(std::int32_t interfaceId, const NetWlanAdhocScanParameter* parameter, void* buffer, std::size_t bufferSize, NetWlanJoinResult** result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanAdhocSetExtInfoElement(void) { +int APS5_VABI sceNetConfigWlanAdhocSetExtInfoElement(std::int32_t interfaceId, NetWlanInformationElement* parameter, std::int32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanAdhocSetWakeOnWlan(void) { +int APS5_VABI sceNetConfigWlanAdhocSetWakeOnWlan(std::int32_t interfaceId, NetWlanWakeParameter* parameter, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2816,12 +2825,12 @@ int APS5_VABI sceNetConfigWlanApStop(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigWlanBackgroundScanQuery(void) { +int APS5_VABI sceNetConfigWlanBackgroundScanQuery(std::int32_t interfaceId, void* buffer, std::int32_t size, NetWlanScanResult** results, std::size_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanBackgroundScanStart(void) { +int APS5_VABI sceNetConfigWlanBackgroundScanStart(std::int32_t interfaceId, NetWlanScanParameter* parameter, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2861,12 +2870,12 @@ int APS5_VABI sceNetConfigWlanInfraLeave(int interfaceId) { return 0; } -int APS5_VABI sceNetConfigWlanInfraScanJoin(void) { +int APS5_VABI sceNetConfigWlanInfraScanJoin(std::int32_t interfaceId, const NetWlanInfraScanParameter* parameter, void* buffer, std::size_t bufferSize, NetWlanJoinResult** result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigWlanScan(void) { +int APS5_VABI sceNetConfigWlanScan(std::int32_t interfaceId, const NetWlanScanParameter* parameter, std::size_t parameterSize, void* buffer, std::size_t bufferSize, NetWlanScanResult** results, std::size_t* count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2956,7 +2965,7 @@ int APS5_VABI sceNetDumpCreate(const char* name, std::int32_t bufferSize, std::u return 0; } -int APS5_VABI sceNetDumpSetFilterParam(void) { +int APS5_VABI sceNetDumpSetFilterParam(std::int32_t socket, const NetDumpFilterParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index d64641d764..1d9c7a4914 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -24,18 +24,10 @@ - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocSetExtInfoElement](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanAdhocSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanBackgroundScanQuery](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanBackgroundScanStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanInfraScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigWlanScan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetStatisticsInfoInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDumpSetFilterParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature ### Functional From 04f2d9a60f37e5096c252aeed3cafdf78502ce3b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:37:24 +0300 Subject: [PATCH 0674/1360] fix(net): reject unsupported diagnostic output after initialization --- core/libs/prx/libSceNet/Export.cpp | 8 ++++++-- core/libs/tests/GuestSceNet.cpp | 13 +++++++++++-- docs/dev/debt/libSceNet.md | 3 ++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index fe8faee481..bf95cdd988 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -724,7 +724,9 @@ int* APS5_VABI sceNetErrnoLoc(void) { } int APS5_VABI sceNetShowNetstat(void) { - return 0; + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + throw std::runtime_error(__func__); } int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { @@ -2110,7 +2112,9 @@ int APS5_VABI sceNetResolverAbort(int rid, int flags) { } int APS5_VABI sceNetShowIfconfig() { - return 0; + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + throw std::runtime_error(__func__); } extern const std::uint8_t in6addr_any_nid_postfix[16] = {}; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 6db4c99913..3f775bd82e 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -239,8 +239,15 @@ static std::string CaptureNetstat(int pool) { } static void CheckNetstat() { - Require(sceNetShowNetstat() == 0); - Require(sceNetShowIfconfig() == 0); + for (const auto diagnostic : {sceNetShowNetstat, sceNetShowIfconfig}) { + bool unsupported = false; + try { + diagnostic(); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); + } Require(sceNetShowRoute() == 0); Require(sceNetShowRoute6() == 0); Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); @@ -1457,6 +1464,8 @@ int main() { Require(Failed(sceNetGetRandom(nullptr), 200)); Require(Failed(sceNetUsleep(0), 200)); Require(Failed(sceNetUsleep(-1), 200)); + Require(Failed(sceNetShowNetstat(), 200)); + Require(Failed(sceNetShowIfconfig(), 200)); Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); CheckSocketHostErrors(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 1d9c7a4914..eb17a8b8d3 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -2,7 +2,8 @@ ### Silent stubs -- [sceNetShowIfconfig, sceNetShowNetstat, sceNetShowRoute, sceNetShowRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs each the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified +- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) return the native not-initialized error and set errno before initialization. Initialized calls throw because native diagnostic collection and formatting are not implemented. +- [sceNetShowRoute, sceNetShowRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs each the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified ### Unknown function info From ce8ee3f598d0c520429a6447fc5ac6f7daf12184 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:41:23 +0300 Subject: [PATCH 0675/1360] fix(netctl): restore scan and network query signatures --- core/libs/prx/libSceNetCtl/Export.cpp | 39 +++++++++++++++++---------- docs/dev/debt/libSceNetCtl.md | 18 +++---------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 03167d8863..c3a558cda9 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -34,6 +34,17 @@ #include <vector> // Network-control state reflects host interface addressing, not PSN sign-in status. +union NetCtlInfoV6; +struct NetCtlConnectionConfiguration; +struct NetCtlScanOptions; +struct NetCtlScanBssidList; +struct NetCtlScanInfo; +struct NetCtlNatInfoInternal; +struct NetCtlBandwidthInfo; +struct NetCtlNetEvConfiguration; +struct NetBweInfo; +struct NetCtlWifiInfo; + static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast<int>(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast<int>(0x80412105); static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast<int>(0x80412106); @@ -592,7 +603,7 @@ int APS5_VABI sceNetCtlGetState2IpcInt(int* state) { return 0; } -int APS5_VABI sceNetCtlGetInfoIpcInt(void) { +int APS5_VABI sceNetCtlGetInfoIpcInt(std::uint32_t code, NetCtlInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -617,7 +628,7 @@ int APS5_VABI sceNetCtlConnectWithRetryIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlConnectConfIpcInt(void) { +int APS5_VABI sceNetCtlConnectConfIpcInt(const NetCtlConnectionConfiguration* configuration) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -627,32 +638,32 @@ int APS5_VABI sceNetCtlDisconnectIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlScanIpcInt(void) { +int APS5_VABI sceNetCtlScanIpcInt(const char* const* ssids, std::uint32_t count, const NetCtlScanOptions* options) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetScanInfoBssidIpcInt(void) { +int APS5_VABI sceNetCtlGetScanInfoBssidIpcInt(NetCtlScanBssidList* bssids, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetScanInfoByBssidIpcInt(void) { +int APS5_VABI sceNetCtlGetScanInfoByBssidIpcInt(const std::uint8_t* bssid, NetCtlScanInfo* info, void* buffer, std::size_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetScanInfoForSsidScanIpcInt(void) { +int APS5_VABI sceNetCtlGetScanInfoForSsidScanIpcInt(NetCtlScanInfo* info, void* buffer, std::size_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetScanInfoBssidForSsidListScanIpcInt(void) { +int APS5_VABI sceNetCtlGetScanInfoBssidForSsidListScanIpcInt(NetCtlScanBssidList* bssids, std::uint32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(void) { +int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(const std::uint8_t* bssid, NetCtlScanInfo* info, void* buffer, std::size_t* size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -691,12 +702,12 @@ int APS5_VABI sceNetCtlGetStateV6IpcInt(int* state) { return 0; } -int APS5_VABI sceNetCtlGetInfoV6IpcInt(void) { +int APS5_VABI sceNetCtlGetInfoV6IpcInt(std::uint32_t code, NetCtlInfoV6* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetNatInfoIpcInt(void) { +int APS5_VABI sceNetCtlGetNatInfoIpcInt(NetCtlNatInfoInternal* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -726,12 +737,12 @@ int APS5_VABI sceNetCtlEnableBandwidthManagementIpcInt(void) { return 0; } -int APS5_VABI sceNetCtlGetBandwidthInfoIpcInt(void) { +int APS5_VABI sceNetCtlGetBandwidthInfoIpcInt(NetCtlBandwidthInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(void) { +int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(NetCtlNetEvConfiguration* configuration) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -840,7 +851,7 @@ int APS5_VABI sceNetBweUnregisterCallbackIpcInt(int callbackId) { return 0; } -int APS5_VABI sceNetBweGetInfoIpcInt(void) { +int APS5_VABI sceNetBweGetInfoIpcInt(NetBweInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -865,7 +876,7 @@ int APS5_VABI sceNetBweSetInternetConnectionTestResultIpcInt(std::int32_t firstR return 0; } -int APS5_VABI sceNetCtlGetWifiInfoIpcInt(void) { +int APS5_VABI sceNetCtlGetWifiInfoIpcInt(NetCtlWifiInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index dbb3c48613..5c9e32df16 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -6,21 +6,11 @@ ### Unknown function info -- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address +- [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - argument registers and IPC transfer directions are recovered for the info, connection, scan, NAT, bandwidth and Wi-Fi queries. The connection configuration is 1464 bytes, scan options 88, BSSID output 96, scan info 112, IPv6 info 256, internal NAT info 196, bandwidth info 32, bandwidth-estimation info 28 and Wi-Fi info 48. Their field semantics still require the receiving service; incomplete declarations preserve this gap. Scan queries transfer a six-byte BSSID and use a 64-bit in/out buffer size. + - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware -- [sceNetCtlGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlConnectConfIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetScanInfoBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetScanInfoByBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetScanInfoForSsidScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetScanInfoBssidForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetScanInfoForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown -- [sceNetCtlGetInfoV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetNatInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetBandwidthInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetNetEvConfigInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpClearEvent](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpRegisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature @@ -32,8 +22,6 @@ - [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlGetWifiInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature ### Functional From d7606dc9fe4f4684659bde5769ab07f620f7b908 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:42:58 +0300 Subject: [PATCH 0676/1360] fix(ime): restore text validation and dictionary signatures --- core/libs/prx/libSceIme/Export.cpp | 31 +++++++++++++++++------------- docs/dev/debt/libSceIme.md | 15 ++------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 5ab0ae9f52..29d014c627 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -16,6 +16,9 @@ #include "prx/libSceIme/include/ImeInput.hpp" struct ImeVshContext; +struct ImeVshParameter; +struct ImePreeditMetaInfo; +struct ImeDictionaryWord; struct ImeVshCaretGeometry { std::int32_t x; @@ -31,6 +34,8 @@ struct ImeVshTextGeometry { std::uint32_t height; }; +static_assert(sizeof(ImeEditText) == 64); +static_assert(offsetof(ImeEvent, param) == 8); static_assert(sizeof(ImeVshCaretGeometry) == 16); static_assert(sizeof(ImeVshTextGeometry) == 16); @@ -600,7 +605,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { } } -int APS5_VABI sceImeVshOpen(void) { +int APS5_VABI sceImeVshOpen(ImeVshContext** context, const ImeVshParameter* parameter, const char16_t* text) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -615,7 +620,7 @@ int APS5_VABI sceImeVshUpdate(ImeVshContext* context) { return 0; } -int APS5_VABI sceImeVshUpdateContext(void) { +int APS5_VABI sceImeVshUpdateContext(ImeVshContext* context, const char16_t* text, std::uint32_t caretIndex) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -625,7 +630,7 @@ int APS5_VABI sceImeVshClearPreedit(ImeVshContext* context) { return 0; } -int APS5_VABI sceImeVshNotifySetText(void) { +int APS5_VABI sceImeVshNotifySetText(ImeVshContext* context, const char16_t* text, std::uint32_t length) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -704,12 +709,12 @@ int APS5_VABI sceImeVshSetSelectionText(ImeVshContext* context, const char16_t* return 0; } -int APS5_VABI sceImeCheckUpdateTextInfo(void) { +int APS5_VABI sceImeCheckUpdateTextInfo(ImeEditText* info, std::uint8_t repair) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeCheckRemoteEventParam(void) { +int APS5_VABI sceImeCheckRemoteEventParam(ImeEvent* event, std::uint8_t repair) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -719,17 +724,17 @@ int APS5_VABI sceImeKeyboardOpenInternal(std::int32_t userId, EventHandler handl return 0; } -int APS5_VABI sceImeVshSetParam(void) { +int APS5_VABI sceImeVshSetParam(ImeVshContext* context, const ImeVshParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshUpdateContext2(void) { +int APS5_VABI sceImeVshUpdateContext2(ImeVshContext* context, const char16_t* text, std::uint32_t caretIndex) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshSendTextInfo(void) { +int APS5_VABI sceImeVshSendTextInfo(ImeVshContext* context, const void* info, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -769,7 +774,7 @@ int APS5_VABI sceImeVshSetPanelPosition(ImeVshContext* context, float x, float y return 0; } -int APS5_VABI sceImeVshNotifyPreeditMetaInfo(void) { +int APS5_VABI sceImeVshNotifyPreeditMetaInfo(ImeVshContext* context, const ImePreeditMetaInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -784,22 +789,22 @@ int APS5_VABI sceImeConfigGet(void) { return 0; } -int APS5_VABI sceImeDicGetWords(void) { +int APS5_VABI sceImeDicGetWords(std::int32_t userId, std::uint64_t dictionary, ImeDictionaryWord* words) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeDicAddWord(void) { +int APS5_VABI sceImeDicAddWord(std::int32_t userId, std::uint64_t dictionary, const ImeDictionaryWord* word) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeDicDeleteWord(void) { +int APS5_VABI sceImeDicDeleteWord(std::int32_t userId, std::uint64_t dictionary, const ImeDictionaryWord* word) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeDicReplaceWord(void) { +int APS5_VABI sceImeDicReplaceWord(std::int32_t userId, std::uint64_t dictionary, const ImeDictionaryWord* oldWord, const ImeDictionaryWord* newWord) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 4e6a62319d..f4689b4ab8 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -6,27 +6,16 @@ ### Unknown function info +- [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - VSH context, UTF-16 text and dictionary argument registers are recovered. The VSH parameter is copied as 256 bytes; preedit metadata is eight bytes. Dictionary operations transfer 224-byte records, with a fixed 11200-byte output for GetWords; the 64-bit dictionary selector and record fields still need receiver-side identification. These payload types remain incomplete. UpdateTextInfo and RemoteEventParam use the existing 64-byte ImeEditText and event payload at offset eight; their byte-sized repair flag permits correcting invalid text ranges and caret positions. + - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn -- [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshUpdateContext](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshNotifySetText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeCheckUpdateTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeCheckRemoteEventParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSetParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshUpdateContext2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshSendTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyCurrentTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshNotifyPreeditMetaInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDicGetWords](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDicAddWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDicDeleteWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeDicReplaceWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [libSceIme pending exports](../../../core/libs/prx/libSceIme/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIme.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From 73fcc2f8674938e8e3dece6c94300cfe11b33d1c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:44:32 +0300 Subject: [PATCH 0677/1360] fix(hmd2): restore gaze and calibration signatures --- core/libs/prx/libSceHmd2/Export.cpp | 128 ++++++++++++++++++++++------ docs/dev/debt/libSceHmd2.md | 2 + 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index f6f53d804c..e78ebbbe10 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -10,6 +10,31 @@ #include <mutex> #include <stdexcept> +namespace Hmd2 { + +struct GazeResultParameter; +struct GazeResult; +struct FoveatedRenderingResult; +struct GazeCalibrationData; +struct GazeUserCalibrationData; +struct GazeUserCalibrationStatus; +struct GazeRawData; +struct GazeObjectSelectionParameter; +struct GazeObjectSelectionResult; +struct GazeObjectSelectionInitializeParameter; +struct GazeObjectSelectionMemoryParameter; +struct GazeObjectSelectionMemoryInfo; + +struct CalibrationPoint { + float x; + float y; + float z; +}; + +static_assert(sizeof(CalibrationPoint) == 12); + +} + namespace { constexpr std::int32_t DeviceHandle = 0x0e010000; @@ -94,12 +119,12 @@ std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { return 0; } -int APS5_VABI sceHmd2GazeGetResult() { +int APS5_VABI sceHmd2GazeGetResult(const Hmd2::GazeResultParameter* parameter, Hmd2::GazeResult* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { +int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const Hmd2::GazeResultParameter* parameter, Hmd2::FoveatedRenderingResult* result) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -292,7 +317,9 @@ APS5_EXPORT("9KnNj-dyASg", Hmd2BeforeInitializeZero); APS5_EXPORT("Qta2bkstbFk", Hmd2BeforeInitializeZero); APS5_EXPORT("5ZcObnDRBXE", Hmd2BeforeInitializeZero); APS5_EXPORT("AvPNn0qx4WQ", Hmd2BeforeInitializeZero); -APS5_EXPORT("jp0apqNGgi8", Hmd2BeforeInitializeZero); +int APS5_VABI sceHmd2SetIpdOffset(std::uint8_t enabled, float offset) { + return BeforeInitialize(0, __func__); +} APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); @@ -310,7 +337,9 @@ APS5_EXPORT("MjY1gBSaed0", Hmd2BeforeInitializeZero); APS5_EXPORT("evAMjJYHqW4", Hmd2BeforeInitializeZero); APS5_EXPORT("r93FLEZ3BBk", Hmd2BeforeInitializeZero); APS5_EXPORT("IdYURdEdNFo", Hmd2BeforeInitializeZero); -APS5_EXPORT("CVcL2Cu2AEI", Hmd2BeforeInitializeZero); +int APS5_VABI sceHmd2GazeActivateGazeTracking(std::int32_t mode) { + return BeforeInitialize(0, __func__); +} APS5_EXPORT("NmKIfVxr2s0", Hmd2BeforeInitializeZero); APS5_EXPORT("KeDLkkp62Gg", Hmd2BeforeInitializeZero); APS5_EXPORT("+aadu6-ra+A", Hmd2BeforeInitializeZero); @@ -347,14 +376,22 @@ APS5_EXPORT("TJ8lyRsjFY4", Hmd2BeforeInitializeError0004); APS5_EXPORT("-XQVcLnyX68", Hmd2BeforeInitializeError0004); APS5_EXPORT("giQwQoHhYYA", Hmd2BeforeInitializeError0004); APS5_EXPORT("zNc9IkgawMI", Hmd2BeforeInitializeError0004); -APS5_EXPORT("-tnG5UJPvAc", Hmd2BeforeInitializeError0004); -APS5_EXPORT("FPhFmG+jXuQ", Hmd2BeforeInitializeError0004); +int APS5_VABI sceHmd2GazeStartUserCalibration() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +int APS5_VABI sceHmd2GazeStopUserCalibration() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} APS5_EXPORT("CKlkvNngofQ", Hmd2BeforeInitializeError0004); -APS5_EXPORT("yL1CZaciFWE", Hmd2BeforeInitializeError0004); +int APS5_VABI sceHmd2GazeSetUserCalibrationPoint(Hmd2::CalibrationPoint point) { + return BeforeInitialize(Hmd2::Error0004, __func__); +} APS5_EXPORT("qlzgyG3s0RY", Hmd2BeforeInitializeError0004); APS5_EXPORT("kTIDrDLzMv0", Hmd2BeforeInitializeError0004); APS5_EXPORT("9Gee0ef8a+U", Hmd2BeforeInitializeError0004); -APS5_EXPORT("ecZVjQdwmBI", Hmd2BeforeInitializeError0004); +int APS5_VABI sceHmd2GazeSetEnabledEye(std::int32_t eye) { + return BeforeInitialize(Hmd2::Error0004, __func__); +} std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull() { return BeforeInitialize(Hmd2::ParameterNull, __func__); @@ -367,14 +404,20 @@ APS5_EXPORT("J44idh9izZM", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("Ps0wAvYaa1E", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("kLnMR8oQ1MY", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("kMz4rbBxvjo", Hmd2BeforeInitializeParameterNull); -APS5_EXPORT("DIU1sFRRaiE", Hmd2BeforeInitializeParameterNull); -APS5_EXPORT("ZQvU4vDzZGQ", Hmd2BeforeInitializeParameterNull); +int APS5_VABI sceHmd2GazeGetCalibrationData(std::int32_t type, Hmd2::GazeCalibrationData* data) { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +int APS5_VABI sceHmd2GazeSetUserCalibrationData(const Hmd2::GazeUserCalibrationData* data) { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} APS5_EXPORT("TJpyGGP9YEY", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("Dih+Odk+RC0", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("oaqdSJw+HVY", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("m35gtHuanXU", Hmd2BeforeInitializeParameterNull); APS5_EXPORT("CTM7QPLJF9Q", Hmd2BeforeInitializeParameterNull); -APS5_EXPORT("+rExHeC9lP0", Hmd2BeforeInitializeParameterNull); +int APS5_VABI sceHmd2GazeObjectSelectionInitialize(const Hmd2::GazeObjectSelectionInitializeParameter* parameter) { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid() { return BeforeInitialize(Hmd2::ParameterInvalid, __func__); @@ -395,18 +438,30 @@ std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized() { APS5_EXPORT("d3SEIYTP6wc", Hmd2BeforeInitializeReprojectionNotInitialized); APS5_EXPORT("EVECbTvBezo", Hmd2BeforeInitializeReprojectionNotInitialized); APS5_EXPORT("ciusPdJKjSU", Hmd2BeforeInitializeReprojectionNotInitialized); -APS5_EXPORT("DIwqTkcZiJo", Hmd2BeforeInitializeReprojectionNotInitialized); -APS5_EXPORT("64ERZWVFBTE", Hmd2BeforeInitializeReprojectionNotInitialized); -APS5_EXPORT("z0+0gXUJWs8", Hmd2BeforeInitializeReprojectionNotInitialized); -APS5_EXPORT("rjAd4nCY4DM", Hmd2BeforeInitializeReprojectionNotInitialized); -APS5_EXPORT("Acyg4vTdGm0", Hmd2BeforeInitializeReprojectionNotInitialized); +int APS5_VABI sceHmd2ReprojectionSetUserEventStart() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionSetUserEventEnd() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionClearUserEventEnd() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionGetCurrentOutput() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); APS5_EXPORT("9up+EPhw1No", Hmd2BeforeInitializeReprojectionNotInitialized); std::int32_t APS5_VABI Hmd2BeforeInitializeError002C() { return BeforeInitialize(Hmd2::Error002C, __func__); } -APS5_EXPORT("V1kLQmP-17U", Hmd2BeforeInitializeError002C); +int APS5_VABI sceHmd2GazeObjectSelectionTerminate() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} std::int32_t APS5_VABI Hmd2BeforeInitializeError1002() { return BeforeInitialize(Hmd2::Error1002, __func__); @@ -563,8 +618,14 @@ APS5_EXPORT("Tc9rasWfrm4", Hmd2CheckFirstThenError0004); APS5_EXPORT("ckTbKTX4KZg", Hmd2CheckFirstThenError0004); APS5_EXPORT("D7576PUbUjk", Hmd2CheckFirstThenError0004); APS5_EXPORT("tTJ04SrBT8U", Hmd2CheckFirstThenError0004); -APS5_EXPORT("sDP7hZ-P6OI", Hmd2CheckFirstThenError0004); -APS5_EXPORT("J+eVNbN9ndY", Hmd2CheckFirstThenError0004); +int APS5_VABI sceHmd2GazeGetDeviceStatus(std::int32_t* status) { + if (status == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +int APS5_VABI sceHmd2GazeGetUserCalibrationStatus(Hmd2::GazeUserCalibrationStatus* status) { + if (status == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} APS5_EXPORT("kozvHBNyxf0", Hmd2CheckFirstThenError0004); APS5_EXPORT("638Tmg5y6U4", Hmd2CheckFirstThenError0004); @@ -576,7 +637,10 @@ APS5_EXPORT("8ETtFtp5zqY", Hmd2CheckFirstThenZero); APS5_EXPORT("CWPkBm3EXQA", Hmd2CheckFirstThenZero); APS5_EXPORT("R62liQQIAaA", Hmd2CheckFirstThenZero); APS5_EXPORT("-bcWLHsdzo0", Hmd2CheckFirstThenZero); -APS5_EXPORT("JngJzgNbu8U", Hmd2CheckFirstThenZero); +int APS5_VABI sceHmd2SetFieldOfView(const Hmd2::FieldOfView* fieldOfView) { + if (fieldOfView == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} APS5_EXPORT("xNBSz8xrGdk", Hmd2CheckFirstThenZero); APS5_EXPORT("yUVPfN-jRIU", Hmd2CheckFirstThenZero); APS5_EXPORT("BKPR3RZ5aO4", Hmd2CheckFirstThenZero); @@ -610,8 +674,16 @@ std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void* a0, con return BeforeInitialize(Hmd2::ParameterInvalid, __func__); } APS5_EXPORT("PPX68fNToFM", Hmd2CheckFirstTwoThenParameterInvalid); -APS5_EXPORT("LxPIfjHaY4c", Hmd2CheckFirstTwoThenParameterInvalid); -APS5_EXPORT("w8gBTTdg+po", Hmd2CheckFirstTwoThenParameterInvalid); +int APS5_VABI sceHmd2GazeObjectSelectionQueryMemory(const Hmd2::GazeObjectSelectionMemoryParameter* parameter, Hmd2::GazeObjectSelectionMemoryInfo* info) { + if (parameter == nullptr) return Hmd2::ParameterNull; + if (info == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +int APS5_VABI sceHmd2GazeObjectSelectionGetSelectedObject(const Hmd2::GazeObjectSelectionParameter* parameter, Hmd2::GazeObjectSelectionResult* result) { + if (parameter == nullptr) return Hmd2::ParameterNull; + if (result == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void* a0, const void* a1) { (void)a0; @@ -647,7 +719,11 @@ std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void* a0, const return BeforeInitialize(Hmd2::ParameterInvalid, __func__); } APS5_EXPORT("72U64E+0z6U", Hmd2CheckSecondThenParameterInvalid); -APS5_EXPORT("LCQzi5ILGrY", Hmd2CheckSecondThenParameterInvalid); +int APS5_VABI sceHmd2GazeGetCalibrationDataMaxSize(std::int32_t type, std::uint32_t* size) { + (void)type; + if (size == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void* a0) { if (a0 == nullptr) return Hmd2::ParameterNull; @@ -702,7 +778,11 @@ std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void* a0, const vo if (a1 == nullptr) return Hmd2::ParameterNull; return BeforeInitialize(Hmd2::InvalidHandle, __func__); } -APS5_EXPORT("wEHILc7TP04", Hmd2CheckSecondThenInvalidHandle); +int APS5_VABI sceHmd2GazeInsertRawDataWithHandle(std::int32_t handle, Hmd2::GazeRawData* data) { + (void)handle; + if (data == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void* a0, const void* a1) { (void)a0; diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 1dacc21ad2..21b7585b25 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -83,3 +83,5 @@ - [Hmd2SlotUpTo2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a72002c` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `wQ44gBwBw44`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known + +- [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte parameter and a 56-byte or 24-byte output. Object selection uses a 120-byte parameter and 56-byte result, a 24-byte initialization parameter, and 8-byte/16-byte memory-query structures. These payload fields remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. From 03e14bd74c8d5bcf9ea7d67a88581c113efcf3a7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:45:31 +0300 Subject: [PATCH 0678/1360] fix(hmd2): restore reprojection frame and event signatures --- core/libs/prx/libSceHmd2/Export.cpp | 45 ++++++++++++++++++++--------- docs/dev/debt/libSceHmd2.md | 5 ++-- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index e78ebbbe10..68fa34443f 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -12,6 +12,23 @@ namespace Hmd2 { +struct ReprojectionStatus; +struct ReprojectionOutput; +struct ReprojectionInitializeParameter; + +struct MirroringParameter { + void* workMemory; + void* displayMemory; +}; + +struct MirroringOption { + std::uint32_t mode; + std::uint32_t reserved[7]; +}; + +static_assert(sizeof(MirroringParameter) == 16); +static_assert(sizeof(MirroringOption) == 32); + struct GazeResultParameter; struct GazeResult; struct FoveatedRenderingResult; @@ -75,7 +92,7 @@ std::int32_t GetDeviceInformation(Hmd2::DeviceInformation* info) { extern "C" { -int APS5_VABI sceHmd2ReprojectionGetStatus() { +int APS5_VABI sceHmd2ReprojectionGetStatus(Hmd2::ReprojectionStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -155,7 +172,7 @@ std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std:: return DeviceHandle; } -int APS5_VABI sceHmd2ReprojectionBeginFrame() { +int APS5_VABI sceHmd2ReprojectionBeginFrame(std::uint64_t frameId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -170,12 +187,12 @@ int APS5_VABI sceHmd2ReprojectionDisableVrMode() { return 0; } -int APS5_VABI sceHmd2ReprojectionEnableMirroring() { +int APS5_VABI sceHmd2ReprojectionEnableMirroring(const Hmd2::MirroringParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionEnableVrMode() { +int APS5_VABI sceHmd2ReprojectionEnableVrMode(std::uint64_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -188,12 +205,12 @@ Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { return {0x4000, 0x100}; } -int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { +int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime(std::uint64_t frameId, std::uint64_t* time) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionInitialize() { +int APS5_VABI sceHmd2ReprojectionInitialize(const Hmd2::ReprojectionInitializeParameter* parameter, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -207,12 +224,12 @@ Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { return {0x6000000, 0x10000}; } -int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { +int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetMirroringOption() { +int APS5_VABI sceHmd2ReprojectionSetMirroringOption(const Hmd2::MirroringOption* option) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -232,7 +249,7 @@ int APS5_VABI sceHmd2ReprojectionSetRenderConfig() { return 0; } -int APS5_VABI sceHmd2ReprojectionSetTiming() { +int APS5_VABI sceHmd2ReprojectionSetTiming(std::int32_t timing) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -284,12 +301,12 @@ int APS5_VABI sceHmd2ReprojectionSet() { return 0; } -int APS5_VABI sceHmd2ReprojectionSetEventGpuEnd() { +int APS5_VABI sceHmd2ReprojectionSetEventGpuEnd(KernelEqueue queue, std::int32_t eventId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetEventGpuStart() { +int APS5_VABI sceHmd2ReprojectionSetEventGpuStart(KernelEqueue queue, std::int32_t eventId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -438,10 +455,10 @@ std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized() { APS5_EXPORT("d3SEIYTP6wc", Hmd2BeforeInitializeReprojectionNotInitialized); APS5_EXPORT("EVECbTvBezo", Hmd2BeforeInitializeReprojectionNotInitialized); APS5_EXPORT("ciusPdJKjSU", Hmd2BeforeInitializeReprojectionNotInitialized); -int APS5_VABI sceHmd2ReprojectionSetUserEventStart() { +int APS5_VABI sceHmd2ReprojectionSetUserEventStart(KernelEqueue queue, std::int32_t eventId) { return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); } -int APS5_VABI sceHmd2ReprojectionSetUserEventEnd() { +int APS5_VABI sceHmd2ReprojectionSetUserEventEnd(KernelEqueue queue, std::int32_t eventId) { return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); } int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { @@ -450,7 +467,7 @@ int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { int APS5_VABI sceHmd2ReprojectionClearUserEventEnd() { return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); } -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput() { +int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(Hmd2::ReprojectionOutput* output) { return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); } APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 21b7585b25..47cbda8590 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -6,12 +6,11 @@ ### Unknown function info -- PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal - PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list -- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature @@ -85,3 +84,5 @@ - [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known - [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte parameter and a 56-byte or 24-byte output. Object selection uses a 120-byte parameter and 56-byte result, a 24-byte initialization parameter, and 8-byte/16-byte memory-query structures. These payload fields remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. + +- [libSceHmd2 reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - frame identifiers and predicted display times are 64-bit. Event setters accept a kernel event queue and 32-bit event ID, consumed by sceKernelTriggerUserEvent. Mirroring uses two memory pointers and a separate 32-byte mode/reserved option. Status and current-output queries write 104 and 32 bytes; those field layouts and the reprojection initialization fields remain incomplete. Positional-reprojection mode is a byte with values 0 through 5, not a boolean. From 8d202ee697a6927f8a7cd80dbb145dfb8f247813 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:46:48 +0300 Subject: [PATCH 0679/1360] fix(hmd): restore device screen and capture signatures --- core/libs/prx/libSceHmd/Export.cpp | 54 +++++++++++++++++++----------- docs/dev/debt/libSceHmd.md | 22 ++---------- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index fbdfeacca6..693276b1a2 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -3,6 +3,20 @@ #include <atomic> +struct HmdVr2dData; +struct HmdPuPositionParameter; +struct HmdOpticalParameter; +struct HmdCrashDumpInfo; +struct HmdScreenOutputParameter; +struct HmdScreenFadeParameter; +struct HmdScreenStatus; +struct HmdCaptureInitializeParameter; +struct HmdCaptureInfo; +struct HmdLiveCaptureParameter; +struct HmdReprojectionDebugInfo; +struct HmdReprojectionMultilayerDebugInfo; +struct HmdReprojectionCallbackParameter; + namespace { std::atomic<bool> initialized{false}; @@ -127,7 +141,7 @@ int APS5_VABI sceHmdInternalSetVirtualDisplaySize(std::uint8_t size) { return 0; } -int APS5_VABI sceHmdInternalSocialScreenSetOutput(void) { +int APS5_VABI sceHmdInternalSocialScreenSetOutput(std::uint32_t output, const HmdScreenOutputParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -220,7 +234,7 @@ int APS5_VABI sceHmdInternalGetRequiredPUPVersion(std::uint32_t* firstVersion, s return 0; } -int APS5_VABI sceHmdReprojectionSetCallback(void) { +int APS5_VABI sceHmdReprojectionSetCallback(const HmdReprojectionCallbackParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -259,7 +273,7 @@ int APS5_VABI sceHmdReprojectionStartMultilayer(void) { return 0; } -int APS5_VABI sceHmdInternalPushVr2dData(void) { +int APS5_VABI sceHmdInternalPushVr2dData(const HmdVr2dData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -284,7 +298,7 @@ int APS5_VABI sceHmdInternalCrashReportReadDataSize(std::uint32_t* dataSize) { return 0; } -int APS5_VABI sceHmdInternalGetVr2dData(void) { +int APS5_VABI sceHmdInternalGetVr2dData(HmdVr2dData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -294,12 +308,12 @@ int APS5_VABI sceHmdInternalAnotherScreenSendAudio(const HmdScreenData* data) { return 0; } -int APS5_VABI sceHmdInternalGetPuPositionParam(void) { +int APS5_VABI sceHmdInternalGetPuPositionParam(HmdPuPositionParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdReprojectionInitializeCapture(void) { +int APS5_VABI sceHmdReprojectionInitializeCapture(const HmdCaptureInitializeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -330,7 +344,7 @@ int APS5_VABI sceHmdReprojectionSetDisplayBuffers(void) { return 0; } -int APS5_VABI sceHmdInternalAnotherScreenSetFadeAndSwitch(void) { +int APS5_VABI sceHmdInternalAnotherScreenSetFadeAndSwitch(const HmdScreenFadeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -359,7 +373,7 @@ int APS5_VABI sceHmdInternalSetS3dPassMode(std::uint8_t mode) { return 0; } -int APS5_VABI sceHmdReprojectionStopCapture(void) { +int APS5_VABI sceHmdReprojectionStopCapture(HmdCaptureInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -408,7 +422,7 @@ int APS5_VABI sceHmdInternalSetPortConnection(std::uint64_t connection) { return 0; } -int APS5_VABI sceHmdReprojectionDebugGetLastInfo(void) { +int APS5_VABI sceHmdReprojectionDebugGetLastInfo(HmdReprojectionDebugInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -498,12 +512,12 @@ int APS5_VABI sceHmdInternalSetHmuPowerControl(std::uint8_t control) { return 0; } -int APS5_VABI sceHmdInternalBindDeviceWithUserId(void) { +int APS5_VABI sceHmdInternalBindDeviceWithUserId(std::int32_t handle, std::int32_t userId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalGetHmuSerialNumber(void) { +int APS5_VABI sceHmdInternalGetHmuSerialNumber(std::uint8_t* serialNumber) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -518,17 +532,17 @@ int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(bool* available) { return 0; } -int APS5_VABI sceHmdInternalAnotherScreenGetAudioStatus(void) { +int APS5_VABI sceHmdInternalAnotherScreenGetAudioStatus(HmdScreenStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalSeparateModeGetAudioStatus(void) { +int APS5_VABI sceHmdInternalSeparateModeGetAudioStatus(HmdScreenStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdReprojectionStartLiveCapture(void) { +int APS5_VABI sceHmdReprojectionStartLiveCapture(const HmdLiveCaptureParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -548,7 +562,7 @@ int APS5_VABI sceHmdReprojectionFinalize(void) { return 0; } -int APS5_VABI sceHmdInternalSocialScreenSetFadeAndSwitch(void) { +int APS5_VABI sceHmdInternalSocialScreenSetFadeAndSwitch(const HmdScreenFadeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -633,7 +647,7 @@ int APS5_VABI sceHmdInternalCrashReportCancel(void) { return 0; } -int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(void) { +int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(HmdScreenStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -703,7 +717,7 @@ int APS5_VABI sceHmdReprojectionUnsetDisplayBuffers(void) { return 0; } -int APS5_VABI sceHmdInternalGetHmuOpticalParam(void) { +int APS5_VABI sceHmdInternalGetHmuOpticalParam(HmdOpticalParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -777,7 +791,7 @@ int APS5_VABI sceHmdInternalMapSharedMemory(void) { return 0; } -int APS5_VABI sceHmdInternalGetCrashDumpInfo(void) { +int APS5_VABI sceHmdInternalGetCrashDumpInfo(HmdCrashDumpInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -859,7 +873,7 @@ int APS5_VABI sceHmdReprojectionStop(void) { return 0; } -int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(void) { +int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(HmdScreenStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -884,7 +898,7 @@ int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(std::uint8_t* en return 0; } -int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(void) { +int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(HmdReprojectionMultilayerDebugInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 73cc19fd6c..2c4ba6528c 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -6,55 +6,37 @@ ### Unknown function info +- [libSceHmd internal and capture exports](../../../core/libs/prx/libSceHmd/Export.cpp) - input/output directions and argument registers are recovered for shared VR2D data, device binding, optical and serial queries, screen status and capture operations. VR2D records are 128 bytes, optical output 16, serial output 16, PU position output 20, crash-dump output 68, audio/video status 32, and reprojection debug outputs 216 or 1136. Payload field meanings and capture callback contracts remain incomplete; these structures use forward declarations. + - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count -- [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionSetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStopCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalBindDeviceWithUserId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetHmuSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSeparateModeGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStartLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 86b8fe3a253267f07f307648a833276027d5e376 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:48:37 +0300 Subject: [PATCH 0680/1360] fix(pad): restore report tracker and vr trigger signatures --- core/libs/prx/libScePad/Export.cpp | 49 +++++++++++++++++++----------- docs/dev/debt/libScePad.md | 12 ++------ 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index ab3e74552d..52d915d3ca 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -11,6 +11,21 @@ #include "prx/libScePad/include/Pad.hpp" #include "prx/libScePad/include/PadState.hpp" +struct PadDataExtended; +struct PadVrControllerData; +struct PadVrControllerDeviceInformation; +struct PadVrTriggerEffect; +struct PadVrTriggerEffects; +struct PadTriggerEffectExtended; +struct PadExtensionReport; +struct PadExtensionUnitInfo; +struct PadTriggerInternalState; +struct PadTrackerData; +struct PadBlasterTrackerData; +struct PadAstonTrackerData; +struct PadUiData; +struct PadUiTuningData; + namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast<int>(0x80920007); @@ -183,11 +198,11 @@ int APS5_VABI scePadOpenExt(int userId, int type, int index, const void* param) return PAD_ERROR_DEVICE_NOT_CONNECTED; } -int APS5_VABI scePadReadExt() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadReadExt(std::int32_t handle, PadDataExtended* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetFeatureReport() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadGetFeatureReport(std::int32_t handle, std::uint8_t reportId, void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetFeatureReport() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadSetFeatureReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -283,12 +298,12 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enab return PAD_OK; } -int APS5_VABI scePadVrControllerGetDeviceInformation() { +int APS5_VABI scePadVrControllerGetDeviceInformation(std::int32_t handle, PadVrControllerDeviceInformation* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerRead() { +int APS5_VABI scePadVrControllerRead(std::int32_t handle, PadVrControllerData* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -305,12 +320,12 @@ int APS5_VABI scePadVrControllerReadState(int handle, void* state) { return 0; } -int APS5_VABI scePadVrControllerSetTriggerEffect() { +int APS5_VABI scePadVrControllerSetTriggerEffect(std::int32_t handle, const PadVrTriggerEffect* effect) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerSetTriggerEffects() { +int APS5_VABI scePadVrControllerSetTriggerEffects(std::int32_t leftHandle, std::int32_t rightHandle, const PadVrTriggerEffects* effects) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -347,7 +362,7 @@ int APS5_VABI scePadGetAccessibilityControllerState(int handle, std::uint32_t* s return PAD_OK; } -int APS5_VABI scePadSetTriggerEffectExt(void) { +int APS5_VABI scePadSetTriggerEffectExt(std::int32_t handle, const PadTriggerEffectExtended* effect) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -357,17 +372,17 @@ int APS5_VABI scePadLastLotusButtonData(void) { return 0; } -int APS5_VABI scePadReadStateExt(void) { +int APS5_VABI scePadReadStateExt(std::int32_t handle, PadDataExtended* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetExtensionReport(void) { +int APS5_VABI scePadSetExtensionReport(std::int32_t handle, const PadExtensionReport* report) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetExtensionUnitInfo(void) { +int APS5_VABI scePadGetExtensionUnitInfo(std::int32_t handle, PadExtensionUnitInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -536,7 +551,7 @@ int APS5_VABI scePadSetTriggerEffectStrength(std::int32_t handle, std::uint32_t return 0; } -int APS5_VABI scePadGetTriggerEffectInternalState(void) { +int APS5_VABI scePadGetTriggerEffectInternalState(std::int32_t handle, PadTriggerInternalState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -586,12 +601,12 @@ int APS5_VABI scePadGetUsbDetectionState(void) { return 0; } -int APS5_VABI scePadUiReadState(void) { +int APS5_VABI scePadUiReadState(std::int32_t handle, PadUiData* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadUiReadStateForTuning(void) { +int APS5_VABI scePadUiReadStateForTuning(std::int32_t handle, PadUiTuningData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -791,12 +806,12 @@ int APS5_VABI scePadSetLightBarForTracker(std::int32_t handle, const PadLightBar return 0; } -int APS5_VABI scePadReadForTracker(void) { +int APS5_VABI scePadReadForTracker(std::int32_t handle, PadTrackerData* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadReadBlasterForTracker(void) { +int APS5_VABI scePadReadBlasterForTracker(std::int32_t handle, PadBlasterTrackerData* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -856,7 +871,7 @@ int APS5_VABI scePadIsMoveReproductionModel(std::int32_t handle) { return 0; } -int APS5_VABI scePadReadAstonForTracker(void) { +int APS5_VABI scePadReadAstonForTracker(std::int32_t handle, PadAstonTrackerData* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 738e21a092..fdd00cf961 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,6 +2,8 @@ ### Unknown function info +- [libScePad report and tracking exports](../../../core/libs/prx/libScePad/Export.cpp) - read, extended-read, tracker, UI, VR-controller and report argument registers are recovered. Feature reports use a byte report ID and 64-bit byte count, capped at 64. VR trigger effects consume a 56-byte record; the dual-controller call uses two handles and a mask with records at offsets 8 and 64. Remaining payload field layouts use incomplete declarations, including extended samples and kernel-owned extension reports. + - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw @@ -11,11 +13,7 @@ - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [scePadSetTriggerEffectExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadLastLotusButtonData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadReadStateExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetExtensionReport](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetExtensionUnitInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadOpenExt2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -24,10 +22,7 @@ - [scePadShareOutputDataWithFunctionType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetButtonRemappingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetTriggerEffectInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetUsbDetectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadUiReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadUiReadStateForTuning](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetStickTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetTriggerTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -44,9 +39,6 @@ - [scePadResetLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadReadForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadReadBlasterForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDeviceInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProcessFocus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From 2f8bf3c16ecfb0f1ec2b68bc9dad8470987f329b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:49:04 +0300 Subject: [PATCH 0681/1360] fix(pad): restore profile and device setting signatures --- core/libs/prx/libScePad/Export.cpp | 52 +++++++++++++++++++----------- docs/dev/debt/libScePad.md | 21 ++---------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 52d915d3ca..4eab1575c4 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -11,6 +11,20 @@ #include "prx/libScePad/include/Pad.hpp" #include "prx/libScePad/include/PadState.hpp" +struct PadStickTuningParameter; +struct PadTriggerTuningParameter; +struct PadHuntProfile; +struct PadLeonardoProfile; +struct PadProfileInfo; +struct PadActiveProfileInfo; +struct PadSavedSettingInfo; +struct PadUsbDetectionState; +struct PadVrUsbConnectionStatus; +struct PadDeviceInfo; +struct PadLicenseControllerInformation; +struct PadRemoteSetting; +struct PadVirtualTriggerSetting; +struct PadProcessFocusParameter; struct PadDataExtended; struct PadVrControllerData; struct PadVrControllerDeviceInformation; @@ -486,12 +500,12 @@ int APS5_VABI scePadGetCapability(std::int32_t deviceId, std::uint32_t* capabili return 0; } -int APS5_VABI scePadShareOutputData(void) { +int APS5_VABI scePadShareOutputData(std::int32_t source, std::int32_t destination) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadShareOutputDataWithFunctionType(void) { +int APS5_VABI scePadShareOutputDataWithFunctionType(std::int32_t source, std::int32_t destination, std::uint32_t functionType) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -596,7 +610,7 @@ int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { return 0; } -int APS5_VABI scePadGetUsbDetectionState(void) { +int APS5_VABI scePadGetUsbDetectionState(std::int32_t deviceId, PadUsbDetectionState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -616,17 +630,17 @@ int APS5_VABI scePadSetStickTriggerTuningMode(std::int32_t handle, std::uint32_t return 0; } -int APS5_VABI scePadSetStickTuningParam(void) { +int APS5_VABI scePadSetStickTuningParam(std::int32_t handle, std::uint32_t stick, const PadStickTuningParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetTriggerTuningParam(void) { +int APS5_VABI scePadSetTriggerTuningParam(std::int32_t handle, const PadTriggerTuningParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetProfileHunt(void) { +int APS5_VABI scePadGetProfileHunt(std::int32_t handle, std::int32_t profileId, PadHuntProfile* profile) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -636,17 +650,17 @@ int APS5_VABI scePadSetProfileHunt(void) { return 0; } -int APS5_VABI scePadResetHunt(void) { +int APS5_VABI scePadResetHunt(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetProfileInfo(void) { +int APS5_VABI scePadGetProfileInfo(std::int32_t deviceId, std::int32_t profileId, PadProfileInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetActiveProfileInfo(void) { +int APS5_VABI scePadGetActiveProfileInfo(std::int32_t handle, PadActiveProfileInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -666,17 +680,17 @@ int APS5_VABI scePadSetActiveProfileSlot(std::int32_t deviceId, std::uint32_t sl return 0; } -int APS5_VABI scePadGetSavedSettingInfo(void) { +int APS5_VABI scePadGetSavedSettingInfo(std::int32_t handle, PadSavedSettingInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetSavedSettingInfo(void) { +int APS5_VABI scePadSetSavedSettingInfo(std::int32_t handle, const PadSavedSettingInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerGetPhysicalUsbConnectionStatus(void) { +int APS5_VABI scePadVrControllerGetPhysicalUsbConnectionStatus(PadVrUsbConnectionStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -696,7 +710,7 @@ int APS5_VABI scePadSetControllerTypeLeonardo(std::int32_t deviceId, std::int32_ return 0; } -int APS5_VABI scePadGetProfileLeonardo(void) { +int APS5_VABI scePadGetProfileLeonardo(std::int32_t handle, std::int32_t profileId, PadLeonardoProfile* profile) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -706,7 +720,7 @@ int APS5_VABI scePadSetProfileLeonardo(void) { return 0; } -int APS5_VABI scePadResetLeonardo(void) { +int APS5_VABI scePadResetLeonardo(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -906,12 +920,12 @@ int APS5_VABI scePadDisconnectDevice(std::int32_t deviceId) { return 0; } -int APS5_VABI scePadGetDeviceInfo(void) { +int APS5_VABI scePadGetDeviceInfo(std::int32_t deviceId, PadDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetProcessFocus(void) { +int APS5_VABI scePadSetProcessFocus(const PadProcessFocusParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -941,12 +955,12 @@ int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(void) { return 0; } -int APS5_VABI scePadVirtualDeviceGetRemoteSetting(void) { +int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t deviceId, PadRemoteSetting* setting) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(void) { +int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, const PadVirtualTriggerSetting* setting) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -976,7 +990,7 @@ int APS5_VABI scePadEnablePs4CompatibleMode(std::int32_t handle, bool enabled) { return 0; } -int APS5_VABI scePadGetLicenseControllerInformation(void) { +int APS5_VABI scePadGetLicenseControllerInformation(std::int32_t handle, PadLicenseControllerInformation* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index fdd00cf961..81f69445a0 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,6 +2,8 @@ ### Unknown function info +- [libScePad profile and device-setting exports](../../../core/libs/prx/libScePad/Export.cpp) - profile getters, tuning, saved settings, USB state, shared output and virtual-device setting arguments are recovered. Hunt and Leonardo reset each consume only a handle; physical VR USB status consumes only an output pointer. Remote settings output is 64 bytes and license-controller information is 1224 bytes. Kernel ioctl payload fields remain incomplete. + - [libScePad report and tracking exports](../../../core/libs/prx/libScePad/Export.cpp) - read, extended-read, tracker, UI, VR-controller and report argument registers are recovered. Feature reports use a byte report ID and 64-bit byte count, capped at 64. VR trigger effects consume a 56-byte record; the dual-controller call uses two handles and a mask with records at offsets 8 and 64. Remaining payload field layouts use incomplete declarations, including extended samples and kernel-owned extension reports. - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled @@ -18,37 +20,18 @@ - [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetLightBarBlinking](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadShareOutputData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadShareOutputDataWithFunctionType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetButtonRemappingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetUsbDetectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetStickTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetTriggerTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadResetHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetProfileInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetActiveProfileInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetSavedSettingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetSavedSettingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVrControllerGetPhysicalUsbConnectionStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadParseUniqueDataLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadResetLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetDeviceInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetProcessFocus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceInsertTriggerInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceSetTriggerEffectSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetVirtualDeviceComponentInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetLicenseControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadHistory](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadRemoteControlInsertDataForAston](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature From f0e10376bb39331d63cd832bdd19c465f563adc2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:50:58 +0300 Subject: [PATCH 0682/1360] fix(http): restore epoll and diagnostic query signatures --- core/libs/prx/libSceHttp/Export.cpp | 13 ++++++++----- docs/dev/debt/libSceHttp.md | 15 ++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 092e6d3fb5..e6e88e47a5 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -24,6 +24,9 @@ // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. static std::atomic<int> g_nextHandle{1}; +struct HttpConnectionStat; +struct HttpRequestStat; + using HttpRequestStatusCallback = void (APS5_VABI *)(int, int, void*); struct RequestStatusRegistration { HttpRequestStatusCallback callback; @@ -706,12 +709,12 @@ int APS5_VABI sceHttpDbgShowMemoryPoolStat(void) { return 0; } -int APS5_VABI sceHttpDbgGetConnectionStat(void) { +int APS5_VABI sceHttpDbgGetConnectionStat(std::int32_t connectionId, HttpConnectionStat* stat) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpDbgGetRequestStat(void) { +int APS5_VABI sceHttpDbgGetRequestStat(std::int32_t requestId, HttpRequestStat* stat) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -731,12 +734,12 @@ int APS5_VABI sceHttpGetAuthEnabled(int id, int* enabled) { return 0; } -int APS5_VABI sceHttpSetEpollId(void) { +int APS5_VABI sceHttpSetEpollId(std::int32_t id, std::int32_t epollId, void* userArg) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttpGetEpollId(void) { +int APS5_VABI sceHttpGetEpollId(std::int32_t id, std::int32_t* epollId, void** userArg) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -837,7 +840,7 @@ int APS5_VABI sceHttpGetRegisteredCtxIds(int contextId, int* netPoolId, int* ssl return 0; } -int APS5_VABI sceHttpGetConnectionStat(void) { +int APS5_VABI sceHttpGetConnectionStat(std::int32_t connectionId, HttpConnectionStat* stat) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 8163e0c50f..59b4ddb5b2 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -8,6 +8,8 @@ ### Unknown function info - [HTTP object options](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - a template starts with redirects followed, blocking calls and no `Accept-Encoding: gzip`, and a connection or request copies its parent's options when it is created. The signatures of the option calls, these defaults, the copying and the order of the checks (`BEFORE_INIT` first; the last `sceHttpTerm` ends every object's options) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4) and are not measured on a PS5. `sceHttpSetDefaultAcceptEncodingGZIPEnabled` sets the gzip value for templates created later in that context, where shadPS4 keeps one value for the library, and `sceHttpUnsetEpoll` takes a template or connection id, where shadPS4 looks only at requests. The options change no request, as none reaches the network +- [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - connection and request diagnostic signatures are recovered; their outputs contain four and three 32-bit words, respectively. Field semantics remain incomplete. Epoll setters/getters distinguish a 32-bit epoll ID from a pointer-sized handle, and use a pointer-sized user argument. The supplied firmware getter dispatch does not handle the pointer-handle selector used by GetEpoll, so its intended successful behavior is not recoverable from this implementation. + - [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end above `INT_MAX` throws instead of wrapping @@ -23,17 +25,12 @@ - [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - read from the disassembly of libSceHttp.sprx on firmware 13.60, not measured on a console and not compared with other emulators. The arguments are (out, src, pool, prepare, require), `prepare` before `require`, the reverse of `sceHttpUriParse`. A null source returns `ERROR_INVALID_URL`; without an output or a pool, a null `require` returns `ERROR_INVALID_VALUE`, otherwise the size is only reported. Only the scheme, hostname, path, username and password are copied, in that order, each with its terminator (a missing one becomes an empty string and takes one byte), and the port; the query, the fragment and the opaque flag stay 0. A pool that is too small clears the output and the pool and returns `ERROR_OUT_OF_MEMORY` (0x80431022, the console's out-of-size error) - [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgGetRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. +- [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. +- [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. +- [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature - [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature ### Functional From 62e4e204f1b35fdc4687be73210f30b6131385ee Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:52:04 +0300 Subject: [PATCH 0683/1360] fix(audioout): restore device id open argument slots --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 2 +- docs/dev/debt/libSceAudioOut.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index c6a9930bb6..41c2ad30a8 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -599,7 +599,7 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } -int APS5_VABI sceAudioOutDeviceIdOpen() { +int APS5_VABI sceAudioOutDeviceIdOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index, std::uint32_t length, std::int32_t frequency, std::uint32_t parameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 80f284769c..dd5aa7cfb3 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -21,7 +21,7 @@ - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [Legacy vibration ports](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp): PPSA99902's [public input wrapper](https://github.com/Nazky/UnleashedRecomp-Prospero/blob/2ce067a1d51bb58670c7ee4a3aed893c504da805/UnleashedRecomp/hid/driver/sdl_hid.cpp#L316) opens type 10 with 256 stereo S16 or F32 frames at 48000 Hz. Mono duplicates its channel as AudioOut2 does; wider vibration layouts throw. Native controller output uses AudioOut2's four-channel layout, or its six-channel PulseAudio mapping. Without a controller sound card the existing RMS motor transform is applied to segments of at most 256 frames, scheduled by the legacy queue's frame count and rate; drain and close remove only that port's segments. Concurrent sources combine by the maximum per motor, and the pad consumer uses this approximation only for controllers other than DualSense. This host timing and motor model is not a console measurement or waveform reproduction; legacy native output is unmeasured on hardware, and user routing retains the single-controller limitation. AudioOut2 retains its separate 80 ms peak hold. -- [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws +- [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the native wrapper consumes a 64-bit device ID, port type, sample count, frequency and format from argument slots 1, 2, 4, 5 and 6. Slot 3 is discarded, with the common open API index position preserved. Device enumeration and mapping to host outputs remain unimplemented; the throwing body is unchanged. - [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. The speaker info reports type 0 with available bits `0x3`, the front left speaker at +30 degrees and the front right one at -30, and -360 for the azimuth and elevation of every other slot. On that console the type followed the HDMI device type and the available bits its channels: type 0 with the device set to TV, 1 to AV amplifier and 2 to sound bar, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) names television, AV receiver and sound bar (3 is headphones there), and available bits `0x3` for 2 channels and `0x3F` for 5.1 (centre at 0, surround pair at +110 and -110, -360 for the LFE). The front pair and the empty slots read the same in every setting; 7.1 on the AV amplifier reported 2 channels on that console's HDMI chain. The host output is always reported as a stereo television here. The console also returns `0x80268006` before `sceAudioOut2Initialize`, `0x80268001` for flags above 1 and `0x8026800C` for a null pointer, which are not modelled. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated - [sceAudioOut2UserCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From 08904c9c47eef1a4a4fae7befe139a11ff1556bd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:53:54 +0300 Subject: [PATCH 0684/1360] fix(net): restore address route and internal statistics signatures --- core/libs/prx/libSceNet/Export.cpp | 10 ++++++---- docs/dev/debt/libSceNet.md | 10 ++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index bf95cdd988..85465086ae 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -96,6 +96,8 @@ struct NetEmulationParameter; struct NetResolverConnection; struct NetThreadParameter; +struct NetIpv4InterfaceAddress; +struct NetStatisticsInfoInternal; struct NetInitializeParameter; struct NetInterfaceInfo; struct NetInterfaceStatistics; @@ -2314,7 +2316,7 @@ int APS5_VABI sceNetGetRandom(std::uint32_t* output) { return 0; } -int APS5_VABI sceNetGetRouteInfo(void) { +int APS5_VABI sceNetGetRouteInfo(std::int32_t family, NetRouteInfo* routes, std::int32_t capacity, std::uint8_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2569,12 +2571,12 @@ int APS5_VABI sceNetConfigFlushRoute6(std::uint32_t interfaceId) { return 0; } -int APS5_VABI sceNetConfigAddIfaddr(void) { +int APS5_VABI sceNetConfigAddIfaddr(int interfaceId, const std::uint32_t* address, const std::uint32_t* broadcastAddress, const std::uint32_t* netmask) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNetConfigGetIfaddr(void) { +int APS5_VABI sceNetConfigGetIfaddr(std::int32_t interfaceId, NetIpv4InterfaceAddress* addresses, std::int32_t capacity) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2954,7 +2956,7 @@ int APS5_VABI sceNetConfigRoutingShowtCtlVar(const char* name) { return 0; } -int APS5_VABI sceNetGetStatisticsInfoInternal(void) { +int APS5_VABI sceNetGetStatisticsInfoInternal(NetStatisticsInfoInternal* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index eb17a8b8d3..def2d8deb6 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -7,19 +7,18 @@ ### Unknown function info +- [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - disassembly confirms the same interface/address/broadcast/netmask arguments as SetIfaddr, entering its shared handler with add mode selected. GetIfaddr copies up to two 16-byte interface-address records. GetRouteInfo uses a family, record buffer, signed capacity and byte flags; internal statistics output is 16 bytes. The address-record and internal-statistics fields remain incomplete. + - [Resolver-connect, thread creation, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, thread, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. -- [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. - [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -27,8 +26,7 @@ - [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetStatisticsInfoInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. ### Functional From 46912523df088dd64c63f570c04611595e94c79e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 22:58:34 +0300 Subject: [PATCH 0685/1360] fix(hmd2): define calibration and object selection memory layouts --- core/libs/prx/libSceHmd2/Export.cpp | 48 +++++++++++++++++++++++++---- docs/dev/debt/libSceHmd2.md | 2 +- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 68fa34443f..15afad777e 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -6,6 +6,7 @@ #include "prx/libSceUserService/UserService.hpp" #include "SceTypes.hpp" +#include <cstddef> #include <cstring> #include <mutex> #include <stdexcept> @@ -32,15 +33,38 @@ static_assert(sizeof(MirroringOption) == 32); struct GazeResultParameter; struct GazeResult; struct FoveatedRenderingResult; -struct GazeCalibrationData; -struct GazeUserCalibrationData; -struct GazeUserCalibrationStatus; +struct GazeCalibrationData { + void* data; + std::int32_t size; +}; +struct GazeUserCalibrationData { + const void* data; + std::int32_t size; +}; +struct GazeUserCalibrationStatus { + std::int32_t status; + std::int32_t eye; + float point[3]; +}; struct GazeRawData; struct GazeObjectSelectionParameter; struct GazeObjectSelectionResult; -struct GazeObjectSelectionInitializeParameter; -struct GazeObjectSelectionMemoryParameter; -struct GazeObjectSelectionMemoryInfo; +struct GazeObjectSelectionInitializeParameter { + std::uint32_t size; + std::uint32_t maxObjects; + void* memory; + std::uint32_t memorySize; + std::uint32_t reserved; +}; +struct GazeObjectSelectionMemoryParameter { + std::uint32_t size; + std::uint32_t maxObjects; +}; +struct GazeObjectSelectionMemoryInfo { + std::uint32_t size; + std::uint32_t memorySize; + std::uint64_t alignment; +}; struct CalibrationPoint { float x; @@ -49,6 +73,18 @@ struct CalibrationPoint { }; static_assert(sizeof(CalibrationPoint) == 12); +static_assert(sizeof(GazeCalibrationData) == 16); +static_assert(offsetof(GazeCalibrationData, size) == 8); +static_assert(sizeof(GazeUserCalibrationData) == 16); +static_assert(offsetof(GazeUserCalibrationData, size) == 8); +static_assert(sizeof(GazeUserCalibrationStatus) == 20); +static_assert(offsetof(GazeUserCalibrationStatus, point) == 8); +static_assert(sizeof(GazeObjectSelectionInitializeParameter) == 24); +static_assert(offsetof(GazeObjectSelectionInitializeParameter, memory) == 8); +static_assert(offsetof(GazeObjectSelectionInitializeParameter, memorySize) == 16); +static_assert(sizeof(GazeObjectSelectionMemoryParameter) == 8); +static_assert(sizeof(GazeObjectSelectionMemoryInfo) == 16); +static_assert(offsetof(GazeObjectSelectionMemoryInfo, alignment) == 8); } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 47cbda8590..41deb3e6e3 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -83,6 +83,6 @@ - [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known -- [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte parameter and a 56-byte or 24-byte output. Object selection uses a 120-byte parameter and 56-byte result, a 24-byte initialization parameter, and 8-byte/16-byte memory-query structures. These payload fields remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. +- [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte parameter and a 56-byte or 24-byte output. Object selection uses a 120-byte parameter and 56-byte result whose fields remain incomplete. Its initialization and memory-query structures are recovered, including the 32-bit object count and memory size, memory pointer and 64-bit alignment. Calibration data descriptors contain a pointer and signed 32-bit size; calibration status contains two 32-bit selectors and three floats. The underlying calibration blob formats remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. - [libSceHmd2 reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - frame identifiers and predicted display times are 64-bit. Event setters accept a kernel event queue and 32-bit event ID, consumed by sceKernelTriggerUserEvent. Mirroring uses two memory pointers and a separate 32-byte mode/reserved option. Status and current-output queries write 104 and 32 bytes; those field layouts and the reprojection initialization fields remain incomplete. Positional-reprojection mode is a byte with values 0 through 5, not a boolean. From e7a9514f8e6a31064187207d78bbe07508d6d440 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:00:23 +0300 Subject: [PATCH 0686/1360] fix(net): restore address callbacks and diagnostic start parameters --- core/libs/prx/libSceNet/Export.cpp | 22 ++++++++++++++++++---- docs/dev/debt/libSceNet.md | 6 ++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 85465086ae..f69fa0f01d 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -99,6 +99,20 @@ struct NetThreadParameter; struct NetIpv4InterfaceAddress; struct NetStatisticsInfoInternal; struct NetInitializeParameter; +struct NetDhcpStartParameter; +struct NetAddress6StartParameter; + +using NetAddressCallback = void (APS5_VABI *)(void* userArg, std::int32_t event, std::int32_t error); + +struct NetInfoDumpParameter { + std::uint32_t flags; + std::uint32_t intervalSeconds; + char path[64]; +}; + +static_assert(sizeof(NetInfoDumpParameter) == 72); +static_assert(offsetof(NetInfoDumpParameter, path) == 8); + struct NetInterfaceInfo; struct NetInterfaceStatistics; @@ -2661,7 +2675,7 @@ int APS5_VABI sceNetDhcpGetInfoEx(std::int32_t interfaceId, std::int32_t type, N return 0; } -int APS5_VABI sceNetDhcpStart(void) { +int APS5_VABI sceNetDhcpStart(std::uint32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, NetDhcpStartParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2721,7 +2735,7 @@ int APS5_VABI sceNetConfigAddRoute6(const NetIpv6Address* destination, const Net return 0; } -int APS5_VABI sceNetAddrConfig6Start(void) { +int APS5_VABI sceNetAddrConfig6Start(std::uint32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, const NetAddress6StartParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2736,7 +2750,7 @@ int APS5_VABI sceNetAddrConfig6GetInfo(std::int32_t interfaceId, NetAddress6Info return 0; } -int APS5_VABI sceNetInfoDumpStart(void) { +int APS5_VABI sceNetInfoDumpStart(std::int32_t threadParameter1, std::int32_t threadParameter2, const NetInfoDumpParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2756,7 +2770,7 @@ int APS5_VABI sceNetDhcpdStop(int interfaceId) { return 0; } -int APS5_VABI sceNetDuplicateIpStart(void) { +int APS5_VABI sceNetDuplicateIpStart(std::int32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index def2d8deb6..3abcb2a50e 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -19,11 +19,7 @@ - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. -- [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. @@ -46,3 +42,5 @@ - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. - [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ends an IPv4 address at the first space, tab, LF, VT, FF or CR and ignores the rest (`"1.2.3.4 x"` converts), leaves the destination and `sceNetErrnoLoc` unchanged when a conversion fails, as PS4 firmware 10.71 does; not yet confirmed on PS5. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and IPv6 text with whitespace fail. + +- [sceNetDhcpStart, sceNetAddrConfig6Start, sceNetDuplicateIpStart and sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) - argument widths and callback ABI are recovered. Address callbacks receive user data, a 32-bit event and a 32-bit error. DHCP configuration is mutable; the 512-byte IPv6 configuration is read-only. Their complete field layouts remain unknown. The two 32-bit thread parameters are stored but not consumed by this firmware thread wrapper, so their original names and units are unconfirmed. Information-dump options contain flags, an interval in seconds and a 64-byte path. All four exports remain unimplemented. From ee807399c3ffc329ce903cdabbe3fd62107a4d85 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:02:49 +0300 Subject: [PATCH 0687/1360] fix(mbus): restore by-value device service condition abi --- core/libs/prx/libSceMbus/PendingExports.cpp | 11 +++++++++-- docs/dev/debt/libSceMbus.md | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index d0596729f0..593546e9b6 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/General.hpp" +#include <cstddef> + struct MbusDeviceDescription; struct MbusDeviceInfo; struct MbusSimulatedHandleStatus; @@ -7,7 +9,12 @@ struct MbusSparkState; struct MbusDriverCallbacks; struct MbusUsbPowerEntry; struct MbusEvent; -struct MbusDeviceCondition; +struct alignas(8) MbusDeviceCondition { + std::byte data[256]; +}; + +static_assert(sizeof(MbusDeviceCondition) == 256); +struct MbusDeviceServiceInfo; struct MbusEventCallbacks { void (APS5_VABI *onOverflow)(std::int32_t eventHandle, void* userData); void (APS5_VABI *onEvent)(std::int32_t eventHandle, void* userData, const MbusEvent* event); @@ -373,7 +380,7 @@ int APS5_VABI sceMbusGetDeviceInfoByBusId_(std::int32_t busId, MbusDeviceInfo* i return 0; } -int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService() { +int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService(std::int32_t flags, MbusDeviceServiceInfo* infos, std::int32_t capacity, std::int32_t* first, std::int32_t* second, MbusDeviceCondition condition) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index a892a6c785..b1bb624f14 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,7 +8,7 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): GetDeviceInfoByConditionForDeviceService and SetDeviceFunctionState still require by-value aggregate layouts. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. +- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): SetDeviceFunctionState still requires its by-value aggregate layout. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. @@ -23,3 +23,5 @@ - [sceMbus settings](../../../core/libs/prx/libSceMbus/PendingExports.cpp): USB power-list entries occupy 12 bytes, with an 8-bit count limited to 100; their fields remain unresolved. Several routing, scratch-data and proxy scalar meanings still require the service handler. Start/StopAudioOutAuxBroadcast and StopAudioOutSharePlay have confirmed zero-argument ABIs. - [sceMbusGetDeviceInfoByCondition_](../../../core/libs/prx/libSceMbus/PendingExports.cpp): its first parameter points to a 256-byte query condition; the second is a 32-bit scalar. Two optional 32-bit outputs and the sized 256-byte record array are confirmed, while condition fields and output meanings remain unresolved. + +- [sceMbusGetDeviceInfoByConditionForDeviceService](../../../core/libs/prx/libSceMbus/PendingExports.cpp): assembly and its DeviceService caller confirm five register arguments followed by a 256-byte condition passed by value on the stack. The condition keeps opaque storage because its service-side fields remain unresolved; byte 228 contains the 16-bit device class. Output records occupy 112 bytes. The two optional outputs are 32-bit values whose meanings remain unresolved. From 7e6ae8f1c9cba4283954f6d8f3f799d9081df1e0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:09:16 +0300 Subject: [PATCH 0688/1360] fix(ime): restore current text notification argument slots --- core/libs/prx/libSceIme/Export.cpp | 4 ++-- docs/dev/debt/libSceIme.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 29d014c627..a67cd80b39 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -744,12 +744,12 @@ int APS5_VABI sceImeVshInformConfirmdString(ImeVshContext* context, const char16 return 0; } -int APS5_VABI sceImeVshNotifyCurrentTextInfo(void) { +int APS5_VABI sceImeVshNotifyCurrentTextInfo(ImeVshContext* context, const char16_t* text, std::int32_t caretIndex, std::int32_t preeditFirst, std::int32_t preeditSecond, std::int32_t selectionFirst, std::int32_t selectionSecond) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(void) { +int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(ImeVshContext* context, const char16_t* text, std::int32_t caretIndex, std::int32_t preeditFirst, std::int32_t preeditSecond, std::int32_t selectionFirst, std::int32_t selectionSecond, std::int32_t option) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index f4689b4ab8..d7134b33e2 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -12,8 +12,6 @@ - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn - [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshNotifyCurrentTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature @@ -25,3 +23,5 @@ - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard - [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5; a `sceImeSetText` or `sceImeSetCaret` that lands while the filter runs bumps a revision beside the session id, and the filtered commit is dropped when either changed, as KytyPS5 drops the commit when its revision changed, so the title's own text or caret setting wins, the edit is discarded and sends no event. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called + +- [sceImeVshNotifyCurrentTextInfo and sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp): assembly confirms seven and eight integer/pointer arguments, including the stack arguments, with no incoming SIMD parameter. Both take a context, UTF-16 string, signed 32-bit caret index and two pairs of 32-bit preedit/selection values. Pair endpoint-versus-length semantics and the reconversion option remain unresolved. Protected bodies are unchanged. From 241fa440942fcd872c24865d6d3dae3e4e6b4001 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:12:45 +0300 Subject: [PATCH 0689/1360] fix(voice): restore resource layout and port validation --- core/libs/SceTypes.hpp | 11 +-- core/libs/prx/libSceVoice/Export.cpp | 109 +++++++++++++++++-------- core/libs/tests/GuestVoice.cpp | 84 ++++++++++++------- core/libs/tests/GuestVoicePortAttr.cpp | 54 ++++++------ docs/dev/debt/libSceVoice.md | 10 +-- 5 files changed, 172 insertions(+), 96 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 978123ba23..390c010495 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -963,13 +963,14 @@ struct VoicePortInfo { }; struct VoiceResourceInfo { - std::uint16_t max_voice_input_ports; - std::uint16_t max_voice_output_ports; - std::uint16_t max_device_input_ports; - std::uint16_t max_device_output_ports; - std::uint16_t max_ports; + std::uint16_t maxVoiceInputPorts; + std::uint16_t maxVoiceOutputPorts; + std::uint16_t maxDeviceInputPorts; + std::uint16_t maxDeviceOutputPorts; }; +static_assert(sizeof(VoiceResourceInfo) == 8); + struct VoiceStartParam { void* container; std::uint32_t mem_size; diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index b2ae0e88ac..b89fea488a 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -23,19 +23,24 @@ constexpr std::uint32_t VoiceFrameMs = 20; constexpr std::int32_t PortAttrVoiceActivity = 1001; constexpr int VoiceErrorNotInitialized = static_cast<int>(0x804E0801u); constexpr int VoiceErrorAlreadyInitialized = static_cast<int>(0x804E0802u); +constexpr auto NotInitialized = static_cast<std::int32_t>(0x804e0801u); +constexpr auto InvalidPort = static_cast<std::int32_t>(0x804e0804u); +constexpr auto InvalidArgument = static_cast<std::int32_t>(0x804e0805u); +constexpr auto PortNotFound = static_cast<std::int32_t>(0x804e0807u); struct Port { std::int32_t type; float volume; std::uint32_t bitrate; bool muted = false; + bool paused = false; }; struct Voice { std::mutex mutex; bool initialized = false; bool started = false; - bool chatEnabled = false; + std::int32_t appType = 0; std::uint32_t nextId = 0; std::map<std::uint32_t, Port> ports; std::set<std::pair<std::uint32_t, std::uint32_t>> connections; @@ -69,6 +74,7 @@ int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { auto& voice = State(); std::lock_guard lock(voice.mutex); if (voice.initialized) return VoiceErrorAlreadyInitialized; + voice.appType = param->app_type; voice.initialized = true; return 0; } @@ -102,7 +108,7 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { voice.ports.clear(); voice.connections.clear(); voice.started = false; - voice.chatEnabled = false; + voice.appType = 0; voice.initialized = false; return 0; } @@ -185,16 +191,36 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { return 0; } -int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { +int APS5_VABI sceVoiceGetPortAttr(std::uint32_t portId, std::int32_t attr, void* value, std::int32_t size) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); - if (attr == PortAttrVoiceActivity) { - if (value == nullptr || size != 1) APS5_INVALID_ARG_EX; - std::memset(value, 0, 1); - return 0; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (value == nullptr) return InvalidArgument; + std::int32_t expectedSize; + switch (attr) { + case 1000: + case 1001: + case 2000: + case 2002: + expectedSize = 1; + break; + case 1002: + case 2001: + expectedSize = 4; + break; + case 1003: + expectedSize = 2; + break; + default: + return InvalidArgument; } - Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); + if (size != expectedSize) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return InvalidPort; + const auto requiredType = attr == 1003 ? PortOutDevice : PortInDevice; + if (found->second.type != requiredType) return InvalidArgument; + Fail(__func__, "device attribute backend is not implemented"); } int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { @@ -231,53 +257,72 @@ int APS5_VABI sceVoiceGetMuteFlag(uint32_t port_id, bool* muted) { } int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo* info) { - if (info == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); - info->max_voice_input_ports = 7; - info->max_voice_output_ports = 7; - info->max_device_input_ports = 1; - info->max_device_output_ports = 1; - info->max_ports = 64; + if (!voice.initialized) return NotInitialized; + if (info == nullptr) return InvalidArgument; + switch (voice.appType) { + case 4: + *info = {0, 0, 4, 5}; + break; + case 0x08000000: + case 0x10000000: + *info = {15, 4, 4, 4}; + break; + case 0x20000000: + *info = {7, 4, 4, 4}; + break; + default: + Fail(__func__, "unsupported initialization mode"); + } return 0; } int APS5_VABI sceVoiceEnableChat(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); - voice.chatEnabled = true; + if (!voice.initialized) return NotInitialized; + Fail(__func__, "audio chat backend is not implemented"); +} + +int APS5_VABI sceVoiceResetPort(std::uint32_t portId) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + if (voice.started && (found->second.type == PortInDevice || found->second.type == PortOutDevice)) Fail(__func__, "device reset backend is not implemented"); return 0; } -int APS5_VABI sceVoiceResetPort(uint32_t port_id) { +int APS5_VABI sceVoicePausePort(std::uint32_t portId) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.paused = true; return 0; } -int APS5_VABI sceVoicePausePort(uint32_t port_id) { +int APS5_VABI sceVoiceResumePort(std::uint32_t portId) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); - return 0; -} - -int APS5_VABI sceVoiceResumePort(uint32_t port_id) { - auto& voice = State(); - std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.paused = false; return 0; } int APS5_VABI sceVoiceDisableChat(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); - voice.chatEnabled = false; - return 0; + if (!voice.initialized) return NotInitialized; + Fail(__func__, "audio chat backend is not implemented"); } int APS5_VABI sceVoiceInitHQ(void) { diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index a8671553de..ac56c3d949 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -3,6 +3,7 @@ #include <cstdint> #include <cstdlib> #include <cstring> +#include <initializer_list> #include <stdexcept> extern "C" { @@ -78,27 +79,35 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { int main() { TestNotInitialized(); - RequireThrows([] { sceVoiceResetPort(0); }); - RequireThrows([] { sceVoicePausePort(0); }); - RequireThrows([] { sceVoiceResumePort(0); }); - RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); + constexpr auto notInitialized = static_cast<std::int32_t>(0x804e0801u); + constexpr auto invalidPort = static_cast<std::int32_t>(0x804e0804u); + constexpr auto invalidArgument = static_cast<std::int32_t>(0x804e0805u); + constexpr auto portNotFound = static_cast<std::int32_t>(0x804e0807u); + Require(sceVoiceResetPort(0) == notInitialized); + Require(sceVoicePausePort(0) == notInitialized); + Require(sceVoiceResumePort(0) == notInitialized); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + Require(sceVoiceEnableChat() == notInitialized); + Require(sceVoiceDisableChat() == notInitialized); + VoiceInitParam init{}; + init.app_type = 0x10000000; + VoiceStartParam start{}; + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceInit(&init, 100) == AlreadyInitialized); + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceGetResourceInfo(nullptr) == invalidArgument); + struct ResourceOutput { + VoiceResourceInfo info; + std::uint32_t guard; + } resourceOutput{{}, 0xa5a5a5a5u}; + Require(sceVoiceGetResourceInfo(&resourceOutput.info) == 0); + Require(resourceOutput.info.maxVoiceInputPorts == 15); + Require(resourceOutput.info.maxVoiceOutputPorts == 4); + Require(resourceOutput.info.maxDeviceInputPorts == 4); + Require(resourceOutput.info.maxDeviceOutputPorts == 4); + Require(resourceOutput.guard == 0xa5a5a5a5u); RequireThrows([] { sceVoiceEnableChat(); }); RequireThrows([] { sceVoiceDisableChat(); }); - VoiceInitParam init{}; - VoiceStartParam start{}; - Require(sceVoiceInit(&init, 0) == 0); - Require(sceVoiceInit(&init, 0) == AlreadyInitialized); - Require(sceVoiceStart(&start) == 0); - RequireThrows([] { sceVoiceGetResourceInfo(nullptr); }); - VoiceResourceInfo resourceInfo{}; - Require(sceVoiceGetResourceInfo(&resourceInfo) == 0); - Require(resourceInfo.max_voice_input_ports == 7); - Require(resourceInfo.max_voice_output_ports == 7); - Require(resourceInfo.max_device_input_ports == 1); - Require(resourceInfo.max_device_output_ports == 1); - Require(resourceInfo.max_ports == 64); - Require(sceVoiceEnableChat() == 0); - Require(sceVoiceDisableChat() == 0); const auto input = CreatePort(2, 0.5f); const auto output = CreatePort(3, 0.25f); Require(sceVoiceConnectIPortToOPort(input, output) == 0); @@ -108,14 +117,17 @@ int main() { Require(sceVoiceResetPort(input) == 0); Require(sceVoiceResetPort(output) == 0); - RequireThrows([&] { sceVoiceResetPort(output + 1); }); + Require(sceVoiceResetPort(output + 1) == portNotFound); + Require(sceVoiceResetPort(128) == invalidPort); + Require(sceVoicePausePort(input) == 0); Require(sceVoicePausePort(input) == 0); Require(sceVoicePausePort(output) == 0); VoicePortInfo paused{}; Require(sceVoiceGetPortInfo(input, &paused) == 0); Require(paused.port_type == before.port_type && paused.state == before.state && paused.frame_size == before.frame_size); Require(sceVoiceResumePort(input) == 0); + Require(sceVoiceResumePort(input) == 0); Require(sceVoiceResumePort(output) == 0); Require(sceVoicePausePortAll() == 0); Require(sceVoicePausePortAll() == 0); @@ -123,8 +135,10 @@ int main() { Require(paused.port_type == before.port_type && paused.state == before.state && paused.frame_size == before.frame_size); Require(sceVoiceResumePortAll() == 0); Require(sceVoiceResumePortAll() == 0); - RequireThrows([&] { sceVoicePausePort(output + 1); }); - RequireThrows([&] { sceVoiceResumePort(output + 1); }); + Require(sceVoicePausePort(output + 1) == portNotFound); + Require(sceVoicePausePort(128) == invalidPort); + Require(sceVoiceResumePort(output + 1) == portNotFound); + Require(sceVoiceResumePort(128) == invalidPort); VoicePortInfo after{}; Require(sceVoiceGetPortInfo(input, &after) == 0); @@ -139,14 +153,26 @@ int main() { Require(sceVoiceDisconnectIPortFromOPort(input, output) == 0); Require(sceVoiceDeletePort(input) == 0); - RequireThrows([&] { sceVoiceResetPort(input); }); - RequireThrows([&] { sceVoicePausePort(input); }); + Require(sceVoiceResetPort(input) == portNotFound); + Require(sceVoicePausePort(input) == portNotFound); Require(sceVoiceResetPort(output) == 0); Require(sceVoiceEnd_nid_postfix() == 0); TestNotInitialized(); - RequireThrows([&] { sceVoiceResetPort(output); }); - RequireThrows([&] { sceVoiceResumePort(output); }); - RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); - RequireThrows([] { sceVoiceEnableChat(); }); - RequireThrows([] { sceVoiceDisableChat(); }); + Require(sceVoiceResetPort(output) == notInitialized); + Require(sceVoiceResumePort(output) == notInitialized); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + Require(sceVoiceEnableChat() == notInitialized); + Require(sceVoiceDisableChat() == notInitialized); + for (const auto mode : {4, 0x08000000, 0x10000000, 0x20000000}) { + init.app_type = mode; + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceGetResourceInfo(&resourceOutput.info) == 0); + Require(resourceOutput.info.maxVoiceInputPorts == (mode == 4 ? 0 : mode == 0x20000000 ? 7 : 15)); + Require(resourceOutput.info.maxVoiceOutputPorts == (mode == 4 ? 0 : 4)); + Require(resourceOutput.info.maxDeviceInputPorts == 4); + Require(resourceOutput.info.maxDeviceOutputPorts == (mode == 4 ? 5 : 4)); + Require(resourceOutput.guard == 0xa5a5a5a5u); + Require(sceVoiceEnd_nid_postfix() == 0); + } + } diff --git a/core/libs/tests/GuestVoicePortAttr.cpp b/core/libs/tests/GuestVoicePortAttr.cpp index 3264ec11c4..082c828ec5 100644 --- a/core/libs/tests/GuestVoicePortAttr.cpp +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -18,8 +18,8 @@ int APS5_VABI sceVoiceGetPortAttr(uint32_t, int32_t, void*, int32_t); static void Require(bool value) { if (!value) std::abort(); } -template <typename Call> -static bool Throws(Call call, std::string_view message) { +template <typename TCall> +static bool Throws(TCall call, std::string_view message) { try { call(); } catch (const std::logic_error& error) { @@ -29,31 +29,37 @@ static bool Throws(Call call, std::string_view message) { } int main() { - constexpr std::int32_t voiceActivity = 1001; - - Require(Throws([] { sceVoiceGetPortAttr(0, voiceActivity, nullptr, 1); }, "library not initialized")); - + constexpr auto notInitialized = static_cast<std::int32_t>(0x804e0801u); + constexpr auto invalidPort = static_cast<std::int32_t>(0x804e0804u); + constexpr auto invalidArgument = static_cast<std::int32_t>(0x804e0805u); + Require(sceVoiceGetPortAttr(128, 1001, nullptr, 1) == notInitialized); VoiceInitParam init{}; - Require(sceVoiceInit(&init, 0) == 0); - + init.app_type = 0x10000000; + Require(sceVoiceInit(&init, 100) == 0); VoicePortParam param{}; - param.port_type = 2; param.volume = 1.0f; - param.voice.bitrate = 16000; - std::uint32_t port = 0; - Require(sceVoiceCreatePort(&port, ¶m) == 0); - - std::array<unsigned char, 3> buffer{0xa5, 0xa5, 0xa5}; - Require(sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1) == 0); - Require(buffer[0] == 0xa5 && buffer[1] == 0 && buffer[2] == 0xa5); - - Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, 1, &buffer[1], 1); }, "attribute 1 not implemented")); - - Require(Throws([&buffer] { sceVoiceGetPortAttr(0x1234, voiceActivity, &buffer[1], 1); }, "unknown port")); - - Require(sceVoiceDeletePort(port) == 0); - Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1); }, "unknown port")); - + std::array<unsigned char, 6> buffer{0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}; + const auto original = buffer; + Require(sceVoiceGetPortAttr(128, 1001, nullptr, 1) == invalidPort); + Require(sceVoiceGetPortAttr(0, 1001, nullptr, 1) == invalidArgument); + for (std::int32_t type = 0; type <= 5; ++type) { + param.port_type = type; + std::uint32_t port = 0; + Require(sceVoiceCreatePort(&port, ¶m) == 0); + for (const auto attr : {1000, 1001, 1002, 1003, 2000, 2001, 2002}) { + const auto size = attr == 1003 ? 2 : attr == 1002 || attr == 2001 ? 4 : 1; + Require(sceVoiceGetPortAttr(port, attr, buffer.data() + 1, size + 1) == invalidArgument); + if (type == (attr == 1003 ? 5 : 0)) { + Require(Throws([&] { sceVoiceGetPortAttr(port, attr, buffer.data() + 1, size); }, "device attribute backend")); + } else { + Require(sceVoiceGetPortAttr(port, attr, buffer.data() + 1, size) == invalidArgument); + } + Require(buffer == original); + } + Require(sceVoiceGetPortAttr(port, 1, buffer.data() + 1, 1) == invalidArgument); + Require(sceVoiceDeletePort(port) == 0); + Require(sceVoiceGetPortAttr(port, 1001, buffer.data() + 1, 1) == invalidPort); + } Require(sceVoiceEnd_nid_postfix() == 0); return 0; } diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index c6fee962c1..df5a8e4c45 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -1,16 +1,13 @@ # libSceVoice technical debt -### Silent stubs +### Functional -- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - attribute 1001 writes one zero byte and returns 0 for an existing port; the backend processes no voice audio. PPSA34501 requests size 1 and reads one byte on success, and this fallback avoids its startup exception. The name follows attribute 1001 (`CELLVOICE_ATTR_VAD`) (Voice Activity Detection) in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellVoice.h), not confirmed for the PS5. The correct result, eligible port types and behaviour are unknown. +- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization still lacks native mode/version validation, allocator callbacks and backend creation; port creation does not enforce native resource limits. +- [sceVoiceEnableChat and sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp): zero-argument signatures and the not-initialized error are confirmed. Initialized calls throw because the required audio chat backend is not implemented; they no longer report success for a local flag change. ### Unknown function info -- [sceVoiceResetPort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(uint32_t portId)` signature is the PS Vita one in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L503-L514) and the PS3 `cellVoiceResetPort` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L493-L510); that the PS5 library takes the same argument is assumed. A reset clears a port's buffered data, and ports here buffer nothing (`sceVoiceReadFromOPort` returns 0 bytes and `sceVoiceWriteToIPort` throws), so it only checks that the library is initialized and the port exists, as RPCS3 does, and keeps the port's connections, volume and mute flag. An uninitialized library or an unknown port throws like the other port calls; the console's error codes are not measured. PPSA23566 imports it -- [sceVoiceGetResourceInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(VoiceResourceInfo* info)` signature and port limits (7 voice inputs, 7 voice outputs, 1 device input, 1 device output, 64 total ports) are the PS Vita ones in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L150-L157); that the PS5 library uses the same layout and limits is assumed. An uninitialized library or a null output pointer throws like the other Voice calls; console limits and error codes are not measured. PPSA23566 imports it -- [sceVoiceEnableChat, sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) take no arguments and return 0, following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) and the `sceAudioOut2EnableChat` model. The host mixes no chat audio, so the calls only record whether chat audio is enabled and fail when the library is not initialized. Console error codes and chat session requirements are unverified. PPSA23566 imports them - [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 -- [sceVoicePausePort, sceVoiceResumePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the `(uint32_t portId)` signature follows [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L516-L538) and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L443-L529), and the console's error codes and its result for pausing a paused port or resuming a running one are unknown (0 here, as in RPCS3). Both only check that the library is initialized and the port exists: VitaSDK documents that a paused port keeps its data and reported state, and nothing here processes port data. PPSA24191 imports them - [sceVoicePausePortAll, sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: both return 0x804E0801 while the library is not initialized and 0 otherwise, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/voice/voice.cpp#L356-L437); prosper reads the same code for `sceVoiceStart`, and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L462-L543) make the same check. Like `sceVoicePausePort` they change nothing that is reported: [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L540-L557) documents that pausing every port keeps the buffered data and the reported state, and nothing here processes port data - [sceVoiceInitHQ](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature @@ -22,3 +19,4 @@ - [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. From 56a1c1beda4749d064a1f66e1e3d2f80c17ead60 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:12:46 +0300 Subject: [PATCH 0690/1360] fix(http2): correct cookie pool ownership and websocket ping abi --- core/libs/prx/libSceHttp2/Export.cpp | 16 +++++++++++----- core/libs/tests/GuestHttp2.cpp | 22 ++++++++++++++-------- docs/dev/debt/libSceHttp2.md | 8 ++++---- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index bc1682e4fe..d97f8d6c6d 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -7,6 +7,8 @@ #include <atomic> #include <deque> #include <mutex> +#include <memory> +#include <limits> #include <stdexcept> #include <unordered_map> #include <vector> @@ -23,6 +25,7 @@ struct Http2Handle { Http2Kind kind; int owner; std::size_t poolSize; + std::shared_ptr<std::byte[]> cookieMemory; }; static std::atomic<int> g_nextHandle{1}; @@ -353,11 +356,14 @@ int APS5_VABI sceHttp2CookieFlush(int id) { return 0; } -int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { +int APS5_VABI sceHttp2CreateCookieBox(std::size_t poolSize) { + if (poolSize < 0x10000) return static_cast<int>(0x817b121du); + if (poolSize > std::numeric_limits<std::size_t>::max() - 0x3fff) throw std::overflow_error("sceHttp2CreateCookieBox: pool size overflow"); + const auto allocationSize = (poolSize + 0x3fff) & ~std::size_t{0x3fff}; + auto memory = std::make_shared<std::byte[]>(allocationSize); std::lock_guard lock(g_handlesMutex); - if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - g_handles[id] = {Http2Kind::CookieBox, lib_http2_ctx_id, 0}; + g_handles[id] = {Http2Kind::CookieBox, 0, poolSize, std::move(memory)}; return id; } @@ -406,12 +412,12 @@ int APS5_VABI sceHttp2WebSocketSendDataMessageAsync(std::int32_t requestId, cons return 0; } -int APS5_VABI sceHttp2WebSocketSetPingInterval() { +int APS5_VABI sceHttp2WebSocketSetPingInterval(std::int32_t requestId, std::uint32_t intervalMicroseconds) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketSetPingTimeout() { +int APS5_VABI sceHttp2WebSocketSetPingTimeout(std::int32_t requestId, std::uint32_t timeoutMicroseconds) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index d29c0021e4..102369b0c4 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -7,7 +7,7 @@ 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 sceHttp2CreateCookieBox(std::size_t); int APS5_VABI sceHttp2DeleteCookieBox(int); int APS5_VABI sceHttp2SetCookieBox(int, int); int APS5_VABI sceHttp2CookieFlush(int); @@ -40,7 +40,7 @@ struct Http2MemoryPoolStats { extern "C" { int APS5_VABI sceHttp2GetMemoryPoolStats(int, Http2MemoryPoolStats*); int APS5_VABI sceHttp2SetResolveRetry(int, std::int32_t); -int APS5_VABI sceHttp2WebSocketSetPingInterval(); +int APS5_VABI sceHttp2WebSocketSetPingInterval(std::int32_t, std::uint32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -64,15 +64,20 @@ static bool Throws(TCall call) { } int main() { + Require(sceHttp2CreateCookieBox(0) == static_cast<int>(0x817b121du)); + Require(sceHttp2CreateCookieBox(0xffff) == static_cast<int>(0x817b121du)); + const int independentBox = sceHttp2CreateCookieBox(0x10000); + Require(independentBox > 0); + Require(sceHttp2DeleteCookieBox(independentBox) == 0); const int context = sceHttp2Init(1, 1, 0x10000, 4); Require(context > 0); Http2MemoryPoolStats stats{1, 1, 1, 1}; Require(sceHttp2GetMemoryPoolStats(context, &stats) == 0); Require(stats.pool_size == 0x10000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); - const int box = sceHttp2CreateCookieBox(context); - const int other = sceHttp2CreateCookieBox(context); + const int box = sceHttp2CreateCookieBox(0x10000); + const int other = sceHttp2CreateCookieBox(0x10000); Require(box > 0 && other > 0 && box != other && box != context); - const int discarded = sceHttp2CreateCookieBox(context); + const int discarded = sceHttp2CreateCookieBox(0x10000); Require(discarded > 0 && discarded != box && discarded != other); Require(sceHttp2DeleteCookieBox(discarded) == 0); Require(sceHttp2DeleteCookieBox(discarded) == InvalidId); @@ -86,7 +91,7 @@ int main() { Require(request > 0); Require(sceHttp2SetCookieBox(request, other) == 0); Require(sceHttp2SetRequestNoContentLength(request) == 0); - Require(Throws([] { sceHttp2WebSocketSetPingInterval(); })); + Require(Throws([] { sceHttp2WebSocketSetPingInterval(0, 1000000); })); Require(sceHttp2CookieFlush(context) == 0); Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast<int>(0x80436063)); @@ -116,7 +121,7 @@ int main() { const int doomedContext = sceHttp2Init(1, 1, 0x2000, 4); Require(doomedContext > 0); - const int doomedContextBox = sceHttp2CreateCookieBox(doomedContext); + const int doomedContextBox = sceHttp2CreateCookieBox(0x10001); const int doomedContextTemplate = sceHttp2CreateTemplate(doomedContext, "agent", 2, 0); Require(doomedContextBox > 0 && doomedContextTemplate > 0); const int doomedContextRequest = sceHttp2CreateRequestWithURL(doomedContextTemplate, "GET", "https://example.com/descendant", 0); @@ -124,7 +129,8 @@ int main() { RequireSslSetters(doomedContext, 0); Require(sceHttp2Term(doomedContext) == 0); RequireSslSetters(doomedContext, InvalidId); - RequireSslSetters(doomedContextBox, InvalidId); + RequireSslSetters(doomedContextBox, 0); + Require(sceHttp2DeleteCookieBox(doomedContextBox) == 0); Require(sceHttp2DeleteCookieBox(doomedContextBox) == InvalidId); RequireSslSetters(doomedContextTemplate, InvalidId); RequireSslSetters(doomedContextRequest, InvalidId); diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index f3db1ce9df..74ba9af4d0 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -2,18 +2,18 @@ ### Silent stubs -- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL`, `sceHttp2CreateCookieBox` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) +- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) ### Unknown function info -- [sceHttp2WebSocketSetPingInterval](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. -- [sceHttp2WebSocketSetPingTimeout](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `-MU9L6S3Fj4`) - imported by PPSA27836 01.000.001, whose loader stopped on the missing export; not called during its start-up. The parameters and the console errors are unknown. Every call throws. - [sceHttp2DeleteCookieBox](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console - [sceHttp2SetResolveRetry](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - signature `(int id, int32_t retry)` from libSceHttp's `sceHttpSetResolveRetry`; the value is accepted and ignored like the other Http2 settings, since no request reaches the network -- [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 +- [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` takes a machine-sized pool size, requires at least 64 KiB and allocates independently of HTTP contexts. The host reserves the pool with 16 KiB rounding but does not reproduce native cookie-heap bookkeeping, identifier ranges or retained request references. Its delete signature and invalid-ID error are confirmed. `sceHttp2SetRequestNoContentLength` takes only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard - [libSceHttp2 pending exports](../../../core/libs/prx/libSceHttp2/Export.cpp) - 18 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttp2.c). ### Functional - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. PPSA15508 passes null kqueue and extra options at original VA 0xb63931, then calls `sceHttp2WaitAsync` with a 24-byte result and a 2,000,000 timeout, and checks the result at offset 8. A null kqueue option therefore retains the same offline completion for direct waiting without an event notification. The timeout is not used because the offline result is already complete. This path follows the original title call and caller control flow, not a console measurement. A supplied kqueue with a missing user event, an extra option, and a wait with no completed operation still throw + +- [WebSocket ping settings](../../../core/libs/prx/libSceHttp2/Export.cpp): both setters take a 32-bit request ID and a 32-bit interval/timeout in microseconds, confirmed by the timer consumer and its clock conversion. The protected implementations remain unchanged. From 582362546c315b03654e63e50aa2a61c2802dd39 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:13:50 +0300 Subject: [PATCH 0691/1360] fix(npauth): validate and retain request timeout settings --- core/libs/prx/libSceNpAuth/Export.cpp | 22 +++++++++++++++------- core/libs/tests/GuestNpAuth.cpp | 12 +++++++++++- docs/dev/debt/libSceNpAuth.md | 5 ++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 7ce7edb5ee..414e8de20b 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -1,4 +1,5 @@ #include <atomic> +#include <array> #include <cstddef> #include <cstdint> #include <mutex> @@ -22,6 +23,8 @@ struct RequestInfo { bool aborted = false; bool polled = false; int result = SCE_NP_ERROR_SIGNED_OUT; + std::int32_t resolveRetry = 0; + std::array<std::uint32_t, 4> timeouts{}; }; std::mutex g_requestMutex; @@ -124,14 +127,19 @@ int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { return NP_POLL_ASYNC_FINISHED; } -int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout) { - (void)resolve_retry; - (void)resolve_timeout; - (void)connect_timeout; - (void)send_timeout; - (void)receive_timeout; +int APS5_VABI sceNpAuthSetTimeout(std::int32_t requestId, std::int32_t resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connectTimeout, std::uint32_t sendTimeout, std::uint32_t receiveTimeout) { + if (resolveRetry < 0) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if ((static_cast<std::uint32_t>(resolveRetry) | resolveTimeout | connectTimeout | sendTimeout | receiveTimeout) == 0) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (resolveTimeout != 0 && resolveTimeout < 1000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (connectTimeout != 0 && connectTimeout < 10000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (sendTimeout != 0 && sendTimeout < 10000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (receiveTimeout != 0 && receiveTimeout < 10000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_requestMutex); - return g_requests.contains(req_id) ? 0 : ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + const auto found = g_requests.find(requestId); + if (found == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + found->second.resolveRetry = resolveRetry; + found->second.timeouts = {resolveTimeout, connectTimeout, sendTimeout, receiveTimeout}; + return 0; } int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 2ab80ba71c..7cabfdb6f0 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -50,7 +50,17 @@ int main() { int issuerId = 0; Require(sceNpAuthSetTimeout(syncReq, 2, 5000000, 10000000, 10000000, 10000000) == 0); - Require(sceNpAuthSetTimeout(asyncReq, 0, 0, 0, 0, 0) == 0); + Require(sceNpAuthSetTimeout(asyncReq, 0, 0, 0, 0, 0) == invalidArgument); + + Require(sceNpAuthSetTimeout(syncReq, -1, 1000000, 10000000, 10000000, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 999999, 10000000, 10000000, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 9999999, 10000000, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 10000000, 9999999, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 10000000, 10000000, 9999999) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 10000000, 10000000, 10000000) == 0); + Require(sceNpAuthSetTimeout(syncReq, 1, 0, 0, 0, 0) == 0); + Require(sceNpAuthSetTimeout(syncReq, 0, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu) == 0); + Require(sceNpAuthSetTimeout(-1, 0, 0, 0, 0, 0) == invalidArgument); int pollUnprocessed = 0; Require(sceNpAuthPollAsync(asyncReq, &pollUnprocessed) == invalidId); diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 2ad98e9bdd..9303b08f53 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -4,4 +4,7 @@ - [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters and returns `SCE_NP_ERROR_SIGNED_OUT` without reading them, as there is no PSN sign-in; the request parameter and code output are not known. Minecraft (PPSA17221) imports it, fails to load without it, and after its sign-in dialog treats the signed-out code as offline play. - [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified -- [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - imported by PPSA01317, which fails to load without it. The signature `(reqId, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout)` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4), which only stubs it. An unknown request ID returns `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` like the other request calls; otherwise it returns OK and keeps nothing, as no request reaches the network the timeouts would bound. Argument checks of the console are unverified + +### Functional + +- [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp): native validation and storage are implemented: retry must be nonnegative, the all-zero tuple is invalid, resolve timeout is zero or at least 1000000, and other timeouts are zero or at least 10000000. Invalid arguments precede request lookup. Stored settings currently have no transport to configure because authentication remains offline. From 17d93d1ff2961caf86c8edf1e7f7344120ea38c1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:20:05 +0300 Subject: [PATCH 0692/1360] fix(npauth): restore authorized app code parameters and validation --- core/libs/SceTypes.hpp | 15 ++++++ core/libs/prx/libSceNpAuth/CMakeLists.txt | 3 +- core/libs/prx/libSceNpAuth/Export.cpp | 11 ++++- core/libs/tests/GuestNpAuth.cpp | 4 +- .../tests/GuestNpAuthAuthorizedAppCode.cpp | 48 ++++++++++++++++--- docs/dev/debt/libSceNpAuth.md | 3 +- 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 390c010495..688c87737a 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -962,6 +962,21 @@ struct VoicePortInfo { std::uint16_t reserved; }; +struct NpAuthClientId; + +struct NpAuthAuthorizedAppCodeParameter { + std::uint64_t size; + std::int32_t userId; + const NpAuthClientId* clientId; + const char* scope; + std::uint32_t option; +}; + +static_assert(sizeof(NpAuthAuthorizedAppCodeParameter) == 40); +static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, clientId) == 16); +static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, scope) == 24); +static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, option) == 32); + struct VoiceResourceInfo { std::uint16_t maxVoiceInputPorts; std::uint16_t maxVoiceOutputPorts; diff --git a/core/libs/prx/libSceNpAuth/CMakeLists.txt b/core/libs/prx/libSceNpAuth/CMakeLists.txt index ce6ce6a2f8..67acfa7500 100644 --- a/core/libs/prx/libSceNpAuth/CMakeLists.txt +++ b/core/libs/prx/libSceNpAuth/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libSceNpAuth) if(BUILD_TESTING) add_test_executable(guest_np_auth_authorized_app_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppCode.cpp) target_include_directories(guest_np_auth_authorized_app_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_auth_authorized_app_code_tests PRIVATE libSceNpAuth) + target_link_libraries(guest_np_auth_authorized_app_code_tests PRIVATE libSceNpAuth libc) + configure_windows_unwind(guest_np_auth_authorized_app_code_tests) add_test(NAME guest_np_auth_authorized_app_code COMMAND guest_np_auth_authorized_app_code_tests) if(WIN32) set_tests_properties(guest_np_auth_authorized_app_code PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 414e8de20b..265c3fd4df 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -3,6 +3,7 @@ #include <cstddef> #include <cstdint> #include <mutex> +#include <stdexcept> #include <unordered_map> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -65,8 +66,14 @@ int APS5_VABI sceNpAuthDeleteRequest(int req_id) { return 0; } -int APS5_VABI sceNpAuthGetAuthorizedAppCode() { - return SCE_NP_ERROR_SIGNED_OUT; +int APS5_VABI sceNpAuthGetAuthorizedAppCode(std::int32_t requestId, const NpAuthAuthorizedAppCodeParameter* parameter, char* code, std::int32_t* issuerId, bool* authorizationRequired) { + (void)issuerId; + if (parameter == nullptr || code == nullptr || authorizationRequired == nullptr) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (parameter->size != sizeof(NpAuthAuthorizedAppCodeParameter)) return static_cast<int>(0x80550302u); + if (parameter->userId == -1 || parameter->clientId == nullptr || parameter->scope == nullptr || parameter->option > 1) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + if (!g_requests.contains(requestId)) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + throw std::logic_error("sceNpAuthGetAuthorizedAppCode: authorization backend is not implemented"); } int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 7cabfdb6f0..95b11eddc5 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -11,7 +11,7 @@ int APS5_VABI sceNpAuthDeleteRequest(int req_id); int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code); int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id); int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id); -int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(std::int32_t, const NpAuthAuthorizedAppCodeParameter*, char*, std::int32_t*, bool*); int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token); @@ -67,7 +67,7 @@ int main() { int waitUnprocessed = 0; Require(sceNpAuthWaitAsync(asyncReq, &waitUnprocessed) == invalidId); - Require(sceNpAuthGetAuthorizedAppCode() == signedOut); + Require(sceNpAuthGetAuthorizedAppCode(syncReq, nullptr, nullptr, nullptr, nullptr) == invalidArgument); Require(sceNpAuthGetIdToken(syncReq, nullptr, token) == invalidArgument); Require(sceNpAuthGetIdToken(syncReq, param, nullptr) == invalidArgument); diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp index f6dfce0de8..a5d7f6fe17 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp @@ -1,13 +1,49 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include <array> #include <cstdlib> +#include <stdexcept> extern "C" { -int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +int APS5_VABI sceNpAuthCreateRequest(); +int APS5_VABI sceNpAuthDeleteRequest(int); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(std::int32_t, const NpAuthAuthorizedAppCodeParameter*, char*, std::int32_t*, bool*); +} + +static void Require(bool condition) { + if (!condition) std::abort(); } int main() { - constexpr int kSignedOut = static_cast<int>(0x80550006); - if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); - if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); - return 0; + constexpr auto invalidArgument = static_cast<int>(0x80550301u); + constexpr auto invalidSize = static_cast<int>(0x80550302u); + constexpr auto requestNotFound = static_cast<int>(0x80550306u); + std::array<char, 136> client{}; + std::array<char, 128> code{}; + code.fill('x'); + const auto original = code; + bool authorizationRequired = true; + NpAuthAuthorizedAppCodeParameter parameter{40, 1, reinterpret_cast<const NpAuthClientId*>(client.data()), "", 0}; + Require(sceNpAuthGetAuthorizedAppCode(0, nullptr, code.data(), nullptr, &authorizationRequired) == invalidArgument); + parameter.size = 39; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == invalidSize); + parameter.size = 40; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, nullptr, nullptr, &authorizationRequired) == invalidArgument); + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, nullptr) == invalidArgument); + parameter.option = 2; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == invalidArgument); + parameter.option = 0; + parameter.userId = -1; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == invalidArgument); + parameter.userId = 1; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == requestNotFound); + const auto requestId = sceNpAuthCreateRequest(); + Require(requestId > 0); + bool threw = false; + try { + sceNpAuthGetAuthorizedAppCode(requestId, ¶meter, code.data(), nullptr, &authorizationRequired); + } catch (const std::logic_error&) { + threw = true; + } + Require(threw && code == original && authorizationRequired); + Require(sceNpAuthDeleteRequest(requestId) == 0); } diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 9303b08f53..9f41ec750a 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,9 +2,10 @@ ### Unknown function info -- [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters and returns `SCE_NP_ERROR_SIGNED_OUT` without reading them, as there is no PSN sign-in; the request parameter and code output are not known. Minecraft (PPSA17221) imports it, fails to load without it, and after its sign-in dialog treats the signed-out code as offline play. - [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified ### Functional - [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp): native validation and storage are implemented: retry must be nonnegative, the all-zero tuple is invalid, resolve timeout is zero or at least 1000000, and other timeouts are zero or at least 10000000. Invalid arguments precede request lookup. Stored settings currently have no transport to configure because authentication remains offline. + +- [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp): the five-argument ABI and 40-byte request parameter are recovered. Pointer, size, user, option and request-ID validation replaces the unconditional signed-out result. Valid calls throw because authorization and asynchronous result delivery are not implemented. Client-ID storage is copied as 136 bytes by the firmware; its internal fields and the complete output-code format remain unresolved. From 10829db69fb0d334f8f12b597b6a8b5593c6984e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:20:06 +0300 Subject: [PATCH 0693/1360] fix(font): validate script and language selection --- core/libs/prx/libSceFont/src/Style.cpp | 38 ++++++++++++++++++++++++-- core/libs/tests/GuestFont.cpp | 27 ++++++++++++++---- docs/dev/debt/libSceFont.md | 3 +- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index e4adb89793..22291654ee 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -3,6 +3,7 @@ #include <algorithm> #include <cstring> +#include <stdexcept> #include "prx/libSceFont/include/FontInternal.hpp" @@ -75,6 +76,29 @@ int GetFontSetting(FontHandle fontHandle, std::map<int, int> FontState::*table, }); } +bool IsScriptLanguageValid(int script, int language) { + if (script == 0x100) return language >= 0x100 && language <= 0x105; + if (script == 0x600) return language >= 0x600 && language <= 0x602; + if (script != 0x3000) return false; + switch (language) { + case 0x3000: + case 0x3011: + case 0x3012: + case 0x3013: + case 0x3014: + case 0x3016: + case 0x3017: + case 0x3018: + case 0x3021: + case 0x3022: + case 0x3023: + case 0x3041: + return true; + default: + return false; + } +} + void ResetScaleOutputs(float* w, float* h) { if (w) *w = 0.0f; if (h) *h = 0.0f; @@ -119,11 +143,21 @@ int APS5_VABI sceFontGetAttribute(FontHandle fontHandle, int attribute, int* now } int APS5_VABI sceFontSetScriptLanguage(FontHandle fontHandle, int fontScript, int fontLanguage) { - return SetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); + const int result = AccessFontSettings(fontHandle, [&](FontState&) { + return IsScriptLanguageValid(fontScript, fontLanguage) ? SCE_FONT_OK : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontSetScriptLanguage: driver language selection is not implemented"); } int APS5_VABI sceFontGetScriptLanguage(FontHandle fontHandle, int fontScript, int* fontLanguage) { - return GetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); + if (fontLanguage) *fontLanguage = 0; + const int result = AccessFontSettings(fontHandle, [&](FontState&) { + if (!fontLanguage) return SCE_FONT_ERROR_INVALID_PARAMETER; + return fontScript == 0x100 || fontScript == 0x600 || fontScript == 0x3000 ? SCE_FONT_OK : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontGetScriptLanguage: driver language query is not implemented"); } int APS5_VABI sceFontSetTypographicDesign(FontHandle fontHandle, int typographic, int feature) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index ac87c2de70..afa5e93d2c 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -302,12 +302,29 @@ int main() { const std::vector<unsigned char> fontData = EmptyGlyphFont(); Require(sceFontOpenFontMemory(library, fontData.data(), static_cast<std::uint32_t>(fontData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); int setting = -1; - Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_OK && setting == 0); - Require(sceFontSetScriptLanguage(font, 3, 7) == SCE_FONT_OK && sceFontSetScriptLanguage(font, 4, 9) == SCE_FONT_OK); - Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_OK && setting == 7); - Require(sceFontGetScriptLanguage(font, 4, &setting) == SCE_FONT_OK && setting == 9); - Require(sceFontGetScriptLanguage(font, 3, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_ERROR_INVALID_PARAMETER && setting == 0); + Require(sceFontSetScriptLanguage(font, 3, 7) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetScriptLanguage(font, 0x100, 0x106) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetScriptLanguage(font, 0x600, 0x603) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetScriptLanguage(font, 0x3000, 0x3015) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetScriptLanguage(font, 0x100, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontSetScriptLanguage(nullptr, 3, 7) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetScriptLanguage(nullptr, 3, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + for (const auto script : {0x100, 0x600, 0x3000}) { + bool setThrew = false; + bool getThrew = false; + try { + sceFontSetScriptLanguage(font, script, script); + } catch (const std::logic_error&) { + setThrew = true; + } + try { + sceFontGetScriptLanguage(font, script, &setting); + } catch (const std::logic_error&) { + getThrew = true; + } + Require(setThrew && getThrew && setting == 0); + } Require(sceFontSetTypographicDesign(font, 1, 2) == SCE_FONT_OK); Require(sceFontGetTypographicDesign(font, 1, &setting) == SCE_FONT_OK && setting == 2); Require(sceFontGetTypographicDesign(font, 5, &setting) == SCE_FONT_OK && setting == 0); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 919ecce40a..a8c0fe64db 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -2,7 +2,8 @@ ### Unknown function info -- [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet +- [sceFontSetTypographicDesign and sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp): signatures are confirmed by the supplied PS5 library. Native feature ranges, defaults and driver-dependent availability are not implemented; the host still accepts arbitrary values and stores them without affecting shaping or rendering. +- [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [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 - [sceFontDettachDeviceCacheBuffer](../../../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. It detaches the cache buffer attached by `sceFontAttachDeviceCacheBuffer`, returns the buffer and its size, and reports `SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER` when no cache buffer is attached - [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` From f60226dcf5155777335b37f103bcfa2263f4d32a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:21:07 +0300 Subject: [PATCH 0694/1360] fix(font): restore writing order and metrics validation --- core/libs/prx/libSceFont/src/WritingLine.cpp | 27 ++++++++++++------ core/libs/tests/GuestFontWritingLine.cpp | 29 ++++++++++++-------- docs/dev/debt/libSceFont.md | 4 +-- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceFont/src/WritingLine.cpp b/core/libs/prx/libSceFont/src/WritingLine.cpp index 35e014c3c8..c0643b51d0 100644 --- a/core/libs/prx/libSceFont/src/WritingLine.cpp +++ b/core/libs/prx/libSceFont/src/WritingLine.cpp @@ -246,19 +246,24 @@ int APS5_VABI sceFontDestroyWritingLine(void** pWritingLine) { } int APS5_VABI sceFontWritingLineClear(void* writingLine) { - auto* line = GetLine(__func__, writingLine); + if (!writingLine) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast<WritingLine*>(writingLine); + { + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; + } ResetOrders(*line); return SCE_FONT_OK; } int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t writingAttribute, const FontWritingMetrics* writingMetrics, void* writingOrderer) { - auto* line = GetLine(__func__, writingLine); - if (writingAttribute == 0) writingAttribute = WRITING_ATTRIBUTE_ID; - if ((writingAttribute & WRITING_ATTRIBUTE_ID_MASK) != WRITING_ATTRIBUTE_ID) - Unsupported(__func__, "writing attribute " + std::to_string(writingAttribute) + " is not modelled"); const std::size_t level = (writingAttribute >> 8) & 0xFF; - if (level >= WRITING_LEVEL_COUNT) Unsupported(__func__, "embedding level " + std::to_string(level) + " (error code not verified)"); - if (!writingOrderer) Unsupported(__func__, "null writing orderer (error code not verified)"); + if (!writingLine || !writingOrderer || (writingAttribute & WRITING_ATTRIBUTE_ID_MASK) != WRITING_ATTRIBUTE_ID || level >= WRITING_LEVEL_COUNT) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast<WritingLine*>(writingLine); + { + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; + } if (line->cursor) ResetOrders(*line); WritingOrder* order = &line->orders.emplace_back(); order->orderer = writingOrderer; @@ -319,8 +324,12 @@ const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* wr } int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingMetrics* writingMetrics) { - auto* line = GetLine(__func__, writingLine); - if (!writingMetrics) Unsupported(__func__, "null metrics pointer (error code not verified)"); + if (!writingMetrics) return SCE_FONT_ERROR_INVALID_PARAMETER; + *writingMetrics = {}; + if (!writingLine) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast<WritingLine*>(writingLine); + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; *writingMetrics = line->renderMetrics; return SCE_FONT_OK; } diff --git a/core/libs/tests/GuestFontWritingLine.cpp b/core/libs/tests/GuestFontWritingLine.cpp index 7b40b889ca..019d9aaffe 100644 --- a/core/libs/tests/GuestFontWritingLine.cpp +++ b/core/libs/tests/GuestFontWritingLine.cpp @@ -108,14 +108,15 @@ int main() { const FontWritingMetrics runC{8.0f, 0.0f, {-4.0f, 1.0f, 2.0f, 5.0f}}; const FontWritingMetrics runD{4.0f, 0.0f, {-4.0f, 1.0f, 0.0f, 20.0f}}; - Require(Throws([&] { sceFontWritingLineWritesOrder(nullptr, Attribute(0), &runA, &a); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, 1, &runA, &a); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, 0x1FB, &runA, &a); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, Attribute(0), &runA, nullptr); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, Attribute(0x7F), &runA, &a); })); + Require(sceFontWritingLineWritesOrder(nullptr, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, 1, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, 0x1FB, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, Attribute(0x7F), &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); - Require(sceFontWritingLineWritesOrder(line, 0, &runA, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, 0, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_OK); Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runB, &b) == SCE_FONT_OK); Require(SpaceIs(line, 16.0f, 1.0f, 0.0f, 1.0f)); Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {})); @@ -222,15 +223,19 @@ int main() { Require(sceFontWritingLineGetOrderingSpace(rtl, nullptr, nullptr, &space, nullptr) == SCE_FONT_OK && space == 15.0f); Require(Throws([&] { sceFontWritingLineGetOrderingSpace(rtl, &space, &space, &space, nullptr); })); Require(Throws([&] { sceFontWritingLineGetOrderingSpace(nullptr, &space, nullptr, nullptr, nullptr); })); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(nullptr, &metrics); })); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(rtl, nullptr); })); - Require(Throws([&] { sceFontWritingLineClear(nullptr); })); + Require(sceFontWritingLineGetRenderMetrics(nullptr, &metrics) == SCE_FONT_ERROR_INVALID_PARAMETER && Same(metrics, {})); + Require(sceFontWritingLineGetRenderMetrics(rtl, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineClear(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); int notALine = 0; Require(sceFontWritingLineRefersRenderStep(nullptr) == nullptr); - Require(Throws([&] { sceFontWritingLineClear(¬ALine); })); + Require(sceFontWritingLineClear(¬ALine) == SCE_FONT_ERROR_INVALID_WRITING); Require(Throws([&] { sceFontWritingLineRefersRenderStep(¬ALine); })); Require(Throws([&] { sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space); })); + metrics = runA; + Require(sceFontWritingLineGetRenderMetrics(¬ALine, &metrics) == SCE_FONT_ERROR_INVALID_WRITING && Same(metrics, {})); + Require(sceFontWritingLineWritesOrder(¬ALine, 0, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(¬ALine, Attribute(0), nullptr, &a) == SCE_FONT_ERROR_INVALID_WRITING); void* bogus = ¬ALine; Require(Throws([&] { sceFontDestroyWritingLine(&bogus); }) && bogus == ¬ALine); @@ -238,8 +243,8 @@ int main() { Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_OK); - Require(Throws([&] { sceFontWritingLineClear(destroyed); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(destroyed, Attribute(0), &runA, &a); })); + Require(sceFontWritingLineClear(destroyed) == SCE_FONT_ERROR_INVALID_WRITING); + Require(sceFontWritingLineWritesOrder(destroyed, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_WRITING); Require(sceFontDestroyWritingLine(<r) == SCE_FONT_OK && ltr == nullptr); Require(sceFontDestroyWritingLine(&rtl) == SCE_FONT_OK && rtl == nullptr); std::puts("font writing line: ok"); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index a8c0fe64db..f26cf6d061 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -8,7 +8,7 @@ - [sceFontDettachDeviceCacheBuffer](../../../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. It detaches the cache buffer attached by `sceFontAttachDeviceCacheBuffer`, returns the buffer and its size, and reports `SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER` when no cache buffer is attached - [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 64-bit writing attribute is read as the PS4 11.00 `libSceFont.sprx` reads it: low byte `0xFA`, embedding level in bits 8-15 (0 to 126), bit 30 and bit 31 marking space before and after the run; the PS5 library was not measured, and the names of the two bits are ours. PPSA01325 builds its attribute the same way (`0xFA | level << 8`, bits 30 and 31) and passes a null metrics pointer when its run has no metrics object. Attribute 0 is accepted and treated as `0xFA` with level 0 and no spacing bits, as before this reading; the PS4 library rejects it and what the PS5 library does with it is unknown. Any other attribute, a level above 126 and a null orderer throw: the PS4 library returns `0x80460002` for them, not confirmed for the PS5 +- [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): supplied PS5 code confirms the four-argument signatures, low attribute byte `0xFA`, embedding levels 0 through 126 and spacing bits 30/31. Invalid attributes, including zero, null orderers and null lines return `0x80460002`; invalid non-null line handles return `0x8046000A`. Metrics are optional. Native creation accepts only forms `0x11`/`0x12` and a null detail, validates font memory and allocates 2208 bytes through its callback; those creation and allocation rules remain unimplemented. - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. @@ -21,7 +21,7 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - 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 Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; 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. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. -- [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, 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) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and other unchecked outputs throw. PS5 validation for writes, clear and render metrics is confirmed and implemented, including zeroing failed metrics outputs; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, 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 graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From 979aa24f40f1cbe0ff2a49666268f26917a7df2e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:23:31 +0300 Subject: [PATCH 0695/1360] fix(net): restore server startup and routing parameter layouts --- core/libs/prx/libSceNet/Export.cpp | 63 ++++++++++++++++++++++++++++-- docs/dev/debt/libSceNet.md | 6 +-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index f69fa0f01d..808cdc7364 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -104,6 +104,63 @@ struct NetAddress6StartParameter; using NetAddressCallback = void (APS5_VABI *)(void* userArg, std::int32_t event, std::int32_t error); +struct NetDhcpdStartParameter { + std::uint32_t addresses[5]; + std::uint32_t maxClients; +}; + +struct NetPppoeStartParameter { + std::uint32_t dnsAddresses[2]; + char userName[128]; + char password[128]; + std::int32_t mtu; + std::uint32_t flags; +}; + +struct NetRoutingAddressParameter { + std::uint32_t address; + std::uint32_t mask; +}; + +struct NetRoutingServerParameter { + std::int32_t count; + std::uint32_t localAddresses[2]; + std::uint32_t remoteAddresses[2]; +}; + +struct NetRoutingRule { + std::int32_t protocol; + std::uint32_t firstAddress; + std::uint16_t firstPort; + std::uint16_t reserved; + std::uint32_t secondAddress; + std::uint16_t secondPort; + std::uint16_t flags; +}; + +struct NetRoutingRuleParameter { + std::int32_t count; + const NetRoutingRule* rules; +}; + +static_assert(sizeof(NetRoutingAddressParameter) == 8); +static_assert(sizeof(NetRoutingServerParameter) == 20); +static_assert(offsetof(NetRoutingServerParameter, remoteAddresses) == 12); +static_assert(sizeof(NetRoutingRule) == 20); +static_assert(offsetof(NetRoutingRule, firstPort) == 8); +static_assert(offsetof(NetRoutingRule, secondAddress) == 12); +static_assert(offsetof(NetRoutingRule, flags) == 18); +static_assert(sizeof(NetRoutingRuleParameter) == 16); +static_assert(offsetof(NetRoutingRuleParameter, rules) == 8); + +static_assert(sizeof(NetDhcpdStartParameter) == 24); +static_assert(offsetof(NetDhcpdStartParameter, maxClients) == 20); +static_assert(sizeof(NetPppoeStartParameter) == 272); +static_assert(offsetof(NetPppoeStartParameter, userName) == 8); +static_assert(offsetof(NetPppoeStartParameter, password) == 136); +static_assert(offsetof(NetPppoeStartParameter, mtu) == 264); +static_assert(offsetof(NetPppoeStartParameter, flags) == 268); + struct NetInfoDumpParameter { std::uint32_t flags; std::uint32_t intervalSeconds; @@ -2760,7 +2817,7 @@ int APS5_VABI sceNetInfoDumpStop(void) { return 0; } -int APS5_VABI sceNetDhcpdStart(void) { +int APS5_VABI sceNetDhcpdStart(std::uint32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, const NetDhcpdStartParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2920,7 +2977,7 @@ int APS5_VABI sceNetConfigEtherSetLinkMode(std::uint32_t interfaceId, int mode) return 0; } -int APS5_VABI sceNetPppoeStart(void) { +int APS5_VABI sceNetPppoeStart(std::uint32_t interfaceId, std::int32_t pppoeInterfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, const NetPppoeStartParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2950,7 +3007,7 @@ int APS5_VABI sceNetConfigSetDefaultScope(std::uint32_t interfaceId, const char* return 0; } -int APS5_VABI sceNetConfigRoutingStart(void) { +int APS5_VABI sceNetConfigRoutingStart(std::uint32_t firstInterfaceId, std::uint32_t secondInterfaceId, const NetRoutingAddressParameter* addressParameter, NetRoutingServerParameter* serverParameter, const NetRoutingRuleParameter* ruleParameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 3abcb2a50e..8c3e26db4d 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -19,9 +19,9 @@ - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. -- [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp): six arguments and the three-argument address callback are confirmed. The 24-byte parameter contains five network-order addresses and a client limit. Individual address roles and the two unused thread-option meanings remain unresolved. +- [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp): seven arguments are confirmed, including the parameter pointer on the stack. The 272-byte parameter contains two DNS addresses, two 128-byte credential arrays, MTU and flags. Callback arguments are user data, event and error; the two unused thread-option meanings remain unresolved. +- [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp): five pointer/scalar arguments are confirmed. Input address data is eight bytes; the mutable server parameter is 20 bytes and duplicates its first address when its count is one. Optional rules contain a count and pointer to 20-byte records with protocol, two address/port pairs and flags. Layouts are defined; rule flag meanings and protocol-specific port interpretation remain unresolved. - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. ### Functional From 4fa7e142c3b6b9e1ddc6f52778f5d4be9e0a1a26 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:26:38 +0300 Subject: [PATCH 0696/1360] fix(npentitlementaccess): restore request and result argument signatures --- core/libs/SceTypes.hpp | 18 +++++++++++ .../prx/libSceNpEntitlementAccess/Export.cpp | 30 ++++++++--------- core/libs/tests/GuestOfflineImports.cpp | 32 +++++++++---------- docs/dev/debt/libSceNpEntitlementAccess.md | 2 +- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 688c87737a..9c81747059 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1411,6 +1411,24 @@ struct NpUnifiedEntitlementLabel { char padding[3]; }; +struct NpServiceEntitlementLabel { + char data[7]; + std::uint8_t reserved[13]; +}; + +struct NpEntitlementAccessTransactionId { + char data[65]; + std::uint8_t reserved[7]; +}; + +struct NpEntitlementAccessListParameter; +struct NpEntitlementAccessUnifiedInfo; +struct NpEntitlementAccessServiceInfo; + +static_assert(sizeof(NpUnifiedEntitlementLabel) == 20); +static_assert(sizeof(NpServiceEntitlementLabel) == 20); +static_assert(sizeof(NpEntitlementAccessTransactionId) == 72); + struct NpEntitlementAccessInitParam { char reserved[32]; }; struct NpEntitlementAccessBootParam { char reserved[32]; }; diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index ba654ff02f..ecec40a1a2 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -118,46 +118,46 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara return 0; } -int APS5_VABI sceNpEntitlementAccessAbortRequest(void) { +int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t requestId) { return 0; } -int APS5_VABI sceNpEntitlementAccessDeleteRequest(void) { +int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t requestId) { return 0; } -int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(void* transaction_id) { - if (!transaction_id) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; +int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(NpEntitlementAccessTransactionId* transactionId) { + if (!transactionId) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(void) { +int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(std::int64_t requestId, std::int32_t* result, std::int32_t* remainingCount) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(void) { +int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, const NpEntitlementAccessTransactionId* transactionId, std::int32_t count, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { +int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabel, const NpEntitlementAccessTransactionId* transactionId, std::int32_t count, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* info) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* info) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabel, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } @@ -175,19 +175,19 @@ int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(uint32_t* flag) { return 0; } -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index fc8be8f7f4..56dadb62ba 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -15,14 +15,14 @@ int APS5_VABI sceNpEntitlementAccessGetEntitlementKey( std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessEntitlementKey*); int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(std::uint32_t*); int APS5_VABI sceNpEntitlementAccessGetPftFlag(std::uint32_t*); -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(); -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(); -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(); -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult); +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult); +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* info); +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabel, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* info); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpActivate(std::int32_t, const void*, std::uint32_t); int APS5_VABI sceRudpGetStatus(void*, std::size_t); @@ -79,14 +79,14 @@ int main() { Require(sceNpEntitlementAccessGetPftFlag(nullptr) == parameter); constexpr int signedOut = static_cast<int>(0x80550006); - Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessPollServiceEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo() == signedOut); - Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo() == signedOut); - Require(sceNpEntitlementAccessRequestServiceEntitlementInfo() == signedOut); - Require(sceNpEntitlementAccessPollServiceEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(0, 0, nullptr, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList(0, nullptr, nullptr, 0, nullptr, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList(0, 0, nullptr, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfoList(0, nullptr, nullptr, 0, nullptr, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo(0, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo(0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfo(0, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfo(0, nullptr, nullptr) == signedOut); Require(sceRudpActivate(-1, nullptr, 0) == static_cast<int>(0x80770001)); std::array<unsigned char, 248> status; diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index 2a9d2f3089..d6f56513d4 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -4,7 +4,7 @@ - [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified +- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort/delete still report success without tracking requests. ### Functional From bffa030a165416cc326d029fce12107e7db7cda5 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:27:49 +0300 Subject: [PATCH 0697/1360] fix(npwebapi2): track push handles and restore validation errors --- core/libs/prx/libSceNpWebApi2/Export.cpp | 54 ++++++++++++++++-------- core/libs/tests/GuestNpWebApi2.cpp | 36 +++++++++------- docs/dev/debt/libSceNpWebApi2.md | 6 +-- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 9a2ebe62dd..ea33832e83 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -10,6 +10,8 @@ // The PSN web API has no service behind it: handles can be created, but requests fail. static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80553402); static constexpr int SCE_NP_WEBAPI2_ERROR_UNAVAILABLE = static_cast<int>(0x80553406); +static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT = static_cast<int>(0x80553404); +static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE = static_cast<int>(0x8055340D); static std::atomic<int> g_nextHandle{1}; struct NpWebApi2PushContextId { @@ -35,6 +37,14 @@ static int CreateLibraryContext(size_t poolSize) { return libCtxId; } +struct PushHandle { + std::int32_t libraryContextId; + std::uint32_t timeout = 0; + bool aborted = false; +}; + +static std::unordered_map<int, PushHandle> pushHandles; + extern "C" { int APS5_VABI sceNpWebApi2AbortRequest(int64_t request_id) { @@ -105,11 +115,11 @@ int APS5_VABI sceNpWebApi2Initialize(int lib_http_ctx_id, size_t pool_size) { return CreateLibraryContext(pool_size); } -int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int lib_ctx_id, NpWebApi2MemoryPoolStats* stats) { - if (stats == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int libraryContextId, NpWebApi2MemoryPoolStats* stats) { std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(lib_ctx_id); - if (pool == g_pools.end()) throw std::invalid_argument("sceNpWebApi2GetMemoryPoolStats: unknown context"); + const auto pool = g_pools.find(libraryContextId); + if (pool == g_pools.end()) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + if (!stats) return 0; *stats = {pool->second, 0, 0, 0}; return 0; } @@ -124,21 +134,29 @@ int APS5_VABI sceNpWebApi2PushEventCreateFilter(int lib_ctx_id, int handle_id, c return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } -int APS5_VABI sceNpWebApi2PushEventCreateHandle(int lib_ctx_id) { - (void)lib_ctx_id; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +int APS5_VABI sceNpWebApi2PushEventCreateHandle(int libraryContextId) { + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const int handleId = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + pushHandles.emplace(handleId, PushHandle{libraryContextId}); + return handleId; } -int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libCtxId, int handleId) { - (void)handleId; +int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libraryContextId, int handleId) { std::lock_guard lock(g_poolsMutex); - if (!g_pools.contains(libCtxId)) throw std::invalid_argument("sceNpWebApi2PushEventAbortHandle: unknown context"); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const auto handle = pushHandles.find(handleId); + if (handle == pushHandles.end() || handle->second.libraryContextId != libraryContextId) return SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE; + handle->second.aborted = true; return 0; } -int APS5_VABI sceNpWebApi2PushEventDeleteHandle(int lib_ctx_id, int handle_id) { - (void)lib_ctx_id; - (void)handle_id; +int APS5_VABI sceNpWebApi2PushEventDeleteHandle(int libraryContextId, int handleId) { + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const auto handle = pushHandles.find(handleId); + if (handle == pushHandles.end() || handle->second.libraryContextId != libraryContextId) return SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE; + pushHandles.erase(handle); return 0; } @@ -173,6 +191,7 @@ int APS5_VABI sceNpWebApi2SendRequest(int64_t request_id, const void* data, size int APS5_VABI sceNpWebApi2Terminate(int lib_ctx_id) { std::lock_guard lock(g_poolsMutex); + std::erase_if(pushHandles, [lib_ctx_id](const auto& item) { return item.second.libraryContextId == lib_ctx_id; }); g_pools.erase(lib_ctx_id); return 0; } @@ -185,11 +204,12 @@ int APS5_VABI sceNpWebApi2PushEventDeleteFilter(std::int32_t libraryContextId, s return 0; } -int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int lib_ctx_id, int handle_id, uint32_t timeout) { - (void)handle_id; - (void)timeout; +int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int libraryContextId, int handleId, std::uint32_t timeout) { std::lock_guard lock(g_poolsMutex); - if (!g_pools.contains(lib_ctx_id)) throw std::invalid_argument("sceNpWebApi2PushEventSetHandleTimeout: unknown context"); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const auto handle = pushHandles.find(handleId); + if (handle == pushHandles.end() || handle->second.libraryContextId != libraryContextId) return SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE; + handle->second.timeout = timeout; return 0; } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 8ae914ceda..eac1a6321f 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -20,6 +20,7 @@ int APS5_VABI sceNpWebApi2Terminate(int); int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int, NpWebApi2MemoryPoolStats*); int APS5_VABI sceNpWebApi2PushEventCreateHandle(int); int APS5_VABI sceNpWebApi2PushEventAbortHandle(int, int); +int APS5_VABI sceNpWebApi2PushEventDeleteHandle(int, int); int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int, int, std::uint32_t); int APS5_VABI sceNpWebApi2CreateUserContext(int, int); int APS5_VABI sceNpWebApi2CreateRequest(int, const char*, const char*, const char*, const void*, std::int64_t*); @@ -30,17 +31,9 @@ int APS5_VABI sceNpWebApi2DeleteRequest(std::int64_t); static void Require(bool value) { if (!value) std::abort(); } -template <typename TCall> -static bool Throws(TCall call) { - try { - call(); - } catch (const std::exception&) { - return true; - } - return false; -} - int main() { + constexpr int invalidContext = static_cast<int>(0x80553404); + constexpr int invalidHandle = static_cast<int>(0x8055340D); const int context = sceNpWebApi2Initialize(1, 0x20000); const int other = sceNpWebApi2Initialize(1, 0x8000); Require(context > 0 && other > 0 && context != other); @@ -48,12 +41,24 @@ int main() { Require(sceNpWebApi2GetMemoryPoolStats(context, &stats) == 0); Require(stats.pool_size == 0x20000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); Require(sceNpWebApi2GetMemoryPoolStats(other, &stats) == 0 && stats.pool_size == 0x8000); - Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, nullptr); })); + Require(sceNpWebApi2GetMemoryPoolStats(context, nullptr) == 0); + Require(sceNpWebApi2GetMemoryPoolStats(-1, nullptr) == invalidContext); + Require(sceNpWebApi2PushEventCreateHandle(-1) == invalidContext); const int handle = sceNpWebApi2PushEventCreateHandle(context); Require(handle > 0); Require(sceNpWebApi2PushEventAbortHandle(context, handle) == 0); - Require(sceNpWebApi2PushEventAbortHandle(context, handle + 1000) == 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle + 1000) == invalidHandle); Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); + Require(sceNpWebApi2PushEventAbortHandle(other, handle) == invalidHandle); + Require(sceNpWebApi2PushEventDeleteHandle(other, handle) == invalidHandle); + Require(sceNpWebApi2PushEventSetHandleTimeout(other, handle, 0) == invalidHandle); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 0) == 0); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, UINT32_MAX) == 0); + Require(sceNpWebApi2PushEventDeleteHandle(context, handle) == 0); + Require(sceNpWebApi2PushEventDeleteHandle(context, handle) == invalidHandle); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == invalidHandle); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 1) == invalidHandle); + const int survivingHandle = sceNpWebApi2PushEventCreateHandle(other); const int user = sceNpWebApi2CreateUserContext(context, 1); std::int64_t request = 0; Require(user > 0 && sceNpWebApi2CreateRequest(user, "sessionManager", "/v1/sessions", "GET", nullptr, &request) == 0); @@ -62,9 +67,10 @@ int main() { Require(sceNpWebApi2AddWebTraceTag(request, nullptr) == static_cast<int>(0x80553402)); Require(sceNpWebApi2DeleteRequest(request) == 0); Require(sceNpWebApi2Terminate(context) == 0); - Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); - Require(Throws([&] { sceNpWebApi2PushEventAbortHandle(context, handle); })); - Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); + Require(sceNpWebApi2GetMemoryPoolStats(context, &stats) == invalidContext); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == invalidContext); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == invalidContext); + Require(sceNpWebApi2PushEventAbortHandle(other, survivingHandle) == 0); Require(sceNpWebApi2Terminate(other) == 0); constexpr int invalidArgument = static_cast<int>(0x80553402); diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index cc65efef68..7c73e05da4 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -11,6 +11,6 @@ - [sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize and sceNpWebApi2IntInitialize2](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - create a library context like `sceNpWebApi2Initialize`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L174-L222) does on the PS4: the argument structures (32 and 40 bytes) and the `struct_size` check that returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT` follow its [declarations](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.h#L15-L31). The context type shadPS4 records (2 for the internal initializers, 3 for presence), the context name and the push configuration group are not kept, and the 0x8000 context limit is not applied. Not measured on a PS5 - [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c). The signatures follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L221) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399); opaque struct pointers use `const void*` or `void*` to preserve source constness. These prototypes are not measured on a console. - [sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - no credible prototype was found; it is declared without parameters solely to resolve known imports and makes no ABI claim. -- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) - the two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. An unknown library context throws, as in `sceNpWebApi2PushEventSetHandleTimeout`. The handle is not checked and nothing is aborted, as push event handles are not tracked and nothing waits on a handle offline -- [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to the initializer that created the context, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws -- [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline +- [Push handle lifecycle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 create/delete/abort signatures and invalid-context/invalid-handle errors are confirmed. Handles track ownership, timeout and abort state; deletion and context termination remove them. Network push operations and cancellation of active requests remain unimplemented. +- [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 confirms the context/statistics arguments and 32-byte output. An invalid context returns `0x80553404` without changing output; a valid context with null output succeeds. Allocator accounting remains absent: peak and current usage still report zero instead of native pool allocations. +- [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 confirms three 32-bit arguments, ownership validation and storing the entire timeout value without range checks. The stored timeout is not consumed because asynchronous push operations are unimplemented. From 2b4dc840993f157e3315e0229ff19d39c93f77f4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:27:49 +0300 Subject: [PATCH 0698/1360] docs(debt): condense remaining font and http2 gaps --- docs/dev/debt/libSceFont.md | 2 +- docs/dev/debt/libSceHttp2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index f26cf6d061..e6bfcbb5bf 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -21,7 +21,7 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - 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 Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; 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. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. -- [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and other unchecked outputs throw. PS5 validation for writes, clear and render metrics is confirmed and implemented, including zeroing failed metrics outputs; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, 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): ordering and spacing follow PS4 measurements; full PS5 algorithm equivalence remains unverified. PS5 validation for writes, clear and render metrics is implemented. Creation still accepts form `0x10` and detail `0x0FD5`, ignores font memory and allocates on the host, contrary to the supplied PS5 code. Destruction, step traversal and ordering-space errors still require comparison. Unsupported handles in those paths throw. - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 74ba9af4d0..73186b8a90 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) +- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for an invalid handle, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) ### Unknown function info From aca0f63647eec5f2dd34d51abe800b68ca594742 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:30:29 +0300 Subject: [PATCH 0699/1360] fix(font): allocate writing lines through caller memory callbacks --- core/libs/prx/libSceFont/src/WritingLine.cpp | 112 +++++++++++++------ core/libs/tests/GuestFontWritingLine.cpp | 74 +++++++++--- docs/dev/debt/libSceFont.md | 6 +- 3 files changed, 140 insertions(+), 52 deletions(-) diff --git a/core/libs/prx/libSceFont/src/WritingLine.cpp b/core/libs/prx/libSceFont/src/WritingLine.cpp index c0643b51d0..7365c1f418 100644 --- a/core/libs/prx/libSceFont/src/WritingLine.cpp +++ b/core/libs/prx/libSceFont/src/WritingLine.cpp @@ -1,7 +1,7 @@ #include <array> #include <cstddef> #include <cstdint> -#include <deque> +#include <memory> #include <mutex> #include <stdexcept> #include <string> @@ -11,24 +11,19 @@ namespace { -constexpr std::int32_t WRITING_FORM_HORIZONTAL = 0x10; constexpr std::int32_t WRITING_FORM_HORIZONTAL_RTL = 0x11; constexpr std::int32_t WRITING_FORM_HORIZONTAL_LTR = 0x12; -constexpr std::uint16_t CREATE_WRITING_LINE_DETAIL_ID = 0x0FD5; constexpr std::uint64_t WRITING_ATTRIBUTE_ID_MASK = 0xFF; constexpr std::uint64_t WRITING_ATTRIBUTE_ID = 0xFA; constexpr std::uint64_t WRITING_ATTRIBUTE_SPACE_BEFORE = std::uint64_t{1} << 30; constexpr std::uint64_t WRITING_ATTRIBUTE_SPACE_AFTER = std::uint64_t{1} << 31; constexpr std::size_t WRITING_LEVEL_COUNT = 0x7F; -struct CreateWritingLineDetail { - std::uint16_t detailId; -}; - struct WritingOrder { void* orderer = nullptr; WritingOrder* prev = nullptr; WritingOrder* next = nullptr; + std::uint64_t reserved = 0; std::uint64_t attribute = 0; FontWritingMetrics metrics{}; }; @@ -39,22 +34,42 @@ struct WritingLevel { }; struct WritingLine { + std::uint16_t kind = 0xF0A; + std::uint16_t reserved = 0; std::int32_t form = 0; + const FontMemory* memory = nullptr; FontWritingMetrics renderMetrics{}; FontWritingLineStep step{}; float advanceX = 0.0f; float advanceY = 0.0f; float orderingSpace = 0.0f; float steppedSpace = 0.0f; - std::size_t lastLevel = 0; + std::uint32_t lastLevel = 0; + std::uint32_t reservedLevel = 0; + WritingOrder* first = nullptr; WritingOrder* last = nullptr; WritingOrder* head = nullptr; WritingOrder* tail = nullptr; WritingOrder* cursor = nullptr; + std::uint64_t reservedCursor = 0; std::array<WritingLevel, WRITING_LEVEL_COUNT> levels{}; - std::deque<WritingOrder> orders; }; +static_assert(sizeof(WritingOrder) == 64); +static_assert(offsetof(WritingOrder, attribute) == 32); +static_assert(offsetof(WritingOrder, metrics) == 40); +static_assert(sizeof(WritingLine) == 2208); +static_assert(offsetof(WritingLine, step) == 40); +static_assert(offsetof(WritingLine, advanceX) == 104); +static_assert(offsetof(WritingLine, last) == 136); +static_assert(offsetof(WritingLine, head) == 144); +static_assert(offsetof(WritingLine, cursor) == 160); +static_assert(offsetof(WritingLine, levels) == 176); + +bool IsMemoryValid(const FontMemory* memory) { + return memory && memory->mem_kind == 0xF00 && memory->iface && memory->iface->alloc && memory->iface->dealloc; +} + std::mutex linesMutex; std::unordered_set<WritingLine*> lines; @@ -62,13 +77,6 @@ std::unordered_set<WritingLine*> lines; throw std::runtime_error(std::string(function) + ": " + what); } -WritingLine* GetLine(const char* function, void* writingLine) { - auto* line = static_cast<WritingLine*>(writingLine); - std::lock_guard lock(linesMutex); - if (!lines.contains(line)) Unsupported(function, "unknown writing line handle (error code not verified)"); - return line; -} - bool IsRightToLeft(const WritingLine& line) { return line.form == WRITING_FORM_HORIZONTAL_RTL; } @@ -94,7 +102,13 @@ float TailSpace(const WritingLine& line, const WritingOrder& order) { } void ResetOrders(WritingLine& line) { - line.orders.clear(); + for (WritingOrder* order = line.head; order;) { + WritingOrder* next = order->next; + std::destroy_at(order); + line.memory->iface->dealloc(line.memory->mspace_handle, order); + order = next; + } + line.first = nullptr; line.levels.fill({}); line.advanceX = 0.0f; line.advanceY = 0.0f; @@ -217,30 +231,41 @@ void ExtendRenderMetrics(WritingLine& line) { extern "C" { int APS5_VABI sceFontCreateWritingLine(const FontMemory* memory, std::int32_t writingForm, const void* writingLineDetail, void** pWritingLine) { - (void)memory; - if (!pWritingLine) Unsupported(__func__, "null writing line pointer (error code not verified)"); - if (writingForm != WRITING_FORM_HORIZONTAL && writingForm != WRITING_FORM_HORIZONTAL_RTL && writingForm != WRITING_FORM_HORIZONTAL_LTR) - Unsupported(__func__, "writing form " + std::to_string(writingForm) + " is not modelled"); - if (writingLineDetail && static_cast<const CreateWritingLineDetail*>(writingLineDetail)->detailId != CREATE_WRITING_LINE_DETAIL_ID) - Unsupported(__func__, "unknown writing line detail id"); - auto* line = new WritingLine{}; + if (!memory) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!IsMemoryValid(memory)) return SCE_FONT_ERROR_INVALID_MEMORY; + if (!pWritingLine || writingLineDetail || (writingForm != WRITING_FORM_HORIZONTAL_RTL && writingForm != WRITING_FORM_HORIZONTAL_LTR)) return SCE_FONT_ERROR_INVALID_PARAMETER; + void* allocation = memory->iface->alloc(memory->mspace_handle, sizeof(WritingLine)); + if (!allocation) return SCE_FONT_ERROR_ALLOCATION_FAILED; + auto* line = std::construct_at(static_cast<WritingLine*>(allocation)); + line->memory = memory; line->form = writingForm; { std::lock_guard lock(linesMutex); - lines.insert(line); + try { + lines.insert(line); + } catch (...) { + std::destroy_at(line); + memory->iface->dealloc(memory->mspace_handle, allocation); + throw; + } } *pWritingLine = line; return SCE_FONT_OK; } int APS5_VABI sceFontDestroyWritingLine(void** pWritingLine) { - if (!pWritingLine || !*pWritingLine) return SCE_FONT_OK; - auto* line = GetLine(__func__, *pWritingLine); + if (!pWritingLine || !*pWritingLine) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast<WritingLine*>(*pWritingLine); { std::lock_guard lock(linesMutex); + if (!lines.contains(line) || !IsMemoryValid(line->memory)) return SCE_FONT_ERROR_INVALID_WRITING; lines.erase(line); } - delete line; + const FontMemory* memory = line->memory; + ResetOrders(*line); + line->kind = 0; + std::destroy_at(line); + memory->iface->dealloc(memory->mspace_handle, line); *pWritingLine = nullptr; return SCE_FONT_OK; } @@ -252,6 +277,7 @@ int APS5_VABI sceFontWritingLineClear(void* writingLine) { std::lock_guard lock(linesMutex); if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; } + if (!IsMemoryValid(line->memory)) return SCE_FONT_ERROR_INVALID_WRITING; ResetOrders(*line); return SCE_FONT_OK; } @@ -264,8 +290,12 @@ int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t wri std::lock_guard lock(linesMutex); if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; } + if (!IsMemoryValid(line->memory)) return SCE_FONT_ERROR_INVALID_PARAMETER; if (line->cursor) ResetOrders(*line); - WritingOrder* order = &line->orders.emplace_back(); + void* allocation = line->memory->iface->alloc(line->memory->mspace_handle, sizeof(WritingOrder)); + if (!allocation) return SCE_FONT_ERROR_ALLOCATION_FAILED; + WritingOrder* order = std::construct_at(static_cast<WritingOrder*>(allocation)); + if (!line->last) line->first = order; order->orderer = writingOrderer; order->attribute = writingAttribute; if (writingMetrics) order->metrics = *writingMetrics; @@ -276,13 +306,17 @@ int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t wri line->advanceX += order->metrics.advanceX; line->advanceY += order->metrics.advanceY; line->last = order; - line->lastLevel = level; + line->lastLevel = static_cast<std::uint32_t>(level); return SCE_FONT_OK; } const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* writingLine) { if (!writingLine) return nullptr; - auto* line = GetLine(__func__, writingLine); + auto* line = static_cast<WritingLine*>(writingLine); + { + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return nullptr; + } if (line->cursor) { ExtendRenderMetrics(*line); } else { @@ -335,7 +369,21 @@ int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingM } int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* advanceSpace, float* headSpace, float* inlineSpace, float* tailSpace) { - auto* line = GetLine(__func__, writingLine); + auto* line = static_cast<WritingLine*>(writingLine); + int error = SCE_FONT_OK; + { + std::lock_guard lock(linesMutex); + if (!line) error = SCE_FONT_ERROR_INVALID_PARAMETER; + else if (!lines.contains(line)) error = SCE_FONT_ERROR_INVALID_WRITING; + else if (line->form != WRITING_FORM_HORIZONTAL_RTL && line->form != WRITING_FORM_HORIZONTAL_LTR) error = SCE_FONT_ERROR_INVALID_PARAMETER; + } + if (error != SCE_FONT_OK) { + if (advanceSpace) *advanceSpace = 0.0f; + if (headSpace) *headSpace = 0.0f; + if (inlineSpace) *inlineSpace = 0.0f; + if (tailSpace) *tailSpace = 0.0f; + return error; + } if (headSpace && !tailSpace) Unsupported(__func__, "a head space pointer without a tail space pointer is not modelled"); if (advanceSpace) *advanceSpace = line->advanceX; if (headSpace) { diff --git a/core/libs/tests/GuestFontWritingLine.cpp b/core/libs/tests/GuestFontWritingLine.cpp index 019d9aaffe..5c311aede3 100644 --- a/core/libs/tests/GuestFontWritingLine.cpp +++ b/core/libs/tests/GuestFontWritingLine.cpp @@ -3,6 +3,7 @@ #include <cstdio> #include <cstdlib> #include <exception> +#include <initializer_list> extern "C" { int APS5_VABI sceFontCreateWritingLine(const FontMemory*, std::int32_t, const void*, void**); @@ -32,12 +33,28 @@ static bool Throws(F f) { return false; } -struct WritingLineDetail { - std::uint16_t detailId; - std::uint16_t reserved[3]; - void* reservedPointers[3]; +struct AllocationState { + std::size_t live = 0; + std::uint32_t lastSize = 0; + bool fail = false; }; +static void* APS5_VABI Allocate(void* object, std::uint32_t size) { + auto& state = *static_cast<AllocationState*>(object); + state.lastSize = size; + if (state.fail) return nullptr; + void* allocation = std::malloc(size); + if (allocation) ++state.live; + return allocation; +} + +static void APS5_VABI Deallocate(void* object, void* allocation) { + auto& state = *static_cast<AllocationState*>(object); + Require(allocation && state.live > 0); + --state.live; + std::free(allocation); +} + constexpr std::uint64_t SPACE_BEFORE = std::uint64_t{1} << 30; constexpr std::uint64_t SPACE_AFTER = std::uint64_t{1} << 31; @@ -80,15 +97,27 @@ static bool OrderIs(void* line, const std::uint64_t* levels, const std::size_t* } int main() { + AllocationState allocations; + FontMemoryInterface memoryInterface{}; + memoryInterface.alloc = Allocate; + memoryInterface.dealloc = Deallocate; FontMemory memory{}; void* line = nullptr; - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, nullptr, nullptr); })); - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x13, nullptr, &line); }) && line == nullptr); - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0, nullptr, &line); }) && line == nullptr); - WritingLineDetail detail{0x0FD4, {}, {}}; - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, &detail, &line); }) && line == nullptr); - detail.detailId = 0x0FD5; - Require(sceFontCreateWritingLine(&memory, 0x10, &detail, &line) == SCE_FONT_OK && line != nullptr); + Require(sceFontCreateWritingLine(nullptr, 0x12, nullptr, &line) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_ERROR_INVALID_MEMORY); + memory.mem_kind = 0xF00; + memory.mspace_handle = &allocations; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_ERROR_INVALID_MEMORY); + memory.iface = &memoryInterface; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (int form : {0, 0x10, 0x13}) Require(sceFontCreateWritingLine(&memory, form, nullptr, &line) == SCE_FONT_ERROR_INVALID_PARAMETER && !line); + std::uint16_t detail = 0x0FD5; + Require(sceFontCreateWritingLine(&memory, 0x12, &detail, &line) == SCE_FONT_ERROR_INVALID_PARAMETER && !line); + allocations.fail = true; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_ERROR_ALLOCATION_FAILED && !line); + allocations.fail = false; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_OK && line); + Require(allocations.lastSize == 2208 && allocations.live == 1); void* ltr = nullptr; Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, <r) == SCE_FONT_OK && ltr != nullptr && ltr != line); void* rtl = nullptr; @@ -116,7 +145,11 @@ int main() { Require(sceFontWritingLineRefersRenderStep(line) == nullptr); Require(sceFontWritingLineWritesOrder(line, 0, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + allocations.fail = true; + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_ERROR_ALLOCATION_FAILED); + allocations.fail = false; Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_OK); + Require(allocations.lastSize == 64); Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runB, &b) == SCE_FONT_OK); Require(SpaceIs(line, 16.0f, 1.0f, 0.0f, 1.0f)); Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {})); @@ -222,7 +255,7 @@ int main() { float space = 7.0f; Require(sceFontWritingLineGetOrderingSpace(rtl, nullptr, nullptr, &space, nullptr) == SCE_FONT_OK && space == 15.0f); Require(Throws([&] { sceFontWritingLineGetOrderingSpace(rtl, &space, &space, &space, nullptr); })); - Require(Throws([&] { sceFontWritingLineGetOrderingSpace(nullptr, &space, nullptr, nullptr, nullptr); })); + Require(sceFontWritingLineGetOrderingSpace(nullptr, &space, nullptr, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER && space == 0.0f); Require(sceFontWritingLineGetRenderMetrics(nullptr, &metrics) == SCE_FONT_ERROR_INVALID_PARAMETER && Same(metrics, {})); Require(sceFontWritingLineGetRenderMetrics(rtl, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontWritingLineClear(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); @@ -230,22 +263,29 @@ int main() { int notALine = 0; Require(sceFontWritingLineRefersRenderStep(nullptr) == nullptr); Require(sceFontWritingLineClear(¬ALine) == SCE_FONT_ERROR_INVALID_WRITING); - Require(Throws([&] { sceFontWritingLineRefersRenderStep(¬ALine); })); - Require(Throws([&] { sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space); })); + Require(sceFontWritingLineRefersRenderStep(¬ALine) == nullptr); + space = 7.0f; + Require(sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space) == SCE_FONT_ERROR_INVALID_WRITING && space == 0.0f); metrics = runA; Require(sceFontWritingLineGetRenderMetrics(¬ALine, &metrics) == SCE_FONT_ERROR_INVALID_WRITING && Same(metrics, {})); Require(sceFontWritingLineWritesOrder(¬ALine, 0, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontWritingLineWritesOrder(¬ALine, Attribute(0), nullptr, &a) == SCE_FONT_ERROR_INVALID_WRITING); void* bogus = ¬ALine; - Require(Throws([&] { sceFontDestroyWritingLine(&bogus); }) && bogus == ¬ALine); + Require(sceFontDestroyWritingLine(&bogus) == SCE_FONT_ERROR_INVALID_WRITING && bogus == ¬ALine); + memory.mem_kind = 0; + Require(sceFontWritingLineClear(line) == SCE_FONT_ERROR_INVALID_WRITING); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontDestroyWritingLine(&line) == SCE_FONT_ERROR_INVALID_WRITING && line); + memory.mem_kind = 0xF00; void* destroyed = line; Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); - Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); - Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_OK); + Require(sceFontDestroyWritingLine(&line) == SCE_FONT_ERROR_INVALID_PARAMETER && line == nullptr); + Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontWritingLineClear(destroyed) == SCE_FONT_ERROR_INVALID_WRITING); Require(sceFontWritingLineWritesOrder(destroyed, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_WRITING); Require(sceFontDestroyWritingLine(<r) == SCE_FONT_OK && ltr == nullptr); Require(sceFontDestroyWritingLine(&rtl) == SCE_FONT_OK && rtl == nullptr); + Require(allocations.live == 0); std::puts("font writing line: ok"); } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index e6bfcbb5bf..585003d2a6 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -8,9 +8,9 @@ - [sceFontDettachDeviceCacheBuffer](../../../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. It detaches the cache buffer attached by `sceFontAttachDeviceCacheBuffer`, returns the buffer and its size, and reports `SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER` when no cache buffer is attached - [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): supplied PS5 code confirms the four-argument signatures, low attribute byte `0xFA`, embedding levels 0 through 126 and spacing bits 30/31. Invalid attributes, including zero, null orderers and null lines return `0x80460002`; invalid non-null line handles return `0x8046000A`. Metrics are optional. Native creation accepts only forms `0x11`/`0x12` and a null detail, validates font memory and allocates 2208 bytes through its callback; those creation and allocation rules remain unimplemented. +- [Writing-line ABI and allocation](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 layouts, creation forms, memory validation, allocator callbacks and error handling are recovered. The 2208-byte line and 64-byte orders use caller memory. Host registry bookkeeping still allocates separately; memory lifetime and concurrent destruction require caller synchronization. - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. -- [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here +- [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 confirms output order, calculations and zeroed outputs on invalid handles. A non-null head output with a null tail output causes an unchecked native write; the host throws instead. With no head output, the tail output remains untouched. - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. - [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown @@ -21,7 +21,7 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - 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 Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; 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. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. -- [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): ordering and spacing follow PS4 measurements; full PS5 algorithm equivalence remains unverified. PS5 validation for writes, clear and render metrics is implemented. Creation still accepts form `0x10` and detail `0x0FD5`, ignores font memory and allocates on the host, contrary to the supplied PS5 code. Destruction, step traversal and ordering-space errors still require comparison. Unsupported handles in those paths throw. +- [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From 7b97d7552c264cddacf733ef8be3dbd91e00d475 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:34:58 +0300 Subject: [PATCH 0700/1360] fix(hmd): restore device queries and reprojection setup signatures --- core/libs/prx/libSceHmd/Export.cpp | 51 ++++++++++++++++++++++-------- docs/dev/debt/libSceHmd.md | 21 ++++-------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 693276b1a2..fdb52dd1fe 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -3,6 +3,24 @@ #include <atomic> +struct HmdReprojectionInitializeParameter; +struct HmdReprojectionMultilayerParameter; +struct HmdDeviceInformation; +struct HmdDistortionParameters; +struct HmdInertialSensorData; +struct HmdStatusReport; + +struct HmdFieldOfView { + float angles[4]; +}; + +struct HmdAssemblyError { + float offsets[4]; +}; + +static_assert(sizeof(HmdFieldOfView) == 16); +static_assert(sizeof(HmdAssemblyError) == 16); + struct HmdVr2dData; struct HmdPuPositionParameter; struct HmdOpticalParameter; @@ -48,6 +66,13 @@ struct HmdMirroringAspect { std::uint16_t reserved2; }; +struct HmdMirroringAspectDebug { + std::uint16_t values[5]; + std::uint8_t mode; +}; + +static_assert(sizeof(HmdMirroringAspectDebug) == 12); +static_assert(offsetof(HmdMirroringAspectDebug, mode) == 10); static_assert(sizeof(HmdMirroringAspect) == 8); static_assert(offsetof(HmdMirroringAspect, aspect) == 2); @@ -219,7 +244,7 @@ int APS5_VABI sceHmdInternalMirroringModeSetAspect(const HmdMirroringAspect* asp return 0; } -int APS5_VABI sceHmdInternalDfuGetStatus(void) { +int APS5_VABI sceHmdInternalDfuGetStatus(std::uint64_t* status) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -339,7 +364,7 @@ int APS5_VABI sceHmdGetDistortionWorkMemorySize(void) { return 0x20000; } -int APS5_VABI sceHmdReprojectionSetDisplayBuffers(void) { +int APS5_VABI sceHmdReprojectionSetDisplayBuffers(std::int32_t videoOutHandle, std::int32_t firstBufferIndex, std::int32_t secondBufferIndex, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -354,7 +379,7 @@ int APS5_VABI sceHmdInternalCreateSharedMemory(void) { return 0; } -int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(void) { +int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(const HmdMirroringAspectDebug* aspect) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -432,13 +457,13 @@ int APS5_VABI sceHmdInternalSetM2LedOn(std::int32_t handle, std::uint32_t ledMas return 0; } -int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, void* fieldOfView) { +int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, HmdFieldOfView* fieldOfView) { (void)handle; if (!fieldOfView) return Hmd::ParameterNull; return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } -int APS5_VABI sceHmdReprojectionAddDisplayBuffer(void) { +int APS5_VABI sceHmdReprojectionAddDisplayBuffer(std::int32_t bufferIndex, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -458,12 +483,12 @@ int APS5_VABI sceHmdInternal3dAudioSendData(std::int32_t port, const void* data, return 0; } -int APS5_VABI sceHmdReprojectionInitialize(void) { +int APS5_VABI sceHmdReprojectionInitialize(const HmdReprojectionInitializeParameter* parameter, std::uint32_t mode, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmdInternalGetDeviceInformationByHandle(void) { +int APS5_VABI sceHmdInternalGetDeviceInformationByHandle(std::int32_t handle, HmdDeviceInformation* information) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -483,7 +508,7 @@ int APS5_VABI sceHmdInternalCrashReportReadData(void* buffer, std::uint32_t* byt return 0; } -int APS5_VABI sceHmdInternalGetDeviceInformation(void) { +int APS5_VABI sceHmdInternalGetDeviceInformation(HmdDeviceInformation* information) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -552,7 +577,7 @@ bool APS5_VABI sceHmdInternalIsGameVr2d(void) { return 0; } -int APS5_VABI sceHmdGetAssyError(void* data) { +int APS5_VABI sceHmdGetAssyError(HmdAssemblyError* data) { if (!data) return Hmd::ParameterNull; return initialized.load() ? Hmd::DeviceDisconnected : Hmd::NotInitialized; } @@ -692,7 +717,7 @@ int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(void) { return 0; } -int APS5_VABI sceHmdReprojectionStartMultilayer2(void) { +int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayerParameter* parameter, std::int32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -761,7 +786,7 @@ int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(void) { return 0; } -int APS5_VABI sceHmdGetDistortionParams(void) { +int APS5_VABI sceHmdGetDistortionParams(HmdDistortionParameters* parameters) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -821,7 +846,7 @@ int APS5_VABI sceHmdInternalSetDefaultLedData(void) { return 0; } -int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, void* data, std::int32_t count) { +int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, HmdInertialSensorData* data, std::uint32_t count) { (void)handle; (void)data; (void)count; @@ -848,7 +873,7 @@ int APS5_VABI sceHmdInternalSeparateModeSendAudio(const HmdScreenData* data) { return 0; } -int APS5_VABI sceHmdInternalGetStatusReport(void) { +int APS5_VABI sceHmdInternalGetStatusReport(std::int32_t handle, HmdStatusReport* report) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 2c4ba6528c..ab19b62b9b 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -10,33 +10,24 @@ - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count - [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionSetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature + +[Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. + +[Reprojection initialization and display-buffer signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered: initialization takes a parameter pointer, 32-bit mode and null reserved pointer; display-buffer setup takes a VideoOut handle, two buffer indices and reserved pointer, while adding a buffer takes an index and reserved pointer. `StartMultilayer2` takes a parameter pointer and 32-bit mode. Parameter fields remain incomplete. Protected bodies are unchanged. + +[Work-memory queries, capture start, separate-mode open and crash-report cancellation](../../../core/libs/prx/libSceHmd/Export.cpp) consume no arguments in the supplied native implementation. The mirroring aspect debug setter takes five 16-bit values followed by a byte mode; the individual scalar meanings remain unresolved. These argument lists are confirmed; protected bodies remain unimplemented. From 166a8aa3e5f6c5b077550ad087c05926801851bc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:36:17 +0300 Subject: [PATCH 0701/1360] fix(npentitlementaccess): reject unimplemented request lifecycle operations --- .../prx/libSceNpEntitlementAccess/Export.cpp | 6 +++--- core/libs/tests/GuestOfflineImports.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceNpEntitlementAccess.md | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index ecec40a1a2..717d50a1ed 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -119,11 +119,11 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara } int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t requestId) { - return 0; + throw std::logic_error("sceNpEntitlementAccessAbortRequest: asynchronous request tracking is unimplemented"); } int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t requestId) { - return 0; + throw std::logic_error("sceNpEntitlementAccessDeleteRequest: asynchronous request tracking is unimplemented"); } int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(NpEntitlementAccessTransactionId* transactionId) { @@ -131,7 +131,7 @@ int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(NpEntitlementAccessTra return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(std::int64_t requestId, std::int32_t* result, std::int32_t* remainingCount) { +int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(std::int64_t requestId, std::int32_t* result, std::int32_t* countResult) { return SCE_NP_ERROR_SIGNED_OUT; } diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 56dadb62ba..f89a850b63 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -9,6 +9,8 @@ #include <string_view> extern "C" { +int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t); +int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t); int APS5_VABI sceHttpSetCookieEnabled(int, int); int APS5_VABI sceHttpSendRequest(int, const void*, std::size_t); int APS5_VABI sceNpEntitlementAccessGetEntitlementKey( @@ -32,6 +34,22 @@ int APS5_VABI sceRudpEnd(); static void Require(bool value) { if (!value) std::abort(); } int main() { + for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { + bool abortFailed = false; + bool deleteFailed = false; + try { + sceNpEntitlementAccessAbortRequest(requestId); + } catch (const std::logic_error&) { + abortFailed = true; + } + try { + sceNpEntitlementAccessDeleteRequest(requestId); + } catch (const std::logic_error&) { + deleteFailed = true; + } + Require(abortFailed && deleteFailed); + } + constexpr int invalidValue = static_cast<int>(0x804311FE); constexpr int network = static_cast<int>(0x80431063); Require(sceHttpSetCookieEnabled(1, 0) == 0); diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index d6f56513d4..7d54ea4fe1 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -4,7 +4,7 @@ - [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort/delete still report success without tracking requests. +- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort/delete throw until request tracking is implemented. ### Functional From eede6587233d91db03e764e02d192a759bfbb3c9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:39:33 +0300 Subject: [PATCH 0702/1360] fix(hmd): restore reprojection submission argument slots --- core/libs/prx/libSceHmd/Export.cpp | 20 +++++++++++++------- docs/dev/debt/libSceHmd.md | 10 +++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index fdb52dd1fe..ab37d80f72 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -3,6 +3,12 @@ #include <atomic> +struct HmdReprojectionFrameParameter; +struct HmdReprojectionWideNearParameter; +struct HmdReprojection2dVrParameter; +struct HmdReprojectionPose; +struct HmdReprojectionOverlayParameter; +struct HmdReprojectionSubmissionParameter; struct HmdReprojectionInitializeParameter; struct HmdReprojectionMultilayerParameter; struct HmdDeviceInformation; @@ -214,7 +220,7 @@ int APS5_VABI sceHmdDistortionGetWorkMemorySize(void) { return 0x20000; } -int APS5_VABI sceHmdReprojectionStartWideNear(void) { +int APS5_VABI sceHmdReprojectionStartWideNear(const HmdReprojectionWideNearParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -293,7 +299,7 @@ int APS5_VABI sceHmdGetDistortionWorkMemoryAlign(void) { return 0x400; } -int APS5_VABI sceHmdReprojectionStartMultilayer(void) { +int APS5_VABI sceHmdReprojectionStartMultilayer(const HmdReprojectionMultilayerParameter* layers, std::uint32_t layerCount, const HmdReprojectionSubmissionParameter* submission, const HmdReprojectionPose* pose, std::uint64_t flipArg, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -647,7 +653,7 @@ int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(std::uint8_t* height) { return 0; } -int APS5_VABI sceHmdReprojectionStart(void) { +int APS5_VABI sceHmdReprojectionStart(const HmdReprojectionFrameParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -717,7 +723,7 @@ int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(void) { return 0; } -int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayerParameter* parameter, std::int32_t mode) { +int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayerParameter* layers, std::uint32_t layerCount, const HmdReprojectionSubmissionParameter* submission, std::uint64_t flipArg, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -761,7 +767,7 @@ int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void) { return 0x810; } -int APS5_VABI sceHmdReprojectionStartWithOverlay(void) { +int APS5_VABI sceHmdReprojectionStartWithOverlay(const HmdReprojectionFrameParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const HmdReprojectionOverlayParameter* overlay, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -781,7 +787,7 @@ int APS5_VABI sceHmdInternalSetDemoMode(std::uint8_t mode) { return 0; } -int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(void) { +int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(const HmdReprojectionWideNearParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const HmdReprojectionOverlayParameter* overlay, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -831,7 +837,7 @@ int APS5_VABI sceHmdInternalDfuReset(void) { return 0; } -int APS5_VABI sceHmdReprojectionStart2dVr(void) { +int APS5_VABI sceHmdReprojectionStart2dVr(const HmdReprojection2dVrParameter* parameter, std::uint64_t flipArg, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index ab19b62b9b..5c3af6e4ef 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -9,25 +9,21 @@ - [libSceHmd internal and capture exports](../../../core/libs/prx/libSceHmd/Export.cpp) - input/output directions and argument registers are recovered for shared VR2D data, device binding, optical and serial queries, screen status and capture operations. VR2D records are 128 bytes, optical output 16, serial output 16, PU position output 20, crash-dump output 68, audio/video status 32, and reprojection debug outputs 216 or 1136. Payload field meanings and capture callback contracts remain incomplete; these structures use forward declarations. - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count -- [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature [Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. -[Reprojection initialization and display-buffer signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered: initialization takes a parameter pointer, 32-bit mode and null reserved pointer; display-buffer setup takes a VideoOut handle, two buffer indices and reserved pointer, while adding a buffer takes an index and reserved pointer. `StartMultilayer2` takes a parameter pointer and 32-bit mode. Parameter fields remain incomplete. Protected bodies are unchanged. +[Reprojection initialization and display-buffer signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered: initialization takes a parameter pointer, 32-bit mode and null reserved pointer; display-buffer setup takes a VideoOut handle, two buffer indices and reserved pointer, while adding a buffer takes an index and reserved pointer. Parameter fields remain incomplete. Protected bodies are unchanged. [Work-memory queries, capture start, separate-mode open and crash-report cancellation](../../../core/libs/prx/libSceHmd/Export.cpp) consume no arguments in the supplied native implementation. The mirroring aspect debug setter takes five 16-bit values followed by a byte mode; the individual scalar meanings remain unresolved. These argument lists are confirmed; protected bodies remain unimplemented. + +[Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. From 0b5ba4470391f61f53e8e0283fcd8663223c65e8 Mon Sep 17 00:00:00 2001 From: krishalivaibhav <krishalivaibhav@gmail.com> Date: Fri, 16 Oct 2026 14:51:53 +0530 Subject: [PATCH 0703/1360] fix(agc): synchronize pending writes before command allocation --- core/libs/prx/libSceAgc/CMakeLists.txt | 16 ++++++++-------- core/libs/prx/libSceAgc/Command/src/Packet.cpp | 2 ++ core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +++++++ .../Execution/include/Driver.hpp | 2 ++ .../Execution/src/Driver/EntryPoints.cpp | 6 ++++++ .../libs/prx/libSceAgcDriver/tests/Recorder.cpp | 17 +++++++++++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index faf498b571..509369d73b 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -157,7 +157,7 @@ add_test_executable(agc_register_command_tests EXCLUDE_FROM_ALL Patch/src/IndirectRegisters.cpp ) target_include_directories(agc_register_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_register_command_tests PRIVATE libc) +target_link_libraries(agc_register_command_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_register_command_tests) if(BUILD_TESTING) set_target_properties(agc_register_command_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -176,7 +176,7 @@ add_test_executable(agc_cb_cond_write_tests EXCLUDE_FROM_ALL 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) +target_link_libraries(agc_cb_cond_write_tests PRIVATE libSceAgcDriver 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) @@ -220,7 +220,7 @@ add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL Patch/src/Branch.cpp ) target_include_directories(agc_branch_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_branch_patch_tests PRIVATE libc) +target_link_libraries(agc_branch_patch_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_branch_patch_tests) if(BUILD_TESTING) set_target_properties(agc_branch_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -240,7 +240,7 @@ add_test_executable(agc_predication_tests EXCLUDE_FROM_ALL Predication/src/Predication.cpp ) target_include_directories(agc_predication_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_predication_tests PRIVATE libc) +target_link_libraries(agc_predication_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_predication_tests) if(BUILD_TESTING) set_target_properties(agc_predication_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -357,7 +357,7 @@ add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL DcbFlow/src/Memory.cpp ) target_include_directories(agc_dma_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_dma_data_tests PRIVATE libc) +target_link_libraries(agc_dma_data_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_dma_data_tests) if(BUILD_TESTING) set_target_properties(agc_dma_data_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -377,7 +377,7 @@ add_test_executable(agc_write_data_patch_tests EXCLUDE_FROM_ALL 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) +target_link_libraries(agc_write_data_patch_tests PRIVATE libSceAgcDriver 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) @@ -397,7 +397,7 @@ add_test_executable(agc_prime_utcl2_tests EXCLUDE_FROM_ALL DcbState/src/Display.cpp ) target_include_directories(agc_prime_utcl2_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_prime_utcl2_tests PRIVATE libc) +target_link_libraries(agc_prime_utcl2_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_prime_utcl2_tests) if(BUILD_TESTING) set_target_properties(agc_prime_utcl2_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -418,7 +418,7 @@ add_test_executable(agc_cond_exec_patch_tests EXCLUDE_FROM_ALL Patch/src/CondExec.cpp ) target_include_directories(agc_cond_exec_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_cond_exec_patch_tests PRIVATE libc) +target_link_libraries(agc_cond_exec_patch_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_cond_exec_patch_tests) if(BUILD_TESTING) set_target_properties(agc_cond_exec_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 6ec0e12165..be43fb7028 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include <algorithm> @@ -67,6 +68,7 @@ void Reserve(CommandBuffer* buffer, std::uint32_t count, const char* function) { std::uint32_t* Allocate(CommandBuffer* buffer, std::uint32_t count, const char* function) { Reserve(buffer, count, function); auto* result = buffer->cursor_up; + AgcDriverPrepareCommandWrite_nid_no_patch(reinterpret_cast<std::uint64_t>(result), static_cast<std::size_t>(count) * sizeof(std::uint32_t)); buffer->cursor_up += count; return result; } diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d4f1ad05ce..5ccfb92cd3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1403,6 +1403,7 @@ target_include_directories(agc_driver_recorder_tests PRIVATE ) target_link_libraries(agc_driver_recorder_tests PRIVATE + libSceAgc libkernel libSceAgcDriver SDL2-static @@ -1422,6 +1423,12 @@ if(BUILD_TESTING) set_tests_properties(agc_recorder_device_selection agc_gpu_timing_complete agc_gpu_timing_incomplete PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_command_memory_reuse COMMAND agc_driver_recorder_tests --command-reuse-only) + set_tests_properties(agc_command_memory_reuse PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_command_memory_reuse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index b599b88f95..384c601ccc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -3,6 +3,7 @@ #include <cstdint> #include <span> +#include <cstddef> #include "SceTypes.hpp" #include "SceShaders.hpp" @@ -12,6 +13,7 @@ void Submit(const Packet* packet, std::uint32_t queue); } +extern "C" void AgcDriverPrepareCommandWrite_nid_no_patch(std::uint64_t address, std::size_t bytes); extern "C" void AgcDriverWaitIdle_nid_postfix(); extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role); extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 04a1f72a02..8e846cf2fa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" @@ -160,3 +161,8 @@ extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Sh } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } + +extern "C" void AgcDriverPrepareCommandWrite_nid_no_patch(std::uint64_t address, std::size_t bytes) { + if (AgcDriver::Graphics::Recorder::SnapshotWriteOverlaps(address, bytes)) + AgcDriver::GuestMemory::FlushGpuWrites(address, bytes); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 888c3485c1..1802b0c285 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "Triangle_frag_spv.h" #include "Triangle_vert_spv.h" @@ -1777,6 +1778,16 @@ void storeRunTests(const Device& device, Recorder& recorder) { Require(words[0] == 1 && words[1] == 2 && words[2] == 5 && words[4] == 4 && words[3] == 0, "the stores did not land (or the later store of a dword lost)"); const auto after = Recorder::StoreCounts(); Require(after.stores == before.stores + 1 && after.replaced == before.replaced + 0 && after.joined == before.joined + 0, "store counters are off"); + store(8, 1); + recorder.NotePendingWrite(address + 32, 4); + auto* commandWords = static_cast<std::uint32_t*>(block); + CommandBuffer disjoint{commandWords + 12, commandWords + 16, commandWords + 12, nullptr, nullptr, nullptr, 0}; + Agc::Command::Emit(&disjoint, 0x7a, {0x20000243, 0x400}, __func__); + Require(recorder.Recording(), "a disjoint command allocation drained the GPU store"); + CommandBuffer reused{commandWords + 6, commandWords + 10, commandWords + 6, nullptr, nullptr, nullptr, 0}; + Agc::Command::Emit(&reused, 0x7a, {0x20000243, 0x400}, __func__); + recorder.Sync(); + Require(words[8] == 0x400, "a pending GPU label overwrote a reused command word"); // The covered-access mask: reported once by the next Commands() call, then cleared. VkAccessFlags covered = 0xffffffffu; recorder.Commands(&covered); @@ -10138,6 +10149,12 @@ int main(int argc, char** argv) { std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && std::string_view(argv[1]) == "--command-reuse-only") { + if (device.GetContext().hostImportAlignment == 0) return 77; + const auto before = Recorder::StoreCounts().stores; + storeRunTests(device, recorder); + return Recorder::StoreCounts().stores != before ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") { copiedDirectOrderingTests(device, recorder); return 0; From 6703b32a3788600e89bd9c9fa6e927e74af0b3ac Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:41:53 +0300 Subject: [PATCH 0704/1360] fix(hmd): restore distortion command output and export aliases --- core/libs/prx/libSceHmd/Export.cpp | 26 +++++++++++++++++++------- docs/dev/debt/libSceHmd.md | 11 ++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index ab37d80f72..8bed93169b 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -3,6 +3,18 @@ #include <atomic> +struct HmdDistortionCommandParameter; +struct HmdWideNearDistortionCommandParameter; +struct Hmd2dVrCommandParameter; + +struct HmdCommandBuffer { + const std::uint32_t* commands; + std::uint32_t dwordCount; +}; + +static_assert(sizeof(HmdCommandBuffer) == 16); +static_assert(offsetof(HmdCommandBuffer, dwordCount) == 8); + struct HmdReprojectionFrameParameter; struct HmdReprojectionWideNearParameter; struct HmdReprojection2dVrParameter; @@ -419,7 +431,7 @@ int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void* buffer, std::uint return 0; } -int APS5_VABI sceHmdDistortionGetCorrectionCommand(void) { +int APS5_VABI sceHmdDistortionGetCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -553,7 +565,7 @@ int APS5_VABI sceHmdInternalGetHmuSerialNumber(std::uint8_t* serialNumber) { return 0; } -int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(void) { +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -608,7 +620,7 @@ int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(std::uint8_t height) { return 0; } -int APS5_VABI sceHmdDistortionInitialize(void) { +int APS5_VABI sceHmdDistortionInitialize(void* workMemory) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -708,7 +720,7 @@ int APS5_VABI sceHmdInternalMmapGetModeId(std::uint32_t* modeId) { return 0; } -int APS5_VABI sceHmdDistortionGet2dVrCommand(void) { +int APS5_VABI sceHmdDistortionGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -718,7 +730,7 @@ int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { return 0; } -int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(void) { +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -728,7 +740,7 @@ int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayer return 0; } -int APS5_VABI sceHmdGetDistortionCorrectionCommand(void) { +int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -943,7 +955,7 @@ int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { return 0x100000; } -int APS5_VABI sceHmdGet2dVrCommand(void) { +int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 5c3af6e4ef..6c4d37abd8 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -10,15 +10,8 @@ - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature [Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. @@ -27,3 +20,7 @@ [Work-memory queries, capture start, separate-mode open and crash-report cancellation](../../../core/libs/prx/libSceHmd/Export.cpp) consume no arguments in the supplied native implementation. The mirroring aspect debug setter takes five 16-bit values followed by a byte mode; the individual scalar meanings remain unresolved. These argument lists are confirmed; protected bodies remain unimplemented. [Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. + +[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Protected bodies are unchanged. + +The ELF export table aliases the [Distortion correction/2D command and work-memory queries](../../../core/libs/prx/libSceHmd/Export.cpp) to their corresponding HMD queries, confirming identical signatures. Distortion initialization consumes only a work-memory pointer. No shader code or calibration constants are copied; protected bodies remain unchanged. From 43398d5572330c9b55844f6572c0c30297660617 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:46:23 +0300 Subject: [PATCH 0705/1360] fix(pad): restore internal data and device control signatures --- core/libs/prx/libScePad/Export.cpp | 55 +++++++++++++++++++++--------- docs/dev/debt/libScePad.md | 19 ++--------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 4eab1575c4..b014e4013b 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -39,6 +39,29 @@ struct PadBlasterTrackerData; struct PadAstonTrackerData; struct PadUiData; struct PadUiTuningData; +struct PadLotusButtonData; +struct PadVersionInfo; +struct PadTelemetryInfo; +struct PadPortInfo; +struct PadVrControllerCombinedState; +struct PadLeonardoData; +struct PadLeonardoUniqueData; +struct PadHistoryData; +struct PadUpdateParameter; + +struct PadLightBarBlinkingParameter { + std::uint8_t values[3]; +}; + +struct PadButtonRemappingInfo { + std::uint8_t mapping[14]; + std::uint8_t reserved; + std::uint8_t mode; +}; + +static_assert(sizeof(PadLightBarBlinkingParameter) == 3); +static_assert(sizeof(PadButtonRemappingInfo) == 16); +static_assert(offsetof(PadButtonRemappingInfo, mode) == 15); namespace { @@ -92,7 +115,7 @@ int APS5_VABI scePadVrControllerSetVibrationMode(std::int32_t handle, std::uint3 return 0; } -int APS5_VABI scePadVrControllerReadCombinedState() { +int APS5_VABI scePadVrControllerReadCombinedState(std::int32_t leftHandle, std::int32_t rightHandle, PadVrControllerCombinedState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -381,7 +404,7 @@ int APS5_VABI scePadSetTriggerEffectExt(std::int32_t handle, const PadTriggerEff return 0; } -int APS5_VABI scePadLastLotusButtonData(void) { +int APS5_VABI scePadLastLotusButtonData(std::int32_t handle, std::uint32_t parameter, PadLotusButtonData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -410,17 +433,17 @@ int APS5_VABI scePadEnableSpecificDeviceClass(std::int32_t deviceClass) { return 0; } -int APS5_VABI scePadOpenExt2(void) { +int APS5_VABI scePadOpenExt2(std::uint64_t deviceId, std::int32_t type, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadDeviceOpen(void) { +int APS5_VABI scePadDeviceOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetDataInternal(void) { +int APS5_VABI scePadGetDataInternal(std::uint8_t port, PadData* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -445,7 +468,7 @@ int APS5_VABI scePadSetLightBarBaseBrightness(std::int32_t handle, std::uint32_t return 0; } -int APS5_VABI scePadSetLightBarBlinking(void) { +int APS5_VABI scePadSetLightBarBlinking(std::int32_t handle, const PadLightBarBlinkingParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -515,7 +538,7 @@ int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationPar return 0; } -int APS5_VABI scePadGetVersionInfo(void) { +int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -530,7 +553,7 @@ int APS5_VABI scePadDisableDataConversion(std::int32_t handle, bool disabled) { return 0; } -int APS5_VABI scePadSetButtonRemappingInfo(void) { +int APS5_VABI scePadSetButtonRemappingInfo(std::int32_t handle, const PadButtonRemappingInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -695,7 +718,7 @@ int APS5_VABI scePadVrControllerGetPhysicalUsbConnectionStatus(PadVrUsbConnectio return 0; } -int APS5_VABI scePadParseUniqueDataLeonardo(void) { +int APS5_VABI scePadParseUniqueDataLeonardo(std::int32_t handle, const PadLeonardoData* data, PadLeonardoUniqueData* uniqueData) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -785,7 +808,7 @@ int APS5_VABI scePadRegisterCollaboInputMgrProcess(void) { return 0; } -int APS5_VABI scePadGetTelemetryInfo(void) { +int APS5_VABI scePadGetTelemetryInfo(std::int32_t handle, PadTelemetryInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -800,7 +823,7 @@ int APS5_VABI scePadGetDeviceId(std::int32_t handle, std::int32_t* deviceId) { return 0; } -int APS5_VABI scePadGetInfoByPortType(void) { +int APS5_VABI scePadGetInfoByPortType(std::int32_t type, PadPortInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -995,7 +1018,7 @@ int APS5_VABI scePadGetLicenseControllerInformation(std::int32_t handle, PadLice return 0; } -int APS5_VABI scePadReadHistory(void) { +int APS5_VABI scePadReadHistory(std::int32_t handle, PadHistoryData* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1010,12 +1033,12 @@ int APS5_VABI scePadStopRecording(void) { return 0; } -int APS5_VABI scePadRemoteControlInsertData(void) { +int APS5_VABI scePadRemoteControlInsertData(const std::uint8_t* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadRemoteControlInsertDataForAston(void) { +int APS5_VABI scePadRemoteControlInsertDataForAston(const std::uint8_t* data) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1045,7 +1068,7 @@ int APS5_VABI scePadBondUpdateIsReady(std::int32_t deviceId, std::int32_t* ready return 0; } -int APS5_VABI scePadBondUpdateStart(void) { +int APS5_VABI scePadBondUpdateStart(std::int32_t handle, const PadUpdateParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1090,7 +1113,7 @@ int APS5_VABI scePadControllerUpdateIsReady(std::int32_t deviceId, std::int32_t* return 0; } -int APS5_VABI scePadControllerUpdateStart(void) { +int APS5_VABI scePadControllerUpdateStart(std::int32_t handle, const PadUpdateParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 81f69445a0..1a6563e40e 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -10,33 +10,20 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown -- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [scePadLastLotusButtonData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadOpenExt2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetLightBarBlinking](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetButtonRemappingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadParseUniqueDataLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadVirtualDeviceInsertTriggerInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadSetVirtualDeviceComponentInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadReadHistory](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadRemoteControlInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadRemoteControlInsertDataForAston](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadBondUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadControllerUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature + +- [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. ### Functional From 4a84bfe06bd48db9fcb455189b5528e114e748f4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:47:43 +0300 Subject: [PATCH 0706/1360] fix(pad): restore profile writes and virtual device parameters --- core/libs/prx/libScePad/Export.cpp | 27 ++++++++++++++++++++------- docs/dev/debt/libScePad.md | 9 ++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index b014e4013b..d66adfa338 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -48,6 +48,19 @@ struct PadLeonardoData; struct PadLeonardoUniqueData; struct PadHistoryData; struct PadUpdateParameter; +struct PadVirtualDeviceParameter; +struct PadVirtualDeviceId; +struct PadVirtualDeviceComponentInformation; + +struct PadVirtualTriggerInternalState { + std::uint32_t states[2]; + std::uint8_t mode; + std::uint8_t flags[2]; +}; + +static_assert(sizeof(PadVirtualTriggerInternalState) == 12); +static_assert(offsetof(PadVirtualTriggerInternalState, mode) == 8); +static_assert(offsetof(PadVirtualTriggerInternalState, flags) == 9); struct PadLightBarBlinkingParameter { std::uint8_t values[3]; @@ -668,7 +681,7 @@ int APS5_VABI scePadGetProfileHunt(std::int32_t handle, std::int32_t profileId, return 0; } -int APS5_VABI scePadSetProfileHunt(void) { +int APS5_VABI scePadSetProfileHunt(std::int32_t handle, std::int32_t profileId, PadHuntProfile* profile, std::uint32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -738,7 +751,7 @@ int APS5_VABI scePadGetProfileLeonardo(std::int32_t handle, std::int32_t profile return 0; } -int APS5_VABI scePadSetProfileLeonardo(void) { +int APS5_VABI scePadSetProfileLeonardo(std::int32_t handle, std::int32_t profileId, PadLeonardoProfile* profile, std::uint32_t* result) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -953,12 +966,12 @@ int APS5_VABI scePadSetProcessFocus(const PadProcessFocusParameter* parameter) { return 0; } -int APS5_VABI scePadVertualDeviceAddDevice(void) { +int APS5_VABI scePadVertualDeviceAddDevice(const PadVirtualDeviceParameter* parameter, PadVirtualDeviceId* deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVirtualDeviceAddDevice(void) { +int APS5_VABI scePadVirtualDeviceAddDevice(const PadVirtualDeviceParameter* parameter, PadVirtualDeviceId* deviceId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -968,12 +981,12 @@ int APS5_VABI scePadVirtualDeviceDeleteDevice(std::int32_t deviceId) { return 0; } -int APS5_VABI scePadVirtualDeviceInsertData(void) { +int APS5_VABI scePadVirtualDeviceInsertData(std::int32_t deviceId, const PadData* data) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(void) { +int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(std::int32_t deviceId, const PadVirtualTriggerInternalState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -1003,7 +1016,7 @@ int APS5_VABI scePadSetPS4BcVibrationMode(std::uint8_t mode) { return 0; } -int APS5_VABI scePadSetVirtualDeviceComponentInformation(void) { +int APS5_VABI scePadSetVirtualDeviceComponentInformation(std::int32_t deviceId, const PadVirtualDeviceComponentInformation* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 1a6563e40e..10146571bc 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -15,16 +15,11 @@ - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [scePadSetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadVirtualDeviceInsertTriggerInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadSetVirtualDeviceComponentInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. +- [libScePad profile writes and virtual devices](../../../core/libs/prx/libScePad/Export.cpp) - profile writes take a handle, profile ID, profile pointer and a 32-bit result pointer; the profile payload direction and fields remain unresolved. Virtual-device creation takes an input record and an output ID pointer, whose width is not exposed by the wrapper. Component information copies 16 bytes; field meanings remain unknown. Virtual trigger state contains two 32-bit states and three control bytes, but the control semantics remain incomplete. + ### Functional - [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. The console's values are not measured From bd7a73eb58fd66b4d2d301b11faa54deeb1575bf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:49:23 +0300 Subject: [PATCH 0707/1360] fix(http): restore cookie and socket callback signatures --- core/libs/prx/libSceHttp/Export.cpp | 6 ++++-- docs/dev/debt/libSceHttp.md | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index e6e88e47a5..1cde24b012 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -27,6 +27,8 @@ static std::atomic<int> g_nextHandle{1}; struct HttpConnectionStat; struct HttpRequestStat; +using HttpCookieSendCallback = int (APS5_VABI *)(std::int32_t, const char*, const char*, void*); +using HttpSocketCreationCallback = void (APS5_VABI *)(std::int32_t, std::int32_t, void*); using HttpRequestStatusCallback = void (APS5_VABI *)(int, int, void*); struct RequestStatusRegistration { HttpRequestStatusCallback callback; @@ -759,7 +761,7 @@ int APS5_VABI sceHttpAddCookie(int contextId, const char* url, const char* cooki return 0; } -int APS5_VABI sceHttpSetCookieSendCallback(void) { +int APS5_VABI sceHttpSetCookieSendCallback(std::int32_t id, HttpCookieSendCallback callback, void* userArg) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -825,7 +827,7 @@ int APS5_VABI sceHttpSetProxy(int id, int mode, int authentication, const char* return 0; } -int APS5_VABI sceHttpSetSocketCreationCallback(void) { +int APS5_VABI sceHttpSetSocketCreationCallback(std::int32_t id, HttpSocketCreationCallback callback, void* userArg) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 59b4ddb5b2..58871b5572 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -29,8 +29,6 @@ - [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. -- [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature ### Functional From cd175c3aad990a30166314107af75e8f61caaedf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:49:24 +0300 Subject: [PATCH 0708/1360] fix(hmd): restore default led data setter arguments --- core/libs/prx/libSceHmd/Export.cpp | 8 +++++++- docs/dev/debt/libSceHmd.md | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 8bed93169b..6f28a18447 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -116,6 +116,12 @@ static_assert(offsetof(HmdScreenData, size) == 8); static_assert(sizeof(HmdEyeDistances) == 8); static_assert(sizeof(HmdEyeOffset) == 12); +struct HmdLedData { + std::uint8_t values[9]; +}; + +static_assert(sizeof(HmdLedData) == 9); + extern "C" { std::int32_t APS5_VABI sceHmdInitialize(const Hmd::InitializeParam* param) { @@ -859,7 +865,7 @@ int APS5_VABI sceHmdInternalMmapGetCount(void) { return 0; } -int APS5_VABI sceHmdInternalSetDefaultLedData(void) { +int APS5_VABI sceHmdInternalSetDefaultLedData(std::int32_t handle, std::uint32_t mask, const HmdLedData* data) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 6c4d37abd8..76f7c2767d 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -11,7 +11,6 @@ - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature [Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. From ea086b23745cb167780f5aed0a3b9600a4e8dd0a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:52:48 +0300 Subject: [PATCH 0709/1360] fix(netctl): restore initialization and callback validation --- core/libs/SceTypes.hpp | 7 +++- core/libs/prx/libSceNetCtl/Export.cpp | 34 ++++++++++++------ core/libs/tests/GuestNetCtl.cpp | 50 ++++++++++++++++++++++++--- core/libs/tests/GuestSceNet.cpp | 2 ++ docs/dev/debt/libSceNetCtl.md | 6 ++-- 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 9c81747059..a85ce12b51 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1334,7 +1334,12 @@ struct NetEpollEvent { NetEpollData data; }; -struct NetCtlNatInfo { std::uint8_t opaque[128]; }; +struct NetCtlNatInfo { + std::uint32_t size; + std::uint8_t opaque[12]; +}; + +static_assert(sizeof(NetCtlNatInfo) == 16); union NetCtlInfo { std::uint8_t opaque[256]; }; diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index c3a558cda9..aea249a719 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -19,6 +19,7 @@ #include <sys/sysctl.h> #endif #endif +#include <atomic> #include <cstdint> #include <cstddef> #include <cstdio> @@ -45,6 +46,9 @@ struct NetCtlNetEvConfiguration; struct NetBweInfo; struct NetCtlWifiInfo; +static constexpr int SCE_NET_CTL_ERROR_NOT_INITIALIZED = static_cast<int>(0x80412101); +static constexpr int SCE_NET_CTL_ERROR_ID_NOT_FOUND = static_cast<int>(0x80412104); +static constexpr int SCE_NET_CTL_ERROR_INVALID_SIZE = static_cast<int>(0x80412111); static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast<int>(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast<int>(0x80412105); static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast<int>(0x80412106); @@ -91,11 +95,13 @@ struct CallbackSlot { int lastEvent = 0; }; +static std::atomic_bool g_initialized{false}; static std::mutex g_callbackLock; static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; std::lock_guard lock(g_callbackLock); for (int index = 0; index < MAX_CALLBACKS; ++index) { @@ -109,8 +115,10 @@ static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCall } static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; std::lock_guard lock(g_callbackLock); + if (!callbacks[cid].func) return SCE_NET_CTL_ERROR_ID_NOT_FOUND; callbacks[cid] = {}; return 0; } @@ -420,6 +428,7 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!HasIpv4Callback()) return 0; const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; CallbackSlot due[MAX_CALLBACKS]; @@ -437,6 +446,7 @@ int APS5_VABI sceNetCtlCheckCallback(void) { } int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR; if (!HostHasAddress(AF_UNSPEC)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; *stat = {}; @@ -445,6 +455,7 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE; HostIpv4Interface host; @@ -476,31 +487,35 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { return 0; } -int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { - if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; +int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (info->size != sizeof(NetCtlNatInfo)) return SCE_NET_CTL_ERROR_INVALID_SIZE; + throw std::logic_error("sceNetCtlGetNatInfo: NAT discovery is not implemented"); } -int APS5_VABI sceNetCtlGetResult(int event_type, int* error_code) { - (void)event_type; - if (!error_code) return SCE_NET_CTL_ERROR_INVALID_ADDR; - *error_code = 0; - return 0; +int APS5_VABI sceNetCtlGetResult(int event, int* result) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; + if (!result) return SCE_NET_CTL_ERROR_INVALID_ADDR; + throw std::logic_error("sceNetCtlGetResult: service event results are not implemented"); } int APS5_VABI sceNetCtlGetState(int* state) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!state) return SCE_NET_CTL_ERROR_INVALID_ADDR; *state = HostHasAddress(AF_UNSPEC) ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED; return 0; } int APS5_VABI sceNetCtlGetStateV6(int* state) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!state) return SCE_NET_CTL_ERROR_INVALID_ADDR; *state = HostHasAddress(AF_INET6) ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED; return 0; } int APS5_VABI sceNetCtlInit(void) { + g_initialized.store(true); return 0; } @@ -520,9 +535,6 @@ int APS5_VABI sceNetCtlGetInfoV6(int code, void* info) { } void APS5_VABI sceNetCtlTerm(void) { - std::lock_guard lock(g_callbackLock); - for (auto& slot : g_callbacks) slot = {}; - for (auto& slot : g_callbacksV6) slot = {}; } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index ea68f8eb02..aa2c54300b 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -32,6 +32,7 @@ struct NetCtlDefaultRoute { #endif extern "C" { +int APS5_VABI sceNetCtlInit(void); int APS5_VABI sceNetCtlCheckCallback(void); int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); @@ -141,13 +142,19 @@ void CheckCallbacks() { Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); sceNetCtlTerm(); Require(sceNetCtlCheckCallback() == 0); - Require(g_first.calls == 1 && g_second.calls == 1); + Require(g_first.calls == 1 && g_second.calls == 2); + Require(sceNetCtlInit() == 0); + Require(sceNetCtlUnregisterCallback(first) == 0); + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6) == 0); + Require(sceNetCtlUnregisterCallback(first) == static_cast<int>(0x80412104)); + Require(sceNetCtlUnregisterCallbackV6(v6) == static_cast<int>(0x80412104)); Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); - Require(g_second.calls == 1 && g_self.calls == 1 && g_v6.calls == 0); + Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 0); Require(sceNetCtlUnregisterCallback(first) == 0); Require(sceNetCtlUnregisterCallbackV6(v6) == 0); } @@ -263,6 +270,20 @@ int main() { #ifdef __APPLE__ CheckDefaultRoutes(); #endif + constexpr int notInitialized = static_cast<int>(0x80412101); + Require(sceNetCtlCheckCallback() == notInitialized); + Require(sceNetCtlGetState(nullptr) == notInitialized); + Require(sceNetCtlGetStateV6(nullptr) == notInitialized); + Require(sceNetCtlGetInfo(0, nullptr) == notInitialized); + Require(sceNetCtlGetResult(0, nullptr) == notInitialized); + Require(sceNetCtlGetNatInfo(nullptr) == notInitialized); + Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == notInitialized); + Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == notInitialized); + Require(sceNetCtlUnregisterCallback(-1) == notInitialized); + Require(sceNetCtlUnregisterCallbackV6(-1) == notInitialized); + sceNetCtlTerm(); + Require(sceNetCtlCheckCallback() == notInitialized); + Require(sceNetCtlInit() == 0); CheckCallbacks(); Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); @@ -271,7 +292,13 @@ int main() { Require(stateV6 == 0 || stateV6 == kStateIpObtained); Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr); int errorCode = -1; - Require(sceNetCtlGetResult(0, &errorCode) == 0 && errorCode == 0); + bool resultUnsupported = false; + try { + sceNetCtlGetResult(0, &errorCode); + } catch (const std::logic_error&) { + resultUnsupported = true; + } + Require(resultUnsupported && errorCode == -1); Require(sceNetCtlGetResultV6(kEventIpObtained, nullptr) == kErrInvalidAddr); for (const int eventType : {kEventDisconnected, 2, kEventIpObtained}) { int errorCodeV6 = -1; @@ -335,6 +362,19 @@ int main() { } Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); - NetCtlNatInfo nat_info{}; - Require(sceNetCtlGetNatInfo(&nat_info) == kErrNotConnected); + NetCtlNatInfo natInfo{}; + for (const auto size : {0u, 15u, 17u, 128u, 0xffffffffu}) { + natInfo.size = size; + Require(sceNetCtlGetNatInfo(&natInfo) == static_cast<int>(0x80412111)); + } + natInfo.size = sizeof(natInfo); + std::memset(natInfo.opaque, kFill, sizeof(natInfo.opaque)); + bool natUnsupported = false; + try { + sceNetCtlGetNatInfo(&natInfo); + } catch (const std::logic_error&) { + natUnsupported = true; + } + Require(natUnsupported && natInfo.size == sizeof(natInfo)); + for (const auto value : natInfo.opaque) Require(value == kFill); } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3f775bd82e..fd59160c0c 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -75,6 +75,7 @@ int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetClearDnsCache(int); int APS5_VABI sceNetGetDnsInfo(std::uint32_t*, int); +int APS5_VABI sceNetCtlInit(void); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); @@ -1874,6 +1875,7 @@ int main() { if (!ipv6Unavailable) CheckStreamSender(28); int state = -1; + Require(sceNetCtlInit() == 0); Require(sceNetCtlGetState(&state) == 0); Require(state == 0 || state == 3); Require(sceNetCtlGetIfStat(nullptr) == static_cast<int>(0x80412107)); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 5c9e32df16..5c6533f6ff 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -1,8 +1,8 @@ # libSceNetCtl technical debt -### Silent stubs +### Functional -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `sceNetCtlGetNatInfo` validates null pointers and returns `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) without performing NAT discovery. +- [sceNetCtlGetNatInfo and sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) - NAT discovery and service event results are not implemented and throw after native initialization and argument checks. NAT information is 16 bytes with a 32-bit size prefix; the remaining fields are unresolved. ### Unknown function info @@ -25,6 +25,6 @@ ### Functional -- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` or `sceNetCtlTerm` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event, because no source says which call delivers them +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event: native `sceNetCtlCheckCallback` also drains the IPv6 service queue, which the host backend does not model - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB From 3985ae95cb2d712d6a688178b1cfdd994cbf6846 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:54:35 +0300 Subject: [PATCH 0710/1360] fix(http): match native header parsing and uri decoding --- core/libs/prx/libSceHttp/src/Uri.cpp | 52 ++++++++++++++++------------ core/libs/tests/GuestHttp.cpp | 17 +++++++-- docs/dev/debt/libSceHttp.md | 2 +- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 670a29e7eb..b03920909b 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -228,34 +228,40 @@ int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const return copyOut(escaped, out, require, prepare); } -int APS5_VABI sceHttpUriUnescape(char* out, size_t* require, size_t prepare, const char* in) { - if (!in || (!out && !require)) return ERROR_INVALID_VALUE; +int APS5_VABI sceHttpUriUnescape(char* out, size_t* requiredSize, size_t capacity, const char* input) { + if (!input || (!out && !requiredSize)) return ERROR_INVALID_VALUE; - const auto hexDigit = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; + const std::string_view source(input); + const auto decode = [](const char* digits, unsigned int& value) { + const auto result = std::from_chars(digits, digits + 2, value, 16); + return result.ec == std::errc{} && result.ptr == digits + 2; }; - const std::string_view input(in); - std::string decoded; - for (size_t i = 0; i < input.size(); ++i) { - if (input[i] != '%') { - decoded.push_back(input[i]); - continue; + for (size_t index = 0; index < source.size(); ++index) { + if (source[index] != '%') continue; + unsigned int value = 0; + if (source.size() - index < 3 || !decode(source.data() + index + 1, value)) return ERROR_INVALID_URL; + index += 2; + } + + size_t written = 0; + for (size_t index = 0; index < source.size(); ++index) { + if (out && written >= capacity) return ERROR_OUT_OF_MEMORY; + char value = source[index]; + if (value == '%') { + unsigned int decoded = 0; + decode(source.data() + index + 1, decoded); + value = static_cast<char>(decoded); + index += 2; } - if (input.size() - i < 3) return ERROR_INVALID_URL; - const int high = hexDigit(input[i + 1]); - const int low = hexDigit(input[i + 2]); - if (high < 0 || low < 0) return ERROR_INVALID_URL; - decoded.push_back(static_cast<char>(high * 16 + low)); - i += 2; + if (out) out[written] = value; + ++written; } - if (out && prepare < decoded.size() + 1) { - std::memcpy(out, decoded.data(), std::min(prepare, decoded.size())); - return ERROR_OUT_OF_MEMORY; + if (out) { + if (written >= capacity) return ERROR_OUT_OF_MEMORY; + out[written] = '\0'; } - return copyOut(decoded, out, require, prepare); + if (requiredSize) *requiredSize = written + 1; + return 0; } int APS5_VABI sceHttpUriMerge(char* merged_url, const char* url, const char* relative_uri, size_t* require, size_t prepare, uint32_t option) { diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 93eb0119d3..0e7dba5121 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -209,9 +209,9 @@ int main() { const std::size_t size = std::strlen(row[1]) + 1; Require(sceHttpUriUnescape(nullptr, &required, 0, row[0]) == 0 && required == size); std::memset(escaped, 'Z', sizeof(escaped)); - required = 77; - Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == 77); - Require(std::string_view(escaped, size - 1) == row[1] && escaped[size - 1] == 'Z'); + required = 123; + Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == 123); + Require(std::memcmp(escaped, row[1], size - 1) == 0 && escaped[size - 1] == 'Z'); Require(sceHttpUriUnescape(escaped, nullptr, size, row[0]) == 0); Require(Equal(escaped, row[1]) && escaped[size] == 'Z'); } @@ -231,6 +231,17 @@ int main() { Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), nullptr) == invalidValue); Require(required == 123 && Equal(escaped, "A/%20")); + for (const char* malformed : {"%", "%1", "%1g", "%gg", "%+1", "%-1", "prefix%4", "prefix%xy"}) { + std::memset(escaped, 'Z', sizeof(escaped)); + required = 123; + Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), malformed) == static_cast<int>(0x80433060)); + Require(required == 123); + for (char value : escaped) Require(value == 'Z'); + } + std::memset(escaped, 'Z', sizeof(escaped)); + Require(sceHttpUriUnescape(escaped, &required, 2, "a%20b") == outOfMemory); + Require(required == 123 && escaped[0] == 'a' && escaped[1] == ' ' && escaped[2] == 'Z'); + char bytes[256], encodedBytes[766], decodedBytes[256]; for (std::size_t i = 1; i < 256; ++i) bytes[i - 1] = static_cast<char>(i); bytes[255] = '\0'; diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 58871b5572..798393a60b 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -15,7 +15,7 @@ - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end 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. 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); decoding and error codes follow a payload process on a PS5 (firmware 13.60). A buffer one byte short of the decoded size got the decoded bytes without a terminator and left `require` alone; a shorter buffer is assumed to get the first `prepare` bytes the same way +- [sceHttpUriUnescape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - the legacy SDK character whitelist is not selected by the host backend. Percent validation, decoding, partial writes on insufficient capacity and success-only size output follow the supplied PS5 module; arbitrary non-percent bytes follow its modern SDK path. - [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before - [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 - [sceHttpSetRequestStatusCallback](../../../core/libs/prx/libSceHttp/Export.cpp) - the three-argument ABI follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). Registrations and user data are retained and removed on request deletion; the offline backend never dispatches callbacks. Invalid ids return `0x80431100`, an uncited assumption, not a verified console error. Handle hierarchy, inheritance and deleted-handle validation remain incomplete; callback statuses and dispatch timing are unknown. From 7eac0fbbac59a79f244bac6e638d9928172e90ed Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:55:55 +0300 Subject: [PATCH 0711/1360] fix(http): restore uri builder limits and output rules --- core/libs/prx/libSceHttp/src/Uri.cpp | 45 +++++++++++++++++----------- core/libs/tests/GuestHttp.cpp | 26 +++++++++++++++- docs/dev/debt/libSceHttp.md | 1 - 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index b03920909b..0f0e8d3660 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -181,31 +181,42 @@ int copyOut(const std::string& value, char* out, size_t* require, size_t prepare extern "C" { -int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const SceHttpUriElement* src_element, uint32_t option) { - if (!src_element) return ERROR_INVALID_URL; - if (!out && !require) return ERROR_INVALID_VALUE; +int APS5_VABI sceHttpUriBuild(char* out, size_t* requiredSize, size_t capacity, const SceHttpUriElement* element, uint32_t option) { + if (!element) return ERROR_INVALID_URL; + if (!out && !requiredSize) return ERROR_INVALID_VALUE; - auto field = [](const char* value) { return value ? std::string_view(value) : std::string_view(); }; - const std::string_view scheme = field(src_element->scheme); - const std::string_view username = option & URI_BUILD_WITH_USERNAME ? field(src_element->username) : std::string_view(); - const std::string_view password = option & URI_BUILD_WITH_PASSWORD ? field(src_element->password) : std::string_view(); - const std::string_view hostname = option & URI_BUILD_WITH_HOSTNAME ? field(src_element->hostname) : std::string_view(); + const auto field = [](const char* value, size_t limit) { + size_t length = 0; + if (value) while (length < limit && value[length] != '\0') ++length; + return value ? std::string_view(value, length) : std::string_view(); + }; + const auto scheme = option & (URI_BUILD_WITH_SCHEME | URI_BUILD_WITH_PORT) ? field(element->scheme, 32) : std::string_view(); + const auto username = option & URI_BUILD_WITH_USERNAME ? field(element->username, 256) : std::string_view(); + const auto password = option & URI_BUILD_WITH_PASSWORD ? field(element->password, 256) : std::string_view(); + const auto hostname = option & URI_BUILD_WITH_HOSTNAME ? field(element->hostname, 255) : std::string_view(); std::string uri; if (option & URI_BUILD_WITH_SCHEME && !scheme.empty()) uri.append(scheme).push_back(':'); - if (!src_element->opaque) uri.append("//"); + if (element->opaque != 1) uri.append("//"); uri.append(username); if (!password.empty()) uri.append(":").append(password); if (!username.empty() || !password.empty()) uri.push_back('@'); - if (hostname.find(':') != std::string_view::npos) uri.append("[").append(hostname).append("]"); - else uri.append(hostname); - if (option & URI_BUILD_WITH_PORT && src_element->port != 0 && src_element->port != defaultPort(scheme)) { - uri.append(":").append(std::to_string(src_element->port)); + uri.append(hostname); + if (option & URI_BUILD_WITH_PORT && element->port != 0) { + std::uint16_t port = 0; + if (scheme.size() >= 5 && equalsIgnoreCase(scheme.substr(0, 5), "https")) port = 443; + else if ((scheme.size() >= 4 && equalsIgnoreCase(scheme.substr(0, 4), "http")) || (scheme.size() >= 3 && equalsIgnoreCase(scheme.substr(0, 3), "ttp"))) port = 80; + if (element->port != port) uri.append(":").append(std::to_string(element->port)); } - if (option & URI_BUILD_WITH_PATH) uri.append(field(src_element->path)); - if (option & URI_BUILD_WITH_QUERY) uri.append(field(src_element->query)); - if (option & URI_BUILD_WITH_FRAGMENT) uri.append(field(src_element->fragment)); - return copyOut(uri, out, require, prepare); + if (option & URI_BUILD_WITH_PATH) uri.append(field(element->path, URI_MAX_LENGTH)); + if (option & URI_BUILD_WITH_QUERY) uri.append(field(element->query, URI_MAX_LENGTH)); + if (option & URI_BUILD_WITH_FRAGMENT) uri.append(field(element->fragment, URI_MAX_LENGTH)); + const size_t size = uri.size() + 1 + ((option & URI_BUILD_WITH_SCHEME) && element->scheme && scheme.empty()); + if (requiredSize) *requiredSize = size; + if (!out) return 0; + if (capacity < size) return ERROR_OUT_OF_MEMORY; + std::memcpy(out, uri.c_str(), uri.size() + 1); + return 0; } int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const char* in) { diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 0e7dba5121..b340eb5a64 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -153,7 +153,7 @@ int main() { Require(sceHttpUriParse(&element, "http://[::1]:8080/index.html", pool, nullptr, sizeof(pool)) == 0); Require(Equal(element.hostname, "::1") && element.port == 8080 && Equal(element.path, "/index.html")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); - Require(Equal(built, "http://[::1]:8080/index.html")); + Require(Equal(built, "http://::1:8080/index.html")); Require(sceHttpUriParse(&element, "mailto:someone@example.com", pool, nullptr, sizeof(pool)) == 0); Require(element.opaque != 0 && element.port == 0); @@ -177,6 +177,30 @@ int main() { Require(sceHttpUriBuild(built, nullptr, sizeof(built), nullptr, buildAll) == invalidUrl); Require(sceHttpUriBuild(nullptr, nullptr, 0, &element, buildAll) == invalidValue); + { + SceHttpUriElement bounded{}; + bounded.opaque = 2; + char scheme[33]; + char hostname[256]; + std::memset(scheme, 's', sizeof(scheme)); + std::memset(hostname, 'h', sizeof(hostname)); + bounded.scheme = scheme; + bounded.hostname = hostname; + char output[512]; + Require(sceHttpUriBuild(output, &required, sizeof(output), &bounded, 3) == 0); + Require(required == 32 + 1 + 2 + 255 + 1); + Require(output[32] == ':' && output[33] == '/' && output[34] == '/'); + Require(output[required - 1] == '\0'); + scheme[0] = '\0'; + Require(sceHttpUriBuild(output, &required, sizeof(output), &bounded, 1) == 0); + Require(required == 4 && Equal(output, "//")); + char prefix[] = "HTTPS-extra"; + bounded.scheme = prefix; + bounded.port = 443; + Require(sceHttpUriBuild(output, &required, sizeof(output), &bounded, 4) == 0); + Require(required == 3 && Equal(output, "//")); + } + char escaped[64]; Require(sceHttpUriEscape(nullptr, &required, 0, "a b/~\xC3\xA9") == 0); Require(required == std::strlen("a%20b%2F~%C3%A9") + 1); diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 798393a60b..b65ff78602 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -10,7 +10,6 @@ - [HTTP object options](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - a template starts with redirects followed, blocking calls and no `Accept-Encoding: gzip`, and a connection or request copies its parent's options when it is created. The signatures of the option calls, these defaults, the copying and the order of the checks (`BEFORE_INIT` first; the last `sceHttpTerm` ends every object's options) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4) and are not measured on a PS5. `sceHttpSetDefaultAcceptEncodingGZIPEnabled` sets the gzip value for templates created later in that context, where shadPS4 keeps one value for the library, and `sceHttpUnsetEpoll` takes a template or connection id, where shadPS4 looks only at requests. The options change no request, as none reaches the network - [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - connection and request diagnostic signatures are recovered; their outputs contain four and three 32-bit words, respectively. Field semantics remain incomplete. Epoll setters/getters distinguish a 32-bit epoll ID from a pointer-sized handle, and use a pointer-sized user argument. The supplied firmware getter dispatch does not handle the pointer-handle selector used by GetEpoll, so its intended successful behavior is not recoverable from this implementation. -- [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end 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 3fff17d08ec6dab71a920f59f264bf498960f105 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:04:30 +0300 Subject: [PATCH 0712/1360] fix(vrtracker2): restore memory modes and initialization layout --- core/libs/prx/libSceVrTracker2/Export.cpp | 62 +++++++++++++++++++---- core/libs/tests/GuestVrTracker2.cpp | 27 ++++++++++ docs/dev/debt/libSceVrTracker2.md | 5 +- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index b850d89f71..3e2444861a 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -4,8 +4,43 @@ #include <stdexcept> #include <string> #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" -struct VrTracker2InitializeParameter; +struct VrTracker2InitializeParameter { + std::uint32_t size; + std::uint32_t mode; + std::int32_t threadPriorities[2]; + std::uint64_t threadAffinityMasks[2]; + void* workMemory; + std::uint32_t workMemorySize; + std::uint32_t workMemoryAlignment; + std::uint32_t option; + std::uint32_t coordinateSystem; + std::uint8_t disableOption; + std::int8_t optionMode; + std::uint8_t reserved[14]; +}; + +struct VrTracker2MemoryParameter { + std::uint32_t size; + std::uint32_t mode; + std::uint8_t reserved[24]; +}; + +struct VrTracker2MemoryRequirement { + std::uint32_t size; + std::uint32_t memorySize; + std::uint32_t memoryAlignment; + std::uint8_t reserved[20]; +}; + +static_assert(sizeof(VrTracker2InitializeParameter) == 72); +static_assert(offsetof(VrTracker2InitializeParameter, threadAffinityMasks) == 16); +static_assert(offsetof(VrTracker2InitializeParameter, workMemory) == 32); +static_assert(offsetof(VrTracker2InitializeParameter, coordinateSystem) == 52); +static_assert(offsetof(VrTracker2InitializeParameter, reserved) == 58); +static_assert(sizeof(VrTracker2MemoryParameter) == 32); +static_assert(sizeof(VrTracker2MemoryRequirement) == 32); struct VrTracker2ResultParameter; struct VrTracker2Result; struct VrTracker2CoordinateLocation; @@ -84,17 +119,22 @@ std::int32_t APS5_VABI sceVrTracker2UnregisterDevice(std::int32_t deviceHandle) return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement) { +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const VrTracker2MemoryParameter* parameter, VrTracker2MemoryRequirement* requirement) { if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - const auto* bytes = static_cast<const std::uint8_t*>(parameter); - std::uint32_t size = 0; - std::memcpy(&size, bytes, sizeof size); - if (size != 0x20) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - for (std::size_t offset = sizeof size; offset < 0x20; ++offset) - if (bytes[offset] != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - const std::uint32_t fields[3] = {0x20, 32u << 20, 64u << 10}; - std::memset(requirement, 0, 0x20); - std::memcpy(requirement, fields, sizeof fields); + if (parameter->size != sizeof(VrTracker2MemoryParameter)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::uint32_t memorySize = 32u << 20; + if (parameter->mode == 1000 || parameter->mode == 1001) { + const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 0x10, sizeof(sdkVersion)); + if (sdkVersion < 0x1000000) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + memorySize = parameter->mode == 1000 ? 7u << 20 : 3u << 20; + } else if (parameter->mode != 0) { + return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + } + for (const auto value : parameter->reserved) + if (value != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + *requirement = {sizeof(VrTracker2MemoryRequirement), memorySize, 64u << 10, {}}; return SCE_OK; } diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index 7b43bc31d0..0bbe3a869e 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" #include <array> #include <cstdint> @@ -23,6 +24,8 @@ std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParamet namespace { +alignas(16) std::array<std::byte, 0x40> process{}; + constexpr std::int32_t NotInitialized = static_cast<std::int32_t>(0x8A740001u); constexpr std::int32_t InvalidArgument = static_cast<std::int32_t>(0x8A740006u); @@ -108,6 +111,30 @@ int main() { Require(sceVrTracker2QueryMemory(rejected.data(), requirement.data()) == InvalidArgument, "query reserved byte"); Require(AllEqual(requirement.data(), requirement.data() + 40, 0xAA), "query rejection leaves output"); } + const std::uint64_t processSize = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &processSize, sizeof(processSize)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters"); + for (const std::uint32_t version : {0u, 0xffffffu, 0x1000000u, 0x7000000u}) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); + for (const std::uint32_t mode : {1000u, 1001u}) { + std::memcpy(parameter.data() + 4, &mode, sizeof(mode)); + requirement.fill(0xAA); + const auto result = sceVrTracker2QueryMemory(parameter.data(), requirement.data()); + if (version < 0x1000000) { + Require(result == InvalidArgument, "query SDK restriction"); + Require(AllEqual(requirement.data(), requirement.data() + requirement.size(), 0xAA), "SDK rejection preserves output"); + } else { + Require(result == 0, "query alternate mode"); + std::uint32_t memorySize; + std::memcpy(&memorySize, requirement.data() + 4, sizeof(memorySize)); + Require(memorySize == (mode == 1000 ? 7u << 20 : 3u << 20), "query alternate memory size"); + Require(AllEqual(requirement.data() + 12, requirement.data() + 32, 0), "query alternate reserved fields"); + Require(AllEqual(requirement.data() + 32, requirement.data() + 40, 0xAA), "query alternate output boundary"); + } + } + } parameter[0] = 0x21; Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 72d3209092..69e3f5cd62 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -2,10 +2,9 @@ ### Silent stubs -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured on firmware 13.60 without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured on firmware 13.60 without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization option semantics and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. ### Unknown function info -- [sceVrTracker2QueryMemory](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2, NID `TwqZnaIjWv4`) - accepts only a parameter whose first `uint32` is 0x20 and whose bytes 4 to 31 are zero, found by searching sizes 8 to 0x400 and setting each field and byte; it then writes 32 bytes, `uint32` 0x20, 32 MiB and 64 KiB followed by zeros, and returns 0, and otherwise returns 0x8A740006 without writing, as it does for a null argument. The meaning of the parameter's reserved bytes and of the written fields is assumed from their values -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - initialization (`6Jy73SRfG-o`) takes one parameter of 0x48 bytes. PPSA05666 passes the size 0x48 at offset 0, `uint32` 0x100 and 0x2BC at 0x8 and 0xC, `uint64` 0x1FFF at 0x10 and 0x18, a pointer to its work memory at 0x20, and the memory query's 32 MiB and 64 KiB as `uint32` at 0x28 and 0x2C, with the other bytes zero; the priority and CPU mask meanings of 0x8 to 0x1F are assumed from the values. Called with that parameter and 32 MiB of anonymous memory from a homebrew payload on a PS5 (firmware 13.60) with no PS VR2 headset, it did not return within a minute, so what it returns without a headset is unknown and it throws +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - initialization (`6Jy73SRfG-o`) takes one parameter of 0x48 bytes. PPSA05666 passes the size 0x48 at offset 0, `uint32` 0x100 and 0x2BC at 0x8 and 0xC, `uint64` 0x1FFF at 0x10 and 0x18, a pointer to its work memory at 0x20, and the memory query's 32 MiB and 64 KiB as `uint32` at 0x28 and 0x2C, with the other bytes zero; thread setup confirms the priority and CPU mask meanings of 0x8 to 0x1F. The 72-byte layout also contains memory size/alignment at 0x28/0x2C, a 32-bit option at 0x30, coordinate system at 0x34 and two byte options at 0x38/0x39. The three option meanings remain unresolved. Called with that parameter and 32 MiB of anonymous memory from a homebrew payload on a PS5 (firmware 13.60) with no PS VR2 headset, it did not return within a minute, so what it returns without a headset is unknown and it throws - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - eight exports throw. `6Jy73SRfG-o` (initialization) and `9R6W33Au4B4` return 0x8A740006 on the console for null arguments, for zeroed, 0xAA- and 0x01-filled buffers and for a size field from 8 to 0x400 in any of the first three arguments; initialization also returned it for the memory query's parameter with 32 MiB aligned to 64 KiB passed inside the parameter or as further arguments, so the parameter layout it accepts is unknown. `NBhWy+Gl0tY` returns 0x8A740006 for those buffers but crashes for null arguments; `jRmuXjsknsI` returns 0x8A740006 for null arguments and crashes for buffers, also when every 8-byte slot points at readable memory; `O8ZkTFtvUL4`, `940WPg+WwIk`, `oZDm1bynCaA` and `U1VYczwqukY` crash for both From 5b01f0555019b42b41348b314bf33d38a80a6baf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:04:31 +0300 Subject: [PATCH 0713/1360] fix(http): apply sdk character rules to uri decoding --- core/libs/prx/libSceHttp/src/Uri.cpp | 17 +++++++++++++++++ core/libs/tests/GuestHttp.cpp | 26 ++++++++++++++++++++++++++ docs/dev/debt/libSceHttp.md | 1 - 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 0f0e8d3660..b1cc8db443 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -7,6 +7,7 @@ #include <string_view> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" namespace { @@ -254,6 +255,22 @@ int APS5_VABI sceHttpUriUnescape(char* out, size_t* requiredSize, size_t capacit index += 2; } + if (!source.empty()) { + std::uint32_t sdkVersion = 0; + const auto* parameters = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::memcpy(&sdkVersion, parameters + 0x10, sizeof(sdkVersion)); + if (sdkVersion < 0x1000000) { + for (size_t index = 0; index < source.size(); ++index) { + const char value = source[index]; + if (value == '%') { + index += 2; + continue; + } + if (!isAsciiAlpha(value) && !isAsciiDigit(value) && value != '*' && value != '-' && value != '.' && value != '_' && value != '+') return ERROR_INVALID_URL; + } + } + } + size_t written = 0; for (size_t index = 0; index < source.size(); ++index) { if (out && written >= capacity) return ERROR_OUT_OF_MEMORY; diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index b340eb5a64..2d0060ef46 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -1,4 +1,6 @@ #include "SceTypes.hpp" +#include "ProcessParametersFixture.hpp" +#include <array> #include "prx/libc/include/general/VabiMacros.hpp" #include <atomic> #include <chrono> @@ -46,6 +48,8 @@ int APS5_VABI sceHttpSetAuthInfoCallback(int, HttpAuthInfoCallback, void*); int APS5_VABI sceHttpParseStatusLine(const char*, std::size_t, std::int32_t*, std::int32_t*, std::int32_t*, const char**, std::size_t*); } +alignas(16) static std::array<std::byte, 0x40> processParameters{}; + static void Require(bool value) { if (!value) std::abort(); } static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } @@ -98,6 +102,28 @@ static void APS5_VABI RequestStatusCallback(int, int, void* argument) { } int main() { + const std::uint64_t processSize = processParameters.size(); + const std::uint32_t processMagic = 0x4942524f; + std::uint32_t sdkVersion = 0x1000000; + std::memcpy(processParameters.data(), &processSize, sizeof(processSize)); + std::memcpy(processParameters.data() + 8, &processMagic, sizeof(processMagic)); + std::memcpy(processParameters.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(processParameters.data(), processParameters.size())); + for (const std::uint32_t version : {0u, 0xffffffu, 0x1000000u}) { + std::memcpy(processParameters.data() + 16, &version, sizeof(version)); + for (unsigned value = 1; value < 256; ++value) { + if (value == '%') continue; + const char input[]{static_cast<char>(value), 0}; + char output[]{'x', 'x'}; + std::size_t required = 123; + const bool allowed = version >= 0x1000000 || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '*' || value == '-' || value == '.' || value == '_' || value == '+'; + Require(sceHttpUriUnescape(output, &required, sizeof(output), input) == (allowed ? 0 : static_cast<int>(0x80433060))); + Require(allowed ? output[0] == input[0] && output[1] == 0 && required == 2 : output[0] == 'x' && output[1] == 'x' && required == 123); + } + char output[2]{}; + Require(sceHttpUriUnescape(output, nullptr, sizeof(output), "%7E") == 0 && output[0] == '~'); + } + std::memcpy(processParameters.data() + 16, &sdkVersion, sizeof(sdkVersion)); int callbackCount = 0; const auto request = sceHttpCreateRequest2(1, "GET", "/", 0); Require(sceHttpSetRequestStatusCallback(request, RequestStatusCallback, &callbackCount) == 0); diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index b65ff78602..3df2c812ff 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -14,7 +14,6 @@ - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end 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. 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) - the legacy SDK character whitelist is not selected by the host backend. Percent validation, decoding, partial writes on insufficient capacity and success-only size output follow the supplied PS5 module; arbitrary non-percent bytes follow its modern SDK path. - [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before - [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 - [sceHttpSetRequestStatusCallback](../../../core/libs/prx/libSceHttp/Export.cpp) - the three-argument ABI follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). Registrations and user data are retained and removed on request deletion; the offline backend never dispatches callbacks. Invalid ids return `0x80431100`, an uncited assumption, not a verified console error. Handle hierarchy, inheritance and deleted-handle validation remain incomplete; callback statuses and dispatch timing are unknown. From 4a57f814cc0f60e68c799ee05f08798a81961eed Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:04:31 +0300 Subject: [PATCH 0714/1360] docs(netctl): clarify remaining callback behavior --- docs/dev/debt/libSceNetCtl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 5c6533f6ff..5e377b9392 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -25,6 +25,6 @@ ### Functional -- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event: native `sceNetCtlCheckCallback` also drains the IPv6 service queue, which the host backend does not model +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for the native service event queues. IPv4 callbacks receive connected/disconnected transitions and an initial event; intermediate connection states and errors are not represented. Native CheckCallback drains both IPv4 and IPv6 queues, but the host backend never delivers IPv6 events. Dispatch copies slots before invoking callbacks, so unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. Initialization, slot limits, invalid/deleted IDs and the no-op Term follow the supplied module. - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB From 6aae4e187f4f1d3066accb8575843e2ac2ab0d4a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:05:48 +0300 Subject: [PATCH 0715/1360] fix(vrtracker2): restore anonymous utility export signatures --- core/libs/prx/libSceVrTracker2/Export.cpp | 36 ++++++++++++++++++----- docs/dev/debt/libSceVrTracker2.md | 4 +-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 3e2444861a..55924bd629 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -41,6 +41,28 @@ static_assert(offsetof(VrTracker2InitializeParameter, coordinateSystem) == 52); static_assert(offsetof(VrTracker2InitializeParameter, reserved) == 58); static_assert(sizeof(VrTracker2MemoryParameter) == 32); static_assert(sizeof(VrTracker2MemoryRequirement) == 32); +struct VrTracker2CalibrationResult; +struct VrTracker2TelemetryData; +struct VrTracker2PoseParameter; +struct VrTracker2PoseResult; + +struct VrTracker2MeshRequirement { + std::uint32_t vertexCount; + std::uint32_t indexCount; + std::uint32_t vertexDataSize; + std::uint32_t indexDataSize; +}; + +struct VrTracker2Pose { + float position[3]; + std::uint8_t padding[4]; + float orientation[4]; +}; + +static_assert(sizeof(VrTracker2MeshRequirement) == 16); +static_assert(sizeof(VrTracker2Pose) == 32); +static_assert(offsetof(VrTracker2Pose, orientation) == 16); + struct VrTracker2ResultParameter; struct VrTracker2Result; struct VrTracker2CoordinateLocation; @@ -280,43 +302,43 @@ std::int32_t APS5_VABI VrTracker2ClearWord(void* output) { } APS5_EXPORT("jRmuXjsknsI", VrTracker2Unknown01); -std::int32_t APS5_VABI VrTracker2Unknown01() { +std::int32_t APS5_VABI VrTracker2Unknown01(VrTracker2CalibrationResult* result, std::uint32_t capacity) { NotImplemented_nid_no_patch("jRmuXjsknsI"); return 0; } APS5_EXPORT("NBhWy+Gl0tY", VrTracker2Unknown02); -std::int32_t APS5_VABI VrTracker2Unknown02() { +std::int32_t APS5_VABI VrTracker2Unknown02(VrTracker2MeshRequirement* requirement, std::uint32_t format) { NotImplemented_nid_no_patch("NBhWy+Gl0tY"); return 0; } APS5_EXPORT("9R6W33Au4B4", VrTracker2Unknown03); -std::int32_t APS5_VABI VrTracker2Unknown03() { +std::int32_t APS5_VABI VrTracker2Unknown03(std::uint32_t mode, void* output, std::size_t capacity, std::size_t* writtenSize) { NotImplemented_nid_no_patch("9R6W33Au4B4"); return 0; } APS5_EXPORT("O8ZkTFtvUL4", VrTracker2Unknown04); -std::int32_t APS5_VABI VrTracker2Unknown04() { +std::int32_t APS5_VABI VrTracker2Unknown04(const VrTracker2ResultParameter* parameter, VrTracker2Result* result) { NotImplemented_nid_no_patch("O8ZkTFtvUL4"); return 0; } APS5_EXPORT("940WPg+WwIk", VrTracker2Unknown05); -std::int32_t APS5_VABI VrTracker2Unknown05() { +std::int32_t APS5_VABI VrTracker2Unknown05(VrTracker2Pose* pose, std::uint64_t coordinateId) { NotImplemented_nid_no_patch("940WPg+WwIk"); return 0; } APS5_EXPORT("oZDm1bynCaA", VrTracker2Unknown06); -std::int32_t APS5_VABI VrTracker2Unknown06() { +std::int32_t APS5_VABI VrTracker2Unknown06(const VrTracker2TelemetryData* data) { NotImplemented_nid_no_patch("oZDm1bynCaA"); return 0; } APS5_EXPORT("U1VYczwqukY", VrTracker2Unknown07); -std::int32_t APS5_VABI VrTracker2Unknown07() { +std::int32_t APS5_VABI VrTracker2Unknown07(const VrTracker2PoseParameter* parameter, VrTracker2PoseResult* result) { NotImplemented_nid_no_patch("U1VYczwqukY"); return 0; } diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 69e3f5cd62..a292162167 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -6,5 +6,5 @@ ### Unknown function info -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - initialization (`6Jy73SRfG-o`) takes one parameter of 0x48 bytes. PPSA05666 passes the size 0x48 at offset 0, `uint32` 0x100 and 0x2BC at 0x8 and 0xC, `uint64` 0x1FFF at 0x10 and 0x18, a pointer to its work memory at 0x20, and the memory query's 32 MiB and 64 KiB as `uint32` at 0x28 and 0x2C, with the other bytes zero; thread setup confirms the priority and CPU mask meanings of 0x8 to 0x1F. The 72-byte layout also contains memory size/alignment at 0x28/0x2C, a 32-bit option at 0x30, coordinate system at 0x34 and two byte options at 0x38/0x39. The three option meanings remain unresolved. Called with that parameter and 32 MiB of anonymous memory from a homebrew payload on a PS5 (firmware 13.60) with no PS VR2 headset, it did not return within a minute, so what it returns without a headset is unknown and it throws -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - eight exports throw. `6Jy73SRfG-o` (initialization) and `9R6W33Au4B4` return 0x8A740006 on the console for null arguments, for zeroed, 0xAA- and 0x01-filled buffers and for a size field from 8 to 0x400 in any of the first three arguments; initialization also returned it for the memory query's parameter with 32 MiB aligned to 64 KiB passed inside the parameter or as further arguments, so the parameter layout it accepts is unknown. `NBhWy+Gl0tY` returns 0x8A740006 for those buffers but crashes for null arguments; `jRmuXjsknsI` returns 0x8A740006 for null arguments and crashes for buffers, also when every 8-byte slot points at readable memory; `O8ZkTFtvUL4`, `940WPg+WwIk`, `oZDm1bynCaA` and `U1VYczwqukY` crash for both +- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. Result and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. From e1d99c4631cca6d26be6bb7e5bfbd37a6f5595d3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:08:10 +0300 Subject: [PATCH 0716/1360] fix(vrtracker2): define versioned tracking query parameters --- core/libs/prx/libSceVrTracker2/Export.cpp | 28 ++++++++++++++++++++++- docs/dev/debt/libSceVrTracker2.md | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 55924bd629..61ccdcc8fd 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -63,7 +63,33 @@ static_assert(sizeof(VrTracker2MeshRequirement) == 16); static_assert(sizeof(VrTracker2Pose) == 32); static_assert(offsetof(VrTracker2Pose, orientation) == 16); -struct VrTracker2ResultParameter; +struct VrTracker2ResultParameter { + std::uint32_t size; + std::int32_t deviceHandle; + std::uint32_t reserved; + std::uint8_t padding[4]; + std::uint64_t timestamp; + std::uint32_t reserved2; + std::uint8_t padding2[4]; + std::uint64_t userTag; + std::uint32_t reserved3[2]; +}; + +struct VrTracker2LegacyResultParameter { + std::uint32_t size; + std::int32_t deviceHandle; + std::uint32_t reserved; + std::uint8_t padding[4]; + std::uint64_t timestamp; + std::uint32_t userTag; + std::uint8_t padding2[4]; +}; + +static_assert(sizeof(VrTracker2ResultParameter) == 48); +static_assert(offsetof(VrTracker2ResultParameter, timestamp) == 16); +static_assert(offsetof(VrTracker2ResultParameter, userTag) == 32); +static_assert(sizeof(VrTracker2LegacyResultParameter) == 32); +static_assert(offsetof(VrTracker2LegacyResultParameter, userTag) == 24); struct VrTracker2Result; struct VrTracker2CoordinateLocation; diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index a292162167..ce9f91ccc8 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -7,4 +7,4 @@ ### Unknown function info - [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. Result and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. From ac2d5f49cbea08d0fbb7cfbdd7e6eed3b6853728 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:08:11 +0300 Subject: [PATCH 0717/1360] fix(hmd2): restore reprojection submission arguments --- core/libs/prx/libSceHmd2/Export.cpp | 35 ++++++++++++++++++++++++++--- docs/dev/debt/libSceHmd2.md | 2 ++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 15afad777e..373c4d1032 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -16,6 +16,35 @@ namespace Hmd2 { struct ReprojectionStatus; struct ReprojectionOutput; struct ReprojectionInitializeParameter; +struct ReprojectionLayer; +struct ReprojectionBufferedLayer; + +struct ReprojectionOption { + std::uint32_t size; + std::uint32_t flags; + std::uint32_t reserved[30]; +}; + +struct ReprojectionParameter { + const ReprojectionLayer* layers; + std::int32_t layerCount; + std::uint8_t padding[4]; + const ReprojectionOption* option; +}; + +struct ReprojectionBufferedParameter { + const ReprojectionBufferedLayer* layers; + std::int32_t layerCount; + std::uint8_t padding[4]; + const ReprojectionOption* option; +}; + +static_assert(sizeof(ReprojectionOption) == 128); +static_assert(sizeof(ReprojectionParameter) == 24); +static_assert(sizeof(ReprojectionBufferedParameter) == 24); +static_assert(offsetof(ReprojectionParameter, option) == 16); +static_assert(offsetof(ReprojectionBufferedParameter, option) == 16); + struct MirroringParameter { void* workMemory; @@ -270,17 +299,17 @@ int APS5_VABI sceHmd2ReprojectionSetMirroringOption(const Hmd2::MirroringOption* return 0; } -int APS5_VABI sceHmd2ReprojectionSetParam() { +int APS5_VABI sceHmd2ReprojectionSetParam(std::uint32_t bufferSetId, std::uint64_t frameId, const Hmd2::ReprojectionParameter* parameter, std::uint64_t reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer() { +int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer(std::uint32_t bufferSetId, std::uint64_t frameId, const Hmd2::ReprojectionBufferedParameter* parameter, std::uint64_t reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetRenderConfig() { +int APS5_VABI sceHmd2ReprojectionSetRenderConfig(std::uint64_t flags, std::uint64_t reserved, std::uint64_t reserved2) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 41deb3e6e3..d3f462f050 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -86,3 +86,5 @@ - [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte parameter and a 56-byte or 24-byte output. Object selection uses a 120-byte parameter and 56-byte result whose fields remain incomplete. Its initialization and memory-query structures are recovered, including the 32-bit object count and memory size, memory pointer and 64-bit alignment. Calibration data descriptors contain a pointer and signed 32-bit size; calibration status contains two 32-bit selectors and three floats. The underlying calibration blob formats remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. - [libSceHmd2 reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - frame identifiers and predicted display times are 64-bit. Event setters accept a kernel event queue and 32-bit event ID, consumed by sceKernelTriggerUserEvent. Mirroring uses two memory pointers and a separate 32-byte mode/reserved option. Status and current-output queries write 104 and 32 bytes; those field layouts and the reprojection initialization fields remain incomplete. Positional-reprojection mode is a byte with values 0 through 5, not a boolean. + +- [Reprojection parameter submission](../../../core/libs/prx/libSceHmd2/Export.cpp) - SetParam and SetParamWithBuffer take a 32-bit buffer-set ID, 64-bit frame ID, parameter pointer and reserved register slot. The parameter contains a layer pointer, signed count and optional 128-byte flags/reserved structure; plain and buffered layer strides are 328 and 472 bytes. Layer union fields remain incomplete. SetRenderConfig consumes three 64-bit slots; only bits 0 and 32 of the first can be nonzero. These exports remain unimplemented. From 29a75503fba3f82a2fbd0160a6d26d61d6848093 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:51:48 +0300 Subject: [PATCH 0718/1360] fix(tools): handle errors cleanly and reconfigure streams in package_release Wrap the package_release entrypoint in main(argv=None) with UTF-8 stream reconfiguration using backslashreplace error handling. Catch RuntimeError, ValueError, and OSError in main and report them via parser.error rather than propagating unhandled tracebacks. Guard missing CMakeCache in windows_runtime and non-directory documentation sources in collect_docs with descriptive errors. --- tools/package_release.py | 52 +++++++++++++++++----------- tools/tests/test_package_release.py | 53 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 19 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index 88558908a6..7c19c061cd 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -4,6 +4,7 @@ import os import re import shutil import stat +import sys import tarfile import tempfile import zipfile @@ -15,12 +16,15 @@ WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "ou def windows_runtime(build): - for line in (build / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): + cache = build / "CMakeCache.txt" + if not cache.is_file(): + raise RuntimeError(f"Missing CMakeCache.txt in build directory: {build}") + for line in cache.read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_CXX_COMPILER:"): compiler = line.partition("=")[2] if compiler and not compiler.endswith("-NOTFOUND"): return Path(compiler).parent - raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {build / 'CMakeCache.txt'}") + raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {cache}") def tracy_runtime(platform, build): @@ -92,6 +96,8 @@ def package(platform, build, output, version): def collect_docs(source, output): + if not source.is_dir(): + raise RuntimeError(f"Documentation source is not a directory: {source}") documents = sorted(document for document in source.rglob("*.md") if document.is_file()) if not documents: raise RuntimeError(f"No Markdown documents found in {source}") @@ -129,7 +135,11 @@ def package_web(build, output, version, revision): asset.replace(destination) -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--platform", choices=("linux", "windows", "web")) parser.add_argument("--build", type=Path) @@ -137,23 +147,27 @@ def main(): parser.add_argument("--revision") parser.add_argument("--docs", type=Path) parser.add_argument("--output", required=True, type=Path) - args = parser.parse_args() - if args.docs is not None: - if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: - parser.error("--docs cannot be combined with --platform, --build, --version or --revision") - collect_docs(args.docs, args.output) - else: - if args.platform is None or args.build is None or args.version is None: - parser.error("--platform, --build and --version are required when --docs is not specified") - if args.platform == "web": - if args.revision is None: - parser.error("--revision is required for --platform web") - package_web(args.build, args.output, args.version, args.revision) + args = parser.parse_args(argv) + try: + if args.docs is not None: + if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: + parser.error("--docs cannot be combined with --platform, --build, --version or --revision") + collect_docs(args.docs, args.output) else: - if args.revision is not None: - parser.error("--revision requires --platform web") - package(args.platform, args.build, args.output, args.version) + if args.platform is None or args.build is None or args.version is None: + parser.error("--platform, --build and --version are required when --docs is not specified") + if args.platform == "web": + if args.revision is None: + parser.error("--revision is required for --platform web") + package_web(args.build, args.output, args.version, args.revision) + else: + if args.revision is not None: + parser.error("--revision requires --platform web") + package(args.platform, args.build, args.output, args.version) + except (RuntimeError, ValueError, OSError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 4abcb1f37b..e20eb74bd2 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -350,9 +350,19 @@ class ReleasePackageTests(unittest.TestCase): package_release.package("linux", self.build, self.output, "../v1.0") self.assertFalse(self.output.exists()) + def test_windows_runtime_missing_cmake_cache_raises_runtime_error(self): + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "Missing CMakeCache.txt"): + package_release.windows_runtime(Path(tmp)) + class DocumentationAssetsTests(unittest.TestCase): + def test_source_not_directory_raises_runtime_error(self): + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "not a directory"): + package_release.collect_docs(Path(tmp) / "nonexistent", Path(tmp) / "assets") + def test_unique_nested_documents_are_flattened(self): with tempfile.TemporaryDirectory() as tmp: source = Path(tmp) / "source" @@ -601,5 +611,48 @@ class ReleaseNotesEdgeCaseTests(unittest.TestCase): self.assertIn("Full Changelog", result) +class ReleaseMainTests(unittest.TestCase): + def test_main_with_docs_arguments(self): + with tempfile.TemporaryDirectory() as tmp: + docs = Path(tmp) / "docs" + docs.mkdir() + (docs / "guide.md").write_text("# Guide", encoding="utf-8") + output = Path(tmp) / "out" + result = package_release.main(["--docs", str(docs), "--output", str(output)]) + self.assertEqual(result, 0) + self.assertEqual((output / "guide.md").read_text(encoding="utf-8"), "# Guide") + + def test_main_reports_packaging_error_via_parser_error(self): + with tempfile.TemporaryDirectory() as tmp: + output = Path(tmp) / "out" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as error: + package_release.main(["--docs", str(Path(tmp) / "nonexistent"), "--output", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("not a directory", stderr.getvalue()) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + out, err = Stream(), Stream() + with patch.object(package_release.sys, "stdout", out), patch.object(package_release.sys, "stderr", err): + with tempfile.TemporaryDirectory() as tmp: + docs = Path(tmp) / "docs" + docs.mkdir() + (docs / "doc.md").write_text("text", encoding="utf-8") + package_release.main(["--docs", str(docs), "--output", str(Path(tmp) / "out")]) + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From 462d45adf099622f3380f52f931ed30f5b1952b4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:09:21 +0300 Subject: [PATCH 0719/1360] fix(npwebapi2): clear the complete memory statistics record --- core/libs/prx/libSceNpWebApi2/Export.cpp | 6 +++++- core/libs/tests/GuestNpWebApi2.cpp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index ea33832e83..daef37aa53 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include <atomic> +#include <cstring> #include <mutex> #include <stdexcept> #include <unordered_map> @@ -27,6 +28,8 @@ struct NpWebApi2MemoryPoolStats { int32_t reserved; }; +static_assert(sizeof(NpWebApi2MemoryPoolStats) == 32); + static std::mutex g_poolsMutex; static std::unordered_map<int, size_t> g_pools; @@ -120,7 +123,8 @@ int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int libraryContextId, NpWebApi2Memo const auto pool = g_pools.find(libraryContextId); if (pool == g_pools.end()) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; if (!stats) return 0; - *stats = {pool->second, 0, 0, 0}; + std::memset(stats, 0, sizeof(*stats)); + stats->pool_size = pool->second; return 0; } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index eac1a6321f..7bd8327e2b 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -2,6 +2,7 @@ #include <cstddef> #include <cstdint> #include <cstdlib> +#include <cstring> #include <stdexcept> struct NpWebApi2MemoryPoolStats { @@ -37,8 +38,11 @@ int main() { const int context = sceNpWebApi2Initialize(1, 0x20000); const int other = sceNpWebApi2Initialize(1, 0x8000); Require(context > 0 && other > 0 && context != other); - NpWebApi2MemoryPoolStats stats{1, 1, 1, 1}; + NpWebApi2MemoryPoolStats stats; + std::memset(&stats, 0xa5, sizeof(stats)); Require(sceNpWebApi2GetMemoryPoolStats(context, &stats) == 0); + const auto* statsBytes = reinterpret_cast<const unsigned char*>(&stats); + for (std::size_t index = sizeof(std::size_t); index < sizeof(stats); ++index) Require(statsBytes[index] == 0); Require(stats.pool_size == 0x20000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); Require(sceNpWebApi2GetMemoryPoolStats(other, &stats) == 0 && stats.pool_size == 0x8000); Require(sceNpWebApi2GetMemoryPoolStats(context, nullptr) == 0); From 82f6f998834424edb84759b0d2c93d766f44159f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:10:03 +0300 Subject: [PATCH 0720/1360] fix(hmd2): define gaze query and foveated result layouts --- core/libs/prx/libSceHmd2/Export.cpp | 17 +++++++++++++++-- docs/dev/debt/libSceHmd2.md | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 373c4d1032..d348fcefe5 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -59,9 +59,22 @@ struct MirroringOption { static_assert(sizeof(MirroringParameter) == 16); static_assert(sizeof(MirroringOption) == 32); -struct GazeResultParameter; +struct GazeResultParameter { + std::uint32_t size; + std::int32_t handle; + std::uint8_t reserved[8]; +}; struct GazeResult; -struct FoveatedRenderingResult; +struct FoveatedRenderingResult { + std::uint32_t size; + std::int32_t status; + float direction[3]; + std::uint8_t reserved[4]; +}; + +static_assert(sizeof(GazeResultParameter) == 16); +static_assert(sizeof(FoveatedRenderingResult) == 24); +static_assert(offsetof(FoveatedRenderingResult, direction) == 8); struct GazeCalibrationData { void* data; std::int32_t size; diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index d3f462f050..36737886e2 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -83,7 +83,7 @@ - [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known -- [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte parameter and a 56-byte or 24-byte output. Object selection uses a 120-byte parameter and 56-byte result whose fields remain incomplete. Its initialization and memory-query structures are recovered, including the 32-bit object count and memory size, memory pointer and 64-bit alignment. Calibration data descriptors contain a pointer and signed 32-bit size; calibration status contains two 32-bit selectors and three floats. The underlying calibration blob formats remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. +- [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte size/handle parameter. The 24-byte foveated result contains size, status and a three-float direction; its trailing four bytes are untouched. The 56-byte full result still needs its eye-field semantics. Object selection uses a 120-byte parameter and 56-byte result whose fields remain incomplete. Its initialization and memory-query structures are recovered, including the 32-bit object count and memory size, memory pointer and 64-bit alignment. Calibration data descriptors contain a pointer and signed 32-bit size; calibration status contains two 32-bit selectors and three floats. The underlying calibration blob formats remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. - [libSceHmd2 reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - frame identifiers and predicted display times are 64-bit. Event setters accept a kernel event queue and 32-bit event ID, consumed by sceKernelTriggerUserEvent. Mirroring uses two memory pointers and a separate 32-byte mode/reserved option. Status and current-output queries write 104 and 32 bytes; those field layouts and the reprojection initialization fields remain incomplete. Positional-reprojection mode is a byte with values 0 through 5, not a boolean. From 9047c23198eb41c7cc3db444631ba0198bb0400d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:12:25 +0300 Subject: [PATCH 0721/1360] fix(audioin): match native hq and async open validation --- core/libs/prx/libSceAudioIn/Export.cpp | 20 +++++----- core/libs/tests/GuestAudioIn.cpp | 51 ++++++++++++-------------- docs/dev/debt/libSceAudioIn.md | 6 +-- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 68fa257250..3f4d0837fb 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -17,12 +17,12 @@ constexpr int AUDIO_IN_ERROR_INVALID_HANDLE = static_cast<int>(0x80260101); constexpr int AUDIO_IN_ERROR_INVALID_SIZE = static_cast<int>(0x80260102); constexpr int AUDIO_IN_ERROR_INVALID_FREQ = static_cast<int>(0x80260103); constexpr int AUDIO_IN_ERROR_INVALID_TYPE = static_cast<int>(0x80260104); +constexpr int AUDIO_IN_ERROR_INVALID_POINTER = static_cast<int>(0x80260105); constexpr int AUDIO_IN_ERROR_INVALID_PARAM = static_cast<int>(0x80260106); constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast<int>(0x80260107); constexpr int AUDIO_IN_ERROR_BUSY = static_cast<int>(0x8026010A); constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 1; constexpr std::uint32_t MAX_QUEUED_BLOCKS = 4; -constexpr std::uint32_t MAX_ASYNC_LEN = 384; using Clock = std::chrono::steady_clock; @@ -175,18 +175,18 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) { return 0; } -int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - if (const int error = checkArguments(type, index, len, freq); error != 0) return error; - if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE; +int32_t APS5_VABI sceAudioInHqOpen(int32_t userId, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ; + if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE; + if (param != 2) return AUDIO_IN_ERROR_INVALID_PARAM; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; return openPort(len, freq, param); } -int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - if (const int error = checkArguments(type, index, len, freq); error != 0 && error != AUDIO_IN_ERROR_INVALID_SIZE) return error; - if (len != 128 && len != 256 && len != MAX_ASYNC_LEN) return AUDIO_IN_ERROR_INVALID_SIZE; +int32_t APS5_VABI sceAudioInAsyncOpen(int32_t userId, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { + if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ; + if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; return openPort(len, freq, param); } @@ -197,7 +197,7 @@ int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { } int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { - if (!gain) throw std::runtime_error("sceAudioInGetGain: null gain"); + if (!gain) return AUDIO_IN_ERROR_INVALID_POINTER; *gain = 1.0f; std::lock_guard lock(g_mutex); return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index 124bc9a880..05b2eac559 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -36,16 +36,6 @@ static bool ReportsNotImplemented(const char* name, TCall call) { return false; } -template <typename TCall> -static bool ThrowsRuntimeError(TCall call) { - try { - call(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - static void SetEnvironment(const char* name, const std::string& value) { #ifdef _WIN32 _putenv_s(name, value.c_str()); @@ -63,6 +53,7 @@ void TestValidation() { constexpr int invalidSize = static_cast<int>(0x80260102); constexpr int invalidFreq = static_cast<int>(0x80260103); constexpr int invalidType = static_cast<int>(0x80260104); + constexpr int invalidPointer = static_cast<int>(0x80260105); constexpr int invalidParam = static_cast<int>(0x80260106); Require(sceAudioInOpen(user, 2, 0, 256, 48000, 2) == invalidType); Require(sceAudioInOpen(user, 0, 1, 256, 48000, 2) == invalidParam); @@ -80,37 +71,40 @@ void TestValidation() { Require(sceAudioInAsyncOpen(user, 0, 0, 385, 48000, 2) == invalidSize); Require(sceAudioInAsyncOpen(user, 0, 0, 200, 48000, 2) == invalidSize); Require(sceAudioInAsyncOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); - Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidSize); Require(sceAudioInHqOpen(user, 0, 0, 256, 48000, 2) == invalidSize); Require(sceAudioInHqOpen(user, 0, 0, 128, 16000, 2) == invalidFreq); for (const int badHandle : {-1, 0, 1, 9}) { float gain = -2.5f; Require(sceAudioInGetGain(badHandle, &gain) == invalidHandle && gain == 1.0f); Require(sceAudioInGetRerouteCount(badHandle) == invalidHandle); - Require(ThrowsRuntimeError([badHandle] { sceAudioInGetGain(badHandle, nullptr); })); + Require(sceAudioInGetGain(badHandle, nullptr) == invalidPointer); } - int port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); + int port = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2); float gain = 0.0f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0); - Require(ThrowsRuntimeError([port] { sceAudioInGetGain(port, nullptr); })); + Require(sceAudioInGetGain(port, nullptr) == invalidPointer); Require(sceAudioInClose(port) == 0); gain = 0.25f; Require(sceAudioInGetGain(port, &gain) == invalidHandle && gain == 1.0f && sceAudioInGetRerouteCount(port) == invalidHandle); port = sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); gain = -1.0f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); - port = sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); - gain = 2.0f; - Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); - port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 0x10); + for (const std::uint32_t format : {0u, 1u, 3u, 0x10u, 0x11u, 0x12u}) Require(sceAudioInHqOpen(user, 0, 0, 128, 48000, format) == invalidParam); + for (const std::uint32_t size : {0u, 127u, 129u, 256u, 384u}) Require(sceAudioInAsyncOpen(user, 0, 0, size, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 2, 1, 256, 16000, 0) == invalidFreq); + Require(sceAudioInAsyncOpen(user, 2, 1, 256, 16000, 0) == invalidFreq); + Require(sceAudioInHqOpen(user, 2, 1, 256, 48000, 0) == invalidSize); + Require(sceAudioInAsyncOpen(user, 2, 1, 256, 48000, 0) == invalidSize); + port = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); gain = 0.5f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); } -void InputSound(int handle, std::vector<std::int16_t>& block) { +void InputSound(int handle, std::vector<std::int16_t>& block, int samples = 256) { for (int attempt = 0; attempt < 200; ++attempt) { - Require(sceAudioInInput(handle, block.data()) == 256); + Require(sceAudioInInput(handle, block.data()) == samples); if (std::any_of(block.begin(), block.end() - 1, [](std::int16_t sample) { return sample != 0; })) return; } Require(false); @@ -138,9 +132,10 @@ void TestCapture() { Require(sceAudioInInput(handle, block.data()) == 256); for (std::size_t i = 0; i < 256 * 2; ++i) Require(block[i] == 0); Require(sceAudioInClose(handle) == 0); - const int async = sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 2); + const int async = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2); Require(async == handle && sceAudioInGetSilentState(async) == 0); - InputSound(async, block); + block.assign(128 * 2 + 1, 0x5555); + InputSound(async, block, 128); Require(std::equal(block.begin(), block.end() - 1, recorded.begin()) && block.back() == 0x5555); Require(sceAudioInClose(async) == 0); std::filesystem::remove(path); @@ -165,7 +160,7 @@ void TestNoDevice() { Require(buffer[256 * 8] == 0xCC); Require(sceAudioInInput(handle, nullptr) == 0); const int hq = sceAudioInHqOpen(user, 0, 0, 128, 48000, 2); - const int async = sceAudioInAsyncOpen(user, 1, 0, 256, 16000, 0x11); + const int async = sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 0x11); Require(hq > 0 && async > 0 && hq != handle && async != handle && hq != async); Require(sceAudioInGetSilentState(hq) == 1 && sceAudioInGetSilentState(async) == 1); std::fill(buffer.begin(), buffer.end(), 0xCC); @@ -173,13 +168,13 @@ void TestNoDevice() { for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); Require(buffer[128 * 4] == 0xCC); std::fill(buffer.begin(), buffer.end(), 0xCC); - Require(sceAudioInInput(async, buffer.data()) == 256); - for (std::size_t i = 0; i < 256 * 4; ++i) Require(buffer[i] == 0); - Require(buffer[256 * 4] == 0xCC); + Require(sceAudioInInput(async, buffer.data()) == 128); + for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[128 * 4] == 0xCC); Require(sceAudioInInput(async, nullptr) == 0); for (int port = 3; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) == portFull); - Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 2) == portFull); Require(sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 1) == portFull); Require(sceAudioInClose(hq) == 0); Require(sceAudioInClose(hq) == invalidHandle); @@ -187,7 +182,7 @@ void TestNoDevice() { Require(sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2) == hq); Require(sceAudioInClose(async) == 0); Require(sceAudioInInput(async, buffer.data()) == invalidHandle); - Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 0x12) == async); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 2) == async); Require(sceAudioInClose(handle) == 0); Require(sceAudioInClose(handle) == invalidHandle); Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 04f555333b..5f2f688821 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the PPSA26344 call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one +- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) - the PS5 module uses the low handle byte as a port index and returns whether its resolved device type equals 8, propagating negative device errors. Host capture devices have no corresponding controller-microphone classification; the protected throwing body remains unchanged. - [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature @@ -42,5 +42,5 @@ ### Functional -- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) describe the higher-quality path: a longer grain returns the invalid-size error and a lower rate the invalid-frequency error. AsyncOpen also accepts the 384-sample grain, the largest [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) documents for the low-latency path, and returns the invalid-size error for every other size; the console's shorter average return interval of an async port's blocks is not modelled, so like a synchronous port `sceAudioInInput` paces one grain at a time. Param 0x10 opens a float mono port like 0x11, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) accepts both, while SharpProspero documents only 0x11. Host capture pacing and console behavior remain unverified. -- [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - signatures and semantics follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs#L92-L110). `sceAudioInGetGain` writes the unattenuated linear gain `1.0f` into `*gain` before validating `handle`, returning `0` for an open port and `0x80260101` (`SCE_AUDIO_IN_ERROR_INVALID_HANDLE`) otherwise; a null `gain` pointer throws because the console result is unverified, and host or system input attenuation is not modelled. `sceAudioInGetRerouteCount` returns `0` for an open port and `0x80260101` otherwise, as each port opens its SDL capture device once and capture-device hot-swapping is not modelled. +- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) - PS5 wrapper validation requires 48000 Hz followed by exactly 128 samples for both calls; HqOpen then requires format 2. The host uses the same SDL capture path for both, so native asynchronous scheduling and high-quality processing are not reproduced. Common port validation, device selection and host capture pacing remain incomplete. +- [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) - GetGain writes 1.0f before handle validation and returns 0x80260105 for a null output, as confirmed by the PS5 module. Native handle bit checks and uninitialized/inactive port errors differ from the host port table. Native reroute counters follow device reassignment; the host opens each SDL device once and reports zero. From f7a7be1a271fb673069808af310e2416e8180965 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:14:05 +0300 Subject: [PATCH 0722/1360] fix(audioout2): restore optional attribute outputs and errors --- .../prx/libSceAudioOut/src/AudioOut2User.cpp | 8 ++++---- .../tests/AudioOut2PortLayouts.cpp | 16 +++++++++++----- docs/dev/debt/libSceAudioOut.md | 6 +++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp index a8c42717bf..1a514be246 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp @@ -32,13 +32,13 @@ int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) { } int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle, uint32_t* contextAttributes, uint32_t* portAttributes) { - if (!contextAttributes || !portAttributes) - throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: null output"); + if (handle == 0 || handle == std::numeric_limits<AudioOut2UserHandle>::max()) return static_cast<int>(0x80268010); + if (!contextAttributes && !portAttributes) return static_cast<int>(0x80268001); std::lock_guard lock(g_usersLock); if (!g_users.contains(handle)) throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: unknown user handle"); - *contextAttributes = 0; - *portAttributes = (1u << 0) | (1u << 1); + if (contextAttributes) *contextAttributes = 0; + if (portAttributes) *portAttributes = (1u << 0) | (1u << 1); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp index 1cad3a7422..7fcfa50334 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp @@ -77,11 +77,17 @@ void TestUserAttributes() { } Require(threw && context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu); }; - rejects(first, true, false); - rejects(first, false, true); - rejects(first, true, true); - rejects(0, false, false); - rejects(std::numeric_limits<AudioOut2UserHandle>::max(), false, false); + context.value = port.value = 0xFFFFFFFFu; + Require(sceAudioOut2UserGetSupportedAttributes(first, nullptr, &port.value) == 0); + Require(context.value == 0xFFFFFFFFu && port.value == 3); + Require(sceAudioOut2UserGetSupportedAttributes(first, &context.value, nullptr) == 0 && context.value == 0); + context.value = port.value = 0xFFFFFFFFu; + Require(sceAudioOut2UserGetSupportedAttributes(first, nullptr, nullptr) == static_cast<int>(0x80268001)); + for (const AudioOut2UserHandle handle : {AudioOut2UserHandle{0}, std::numeric_limits<AudioOut2UserHandle>::max()}) { + Require(sceAudioOut2UserGetSupportedAttributes(handle, &context.value, &port.value) == static_cast<int>(0x80268010)); + Require(sceAudioOut2UserGetSupportedAttributes(handle, nullptr, nullptr) == static_cast<int>(0x80268010)); + Require(context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu); + } Require(sceAudioOut2UserDestroy(first) == 0); rejects(first, false, false); std::thread([&] { diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index dd5aa7cfb3..1e7b243b57 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -4,11 +4,11 @@ - [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. -- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. +- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) - the PS5 module confirms a pointer-sized user handle and two optional uint32 outputs. Handles 0/-1 return 0x80268010 before checking outputs; two null outputs return 0x80268001. Host masks still describe only implemented PCM/gain support, unlike native SDK- and device-dependent masks. Unknown or destroyed host handles throw; native arbitrary handles are pointers and cannot be validated by the host registry. Port ownership and busy-user destruction remain unimplemented. - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. -- [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result +- [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) - native initialization requires AudioOut2 initialization (0x80268006 otherwise), returns success immediately when already initialized, and otherwise rejects nonzero flags with 0x80268201. Term returns 0x80268203 without mastering initialization and clears the mastering state after a successful service call. The protected Init body remains unchanged; host Term still returns success without this lifecycle or native mastering allocations. - [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the errors `0x80268201` and `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does; a null parameter returns `0x80268201`, the mastering invalid-API-parameter error KytyPS5 returns for a null state, which is an inference rather than a measurement. The meaning of `output` and `user` is unknown and both are ignored - [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. The debug state ids are unknown, so every id is accepted, and a null param returns `0x80260502` like the other AudioOut2 calls. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured @@ -110,7 +110,7 @@ ### Functional -- [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. +- [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without initialization validation or a chat service; repeated-call and no-session behavior therefore still depends on the missing service. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. From b5d626bc218984d1682135ad60fbfc92b5adfb79 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:19:50 +0300 Subject: [PATCH 0723/1360] fix(audioout): enforce native monitor buffer validation --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 29 +++++--------- .../tests/AudioOutExMonitorInfo.cpp | 39 ++++++------------- .../tests/AudioOutSysHdmiMonitorInfo.cpp | 15 +++---- docs/dev/debt/libSceAudioOut.md | 4 +- 4 files changed, 31 insertions(+), 56 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 41c2ad30a8..9faeaef957 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -40,7 +40,7 @@ static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; static constexpr int MONITOR_TYPE_HDMI = 1; static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; -static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0; +static constexpr std::uint32_t EX_MONITOR_INFO_SIZE = 0x400; static constexpr std::uint32_t EX_OPEN_MODE_MAX = 10; static constexpr std::uint32_t EX_OPEN_MODES = 0x61F; static constexpr std::uint32_t EX_CONFIGURE_MODE_DEFAULT = 0xFF; @@ -635,15 +635,10 @@ int APS5_VABI sceAudioOutSysConfigureOutput(int type, std::uint64_t flags, std:: } int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int type, void* info, std::uint32_t size) { - if (type != MONITOR_TYPE_HDMI) { - throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type " + std::to_string(type) + " not supported"); - } - if (size != HDMI_MONITOR_INFO_SIZE) { - throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: size " + std::to_string(size) + " not supported"); - } - if (info == nullptr) { - return -2144993276; - } + if (info == nullptr) return static_cast<int>(0x80260004); + if (size != HDMI_MONITOR_INFO_SIZE) return static_cast<int>(0x80260006); + if (type != MONITOR_TYPE_HDMI && type != 0x10) return static_cast<int>(0x8026000C); + if (type == 0x10) throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"); std::memset(info, 0, HDMI_MONITOR_INFO_SIZE); return 0; } @@ -703,16 +698,10 @@ int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std } int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std::uint32_t size) { - if (type != 0 || index != 0) { - throw std::runtime_error("sceAudioOutExGetMonitorInfo: monitor " + std::to_string(type) + ", " + std::to_string(index) + " not supported"); - } - if (size < EX_MONITOR_INFO_MIN_SIZE) { - throw std::runtime_error("sceAudioOutExGetMonitorInfo: size " + std::to_string(size) + " not supported"); - } - if (info == nullptr) { - return -2144993276; - } - std::memset(info, 0, size); + if (type != 0) return static_cast<int>(0x80260003); + if (info == nullptr) return static_cast<int>(0x80260004); + if (size != EX_MONITOR_INFO_SIZE) return static_cast<int>(0x80260006); + std::memset(info, 0, EX_MONITOR_INFO_SIZE); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp index fecdf065e1..6fcced4ffb 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp @@ -5,8 +5,6 @@ #include <cstdio> #include <cstdlib> #include <cstring> -#include <stdexcept> -#include <string> extern "C" { int APS5_VABI sceAudioOutExGetMonitorInfo(int, int, void*, std::uint32_t); @@ -21,28 +19,16 @@ static void Require(bool value, const char* message) { namespace { constexpr std::uint32_t titleSize = 0x400; -constexpr std::uint32_t minimumSize = 0xA0; constexpr std::size_t descriptorCountOffset = 0x98; constexpr std::size_t guardSize = 16; constexpr std::uint8_t untouched = 0xA5; constexpr int invalidPointer = static_cast<int>(0x80260004); -bool Throws(int type, int index, std::uint32_t size, const std::string& expected) { - std::array<std::uint8_t, titleSize> info{}; - try { - sceAudioOutExGetMonitorInfo(type, index, info.data(), size); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == expected) return true; - std::fprintf(stderr, "threw: %s\n", error.what()); - return false; - } - return false; -} - -void CheckEmpty(std::uint32_t size) { +void CheckEmpty(int index) { + constexpr auto size = titleSize; std::array<std::uint8_t, titleSize + guardSize> buffer{}; buffer.fill(untouched); - Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == 0, "the monitor query must succeed"); + Require(sceAudioOutExGetMonitorInfo(0, index, buffer.data(), size) == 0, "the monitor query must succeed"); for (std::uint32_t i = 0; i < size; i++) { Require(buffer[i] == 0, "the monitor info must describe a sink without audio descriptors"); } @@ -57,15 +43,14 @@ void CheckEmpty(std::uint32_t size) { } int main() { - CheckEmpty(titleSize); - CheckEmpty(minimumSize); - Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, titleSize) == invalidPointer, - "a null buffer must report an invalid pointer"); - Require(Throws(0, 0, minimumSize - 1, "sceAudioOutExGetMonitorInfo: size 159 not supported"), - "a buffer without room for the descriptor count must throw"); - Require(Throws(1, 0, titleSize, "sceAudioOutExGetMonitorInfo: monitor 1, 0 not supported"), - "a nonzero first argument must throw"); - Require(Throws(0, 1, titleSize, "sceAudioOutExGetMonitorInfo: monitor 0, 1 not supported"), - "a nonzero second argument must throw"); + for (const int index : {0, 1, -1, 100}) CheckEmpty(index); + std::array<std::uint8_t, titleSize + guardSize> buffer; + buffer.fill(untouched); + for (const std::uint32_t size : {0u, 0xA0u, titleSize - 1, titleSize + 1, 0xffffffffu}) { + Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == static_cast<int>(0x80260006), "exact size required"); + Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, size) == invalidPointer, "pointer checked before size"); + Require(sceAudioOutExGetMonitorInfo(1, 0, nullptr, size) == static_cast<int>(0x80260003), "type checked before pointer"); + for (const auto value : buffer) Require(value == untouched, "failure preserves output"); + } return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp index ce23a664d7..7eb1547ce7 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp @@ -59,13 +59,14 @@ int main() { Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, nullptr, infoSize) == invalidPointer, "a null buffer must report an invalid pointer"); - Require(Throws(monitorTypeHdmi, infoSize - 1, - "sceAudioOutSysGetHdmiMonitorInfo: size 383 not supported"), "a short buffer must throw"); - Require(Throws(monitorTypeHdmi, infoSize + 1, - "sceAudioOutSysGetHdmiMonitorInfo: size 385 not supported"), "a long buffer must throw"); + buffer.fill(untouched); + for (const std::uint32_t size : {0u, infoSize - 1, infoSize + 1, 0xffffffffu}) { + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), size) == static_cast<int>(0x80260006), "exact size required"); + Require(sceAudioOutSysGetHdmiMonitorInfo(0, nullptr, size) == invalidPointer, "pointer checked before size and type"); + for (const auto value : buffer) Require(value == untouched, "failure preserves output"); + } + for (const int type : {0, 2, -1, 17}) Require(sceAudioOutSysGetHdmiMonitorInfo(type, buffer.data(), infoSize) == static_cast<int>(0x8026000C), "unsupported type"); Require(Throws(0x10, infoSize, - "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "monitor type 0x10 must throw"); - Require(Throws(0, infoSize, - "sceAudioOutSysGetHdmiMonitorInfo: monitor type 0 not supported"), "monitor type 0 must throw"); + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "unimplemented device query"); return 0; } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 1e7b243b57..792d6e1a9e 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -6,7 +6,7 @@ - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) - the PS5 module confirms a pointer-sized user handle and two optional uint32 outputs. Handles 0/-1 return 0x80268010 before checking outputs; two null outputs return 0x80268001. Host masks still describe only implemented PCM/gain support, unlike native SDK- and device-dependent masks. Unknown or destroyed host handles throw; native arbitrary handles are pointers and cannot be validated by the host registry. Port ownership and busy-user destruction remain unimplemented. - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown -- [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured +- [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) - native validation requires first argument zero, a non-null buffer and exactly 1024 bytes, in that order (errors 0x80260003/04/06); the second argument is ignored. The host still reports no audio descriptors by zeroing the record. Native content comes from monitor/device services and system audio configuration, which the SDL backend does not reproduce. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) - native initialization requires AudioOut2 initialization (0x80268006 otherwise), returns success immediately when already initialized, and otherwise rejects nonzero flags with 0x80268201. Term returns 0x80268203 without mastering initialization and clears the mastering state after a successful service call. The protected Init body remains unchanged; host Term still returns success without this lifecycle or native mastering allocations. - [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the errors `0x80268201` and `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does; a null parameter returns `0x80268201`, the mastering invalid-API-parameter error KytyPS5 returns for a null state, which is an inference rather than a measurement. The meaning of `output` and `user` is unknown and both are ignored @@ -115,4 +115,4 @@ - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. - [sceAudioOut2LoInit, sceAudioOut2LoContext* and sceAudioOut2LoPort* calls](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) have no low-latency path: each forwards to its `sceAudioOut2` counterpart, so grain and queue depth are the context's `num_grains` and `queue_depth`, and playback keeps the 40 ms cushion. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L489-L545) only says each call takes the same handles and parameter blocks as its counterpart and drives the low-latency clock with a shallow queue and a small grain ([docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/audio.md#L662-L668)). Unverified: the console's grain and depth limits, its error for a handle of the other service (accepted here) and what `sceAudioOut2LoInit` returns when repeated (0 here). -- [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. +- [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) - native validation requires a non-null buffer and exactly 384 bytes, then accepts types 1 and 16. Mapped pointer/size/type errors are 0x80260004/06/0C. The host still zeroes type 1 output and throws for type 16: native monitor descriptors and system audio settings are unavailable. Driver initialization failures and service errors are not represented. From 9b584b01ad76dd2820c988e28e0d4eadc4321870 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:19:52 +0300 Subject: [PATCH 0724/1360] docs(hmd2): separate recovered contracts from remaining gaps --- docs/dev/debt/libSceHmd2.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 36737886e2..20fdeb5810 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -2,15 +2,13 @@ ### Silent stubs -- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016) for any param, so the other calls answer as before initialization and `sceHmd2Open` returns `NotInitialized` (0x8a720002). These results are restored from the behaviour before #7323 and are not measured on hardware: firmware 13.20's libSceHmd2, with the `/dev/hmd2_cmd` state measured on a retail PS5 with no headset connected (connected flag 0, status 2, user -1), initializes, returns a handle from `sceHmd2Open` and reports `NotDetected`, which the code past the initialization check models but no title reaches. PPSA01317 passes a pointer to 16 zeroed bytes, only tests the result for non-zero and then skips its VR set-up; with initialization succeeding it stopped on `sceHmd2ReprojectionQueryBufferSizeAlign` (#6350). That export, `sceHmd2ReprojectionQueryDisplayBufferSizeAlign` and `sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign` stay unimplemented and throw pending console measurements of their sizes and alignments. The other reprojection and gaze exports throw, and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes +- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: Initialize returns unsupported-feature, so normal calls never reach initialized device behavior. Reprojection and gaze operations retain their throwing bodies. Four size/alignment queries return measured constants; QueryBufferSizeAlign still throws. Device setup, headset tracking and rendering are not implemented. ### Unknown function info - [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal -- PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list -- PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list -- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceHmd2 legacy exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - SeeThroughDraw, SeeThroughIC, VideoOutEq, WaitShader, SeeThroughCallbackGetPoseDataFunc, ReprojectionClear, ReprojectionSet and RpeorjectionSetParam have no matching symbols or callers in the supplied system modules. Their signatures remain unresolved. ClearUserEventEnd/Start, GazeStart/StopUserCalibration and GazeObjectSelectionTerminate are confirmed to take no arguments; their behavior after initialization remains unimplemented. - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature @@ -34,7 +32,7 @@ ### Functional - [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled -- [sceHmd2ReprojectionQueryDisplayBufferSizeAlign, sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign, sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign and sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - each returns a `{uint64 size, uint64 alignment}` pair in RAX:RDX; the values were measured on a retail PS5 (firmware 13.60) with no PS VR2 headset from a homebrew payload that loaded `libSceHmd2.sprx` without initializing it, and stayed the same for every argument tried; `sceHmd2ReprojectionQueryBufferSizeAlign` measured there returns 0x100828 and 0x100 for every argument, including the 2, 0xAF and 1 that PPSA05666 passes. They are assumed not to depend on initialization or on a connected headset, and their parameters are unknown +- [Reprojection size/alignment queries](../../../core/libs/prx/libSceHmd2/Export.cpp) - all five exports consume no arguments and return two uint64 values in RAX:RDX. Four host queries return values measured without a headset; native display sizes are calculated through the graphics layout service and see-through size follows internal configuration. QueryBufferSizeAlign changes calculation with runtime state; its protected body still throws and its declared scalar return type does not represent the native pair. Correcting that return type also requires changing its return statement, outside the permitted body edits. - [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720004` to these 15 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `kAMsjYHBbNQ` (sceHmd2InternalSetVolume), `5m73zmLpsQ0` (sceHmd2InternalResetViewPosition), `EPeH4xuQ2XQ` (sceHmd2InternalGetStatus), `TJ8lyRsjFY4` (sceHmd2InternalClearTrackerFts), `-XQVcLnyX68` (sceHmd2InternalSetSlamCommand), `giQwQoHhYYA` (sceHmd2InternalDfuStart), `zNc9IkgawMI`, `-tnG5UJPvAc` (sceHmd2GazeStartUserCalibration), `FPhFmG+jXuQ` (sceHmd2GazeStopUserCalibration), `CKlkvNngofQ`, `yL1CZaciFWE` (sceHmd2GazeSetUserCalibrationPoint), `qlzgyG3s0RY`, `kTIDrDLzMv0`, `9Gee0ef8a+U`, `ecZVjQdwmBI` (sceHmd2GazeSetEnabledEye). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded @@ -47,7 +45,6 @@ - [Hmd2BeforeInitializeMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers -1 to these 12 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `EAkv8Mqt33E` (sceHmd2InternalRelocalizerStart), `Rv7bkmKvmz0` (sceHmd2InternalRelocalizerStop), `1oPZizn0SNA` (sceHmd2InternalTrackingStart), `TwCpE-nNrlo` (sceHmd2InternalTrackingStop), `jkqAeQfdtxw` (sceHmd2InternalLedDetectorStart), `+l0RQph81Z8` (sceHmd2InternalLedDetectorStop), `qsmAtGw1Ncc` (sceHmd2InternalImageStart), `uKhwYoMsIsA` (sceHmd2InternalImageStop), `WNdaigr94dI` (sceHmd2InternalGenDataStart), `P6S5ZVWf-H0` (sceHmd2InternalGenDataStop), `sun6NWE1sdg` (sceHmd2InternalSlamTraceStart), `--TFsJYXo04` (sceHmd2InternalSlamTraceStop). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeValue0400](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x400` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `BWqcwjCozzI`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeValue20000](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x20000` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `PRz7TdBGUI0`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded -- [libSceHmd2 pre-initialization sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the names in the entries above are the aerolib names of the NIDs where one exists; the error names `Error0004`, `Error002C`, `Error1002` and `Error1003` are placeholders for codes whose meaning is unknown, and `ReprojectionNotInitialized` is ours because every export answering `0x8a72000b` is a reprojection one. Exports that answered differently to null and to zeroed arguments, wrote output, returned a pointer-like value, answered `0x8a72000a` (which `sceHmd2Initialize` also returns for some applications) or crashed the probe are left as they were - [Hmd2CheckFirstThenError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720004` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 20 exports: `9WeZr4kYfzk` (sceHmd2InternalGetFwVersion), `N+5h1hCOFAg` (sceHmd2InternalGetRequiredPUPVersion), `w2vEs2y5ZmE` (sceHmd2InternalGetOpticalParam), `L5gat1pe0y0` (sceHmd2InternalSetCameraSeeThroughParam), `4GnLHR9NJ1w`, `AHk7nGSzef0` (sceHmd2InternalGetTrackerFtsData), `ncjX9d3mc0M`, `l63QYAUuogc` (sceHmd2InternalGetSlamCommand), `mrcBySCdXSo` (sceHmd2InternalDfuGetStatus), `ieBra+td8sU` (sceHmd2InternalDfuGetUpdateStatus), `s+rzBnewgGs` (sceHmd2InternalDfuCheckAfterPvt), `Nfcm0GBdzZI` (sceHmd2InternalRelocalizerGetVersion), `Tc9rasWfrm4` (sceHmd2InternalSetHomeCalibration), `ckTbKTX4KZg`, `D7576PUbUjk` (sceHmd2InternalGazeReadStatus), `tTJ04SrBT8U`, `sDP7hZ-P6OI` (sceHmd2GazeGetDeviceStatus), `J+eVNbN9ndY` (sceHmd2GazeGetUserCalibrationStatus), `kozvHBNyxf0`, `638Tmg5y6U4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2CheckFirstThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `8ETtFtp5zqY` (sceHmd2InternalGetVrMode), `CWPkBm3EXQA`, `R62liQQIAaA` (sceHmd2InternalGetFakeLensSeparationDistance), `-bcWLHsdzo0`, `JngJzgNbu8U` (sceHmd2SetFieldOfView), `xNBSz8xrGdk`, `yUVPfN-jRIU`, `BKPR3RZ5aO4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2CheckFirstThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `IG4y+drWAWw` (sceHmd2InternalRelocalizerReadData), `rhYUml9vppE` (sceHmd2InternalRelocalizerReadDataAsync), `6574xHS+jZk` (sceHmd2InternalRelocalizerReadData2), `mXzaY6xVoxg` (sceHmd2InternalTrackingReadData), `6af-HIZQQn4` (sceHmd2InternalTrackingReadData2), `6qWtdTHQsTk`, `pUm8t8xWhFg`, `A0XKI-WjxK8` (sceHmd2InternalImageReadData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded @@ -66,7 +63,6 @@ - [Hmd2CheckFirstAndThirdThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `zn7PHGzzEyI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2CheckSecondThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `wEHILc7TP04` (sceHmd2GazeInsertRawDataWithHandle). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `W-ZIlUXcrNA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded -- [libSceHmd2 null-argument sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `Error1010` and `Error800d0203` are placeholder names. Left as they were: the exports that write output (`omimgG5itSs`, `FBUEhH9tQ1A`, `6PeHcAaKYg8`, `yur+xPdWPAg`, `RnieDDp5ge4`, `jM3NEJA11yw`, `b0H-n-tUk0U`) and those whose answer with a single null argument is neither `0x8a720008` nor their all-present answer, which take a handle or slot number rather than a pointer first (`MXhloJ6Iw3w`, `19wrIIiqFgM`, `Z389vAm47Ys`, `cDcrvEG8j8k`, `tuqJO6O6cb4`, `fd4fu1cziaI`, `UxMg7iL4nRs`, `K1GELwlaaGc`, `RFn39VCHjGY`, `FRoSjceDv10`, `wiv8+7ogrq0`, `wQ44gBwBw44`) - [Hmd2WriteStateTwo](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the five bytes `02 00 00 00 00` at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `omimgG5itSs`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2WriteSize0x2000Align0x100](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 64-bit values `0x2000` through its first argument and `0x100` through its second and returns 0; a null first or second argument returns `0x8a720008`: `6PeHcAaKYg8`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2WriteFourConstants](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 16 bytes `e1 f0 74 3c d8 f0 74 3c 1f f1 74 3c d8 f0 74 3c` (four floats near 0.01495) at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `yur+xPdWPAg`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded From 772273eb693b271c82d099b16c05ea51f071ae21 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:20:44 +0300 Subject: [PATCH 0725/1360] fix(shareplay): restore initialization validation and state --- core/libs/prx/libSceSharePlay/Export.cpp | 20 +++++++++++++++++--- core/libs/tests/GuestSharePlay.cpp | 21 +++++++++++++++++---- docs/dev/debt/libSceSharePlay.md | 6 +++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 216ab34643..a1dcf5eadf 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -1,24 +1,36 @@ #include <cstdint> #include <cstddef> +#include <mutex> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" static constexpr int SCE_SHARE_PLAY_ERROR_INVALID_ARGS = static_cast<int>(0x810E0001); +static constexpr int SCE_SHARE_PLAY_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x810E0003); +static constexpr int SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED = static_cast<int>(0x810E0004); +static std::mutex g_mutex; +static bool g_initialized = false; static constexpr std::int32_t SCE_USER_SERVICE_USER_ID_INVALID = -1; extern "C" { int APS5_VABI sceSharePlayInitialize(void* heap, size_t heapSize) { - (void)heap; - if (heapSize == 0) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_mutex); + if (g_initialized) return SCE_SHARE_PLAY_ERROR_ALREADY_INITIALIZED; + if (heap && heapSize < 0x1800) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; + g_initialized = true; return 0; } int APS5_VABI sceSharePlayTerminate(void) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; + g_initialized = false; return 0; } int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* info) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; if (info == nullptr) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; *info = {}; info->hostUserId = SCE_USER_SERVICE_USER_ID_INVALID; @@ -26,7 +38,9 @@ int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* in return 0; } -int APS5_VABI sceSharePlayProhibitController(int prohibited) { +int APS5_VABI sceSharePlayProhibitController(std::uint8_t prohibited) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; if (prohibited != 0 && prohibited != 1) APS5_INVALID_ARG_EX; return 0; } diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index fe2819c276..269d05fb4d 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -4,12 +4,13 @@ #include <cstdlib> #include <cstring> #include <exception> +#include <span> extern "C" { int APS5_VABI sceSharePlayInitialize(void*, std::size_t); int APS5_VABI sceSharePlayTerminate(void); int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA*); -int APS5_VABI sceSharePlayProhibitController(int); +int APS5_VABI sceSharePlayProhibitController(std::uint8_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -26,12 +27,19 @@ static bool Throws(TCall call) { int main() { alignas(16) static unsigned char heap[0x1800]; + constexpr int notInitialized = static_cast<int>(0x810E0004); + constexpr int alreadyInitialized = static_cast<int>(0x810E0003); + constexpr int invalidArgument = static_cast<int>(0x810E0001); + Require(sceSharePlayTerminate() == notInitialized); + Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == notInitialized); + Require(sceSharePlayProhibitController(2) == notInitialized); Require(sceSharePlayInitialize(nullptr, sizeof(heap)) == 0); + Require(sceSharePlayInitialize(heap, 0) == alreadyInitialized); Require(sceSharePlayTerminate() == 0); Require(sceSharePlayInitialize(heap, sizeof(heap)) == 0); Require(sceSharePlayTerminate() == 0); - Require(Throws([&] { sceSharePlayInitialize(heap, 0); })); - Require(Throws([&] { sceSharePlayInitialize(nullptr, 0); })); + for (const std::size_t size : {std::size_t{0}, std::size_t{1}, sizeof(heap) - 1}) Require(sceSharePlayInitialize(heap, size) == invalidArgument); + Require(sceSharePlayInitialize(nullptr, 0) == 0); SharePlayConnectionInfoA info; std::memset(&info, 0x5a, sizeof(info)); Require(sceSharePlayGetCurrentConnectionInfoA(&info) == 0); @@ -43,5 +51,10 @@ int main() { Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(0) == 0); Require(Throws([] { sceSharePlayProhibitController(2); })); - Require(Throws([] { sceSharePlayProhibitController(-1); })); + Require(Throws([] { sceSharePlayProhibitController(255); })); + Require(sceSharePlayTerminate() == 0); + std::memset(&info, 0x5a, sizeof(info)); + Require(sceSharePlayGetCurrentConnectionInfoA(&info) == notInitialized); + for (const auto value : std::as_bytes(std::span(&info, 1))) Require(value == std::byte{0x5a}); + Require(sceSharePlayTerminate() == notInitialized); } diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md index 167653b1ff..cd999b7fab 100644 --- a/docs/dev/debt/libSceSharePlay.md +++ b/docs/dev/debt/libSceSharePlay.md @@ -4,9 +4,9 @@ - [sceSharePlayCrashDaemon, sceSharePlayGetCurrentConnectionInfo, sceSharePlayGetCurrentInfo, sceSharePlayGetEvent, sceSharePlayNotifyDialogOpen, sceSharePlayNotifyForceCloseForCdlg, sceSharePlayResumeScreenForCdlg, sceSharePlayServerLock](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. - [sceSharePlayServerUnLock, sceSharePlaySetMode, sceSharePlaySetProhibition, sceSharePlaySetProhibitionModeWithAppId, sceSharePlayStartStandby, sceSharePlayStartStreaming, sceSharePlayStopStandby, sceSharePlayStopStreaming](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. -- [sceSharePlayInitialize](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the library keeps no state, so the heap is not used. A null heap is accepted: PPSA11386 passes a null heap and 0x1800 bytes and stops on any non-zero result. The error for a zero size is unknown, so it throws ### Functional -- [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - accepts 0 (allow) and 1 (prohibit) and returns 0, following [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Share/SharePlayExports.cs#L9-L31). The setting is not stored, as no Share Play session or visitor controller exists and no export reads it back. sharpemu rejects any other value with its synthetic `0x80020003`, not a Share Play code, so other values throw. Like the other exports here it does not require `sceSharePlayInitialize`. The `int` argument type and the console's error for other values are unverified +- [sceSharePlayInitialize and sceSharePlayTerminate](../../../core/libs/prx/libSceSharePlay/Export.cpp) - native heap validation and initialization state are recovered: a null heap ignores the supplied size, a supplied heap requires at least 0x1800 bytes, repeated initialization returns 0x810E0003 and termination before initialization returns 0x810E0004. The host does not allocate the native internal heap, connect to the Share Play service or reproduce its failures. +- [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the native module confirms a 72-byte result, initial zero fields and both user IDs -1. Initialization is checked before the output pointer. The host retains this initial disconnected state; native event draining, cached updates and service reconnection are unimplemented. +- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the argument is one byte, forwarded unchanged to the service. The host checks initialization and accepts 0/1 without a visitor controller; other byte values still throw because their handling belongs to the unimplemented service. From 7b55120dca14c13c418ef2733fc77d2fa4f3dce1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:24:50 +0300 Subject: [PATCH 0726/1360] fix(livestreaming): restore initialization and program info contracts --- .../prx/libSceGameLiveStreaming/Export.cpp | 25 +++++++++++-- core/libs/tests/GuestGameLiveStreaming.cpp | 37 +++++++++---------- docs/dev/debt/libSceGameLiveStreaming.md | 9 ++++- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index 71b57a3129..d4a4a9bdbd 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -4,6 +4,15 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceUserService/UserService.hpp" #include <cstring> +#include <mutex> + +static std::mutex g_mutex; +static bool g_initialized = false; +static constexpr int ErrorInvalidArgument = static_cast<int>(0x80A00002); +static constexpr int ErrorAlreadyInitialized = static_cast<int>(0x80A00003); +static constexpr int ErrorNotInitialized = static_cast<int>(0x80A00004); + +struct GameLiveStreamingProgramInfo; struct GameLiveStreamingStatus2 { std::int32_t userId; @@ -19,23 +28,31 @@ static_assert(sizeof(GameLiveStreamingStatus2) == 72); extern "C" { -int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { - if (heap_size == 0) APS5_INVALID_ARG_EX; +int APS5_VABI sceGameLiveStreamingInitialize(size_t heapSize) { + if (heapSize != 0x4000) return ErrorInvalidArgument; + std::lock_guard lock(g_mutex); + if (g_initialized) return ErrorAlreadyInitialized; + g_initialized = true; return 0; } int APS5_VABI sceGameLiveStreamingTerminate(void) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + g_initialized = false; return 0; } int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status) { - if (status == nullptr) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (status == nullptr) return ErrorInvalidArgument; std::memset(status, 0, sizeof(*status)); status->userId = USER_SERVICE_USER_ID_INVALID; return 0; } -int APS5_VABI sceGameLiveStreamingGetProgramInfo() { +int APS5_VABI sceGameLiveStreamingGetProgramInfo(GameLiveStreamingProgramInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp index a60c2538cd..8f6f879c8a 100644 --- a/core/libs/tests/GuestGameLiveStreaming.cpp +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -4,6 +4,7 @@ #include <cstdlib> #include <cstring> #include <stdexcept> +#include <span> struct GameLiveStreamingStatus2 { std::int32_t userId; @@ -21,7 +22,8 @@ extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(std::size_t heap_size); int APS5_VABI sceGameLiveStreamingTerminate(void); int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status); -int APS5_VABI sceGameLiveStreamingGetProgramInfo(void); +struct GameLiveStreamingProgramInfo; +int APS5_VABI sceGameLiveStreamingGetProgramInfo(GameLiveStreamingProgramInfo*); } namespace { @@ -33,23 +35,16 @@ constexpr std::int32_t kInvalidUserId = -1; } int main() { - bool threwZeroHeap = false; - try { - sceGameLiveStreamingInitialize(0); - } catch (const std::invalid_argument&) { - threwZeroHeap = true; - } - Require(threwZeroHeap); - - Require(sceGameLiveStreamingInitialize(1024 * 1024) == 0); - - bool threwNullStatus = false; - try { - sceGameLiveStreamingGetCurrentStatus2(nullptr); - } catch (const std::invalid_argument&) { - threwNullStatus = true; - } - Require(threwNullStatus); + constexpr int invalidArgument = static_cast<int>(0x80A00002); + constexpr int alreadyInitialized = static_cast<int>(0x80A00003); + constexpr int notInitialized = static_cast<int>(0x80A00004); + Require(sceGameLiveStreamingTerminate() == notInitialized); + Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == notInitialized); + for (const std::size_t size : {0u, 1u, 0x3fffu, 0x4001u, 0x100000u}) Require(sceGameLiveStreamingInitialize(size) == invalidArgument); + Require(sceGameLiveStreamingInitialize(0x4000) == 0); + Require(sceGameLiveStreamingInitialize(0) == invalidArgument); + Require(sceGameLiveStreamingInitialize(0x4000) == alreadyInitialized); + Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == invalidArgument); GameLiveStreamingStatus2 status{}; std::memset(&status, 0x7f, sizeof(status)); @@ -69,11 +64,15 @@ int main() { bool threwProgramInfo = false; try { - sceGameLiveStreamingGetProgramInfo(); + sceGameLiveStreamingGetProgramInfo(nullptr); } catch (const std::runtime_error&) { threwProgramInfo = true; } Require(threwProgramInfo); Require(sceGameLiveStreamingTerminate() == 0); + std::memset(&status, 0x7f, sizeof(status)); + Require(sceGameLiveStreamingGetCurrentStatus2(&status) == notInitialized); + for (const auto value : std::as_bytes(std::span(&status, 1))) Require(value == std::byte{0x7f}); + Require(sceGameLiveStreamingTerminate() == notInitialized); } diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index ae477a6346..e85a882199 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -1,9 +1,14 @@ # libSceGameLiveStreaming technical debt +### Functional + +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [libSceGameLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - native initialization requires exactly 0x4000 heap bytes before checking repeated initialization. Host lifecycle and pointer errors now match the wrapper, but service registration, allocation failures and asynchronous service readiness are not modelled. The host becomes ready immediately after initialization. +- [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. +- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - consumes one output pointer and uses a 4940-byte record. Field semantics remain unresolved; the protected body remains unimplemented. + ### Unknown function info -- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws -- [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id - [sceGameLiveStreamingScreenInitialize](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingScreenInitializeSeparateModeParameter](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingScreenTerminate](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature From 7b1bf85ffd38f388a30674ca48dc5d94f91c1a29 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:26:00 +0300 Subject: [PATCH 0727/1360] fix(appcontent): enforce required output and mount pointers --- core/libs/prx/libSceAppContent/Export.cpp | 18 +++++++++--------- core/libs/tests/GuestAppContent.cpp | 5 ++++- docs/dev/debt/libSceAppContent.md | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index bb0eba21ea..30a0b1bc1c 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -73,10 +73,10 @@ int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifie 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; +int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t serviceLabel, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum) { + (void)serviceLabel; + if (!hitNum) return SCE_APP_CONTENT_ERROR_PARAMETER; + *hitNum = 0; return 0; } @@ -103,12 +103,12 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_ return 0; } -int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - if (!mount_point || !available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; +int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) { + if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mountPoint->data, DOWNLOAD_MOUNT_POINT, sizeof(mountPoint->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; if (quotaKb == 0) { - *available_space_kb = 0; + *availableSpaceKb = 0; return 0; } const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); @@ -118,7 +118,7 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount if (entry.is_regular_file()) usedKb += (entry.file_size() + 1023u) / 1024u; } const std::uint64_t hostKb = std::filesystem::space(directory).available / 1024u; - *available_space_kb = static_cast<size_t>(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb)); + *availableSpaceKb = static_cast<size_t>(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb)); return 0; } diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 6d3745cd70..4cf0721f55 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -51,7 +51,10 @@ int main() { Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + availableKb = 12345; Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, &availableKb) == ErrorParameter); + Require(availableKb == 12345); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, nullptr) == ErrorParameter); AppContentMountPoint invalidMount{}; Require(sceAppContentDownloadDataGetAvailableSpaceKb(&invalidMount, &availableKb) == ErrorNotMounted); const auto downloadData = ResolvePath_nid_no_patch("/download0"); @@ -170,7 +173,7 @@ int main() { 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, info, 1, nullptr) == ErrorParameter); 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/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 62bdd6066f..373aa6fdba 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -9,5 +9,7 @@ - [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 17 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional +- [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) does not track native service readiness or initialization. Native operations reject uninitialized use with `0x80D90001`; SDK 0x01500000 and later also reject repeated initialization and require zeroed initialization fields. Native entitlement operations additionally validate label bytes 17 through 19 from SDK 0x03500000. These checks and SDK-dependent service-error translation are not implemented. + - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. From fc8970626125992e374cb2fd0c489c8277fa5b8a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:28:19 +0300 Subject: [PATCH 0728/1360] fix(vrtracker2): split anonymous tracking query signatures --- core/libs/prx/libSceVrTracker2/Export.cpp | 82 ++++++++++++++++++++--- docs/dev/debt/libSceVrTracker2.md | 2 + 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 61ccdcc8fd..3d67c0619a 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -125,6 +125,40 @@ std::int32_t ClearThenReportNotInitialized(void* output, std::size_t size) { } +struct VrTracker2RelativePoseResult { + VrTracker2Pose pose; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(VrTracker2RelativePoseResult) == 64); + +struct VrTracker2ScalarResult { + std::uint32_t size; + float value; +}; + +struct VrTracker2TrackingOptions { + std::int32_t options[3]; + std::uint8_t reset; + std::uint8_t reserved[3]; + float vector[3]; +}; + +struct VrTracker2TrackingOptionsResult { + VrTracker2TrackingOptions options; + float position[3]; +}; + +struct VrTracker2DiagnosticResult; +struct VrTracker2ExtendedResult; +struct VrTracker2TrackingConfiguration; + +static_assert(sizeof(VrTracker2ScalarResult) == 8); +static_assert(sizeof(VrTracker2TrackingOptions) == 28); +static_assert(offsetof(VrTracker2TrackingOptions, vector) == 16); +static_assert(sizeof(VrTracker2TrackingOptionsResult) == 40); +static_assert(offsetof(VrTracker2TrackingOptionsResult, position) == 28); + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -191,14 +225,42 @@ std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParamet return 0; } -APS5_EXPORT("1m6DD9BBmSE", VrTracker2NotInitialized); -APS5_EXPORT("SgunslIO-C0", VrTracker2NotInitialized); -APS5_EXPORT("KMIToVnHrhc", VrTracker2NotInitialized); -APS5_EXPORT("XfEBVemeJyE", VrTracker2NotInitialized); -APS5_EXPORT("y79S3-zrJKg", VrTracker2NotInitialized); +APS5_EXPORT("1m6DD9BBmSE", VrTracker2GetRelativePose); +std::int32_t APS5_VABI VrTracker2GetRelativePose(std::int32_t firstDeviceHandle, std::int32_t secondDeviceHandle, VrTracker2RelativePoseResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("SgunslIO-C0", VrTracker2GetDiagnosticResult); +std::int32_t APS5_VABI VrTracker2GetDiagnosticResult(VrTracker2DiagnosticResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("KMIToVnHrhc", VrTracker2SetTrackingOptions); +std::int32_t APS5_VABI VrTracker2SetTrackingOptions(const VrTracker2TrackingOptions* options) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("XfEBVemeJyE", VrTracker2GetTrackingOptions); +std::int32_t APS5_VABI VrTracker2GetTrackingOptions(VrTracker2TrackingOptionsResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("y79S3-zrJKg", VrTracker2GetExtendedResult); +std::int32_t APS5_VABI VrTracker2GetExtendedResult(VrTracker2ExtendedResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("b777Kpfe2J8", VrTracker2NotInitialized); -APS5_EXPORT("UfUYgiT7VK0", VrTracker2NotInitialized); -APS5_EXPORT("MB7B6396K6U", VrTracker2NotInitialized); +APS5_EXPORT("UfUYgiT7VK0", VrTracker2GetCounter); +std::int32_t APS5_VABI VrTracker2GetCounter(std::uint64_t* counter, std::uint32_t selector) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("MB7B6396K6U", VrTracker2SetTrackingConfiguration); +std::int32_t APS5_VABI VrTracker2SetTrackingConfiguration(const VrTracker2TrackingConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("H2Mnx7gz4ac", VrTracker2NotInitialized); APS5_EXPORT("SgNHzN6XaEc", VrTracker2NotInitialized); APS5_EXPORT("sYBNLrMxjAM", VrTracker2NotInitialized); @@ -211,7 +273,11 @@ APS5_EXPORT("914+9oJxNts", VrTracker2NotInitialized); APS5_EXPORT("c6SZtdEjH0E", VrTracker2NotInitialized); APS5_EXPORT("74IXIMe+Lto", VrTracker2NotInitialized); APS5_EXPORT("hmahOYMiYNY", VrTracker2NotInitialized); -APS5_EXPORT("pNXc6ZlLq-8", VrTracker2NotInitialized); +APS5_EXPORT("pNXc6ZlLq-8", VrTracker2GetScalarResult); +std::int32_t APS5_VABI VrTracker2GetScalarResult(VrTracker2ScalarResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("omJ7b8FqAN0", VrTracker2NotInitialized); APS5_EXPORT("G2PWj1HLqn8", VrTracker2NotInitialized); APS5_EXPORT("73zcxAFawYk", VrTracker2NotInitialized); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index ce9f91ccc8..fc0416c069 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -8,3 +8,5 @@ - [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. + +- [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two device handles and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. From 12215c498dc8023344d8023b3a40368691606f1f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:29:33 +0300 Subject: [PATCH 0729/1360] fix(vrtracker2): restore diagnostic and configuration arguments --- core/libs/prx/libSceVrTracker2/Export.cpp | 79 ++++++++++++++++++++--- docs/dev/debt/libSceVrTracker2.md | 2 + 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 3d67c0619a..b5499df15f 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -159,6 +159,25 @@ static_assert(offsetof(VrTracker2TrackingOptions, vector) == 16); static_assert(sizeof(VrTracker2TrackingOptionsResult) == 40); static_assert(offsetof(VrTracker2TrackingOptionsResult, position) == 28); +struct VrTracker2ModeParameter { + std::uint32_t size; + std::uint32_t mode; + float value; +}; + +struct VrTracker2DiagnosticText { + char sections[6][512]; +}; + +struct VrTracker2CompositePoseResult; +struct VrTracker2CameraImages; +struct VrTracker2SensorFrame; +struct VrTracker2TrackingStatus; +struct VrTracker2BoundaryParameter; + +static_assert(sizeof(VrTracker2ModeParameter) == 12); +static_assert(sizeof(VrTracker2DiagnosticText) == 3072); + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -250,7 +269,11 @@ std::int32_t APS5_VABI VrTracker2GetExtendedResult(VrTracker2ExtendedResult* res return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -APS5_EXPORT("b777Kpfe2J8", VrTracker2NotInitialized); +APS5_EXPORT("b777Kpfe2J8", VrTracker2GetCompositePose); +std::int32_t APS5_VABI VrTracker2GetCompositePose(const VrTracker2ResultParameter* parameter, VrTracker2CompositePoseResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("UfUYgiT7VK0", VrTracker2GetCounter); std::int32_t APS5_VABI VrTracker2GetCounter(std::uint64_t* counter, std::uint32_t selector) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; @@ -262,17 +285,53 @@ std::int32_t APS5_VABI VrTracker2SetTrackingConfiguration(const VrTracker2Tracki } APS5_EXPORT("H2Mnx7gz4ac", VrTracker2NotInitialized); -APS5_EXPORT("SgNHzN6XaEc", VrTracker2NotInitialized); -APS5_EXPORT("sYBNLrMxjAM", VrTracker2NotInitialized); -APS5_EXPORT("3gQncidodmw", VrTracker2NotInitialized); -APS5_EXPORT("qnCmEbUGqWY", VrTracker2NotInitialized); -APS5_EXPORT("oGNn7JIdMHw", VrTracker2NotInitialized); +APS5_EXPORT("SgNHzN6XaEc", VrTracker2GetTrackingConfiguration); +std::int32_t APS5_VABI VrTracker2GetTrackingConfiguration(VrTracker2TrackingConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("sYBNLrMxjAM", VrTracker2GetCameraImages); +std::int32_t APS5_VABI VrTracker2GetCameraImages(VrTracker2CameraImages* images) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("3gQncidodmw", VrTracker2GetCameraImage); +std::int32_t APS5_VABI VrTracker2GetCameraImage(void* image, std::uint32_t size, std::uint32_t* frameId) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("qnCmEbUGqWY", VrTracker2GetDiagnosticText); +std::int32_t APS5_VABI VrTracker2GetDiagnosticText(VrTracker2DiagnosticText* text) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("oGNn7JIdMHw", VrTracker2GetSensorFrame); +std::int32_t APS5_VABI VrTracker2GetSensorFrame(VrTracker2SensorFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("80IxOn8swIA", VrTracker2NotInitialized); APS5_EXPORT("NTbJNAZA4ro", VrTracker2NotInitialized); -APS5_EXPORT("914+9oJxNts", VrTracker2NotInitialized); -APS5_EXPORT("c6SZtdEjH0E", VrTracker2NotInitialized); -APS5_EXPORT("74IXIMe+Lto", VrTracker2NotInitialized); -APS5_EXPORT("hmahOYMiYNY", VrTracker2NotInitialized); +APS5_EXPORT("914+9oJxNts", VrTracker2GetTrackingStatus); +std::int32_t APS5_VABI VrTracker2GetTrackingStatus(VrTracker2TrackingStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("c6SZtdEjH0E", VrTracker2GetBoundaryParameter); +std::int32_t APS5_VABI VrTracker2GetBoundaryParameter(VrTracker2BoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("74IXIMe+Lto", VrTracker2SetBoundaryParameter); +std::int32_t APS5_VABI VrTracker2SetBoundaryParameter(const VrTracker2BoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("hmahOYMiYNY", VrTracker2SetMode); +std::int32_t APS5_VABI VrTracker2SetMode(const VrTracker2ModeParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("pNXc6ZlLq-8", VrTracker2GetScalarResult); std::int32_t APS5_VABI VrTracker2GetScalarResult(VrTracker2ScalarResult* result) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index fc0416c069..e3b07e4421 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -10,3 +10,5 @@ - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. - [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two device handles and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. + +- [Anonymous diagnostic exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate pointer contracts for composite poses, configuration, camera images, sensor frames, tracking status and boundary parameters. The camera-image query takes a 32-bit byte count and an in/out frame identifier; diagnostics use six 512-byte text sections, and the mode setter uses a 12-byte size/mode/float record. Composite pose, sensor/status, configuration and boundary fields still require reconstruction; none of these exports supports initialized operation. From a180963101be9329b3d1a2edd18ad2a11bbb580b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:30:42 +0300 Subject: [PATCH 0730/1360] fix(vrtracker2): define camera frame and scalar option contracts --- core/libs/prx/libSceVrTracker2/Export.cpp | 58 ++++++++++++++++++++--- docs/dev/debt/libSceVrTracker2.md | 2 + 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index b5499df15f..d74218d1f8 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -178,6 +178,22 @@ struct VrTracker2BoundaryParameter; static_assert(sizeof(VrTracker2ModeParameter) == 12); static_assert(sizeof(VrTracker2DiagnosticText) == 3072); +struct VrTracker2CameraFrame { + std::uint32_t selector; + std::uint32_t identifiers[2]; + std::uint32_t width; + std::uint32_t height; + std::uint32_t pitch; + const void* pixels; + std::int32_t bufferIndex; +}; + +struct VrTracker2CameraFrameMetadata; + +static_assert(offsetof(VrTracker2CameraFrame, pixels) == 24); +static_assert(offsetof(VrTracker2CameraFrame, bufferIndex) == 32); +static_assert(sizeof(VrTracker2CameraFrame) == 40); + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -353,15 +369,43 @@ APS5_EXPORT("4+B+TcHEHks", VrTracker2NotInitialized); APS5_EXPORT("cjDz4kYJKTw", VrTracker2NotInitialized); APS5_EXPORT("BjVg0fKV9VA", VrTracker2NotInitialized); APS5_EXPORT("oDV5j-2Luic", VrTracker2NotInitialized); -APS5_EXPORT("t+86o9sCZ-Y", VrTracker2NotInitialized); -APS5_EXPORT("TIpYP35JzUs", VrTracker2NotInitialized); -APS5_EXPORT("LrSc+KuxIkg", VrTracker2NotInitialized); -APS5_EXPORT("Ur+M5wy+QBY", VrTracker2NotInitialized); -APS5_EXPORT("OvKC351Udqo", VrTracker2NotInitialized); +APS5_EXPORT("t+86o9sCZ-Y", VrTracker2AcquireCameraFrame); +std::int32_t APS5_VABI VrTracker2AcquireCameraFrame(std::uint32_t selector, VrTracker2CameraFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("TIpYP35JzUs", VrTracker2AcquireCameraFrameWithMetadata); +std::int32_t APS5_VABI VrTracker2AcquireCameraFrameWithMetadata(std::uint32_t selector, VrTracker2CameraFrame* frame, VrTracker2CameraFrameMetadata* metadata) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("LrSc+KuxIkg", VrTracker2ReleaseCameraFrame); +std::int32_t APS5_VABI VrTracker2ReleaseCameraFrame(const VrTracker2CameraFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("Ur+M5wy+QBY", VrTracker2SetScalarOption); +std::int32_t APS5_VABI VrTracker2SetScalarOption(float value) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("OvKC351Udqo", VrTracker2GetScalarOption); +std::int32_t APS5_VABI VrTracker2GetScalarOption(float* value) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; APS5_EXPORT("LQV4nz6wteY", VrTracker2NotInitialized); APS5_EXPORT("x0pGthEwf88", VrTracker2NotInitialized); -APS5_EXPORT("d5Fjs0rHkBg", VrTracker2NotInitialized); -APS5_EXPORT("Yed3ZONcuMk", VrTracker2NotInitialized); +APS5_EXPORT("d5Fjs0rHkBg", VrTracker2SelectDevice); +std::int32_t APS5_VABI VrTracker2SelectDevice(std::int32_t deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("Yed3ZONcuMk", VrTracker2GetSelectedDevice); +std::int32_t APS5_VABI VrTracker2GetSelectedDevice(std::int32_t* deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; APS5_EXPORT("fkptW6uRdWc", VrTracker2NotInitialized); APS5_EXPORT("HDlD-xE1Xuk", VrTracker2NotInitialized); APS5_EXPORT("IGdE6nsMTAY", VrTracker2NotInitialized); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index e3b07e4421..3217ba4d5a 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -12,3 +12,5 @@ - [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two device handles and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. - [Anonymous diagnostic exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate pointer contracts for composite poses, configuration, camera images, sensor frames, tracking status and boundary parameters. The camera-image query takes a 32-bit byte count and an in/out frame identifier; diagnostics use six 512-byte text sections, and the mode setter uses a 12-byte size/mode/float record. Composite pose, sensor/status, configuration and boundary fields still require reconstruction; none of these exports supports initialized operation. + +- [Camera-frame exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) do not acquire or release native buffers. Their descriptor has a selector, two identifiers, dimensions, pitch, pixel pointer and buffer index; native acquisition writes the first 36 bytes, leaving natural tail padding untouched. The optional 48-byte metadata record and identifier meanings remain unresolved. The scalar-option setter/getter use a float value/output, but its units and meaning remain unresolved. From dab466203693867546556cce7d04b6b13725dd7f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:32:05 +0300 Subject: [PATCH 0731/1360] fix(vrtracker2): restore device and spatial query signatures --- core/libs/prx/libSceVrTracker2/Export.cpp | 75 ++++++++++++++++++++--- docs/dev/debt/libSceVrTracker2.md | 2 + 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index d74218d1f8..07d244a879 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -194,6 +194,21 @@ static_assert(offsetof(VrTracker2CameraFrame, pixels) == 24); static_assert(offsetof(VrTracker2CameraFrame, bufferIndex) == 32); static_assert(sizeof(VrTracker2CameraFrame) == 40); +struct VrTracker2SpatialQueryResult { + std::uint32_t size; + std::int32_t status; + float vectors[2][3]; + float value; +}; + +struct VrTracker2DeviceCalibrationStatus; +struct VrTracker2DeviceBoundaryParameter; +struct VrTracker2ResetPoseParameter; + +static_assert(sizeof(VrTracker2SpatialQueryResult) == 36); +static_assert(offsetof(VrTracker2SpatialQueryResult, vectors) == 8); +static_assert(offsetof(VrTracker2SpatialQueryResult, value) == 32); + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -406,16 +421,56 @@ std::int32_t APS5_VABI VrTracker2GetSelectedDevice(std::int32_t* deviceHandle) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } ; -APS5_EXPORT("fkptW6uRdWc", VrTracker2NotInitialized); -APS5_EXPORT("HDlD-xE1Xuk", VrTracker2NotInitialized); -APS5_EXPORT("IGdE6nsMTAY", VrTracker2NotInitialized); -APS5_EXPORT("DSTergmOvvE", VrTracker2NotInitialized); -APS5_EXPORT("B7hPwvvg79E", VrTracker2NotInitialized); -APS5_EXPORT("ZwnIsDe1eNE", VrTracker2NotInitialized); -APS5_EXPORT("B2vw+s-0Dfo", VrTracker2NotInitialized); -APS5_EXPORT("4LNTLivfRuA", VrTracker2NotInitialized); -APS5_EXPORT("edajhZhz9Uo", VrTracker2NotInitialized); -APS5_EXPORT("QS+7oqiszuc", VrTracker2NotInitialized); +APS5_EXPORT("fkptW6uRdWc", VrTracker2ResetDevicePose); +std::int32_t APS5_VABI VrTracker2ResetDevicePose(std::int32_t deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("HDlD-xE1Xuk", VrTracker2ResetPose); +std::int32_t APS5_VABI VrTracker2ResetPose(const VrTracker2ResetPoseParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("IGdE6nsMTAY", VrTracker2QueryPosition); +std::int32_t APS5_VABI VrTracker2QueryPosition(const float* position, VrTracker2SpatialQueryResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("DSTergmOvvE", VrTracker2QueryDevicePosition); +std::int32_t APS5_VABI VrTracker2QueryDevicePosition(std::int32_t deviceHandle, VrTracker2SpatialQueryResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("B7hPwvvg79E", VrTracker2GetTrackingBounds); +std::int32_t APS5_VABI VrTracker2GetTrackingBounds(VrTracker2OrientedBoundingBox* bounds) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("ZwnIsDe1eNE", VrTracker2GetDeviceState); +std::int32_t APS5_VABI VrTracker2GetDeviceState(std::int32_t deviceHandle, std::int32_t* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("B2vw+s-0Dfo", VrTracker2GetDeviceCalibrationStatus); +std::int32_t APS5_VABI VrTracker2GetDeviceCalibrationStatus(std::int32_t deviceHandle, VrTracker2DeviceCalibrationStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("4LNTLivfRuA", VrTracker2SetDeviceBoundary); +std::int32_t APS5_VABI VrTracker2SetDeviceBoundary(std::int32_t deviceHandle, std::int32_t selector, const VrTracker2DeviceBoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("edajhZhz9Uo", VrTracker2GetDeviceBoundary); +std::int32_t APS5_VABI VrTracker2GetDeviceBoundary(std::int32_t deviceHandle, std::int32_t selector, VrTracker2DeviceBoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QS+7oqiszuc", VrTracker2ApplyDeviceBoundary); +std::int32_t APS5_VABI VrTracker2ApplyDeviceBoundary(std::int32_t deviceHandle, std::int32_t selector) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("-dDyvzd2jf0", VrTracker2NotInitialized); APS5_EXPORT("k8JDLkCrOgs", VrTracker2NotInitialized); APS5_EXPORT("-HFx+dnDSOI", VrTracker2NotInitialized); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 3217ba4d5a..58e11d0671 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -14,3 +14,5 @@ - [Anonymous diagnostic exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate pointer contracts for composite poses, configuration, camera images, sensor frames, tracking status and boundary parameters. The camera-image query takes a 32-bit byte count and an in/out frame identifier; diagnostics use six 512-byte text sections, and the mode setter uses a 12-byte size/mode/float record. Composite pose, sensor/status, configuration and boundary fields still require reconstruction; none of these exports supports initialized operation. - [Camera-frame exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) do not acquire or release native buffers. Their descriptor has a selector, two identifiers, dimensions, pitch, pixel pointer and buffer index; native acquisition writes the first 36 bytes, leaving natural tail padding untouched. The optional 48-byte metadata record and identifier meanings remain unresolved. The scalar-option setter/getter use a float value/output, but its units and meaning remain unresolved. + +- [Device and spatial queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Position queries use a three-float input or a device handle and a 36-byte size/status/two-vector/scalar result; the vector and scalar meanings remain unresolved. The bounds query uses the recovered 80-byte oriented box. Device-boundary operations consume a handle and selector; their internal parameter layout differs from the public boundary record and remains unresolved, as do the 28-byte device-calibration status and reset-pose input. From ea06e78756cd86aaa1774830a7918de8df468420 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:33:30 +0300 Subject: [PATCH 0732/1360] fix(vrtracker2): restore map and tracking flag arguments --- core/libs/prx/libSceVrTracker2/Export.cpp | 33 +++++++++++++++++++---- docs/dev/debt/libSceVrTracker2.md | 2 ++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 07d244a879..c029429275 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -209,6 +209,9 @@ static_assert(sizeof(VrTracker2SpatialQueryResult) == 36); static_assert(offsetof(VrTracker2SpatialQueryResult, vectors) == 8); static_assert(offsetof(VrTracker2SpatialQueryResult, value) == 32); +struct VrTracker2MapData; +struct VrTracker2MapStatus; + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -471,15 +474,27 @@ std::int32_t APS5_VABI VrTracker2ApplyDeviceBoundary(std::int32_t deviceHandle, return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -APS5_EXPORT("-dDyvzd2jf0", VrTracker2NotInitialized); -APS5_EXPORT("k8JDLkCrOgs", VrTracker2NotInitialized); +APS5_EXPORT("-dDyvzd2jf0", VrTracker2GetDeviceMapState); +std::int32_t APS5_VABI VrTracker2GetDeviceMapState(std::int32_t deviceHandle, std::int32_t* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("k8JDLkCrOgs", VrTracker2InitializeMap); +std::int32_t APS5_VABI VrTracker2InitializeMap(std::int32_t deviceHandle, const float* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("-HFx+dnDSOI", VrTracker2NotInitialized); APS5_EXPORT("wgDeDXirl0g", VrTracker2NotInitialized); APS5_EXPORT("a1kWSTYGBxg", VrTracker2NotInitialized); APS5_EXPORT("zUwxBEKr3fc", VrTracker2NotInitialized); APS5_EXPORT("TC7VTVzj0xc", VrTracker2NotInitialized); APS5_EXPORT("1DwROxtuANs", VrTracker2NotInitialized); -APS5_EXPORT("wwbt4s8cbMQ", VrTracker2NotInitialized); +APS5_EXPORT("wwbt4s8cbMQ", VrTracker2GetTrackingFlags); +std::int32_t APS5_VABI VrTracker2GetTrackingFlags(bool* active, std::uint8_t* flags) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("eGxBJ0IolOA", VrTracker2NotInitialized); APS5_EXPORT("9BaFhspHF+E", VrTracker2NotInitialized); APS5_EXPORT("lb9q5nJ4vzk", VrTracker2NotInitialized); @@ -487,7 +502,11 @@ APS5_EXPORT("ZsD1-thjXS4", VrTracker2NotInitialized); APS5_EXPORT("QdHQnM0XICI", VrTracker2NotInitialized); APS5_EXPORT("Ek4WMOvwv6g", VrTracker2NotInitialized); APS5_EXPORT("W1YAO6G+hNc", VrTracker2NotInitialized); -APS5_EXPORT("HAJB7irjl+0", VrTracker2NotInitialized); +APS5_EXPORT("HAJB7irjl+0", VrTracker2GetMapStatus); +std::int32_t APS5_VABI VrTracker2GetMapStatus(VrTracker2MapStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("x2mN50nKScs", VrTracker2NotInitialized); APS5_EXPORT("cCKExge5d3A", VrTracker2NotInitialized); APS5_EXPORT("gn6e5I4g9bw", VrTracker2NotInitialized); @@ -499,7 +518,11 @@ APS5_EXPORT("DBa3wov7dKQ", VrTracker2NotInitialized); APS5_EXPORT("ojTGClQ52r4", VrTracker2NotInitialized); APS5_EXPORT("oOLE62H66z0", VrTracker2NotInitialized); APS5_EXPORT("MHekhnRBKAw", VrTracker2NotInitialized); -APS5_EXPORT("JnSmj+5E8WY", VrTracker2NotInitialized); +APS5_EXPORT("JnSmj+5E8WY", VrTracker2GetMapData); +std::int32_t APS5_VABI VrTracker2GetMapData(VrTracker2MapData* data, std::uint32_t size) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("N1P+melAgb8", VrTracker2NotInitialized); APS5_EXPORT("-e7A3rAM3Yk", VrTracker2NotInitialized); APS5_EXPORT("RefT1spBleg", VrTracker2NotInitialized); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 58e11d0671..de8e5e0a03 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -16,3 +16,5 @@ - [Camera-frame exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) do not acquire or release native buffers. Their descriptor has a selector, two identifiers, dimensions, pitch, pixel pointer and buffer index; native acquisition writes the first 36 bytes, leaving natural tail padding untouched. The optional 48-byte metadata record and identifier meanings remain unresolved. The scalar-option setter/getter use a float value/output, but its units and meaning remain unresolved. - [Device and spatial queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Position queries use a three-float input or a device handle and a 36-byte size/status/two-vector/scalar result; the vector and scalar meanings remain unresolved. The bounds query uses the recovered 80-byte oriented box. Device-boundary operations consume a handle and selector; their internal parameter layout differs from the public boundary record and remains unresolved, as do the 28-byte device-calibration status and reset-pose input. + +- [Map and flag queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Map initialization takes a device handle and a pointer to one float; map data retrieval takes an output and a 32-bit size constrained to 2048. Map data and status field meanings remain unresolved. Tracking flags use a required boolean output and an optional byte output, rather than an argument-free alias. From eab1127190ab8b69095ed0f776e03bc9653b841f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:34:17 +0300 Subject: [PATCH 0733/1360] fix(vrtracker2): separate diagnostic record pointer contracts --- core/libs/prx/libSceVrTracker2/Export.cpp | 63 +++++++++++++++++++---- docs/dev/debt/libSceVrTracker2.md | 2 + 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index c029429275..931fa8a418 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -212,6 +212,15 @@ static_assert(offsetof(VrTracker2SpatialQueryResult, value) == 32); struct VrTracker2MapData; struct VrTracker2MapStatus; +struct VrTracker2RuntimeStatus; +struct VrTracker2DiagnosticState; +struct VrTracker2ExtendedDiagnosticState; +struct VrTracker2SensorDiagnostics; +struct VrTracker2DeviceDiagnostics; +struct VrTracker2TimingDiagnostics; +struct VrTracker2MapConfiguration; +struct VrTracker2SensorSummary; + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -489,19 +498,51 @@ APS5_EXPORT("wgDeDXirl0g", VrTracker2NotInitialized); APS5_EXPORT("a1kWSTYGBxg", VrTracker2NotInitialized); APS5_EXPORT("zUwxBEKr3fc", VrTracker2NotInitialized); APS5_EXPORT("TC7VTVzj0xc", VrTracker2NotInitialized); -APS5_EXPORT("1DwROxtuANs", VrTracker2NotInitialized); +APS5_EXPORT("1DwROxtuANs", VrTracker2GetRuntimeStatus); +std::int32_t APS5_VABI VrTracker2GetRuntimeStatus(VrTracker2RuntimeStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("wwbt4s8cbMQ", VrTracker2GetTrackingFlags); std::int32_t APS5_VABI VrTracker2GetTrackingFlags(bool* active, std::uint8_t* flags) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -APS5_EXPORT("eGxBJ0IolOA", VrTracker2NotInitialized); -APS5_EXPORT("9BaFhspHF+E", VrTracker2NotInitialized); -APS5_EXPORT("lb9q5nJ4vzk", VrTracker2NotInitialized); -APS5_EXPORT("ZsD1-thjXS4", VrTracker2NotInitialized); -APS5_EXPORT("QdHQnM0XICI", VrTracker2NotInitialized); -APS5_EXPORT("Ek4WMOvwv6g", VrTracker2NotInitialized); -APS5_EXPORT("W1YAO6G+hNc", VrTracker2NotInitialized); +APS5_EXPORT("eGxBJ0IolOA", VrTracker2GetDiagnosticState); +std::int32_t APS5_VABI VrTracker2GetDiagnosticState(VrTracker2DiagnosticState* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("9BaFhspHF+E", VrTracker2GetExtendedDiagnosticState); +std::int32_t APS5_VABI VrTracker2GetExtendedDiagnosticState(VrTracker2ExtendedDiagnosticState* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("lb9q5nJ4vzk", VrTracker2GetSensorDiagnostics); +std::int32_t APS5_VABI VrTracker2GetSensorDiagnostics(VrTracker2SensorDiagnostics* diagnostics) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("ZsD1-thjXS4", VrTracker2GetDeviceDiagnostics); +std::int32_t APS5_VABI VrTracker2GetDeviceDiagnostics(VrTracker2DeviceDiagnostics* diagnostics) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QdHQnM0XICI", VrTracker2GetTimingDiagnostics); +std::int32_t APS5_VABI VrTracker2GetTimingDiagnostics(VrTracker2TimingDiagnostics* diagnostics) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("Ek4WMOvwv6g", VrTracker2GetMapConfiguration); +std::int32_t APS5_VABI VrTracker2GetMapConfiguration(VrTracker2MapConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("W1YAO6G+hNc", VrTracker2SetMapConfiguration); +std::int32_t APS5_VABI VrTracker2SetMapConfiguration(const VrTracker2MapConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("HAJB7irjl+0", VrTracker2GetMapStatus); std::int32_t APS5_VABI VrTracker2GetMapStatus(VrTracker2MapStatus* status) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; @@ -526,7 +567,11 @@ std::int32_t APS5_VABI VrTracker2GetMapData(VrTracker2MapData* data, std::uint32 APS5_EXPORT("N1P+melAgb8", VrTracker2NotInitialized); APS5_EXPORT("-e7A3rAM3Yk", VrTracker2NotInitialized); APS5_EXPORT("RefT1spBleg", VrTracker2NotInitialized); -APS5_EXPORT("DsOfryhbm4U", VrTracker2NotInitialized); +APS5_EXPORT("DsOfryhbm4U", VrTracker2GetSensorSummary); +std::int32_t APS5_VABI VrTracker2GetSensorSummary(VrTracker2SensorSummary* summary) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + std::int32_t APS5_VABI VrTracker2NotInitialized() { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index de8e5e0a03..f1a9ea843c 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -18,3 +18,5 @@ - [Device and spatial queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Position queries use a three-float input or a device handle and a 36-byte size/status/two-vector/scalar result; the vector and scalar meanings remain unresolved. The bounds query uses the recovered 80-byte oriented box. Device-boundary operations consume a handle and selector; their internal parameter layout differs from the public boundary record and remains unresolved, as do the 28-byte device-calibration status and reset-pose input. - [Map and flag queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Map initialization takes a device handle and a pointer to one float; map data retrieval takes an output and a 32-bit size constrained to 2048. Map data and status field meanings remain unresolved. Tracking flags use a required boolean output and an optional byte output, rather than an argument-free alias. + +- [Diagnostic records](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have individual output-pointer signatures, but field interpretation remains incomplete. Native runtime status clears 88 bytes; diagnostic state writes 120 bytes, sensor diagnostics reach byte 339, device diagnostics reach byte 6983, timing diagnostics write 80 bytes, and sensor summary writes 32 bytes. The map-configuration getter/setter exchange the same 72-byte record. These exports still return only the uninitialized error. From e1cfd8adfba8f39fb94b96572154593019a00b2c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:35:47 +0300 Subject: [PATCH 0734/1360] fix(vrtracker2): restore coordinate and boundary fitting contracts --- core/libs/prx/libSceVrTracker2/Export.cpp | 63 +++++++++++++++++++---- docs/dev/debt/libSceVrTracker2.md | 4 +- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 931fa8a418..0f5bf941c7 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -221,6 +221,29 @@ struct VrTracker2TimingDiagnostics; struct VrTracker2MapConfiguration; struct VrTracker2SensorSummary; +struct VrTracker2CoordinateResultParameter { + std::uint32_t coordinateSystem; + std::uint32_t reserved; + VrTracker2ResultParameter parameter; +}; + +struct VrTracker2BoundaryFitResult { + std::uint8_t flags[4]; + float dimensions[4]; +}; + +struct VrTracker2VectorResult { + std::uint32_t size; + float value[3]; +}; + +struct VrTracker2ExtendedTrackingResult; + +static_assert(sizeof(VrTracker2CoordinateResultParameter) == 56); +static_assert(offsetof(VrTracker2CoordinateResultParameter, parameter) == 8); +static_assert(sizeof(VrTracker2BoundaryFitResult) == 20); +static_assert(sizeof(VrTracker2VectorResult) == 16); + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -288,7 +311,7 @@ std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParamet } APS5_EXPORT("1m6DD9BBmSE", VrTracker2GetRelativePose); -std::int32_t APS5_VABI VrTracker2GetRelativePose(std::int32_t firstDeviceHandle, std::int32_t secondDeviceHandle, VrTracker2RelativePoseResult* result) { +std::int32_t APS5_VABI VrTracker2GetRelativePose(std::uint32_t firstCoordinateSystem, std::uint32_t secondCoordinateSystem, VrTracker2RelativePoseResult* result) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } @@ -423,13 +446,13 @@ std::int32_t APS5_VABI VrTracker2GetScalarOption(float* value) { ; APS5_EXPORT("LQV4nz6wteY", VrTracker2NotInitialized); APS5_EXPORT("x0pGthEwf88", VrTracker2NotInitialized); -APS5_EXPORT("d5Fjs0rHkBg", VrTracker2SelectDevice); -std::int32_t APS5_VABI VrTracker2SelectDevice(std::int32_t deviceHandle) { +APS5_EXPORT("d5Fjs0rHkBg", VrTracker2SelectCoordinateSystem); +std::int32_t APS5_VABI VrTracker2SelectCoordinateSystem(std::uint32_t coordinateSystem) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } ; -APS5_EXPORT("Yed3ZONcuMk", VrTracker2GetSelectedDevice); -std::int32_t APS5_VABI VrTracker2GetSelectedDevice(std::int32_t* deviceHandle) { +APS5_EXPORT("Yed3ZONcuMk", VrTracker2GetSelectedCoordinateSystem); +std::int32_t APS5_VABI VrTracker2GetSelectedCoordinateSystem(std::uint32_t* coordinateSystem) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } ; @@ -493,11 +516,31 @@ std::int32_t APS5_VABI VrTracker2InitializeMap(std::int32_t deviceHandle, const return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -APS5_EXPORT("-HFx+dnDSOI", VrTracker2NotInitialized); -APS5_EXPORT("wgDeDXirl0g", VrTracker2NotInitialized); -APS5_EXPORT("a1kWSTYGBxg", VrTracker2NotInitialized); -APS5_EXPORT("zUwxBEKr3fc", VrTracker2NotInitialized); -APS5_EXPORT("TC7VTVzj0xc", VrTracker2NotInitialized); +APS5_EXPORT("-HFx+dnDSOI", VrTracker2GetCoordinateTrackingResult); +std::int32_t APS5_VABI VrTracker2GetCoordinateTrackingResult(const VrTracker2CoordinateResultParameter* parameter, VrTracker2ExtendedTrackingResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("wgDeDXirl0g", VrTracker2GetExtendedTrackingResult); +std::int32_t APS5_VABI VrTracker2GetExtendedTrackingResult(const VrTracker2ResultParameter* parameter, VrTracker2ExtendedTrackingResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("a1kWSTYGBxg", VrTracker2FitBoundary); +std::int32_t APS5_VABI VrTracker2FitBoundary(const VrTracker2BoundaryGeometry* boundary, VrTracker2BoundaryFitResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("zUwxBEKr3fc", VrTracker2FitBoundaryWithBounds); +std::int32_t APS5_VABI VrTracker2FitBoundaryWithBounds(const VrTracker2BoundaryGeometry* boundary, VrTracker2OrientedBoundingBox* firstBounds, VrTracker2OrientedBoundingBox* secondBounds, VrTracker2BoundaryFitResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("TC7VTVzj0xc", VrTracker2GetDeviceCenter); +std::int32_t APS5_VABI VrTracker2GetDeviceCenter(std::int32_t deviceHandle, VrTracker2VectorResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("1DwROxtuANs", VrTracker2GetRuntimeStatus); std::int32_t APS5_VABI VrTracker2GetRuntimeStatus(VrTracker2RuntimeStatus* status) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index f1a9ea843c..55cb5b67a7 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -9,7 +9,7 @@ - [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. -- [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two device handles and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. +- [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two coordinate-system selectors and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. - [Anonymous diagnostic exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate pointer contracts for composite poses, configuration, camera images, sensor frames, tracking status and boundary parameters. The camera-image query takes a 32-bit byte count and an in/out frame identifier; diagnostics use six 512-byte text sections, and the mode setter uses a 12-byte size/mode/float record. Composite pose, sensor/status, configuration and boundary fields still require reconstruction; none of these exports supports initialized operation. @@ -20,3 +20,5 @@ - [Map and flag queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Map initialization takes a device handle and a pointer to one float; map data retrieval takes an output and a 32-bit size constrained to 2048. Map data and status field meanings remain unresolved. Tracking flags use a required boolean output and an optional byte output, rather than an argument-free alias. - [Diagnostic records](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have individual output-pointer signatures, but field interpretation remains incomplete. Native runtime status clears 88 bytes; diagnostic state writes 120 bytes, sensor diagnostics reach byte 339, device diagnostics reach byte 6983, timing diagnostics write 80 bytes, and sensor summary writes 32 bytes. The map-configuration getter/setter exchange the same 72-byte record. These exports still return only the uninitialized error. + +- [Extended result and boundary fitting](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. The extended result spans 816 bytes; its coordinate-specific parameter prefixes the 48-byte query with a coordinate selector and four-byte padding. Boundary fitting takes the recovered point list, optionally returns two 80-byte bounds, and writes four byte flags plus four floats; flag and dimension semantics remain unresolved. The device-center output is a 16-byte size/three-float record. From 6efa62b7fa717cf6aa8bf589d16c96bfcde5de47 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:37:27 +0300 Subject: [PATCH 0735/1360] fix(vrtracker2): restore mesh and image pose signatures --- core/libs/prx/libSceVrTracker2/Export.cpp | 41 +++++++++++++++++++---- docs/dev/debt/libSceVrTracker2.md | 2 ++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 0f5bf941c7..db5458a308 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -244,6 +244,11 @@ static_assert(offsetof(VrTracker2CoordinateResultParameter, parameter) == 8); static_assert(sizeof(VrTracker2BoundaryFitResult) == 20); static_assert(sizeof(VrTracker2VectorResult) == 16); +struct VrTracker2MeshVertex; +struct VrTracker2TimedPoseResult; +struct VrTracker2ImagePoseResult; +struct VrTracker2ImageFrame; + extern "C" { std::int32_t APS5_VABI sceVrTracker2Finalize() { @@ -599,17 +604,41 @@ APS5_EXPORT("loU5lT92Zrw", VrTracker2NotInitialized); APS5_EXPORT("o6fsgA3R2Yc", VrTracker2NotInitialized); APS5_EXPORT("AeBtEIE0idk", VrTracker2NotInitialized); APS5_EXPORT("DBa3wov7dKQ", VrTracker2NotInitialized); -APS5_EXPORT("ojTGClQ52r4", VrTracker2NotInitialized); -APS5_EXPORT("oOLE62H66z0", VrTracker2NotInitialized); -APS5_EXPORT("MHekhnRBKAw", VrTracker2NotInitialized); +APS5_EXPORT("ojTGClQ52r4", VrTracker2BuildMesh); +std::int32_t APS5_VABI VrTracker2BuildMesh(VrTracker2MeshVertex* vertices, std::uint16_t* indices, std::uint32_t format) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("oOLE62H66z0", VrTracker2GetMapScalar); +std::int32_t APS5_VABI VrTracker2GetMapScalar(float* value, const VrTracker2MapData* data) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("MHekhnRBKAw", VrTracker2GetTimedPose); +std::int32_t APS5_VABI VrTracker2GetTimedPose(VrTracker2TimedPoseResult* result, std::uint64_t timestamp, std::uint64_t userTag) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("JnSmj+5E8WY", VrTracker2GetMapData); std::int32_t APS5_VABI VrTracker2GetMapData(VrTracker2MapData* data, std::uint32_t size) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -APS5_EXPORT("N1P+melAgb8", VrTracker2NotInitialized); -APS5_EXPORT("-e7A3rAM3Yk", VrTracker2NotInitialized); -APS5_EXPORT("RefT1spBleg", VrTracker2NotInitialized); +APS5_EXPORT("N1P+melAgb8", VrTracker2GetPoseScalar); +std::int32_t APS5_VABI VrTracker2GetPoseScalar(float* value) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("-e7A3rAM3Yk", VrTracker2SubmitImageFrame); +std::int32_t APS5_VABI VrTracker2SubmitImageFrame(const VrTracker2ImageFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("RefT1spBleg", VrTracker2GetImagePose); +std::int32_t APS5_VABI VrTracker2GetImagePose(VrTracker2ImagePoseResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("DsOfryhbm4U", VrTracker2GetSensorSummary); std::int32_t APS5_VABI VrTracker2GetSensorSummary(VrTracker2SensorSummary* summary) { return VR_TRACKER2_ERROR_NOT_INITIALIZED; diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 55cb5b67a7..8e138fc9db 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -22,3 +22,5 @@ - [Diagnostic records](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have individual output-pointer signatures, but field interpretation remains incomplete. Native runtime status clears 88 bytes; diagnostic state writes 120 bytes, sensor diagnostics reach byte 339, device diagnostics reach byte 6983, timing diagnostics write 80 bytes, and sensor summary writes 32 bytes. The map-configuration getter/setter exchange the same 72-byte record. These exports still return only the uninitialized error. - [Extended result and boundary fitting](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. The extended result spans 816 bytes; its coordinate-specific parameter prefixes the 48-byte query with a coordinate selector and four-byte padding. Boundary fitting takes the recovered point list, optionally returns two 80-byte bounds, and writes four byte flags plus four floats; flag and dimension semantics remain unresolved. The device-center output is a 16-byte size/three-float record. + +- [Mesh and image-pose utilities](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Mesh generation takes vertex and 16-bit index outputs plus a 32-bit format. Timed-pose queries take an in/out result, timestamp and user tag. Image submission consumes a record with format at byte 16 and a data pointer at byte 80; image-pose retrieval uses an in/out record with a buffer pointer and capacity. Record fields and initialized algorithms remain incomplete. Scalar queries use float outputs; the map-scalar query also consumes map data. From 23478ef090f024b8b74efac0dd69fb798ddd179f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:43:01 +0300 Subject: [PATCH 0736/1360] fix(jpegenc): validate restart interval bounds --- core/libs/prx/libSceJpegEnc/Export.cpp | 14 ++++++-------- core/libs/tests/GuestJpegEnc.cpp | 5 +++-- docs/dev/debt/libSceJpegEnc.md | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index 6de513f53d..4ebcc09712 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -82,7 +82,7 @@ std::int32_t validateEncodeParam(const JpegEncEncodeParam* param) { if (param->sampling_type != SAMPLING_TYPE_FULL && param->sampling_type != SAMPLING_TYPE_422 && param->sampling_type != SAMPLING_TYPE_420) { return SCE_JPEG_ENC_ERROR_INVALID_PARAM; } - if (param->restart_interval > static_cast<std::int32_t>(MAX_IMAGE_DIMENSION)) return SCE_JPEG_ENC_ERROR_INVALID_PARAM; + if (param->restart_interval < RESTART_INTERVAL_PER_ROW || param->restart_interval > static_cast<std::int32_t>(MAX_IMAGE_DIMENSION)) return SCE_JPEG_ENC_ERROR_INVALID_PARAM; switch (param->pixel_format) { case PIXEL_FORMAT_R8G8B8A8: @@ -186,25 +186,23 @@ int32_t APS5_VABI sceJpegEncDelete(void* handle) { return 0; } -int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param, JpegEncOutputInfo* output_info) { +int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param, JpegEncOutputInfo* outputInfo) { if (!toEncoder(handle)) return SCE_JPEG_ENC_ERROR_INVALID_HANDLE; 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 < 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<std::uint8_t> pixels = toPackedPixels(*param, channels); const std::uint32_t restartBlocks = param->restart_interval > 0 ? static_cast<std::uint32_t>(param->restart_interval) : 0; const std::uint32_t restartRows = param->restart_interval == RESTART_INTERVAL_PER_ROW ? 1 : 0; - const std::vector<std::uint8_t> jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param), - restartBlocks, restartRows); + const std::vector<std::uint8_t> 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()); - if (output_info) { - output_info->size = static_cast<std::uint32_t>(jpeg.size()); - output_info->height = param->image_height; + if (outputInfo) { + outputInfo->size = static_cast<std::uint32_t>(jpeg.size()); + outputInfo->height = param->image_height; } return 0; } diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 52437df7a5..8206423e5f 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -278,8 +278,9 @@ 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 = -2; }); })); - Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }); })); + Require(encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = -2; }) == invalidParam); + Require(encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }) == invalidParam); + Require(encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; p.restart_interval = -2; }) == invalidParam); constexpr std::uint32_t wideWidth = 48; constexpr std::uint32_t wideHeight = 32; diff --git a/docs/dev/debt/libSceJpegEnc.md b/docs/dev/debt/libSceJpegEnc.md index 2bff53d167..d64816c332 100644 --- a/docs/dev/debt/libSceJpegEnc.md +++ b/docs/dev/debt/libSceJpegEnc.md @@ -1,5 +1,5 @@ # libSceJpegEnc technical debt -### Unknown function info +### Unimplemented functionality -- [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 +- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - MJPEG mode throws; its encoding path is not implemented. Restart intervals are confirmed by the native validator, MCU counter and DRI writer: 0 disables restart markers, -1 selects one MCU row, and 1 through 65535 specify an MCU count. Other values return the invalid-parameter error. From 65f686c33672c51d7e307b6f9e47f9d781f96c9d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:44:50 +0300 Subject: [PATCH 0737/1360] fix(jpegdec): restore error codes and work memory contract --- core/libs/prx/libSceJpegDec/Export.cpp | 55 +++++++++++++++----------- core/libs/tests/GuestJpegDec.cpp | 37 ++++++++++------- docs/dev/debt/libSceJpegDec.md | 2 +- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 19ec61c18b..5232a524e1 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -18,18 +18,18 @@ static_assert(sizeof(JpegDecImageInfo) == 0x24); namespace { -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_ADDR = static_cast<std::int32_t>(0x80690001); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_SIZE = static_cast<std::int32_t>(0x80690002); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_PARAM = static_cast<std::int32_t>(0x80690003); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_HANDLE = static_cast<std::int32_t>(0x80690004); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast<std::int32_t>(0x80690005); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast<std::int32_t>(0x80690010); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast<std::int32_t>(0x80690012); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_ADDR = static_cast<std::int32_t>(0x80650001); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_SIZE = static_cast<std::int32_t>(0x80650002); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_PARAM = static_cast<std::int32_t>(0x80650003); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_HANDLE = static_cast<std::int32_t>(0x80650004); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast<std::int32_t>(0x80650005); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast<std::int32_t>(0x80650010); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast<std::int32_t>(0x80650012); constexpr std::uint32_t ATTRIBUTE_CSC_ORDINARY = 1; constexpr std::uint32_t ATTRIBUTE_CSC_FULL = 2; -constexpr std::uint32_t MEMORY_SIZE = 0x20; -constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +constexpr std::uint32_t MEMORY_OVERHEAD = 29536; +constexpr std::uintptr_t HANDLE_ALIGNMENT = 32; constexpr std::uint16_t DECODE_MODE_NORMAL = 0; constexpr std::uint16_t DOWN_SCALE_NONE = 1; @@ -48,17 +48,24 @@ constexpr std::uint32_t MAX_PACKED_DIMENSION = 32767; struct Context { Context* self; std::uint32_t attribute; + std::uint32_t maxImageWidth; }; -static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); +static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_OVERHEAD); std::int32_t validateCreateParam(const JpegDecCreateParam* param) { - if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->this_size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->attribute != ATTRIBUTE_CSC_ORDINARY && param->attribute != ATTRIBUTE_CSC_FULL) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->max_image_width == 0 || param->max_image_width > 65535) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; return 0; } +std::uint32_t workMemorySize(const JpegDecCreateParam& param) { + const std::uint32_t alignedWidth = (param.max_image_width + 31) & ~std::uint32_t{31}; + return MEMORY_OVERHEAD + (alignedWidth + 16) * (param.attribute == ATTRIBUTE_CSC_ORDINARY ? 54 : 166); +} + std::uint32_t requireDownScale(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); if (downScale != DOWN_SCALE_NONE && downScale != DOWN_SCALE_HALF && downScale != DOWN_SCALE_QUARTER && downScale != DOWN_SCALE_EIGHTH) @@ -92,25 +99,27 @@ std::span<const std::uint8_t> toBytes(const void* data, std::uint32_t size) { extern "C" { -int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memory_address, uint32_t memory_size, void** handle) { +int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - if (!memory_address || !handle) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; - if (memory_size < MEMORY_SIZE) return SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY; - const auto address = reinterpret_cast<std::uintptr_t>(memory_address); + if (!memoryAddress) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (memorySize < workMemorySize(*param)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (!handle) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + const auto address = reinterpret_cast<std::uintptr_t>(memoryAddress); const std::uintptr_t aligned = (address + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); auto* context = new (reinterpret_cast<void*>(aligned)) Context{}; context->self = context; context->attribute = param->attribute; + context->maxImageWidth = (param->max_image_width + 31) & ~std::uint32_t{31}; *handle = context; return 0; } -int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param, JpegDecImageInfo* image_info) { +int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param, JpegDecImageInfo* imageInfo) { const Context* context = toContext(handle); if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; (void)context; - if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (!param->jpeg_mem_addr || !param->image_mem_addr || (reinterpret_cast<std::uintptr_t>(param->image_mem_addr) & 3u) != 0) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; @@ -153,7 +162,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param } } - if (image_info) fillImageInfo(*header, downScale, image_info); + if (imageInfo) fillImageInfo(*header, downScale, imageInfo); if (outputWidth > MAX_PACKED_DIMENSION || outputHeight > MAX_PACKED_DIMENSION) return 0; return static_cast<std::int32_t>(outputWidth << 16 | outputHeight); } @@ -165,21 +174,21 @@ int32_t APS5_VABI sceJpegDecDelete(void* handle) { return 0; } -int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecImageInfo* image_info) { - if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; +int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecImageInfo* imageInfo) { + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr || !imageInfo) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::optional<Decoder::Jpeg::Header> header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; - fillImageInfo(*header, downScale, image_info); + fillImageInfo(*header, downScale, imageInfo); return 0; } int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam* param) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - return static_cast<std::int32_t>(MEMORY_SIZE); + return static_cast<std::int32_t>(workMemorySize(*param)); } int APS5_VABI sceJpegDecDecodeWithInputControl() { diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 018dba662d..93c5242f8c 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -68,17 +68,16 @@ static JpegDecDecodeParam DecodeParam(const std::vector<std::uint8_t>& jpeg, std } int main() { - constexpr std::int32_t invalidAddr = static_cast<std::int32_t>(0x80690001); - constexpr std::int32_t invalidSize = static_cast<std::int32_t>(0x80690002); - constexpr std::int32_t invalidParam = static_cast<std::int32_t>(0x80690003); - constexpr std::int32_t invalidHandle = static_cast<std::int32_t>(0x80690004); - constexpr std::int32_t invalidWorkMemory = static_cast<std::int32_t>(0x80690005); - constexpr std::int32_t invalidData = static_cast<std::int32_t>(0x80690010); + constexpr std::int32_t invalidAddr = static_cast<std::int32_t>(0x80650001); + constexpr std::int32_t invalidSize = static_cast<std::int32_t>(0x80650002); + constexpr std::int32_t invalidParam = static_cast<std::int32_t>(0x80650003); + constexpr std::int32_t invalidHandle = static_cast<std::int32_t>(0x80650004); + constexpr std::int32_t invalidData = static_cast<std::int32_t>(0x80650010); const std::vector<std::uint8_t> jpeg = TestJpeg(7, 5, 3); JpegDecImageInfo info{}; const JpegDecParseParam parseParam{jpeg.data(), static_cast<std::uint32_t>(jpeg.size()), 0, 1}; - Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); + Require(sceJpegDecParseHeader(nullptr, &info) == invalidAddr); Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); @@ -106,10 +105,17 @@ int main() { const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); - Require(memorySize > 0); + Require(memorySize == 32128); const JpegDecCreateParam fullParam{sizeof(JpegDecCreateParam), 2, info.image_width}; - Require(sceJpegDecQueryMemorySize(&fullParam) == memorySize); - Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); + const std::int32_t fullMemorySize = sceJpegDecQueryMemorySize(&fullParam); + Require(fullMemorySize == 37504); + for (const std::uint32_t width : {0u, 65536u, UINT32_MAX}) { + const JpegDecCreateParam badWidth{sizeof(JpegDecCreateParam), 1, width}; + Require(sceJpegDecQueryMemorySize(&badWidth) == invalidParam); + } + const JpegDecCreateParam largestWidth{sizeof(JpegDecCreateParam), 1, 65535}; + Require(sceJpegDecQueryMemorySize(&largestWidth) == 3569344); + Require(sceJpegDecQueryMemorySize(nullptr) == invalidAddr); const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 4, 1, 7}; Require(sceJpegDecQueryMemorySize(&badSize) == invalidSize); for (const std::uint32_t attribute : {0u, 3u}) { @@ -117,12 +123,13 @@ int main() { Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); } - std::vector<std::uint8_t> memory(static_cast<std::size_t>(memorySize) + 8); + std::vector<std::uint8_t> memory(static_cast<std::size_t>(fullMemorySize) + 32); void* handle = nullptr; - Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); + Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidSize); Require(sceJpegDecCreate(¶m, memory.data(), static_cast<std::uint32_t>(memorySize), nullptr) == invalidAddr); - Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast<std::uint32_t>(memorySize), &handle) == 0); - Require(handle != nullptr); + Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast<std::uint32_t>(fullMemorySize), &handle) == 0); + Require(handle != nullptr && reinterpret_cast<std::uintptr_t>(handle) % 32 == 0); + Require(sceJpegDecCreate(¶m, memory.data(), 0, nullptr) == invalidSize); Require(sceJpegDecDelete(nullptr) == invalidHandle); Require(sceJpegDecDelete(handle) == 0); @@ -134,7 +141,7 @@ int main() { std::vector<std::uint8_t> image(7 * 5 * 4, 0xCC); JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); - Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); + Require(sceJpegDecDecode(handle, nullptr, &info) == invalidAddr); JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index e5b7252fa0..92ff01d77a 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. The error codes are libScePngDec's: no reference lists the JpegDec ones, and SharpProspero raises 0x80650020 on its JpegDec path where it raises libScePngDec's 0x80690020 on its PngDec path. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is libScePngDec's +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is confirmed by the native export ### Functional From a058d8d8faadb4e4cd2c6307afa7fcbc3dc8090a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:45:58 +0300 Subject: [PATCH 0738/1360] fix(jpegdec): enforce native decode parameter limits --- core/libs/prx/libSceJpegDec/Export.cpp | 39 +++++++++++++++++--------- core/libs/tests/GuestJpegDec.cpp | 20 ++++++++++--- docs/dev/debt/libSceJpegDec.md | 4 +-- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 5232a524e1..43883fc455 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -22,7 +22,7 @@ constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_ADDR = static_cast<std::int32_ constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_SIZE = static_cast<std::int32_t>(0x80650002); constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_PARAM = static_cast<std::int32_t>(0x80650003); constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_HANDLE = static_cast<std::int32_t>(0x80650004); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast<std::int32_t>(0x80650005); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_IMAGE_TOO_WIDE = static_cast<std::int32_t>(0x80650006); constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast<std::int32_t>(0x80650010); constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast<std::int32_t>(0x80650012); @@ -66,11 +66,12 @@ std::uint32_t workMemorySize(const JpegDecCreateParam& param) { return MEMORY_OVERHEAD + (alignedWidth + 16) * (param.attribute == ATTRIBUTE_CSC_ORDINARY ? 54 : 166); } -std::uint32_t requireDownScale(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { +bool validDecodeOptions(std::uint16_t decodeMode, std::uint16_t downScale) { + return decodeMode <= 1 && (downScale == DOWN_SCALE_NONE || downScale == DOWN_SCALE_HALF || downScale == DOWN_SCALE_QUARTER || downScale == DOWN_SCALE_EIGHTH); +} + +void requireDecodeMode(const char* function, std::uint16_t decodeMode) { if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); - if (downScale != DOWN_SCALE_NONE && downScale != DOWN_SCALE_HALF && downScale != DOWN_SCALE_QUARTER && downScale != DOWN_SCALE_EIGHTH) - throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); - return downScale; } Context* toContext(void* handle) { @@ -118,13 +119,16 @@ int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memory int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param, JpegDecImageInfo* imageInfo) { const Context* context = toContext(handle); if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; - (void)context; if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; - if (!param->jpeg_mem_addr || !param->image_mem_addr || (reinterpret_cast<std::uintptr_t>(param->image_mem_addr) & 3u) != 0) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if ((reinterpret_cast<std::uintptr_t>(param->image_mem_addr) & 3) != 0 && (param->pixel_format == 0 || param->pixel_format == 1 || param->pixel_format == 9 || param->pixel_format == 10)) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (param->alpha_value > 0xFF) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); + if (!validDecodeOptions(param->decode_mode, param->down_scale)) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->pixel_format != 0 && param->pixel_format != 1 && param->pixel_format != 9 && param->pixel_format != 10 && param->pixel_format != 11) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->alpha_value > 255) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + requireDecodeMode(__func__, param->decode_mode); + if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) throw std::runtime_error("sceJpegDecDecode: pixel format is not implemented"); + const std::uint32_t downScale = param->down_scale; const std::span<const std::uint8_t> jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional<Decoder::Jpeg::Header> header = Decoder::Jpeg::ParseHeader(jpeg); @@ -134,8 +138,12 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param const std::uint64_t rowSize = static_cast<std::uint64_t>(outputWidth) * BYTES_PER_PIXEL; const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; - if (pitch < rowSize) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if ((outputHeight - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->image_pitch != 0) { + if (pitch < rowSize || pitch % 4 != 0 || pitch * outputHeight >= 0x80000000 || pitch * outputHeight > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + } else if (pitch * outputHeight > param->image_mem_size) { + return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + } + if (((outputWidth + 31) & ~std::uint32_t{31}) > context->maxImageWidth) return SCE_JPEG_DEC_ERROR_IMAGE_TOO_WIDE; const std::optional<Decoder::Jpeg::Image> image = downScale == DOWN_SCALE_NONE ? Decoder::Jpeg::Decode(jpeg) : Decoder::Jpeg::DecodeScaled(jpeg, downScale); if (!image || image->width != outputWidth || image->height != outputHeight) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; @@ -176,9 +184,12 @@ int32_t APS5_VABI sceJpegDecDelete(void* handle) { int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecImageInfo* imageInfo) { if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; - if (!param->jpeg_mem_addr || !imageInfo) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); + if (!validDecodeOptions(param->decode_mode, param->down_scale)) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!imageInfo) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + requireDecodeMode(__func__, param->decode_mode); + const std::uint32_t downScale = param->down_scale; const std::optional<Decoder::Jpeg::Header> header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; fillImageInfo(*header, downScale, imageInfo); diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 93c5242f8c..1e41580fd3 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -83,7 +83,7 @@ int main() { Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); for (const std::uint16_t downScale : {std::uint16_t{0}, std::uint16_t{3}, std::uint16_t{16}}) { const JpegDecParseParam unknownScaleParse{jpeg.data(), static_cast<std::uint32_t>(jpeg.size()), 0, downScale}; - Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&unknownScaleParse, &info); })); + Require(sceJpegDecParseHeader(&unknownScaleParse, &info) == invalidParam); } const JpegDecParseParam otherModeParse{jpeg.data(), static_cast<std::uint32_t>(jpeg.size()), 1, 1}; Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&otherModeParse, &info); })); @@ -147,7 +147,16 @@ int main() { Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); JpegDecDecodeParam unknownScale = decodeParam; unknownScale.down_scale = 3; - Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &unknownScale, &info); })); + Require(sceJpegDecDecode(handle, &unknownScale, &info) == invalidParam); + JpegDecDecodeParam invalidOptions = decodeParam; + invalidOptions.alpha_value = 256; + Require(sceJpegDecDecode(handle, &invalidOptions, &info) == invalidParam); + invalidOptions = decodeParam; + invalidOptions.decode_mode = 2; + Require(sceJpegDecDecode(handle, &invalidOptions, &info) == invalidParam); + invalidOptions = decodeParam; + invalidOptions.image_mem_addr = image.data() + 1; + Require(sceJpegDecDecode(handle, &invalidOptions, &info) == invalidAddr); std::vector<std::uint8_t> invalidAlphaImage(7 * 5 * 4, 0xCC); JpegDecDecodeParam invalidAlpha = DecodeParam(jpeg, invalidAlphaImage, 7 * 4); invalidAlpha.alpha_value = 0x100; @@ -175,7 +184,7 @@ int main() { } constexpr std::uint32_t pitch = 7 * 4 + 8; - std::vector<std::uint8_t> bgra(4 * pitch + 7 * 4, 0xEE); + std::vector<std::uint8_t> bgra(5 * pitch, 0xEE); JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra, pitch); bgraParam.pixel_format = 1; bgraParam.alpha_value = 0x77; @@ -192,6 +201,9 @@ int main() { } const std::vector<std::uint8_t> large = TestJpeg(37, 21, 3); + std::vector<std::uint8_t> wideOutput(37 * 21 * 4); + JpegDecDecodeParam wideParam = DecodeParam(large, wideOutput, 0); + Require(sceJpegDecDecode(handle, &wideParam, nullptr) == static_cast<std::int32_t>(0x80650006)); const std::vector<std::uint8_t> largeGray = TestJpeg(19, 11, 1); for (const auto* source : {&large, &largeGray}) { const std::optional<Decoder::Jpeg::Header> sourceHeader = Decoder::Jpeg::ParseHeader(*source); @@ -215,7 +227,7 @@ int main() { } const std::uint32_t paddedPitch = width * 4 + 4; - std::vector<std::uint8_t> padded((height - 1) * paddedPitch + width * 4, 0xEE); + std::vector<std::uint8_t> padded(height * paddedPitch, 0xEE); JpegDecDecodeParam paddedParam = DecodeParam(*source, padded, paddedPitch); paddedParam.down_scale = downScale; paddedParam.pixel_format = 1; diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 92ff01d77a..fc660f7950 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -3,8 +3,8 @@ ### Unknown function info - [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is confirmed by the native export +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. the output-width limit, aligned output address, pitch, alpha and option validation are confirmed by native checks, and the packed `width << 16 | height` decode result is confirmed by the native export ### Functional -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified. In `sceJpegDecDecode`, rejecting an output `image_mem_addr` not aligned to 4 bytes and rejecting an `alpha_value` above 0xFF follow libScePngDec validation and are not verified from a libSceJpegDec binary disassembly +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode mode 1 and pixel formats 9, 10 and 11 throw; invalid modes and down scales return the native invalid-parameter error, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The native ceiling division and packed scaled output dimensions are confirmed; exact decoded pixel values remain unverified From c3eba45600e097c154cf60ff51426a9f12388d24 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:47:59 +0300 Subject: [PATCH 0739/1360] fix(jpegdec): recover image metadata and coefficient requirements --- core/Decoder/Jpeg/include/Decoder/Jpeg.hpp | 4 +++ core/Decoder/Jpeg/src/Jpeg.cpp | 28 ++++++++++++++++++- core/libs/prx/libSceJpegDec/Export.cpp | 32 ++++++++++++++++++++-- core/libs/tests/GuestJpegDec.cpp | 27 ++++++++++++++++++ docs/dev/debt/libSceJpegDec.md | 2 +- 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index ed59467ac3..62d09b0698 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -19,6 +19,10 @@ struct Header { std::uint32_t width; std::uint32_t height; std::uint32_t channels; + std::uint8_t componentIds[4]{}; + std::uint8_t samplingFactors[4]{}; + bool progressive = false; + bool app0BeforeFrame = false; }; enum class Sampling { diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index e8e6aa19c3..96ace625a4 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -126,7 +126,33 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) { int channels = 0; if (!stbi_info_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels)) return std::nullopt; if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt; - return Header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)}; + Header header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)}; + std::size_t offset = 2; + while (offset < jpeg.size()) { + if (jpeg[offset++] != 0xFF) return std::nullopt; + while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset; + if (offset == jpeg.size()) return std::nullopt; + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xD9 || marker == 0xDA) return std::nullopt; + if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD8)) continue; + if (jpeg.size() - offset < 2) return std::nullopt; + const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1]; + if (length < 2 || length > jpeg.size() - offset) return std::nullopt; + if (marker == 0xE0) header.app0BeforeFrame = true; + if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) { + if (length < 8) return std::nullopt; + header.channels = jpeg[offset + 7]; + if (header.channels == 0 || header.channels > 4 || length != 8 + 3 * header.channels) return std::nullopt; + header.progressive = marker == 0xC2; + for (std::uint32_t i = 0; i < header.channels; ++i) { + header.componentIds[i] = jpeg[offset + 8 + 3 * i]; + header.samplingFactors[i] = jpeg[offset + 9 + 3 * i]; + } + return header; + } + offset += length; + } + return std::nullopt; } std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) { diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 43883fc455..efec1932dc 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -40,7 +40,9 @@ constexpr std::uint16_t DOWN_SCALE_EIGHTH = 8; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; -constexpr std::uint16_t COLOR_SPACE_RGB = 3; +constexpr std::uint16_t COLOR_SPACE_UNKNOWN = 0; +constexpr std::uint16_t COLOR_SPACE_YCBCR = 1; +constexpr std::uint16_t COLOR_SPACE_GRAY = 2; constexpr std::uint32_t BYTES_PER_PIXEL = 4; constexpr std::uint32_t MAX_PACKED_DIMENSION = 32767; @@ -85,9 +87,29 @@ void fillImageInfo(const Decoder::Jpeg::Header& header, std::uint32_t downScale, *info = {}; info->image_width = header.width; info->image_height = header.height; - info->color_space = COLOR_SPACE_RGB; + info->color_space = COLOR_SPACE_UNKNOWN; + if (header.channels == 1) info->color_space = COLOR_SPACE_GRAY; + if (header.channels == 3 && (header.app0BeforeFrame || (header.componentIds[0] == 1 && header.componentIds[1] == 2 && header.componentIds[2] == 3))) info->color_space = COLOR_SPACE_YCBCR; info->component_number = static_cast<std::uint16_t>(header.channels); - info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + std::uint32_t maxHorizontal = 1; + std::uint32_t maxVertical = 1; + std::uint32_t blocksPerMcu = 0; + for (std::uint32_t i = 0; i < header.channels; ++i) { + info->component_id[i] = header.componentIds[i]; + info->sampling_factor[i] = header.samplingFactors[i]; + const std::uint32_t horizontal = header.channels == 1 ? 1 : header.samplingFactors[i] >> 4; + const std::uint32_t vertical = header.channels == 1 ? 1 : header.samplingFactors[i] & 15; + if (horizontal > maxHorizontal) maxHorizontal = horizontal; + if (vertical > maxVertical) maxVertical = vertical; + blocksPerMcu += horizontal * vertical; + } + if (info->color_space == COLOR_SPACE_GRAY) info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + if (info->color_space == COLOR_SPACE_YCBCR) info->suitable_csc_attribute = (12 / maxHorizontal) * blocksPerMcu < 37 ? ATTRIBUTE_CSC_ORDINARY : ATTRIBUTE_CSC_FULL; + if (header.progressive) { + const std::uint32_t mcuColumns = (header.width + 8 * maxHorizontal - 1) / (8 * maxHorizontal); + const std::uint32_t mcuRows = (header.height + 8 * maxVertical - 1) / (8 * maxVertical); + info->coefficient_mem_size = mcuColumns * mcuRows * blocksPerMcu * 128 + 64; + } info->output_image_width = Decoder::Jpeg::ScaledDimension(header.width, downScale); info->output_image_height = Decoder::Jpeg::ScaledDimension(header.height, downScale); } @@ -133,6 +155,10 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param const std::span<const std::uint8_t> jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional<Decoder::Jpeg::Header> header = Decoder::Jpeg::ParseHeader(jpeg); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; + JpegDecImageInfo parsedInfo{}; + fillImageInfo(*header, downScale, &parsedInfo); + if (parsedInfo.coefficient_mem_size != 0 && (!param->coefficient_mem_addr || param->coefficient_mem_size < parsedInfo.coefficient_mem_size)) return static_cast<std::int32_t>(0x80650005); + if (parsedInfo.color_space == COLOR_SPACE_UNKNOWN) return static_cast<std::int32_t>(0x80650011); const std::uint32_t outputWidth = Decoder::Jpeg::ScaledDimension(header->width, downScale); const std::uint32_t outputHeight = Decoder::Jpeg::ScaledDimension(header->height, downScale); diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 1e41580fd3..ff526ac2b8 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -98,10 +98,31 @@ int main() { const JpegDecParseParam grayParse{grayJpeg.data(), static_cast<std::uint32_t>(grayJpeg.size()), 0, 1}; Require(sceJpegDecParseHeader(&grayParse, &info) == 0); Require(info.image_width == 3 && info.image_height == 2 && info.component_number == 1); + Require(info.color_space == 2 && info.component_id[0] == 1 && info.sampling_factor[0] == 0x11); + Require(info.component_id[1] == 0 && info.sampling_factor[1] == 0); Require(sceJpegDecParseHeader(&parseParam, &info) == 0); Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); Require(info.output_image_width == 7 && info.output_image_height == 5); Require(info.coefficient_mem_size == 0 && info.suitable_csc_attribute == 1); + Require(info.color_space == 1); + for (std::uint32_t i = 0; i < 3; ++i) Require(info.component_id[i] == i + 1 && info.sampling_factor[i] == 0x11); + std::vector<std::uint8_t> progressiveHeader = jpeg; + bool changedFrame = false; + for (std::size_t offset = 2; offset + 3 < progressiveHeader.size();) { + const std::uint8_t marker = progressiveHeader[offset + 1]; + const std::size_t length = (static_cast<std::size_t>(progressiveHeader[offset + 2]) << 8) | progressiveHeader[offset + 3]; + if (marker == 0xC0) { + progressiveHeader[offset + 1] = 0xC2; + changedFrame = true; + break; + } + offset += 2 + length; + } + Require(changedFrame); + const JpegDecParseParam progressiveParse{progressiveHeader.data(), static_cast<std::uint32_t>(progressiveHeader.size()), 0, 1}; + Require(sceJpegDecParseHeader(&progressiveParse, &info) == 0); + Require(info.coefficient_mem_size == 448); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); @@ -140,6 +161,12 @@ int main() { std::vector<std::uint8_t> image(7 * 5 * 4, 0xCC); JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); + JpegDecDecodeParam progressiveDecode = DecodeParam(progressiveHeader, image, 7 * 4); + Require(sceJpegDecDecode(handle, &progressiveDecode, &info) == static_cast<std::int32_t>(0x80650005)); + std::array<std::uint8_t, 447> insufficientCoefficients{}; + progressiveDecode.coefficient_mem_addr = insufficientCoefficients.data(); + progressiveDecode.coefficient_mem_size = static_cast<std::uint32_t>(insufficientCoefficients.size()); + Require(sceJpegDecDecode(handle, &progressiveDecode, &info) == static_cast<std::int32_t>(0x80650005)); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); Require(sceJpegDecDecode(handle, nullptr, &info) == invalidAddr); JpegDecDecodeParam badFormat = decodeParam; diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index fc660f7950..dad95387c4 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. the output-width limit, aligned output address, pitch, alpha and option validation are confirmed by native checks, and the packed `width << 16 | height` decode result is confirmed by the native export +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Native field copies confirm the image-info layout. Color space, component identifiers, sampling factors, CSC selection, progressive coefficient-buffer size and scaled dimensions are recovered from the native header helpers. The host now checks the aligned output-width limit and progressive coefficient-buffer capacity. The native decoder also limits internal row-buffer consumption for unusual sampling factors; that additional capacity check is not yet implemented. ### Functional From d0417623a77785fddc01528d98e149e1e844a976 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:49:23 +0300 Subject: [PATCH 0740/1360] fix(jpegdec): enforce component row buffer capacity --- core/libs/prx/libSceJpegDec/Export.cpp | 24 +++++++++++++++++++++++- core/libs/tests/GuestJpegDec.cpp | 13 +++++++++++++ docs/dev/debt/libSceJpegDec.md | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index efec1932dc..aeddcbeb6c 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -51,6 +51,7 @@ struct Context { Context* self; std::uint32_t attribute; std::uint32_t maxImageWidth; + std::uint32_t rowBufferCapacity; }; static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_OVERHEAD); @@ -114,6 +115,26 @@ void fillImageInfo(const Decoder::Jpeg::Header& header, std::uint32_t downScale, info->output_image_height = Decoder::Jpeg::ScaledDimension(header.height, downScale); } +std::uint32_t rowBufferSize(const Decoder::Jpeg::Header& header, std::uint32_t downScale) { + std::uint32_t maxHorizontal = 1; + if (header.channels != 1) { + for (std::uint32_t i = 0; i < header.channels; ++i) { + const std::uint32_t horizontal = header.samplingFactors[i] >> 4; + if (horizontal > maxHorizontal) maxHorizontal = horizontal; + } + } + const std::uint32_t mcuColumns = (header.width + 8 * maxHorizontal - 1) / (8 * maxHorizontal); + const std::uint32_t blockSize = 8 / downScale; + std::uint32_t size = 0; + for (std::uint32_t i = 0; i < header.channels; ++i) { + const std::uint32_t horizontal = header.channels == 1 ? 1 : header.samplingFactors[i] >> 4; + const std::uint32_t vertical = header.channels == 1 ? 1 : header.samplingFactors[i] & 15; + const std::uint32_t stride = (mcuColumns * blockSize * horizontal + 7) & ~std::uint32_t{7}; + size += stride * vertical * blockSize; + } + return (size + 31) & ~std::uint32_t{31}; +} + std::span<const std::uint8_t> toBytes(const void* data, std::uint32_t size) { return {static_cast<const std::uint8_t*>(data), size}; } @@ -134,6 +155,7 @@ int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memory context->self = context; context->attribute = param->attribute; context->maxImageWidth = (param->max_image_width + 31) & ~std::uint32_t{31}; + context->rowBufferCapacity = (context->maxImageWidth + 16) * (param->attribute == ATTRIBUTE_CSC_ORDINARY ? 24 : 80); *handle = context; return 0; } @@ -169,7 +191,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param } else if (pitch * outputHeight > param->image_mem_size) { return SCE_JPEG_DEC_ERROR_INVALID_SIZE; } - if (((outputWidth + 31) & ~std::uint32_t{31}) > context->maxImageWidth) return SCE_JPEG_DEC_ERROR_IMAGE_TOO_WIDE; + if (((outputWidth + 31) & ~std::uint32_t{31}) > context->maxImageWidth || rowBufferSize(*header, downScale) > context->rowBufferCapacity) return SCE_JPEG_DEC_ERROR_IMAGE_TOO_WIDE; const std::optional<Decoder::Jpeg::Image> image = downScale == DOWN_SCALE_NONE ? Decoder::Jpeg::Decode(jpeg) : Decoder::Jpeg::DecodeScaled(jpeg, downScale); if (!image || image->width != outputWidth || image->height != outputHeight) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index ff526ac2b8..6a8273a54e 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -161,6 +161,19 @@ int main() { std::vector<std::uint8_t> image(7 * 5 * 4, 0xCC); JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); + std::vector<std::uint8_t> tallSampling = TestJpeg(31, 5, 3); + for (std::size_t offset = 2; offset + 3 < tallSampling.size();) { + const std::uint8_t marker = tallSampling[offset + 1]; + const std::size_t length = (static_cast<std::size_t>(tallSampling[offset + 2]) << 8) | tallSampling[offset + 3]; + if (marker == 0xC0) { + tallSampling[offset + 11] = 0x14; + break; + } + offset += 2 + length; + } + std::vector<std::uint8_t> tallSamplingOutput(31 * 5 * 4); + JpegDecDecodeParam tallSamplingDecode = DecodeParam(tallSampling, tallSamplingOutput, 0); + Require(sceJpegDecDecode(handle, &tallSamplingDecode, &info) == static_cast<std::int32_t>(0x80650006)); JpegDecDecodeParam progressiveDecode = DecodeParam(progressiveHeader, image, 7 * 4); Require(sceJpegDecDecode(handle, &progressiveDecode, &info) == static_cast<std::int32_t>(0x80650005)); std::array<std::uint8_t, 447> insufficientCoefficients{}; diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index dad95387c4..b00f07b6d9 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Native field copies confirm the image-info layout. Color space, component identifiers, sampling factors, CSC selection, progressive coefficient-buffer size and scaled dimensions are recovered from the native header helpers. The host now checks the aligned output-width limit and progressive coefficient-buffer capacity. The native decoder also limits internal row-buffer consumption for unusual sampling factors; that additional capacity check is not yet implemented. +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Native field copies confirm the image-info layout. Color space, component identifiers, sampling factors, CSC selection, progressive coefficient-buffer size and scaled dimensions are recovered from the native header helpers. The host now checks the aligned output-width limit and progressive coefficient-buffer capacity. The native per-component row-buffer calculation is also enforced against the capacity selected by the creation attribute and maximum width. ### Functional From bc730915a08ae8642502d8aebc0e9bcff2764469 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:50:29 +0300 Subject: [PATCH 0741/1360] docs(jpegdec): retain remaining decoder compatibility gaps --- docs/dev/debt/libSceJpegDec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index b00f07b6d9..174718fdd4 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -3,8 +3,8 @@ ### Unknown function info - [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. Error codes, creation validation, 32-byte handle alignment and the attribute/width-dependent work-memory size are confirmed by the native exports. Native field copies confirm the image-info layout. Color space, component identifiers, sampling factors, CSC selection, progressive coefficient-buffer size and scaled dimensions are recovered from the native header helpers. The host now checks the aligned output-width limit and progressive coefficient-buffer capacity. The native per-component row-buffer calculation is also enforced against the capacity selected by the creation attribute and maximum width. ### Functional -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode mode 1 and pixel formats 9, 10 and 11 throw; invalid modes and down scales return the native invalid-parameter error, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The native ceiling division and packed scaled output dimensions are confirmed; exact decoded pixel values remain unverified +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp): work-memory size, alignment, creation checks, image-info fields, progressive coefficient-buffer capacity, row-buffer capacity and scaled dimensions are recovered from the native module. The host still allocates its decoder storage separately rather than using the guest work and coefficient regions. Malformed-header diagnostics and output mutation on failure are not fully matched. +- [sceJpegDecDecode](../../../core/libs/prx/libSceJpegDec/Export.cpp): decode mode 1 and pixel formats 9, 10 and 11 remain unimplemented and throw. Exact decoded pixel values and color conversion are not verified against the native decoder. From 0a8ee1520d50cc31d036e166bb66d77010078e27 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:50:30 +0300 Subject: [PATCH 0742/1360] fix(pngenc): allow unfiltered rows with partial filter masks --- core/libs/prx/libScePngEnc/Export.cpp | 2 +- core/libs/tests/GuestPngEnc.cpp | 5 +++-- docs/dev/debt/libScePngEnc.md | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libScePngEnc/Export.cpp b/core/libs/prx/libScePngEnc/Export.cpp index 4ff0af04e3..a4bad699c4 100644 --- a/core/libs/prx/libScePngEnc/Export.cpp +++ b/core/libs/prx/libScePngEnc/Export.cpp @@ -55,7 +55,7 @@ PngEncContext* context(void* handle) { std::uint8_t filterSet(std::uint16_t filterType) { if (filterType == FILTER_ALL) return Decoder::Png::FILTER_ALL; if (filterType == 0) return Decoder::Png::FILTER_NONE; - std::uint8_t filters = 0; + std::uint8_t filters = Decoder::Png::FILTER_NONE; if (filterType & FILTER_SUB) filters |= Decoder::Png::FILTER_SUB; if (filterType & FILTER_UP) filters |= Decoder::Png::FILTER_UP; if (filterType & FILTER_AVERAGE) filters |= Decoder::Png::FILTER_AVERAGE; diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 7984c2fff5..3a84ca0f83 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -126,8 +126,9 @@ int main() { {0, FILTER_NONE}, {1, FILTER_SUB}, {2, FILTER_UP}, {4, FILTER_AVERAGE}, {8, FILTER_PAETH}, {3, FILTER_SUB | FILTER_UP}, {6, FILTER_UP | FILTER_AVERAGE}, {9, FILTER_SUB | FILTER_PAETH}, {14, FILTER_UP | FILTER_AVERAGE | FILTER_PAETH}, {15, FILTER_ALL}}; - for (const auto& [filterType, filters] : filterSets) Require(encodeWith(filterType) == Encode(rows, 16, 6, 4, {6, filters})); - Require(encodeWith(3) != encodeWith(1) && encodeWith(3) != encodeWith(2) && encodeWith(15) != encodeWith(14)); + for (const auto& [filterType, filters] : filterSets) Require(encodeWith(filterType) == Encode(rows, 16, 6, 4, {6, static_cast<std::uint8_t>(filters | FILTER_NONE)})); + const std::vector<std::uint8_t> unfiltered = Encode(rows, 16, 6, 4, {6, FILTER_NONE}); + Require(encodeWith(0) == unfiltered); encode.png_mem_size = 20; PngEncOutputInfo info{1, 1}; diff --git a/docs/dev/debt/libScePngEnc.md b/docs/dev/debt/libScePngEnc.md index 0ba7edc2bf..e5a107d55e 100644 --- a/docs/dev/debt/libScePngEnc.md +++ b/docs/dev/debt/libScePngEnc.md @@ -1,5 +1,5 @@ # libScePngEnc technical debt -### Unknown function info +### Functional -- [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 +- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp): native filter setup always includes the unfiltered candidate, including for partial masks. The host now permits it too; exact filter-selection ties, compressed bytes and partial-output behavior are not matched to the native encoder. Creation memory layout and encode validation still differ from the supplied module. From 13fa4033a4409f859da46efd95d555ab219634d9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:51:26 +0300 Subject: [PATCH 0743/1360] fix(pngenc): restore creation memory contract --- core/libs/prx/libScePngEnc/Export.cpp | 44 +++++++++++++++++++-------- core/libs/tests/GuestPngEnc.cpp | 16 ++++++---- docs/dev/debt/libScePngEnc.md | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libScePngEnc/Export.cpp b/core/libs/prx/libScePngEnc/Export.cpp index a4bad699c4..cb8cddc9ce 100644 --- a/core/libs/prx/libScePngEnc/Export.cpp +++ b/core/libs/prx/libScePngEnc/Export.cpp @@ -1,4 +1,6 @@ #include <algorithm> +#include <bit> +#include <new> #include <cstdint> #include <cstddef> #include <exception> @@ -18,7 +20,7 @@ constexpr int PNG_ENC_ERROR_FATAL = static_cast<int>(0x80690120); constexpr std::uint32_t MAX_IMAGE_WIDTH = 1000000; constexpr std::uint32_t MAX_IMAGE_HEIGHT = 1000000; -constexpr std::uint32_t MAX_FILTER_NUMBER = 5; +constexpr std::uint32_t MAX_FILTER_NUMBER = 4; constexpr std::uint16_t COLOR_SPACE_RGB = 3; constexpr std::uint16_t COLOR_SPACE_RGBA = 19; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; @@ -28,12 +30,15 @@ constexpr std::uint16_t FILTER_UP = 2; constexpr std::uint16_t FILTER_AVERAGE = 4; constexpr std::uint16_t FILTER_PAETH = 8; constexpr std::uint16_t FILTER_ALL = FILTER_SUB | FILTER_UP | FILTER_AVERAGE | FILTER_PAETH; -constexpr std::uint64_t CONTEXT_MAGIC = 0x434E45474E505341ull; +constexpr std::uintptr_t HANDLE_ALIGNMENT = 16; struct PngEncContext { - std::uint64_t magic; - std::uint32_t maxImageWidth; + PngEncContext* self; + std::uint32_t size; std::uint32_t reserved; + std::uint32_t maxImageWidth; + std::uint32_t maxFilterNumber; + std::uint32_t scratchSize; }; static_assert(sizeof(PngEncCreateParam) == 16); @@ -42,14 +47,19 @@ static_assert(sizeof(PngEncOutputInfo) == 8); int validateCreateParam(const PngEncCreateParam* param) { if (!param) return PNG_ENC_ERROR_INVALID_ADDR; - if (param->attribute != 0 || param->max_filter_number > MAX_FILTER_NUMBER) return PNG_ENC_ERROR_INVALID_PARAM; - if (param->max_image_width == 0 || param->max_image_width > MAX_IMAGE_WIDTH) return PNG_ENC_ERROR_INVALID_SIZE; + if (param->this_size != sizeof(PngEncCreateParam)) return PNG_ENC_ERROR_INVALID_SIZE; + if (param->attribute != 0 || param->max_image_width == 0 || param->max_image_width > MAX_IMAGE_WIDTH || param->max_filter_number > MAX_FILTER_NUMBER) return PNG_ENC_ERROR_INVALID_PARAM; return 0; } +std::uint32_t workMemorySize(const PngEncCreateParam& param) { + return 285808 + ((4 * param.max_image_width + 28) & ~std::uint32_t{31}) * (param.max_filter_number + 2); +} + PngEncContext* context(void* handle) { + if (reinterpret_cast<std::uintptr_t>(handle) % HANDLE_ALIGNMENT != 0) return nullptr; auto* ctx = static_cast<PngEncContext*>(handle); - return ctx && ctx->magic == CONTEXT_MAGIC ? ctx : nullptr; + return ctx && ctx->self == ctx ? ctx : nullptr; } std::uint8_t filterSet(std::uint16_t filterType) { @@ -69,22 +79,30 @@ extern "C" { int APS5_VABI scePngEncQueryMemorySize(const PngEncCreateParam* param) { if (const int result = validateCreateParam(param); result != 0) return result; - return sizeof(PngEncContext); + return static_cast<int>(workMemorySize(*param)); } int APS5_VABI scePngEncCreate(const PngEncCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) { if (const int result = validateCreateParam(param); result != 0) return result; - if (!memoryAddress || !handle) return PNG_ENC_ERROR_INVALID_ADDR; - if (memorySize < sizeof(PngEncContext)) return PNG_ENC_ERROR_INVALID_SIZE; - *static_cast<PngEncContext*>(memoryAddress) = {CONTEXT_MAGIC, param->max_image_width, 0}; - *handle = memoryAddress; + if (!memoryAddress) return PNG_ENC_ERROR_INVALID_ADDR; + const std::uint32_t requiredSize = workMemorySize(*param); + if (memorySize < requiredSize) return PNG_ENC_ERROR_INVALID_SIZE; + if (!handle) return PNG_ENC_ERROR_INVALID_ADDR; + const std::uintptr_t aligned = (reinterpret_cast<std::uintptr_t>(memoryAddress) + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); + auto* ctx = new (reinterpret_cast<void*>(aligned)) PngEncContext{}; + ctx->self = ctx; + ctx->size = sizeof(PngEncCreateParam); + ctx->maxImageWidth = (param->max_image_width + 7) & ~std::uint32_t{7}; + ctx->maxFilterNumber = param->max_filter_number; + ctx->scratchSize = requiredSize - 112; + *handle = ctx; return 0; } int APS5_VABI scePngEncDelete(void* handle) { PngEncContext* ctx = context(handle); if (!ctx) return PNG_ENC_ERROR_INVALID_HANDLE; - ctx->magic = 0; + ctx->self = nullptr; return 0; } diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 3a84ca0f83..62287f1c73 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -26,28 +26,32 @@ int main() { constexpr std::uint32_t height = 3; constexpr std::uint32_t pitch = 24; - PngEncCreateParam create{sizeof(PngEncCreateParam), 0, 16, 5}; + PngEncCreateParam create{sizeof(PngEncCreateParam), 0, 16, 4}; const int memorySize = scePngEncQueryMemorySize(&create); - Require(memorySize > 0); + Require(memorySize == 286192); Require(scePngEncQueryMemorySize(nullptr) == invalidAddr); PngEncCreateParam bad = create; bad.attribute = 1; Require(scePngEncQueryMemorySize(&bad) == invalidParam); bad = create; - bad.max_filter_number = 6; + bad.max_filter_number = 5; Require(scePngEncQueryMemorySize(&bad) == invalidParam); bad = create; bad.max_image_width = 0; - Require(scePngEncQueryMemorySize(&bad) == invalidSize); + Require(scePngEncQueryMemorySize(&bad) == invalidParam); bad.max_image_width = 1000001; - Require(scePngEncQueryMemorySize(&bad) == invalidSize); + Require(scePngEncQueryMemorySize(&bad) == invalidParam); + bad = create; + bad.this_size = 0; + Require(scePngEncQueryMemorySize(&bad) == invalidSize); std::vector<std::uint64_t> memory((memorySize + 7) / 8); void* handle = nullptr; Require(scePngEncCreate(&create, nullptr, memorySize, &handle) == invalidAddr); Require(scePngEncCreate(&create, memory.data(), memorySize - 1, &handle) == invalidSize); - Require(scePngEncCreate(&create, memory.data(), memorySize, &handle) == 0 && handle == memory.data()); + Require(scePngEncCreate(&create, memory.data(), memorySize, &handle) == 0 && reinterpret_cast<std::uintptr_t>(handle) % 16 == 0); + Require(scePngEncCreate(&create, memory.data(), 0, nullptr) == invalidSize); std::vector<std::uint8_t> image(pitch * height, 0xEE); for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) { diff --git a/docs/dev/debt/libScePngEnc.md b/docs/dev/debt/libScePngEnc.md index e5a107d55e..a73a10572d 100644 --- a/docs/dev/debt/libScePngEnc.md +++ b/docs/dev/debt/libScePngEnc.md @@ -2,4 +2,4 @@ ### Functional -- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp): native filter setup always includes the unfiltered candidate, including for partial masks. The host now permits it too; exact filter-selection ties, compressed bytes and partial-output behavior are not matched to the native encoder. Creation memory layout and encode validation still differ from the supplied module. +- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp): native filter setup always includes the unfiltered candidate, including for partial masks. The host now permits it too; exact filter-selection ties, compressed bytes and partial-output behavior are not matched to the native encoder. Creation validation, work-memory size, 16-byte handle alignment and the context prefix now match the supplied module. Host scratch storage remains separately allocated, and encode validation still differs. From d616c4c3ce3d26669d5a4546f58e09ecc0e66c14 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:52:10 +0300 Subject: [PATCH 0744/1360] fix(pngenc): validate input buffers and filter capacity --- core/libs/prx/libScePngEnc/Export.cpp | 23 ++++++++++++----------- core/libs/tests/GuestPngEnc.cpp | 26 +++++++++++++++++++++----- docs/dev/debt/libScePngEnc.md | 2 +- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libScePngEnc/Export.cpp b/core/libs/prx/libScePngEnc/Export.cpp index cb8cddc9ce..c6e3c9532a 100644 --- a/core/libs/prx/libScePngEnc/Export.cpp +++ b/core/libs/prx/libScePngEnc/Export.cpp @@ -109,19 +109,20 @@ int APS5_VABI scePngEncDelete(void* handle) { int APS5_VABI scePngEncEncode(void* handle, const PngEncEncodeParam* param, PngEncOutputInfo* outputInfo) { const PngEncContext* ctx = context(handle); if (!ctx) return PNG_ENC_ERROR_INVALID_HANDLE; - if (!param) return PNG_ENC_ERROR_INVALID_PARAM; - if (!param->image_mem_addr || !param->png_mem_addr) return PNG_ENC_ERROR_INVALID_ADDR; - if ((param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) - || (param->color_space != COLOR_SPACE_RGB && param->color_space != COLOR_SPACE_RGBA) || param->bit_depth != 8 - || param->clut_number != 0 || (param->filter_type & ~FILTER_ALL) != 0 || param->compression_level > 9) { - return PNG_ENC_ERROR_INVALID_PARAM; - } - + if (!param) return PNG_ENC_ERROR_INVALID_ADDR; + if (!param->image_mem_addr || reinterpret_cast<std::uintptr_t>(param->image_mem_addr) % 4 != 0 || !param->png_mem_addr) return PNG_ENC_ERROR_INVALID_ADDR; + if (param->image_mem_size == 0 || param->png_mem_size == 0) return PNG_ENC_ERROR_INVALID_SIZE; const std::uint32_t width = param->image_width; const std::uint32_t height = param->image_height; - if (width == 0 || height == 0 || width > ctx->maxImageWidth || height > MAX_IMAGE_HEIGHT || param->png_mem_size == 0 - || param->image_pitch < width * 4 || static_cast<std::uint64_t>(param->image_pitch) * (height - 1) + width * 4 > param->image_mem_size) { - return PNG_ENC_ERROR_INVALID_SIZE; + const std::uint64_t inputSize = static_cast<std::uint64_t>(param->image_pitch) * height; + if (width == 0 || width > ctx->maxImageWidth || height == 0 || height > MAX_IMAGE_HEIGHT + || param->image_pitch == 0 || param->image_pitch % 4 != 0 || param->image_pitch > 0x0FFFFFFF || param->image_pitch / 4 < width + || inputSize > 0x7FFFFFFF || inputSize > param->image_mem_size) return PNG_ENC_ERROR_INVALID_PARAM; + if ((param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) + || (param->color_space != COLOR_SPACE_RGB && param->color_space != COLOR_SPACE_RGBA) || param->bit_depth != 8 + || param->clut_number != 0 || (param->filter_type & ~FILTER_ALL) != 0 + || static_cast<std::uint32_t>(std::popcount(param->filter_type)) > ctx->maxFilterNumber || param->compression_level > 9) { + return PNG_ENC_ERROR_INVALID_PARAM; } const std::uint32_t channels = param->color_space == COLOR_SPACE_RGBA ? 4 : 3; diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 62287f1c73..d8227b3bab 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -140,7 +140,7 @@ int main() { encode.png_mem_size = static_cast<std::uint32_t>(png.size()); PngEncEncodeParam invalid = encode; - Require(scePngEncEncode(handle, nullptr, nullptr) == invalidParam); + Require(scePngEncEncode(handle, nullptr, nullptr) == invalidAddr); invalid.image_mem_addr = nullptr; Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr); for (auto change : {+[](PngEncEncodeParam& p) { p.pixel_format = 2; }, +[](PngEncEncodeParam& p) { p.color_space = 4; }, @@ -151,15 +151,31 @@ int main() { Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); } for (auto change : {+[](PngEncEncodeParam& p) { p.image_width = 0; }, +[](PngEncEncodeParam& p) { p.image_width = 17; }, - +[](PngEncEncodeParam& p) { p.image_pitch = 19; }, +[](PngEncEncodeParam& p) { p.image_mem_size = 67; }, - +[](PngEncEncodeParam& p) { p.png_mem_size = 0; }}) { + +[](PngEncEncodeParam& p) { p.image_pitch = 19; }, +[](PngEncEncodeParam& p) { p.image_mem_size = 67; }}) { invalid = encode; change(invalid); - Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize); + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); } invalid = encode; invalid.image_mem_size = pitch * (height - 1) + width * 4; - Require(scePngEncEncode(handle, &invalid, nullptr) > 0); + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); + invalid = encode; + invalid.png_mem_size = 0; + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize); + invalid = encode; + invalid.image_mem_addr = image.data() + 1; + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr); + PngEncCreateParam oneFilter = create; + oneFilter.max_filter_number = 1; + void* oneFilterHandle = nullptr; + std::vector<std::uint64_t> oneFilterMemory((scePngEncQueryMemorySize(&oneFilter) + 7) / 8); + Require(scePngEncCreate(&oneFilter, oneFilterMemory.data(), static_cast<std::uint32_t>(oneFilterMemory.size() * 8), &oneFilterHandle) == 0); + invalid = encode; + invalid.filter_type = 3; + Require(scePngEncEncode(oneFilterHandle, &invalid, nullptr) == invalidParam); + invalid.filter_type = 1; + Require(scePngEncEncode(oneFilterHandle, &invalid, nullptr) > 0); + Require(scePngEncDelete(oneFilterHandle) == 0); Require(scePngEncDelete(handle) == 0); Require(scePngEncDelete(handle) == invalidHandle); diff --git a/docs/dev/debt/libScePngEnc.md b/docs/dev/debt/libScePngEnc.md index a73a10572d..88f5089fbc 100644 --- a/docs/dev/debt/libScePngEnc.md +++ b/docs/dev/debt/libScePngEnc.md @@ -2,4 +2,4 @@ ### Functional -- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp): native filter setup always includes the unfiltered candidate, including for partial masks. The host now permits it too; exact filter-selection ties, compressed bytes and partial-output behavior are not matched to the native encoder. Creation validation, work-memory size, 16-byte handle alignment and the context prefix now match the supplied module. Host scratch storage remains separately allocated, and encode validation still differs. +- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp): native filter setup always includes the unfiltered candidate, including for partial masks. The host now permits it too; exact filter-selection ties, compressed bytes and partial-output behavior are not matched to the native encoder. Creation validation, work-memory size, 16-byte handle alignment and the context prefix now match the supplied module. Input-buffer alignment, pitch, dimensions, buffer capacity and selected-filter count now follow the native checks. Host scratch storage remains separately allocated. From 69a0a691d187d7dce0a7cf4366c1c358d519c36d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:53:39 +0300 Subject: [PATCH 0745/1360] fix(audio3d): restore attribute count output contract --- core/libs/prx/libSceAudio3d/Export.cpp | 15 +++++---------- core/libs/tests/GuestAudio3dPort.cpp | 9 +++++---- docs/dev/debt/libSceAudio3d.md | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 28854734cc..4420537497 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -199,22 +199,17 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve return 0; } -int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t port_id, uint32_t* attributes, uint32_t* num_attributes) { +int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t portId, uint32_t* attributes, uint32_t* numAttributes) { std::lock_guard lock(g_mutex); - if (num_attributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (!PortIsOpen(portId)) return AUDIO3D_ERROR_INVALID_PORT; + if (numAttributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; constexpr std::uint32_t supported[]{ AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL, AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS, AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE, }; - if (attributes == nullptr) { - *num_attributes = std::size(supported); - return 0; - } - const std::uint32_t count = std::min<std::uint32_t>(*num_attributes, std::size(supported)); - std::copy_n(std::begin(supported), count, attributes); - *num_attributes = count; + *numAttributes = std::size(supported); + if (attributes != nullptr) std::copy(std::begin(supported), std::end(supported), attributes); return 0; } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 5b5467437d..7adbb92ede 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -155,10 +155,11 @@ void CheckAttributes() { Require(sceAudio3dPortGetAttributesSupported(0, nullptr, &num_attributes) == 0 && num_attributes == 3, "null output reports the supported attribute count"); Require(sceAudio3dPortGetAttributesSupported(1, nullptr, &num_attributes) == INVALID_PORT, "supported attributes reject an invalid port"); Require(sceAudio3dPortGetAttributesSupported(0, nullptr, nullptr) == INVALID_PARAMETER, "supported attributes require a count pointer"); - num_attributes = 2; - std::uint32_t partial[3]{0, 0, 99}; - Require(sceAudio3dPortGetAttributesSupported(0, partial, &num_attributes) == 0 && num_attributes == 2, "supported attributes respect the caller capacity"); - Require(partial[0] == supported[0] && partial[1] == supported[1] && partial[2] == 99, "supported attributes copy only the requested entries"); + Require(sceAudio3dPortGetAttributesSupported(1, nullptr, nullptr) == INVALID_PORT, "port validation precedes count validation"); + num_attributes = 0; + std::uint32_t output[4]{0, 0, 0, 99}; + Require(sceAudio3dPortGetAttributesSupported(0, output, &num_attributes) == 0 && num_attributes == 3, "the count is output only"); + Require(output[0] == supported[0] && output[1] == supported[1] && output[2] == supported[2] && output[3] == 99, "the complete list is written without touching the following element"); num_attributes = 3; std::uint32_t all[3]{}; Require(sceAudio3dPortGetAttributesSupported(0, all, &num_attributes) == 0 && num_attributes == 3, "all supported attributes are returned"); diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index c7d94441a4..e83c9da9a2 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -11,7 +11,7 @@ ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too -- [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body +- [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp): the native export validates the port before the count pointer, writes the total count without reading its incoming value, and writes the complete list when an array is supplied. These checks are matched. The list itself remains the host's three port attributes: the native list derives from AudioOut2 device capability masks, SDK version and platform mode, which the host does not yet model. - [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 - [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature From c55342edbb636ded88c78aa64a819494a5f06adb Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:56:09 +0300 Subject: [PATCH 0746/1360] fix(rtc): use current time for null formatting ticks --- core/libs/prx/libSceRtc/Export.cpp | 44 +++++++++++++++--------------- docs/dev/debt/libSceRtc.md | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 7364077dff..3cc3b045de 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -579,64 +579,64 @@ int APS5_VABI sceRtcSetMicrosecond(RtcDateTime* time, int microsecond) { return 0; } -int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { - if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; +int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZoneMinutes) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; RtcTick now{}; if (!utc) { now.tick = currentTick(); utc = &now; } - if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; + if (timeZoneMinutes < -1439 || timeZoneMinutes > 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; + if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; const RtcDateTime time = fromTick(local.tick); - int written = std::snprintf(date_time, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); - if (time_zone_minutes == 0) { - std::snprintf(date_time + written, 2, "Z"); + int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); + if (timeZoneMinutes == 0) { + std::snprintf(dateTime + written, 2, "Z"); } else { - const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; - std::snprintf(date_time + written, 7, "%c%02d:%02d", time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; + std::snprintf(dateTime + written, 7, "%c%02d:%02d", timeZoneMinutes < 0 ? '-' : '+', offset / 60, offset % 60); } return 0; } -int APS5_VABI sceRtcFormatRFC3339LocalTime(char* date_time, const RtcTick* utc) { - if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; +int APS5_VABI sceRtcFormatRFC3339LocalTime(char* dateTime, const RtcTick* utc) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; RtcTick now{}; if (!utc) { now.tick = currentTick(); utc = &now; } if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; - return sceRtcFormatRFC3339(date_time, utc, localOffsetMinutes(*utc)); + return sceRtcFormatRFC3339(dateTime, utc, localOffsetMinutes(*utc)); } -int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_zone_minutes) { - if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; +int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZoneMinutes) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; RtcTick now{}; if (!utc) { now.tick = currentTick(); utc = &now; } - if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; + if (timeZoneMinutes < -1439 || timeZoneMinutes > 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; + if (const int result = addTicks(&local, utc, timeZoneMinutes, 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); + const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; + std::snprintf(dateTime, 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, timeZoneMinutes < 0 ? '-' : '+', offset / 60, offset % 60); return 0; } -int APS5_VABI sceRtcFormatRFC2822LocalTime(char* date_time, const RtcTick* utc) { - if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; +int APS5_VABI sceRtcFormatRFC2822LocalTime(char* dateTime, const RtcTick* utc) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; RtcTick now{}; if (!utc) { now.tick = currentTick(); utc = &now; } if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; - return sceRtcFormatRFC2822(date_time, utc, localOffsetMinutes(*utc)); + return sceRtcFormatRFC2822(dateTime, utc, localOffsetMinutes(*utc)); } int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 8b683a05c6..56985f25a2 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -7,8 +7,8 @@ ### Unknown function info - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. -- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick formats the current time, as shadPS4 does and as these three and `sceRtcFormatRFC3339` did in a payload process on a PS5 (firmware 13.60); offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`, also with a null tick there; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date +- [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console - [sceRtcGetCurrentAdNetworkTick, sceRtcGetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clocks 0x12 and 0x11 and falls back to network clock 0x10 when either clock is unsupported. These getters use their process-local clock when it was set, otherwise they follow the process-local network clock. The firmware's unsupported-clock result is not modelled. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console - [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick, sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a valid tick sets a process-local clock that advances from a monotonic host clock; the host system clock is not changed. The console sets clock 0, 0x10, 0x12 or 0x11. Null pointers return `0x80B50002` and ticks before the Unix epoch return `0x80B50003`, from disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console. sceRtcSetConf passes its third and fourth arguments to the same clock call with clock 0 and throws; what the pair selects is unknown From 2956059cc8b550fe0d3b6cf024e2e0cf54760083 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:57:57 +0300 Subject: [PATCH 0747/1360] fix(audiopropagation): type audio path update descriptors --- .../AudioPropagation.hpp | 27 ++++++++++++++++++- .../prx/libSceAudioPropagation/Export.cpp | 2 +- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 538b6a867f..ad7f009806 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -76,6 +76,31 @@ struct Attribute { std::uint32_t reserved2; }; +struct PathPoint { + AudioPropagationStructDescriptor desc; + AudioPropagationHandle room; + float gain; + float position[3]; + std::byte opaque[8]; +}; + +struct AudioPath { + AudioPropagationStructDescriptor desc; + PathPoint points[4]; + std::uint32_t pointCount; + std::uint32_t flags; + std::byte opaque[8]; +}; + +static_assert(sizeof(PathPoint) == 48); +static_assert(offsetof(PathPoint, room) == 16); +static_assert(offsetof(PathPoint, gain) == 24); +static_assert(offsetof(PathPoint, position) == 28); +static_assert(sizeof(AudioPath) == 224); +static_assert(offsetof(AudioPath, points) == 16); +static_assert(offsetof(AudioPath, pointCount) == 208); +static_assert(offsetof(AudioPath, flags) == 212); + struct RenderInfo { AudioPropagationStructDescriptor desc; AudioPropagationHandle source; @@ -125,7 +150,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHan std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); -std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain); +std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPropagation::AudioPath* data, float gain); std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const AudioPropagation::RenderInfo* infos, std::uint32_t count); } diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index d1af51030e..f32ef42485 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -293,7 +293,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa return 0; } -std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain) { +std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPath* data, float gain) { static_cast<void>(path); static_cast<void>(data); static_cast<void>(gain); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 1800ba874d..98545297ca 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -6,6 +6,6 @@ ### Unknown function info -- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. +- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. - [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` selects reflection order; descriptor validation, ray ownership and path generation remain unimplemented; `sceAudioPropagationSourceGetAudioPath` output width and invalid-handle/null-output/index errors are confirmed. 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. Other functions still throw for invalid input; their native validation precedence is not yet implemented - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From 7a596d28aa1bcc58548159db259233e25705728f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 09:59:24 +0300 Subject: [PATCH 0748/1360] fix(audiopropagation): restore mutable render descriptor fields --- core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp | 7 ++++--- core/libs/prx/libSceAudioPropagation/Export.cpp | 5 ++++- core/libs/tests/GuestAudioPropagation.cpp | 4 ++++ docs/dev/debt/libSceAudioPropagation.md | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index ad7f009806..63e23fb5e8 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -104,10 +104,10 @@ static_assert(offsetof(AudioPath, flags) == 212); struct RenderInfo { AudioPropagationStructDescriptor desc; AudioPropagationHandle source; - void* output; + float* output; std::size_t outputSize; std::uint32_t format; - std::uint32_t reserved; + std::uint32_t channelCount; }; struct PortalParams { @@ -122,6 +122,7 @@ static_assert(sizeof(Attribute) == 0x20); static_assert(sizeof(RenderInfo) == RenderInfoSize); static_assert(offsetof(RenderInfo, source) == 0x10); static_assert(offsetof(RenderInfo, format) == 0x28); +static_assert(offsetof(RenderInfo, channelCount) == 0x2c); static_assert(sizeof(PortalParams) == PortalParamsSize); static_assert(offsetof(PortalParams, rooms) == 0x50); @@ -151,7 +152,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle s std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPropagation::AudioPath* data, float gain); -std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const AudioPropagation::RenderInfo* infos, std::uint32_t count); +std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, AudioPropagation::RenderInfo* infos, std::uint32_t count); } diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index f32ef42485..27fca89aad 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -301,7 +301,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHan return 0; } -std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const RenderInfo* infos, std::uint32_t count) { +std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, RenderInfo* infos, std::uint32_t count) { require(infos != nullptr && count != 0, __func__); auto& state = registry(); std::lock_guard lock(state.mutex); @@ -313,6 +313,9 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(info.output != nullptr && info.outputSize != 0, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); } + for (std::uint32_t index = 0; index < count; ++index) { + infos[index].channelCount = 0; + } return 0; } diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index ebb42f21d9..071ab4f0ce 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -176,6 +176,10 @@ int main() { reset(); Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render failed"); Require(untouched(), "empty source erased an existing mix"); + infos[0].channelCount = 99; + Require(sceAudioPropagationSourceRender(system, infos, 1) == 0 && infos[0].channelCount == 0, "fixed render format clears the channel count"); + const auto* tail = reinterpret_cast<const std::uint8_t*>(outputs[1]); + Require(tail[0] == 0xab, "render wrote past the output size"); reset(); infos[0].outputSize = sizeof(float) * 16; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 98545297ca..61e8a959a9 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. -- [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` selects reflection order; descriptor validation, ray ownership and path generation remain unimplemented; `sceAudioPropagationSourceGetAudioPath` output width and invalid-handle/null-output/index errors are confirmed. 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. Other functions still throw for invalid input; their native validation precedence is not yet implemented +- [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` selects reflection order; descriptor validation, ray ownership and path generation remain unimplemented; `sceAudioPropagationSourceGetAudioPath` output width and invalid-handle/null-output/index errors are confirmed. The native render call accepts an array of mutable 48-byte records, with float output at +0x18, byte capacity at +0x20, format at +0x28 and channel count at +0x2c. Format 2 uses 16 float channels per system frame and clears the channel count after rendering; only this format remains supported by the host silence model. The native count limit and output capacity depend on system configuration that the host does not store; those checks remain unmatched. Other functions still throw for invalid input; their native validation precedence is not yet implemented - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From 8a1bd363ef5cc506e9c3a06dc1d254663c89305d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:04:31 +0300 Subject: [PATCH 0749/1360] fix(move): restore query validation and invalid handle errors --- core/libs/prx/libSceMove/Export.cpp | 18 ++++++++---------- core/libs/tests/GuestMove.cpp | 7 +++++-- docs/dev/debt/libSceMove.md | 11 ++++------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index 2ffc51523e..effeda1e5c 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -28,10 +28,12 @@ struct Slot { std::unordered_map<int32_t, Slot> openHandles; -int checkHandle(int32_t handle) { +int checkHandle(int32_t handle, bool validArguments = true) { std::lock_guard lock(stateMutex); if (!initialized) return MOVE_ERROR_NOT_INIT; - return openHandles.contains(handle) ? MOVE_OK : MOVE_ERROR_INVALID_HANDLE; + if (!validArguments) return MOVE_ERROR_INVALID_ARG; + if (!openHandles.contains(handle)) return MOVE_ERROR_INVALID_HANDLE; + return MOVE_OK; } } @@ -73,26 +75,22 @@ int APS5_VABI sceMoveClose(int32_t handle) { } int APS5_VABI sceMoveGetDeviceInfo(int32_t handle, void* info) { - if (const int result = checkHandle(handle); result != MOVE_OK) return result; - if (!info) return MOVE_ERROR_INVALID_ARG; + if (const int result = checkHandle(handle, info != nullptr); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveGetExtensionPortInfo(int32_t handle, void* data) { - if (const int result = checkHandle(handle); result != MOVE_OK) return result; - if (!data) return MOVE_ERROR_INVALID_ARG; + if (const int result = checkHandle(handle, data != nullptr); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { - if (const int result = checkHandle(handle); result != MOVE_OK) return result; - if (!data) return MOVE_ERROR_INVALID_ARG; + if (const int result = checkHandle(handle, data != nullptr); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* outCount) { - if (const int result = checkHandle(handle); result != MOVE_OK) return result; - if (timestamp < 0 || !data || !outCount) return MOVE_ERROR_INVALID_ARG; + if (const int result = checkHandle(handle, timestamp >= 0 && data != nullptr && outCount != nullptr); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp index b114030e35..bbb55c541d 100644 --- a/core/libs/tests/GuestMove.cpp +++ b/core/libs/tests/GuestMove.cpp @@ -85,8 +85,11 @@ void RequireUnknownHandle(std::int32_t handle) { Require(sceMoveSetVibration(handle, 0xFF) == kInvalidHandle); Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kInvalidHandle); Require(sceMoveResetLightSphere(handle) == kInvalidHandle); - Require(sceMoveGetDeviceInfo(handle, nullptr) == kInvalidHandle); - Require(sceMoveReadStateRecent(handle, -1, nullptr, nullptr) == kInvalidHandle); + Require(sceMoveGetDeviceInfo(handle, nullptr) == kInvalidArg); + Require(sceMoveGetExtensionPortInfo(handle, nullptr) == kInvalidArg); + Require(sceMoveReadStateLatest(handle, nullptr) == kInvalidArg); + Require(sceMoveReadStateRecent(handle, -1, buffer.bytes, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, nullptr) == kInvalidArg); Require(buffer.Untouched()); Require(count == -7); } diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md index d1660178ce..bc12dfcafb 100644 --- a/docs/dev/debt/libSceMove.md +++ b/docs/dev/debt/libSceMove.md @@ -1,10 +1,7 @@ # libSceMove technical debt -### Silent stubs +### Functional -- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) implements only the no-controller path: initialization and termination succeed, `sceMoveOpen` returns a new handle, `sceMoveClose` and `sceMoveResetLightSphere` succeed for an open handle, and the device info, state, extension port, vibration and `sceMoveSetLightSphere` calls return 1 (no controller connected) and write nothing - -### Unknown function info - -- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - behaviour, error codes and the handle values (`0x30B0100`, then every `0x100`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/move/move.cpp) (PS4), which checks no handle or open arguments; none is measured on a PS5. Signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), except `sceMoveGetExtensionPortInfo`, which follows shadPS4. Before `sceMoveInit` and after `sceMoveTerm` every other call returns `NOT_INIT` (`0x80EE0001`); a null output or a negative timestamp returns `INVALID_ARG` (`0x80EE0003`). Closes when: measured on a PS5 -- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - a handle that was never opened, is closed or was opened before `sceMoveTerm` returns `INVALID_HANDLE` (`0x80EE0004`), checked before `INVALID_ARG`, and `sceMoveOpen` returns `ALREADY_OPENED` (`0x80EE0007`) for a type and index that are already open; both codes are named in [shadPS4's move_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/move/move_error.h), which never returns them, so the results and their order are not measured. `sceMoveOpen` throws for a user other than the initial user, whose console result is not known, and accepts any type and index. Closes when: measured on a PS5 +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp): the host implements only disconnected controllers. The native module confirms initialization errors, disconnected return 1 without output writes, invalid-handle error 0x80EE0004, and initialization then argument then handle validation for queries. Reset-light-sphere still omits the device ioctl and its failure mapping. +- [sceMoveOpen and sceMoveClose](../../../core/libs/prx/libSceMove/Export.cpp): protected bodies remain unchanged. Native opening resolves a user/device through the input service, rejects duplicate device handles with 0x80EE0007, limits storage to eight slots with 0x80EE0005, and obtains handles from the device ioctl. The host generates monotonically increasing handles, accepts arbitrary type/index values and returns 0x80EE0007 for duplicate slots and throws for other users. Closing an unknown handle returns the confirmed 0x80EE0004. +- [sceMoveReadStateRecent](../../../core/libs/prx/libSceMove/Export.cpp): the native module permits a null data array for SDK versions through 0x03ffffff while still requiring a count pointer and nonnegative timestamp. The host requires the data array for every SDK version. Connected state records and device-service failures remain unimplemented. From 1371338623d2eb30cb32441d117a4b1fa17c5bfc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:04:33 +0300 Subject: [PATCH 0750/1360] fix(loginservice): restore read-only device request signature --- core/libs/prx/libSceLoginService/Export.cpp | 4 +++- docs/dev/debt/libSceLoginService.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceLoginService/Export.cpp b/core/libs/prx/libSceLoginService/Export.cpp index 43a1b1dcdf..71262ec59b 100644 --- a/core/libs/prx/libSceLoginService/Export.cpp +++ b/core/libs/prx/libSceLoginService/Export.cpp @@ -2,6 +2,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct LoginServiceDeviceRequest; + extern "C" { int APS5_VABI sceLoginServiceInitialize(void) { @@ -12,7 +14,7 @@ int APS5_VABI sceLoginServiceTerminate(void) { return 0; } -int APS5_VABI sceLoginServiceRequestDevices(void* param) { +int APS5_VABI sceLoginServiceRequestDevices(const LoginServiceDeviceRequest* param) { (void)param; NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceLoginService.md b/docs/dev/debt/libSceLoginService.md index f33713e90d..db043fd9c7 100644 --- a/docs/dev/debt/libSceLoginService.md +++ b/docs/dev/debt/libSceLoginService.md @@ -6,4 +6,4 @@ ### Unknown function info -- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - PPSA34015 passes one pointer to a stack buffer (`lea rdi, [rbp-0x80]`) then calls `sceLoginServiceTerminate`. The buffer layout, other arguments and console errors are unknown, so every call throws without reading or writing caller memory +- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp): the native wrapper confirms one read-only request pointer and transfers exactly 64 bytes to the login service. Null input returns 0x813a0003 before the initialization check; an uninitialized service returns 0x813a0001. The request fields are interpreted by the service backend and remain unresolved. The protected export body is unchanged. From 87c42d8e11028e45e87ee667495b2feba4b4e7f7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:09:36 +0300 Subject: [PATCH 0751/1360] fix(acm): restore panner command encoding and validation --- core/libs/prx/libSceAcm/Export.cpp | 41 +++++++++++++++++++++++++++++- core/libs/tests/GuestAcm.cpp | 32 +++++++++++++++++++++-- docs/dev/debt/libSceAcm.md | 4 +-- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index abc5dc83bc..499ad6d0ba 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -87,7 +87,46 @@ int APS5_VABI sceAcm_FFT(void) { return 0; } -int APS5_VABI sceAcm_Panner(void) { +int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, const void* const* sources, std::uint32_t sourceMode, std::uint32_t outputMode, std::uint32_t channelCount, const void* const* sourceParameters, const void* const* sourceMatrices, const void* const* inputChannels, void* const* outputChannels) { + if (!batch || sourceCount == 0 || sourceCount > 65535 || sourceMode > 2 || outputMode > 1 || channelCount == 0 || channelCount > 36 || !sources || !sourceParameters || !sourceMatrices || !outputChannels) return SCE_ACM_ERROR_BAD_POINTER; + std::uintptr_t alignment = reinterpret_cast<std::uintptr_t>(sources) | reinterpret_cast<std::uintptr_t>(sourceParameters) | reinterpret_cast<std::uintptr_t>(sourceMatrices) | reinterpret_cast<std::uintptr_t>(inputChannels) | reinterpret_cast<std::uintptr_t>(outputChannels); + for (std::uint32_t index = 0; index < sourceCount; ++index) { + if (!sources[index] || !sourceParameters[index] || !sourceMatrices[index]) return SCE_ACM_ERROR_BAD_POINTER; + std::uint32_t parameterMode; + std::memcpy(¶meterMode, sourceParameters[index], sizeof(parameterMode)); + if (parameterMode > 1) return SCE_ACM_ERROR_BAD_POINTER; + alignment |= reinterpret_cast<std::uintptr_t>(sources[index]) | reinterpret_cast<std::uintptr_t>(sourceParameters[index]) | reinterpret_cast<std::uintptr_t>(sourceMatrices[index]); + } + for (std::uint32_t index = 0; index < channelCount; ++index) { + if (!outputChannels[index]) return SCE_ACM_ERROR_BAD_POINTER; + alignment |= reinterpret_cast<std::uintptr_t>(outputChannels[index]); + if (inputChannels) alignment |= reinterpret_cast<std::uintptr_t>(inputChannels[index]); + } + if (alignment & 127) return static_cast<int>(0x8194000A); + const auto offset = batch->offset; + const auto capacity = batch->buffer_size; + if (offset > capacity || capacity - offset < 8) return static_cast<int>(0x81940009); + auto* buffer = static_cast<std::byte*>(batch->buffer); + const auto writeWord = [buffer](std::size_t position, std::uint64_t value) { std::memcpy(buffer + position, &value, sizeof(value)); }; + const auto encodedCount = sourceCount < 255 ? sourceCount : 255; + writeWord(offset, (static_cast<std::uint64_t>(channelCount) << 24) | (static_cast<std::uint64_t>(encodedCount) << 16) | (std::uint64_t{1} << (sourceMode + 32)) | (std::uint64_t{1} << (outputMode + 35)) | 0x300); + if (capacity < 128 || capacity - offset < 152) return static_cast<int>(0x81940009); + const auto descriptorOffset = capacity - 128; + std::memset(buffer + descriptorOffset, 0, 128); + writeWord(descriptorOffset, sourceCount); + writeWord(descriptorOffset + 8, reinterpret_cast<std::uintptr_t>(inputChannels)); + writeWord(descriptorOffset + 16, reinterpret_cast<std::uintptr_t>(outputChannels)); + writeWord(descriptorOffset + 24, reinterpret_cast<std::uintptr_t>(sources)); + writeWord(descriptorOffset + 32, sourceMode | (static_cast<std::uint64_t>(outputMode) << 32)); + writeWord(descriptorOffset + 40, channelCount); + writeWord(descriptorOffset + 48, reinterpret_cast<std::uintptr_t>(sourceParameters)); + writeWord(descriptorOffset + 56, reinterpret_cast<std::uintptr_t>(sourceMatrices)); + writeWord(offset + 8, 0x8000000000000D); + writeWord(offset + 16, reinterpret_cast<std::uintptr_t>(buffer + descriptorOffset)); + if (capacity - offset < 160) return static_cast<int>(0x81940009); + writeWord(offset + 24, 10); + batch->offset = offset + 32; + batch->buffer_size = descriptorOffset; return 0; } diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index c6691f898d..c9760348db 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -2,6 +2,8 @@ #include <cstddef> #include <cstdint> #include <cstdlib> +#include <cstring> +#include "SceTypes.hpp" extern "C" { int APS5_VABI sceAcmContextCreate(std::uint32_t*); @@ -11,7 +13,7 @@ int APS5_VABI sceAcmBatchStartBuffers(std::uint32_t, std::uint32_t, const void* int APS5_VABI sceAcmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAcm_ConvReverb_SharedInput(void); int APS5_VABI sceAcm_FFT(void); -int APS5_VABI sceAcm_Panner(void); +int APS5_VABI sceAcm_Panner(AcmBatchInfo*, std::uint32_t, const void* const*, std::uint32_t, std::uint32_t, std::uint32_t, const void* const*, const void* const*, const void* const*, void* const*); } static void Require(bool value) { @@ -52,7 +54,33 @@ int main() { Require(sceAcm_ConvReverb_SharedInput() == 0); Require(sceAcm_FFT() == 0); - Require(sceAcm_Panner() == 0); + alignas(128) std::byte commands[256]{}; + alignas(128) std::uint32_t sourceData[32]{}; + alignas(128) const void* sources[16]{sourceData}; + alignas(128) void* outputs[16]{sourceData}; + AcmBatchInfo commandBatch{commands, 0, sizeof(commands)}; + Require(sceAcm_Panner(&commandBatch, 0, sources, 0, 0, 1, sources, sources, nullptr, outputs) == BadPointer); + sourceData[0] = 2; + Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == BadPointer); + sourceData[0] = 0; + outputs[0] = reinterpret_cast<std::byte*>(sourceData) + 4; + Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == static_cast<int>(0x8194000A)); + outputs[0] = sourceData; + Require(sceAcm_Panner(&commandBatch, 1, sources, 2, 1, 1, sources, sources, nullptr, outputs) == 0); + Require(commandBatch.offset == 32 && commandBatch.buffer_size == 128); + std::uint64_t commandWords[4]; + std::memcpy(commandWords, commands, sizeof(commandWords)); + Require(commandWords[0] == ((std::uint64_t{1} << 34) | (std::uint64_t{1} << 36) | 0x01010300)); + Require(commandWords[1] == 0x8000000000000D && commandWords[2] == reinterpret_cast<std::uintptr_t>(commands + 128) && commandWords[3] == 10); + std::uint64_t descriptor[16]; + std::memcpy(descriptor, commands + 128, sizeof(descriptor)); + Require(descriptor[0] == 1 && descriptor[1] == 0 && descriptor[2] == reinterpret_cast<std::uintptr_t>(outputs)); + Require(descriptor[3] == reinterpret_cast<std::uintptr_t>(sources) && descriptor[4] == 0x100000002 && descriptor[5] == 1); + Require(descriptor[6] == reinterpret_cast<std::uintptr_t>(sources) && descriptor[7] == reinterpret_cast<std::uintptr_t>(sources)); + for (std::size_t index = 8; index < 16; ++index) Require(descriptor[index] == 0); + commandBatch = {commands, 0, 152}; + Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == static_cast<int>(0x81940009)); + Require(commandBatch.offset == 0 && commandBatch.buffer_size == 152); Require(sceAcmContextDestroy(context1) == 0); Require(sceAcmContextDestroy(context1) == BadContext); diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index 7bdccfcf99..cde21f87be 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -2,11 +2,11 @@ ### Silent stubs -- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, so `sceAcm_ConvReverb_SharedInput`, `sceAcm_FFT` and `sceAcm_Panner` return success without encoding audio commands. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner commands. `sceAcm_ConvReverb_SharedInput` and `sceAcm_FFT` return success without encoding audio commands. ### Unknown function info -- Context validation error codes (0x81940002 for bad context and 0x81940006 for bad pointer) for [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) follow prosper's `hle_audio_spatial.cpp`. KytyPS5 (`acm.cpp`) and prosper do not track individual batch identifiers, so `sceAcmBatchWait` returns success for valid contexts without inspecting batch IDs. Hardware ACM DSP coprocessor scheduling and console error verification remain unmeasured. +- Context creation, batch submission and waiting use host identifiers instead of the native device and ioctl requests. Batch identifiers, timeout handling, DSP scheduling and service-side errors remain unimplemented. Panner source parameter and matrix payloads are forwarded by the native library; their DSP interpretation remains unverified. - [sceAcm_ConvReverb_SharedIr, sceAcm_IFFT](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAcm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [`sceAcmBatchJobNotification`](../../../core/libs/prx/libSceAcm/Export.cpp) is a parameterless pending declaration for import resolution. Its signature and behavior are unverified; it throws through `NotImplemented_nid_no_patch`. From e6a7c626194885f5aefce655233540acc1bb7d7b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:11:48 +0300 Subject: [PATCH 0752/1360] fix(acm): restore fft command encoding and argument contract --- core/libs/prx/libSceAcm/Export.cpp | 74 +++++++++++++++++++++++++++++- core/libs/tests/GuestAcm.cpp | 40 +++++++++++++++- docs/dev/debt/libSceAcm.md | 4 +- 3 files changed, 113 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 499ad6d0ba..8ce60dc2df 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -83,7 +83,79 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { return 0; } -int APS5_VABI sceAcm_FFT(void) { +int APS5_VABI sceAcm_FFT(AcmBatchInfo* batch, std::int32_t transformSize, std::uint32_t transformCount, std::int32_t inputFormat, const void* const* inputs, std::int32_t outputFormat, void* const* outputs, std::int32_t flags) { + if (!batch || transformCount == 0 || transformCount > 0x7FFFFFFF || !inputs || !outputs || (flags & 3) == 3 || inputFormat < 0 || inputFormat > 1 || outputFormat < 0 || outputFormat > 1) return SCE_ACM_ERROR_BAD_POINTER; + if (transformSize != 512 && transformSize != 1024 && transformSize != 2048) return SCE_ACM_ERROR_BAD_POINTER; + const bool realInput = (flags & 1) != 0; + if (transformSize == 2048 && !realInput) return SCE_ACM_ERROR_BAD_POINTER; + const std::uint32_t inputBytes = transformSize * (inputFormat == 1 ? 2 : 4); + const std::uint32_t outputBytes = transformSize * (outputFormat == 1 ? 2 : 4); + const std::uint32_t inputBlocks = (inputBytes >> 9) >> (realInput ? 1 : 0); + const std::uint32_t outputBlocks = outputBytes >> 9; + std::uint32_t transformCommand; + std::uint32_t inputCommand; + std::uint32_t outputCommand; + std::uint32_t setupCommand; + std::uint32_t sizeCode; + if (transformSize == 512) { + sizeCode = 4; + transformCommand = realInput ? 7 : 11; + inputCommand = realInput ? 2 * inputFormat : 9 + inputFormat; + outputCommand = 4 | outputFormat; + setupCommand = realInput ? 6 : 12; + } else if (transformSize == 1024) { + sizeCode = 5; + transformCommand = realInput ? 8 : 13; + inputCommand = realInput ? 2 * inputFormat : 14 | inputFormat; + outputCommand = 5 | (2 * outputFormat); + setupCommand = realInput ? 10 : 12; + } else { + sizeCode = 6; + transformCommand = 6; + inputCommand = 2 * inputFormat; + outputCommand = 4 | outputFormat; + setupCommand = 8; + } + auto* buffer = static_cast<std::byte*>(batch->buffer); + auto offset = batch->offset; + const auto capacity = batch->buffer_size; + const auto appendWord = [&](std::uint64_t value) { + if (offset > capacity || capacity - offset < sizeof(value)) return false; + std::memcpy(buffer + offset, &value, sizeof(value)); + offset += sizeof(value); + return true; + }; + std::uint64_t header = static_cast<std::uint64_t>(sizeCode) << 8; + if (!(flags & 8)) { + const auto encodedCount = transformCount < 64000 ? transformCount : 64000; + header |= (std::uint64_t{1} << 16) | (static_cast<std::uint64_t>(transformSize) << 8) | (static_cast<std::uint64_t>(flags & 6) << 19) | (static_cast<std::uint64_t>(flags & 1) << 23) | (std::uint64_t{1} << (inputFormat == 1 ? 24 : 26)) | (std::uint64_t{1} << (outputFormat == 1 ? 25 : 27)) | (static_cast<std::uint64_t>(encodedCount) << 28); + } + constexpr int noSpace = static_cast<int>(0x81940009); + constexpr int badAlignment = static_cast<int>(0x8194000A); + if (!appendWord(header) || !appendWord(10 | (setupCommand << 8))) return noSpace; + for (std::uint32_t index = 0; index < transformCount; ++index) { + const auto inputIndex = static_cast<std::size_t>(index) * ((flags & 2) ? 2 : 1); + const auto input = reinterpret_cast<std::uintptr_t>(inputs[inputIndex]); + const auto secondInput = (flags & 2) ? reinterpret_cast<std::uintptr_t>(inputs[inputIndex + 1]) : 0; + if (!(flags & 2) && !input) return SCE_ACM_ERROR_BAD_POINTER; + if ((input | secondInput) & 127) return badAlignment; + const auto inputRecordSize = (flags & 2) ? 24u : 16u; + if (offset > capacity || capacity - offset < inputRecordSize) return noSpace; + appendWord(((flags & 2) ? 7 : 6) | (static_cast<std::uint64_t>(inputBlocks) << 32) | ((flags & 2) ? static_cast<std::uint64_t>(inputBytes / 2) << 48 : 0)); + appendWord(input); + if (flags & 2) appendWord(secondInput); + if (!appendWord(10 | (inputCommand << 8)) || !appendWord(10 | (transformCommand << 8)) || !appendWord(10 | (outputCommand << 8))) return noSpace; + const auto outputIndex = static_cast<std::size_t>(index) * ((flags & 4) ? 2 : 1); + const auto output = reinterpret_cast<std::uintptr_t>(outputs[outputIndex]); + const auto secondOutput = (flags & 4) ? reinterpret_cast<std::uintptr_t>(outputs[outputIndex + 1]) : 0; + if ((output | secondOutput) & 127) return badAlignment; + const auto outputRecordSize = (flags & 4) ? 24u : 16u; + if (offset > capacity || capacity - offset < outputRecordSize) return noSpace; + appendWord(((flags & 4) ? 9 : 8) | (static_cast<std::uint64_t>(outputBlocks) << 32) | ((flags & 4) ? static_cast<std::uint64_t>(outputBytes / 2) << 48 : 0)); + appendWord(output); + if (flags & 4) appendWord(secondOutput); + } + batch->offset = offset; return 0; } diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index c9760348db..9f78524061 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -3,6 +3,7 @@ #include <cstdint> #include <cstdlib> #include <cstring> +#include <initializer_list> #include "SceTypes.hpp" extern "C" { @@ -12,7 +13,7 @@ int APS5_VABI sceAcmBatchStartBuffer(std::uint32_t, const void*, std::size_t, vo int APS5_VABI sceAcmBatchStartBuffers(std::uint32_t, std::uint32_t, const void* const[], void*, std::uint32_t*); int APS5_VABI sceAcmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAcm_ConvReverb_SharedInput(void); -int APS5_VABI sceAcm_FFT(void); +int APS5_VABI sceAcm_FFT(AcmBatchInfo*, std::int32_t, std::uint32_t, std::int32_t, const void* const*, std::int32_t, void* const*, std::int32_t); int APS5_VABI sceAcm_Panner(AcmBatchInfo*, std::uint32_t, const void* const*, std::uint32_t, std::uint32_t, std::uint32_t, const void* const*, const void* const*, const void* const*, void* const*); } @@ -53,7 +54,7 @@ int main() { Require(sceAcmBatchWait(context2, batch2, 0) == 0); Require(sceAcm_ConvReverb_SharedInput() == 0); - Require(sceAcm_FFT() == 0); + Require(sceAcm_FFT(nullptr, 0, 0, 0, nullptr, 0, nullptr, 0) == BadPointer); alignas(128) std::byte commands[256]{}; alignas(128) std::uint32_t sourceData[32]{}; alignas(128) const void* sources[16]{sourceData}; @@ -78,6 +79,41 @@ int main() { Require(descriptor[3] == reinterpret_cast<std::uintptr_t>(sources) && descriptor[4] == 0x100000002 && descriptor[5] == 1); Require(descriptor[6] == reinterpret_cast<std::uintptr_t>(sources) && descriptor[7] == reinterpret_cast<std::uintptr_t>(sources)); for (std::size_t index = 8; index < 16; ++index) Require(descriptor[index] == 0); + commandBatch = {commands, 0, sizeof(commands)}; + Require(sceAcm_FFT(&commandBatch, 2048, 1, 0, sources, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 3) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 0) == 0); + Require(commandBatch.offset == 72 && commandBatch.buffer_size == sizeof(commands)); + std::uint64_t fftWords[9]; + std::memcpy(fftWords, commands, sizeof(fftWords)); + Require(fftWords[0] == 0x1C030400 && fftWords[1] == 0xC0A); + Require(fftWords[2] == 0x400000006 && fftWords[3] == reinterpret_cast<std::uintptr_t>(sourceData)); + Require(fftWords[4] == 0x90A && fftWords[5] == 0xB0A && fftWords[6] == 0x40A); + Require(fftWords[7] == 0x400000008 && fftWords[8] == reinterpret_cast<std::uintptr_t>(sourceData)); + commandBatch = {commands, 0, 71}; + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 0) == static_cast<int>(0x81940009)); + Require(commandBatch.offset == 0); + commandBatch = {commands, 0, sizeof(commands)}; + Require(sceAcm_FFT(&commandBatch, 2048, 1, 1, sources, 1, outputs, 9) == 0); + std::memcpy(fftWords, commands, sizeof(fftWords)); + Require(fftWords[0] == 0x600 && fftWords[1] == 0x80A && fftWords[4] == 0x20A && fftWords[5] == 0x60A && fftWords[6] == 0x50A); + sources[1] = sourceData; + outputs[1] = sourceData; + for (const auto size : {512, 1024, 2048}) { + for (std::int32_t inputFormat = 0; inputFormat < 2; ++inputFormat) { + for (std::int32_t outputFormat = 0; outputFormat < 2; ++outputFormat) { + for (std::int32_t flags = 0; flags < 16; ++flags) { + commandBatch = {commands, 0, sizeof(commands)}; + const auto result = sceAcm_FFT(&commandBatch, size, 1, inputFormat, sources, outputFormat, outputs, flags); + if ((flags & 3) == 3 || (size == 2048 && !(flags & 1))) { + Require(result == BadPointer && commandBatch.offset == 0); + } else { + Require(result == 0 && commandBatch.offset == 72 + ((flags & 2) ? 8 : 0) + ((flags & 4) ? 8 : 0)); + } + } + } + } + } commandBatch = {commands, 0, 152}; Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == static_cast<int>(0x81940009)); Require(commandBatch.offset == 0 && commandBatch.buffer_size == 152); diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index cde21f87be..dde32286b3 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -2,11 +2,11 @@ ### Silent stubs -- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner commands. `sceAcm_ConvReverb_SharedInput` and `sceAcm_FFT` return success without encoding audio commands. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner and FFT commands. `sceAcm_ConvReverb_SharedInput` returns success without encoding audio commands. ### Unknown function info -- Context creation, batch submission and waiting use host identifiers instead of the native device and ioctl requests. Batch identifiers, timeout handling, DSP scheduling and service-side errors remain unimplemented. Panner source parameter and matrix payloads are forwarded by the native library; their DSP interpretation remains unverified. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) context creation, batch submission and waiting use host identifiers instead of the native device and ioctl requests. Batch identifiers, timeout handling, DSP scheduling and service-side errors remain unimplemented. Panner source parameter and matrix payloads are forwarded by the native library; their DSP interpretation remains unverified. - [sceAcm_ConvReverb_SharedIr, sceAcm_IFFT](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAcm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [`sceAcmBatchJobNotification`](../../../core/libs/prx/libSceAcm/Export.cpp) is a parameterless pending declaration for import resolution. Its signature and behavior are unverified; it throws through `NotImplemented_nid_no_patch`. From c4e56885099a83e67434027e9e707b16dd9e9c5c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:11:49 +0300 Subject: [PATCH 0753/1360] docs(audiopropagation): clarify remaining render gaps --- docs/dev/debt/libSceAudioPropagation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 61e8a959a9..578552a304 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. -- [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` selects reflection order; descriptor validation, ray ownership and path generation remain unimplemented; `sceAudioPropagationSourceGetAudioPath` output width and invalid-handle/null-output/index errors are confirmed. The native render call accepts an array of mutable 48-byte records, with float output at +0x18, byte capacity at +0x20, format at +0x28 and channel count at +0x2c. Format 2 uses 16 float channels per system frame and clears the channel count after rendering; only this format remains supported by the host silence model. The native count limit and output capacity depend on system configuration that the host does not store; those checks remain unmatched. Other functions still throw for invalid input; their native validation precedence is not yet implemented +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options and frame configuration remain unknown. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes silence over the supplied capacity, without native accumulation or frame/count limits. Other formats and native validation precedence remain unimplemented. Source/system diagnostic fields still need reconstruction. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From 83fbdef29930035934771596cf41cd611baf2863 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:15:51 +0300 Subject: [PATCH 0754/1360] fix(acm): recover shared reverb descriptors and signature --- core/libs/SceTypes.hpp | 42 ++++++++++++++++++++++++++++++ core/libs/prx/libSceAcm/Export.cpp | 2 +- core/libs/tests/GuestAcm.cpp | 4 +-- docs/dev/debt/libSceAcm.md | 2 ++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index a85ce12b51..b0d1fd063f 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -432,6 +432,48 @@ struct AcmBatchError { std::uint32_t reserved[8]; }; +struct AcmReverbBuffer { + void* samples; + const void* blockMetadata; +}; + +struct AcmReverbInput { + std::uint32_t blockSize; + std::uint32_t blockCount; + std::uint32_t channelCount; + std::uint32_t sampleFormat; + std::uint32_t currentBlock; + std::uint32_t availableBlocks; + const AcmReverbBuffer* const* history; + const void* const* inputChannels; + std::uint32_t pendingBlocks; +}; + +struct AcmReverbFilter { + std::uint32_t blockSize; + std::uint32_t blockCount; + std::uint32_t channelCount; + std::uint32_t sampleFormat; + const void* blockMetadata; + const AcmReverbBuffer* const* channels; +}; + +struct AcmReverbOutput { + std::uint32_t blockSize; + std::uint32_t channelCount; + void* const* samples; + void* const* workspace; + float gain; + float previousGain; + void* const* previousSamples; +}; + +static_assert(sizeof(AcmReverbBuffer) == 16); +static_assert(offsetof(AcmReverbInput, history) == 24); +static_assert(offsetof(AcmReverbInput, pendingBlocks) == 40); +static_assert(sizeof(AcmReverbFilter) == 32); +static_assert(offsetof(AcmReverbOutput, previousSamples) == 32); + struct AcmBatchInfo { void* buffer; std::size_t offset; diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 8ce60dc2df..914d474f0b 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -79,7 +79,7 @@ int APS5_VABI sceAcmContextDestroy(AcmContextId context) { return 0; } -int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { +int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo* batch, std::uint32_t mode, AcmReverbInput* input, std::uint32_t outputCount, const AcmReverbFilter* const* filters, const float* gains, const AcmReverbOutput* const* outputs) { return 0; } diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index 9f78524061..9ba39f03ee 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -12,7 +12,7 @@ int APS5_VABI sceAcmContextDestroy(std::uint32_t); int APS5_VABI sceAcmBatchStartBuffer(std::uint32_t, const void*, std::size_t, void*, std::uint32_t*); int APS5_VABI sceAcmBatchStartBuffers(std::uint32_t, std::uint32_t, const void* const[], void*, std::uint32_t*); int APS5_VABI sceAcmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t); -int APS5_VABI sceAcm_ConvReverb_SharedInput(void); +int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo*, std::uint32_t, AcmReverbInput*, std::uint32_t, const AcmReverbFilter* const*, const float*, const AcmReverbOutput* const*); int APS5_VABI sceAcm_FFT(AcmBatchInfo*, std::int32_t, std::uint32_t, std::int32_t, const void* const*, std::int32_t, void* const*, std::int32_t); int APS5_VABI sceAcm_Panner(AcmBatchInfo*, std::uint32_t, const void* const*, std::uint32_t, std::uint32_t, std::uint32_t, const void* const*, const void* const*, const void* const*, void* const*); } @@ -53,7 +53,7 @@ int main() { Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); Require(sceAcmBatchWait(context2, batch2, 0) == 0); - Require(sceAcm_ConvReverb_SharedInput() == 0); + Require(sceAcm_ConvReverb_SharedInput(nullptr, 0, nullptr, 0, nullptr, nullptr, nullptr) == 0); Require(sceAcm_FFT(nullptr, 0, 0, 0, nullptr, 0, nullptr, 0) == BadPointer); alignas(128) std::byte commands[256]{}; alignas(128) std::uint32_t sourceData[32]{}; diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index dde32286b3..c94c8ce717 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -10,3 +10,5 @@ - [sceAcm_ConvReverb_SharedIr, sceAcm_IFFT](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAcm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [`sceAcmBatchJobNotification`](../../../core/libs/prx/libSceAcm/Export.cpp) is a parameterless pending declaration for import resolution. Its signature and behavior are unverified; it throws through `NotImplemented_nid_no_patch`. + +- The shared-input reverb signature and descriptor fields are recovered from native accesses. Input history indices are mutable; block metadata representation, reverb command encoding and DSP interpretation remain unimplemented in [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp). From ec85f6280d9f63e682ea5b3bd09048fae699cf6a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:15:52 +0300 Subject: [PATCH 0755/1360] fix(systemgesture): restore native query error contracts --- core/libs/prx/libSceSystemGesture/Export.cpp | 32 +++++++++----------- core/libs/tests/GuestSystemGesture.cpp | 10 +++--- docs/dev/debt/libSceSystemGesture.md | 4 +-- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index 4d74061f67..c73a119122 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -10,9 +10,9 @@ #include <vector> static constexpr int32_t GESTURE_HANDLE = 1; -static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80D10002); -static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE = static_cast<int>(0x80D10003); -static constexpr int SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY = static_cast<int>(0x80D10005); +static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001); +static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE = static_cast<int>(0x80890006); +static constexpr int SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY = static_cast<int>(0x80890004); namespace { @@ -161,35 +161,33 @@ int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void) { return 0; } -int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gesture_handle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gestureHandle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (gestureHandle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; std::lock_guard lock(primitiveTouchMutex); if (index >= primitiveTouches.size()) return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; - if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; *event = buildPrimitiveTouchEvent(primitiveTouches[index]); return 0; } -int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(int32_t gesture_handle, uint16_t primitiveId, SystemGesturePrimitiveTouchEvent* event) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(int32_t gestureHandle, uint16_t primitiveId, SystemGesturePrimitiveTouchEvent* event) { if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (gestureHandle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; std::lock_guard lock(primitiveTouchMutex); const auto tracked = std::find_if(primitiveTouches.begin(), primitiveTouches.end(), [&](const TrackedTouch& touch) { return touch.primitiveId == primitiveId; }); - if (tracked == primitiveTouches.end()) - throw std::runtime_error(std::string(__func__) + ": no primitive touch event with id " + std::to_string(primitiveId)); + if (tracked == primitiveTouches.end()) return static_cast<int>(0x80890005); *event = buildPrimitiveTouchEvent(*tracked); return 0; } -int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gesture_handle, SystemGesturePrimitiveTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; - if (!number_of_event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; +int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gestureHandle, SystemGesturePrimitiveTouchEvent* eventBuffer, uint32_t capacityOfBuffer, uint32_t* numberOfEvent) { + if (!eventBuffer || !capacityOfBuffer || !numberOfEvent) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (gestureHandle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; std::lock_guard lock(primitiveTouchMutex); - const std::uint32_t reported = static_cast<std::uint32_t>(std::min<std::size_t>(primitiveTouches.size(), capacity_of_buffer)); - if (reported > 0 && !event_buffer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - for (std::uint32_t i = 0; i < reported; ++i) event_buffer[i] = buildPrimitiveTouchEvent(primitiveTouches[i]); - *number_of_event = reported; + const std::uint32_t reported = static_cast<std::uint32_t>(std::min<std::size_t>(primitiveTouches.size(), capacityOfBuffer)); + for (std::uint32_t i = 0; i < reported; ++i) eventBuffer[i] = buildPrimitiveTouchEvent(primitiveTouches[i]); + *numberOfEvent = reported; return 0; } diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 01d5123939..8444279cc7 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -174,12 +174,14 @@ static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, i Require(byId.current_position.x == 300.0f && byId.current_position.y == 400.0f); Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 4, nullptr) == invalidArgument); Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle + 1, 4, &byId) == invalidHandle); - Require(Throws([&] { sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 9, &byId); })); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 9, &byId) == static_cast<int>(0x80890005)); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle + 1, 0, nullptr) == invalidArgument); std::array<SystemGesturePrimitiveTouchEvent, 2> events{}; std::uint32_t reported = 7; Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 1, &reported) == 0); Require(reported == 1); + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 0, &reported) == invalidArgument); Require(events[0].primitive_id == 3); reported = 7; Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 2, &reported) == 0); @@ -209,8 +211,8 @@ static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, i } int main() { - constexpr int invalidArgument = static_cast<int>(0x80D10002); - constexpr int invalidHandle = static_cast<int>(0x80D10003); + constexpr int invalidArgument = static_cast<int>(0x80890001); + constexpr int invalidHandle = static_cast<int>(0x80890006); const std::int32_t handle = sceSystemGestureOpen(0, nullptr); std::array<unsigned char, 64> buffer; @@ -229,7 +231,7 @@ int main() { Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle + 1) == invalidHandle); - constexpr int indexOutOfArray = static_cast<int>(0x80D10005); + constexpr int indexOutOfArray = static_cast<int>(0x80890004); SystemGestureTouchRecognizer recognizer{}; SystemGestureRectangle rectangle{}; count = 7; diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index e0bc2e09c2..eff9ccbe7c 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,9 +2,9 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetTouchEvents`, `GetTouchEventsCount`, `GetTouchEventByIndex`, `GetTouchEventByEventID` and the recognizer append, remove, reset and update calls report that no recognizer ever matches, so a title receives primitive touch events but no tap, drag, hold, pinch, rotation or flick event. Handle and pointer arguments are still checked across all query entry points. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known +- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetTouchEvents`, `GetTouchEventsCount`, `GetTouchEventByIndex`, `GetTouchEventByEventID` and the recognizer append, remove, reset and update calls report that no recognizer ever matches, so a title receives primitive touch events but no tap, drag, hold, pinch, rotation or flick event. Handle and pointer arguments are still checked across all query entry points. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known ### Unknown function info -- [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureUpdatePrimitiveTouchRecognizer` reads the pad reports the title passes in, `{int32 padHandle, int32 reportNumber, const ScePadData* padDataBuffer, 8 reserved bytes}`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/585d06fa7c04698c41272b95a696a14c4180e13e/src/ps4_libscesystemgesture.pas) records `SceSystemGestureTouchPadData`; `padHandle` is not checked and a report of a pad other than the one the title opened is not told apart. The touch states 0 inactive, 1 begin, 2 active and 3 end are the Vita ones in [RPCS3](https://github.com/RPCS3/rpcs3/blob/38c444cfa1dbe152daa63b1703c12172078cd056/rpcs3/Emu/PSP2/Modules/sceSystemGesture.h), whose recognizer, rectangle and primitive event layouts are the ones main already uses, and no public source states the PS5 values. A primitive keeps the touch id the pad reports, its positions are the pad's touchpad coordinates unscaled, `delta_time` and `elapsed_time` come from the reports' own `timestamp`, and `is_updated` is 1 for a primitive whose state or position changed in the update; the console's units, scaling and `is_updated` rule are unverified. An ended touch stays readable as one `end` event until the next report sampled drops it. A null `pInputData`, a null `padDataBuffer` or a `reportNumber` of 0 or less leaves the events unchanged and returns success, an event buffer shorter than the event count reports as many events as it holds, and `sceSystemGestureGetPrimitiveTouchEventByPrimitiveID` throws for an id that no tracked touch has; what the console answers in those cases is not known +- [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureUpdatePrimitiveTouchRecognizer` reads the pad reports the title passes in, `{int32 padHandle, int32 reportNumber, const ScePadData* padDataBuffer, 8 reserved bytes}`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/585d06fa7c04698c41272b95a696a14c4180e13e/src/ps4_libscesystemgesture.pas) records `SceSystemGestureTouchPadData`; `padHandle` is not checked and a report of a pad other than the one the title opened is not told apart. The touch states 0 inactive, 1 begin, 2 active and 3 end are the Vita ones in [RPCS3](https://github.com/RPCS3/rpcs3/blob/38c444cfa1dbe152daa63b1703c12172078cd056/rpcs3/Emu/PSP2/Modules/sceSystemGesture.h), whose recognizer, rectangle and primitive event layouts are the ones main already uses, and no public source states the PS5 values. A primitive keeps the touch id the pad reports, its positions are the pad's touchpad coordinates unscaled, `delta_time` and `elapsed_time` come from the reports' own `timestamp`, and `is_updated` is 1 for a primitive whose state or position changed in the update; the console's units, scaling and `is_updated` rule are unverified. An ended touch stays readable as one `end` event until the next report sampled drops it. A null `pInputData`, a null `padDataBuffer` or a `reportNumber` of 0 or less leaves the events unchanged and returns success, an event buffer shorter than the event count reports as many events as it holds, while native update null-pointer and report-count checks are not yet matched - [sceSystemGestureDebugGetVersion](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemGesture.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim From b27046d68d0c052f02ebc48d1e4a60ffce680021 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:16:51 +0300 Subject: [PATCH 0756/1360] fix(systemgesture): isolate contexts and restore handle lifecycle --- core/libs/prx/libSceSystemGesture/Export.cpp | 183 ++++++++++++------- core/libs/tests/GuestSystemGesture.cpp | 29 ++- 2 files changed, 146 insertions(+), 66 deletions(-) diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index c73a119122..346cd57893 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -3,13 +3,13 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include <algorithm> +#include <array> #include <cstring> #include <mutex> #include <stdexcept> #include <string> #include <vector> -static constexpr int32_t GESTURE_HANDLE = 1; static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001); static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE = static_cast<int>(0x80890006); static constexpr int SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY = static_cast<int>(0x80890004); @@ -54,7 +54,19 @@ struct TrackedTouch { }; std::mutex primitiveTouchMutex; -std::vector<TrackedTouch> primitiveTouches; +struct GestureContext { + std::int32_t handle = 0; + std::vector<TrackedTouch> touches; +}; + +std::array<GestureContext, 8> gestureContexts; +bool gestureInitialized = false; +std::uint16_t gestureGeneration = 0; + +GestureContext* findGestureContext(std::int32_t gestureHandle) { + const auto found = std::find_if(gestureContexts.begin(), gestureContexts.end(), [gestureHandle](const GestureContext& context) { return context.handle != 0 && context.handle == gestureHandle; }); + return found == gestureContexts.end() ? nullptr : &*found; +} struct TouchSample { std::uint16_t primitiveId; @@ -73,7 +85,7 @@ std::vector<TouchSample> reportSamples(const PadData& report) { return samples; } -void sampleReport(const PadData& report) { +void sampleReport(std::vector<TrackedTouch>& primitiveTouches, const PadData& report) { std::erase_if(primitiveTouches, [](const TrackedTouch& touch) { return touch.eventState == TOUCH_STATE_END; }); for (TrackedTouch& touch : primitiveTouches) { touch.sampled = false; @@ -132,19 +144,29 @@ SystemGesturePrimitiveTouchEvent buildPrimitiveTouchEvent(const TrackedTouch& to extern "C" { -int APS5_VABI sceSystemGestureAppendTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureAppendTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureClose(int32_t gesture_handle) { - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureClose(int32_t gestureHandle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + auto* context = findGestureContext(gestureHandle); + if (!context) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + context->touches.clear(); + context->handle = 0; + return 0; } -int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer, int32_t type, const SystemGestureRectangle* rectangle, const void* param) { +int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer, int32_t type, const SystemGestureRectangle* rectangle, const void* param) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); (void)param; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; RecognizerState state{}; state.magic = RECOGNIZER_STATE_MAGIC; @@ -157,79 +179,93 @@ int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gesture_handle, Syst int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void) { std::lock_guard lock(primitiveTouchMutex); - primitiveTouches.clear(); + gestureInitialized = false; return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gestureHandle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { - if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - if (gestureHandle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; std::lock_guard lock(primitiveTouchMutex); - if (index >= primitiveTouches.size()) return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; - *event = buildPrimitiveTouchEvent(primitiveTouches[index]); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (index >= findGestureContext(gestureHandle)->touches.size()) return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; + *event = buildPrimitiveTouchEvent(findGestureContext(gestureHandle)->touches[index]); return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(int32_t gestureHandle, uint16_t primitiveId, SystemGesturePrimitiveTouchEvent* event) { - if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - if (gestureHandle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; std::lock_guard lock(primitiveTouchMutex); - const auto tracked = std::find_if(primitiveTouches.begin(), primitiveTouches.end(), + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + const auto tracked = std::find_if(findGestureContext(gestureHandle)->touches.begin(), findGestureContext(gestureHandle)->touches.end(), [&](const TrackedTouch& touch) { return touch.primitiveId == primitiveId; }); - if (tracked == primitiveTouches.end()) return static_cast<int>(0x80890005); + if (tracked == findGestureContext(gestureHandle)->touches.end()) return static_cast<int>(0x80890005); *event = buildPrimitiveTouchEvent(*tracked); return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gestureHandle, SystemGesturePrimitiveTouchEvent* eventBuffer, uint32_t capacityOfBuffer, uint32_t* numberOfEvent) { - if (!eventBuffer || !capacityOfBuffer || !numberOfEvent) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - if (gestureHandle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; std::lock_guard lock(primitiveTouchMutex); - const std::uint32_t reported = static_cast<std::uint32_t>(std::min<std::size_t>(primitiveTouches.size(), capacityOfBuffer)); - for (std::uint32_t i = 0; i < reported; ++i) eventBuffer[i] = buildPrimitiveTouchEvent(primitiveTouches[i]); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!eventBuffer || !capacityOfBuffer || !numberOfEvent) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + const std::uint32_t reported = static_cast<std::uint32_t>(std::min<std::size_t>(findGestureContext(gestureHandle)->touches.size(), capacityOfBuffer)); + for (std::uint32_t i = 0; i < reported; ++i) eventBuffer[i] = buildPrimitiveTouchEvent(findGestureContext(gestureHandle)->touches[i]); *numberOfEvent = reported; return 0; } -int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gesture_handle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gestureHandle) { std::lock_guard lock(primitiveTouchMutex); - return static_cast<int>(primitiveTouches.size()); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + return static_cast<int>(findGestureContext(gestureHandle)->touches.size()); } -int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t eventId, SystemGestureTouchEvent* event) { +int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, uint32_t eventId, SystemGestureTouchEvent* event) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); (void)eventId; (void)event; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } -int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { +int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); (void)index; (void)event; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } -int APS5_VABI sceSystemGestureGetTouchEvents(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { +int APS5_VABI sceSystemGestureGetTouchEvents(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchEvent* eventBuffer, uint32_t capacityOfBuffer, uint32_t* numberOfEvent) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); (void)recognizer; - (void)event_buffer; - (void)capacity_of_buffer; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; - if (!number_of_event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - *number_of_event = 0; + (void)eventBuffer; + (void)capacityOfBuffer; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!numberOfEvent) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + *numberOfEvent = 0; return 0; } -int APS5_VABI sceSystemGestureGetTouchEventsCount(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer) { +int APS5_VABI sceSystemGestureGetTouchEventsCount(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); (void)recognizer; - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + return findGestureContext(gestureHandle) != nullptr ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; } -int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchRecognizerInformation* information) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchRecognizerInformation* information) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer || !information) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; RecognizerState state{}; if (!loadRecognizerState(recognizer, state)) @@ -242,61 +278,82 @@ int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(int32_t gesture_hand return 0; } -int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(const void* param) { - (void)param; +int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer() { std::lock_guard lock(primitiveTouchMutex); - primitiveTouches.clear(); + gestureInitialized = true; return 0; } -int32_t APS5_VABI sceSystemGestureOpen(int32_t input_type, const void* param) { - (void)input_type; - (void)param; - return GESTURE_HANDLE; +int32_t APS5_VABI sceSystemGestureOpen(int32_t inputType, const void* param) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (inputType != 0 || param) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + for (std::size_t index = 0; index < gestureContexts.size(); ++index) { + auto& context = gestureContexts[index]; + if (context.handle != 0) continue; + ++gestureGeneration; + if (gestureGeneration == 0) gestureGeneration = 1; + context.touches.clear(); + context.handle = static_cast<std::int32_t>(1191182336u + (static_cast<std::uint32_t>(gestureGeneration) << 8) + index); + return context.handle; + } + return static_cast<int>(0x8089000A); } -int APS5_VABI sceSystemGestureRemoveTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureRemoveTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(int32_t gesture_handle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(int32_t gestureHandle) { std::lock_guard lock(primitiveTouchMutex); - primitiveTouches.clear(); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + findGestureContext(gestureHandle)->touches.clear(); return 0; } -int APS5_VABI sceSystemGestureResetTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureResetTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(int32_t gesture_handle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(int32_t gestureHandle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; return 0; } -int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gesture_handle, const void* param) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gestureHandle, const void* param) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!param) return 0; const auto* input = static_cast<const SystemGestureTouchPadData*>(param); if (!input->pad_data_buffer || input->report_number <= 0) return 0; - std::lock_guard lock(primitiveTouchMutex); - for (std::int32_t i = 0; i < input->report_number; ++i) sampleReport(input->pad_data_buffer[i]); + for (std::int32_t i = 0; i < input->report_number; ++i) sampleReport(findGestureContext(gestureHandle)->touches, input->pad_data_buffer[i]); return 0; } -int APS5_VABI sceSystemGestureUpdateTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdateTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer, const SystemGestureRectangle* rectangle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer, const SystemGestureRectangle* rectangle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast<int>(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; if (!rectangle) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; RecognizerState state{}; diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 8444279cc7..db06160408 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -10,6 +10,7 @@ #include <stdexcept> extern "C" { +int APS5_VABI sceSystemGestureClose(std::int32_t); std::int32_t APS5_VABI sceSystemGestureOpen(std::int32_t, const void*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(std::int32_t, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(std::int32_t); @@ -27,7 +28,7 @@ int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(std::int32_t, void* int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(std::int32_t); int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(std::int32_t); int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const void*); -int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(const void*); +int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(); int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(std::int32_t, std::uint16_t, void*); } @@ -118,7 +119,7 @@ static SystemGesturePrimitiveTouchEvent SingleEvent(std::int32_t handle) { } static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, int invalidHandle, int indexOutOfArray) { - Require(sceSystemGestureInitializePrimitiveTouchRecognizer(nullptr) == 0); + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); const PadData pressed = TouchReport(1000, {{7, 480, 471}}); Require(UpdateWithReports(handle, &pressed, 1) == 0); @@ -207,12 +208,17 @@ static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, i Require(UpdateWithReports(handle, &pressed, 1) == 0); Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); - Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == static_cast<int>(0x80890003)); + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); } int main() { constexpr int invalidArgument = static_cast<int>(0x80890001); constexpr int invalidHandle = static_cast<int>(0x80890006); + Require(sceSystemGestureOpen(0, nullptr) == static_cast<int>(0x80890003)); + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureOpen(1, nullptr) == invalidArgument); const std::int32_t handle = sceSystemGestureOpen(0, nullptr); std::array<unsigned char, 64> buffer; @@ -273,5 +279,22 @@ int main() { TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); + std::array<std::int32_t, 7> otherHandles{}; + for (auto& otherHandle : otherHandles) { + otherHandle = sceSystemGestureOpen(0, nullptr); + Require(otherHandle > 0 && otherHandle != handle); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(otherHandle) == 0); + } + Require(sceSystemGestureOpen(0, nullptr) == static_cast<int>(0x8089000A)); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + Require(sceSystemGestureClose(otherHandles[0]) == 0); + Require(sceSystemGestureClose(otherHandles[0]) == invalidHandle); + const auto replacement = sceSystemGestureOpen(0, nullptr); + Require(replacement > 0 && replacement != otherHandles[0]); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(otherHandles[0]) == invalidHandle); + Require(sceSystemGestureClose(replacement) == 0); + for (std::size_t index = 1; index < otherHandles.size(); ++index) Require(sceSystemGestureClose(otherHandles[index]) == 0); + Require(sceSystemGestureClose(handle) == 0); + Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); return 0; } From 91c0535cd7c0f8e515fcd8479bcc58fdce7acdeb Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:17:39 +0300 Subject: [PATCH 0757/1360] fix(systemgesture): validate native pad report input --- core/libs/prx/libSceSystemGesture/Export.cpp | 12 ++++++------ core/libs/tests/GuestSystemGesture.cpp | 13 ++++++++----- docs/dev/debt/libSceSystemGesture.md | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index 346cd57893..5e1fb249e6 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -331,14 +331,14 @@ int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(int32_t gestureHandle) { return 0; } -int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gestureHandle, const void* param) { +int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gestureHandle, const SystemGestureTouchPadData* input) { std::lock_guard lock(primitiveTouchMutex); if (!gestureInitialized) return static_cast<int>(0x80890003); - if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; - if (!param) return 0; - const auto* input = static_cast<const SystemGestureTouchPadData*>(param); - if (!input->pad_data_buffer || input->report_number <= 0) return 0; - for (std::int32_t i = 0; i < input->report_number; ++i) sampleReport(findGestureContext(gestureHandle)->touches, input->pad_data_buffer[i]); + if (!input || !input->pad_data_buffer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + auto* context = findGestureContext(gestureHandle); + if (!context) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (input->report_number < 0 || input->report_number > 1) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (input->report_number == 1) sampleReport(context->touches, *input->pad_data_buffer); return 0; } diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index db06160408..656f943aca 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -27,7 +27,7 @@ int APS5_VABI sceSystemGestureUpdateTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(std::int32_t, void*, const void*); int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(std::int32_t); int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(std::int32_t); -int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const void*); +int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const SystemGestureTouchPadData*); int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(); int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(std::int32_t, std::uint16_t, void*); @@ -191,11 +191,14 @@ static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, i Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 2, &event) == indexOutOfArray); Require(UpdateWithReports(handle + 1, &pair, 1) == invalidHandle); - Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == 0); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == invalidArgument); Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); const std::array<PadData, 2> sequence{TouchReport(7000, {{3, 110, 200}}), TouchReport(8000, {{3, 130, 210}})}; - Require(UpdateWithReports(handle, sequence.data(), 2) == 0); + Require(UpdateWithReports(handle, sequence.data(), 2) == invalidArgument); + Require(UpdateWithReports(handle, sequence.data(), -1) == invalidArgument); + Require(UpdateWithReports(handle, sequence.data(), 1) == 0); + Require(UpdateWithReports(handle, sequence.data() + 1, 1) == 0); Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); event = SingleEvent(handle); Require(event.primitive_id == 3); @@ -261,7 +264,7 @@ int main() { Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, &rectangle) == 0); Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle) == 0); Require(sceSystemGestureUpdateAllTouchRecognizer(handle) == 0); - Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == 0); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == invalidArgument); Require(sceSystemGestureAppendTouchRecognizer(handle, nullptr) == invalidArgument); Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, nullptr) == invalidArgument); Require(sceSystemGestureAppendTouchRecognizer(handle + 1, &recognizer) == invalidHandle); @@ -275,7 +278,7 @@ int main() { Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle + 1, &recognizer, &rectangle) == invalidHandle); Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle + 1) == invalidHandle); Require(sceSystemGestureUpdateAllTouchRecognizer(handle + 1) == invalidHandle); - Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidHandle); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidArgument); TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index eff9ccbe7c..5d9acec690 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -6,5 +6,5 @@ ### Unknown function info -- [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureUpdatePrimitiveTouchRecognizer` reads the pad reports the title passes in, `{int32 padHandle, int32 reportNumber, const ScePadData* padDataBuffer, 8 reserved bytes}`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/585d06fa7c04698c41272b95a696a14c4180e13e/src/ps4_libscesystemgesture.pas) records `SceSystemGestureTouchPadData`; `padHandle` is not checked and a report of a pad other than the one the title opened is not told apart. The touch states 0 inactive, 1 begin, 2 active and 3 end are the Vita ones in [RPCS3](https://github.com/RPCS3/rpcs3/blob/38c444cfa1dbe152daa63b1703c12172078cd056/rpcs3/Emu/PSP2/Modules/sceSystemGesture.h), whose recognizer, rectangle and primitive event layouts are the ones main already uses, and no public source states the PS5 values. A primitive keeps the touch id the pad reports, its positions are the pad's touchpad coordinates unscaled, `delta_time` and `elapsed_time` come from the reports' own `timestamp`, and `is_updated` is 1 for a primitive whose state or position changed in the update; the console's units, scaling and `is_updated` rule are unverified. An ended touch stays readable as one `end` event until the next report sampled drops it. A null `pInputData`, a null `padDataBuffer` or a `reportNumber` of 0 or less leaves the events unchanged and returns success, an event buffer shorter than the event count reports as many events as it holds, while native update null-pointer and report-count checks are not yet matched +- [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp): pad handles are not validated against native controller information. Begin, active and end states are confirmed, but cancellation state 4, motion thresholds and SDK-dependent duplicate-report handling remain unimplemented. Timing, coordinate conversion and update flags still use the host approximation. Recognizer allocation and native descriptor layouts remain incomplete. - [sceSystemGestureDebugGetVersion](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemGesture.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim From 25b3a519b0a6dc9cdfe546e81cfc57efe652131c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:20:03 +0300 Subject: [PATCH 0758/1360] fix(playgo): enforce optional chunk types and query limits --- core/libs/prx/libScePlayGo/Export.cpp | 32 ++++++++++++++------------- core/libs/tests/GuestPlayGo.cpp | 24 ++++++++++++++++---- docs/dev/debt/libScePlayGo.md | 2 +- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 165a767c99..9fae1d940e 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -131,20 +131,21 @@ int APS5_VABI scePlayGoClose(int handle) { return 0; } -int APS5_VABI scePlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list, uint32_t number_of_entries, uint32_t* out_entries) { +int APS5_VABI scePlayGoGetChunkId(int handle, uint16_t* outChunkIdList, uint32_t numberOfEntries, uint32_t* outEntries) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - if (!out_entries) return SCE_PLAYGO_ERROR_BAD_POINTER; + if (!outEntries) return SCE_PLAYGO_ERROR_BAD_POINTER; const auto& valid = Chunks().ids; - if (!out_chunk_id_list) { - *out_entries = static_cast<uint32_t>(valid.size()); + if (!outChunkIdList) { + *outEntries = static_cast<uint32_t>(valid.size()); return 0; } + if (numberOfEntries == 0 || numberOfEntries > 1000) return SCE_PLAYGO_ERROR_BAD_SIZE; uint32_t written = 0; for (const auto id : valid) { - if (written == number_of_entries) break; - out_chunk_id_list[written++] = id; + if (written == numberOfEntries) break; + outChunkIdList[written++] = id; } - *out_entries = written; + *outEntries = written; return 0; } @@ -174,18 +175,19 @@ int APS5_VABI scePlayGoGetLanguageMask(int handle, uint64_t* out_language_mask) return 0; } -int APS5_VABI scePlayGoGetLocus(int handle, const uint16_t* chunk_ids, uint32_t number_of_entries, int8_t* out_loci) { +int APS5_VABI scePlayGoGetLocus(int handle, const uint16_t* chunkIds, uint32_t numberOfEntries, int8_t* outLoci) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - if (!out_loci) return SCE_PLAYGO_ERROR_BAD_POINTER; - if (const int error = ValidateChunks(chunk_ids, number_of_entries)) return error; - for (uint32_t index = 0; index < number_of_entries; ++index) out_loci[index] = PLAYGO_LOCUS_LOCAL_FAST; + if (!outLoci) return SCE_PLAYGO_ERROR_BAD_POINTER; + if (chunkIds && numberOfEntries > 1000) return SCE_PLAYGO_ERROR_BAD_SIZE; + if (const int error = ValidateChunks(chunkIds, numberOfEntries)) return error; + for (uint32_t index = 0; index < numberOfEntries; ++index) outLoci[index] = PLAYGO_LOCUS_LOCAL_FAST; return 0; } -int APS5_VABI scePlayGoGetOptionalChunk(int handle, int32_t type, PlayGoOptionalChunk* option) { +int APS5_VABI scePlayGoGetOptionalChunk(int handle, std::uint32_t type, PlayGoOptionalChunk* option) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; + if (type > 2) return static_cast<int>(0x80B20024); option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } @@ -199,10 +201,10 @@ int APS5_VABI scePlayGoGetProgress(int handle, const uint16_t* chunk_ids, uint32 return 0; } -int APS5_VABI scePlayGoGetSupportedOptionalChunk(int handle, int32_t type, PlayGoOptionalChunk* option) { +int APS5_VABI scePlayGoGetSupportedOptionalChunk(int handle, std::uint32_t type, PlayGoOptionalChunk* option) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; + if (type > 2) return static_cast<int>(0x80B20024); option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 918e6ad031..c03f4761ca 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> #include <cstdlib> +#include <cstdio> +#include <source_location> #include <filesystem> #include <fstream> #include <initializer_list> @@ -18,15 +20,19 @@ int APS5_VABI scePlayGoOpen(int*, const void*); int APS5_VABI scePlayGoClose(int); int APS5_VABI scePlayGoGetLanguageMask(int, std::uint64_t*); int APS5_VABI scePlayGoSetLanguageMask(int, std::uint64_t); -int APS5_VABI scePlayGoGetSupportedOptionalChunk(int, std::int32_t, PlayGoOptionalChunk*); -int APS5_VABI scePlayGoGetOptionalChunk(int, std::int32_t, PlayGoOptionalChunk*); +int APS5_VABI scePlayGoGetSupportedOptionalChunk(int, std::uint32_t, PlayGoOptionalChunk*); +int APS5_VABI scePlayGoGetOptionalChunk(int, std::uint32_t, PlayGoOptionalChunk*); int APS5_VABI scePlayGoSetToDoList(int, const PlayGoToDo*, std::uint32_t); int APS5_VABI scePlayGoGetToDoList(int, PlayGoToDo*, std::uint32_t, std::uint32_t*); int APS5_VABI scePlayGoGetLocus(int, const std::uint16_t*, std::uint32_t, std::int8_t*); int APS5_VABI scePlayGoGetChunkId(int, std::uint16_t*, std::uint32_t, std::uint32_t*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "requirement failed at line %u\n", location.line()); + std::abort(); +} template <typename Operation> static void RequireMissingChunkMetadata(Operation&& operation) { @@ -75,8 +81,9 @@ static int CheckScenarioWithoutChunkTable(const char* root) { std::uint32_t chunkCount = 0; Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); std::vector<std::uint16_t> ids(chunkCount + 1, 0xFFFF); - Require(scePlayGoGetChunkId(handle, ids.data(), static_cast<std::uint32_t>(ids.size()), &chunkCount) == 0 && chunkCount == 1000); + Require(scePlayGoGetChunkId(handle, ids.data(), chunkCount, &chunkCount) == 0 && chunkCount == 1000); for (std::uint32_t index = 0; index < chunkCount; ++index) Require(ids[index] == index); + Require(ids.back() == 0xFFFF); std::vector<std::int8_t> listedLoci(chunkCount, 0); Require(scePlayGoGetLocus(handle, ids.data(), chunkCount, listedLoci.data()) == 0); for (const auto locus : listedLoci) Require(locus == 3); @@ -116,6 +123,9 @@ static int CheckNoPlayGoFile(const char* root) { Require(getOptionalChunk(handle + 1, 0, &option) == badHandle); Require(getOptionalChunk(handle, 0, nullptr) == badPointer); Require(getOptionalChunk(handle, 0, &option) == 0 && option.bitmask == ~0ull); + Require(getOptionalChunk(handle, 3, &option) == static_cast<int>(0x80B20024)); + Require(getOptionalChunk(handle, 0xFFFFFFFF, &option) == static_cast<int>(0x80B20024)); + Require(getOptionalChunk(handle, 3, nullptr) == badPointer); } PlayGoToDo todo[2] = {{0, 3, 0}, {0, 0, 0}}; @@ -135,6 +145,12 @@ static int CheckNoPlayGoFile(const char* root) { const std::uint16_t chunk = 0; std::int8_t locus = 0; Require(scePlayGoGetLocus(handle, &chunk, 0, &locus) == badSize); + Require(scePlayGoGetLocus(handle, &chunk, 1001, &locus) == badSize); + std::uint32_t invalidListCount = 42; + std::uint16_t invalidListOutput = 0; + Require(scePlayGoGetChunkId(handle, &invalidListOutput, 0, &invalidListCount) == badSize); + Require(scePlayGoGetChunkId(handle, &invalidListOutput, 1001, &invalidListCount) == badSize); + Require(invalidListCount == 42); Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); const std::uint16_t undeclared[2] = {1, 10}; std::int8_t loci[2] = {0, 0}; diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index cfd6b6de0d..40a6135747 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -5,4 +5,4 @@ - [scePlayGoRequestNextChunk, scePlayGoSnapshot](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [scePlayGoSetLanguageMask](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - stores the mask that `scePlayGoGetLanguageMask` then reports, and returns `SCE_PLAYGO_ERROR_BAD_HANDLE` for another handle, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/playgo/playgo.cpp#L346-L359) does on the PS4. shadPS4 also returns `SCE_PLAYGO_ERROR_NOT_INITIALIZED` before `scePlayGoInitialize` and starts from the system language's mask; here the mask starts with every language and initialization is not tracked. Not measured on a PS5 - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, and a package with more chunks would see ids from 1000 up as not listed. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for `type` 0, 1 or 2, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. Native requests confirm types 0 through 2 and an eight-byte result; their meanings and bit layout are still determined by the external service, not this module; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. From f2224eae0c689fcb43948518ac6c4dc6fcd390c3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:23:18 +0300 Subject: [PATCH 0759/1360] docs(netctl): narrow unresolved aprp signature evidence --- docs/dev/debt/libSceNetCtl.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 5e377b9392..2e936ee5bc 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -11,17 +11,7 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown -- [sceNetCtlApRpCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpClearEvent](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpRegisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpUnregisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpGetState](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature -- [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [ApRp exports](../../../core/libs/prx/libSceNetCtl/Export.cpp) are constant-success functions in the supplied native module and consume no arguments. No callers were found in the supplied system-library decompilations, so unused callback, output and configuration argument types remain unresolved. Protected stub bodies are unchanged. ### Functional From d9c2facdb9f3ae64073066e16dd9d7c067893c30 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:29:25 +0300 Subject: [PATCH 0760/1360] fix(ampr): preserve nop payload when data is absent --- core/libs/prx/libSceAmpr/Export.cpp | 2 -- core/libs/tests/GuestAmpr.cpp | 4 ++++ docs/dev/debt/libSceAmpr.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 6e55ecbd7b..fe55109c4c 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -96,7 +96,6 @@ static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, con if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast<std::uint32_t>(bytes)}; std::uint8_t* destination = buffer->base + buffer->offset; - std::memset(destination, 0, bytes); std::memcpy(destination, &header, sizeof(header)); if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u); buffer->offset += static_cast<std::uint32_t>(bytes); @@ -432,7 +431,6 @@ int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uin int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; - if (dwords != 0u && !data) throw std::invalid_argument("sceAmprCommandBufferNopWithData: null data"); return AppendNop(buffer, dwords, data); } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index ca17fc2c23..f6017f152f 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -470,6 +470,10 @@ void TestNops() { RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); Require(std::memcmp(recorder.memory.data() + offset + sizeof(Apr::CommandHeader), data, sizeof(data)) == 0); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); + const auto emptyPayloadOffset = recorder.Offset(); + std::memset(recorder.memory.data() + emptyPayloadOffset, 0x5A, sceAmprMeasureCommandSizeNopWithData(4)); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, nullptr) == 0); + for (std::size_t index = 0; index < 12; ++index) Require(recorder.memory[emptyPayloadOffset + sizeof(Apr::CommandHeader) + index] == 0x5A); Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); const auto rejected = static_cast<std::uint64_t>(static_cast<std::uint32_t>(invalidArgument)); diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 764ea7ec20..ae062de3f3 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -7,7 +7,7 @@ - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. - [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted -- [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded +- [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp): colors are not recorded and command sizes use the host encoding. Native signatures confirm that set takes a color pointer and push/measure take the color value. - [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled ### Functional From 808e9d9da21f79d4f33d0bab570fc40b2676756a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:29:27 +0300 Subject: [PATCH 0761/1360] fix(vrtracker2): restore legacy input pointer contracts --- core/libs/prx/libSceVrTracker2/Export.cpp | 33 ++++++++++++++++++++--- docs/dev/debt/libSceVrTracker2.md | 2 ++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index db5458a308..2e521f452e 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -41,6 +41,21 @@ static_assert(offsetof(VrTracker2InitializeParameter, coordinateSystem) == 52); static_assert(offsetof(VrTracker2InitializeParameter, reserved) == 58); static_assert(sizeof(VrTracker2MemoryParameter) == 32); static_assert(sizeof(VrTracker2MemoryRequirement) == 32); +struct VrTracker2LegacyParameter12 { + std::uint32_t size; + std::uint8_t opaque[8]; +}; + +struct VrTracker2LegacyParameter20 { + std::uint32_t size; + std::uint8_t opaque[16]; +}; + +struct VrTracker2UnsupportedCoordinateParameter; + +static_assert(sizeof(VrTracker2LegacyParameter12) == 12); +static_assert(sizeof(VrTracker2LegacyParameter20) == 20); + struct VrTracker2CalibrationResult; struct VrTracker2TelemetryData; struct VrTracker2PoseParameter; @@ -408,12 +423,24 @@ std::int32_t APS5_VABI VrTracker2GetScalarResult(VrTracker2ScalarResult* result) return VR_TRACKER2_ERROR_NOT_INITIALIZED; } -APS5_EXPORT("omJ7b8FqAN0", VrTracker2NotInitialized); -APS5_EXPORT("G2PWj1HLqn8", VrTracker2NotInitialized); +APS5_EXPORT("omJ7b8FqAN0", VrTracker2LegacyQuery12); +std::int32_t APS5_VABI VrTracker2LegacyQuery12(const VrTracker2LegacyParameter12* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("G2PWj1HLqn8", VrTracker2LegacyQuery20); +std::int32_t APS5_VABI VrTracker2LegacyQuery20(const VrTracker2LegacyParameter20* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("73zcxAFawYk", VrTracker2NotInitialized); APS5_EXPORT("vegrWFNiQwc", VrTracker2NotInitialized); APS5_EXPORT("6r01IawmypA", VrTracker2NotInitialized); -APS5_EXPORT("zeof7sF2fqE", VrTracker2NotInitialized); +APS5_EXPORT("zeof7sF2fqE", VrTracker2UnsupportedCoordinateOperation); +std::int32_t APS5_VABI VrTracker2UnsupportedCoordinateOperation(const VrTracker2UnsupportedCoordinateParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + APS5_EXPORT("Z0rfHOHQV1w", VrTracker2NotInitialized); APS5_EXPORT("4l7x+uy3dWw", VrTracker2NotInitialized); APS5_EXPORT("ZXlt-Xb3da4", VrTracker2NotInitialized); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 8e138fc9db..29b19cc61c 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -24,3 +24,5 @@ - [Extended result and boundary fitting](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. The extended result spans 816 bytes; its coordinate-specific parameter prefixes the 48-byte query with a coordinate selector and four-byte padding. Boundary fitting takes the recovered point list, optionally returns two 80-byte bounds, and writes four byte flags plus four floats; flag and dimension semantics remain unresolved. The device-center output is a 16-byte size/three-float record. - [Mesh and image-pose utilities](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Mesh generation takes vertex and 16-bit index outputs plus a 32-bit format. Timed-pose queries take an in/out result, timestamp and user tag. Image submission consumes a record with format at byte 16 and a data pointer at byte 80; image-pose retrieval uses an in/out record with a buffer pointer and capacity. Record fields and initialized algorithms remain incomplete. Scalar queries use float outputs; the map-scalar query also consumes map data. + +- [Legacy unsupported operations](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate input-pointer signatures. Two consume only the size prefix of 12/20-byte records, leaving their other fields unrecoverable from these bodies. The coordinate operation reads a selector at byte 4 but always returns unsupported after initialization; the remaining record layout is unresolved. From ffbb7aabbe6645890bc0112fd96401036bfa87d3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:29:30 +0300 Subject: [PATCH 0762/1360] fix(ime): restore configuration argument slots --- core/libs/prx/libSceIme/Export.cpp | 4 ++-- docs/dev/debt/libSceIme.md | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index a67cd80b39..9485063034 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -779,12 +779,12 @@ int APS5_VABI sceImeVshNotifyPreeditMetaInfo(ImeVshContext* context, const ImePr return 0; } -int APS5_VABI sceImeConfigSet(void) { +int APS5_VABI sceImeConfigSet(std::uint64_t unusedArgument, std::uint64_t configuration, std::uint32_t key, std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceImeConfigGet(void) { +int APS5_VABI sceImeConfigGet(std::uint64_t unusedArgument, std::uint64_t configuration, std::uint32_t key) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index d7134b33e2..b26437726a 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -11,9 +11,8 @@ - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. - [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn -- [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature -- [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types cannot be recovered from that body. +- [sceImeConfigSet and sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) preserve the native argument slots: an unused first slot, a 64-bit configuration value, a 32-bit key and, for set, a 32-bit value. The service receives a 24-byte request; configuration/key semantics and unused argument types remain unresolved. The getter advertises one output transfer but does not initialize its descriptor in this module. Protected bodies remain unchanged. - [libSceIme pending exports](../../../core/libs/prx/libSceIme/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIme.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From 4375cf06ab76fd8498d4a6145a80ecd962984a9a Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 20 Oct 2026 21:31:38 -0400 Subject: [PATCH 0763/1360] fix(agc): resolve and render a multisampled target through its sRGB twin format Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/MultisampleTarget.cpp | 85 +++++- .../tests/execution/MultisampleRetirement.cpp | 43 +++ .../tests/execution/MultisampleSrgbTwin.cpp | 247 ++++++++++++++++++ 3 files changed, 362 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 8911499a06..439edfd0a1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -49,6 +49,16 @@ bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } +constexpr VkImageUsageFlags twinViewUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + +VkFormat colorTwinFormat(const Context& context, VkFormat format) { + if (const auto srgb = SrgbTwinFormat(context, format); srgb != VK_FORMAT_UNDEFINED) return srgb; + for (const auto unorm : {VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_A8B8G8R8_UNORM_PACK32}) { + if (SrgbTwinFormat(context, unorm) == format) return unorm; + } + return VK_FORMAT_UNDEFINED; +} + bool recreated(const ColorTarget& resident, const ColorTarget& use) { return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes; } @@ -101,6 +111,13 @@ public: return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0; }; if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED; + twinFormat = colorTwinFormat(context, target.format); + if (twinFormat != VK_FORMAT_UNDEFINED) { + VkFormatProperties twinProperties{}; + context.formatProperties(context.physical, twinFormat, &twinProperties); + const VkImageCreateFlags twinFlags = orderFlags | VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + if ((twinProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) == 0 || !storageSupports(target.format, usage, twinFlags) || !storageSupports(twinFormat, twinViewUsage, twinFlags)) twinFormat = VK_FORMAT_UNDEFINED; + } try { const auto created = allocate(usage, orderFlags); image = created.image; @@ -135,6 +152,16 @@ public: try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; info.flags = flags; + const std::array<VkFormat, 2> viewFormats{target.format, twinFormat}; + VkImageFormatListCreateInfo formatList{VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO}; + if (twinFormat != VK_FORMAT_UNDEFINED) { + if ((flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { + formatList.viewFormatCount = static_cast<std::uint32_t>(viewFormats.size()); + formatList.pViewFormats = viewFormats.data(); + info.pNext = &formatList; + } + info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; @@ -203,6 +230,7 @@ public: Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); for (auto& [key, texture] : textures) retiredTextures.push_back(std::move(texture)); textures.clear(); + retired.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, std::exchange(twinView, VK_NULL_HANDLE)}); retired.push_back({image, memory, view}); image = storable.image; memory = storable.memory; @@ -336,14 +364,39 @@ public: return {shared_from_this(), texture.get()}; } - VkRenderPass ResolvePass() { + VkImageView View(VkFormat format) { + if (format == target.format) return view; + Require(twinFormat != VK_FORMAT_UNDEFINED && format == twinFormat, "a multisampled color target is only viewed in its own format or its sRGB twin"); + if (twinView == VK_NULL_HANDLE) { + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = twinFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = twinViewUsage; + viewInfo.pNext = &viewUsage; + Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &twinView), "vkCreateImageView multisampled target twin"); + } + return twinView; + } + + bool Serves(const ColorTarget& use) const { + if (use.format == target.format) return sameTarget(target, use); + auto twin = use; + twin.format = target.format; + return twinFormat != VK_FORMAT_UNDEFINED && use.format == twinFormat && sameTarget(target, twin); + } + + VkRenderPass ResolvePass(VkFormat format) { + auto& resolvePass = resolvePasses[format]; if (resolvePass != VK_NULL_HANDLE) return resolvePass; std::array<VkAttachmentDescription, 2> attachments{}; - attachments[0].format = target.format; + attachments[0].format = format; attachments[0].samples = static_cast<VkSampleCountFlagBits>(target.samples); attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[1].format = target.format; + attachments[1].format = format; attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -376,7 +429,7 @@ public: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; - VkRenderPass resolvePass = VK_NULL_HANDLE; + std::map<VkFormat, VkRenderPass> resolvePasses; MultisampledCmask cmask; MultisampledColorFill colorFill; bool fromTexture = false; @@ -391,6 +444,8 @@ private: VkImageUsageFlags usage = 0; VkFormat storageFormat = VK_FORMAT_UNDEFINED; VkImageCreateFlags storageFlags = 0; + VkFormat twinFormat = VK_FORMAT_UNDEFINED; + VkImageView twinView = VK_NULL_HANDLE; std::vector<Allocation> retired; VkImageView storageView = VK_NULL_HANDLE; bool storageWritten = false; @@ -487,8 +542,9 @@ private: void release() noexcept { textures.clear(); retiredTextures.clear(); - if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + for (const auto& [format, resolvePass] : resolvePasses) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr); if (orderView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, orderView, nullptr); + if (twinView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, twinView, nullptr); if (storageView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, storageView, nullptr); destroy({image, memory, view}); for (const auto& allocation : retired) destroy(allocation); @@ -610,11 +666,13 @@ void discardRetiredTargets() { std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); }); } -MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { +MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) { discardRetiredTargets(); for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; - if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (target->context.device != context.device || !target->Serves(requested)) continue; + auto use = requested; + use.format = target->target.format; if (recreated(target->target, use)) { { std::unique_lock lock(guardedMutex()); @@ -636,6 +694,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { target->ApplyFastClear(use); return *target; } + const auto& use = requested; auto target = std::make_shared<MultisampleTarget>(context, use); target->Keep(); targets().push_back(std::move(target)); @@ -956,9 +1015,9 @@ SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const Col VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) { std::lock_guard lock(targetsMutex()); - const auto& acquired = acquire(context, target); + auto& acquired = acquire(context, target); if (image != nullptr) *image = acquired.image; - return acquired.view; + return acquired.View(target.format); } VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image) { @@ -966,7 +1025,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar auto& acquired = acquire(context, target); owner = acquired.shared_from_this(); if (image != nullptr) *image = acquired.image; - return acquired.view; + return acquired.View(target.format); } void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr<void>& owner) { @@ -1052,12 +1111,12 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); target.Keep(); - const std::array<VkImageView, 2> views{target.view, destinationView}; - auto framebuffer = std::make_shared<Framebuffer>(context, target.ResolvePass(), views, source.extent); + const std::array<VkImageView, 2> views{target.View(source.format), destinationView}; + auto framebuffer = std::make_shared<Framebuffer>(context, target.ResolvePass(source.format), views, source.extent); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = target.ResolvePass(); + begin.renderPass = target.ResolvePass(source.format); begin.framebuffer = framebuffer->Handle(); begin.renderArea = {{0, 0}, source.extent}; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index b603cc7326..9422da8090 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -14,6 +14,7 @@ #else #include <sys/mman.h> #endif +#include <algorithm> #include <chrono> #include <condition_variable> #include <cstdint> @@ -66,6 +67,15 @@ PFN_vkDestroyImageView realDestroyImageView = nullptr; PFN_vkCmdClearColorImage realClearColorImage = nullptr; PFN_vkCmdCopyImage realCopyImage = nullptr; +[[noreturn]] void FailViewOutlivesImage() { + std::fputs("an MSAA image would be destroyed before one of its views; stopping\n", stderr); + std::fflush(stderr); +#ifdef _WIN32 + TerminateProcess(GetCurrentProcess(), 1); +#endif + std::_Exit(1); +} + [[noreturn]] void FailPendingImage() { std::fputs("an unsubmitted MSAA image would be destroyed; stopping without submitting it\n", stderr); std::fflush(stderr); @@ -104,6 +114,7 @@ VKAPI_ATTR void VKAPI_CALL CountDestroyImage(VkDevice device, VkImage image, con for (const auto& [pending, generation] : pendingImages) { if (pending == image && images.at(image).generation == generation) FailPendingImage(); } + if (std::any_of(views.begin(), views.end(), [&](const auto& entry) { return entry.second == image; })) FailViewOutlivesImage(); realDestroyImage(device, image, callbacks); images.erase(image); ++destroyed; @@ -514,6 +525,37 @@ public: AwaitImages(0, "promoted MSAA targets were not fully released"); } + void StorageTwin() { + GuestBlock block; + const auto target = Target(block); + auto twin = target; + twin.format = VK_FORMAT_R8G8B8A8_SRGB; + const auto resource = Resource(block); + VkImage renderedImage = VK_NULL_HANDLE, twinImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE, promotedTwinImage = VK_NULL_HANDLE; + Image rendered{}, promoted{}; + VkImageView twinView = VK_NULL_HANDLE; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + rendered = ImageFor(MultisampleTargetView(context, target), &renderedImage); + twinView = MultisampleTargetView(context, twin); + ImageFor(twinView, &twinImage); + Require(twinImage == renderedImage, "an sRGB use of an MSAA surface rendered as UNORM does not reach its samples"); + } + Sync(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + promoted = ImageFor(MultisampleTargetStorageView(context, resource), &promotedImage); + Require(promoted.generation != rendered.generation, "MSAA storage promotion kept the rendered image"); + const auto promotedTwin = MultisampleTargetView(context, twin); + ImageFor(promotedTwin, &promotedTwinImage); + Require(promotedTwinImage == promotedImage, "after storage promotion an sRGB use of the MSAA surface still views the retired image"); + } + Sync(); + block.Unmap(); + Clear(); + AwaitImages(0, "MSAA targets with twin views were not fully released"); + } + private: AgcDriver::VulkanDevice& device; Context context; @@ -533,6 +575,7 @@ int main() { run.SampledRemap(); run.StorageRemap(); run.OrderRemap(); + run.StorageTwin(); std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp new file mode 100644 index 0000000000..a28966b839 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp @@ -0,0 +1,247 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <deque> +#include <initializer_list> +#include <iostream> +#include <memory> +#include <mutex> +#include <stdexcept> +#include <utility> + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTarget; +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Side = 64; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast<std::byte*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast<std::byte*>(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "multisampled sRGB twin: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t Address() const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(block)); } + +private: + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque<GuestBlock>(); + return blocks->emplace_back(); +} + +ColorTarget MultisampledTarget(const GuestBlock& color, VkFormat format) { + ColorTarget target{}; + target.address = color.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = format; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + return target; +} + +struct Acquired { + VkImageView view = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + std::shared_ptr<void> owner; +}; + +Acquired Acquire(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, const ColorTarget& target) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Acquired acquired; + acquired.view = AgcDriver::Graphics::MultisampleTargetView(context, target, acquired.owner, &acquired.image); + Require(acquired.view != VK_NULL_HANDLE && acquired.image != VK_NULL_HANDLE && acquired.owner != nullptr, "the multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + return acquired; +} + +std::array<std::byte, 4> UniformTexel(const AgcDriver::Graphics::Context& context, VkImage multisampled, VkFormat format) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {Side, Side, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage resolved = VK_NULL_HANDLE; + AgcDriver::Graphics::Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &resolved), "vkCreateImage resolved texels"); + VkMemoryRequirements requirements{}; + context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, resolved, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VkDeviceMemory memory = VK_NULL_HANDLE; + AgcDriver::Graphics::Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory resolved texels"); + AgcDriver::Graphics::Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, resolved, memory, 0), "vkBindImageMemory resolved texels"); + AgcDriver::Graphics::Buffer readback(context, static_cast<std::size_t>(Side) * Side * 4u, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + AgcDriver::Graphics::CommandBatch batch(context); + const auto commands = batch.Handle(); + std::array<VkImageMemoryBarrier, 2> barriers{}; + for (auto& barrier : barriers) { + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + } + barriers[0].image = multisampled; + barriers[0].srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barriers[0].oldLayout = barriers[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; + barriers[1].image = resolved; + barriers[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barriers[1].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barriers[1].newLayout = VK_IMAGE_LAYOUT_GENERAL; + context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, static_cast<std::uint32_t>(barriers.size()), barriers.data()); + VkImageResolve region{}; + region.srcSubresource = region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.extent = {Side, Side, 1}; + context.Function<PFN_vkCmdResolveImage>("vkCmdResolveImage")(commands, multisampled, VK_IMAGE_LAYOUT_GENERAL, resolved, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + VkMemoryBarrier toCopy{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toCopy.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &toCopy, 0, nullptr, 0, nullptr); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {Side, Side, 1}; + context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, resolved, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr); + batch.SubmitAndWait(); + } + readback.Invalidate(); + const auto bytes = readback.Bytes(); + std::array<std::byte, 4> texel{}; + std::copy_n(bytes.begin(), texel.size(), texel.begin()); + for (std::size_t offset = 0; offset < bytes.size(); offset += texel.size()) Require(std::equal(texel.begin(), texel.end(), bytes.begin() + static_cast<std::ptrdiff_t>(offset)), "a cleared multisampled target holds different texels"); + context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, resolved, nullptr); + context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr); + return texel; +} + +std::array<std::byte, 4> Bytes(std::uint32_t pattern) { + std::array<std::byte, 4> bytes{}; + std::memcpy(bytes.data(), &pattern, bytes.size()); + return bytes; +} + +void FillTakenThroughTwin(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, VkFormat created, VkFormat used, std::uint32_t pattern) { + auto& color = NewBlock(); + const auto target = MultisampledTarget(color, created); + const auto rendered = Acquire(device, context, target); + const auto bytes = static_cast<std::size_t>(AgcDriver::Graphics::MultisampledColorBytes(target)); + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), bytes, pattern), "the color fill is not taken as the target's clear in its creation format"); + AgcDriver::Graphics::NoteColorMetadataFill(color.Address(), bytes, pattern); + const auto twin = Acquire(device, context, MultisampledTarget(color, used)); + Require(twin.image == rendered.image, "the twin use of the filled target does not reach its samples"); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(UniformTexel(context, twin.image, created) == Bytes(pattern), "a fill taken as a clear through the twin format does not store the guest's bytes"); +} + +bool TwinFormatsShareTheirSamples(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::cout << "skipped, the device has no 4x color targets\n"; + return false; + } + struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + } + } clearTargets{device, context.device}; + for (const auto [first, second] : {std::pair{VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM}, std::pair{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB}}) { + if (AgcDriver::Graphics::SrgbTwinFormat(context, first) != second && AgcDriver::Graphics::SrgbTwinFormat(context, second) != first) continue; + auto& color = NewBlock(); + const auto rendered = Acquire(device, context, MultisampledTarget(color, first)); + const auto twin = Acquire(device, context, MultisampledTarget(color, second)); + Require(twin.image == rendered.image && twin.owner == rendered.owner, "a use of a multisampled surface in its sRGB twin format does not reach the samples rendered in the other format"); + Require(twin.view != rendered.view, "a use of a multisampled surface in its sRGB twin format is given the view of the other format"); + const auto again = Acquire(device, context, MultisampledTarget(color, first)); + Require(again.image == rendered.image && again.view == rendered.view, "returning to the first format does not give back the first format's view"); + } + if (AgcDriver::Graphics::SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB) { + FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, 0x80808080u); + FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_R8G8B8A8_UNORM, 0xff0000ffu); + } + auto& other = NewBlock(); + const auto unorm = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R8G8B8A8_UNORM)); + const auto floating = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R16G16_SFLOAT)); + Require(floating.image != unorm.image, "a multisampled surface used in an unrelated format of the same size shares the other format's samples"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool tested = TwinFormatsShareTheirSamples(*device); + LibcRunShutdown_nid_postfix(); + if (!tested) return VulkanTestSkipped; + std::cout << "multisampled sRGB twin tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + LibcRunShutdown_nid_postfix(); + } catch (...) { + } + return 1; + } +} From 3823004797a1b38c2a4aadbcec6a49b1ed745a27 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:29:32 +0300 Subject: [PATCH 0764/1360] fix(mouse): validate default absolute position bounds --- core/libs/prx/libSceMouse/src/mouse_impl.cpp | 3 ++- core/libs/prx/libSceMouse/tests/Mouse.cpp | 3 +++ docs/dev/debt/libSceMouse.md | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index d73ccb929d..b4df78375a 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -82,10 +82,11 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } -int ResetAbsolutePosition(std::int32_t handle, std::uint16_t, std::uint16_t) { +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y) { std::lock_guard lock(mouseMutex); if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + if (x >= 1920 || y >= 1080) return MOUSE_ERROR_INVALID_ARG; return MOUSE_OK; } diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index 3fd5a90ca2..7e7b1fcf13 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -34,6 +34,9 @@ int main() { Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); Require(sceMouseResetAbsolutePosition(42, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1919, 1079) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1920, 1079) == MOUSE_ERROR_INVALID_ARG); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1919, 1080) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseRead(42, data, 1) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseRead(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseRead(MOUSE_HANDLE, data, 65) == MOUSE_ERROR_INVALID_ARG); diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md index 19fe9ab3b5..afca5f0db7 100644 --- a/docs/dev/debt/libSceMouse.md +++ b/docs/dev/debt/libSceMouse.md @@ -6,4 +6,4 @@ ### Functional -- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - signature taken from a call site (a handle, then x and y as 16-bit values: PPSA30601 passes the centre of its display size); behaviour not measured on hardware. Validates initialization and the handle like `sceMouseRead` and returns `MOUSE_OK`; the position is not stored because the library reports relative motion only +- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the position is not stored because the host reports relative motion only. Native input widths and validation are confirmed, including the default 1920 by 1080 bounds; other native coordinate modes and absolute reports remain unimplemented From 5bf4c57a547877d66749061e6262177a0eb63f32 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Wed, 21 Oct 2026 03:38:08 -0400 Subject: [PATCH 0765/1360] test(agc): run the MSAA twin storage case only with shaderStorageImageMultisample Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/execution/MultisampleRetirement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index 9422da8090..27f7d4ae49 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -575,7 +575,7 @@ int main() { run.SampledRemap(); run.StorageRemap(); run.OrderRemap(); - run.StorageTwin(); + if (context.storageImageMultisample) run.StorageTwin(); std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes"); return 0; } catch (const std::exception& error) { From e621c63b572b1f4b273c0c4fa23fbb3832e6b562 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:29:34 +0300 Subject: [PATCH 0766/1360] docs(http): clarify unresolved debug exports --- docs/dev/debt/libSceHttp.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 3df2c812ff..fce148fc5f 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -21,8 +21,8 @@ - [sceHttpSetAuthInfoCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - read from the disassembly of libSceHttp.sprx on firmware 13.60, not measured on a console and not compared with other emulators. The arguments are (out, src, pool, prepare, require), `prepare` before `require`, the reverse of `sceHttpUriParse`. A null source returns `ERROR_INVALID_URL`; without an output or a pool, a null `require` returns `ERROR_INVALID_VALUE`, otherwise the size is only reported. Only the scheme, hostname, path, username and password are copied, in that order, each with its terminator (a missing one becomes an empty string and takes one byte), and the port; the query, the fragment and the opaque flag stay 0. A pool that is too small clears the output and the pool and returns `ERROR_OUT_OF_MEMORY` (0x80431022, the console's out-of-size error) -- [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature -- [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the export is absent from the supplied native module; its signature remains unresolved. +- [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) returns constant success without consuming arguments in the supplied module; the callback contract remains unresolved. - [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. - [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. From ce4d3ab989a50240abb428578acea370e2381057 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:34:41 +0300 Subject: [PATCH 0767/1360] fix(audiopropagation): recover system options and cpu memory sizing --- .../AudioPropagation.hpp | 24 ++++++- .../prx/libSceAudioPropagation/Export.cpp | 39 +++++++++-- core/libs/tests/GuestAudioPropagation.cpp | 69 ++++++++++++++----- docs/dev/debt/libSceAudioPropagation.md | 4 +- 4 files changed, 108 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 63e23fb5e8..99db7ef6e3 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -27,6 +27,26 @@ inline constexpr std::size_t PortalParamsSize = 0x60; inline constexpr std::uint32_t PathEntryId = 0x010107d9; inline constexpr std::size_t PathEntrySize = 0x28; +inline constexpr std::uint32_t SystemOptionsId = 0x010107d5; +inline constexpr std::size_t SystemOptionsSize = 56; + +struct SystemOptions { + AudioPropagationStructDescriptor desc; + std::uint32_t maxSources; + std::uint32_t maxMaterials; + std::uint32_t parameter24; + std::uint32_t reflectionOrder; + std::uint32_t frameSamples; + float propagationSpeed; + float maxPathLength; + std::uint32_t flags; + std::uint8_t opaque[8]; +}; + +static_assert(sizeof(SystemOptions) == SystemOptionsSize); +static_assert(offsetof(SystemOptions, frameSamples) == 32); +static_assert(offsetof(SystemOptions, flags) == 44); + struct SourceInfo; struct SystemInfo; struct ReportRegistration; @@ -130,8 +150,8 @@ static_assert(offsetof(PortalParams, rooms) == 0x50); extern "C" { -std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const void* options, AudioPropagationSystemMemory* outMemory); -std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const void* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem); +std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const AudioPropagation::SystemOptions* options, AudioPropagationSystemMemory* outMemory); +std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const AudioPropagation::SystemOptions* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem); std::int32_t APS5_VABI sceAudioPropagationSystemDestroy(AudioPropagationHandle system); std::int32_t APS5_VABI sceAudioPropagationSystemSetAttributes(AudioPropagationHandle system, const AudioPropagation::Attribute* attributes, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSystemRegisterMaterial(AudioPropagationHandle system, const AudioPropagationStructDescriptor* material, AudioPropagationHandle* outMaterial); diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 27fca89aad..bf79473aa2 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/General.hpp" #include <cstdint> +#include <cmath> #include <cstring> #include <mutex> #include <stdexcept> @@ -115,19 +116,45 @@ void requireNoPropagation(bool none, const char* function) { extern "C" { -std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const void* options, AudioPropagationSystemMemory* outMemory) { - require(options != nullptr, __func__); - requireDescriptor(outMemory != nullptr ? &outMemory->desc : nullptr, SystemMemoryId, SystemMemorySize, __func__); - outMemory->size_cpu_mem = sizeof(SystemState); +std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const SystemOptions* options, AudioPropagationSystemMemory* outMemory) { + if (!options || !outMemory) return ErrorNullPointer; + if (outMemory->desc.id != SystemMemoryId || outMemory->desc.size != SystemMemorySize) return static_cast<std::int32_t>(0x8A700007); + outMemory->p_cpu_mem = nullptr; + outMemory->size_cpu_mem = 0; + outMemory->p_gpu_mem = nullptr; outMemory->size_gpu_mem = 0; + if (options->desc.id != SystemOptionsId || options->desc.size != SystemOptionsSize) return static_cast<std::int32_t>(0x8A700007); + if (options->maxSources == 0 || options->maxSources > 64 || options->maxMaterials == 0 || options->maxMaterials > 64 || options->parameter24 != 6 || options->reflectionOrder == 0 || options->reflectionOrder > 2) return ErrorInvalidArgument; + const auto frameSamples = options->frameSamples; + if (frameSamples != 256 && frameSamples != 512 && frameSamples != 1024 && frameSamples != 2048 && frameSamples != 4096) return ErrorInvalidArgument; + if (!(options->propagationSpeed > 0x1p-23f) || options->propagationSpeed > 1372.0f || !(options->maxPathLength > 0x1p-23f) || options->maxPathLength > 1372.0f || options->flags > 31 || ((options->flags & 1) && frameSamples != 512)) return ErrorInvalidArgument; + if (options->flags & 1) throw std::runtime_error(std::string(__func__) + ": GPU memory requirements depend on an unsupported native runtime query"); + const std::uint64_t sources = options->maxSources; + const std::uint64_t paths = sources * (options->reflectionOrder == 1 ? 6 : 36); + const float delaySeconds = options->maxPathLength / options->propagationSpeed; + const float delaySamples = delaySeconds * 48000.0f; + const auto delayCapacity = static_cast<std::uint64_t>(std::ceil(delaySamples)); + std::uint64_t bytes = sources * 1024 + paths * (32 * (options->reflectionOrder + 2) + 8928); + bytes += frameSamples == 256 ? 2048 : 4 * frameSamples; + bytes += 4 * delayCapacity * sources; + bytes += bytes & 15; + bytes += 160 * paths + 12352 + 4208 * options->maxMaterials; + if (frameSamples == 256) bytes += sources * ((options->flags & 2) ? 36896 : 16416); + if (options->flags & 4) bytes += 2000656; + if (options->flags & 8) bytes += 288 * paths; + if (options->flags & 16) bytes += 144 * paths; + outMemory->size_cpu_mem = bytes + 5368 - ((bytes & 15) ^ 8); return 0; } -std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const void* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem) { +std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const SystemOptions* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem) { require(options != nullptr && outSystem != nullptr, __func__); requireDescriptor(memory != nullptr ? &memory->desc : nullptr, SystemMemoryId, SystemMemorySize, __func__); + auto requiredMemory = *memory; + const auto result = sceAudioPropagationSystemQueryMemory(options, &requiredMemory); + if (result != 0) return result; const auto address = reinterpret_cast<std::uintptr_t>(memory->p_cpu_mem); - require(address != 0 && address % alignof(SystemState) == 0 && memory->size_cpu_mem >= sizeof(SystemState) && memory->size_gpu_mem == 0, __func__); + require(address != 0 && address % alignof(SystemState) == 0 && memory->size_cpu_mem >= requiredMemory.size_cpu_mem && memory->size_gpu_mem == 0, __func__); auto& state = registry(); std::lock_guard lock(state.mutex); require(!state.systems.contains(address), __func__); diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 071ab4f0ce..5e03aefa0f 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAudioPropagation/AudioPropagation.hpp" #include <cstdint> +#include <vector> #include <cstdio> #include <cstdlib> #include <cstring> @@ -50,38 +51,69 @@ AudioPropagation::Attribute Attribute(const void* value, std::size_t size) { } int main() { - const std::uint64_t options[7]{}; - alignas(16) std::uint8_t arena[64]{}; + const AudioPropagation::SystemOptions options{{AudioPropagation::SystemOptionsId, AudioPropagation::SystemOptionsSize}, 2, 2, 6, 1, 256, 343.0f, 343.0f, 0, {}}; auto memory = Memory(); - Require(sceAudioPropagationSystemQueryMemory(options, &memory) == 0, "query memory failed"); - Require(memory.size_cpu_mem != 0 && memory.size_cpu_mem <= sizeof(arena), "cpu size outside the arena"); + Require(sceAudioPropagationSystemQueryMemory(&options, &memory) == 0, "query memory failed"); + Require(memory.size_cpu_mem == 557264, "native CPU memory size mismatch"); + std::vector<std::uint64_t> arenaStorage((memory.size_cpu_mem + 7) / 8); + auto* arena = reinterpret_cast<std::uint8_t*>(arenaStorage.data()); Require(memory.size_gpu_mem == 0 && memory.p_cpu_mem == nullptr && memory.p_gpu_mem == nullptr, "query wrote more than the sizes"); - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemQueryMemory(nullptr, &memory); }, "null options"); - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemQueryMemory(options, nullptr); }, "null memory"); + Require(sceAudioPropagationSystemQueryMemory(nullptr, &memory) == AudioPropagation::ErrorNullPointer, "null options"); + Require(sceAudioPropagationSystemQueryMemory(&options, nullptr) == AudioPropagation::ErrorNullPointer, "null memory"); auto wrong = Memory(); wrong.desc.size = 0x28; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemQueryMemory(options, &wrong); }, "wrong descriptor size"); + Require(sceAudioPropagationSystemQueryMemory(&options, &wrong) == static_cast<std::int32_t>(0x8A700007), "wrong descriptor size"); wrong = Memory(); wrong.desc.id = AudioPropagation::RayId; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemQueryMemory(options, &wrong); }, "wrong descriptor id"); + Require(sceAudioPropagationSystemQueryMemory(&options, &wrong) == static_cast<std::int32_t>(0x8A700007), "wrong descriptor id"); + + auto probeOptions = options; + auto probeMemory = Memory(); + probeMemory.p_cpu_mem = arena; + probeMemory.p_gpu_mem = arena; + probeOptions.maxSources = 0; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "zero source capacity accepted"); + Require(probeMemory.p_cpu_mem == nullptr && probeMemory.p_gpu_mem == nullptr && probeMemory.size_cpu_mem == 0 && probeMemory.size_gpu_mem == 0, "invalid options did not clear memory output"); + probeOptions = options; + probeOptions.parameter24 = 7; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "unsupported parameter accepted"); + probeOptions = options; + probeOptions.frameSamples = 768; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "unsupported frame size accepted"); + probeOptions = options; + probeOptions.propagationSpeed = 0x1p-23f; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "invalid propagation speed accepted"); + probeOptions = options; + probeOptions.frameSamples = 512; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == 0 && probeMemory.size_cpu_mem == 524432, "512 sample memory size mismatch"); + probeOptions = options; + probeOptions.reflectionOrder = 2; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == 0 && probeMemory.size_cpu_mem == 1110608, "second order memory size mismatch"); + probeOptions = options; + probeOptions.flags = 30; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == 0 && probeMemory.size_cpu_mem == 2604064, "option memory sizes mismatch"); + probeOptions.flags = 1; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "GPU frame size constraint ignored"); + probeOptions.frameSamples = 512; + RequireThrows<std::runtime_error>([&] { sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory); }, "unknown GPU memory query returned a size"); AudioPropagationHandle system = 0; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "create without cpu memory"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "create without cpu memory"); memory.p_cpu_mem = arena + 1; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "misaligned cpu memory"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "misaligned cpu memory"); memory.p_cpu_mem = arena; memory.size_gpu_mem = 16; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "unrequested gpu memory"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "unrequested gpu memory"); memory.size_gpu_mem = 0; const auto required = memory.size_cpu_mem; memory.size_cpu_mem = required - 1; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "cpu memory too small"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "cpu memory too small"); memory.size_cpu_mem = required; - Require(sceAudioPropagationSystemCreate(options, &memory, &system) == 0, "create failed"); + Require(sceAudioPropagationSystemCreate(&options, &memory, &system) == 0, "create failed"); Require(system == reinterpret_cast<std::uintptr_t>(arena), "system handle is not its memory"); AudioPropagationHandle twice = 0; - RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(options, &memory, &twice); }, "second system in the same memory"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemCreate(&options, &memory, &twice); }, "second system in the same memory"); const std::uint64_t value = 1; const auto attribute = Attribute(&value, sizeof(value)); @@ -145,12 +177,13 @@ int main() { Require(sceAudioPropagationSourceSetAudioPaths(source, nullptr, 0) == 0, "empty audio paths failed"); RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceSetAudioPaths(source, rays, 1); }, "set audio paths"); - alignas(16) std::uint8_t otherArena[64]{}; + std::vector<std::uint64_t> otherArenaStorage(arenaStorage.size()); + auto* otherArena = reinterpret_cast<std::uint8_t*>(otherArenaStorage.data()); auto otherMemory = Memory(); - Require(sceAudioPropagationSystemQueryMemory(options, &otherMemory) == 0, "second query memory failed"); + Require(sceAudioPropagationSystemQueryMemory(&options, &otherMemory) == 0, "second query memory failed"); otherMemory.p_cpu_mem = otherArena; AudioPropagationHandle otherSystem = 0; - Require(sceAudioPropagationSystemCreate(options, &otherMemory, &otherSystem) == 0, "second system create failed"); + Require(sceAudioPropagationSystemCreate(&options, &otherMemory, &otherSystem) == 0, "second system create failed"); AudioPropagationHandle otherSource = 0; Require(sceAudioPropagationSourceCreate(otherSystem, &otherSource) == 0, "second system source create failed"); AudioPropagationHandle secondSource = 0; @@ -251,7 +284,7 @@ int main() { Require(sceAudioPropagationSystemUnregisterMaterial(lateMaterial) == 0, "orphaned material unregister is not a no-op"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceDestroy(system, lateSource); }, "orphan destroyed twice"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSystemDestroy(system); }, "destroy twice"); - Require(sceAudioPropagationSystemCreate(options, &memory, &system) == 0, "memory reuse after destroy failed"); + Require(sceAudioPropagationSystemCreate(&options, &memory, &system) == 0, "memory reuse after destroy failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "second destroy failed"); RequireThrows<std::runtime_error>([&] { sceAudioPropagationPathGetNumPoints(); }, "path get num points throw"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 578552a304..3d57114611 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,10 +2,10 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose no paths or rays, rendering preserves the destination mix without adding path contributions, and memory queries return only a 16-byte CPU header. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose no paths or rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options and frame configuration remain unknown. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes silence over the supplied capacity, without native accumulation or frame/count limits. Other formats and native validation precedence remain unimplemented. Source/system diagnostic fields still need reconstruction. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes silence over the supplied capacity, without native accumulation or frame/count limits. Other formats and native validation precedence remain unimplemented. Source/system diagnostic fields still need reconstruction. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From 05380f7402242caa2492c438833a2cb4f0e672e9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:35:43 +0300 Subject: [PATCH 0768/1360] fix(audiopropagation): enforce configured render frame limits --- .../prx/libSceAudioPropagation/Export.cpp | 10 ++++++--- core/libs/tests/GuestAudioPropagation.cpp | 21 ++++++++++++------- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index bf79473aa2..fb835aca4d 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -23,7 +23,8 @@ constexpr std::uint64_t SystemMagic = [] { struct SystemState { std::uint64_t magic; - std::uint64_t reserved; + std::uint32_t frameSamples; + std::uint32_t maxSources; }; enum class Kind { Room, Portal, Source, Material }; @@ -158,7 +159,7 @@ std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const SystemOptions* opti auto& state = registry(); std::lock_guard lock(state.mutex); require(!state.systems.contains(address), __func__); - *static_cast<SystemState*>(memory->p_cpu_mem) = SystemState{SystemMagic, 0}; + *static_cast<SystemState*>(memory->p_cpu_mem) = SystemState{SystemMagic, options->frameSamples, options->maxSources}; state.systems.insert(address); *outSystem = address; return 0; @@ -333,11 +334,14 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy auto& state = registry(); std::lock_guard lock(state.mutex); requireSystem(state, system, __func__); + const auto& configuration = *reinterpret_cast<const SystemState*>(system); + require(count <= configuration.maxSources, __func__); + const auto requiredBytes = static_cast<std::size_t>(configuration.frameSamples) * 16 * sizeof(float); for (std::uint32_t index = 0; index < count; ++index) { const auto& info = infos[index]; requireDescriptor(&info.desc, RenderInfoId, RenderInfoSize, __func__); require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); - require(info.output != nullptr && info.outputSize != 0, __func__); + require(info.output != nullptr && info.outputSize >= requiredBytes, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); } for (std::uint32_t index = 0; index < count; ++index) { diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 5e03aefa0f..0f060fd0a5 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -189,11 +189,12 @@ int main() { AudioPropagationHandle secondSource = 0; Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); - float outputs[2][64]; - float expected[2][64]; + constexpr std::size_t frameValues = 256 * 16; + float outputs[2][frameValues + 16]; + float expected[2][frameValues + 16]; const auto fill = [&] { for (std::size_t channel = 0; channel < 2; ++channel) { - for (std::size_t sample = 0; sample < 64; ++sample) { + for (std::size_t sample = 0; sample < frameValues + 16; ++sample) { outputs[channel][sample] = static_cast<float>(sample + 1) / (channel == 0 ? 128.0f : -128.0f); } } @@ -211,10 +212,13 @@ int main() { Require(untouched(), "empty source erased an existing mix"); infos[0].channelCount = 99; Require(sceAudioPropagationSourceRender(system, infos, 1) == 0 && infos[0].channelCount == 0, "fixed render format clears the channel count"); - const auto* tail = reinterpret_cast<const std::uint8_t*>(outputs[1]); - Require(tail[0] == 0xab, "render wrote past the output size"); + Require(untouched(), "render wrote past the output size"); reset(); - infos[0].outputSize = sizeof(float) * 16; + infos[0].outputSize = sizeof(float) * (frameValues - 1); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "short frame output accepted"); + Require(untouched(), "short output modified a render buffer"); + reset(); + infos[0].outputSize = sizeof(float) * frameValues; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); Require(untouched(), "empty sources modified separate destination mixes or their tails"); reset(); @@ -222,9 +226,9 @@ int main() { Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into a shared mix failed"); Require(untouched(), "empty sources modified a shared destination mix"); reset(); - infos[0].outputSize = sizeof(float) * 32; + infos[0].outputSize = sizeof(float) * frameValues; infos[1].output = outputs[0] + 16; - infos[1].outputSize = sizeof(float) * 32; + infos[1].outputSize = sizeof(float) * frameValues; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into overlapping mixes failed"); Require(untouched(), "empty sources modified overlapping destination mixes"); reset(); @@ -235,6 +239,7 @@ int main() { reset(); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 0); }, "render without infos"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 3); }, "render exceeds source capacity"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, nullptr, 1); }, "render with null infos"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system + 16, infos, 1); }, "render on an unknown system"); infos[1].desc.id = AudioPropagation::RayId; diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 3d57114611..501b37adab 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes silence over the supplied capacity, without native accumulation or frame/count limits. Other formats and native validation precedence remain unimplemented. Source/system diagnostic fields still need reconstruction. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. Source/system diagnostic fields still need reconstruction. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. From 6f70da54125088b77a13dcd90f8496aaceea4e05 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:36:38 +0300 Subject: [PATCH 0769/1360] fix(hmd): restore default led output signature --- core/libs/prx/libSceHmd/Export.cpp | 2 +- docs/dev/debt/libSceHmd.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 6f28a18447..3ede810dfe 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -542,7 +542,7 @@ int APS5_VABI sceHmdReprojectionStartCapture(void) { return 0; } -int APS5_VABI sceHmdInternalGetDefaultLedData(void) { +int APS5_VABI sceHmdInternalGetDefaultLedData(std::int32_t handle, std::uint32_t* mask, HmdLedData* data) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 76f7c2767d..3af366507e 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -9,8 +9,8 @@ - [libSceHmd internal and capture exports](../../../core/libs/prx/libSceHmd/Export.cpp) - input/output directions and argument registers are recovered for shared VR2D data, device binding, optical and serial queries, screen status and capture operations. VR2D records are 128 bytes, optical output 16, serial output 16, PU position output 20, crash-dump output 68, audio/video status 32, and reprojection debug outputs 216 or 1136. Payload field meanings and capture callback contracts remain incomplete; these structures use forward declarations. - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count -- [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature -- [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types remain unresolved. +- [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) takes an unused handle, a 32-bit mask output and a nine-byte LED output. Output sizes match the paired setter; mask bits and LED payload semantics remain unresolved. The protected body is unchanged. [Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. From 597d17ecb21f837151e2878df1b3001243dd7783 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:36:59 +0300 Subject: [PATCH 0770/1360] docs(net): record verified thread exit contract --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 8c3e26db4d..f10d1eb873 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -17,7 +17,7 @@ - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. -- [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp): six arguments and the three-argument address callback are confirmed. The 24-byte parameter contains five network-order addresses and a client limit. Individual address roles and the two unused thread-option meanings remain unresolved. - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp): seven arguments are confirmed, including the parameter pointer on the stack. The 272-byte parameter contains two DNS addresses, two 128-byte credential arrays, MTU and flags. Callback arguments are user data, event and error; the two unused thread-option meanings remain unresolved. From e2a2427c69907c2226307f865ae87d1f01489f54 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:41:32 +0300 Subject: [PATCH 0771/1360] fix(ult): remove nonexistent creation version arguments --- core/libs/prx/libSceUlt/Export.cpp | 28 ++++++++-------- core/libs/tests/GuestUltThread.cpp | 52 +++++++++++++++--------------- docs/dev/debt/libSceUlt.md | 3 +- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index f449f06dba..8cae0b365c 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -876,40 +876,40 @@ int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { return ULT_OK; } -int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam, std::uint32_t buildVersion) { - return sceUltWaitingQueueResourcePoolCreate(pool, name, numThreads, numSyncObjects, workArea, optParam, buildVersion); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam) { + return sceUltWaitingQueueResourcePoolCreate(pool, name, numThreads, numSyncObjects, workArea, optParam, 0); } -int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { - return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, buildVersion); +int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam) { + return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, 0); } int APS5_VABI _sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uint32_t buildVersion) { return sceUltMutexOptParamInitialize(optParam, buildVersion); } -int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { - return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, buildVersion); +int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam) { + return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, 0); } -int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { - return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, buildVersion); +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam) { + return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, 0); } -int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { - return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, buildVersion); +int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam) { + return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, 0); } int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam* optParam, std::uint32_t buildVersion) { return sceUltUlthreadRuntimeOptParamInitialize(optParam, buildVersion); } -int APS5_VABI _sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam, std::uint32_t buildVersion) { - return sceUltUlthreadRuntimeCreate(runtime, name, maxNumUlthread, numWorkerThread, workArea, optParam, buildVersion); +int APS5_VABI _sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam) { + return sceUltUlthreadRuntimeCreate(runtime, name, maxNumUlthread, numWorkerThread, workArea, optParam, 0); } -int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthreadEntry entry, std::uint64_t arg, void* context, std::uint64_t sizeContext, void* runtime, const void* optParam, std::uint32_t buildVersion) { - return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); +int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthreadEntry entry, std::uint64_t arg, void* context, std::uint64_t sizeContext, void* runtime, const void* optParam) { + return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, 0); } int APS5_VABI sceUltConditionVariableSignalAll(void* conditionVariable) { diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 9f4a146251..b2d3cec977 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -21,18 +21,18 @@ int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(void*, std::uint32_t); -int APS5_VABI _sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::int32_t, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); +int APS5_VABI _sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); +int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::int32_t, void*, const void*); int APS5_VABI sceUltSemaphoreAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreRelease(void*, std::int32_t); int APS5_VABI _sceUltMutexOptParamInitialize(void*, std::uint32_t); -int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*); int APS5_VABI sceUltMutexLock(void*); int APS5_VABI sceUltMutexUnlock(void*); -int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*); +int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*); int APS5_VABI sceUltQueuePush(void*, const void*); int APS5_VABI sceUltQueuePop(void*, void*); int APS5_VABI sceUltMutexDestroy(void*); @@ -132,8 +132,8 @@ static void TryJoin() { std::memset(optParam, 0xff, sizeof(optParam)); CHECK(_sceUltUlthreadRuntimeOptParamInitialize(nullptr, 0) == Null); CHECK(_sceUltUlthreadRuntimeOptParamInitialize(optParam, 0) == Ok && optParam[0] == 0); - CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltUlthreadCreate(&thread, "parked", Parked, 0x3C, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltUlthreadCreate(&thread, "parked", Parked, 0x3C, nullptr, 0, &runtime, nullptr) == Ok); while (!gStarted.load(std::memory_order_acquire)) std::this_thread::yield(); std::int32_t status = 0; @@ -155,16 +155,16 @@ static void Destroy() { Thread thread; CHECK(sceUltUlthreadRuntimeDestroy(nullptr) == Null); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); - CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltUlthreadRuntimeCreate(&other, "other", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0x11, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&other, "other", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0x11, nullptr, 0, &runtime, nullptr) == Ok); CHECK(sceUltUlthreadRuntimeDestroy(&other) == Ok); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); std::int32_t status = 0; CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x11); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); - CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr, 0) == State); + CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr) == State); CHECK(sceUltFinalize() == Ok); } @@ -172,13 +172,13 @@ static void UnderscoredCreate() { alignas(8) static std::uint8_t pool[256]; alignas(8) static std::uint8_t semaphore[256]; alignas(8) static std::uint8_t unknownPool[256]; - CHECK(_sceUltWaitingQueueResourcePoolCreate(nullptr, "pool", 1, 1, nullptr, nullptr, 0) == Null); - CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltSemaphoreCreate(nullptr, "semaphore", 1, pool, nullptr, 0) == Null); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", -1, pool, nullptr, 0) != Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, unknownPool, nullptr, 0) != Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr, 0) == Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr, 0) == State); + CHECK(_sceUltWaitingQueueResourcePoolCreate(nullptr, "pool", 1, 1, nullptr, nullptr) == Null); + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltSemaphoreCreate(nullptr, "semaphore", 1, pool, nullptr) == Null); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", -1, pool, nullptr) != Ok); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, unknownPool, nullptr) != Ok); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr) == Ok); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr) == State); CHECK(sceUltSemaphoreAcquire(semaphore, 2) == Ok); CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); alignas(8) static std::uint8_t mutexOptParam[128]; @@ -187,16 +187,16 @@ static void UnderscoredCreate() { CHECK(_sceUltMutexOptParamInitialize(nullptr, 0) == Null); CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0) == Ok); CHECK(mutexOptParam[8] == 0 && mutexOptParam[15] == 0); - CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam, 0) == Null); - CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam, 0) == Ok); + CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Ok); CHECK(sceUltMutexLock(mutex) == Ok); CHECK(sceUltMutexUnlock(mutex) == Ok); alignas(8) static std::uint8_t dataPool[512]; alignas(8) static std::uint8_t queue[512]; - CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Null); - CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr, 0) == Null); - CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, nullptr, 0) == Ok); + CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr) == Null); + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr) == Ok); + CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr) == Null); + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, nullptr) == Ok); std::uint64_t value = 0x1122334455667788ull, output = 0; CHECK(sceUltQueuePush(queue, &value) == Ok); CHECK(sceUltQueuePop(queue, &output) == Ok && output == value); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 69ec383a4a..e20ac05764 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -4,10 +4,9 @@ - [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. -- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. +- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified -- [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. The nested context in `UltUlthreadInfo` still requires reconstruction. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here From 0a603c963401038cfacba9d1acf8e57014f1af3e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:42:28 +0300 Subject: [PATCH 0772/1360] fix(ult): restore resource pool work area formulas --- core/libs/prx/libSceUlt/Export.cpp | 13 +++++-------- core/libs/tests/GuestUltQueue.cpp | 7 ++++++- docs/dev/debt/libSceUlt.md | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 8cae0b365c..3e77855736 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -443,8 +443,8 @@ int APS5_VABI sceUltConditionVariableWait(void* condition) { return ULT_OK; } -int APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t numThreads, std::uint32_t numSyncObjects) { - return static_cast<int>(alignUp(static_cast<std::uint64_t>(numThreads + numSyncObjects) * 256u, 8u)); +std::uint64_t APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t numThreads, std::uint32_t numSyncObjects) { + return 32ull * (numSyncObjects + 2u * numThreads + 2u); } int APS5_VABI sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam, std::uint32_t buildVersion) { @@ -501,14 +501,11 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { } std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject) { - if (dataSize > UINT64_MAX - 7u) throw std::out_of_range("ULT queue data size overflow"); - const auto stride = alignUp(dataSize, 8u); - const auto queueArea = static_cast<std::uint64_t>(numQueueObject) * 512u; - if (stride != 0 && numData > (UINT64_MAX - queueArea) / stride) { + const auto nodeArea = 32ull * (numQueueObject + 2u * numData + 3u); + if (dataSize != 0 && numData > (UINT64_MAX - nodeArea) / dataSize) { throw std::out_of_range("ULT queue work area size overflow"); } - const std::uint64_t dataArea = static_cast<std::uint64_t>(numData) * stride; - return alignUp(dataArea + queueArea, 8u); + return static_cast<std::uint64_t>(numData) * dataSize + nodeArea; } int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { diff --git a/core/libs/tests/GuestUltQueue.cpp b/core/libs/tests/GuestUltQueue.cpp index 2a93f78c0c..91fba11ef9 100644 --- a/core/libs/tests/GuestUltQueue.cpp +++ b/core/libs/tests/GuestUltQueue.cpp @@ -13,6 +13,7 @@ extern "C" { int APS5_VABI sceUltInitialize(); int APS5_VABI sceUltFinalize(); +std::uint64_t APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t, std::uint32_t); std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t, std::uint64_t, std::uint32_t); int APS5_VABI sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*, std::uint32_t); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); @@ -93,7 +94,11 @@ static void Validation() { CHECK(sceUltQueueDataResourcePoolDestroy(&fixture.pool) == Busy); CHECK(sceUltQueueDataResourcePoolDestroy(nullptr) == Null); CHECK(sceUltQueueDestroy(nullptr) == Null); - CHECK(sceUltQueueDataResourcePoolGetWorkAreaSize(3, 9, 2) == 1072); + CHECK(sceUltQueueDataResourcePoolGetWorkAreaSize(3, 9, 2) == 379); + CHECK(sceUltQueueDataResourcePoolGetWorkAreaSize(0, 0, 0) == 96); + CHECK(sceUltWaitingQueueResourcePoolGetWorkAreaSize(0, 0) == 64); + CHECK(sceUltWaitingQueueResourcePoolGetWorkAreaSize(3, 2) == 320); + CHECK(sceUltWaitingQueueResourcePoolGetWorkAreaSize(0, 0x08000000u) == 0x100000040ull); bool overflow = false; try { sceUltQueueDataResourcePoolGetWorkAreaSize(1, UINT64_MAX, 1); } catch (const std::out_of_range&) { overflow = true; } diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index e20ac05764..3998c27c93 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -12,7 +12,7 @@ - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional -- [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`. +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. - [sceUltMutexDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) returns `ULT_ERROR_BUSY` when the mutex is held or has waiting threads, tracked via owner and waiter counts in `UltMutexState`. Prosper (`hle_ult.cpp`) logs a destroy of a held mutex and returns `ULT_OK`; AnyPS5 returns `ULT_ERROR_BUSY` to prevent destroying active mutexes. From ab8265170090c9a4773d277b7167205b34edc5d0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:44:19 +0300 Subject: [PATCH 0773/1360] fix(ult): restore mutex option layout and validation --- core/libs/SceTypes.hpp | 10 +++++++--- core/libs/prx/libSceUlt/Export.cpp | 18 +++++++++++++++++- core/libs/tests/GuestUltThread.cpp | 25 +++++++++++++++++++++++-- docs/dev/debt/libSceUlt.md | 3 ++- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index b0d1fd063f..38b336e42c 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -2117,12 +2117,16 @@ struct UserServiceGamePresets { std::uint32_t audio_language; }; -struct UltMutexOptParam { - std::uint32_t reserved_header[2]; +struct alignas(8) UltMutexOptParam { + std::uint32_t header[3]; + std::uint32_t buildVersion; std::uint32_t attribute; - std::uint32_t reserved0; + std::uint32_t reserved[27]; }; +static_assert(sizeof(UltMutexOptParam) == 128); +static_assert(offsetof(UltMutexOptParam, attribute) == 16); + struct UltUlthreadRuntimeOptParam { std::uint8_t bytes[128]; }; using UltUlthreadEntry = std::int32_t (APS5_VABI *)(std::uint64_t); diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 3e77855736..b200b94561 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -4,6 +4,7 @@ #include <cstddef> #include <cstdint> #include <cstring> +#include <iterator> #include <limits> #include <memory> #include <mutex> @@ -188,11 +189,17 @@ int APS5_VABI sceUltFinalize() { } int APS5_VABI sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uint32_t buildVersion) { - (void)buildVersion; if (optParam == nullptr) { return ULT_ERROR_NULL; } + if ((reinterpret_cast<std::uintptr_t>(optParam) & 7u) != 0) { + return ULT_ERROR_ALIGNMENT; + } std::memset(optParam, 0, sizeof(*optParam)); + optParam->header[0] = 0x6C696275; + optParam->header[1] = 0x6C746D61; + optParam->header[2] = 0x67696300; + optParam->buildVersion = buildVersion; return ULT_OK; } @@ -886,6 +893,15 @@ int APS5_VABI _sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::ui } int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam) { + if (mutex == nullptr || name == nullptr || waitingQueueResourcePool == nullptr) { + return ULT_ERROR_NULL; + } + if (((reinterpret_cast<std::uintptr_t>(mutex) | reinterpret_cast<std::uintptr_t>(optParam) | reinterpret_cast<std::uintptr_t>(waitingQueueResourcePool)) & 7u) != 0) { + return ULT_ERROR_ALIGNMENT; + } + if (optParam != nullptr && (optParam->header[0] != 0x6C696275 || optParam->header[1] != 0x6C746D61 || optParam->header[2] != 0x67696300 || optParam->attribute > 1 || std::any_of(std::begin(optParam->reserved), std::end(optParam->reserved), [](std::uint32_t value) { return value != 0; }))) { + return ULT_ERROR_INVALID; + } return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, 0); } diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index b2d3cec977..ffa71f1597 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -47,6 +47,8 @@ int APS5_VABI sceUltGetConditionVariableInfo(void*, UltConditionVariableInfo*, U static constexpr int Ok = 0; static constexpr int Null = -2139029503; +static constexpr int Alignment = -2139029502; +static constexpr int Invalid = -2139029500; static constexpr int State = -2139029498; static constexpr int Busy = -2139029497; static constexpr int Again = -2139029496; @@ -181,12 +183,31 @@ static void UnderscoredCreate() { CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr) == State); CHECK(sceUltSemaphoreAcquire(semaphore, 2) == Ok); CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); - alignas(8) static std::uint8_t mutexOptParam[128]; + alignas(8) static std::uint8_t mutexOptParam[136]; alignas(8) static std::uint8_t mutex[256]; std::memset(mutexOptParam, 0xFF, sizeof(mutexOptParam)); CHECK(_sceUltMutexOptParamInitialize(nullptr, 0) == Null); + CHECK(_sceUltMutexOptParamInitialize(mutexOptParam + 1, 0) == Alignment); + CHECK(mutexOptParam[0] == 0xFF && mutexOptParam[127] == 0xFF); + CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0x12345678) == Ok); + UltMutexOptParam options{}; + std::memcpy(&options, mutexOptParam, sizeof(options)); + CHECK(options.header[0] == 0x6C696275 && options.header[1] == 0x6C746D61 && options.header[2] == 0x67696300); + CHECK(options.buildVersion == 0x12345678 && options.attribute == 0); + for (std::size_t index = 16; index < 128; ++index) CHECK(mutexOptParam[index] == 0); + for (std::size_t index = 128; index < sizeof(mutexOptParam); ++index) CHECK(mutexOptParam[index] == 0xFF); + CHECK(_sceUltMutexCreate(mutex, nullptr, pool, mutexOptParam) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", nullptr, mutexOptParam) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam + 1) == Alignment); + mutexOptParam[127] = 1; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Invalid); + mutexOptParam[127] = 0; + mutexOptParam[16] = 2; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Invalid); + mutexOptParam[16] = 0; + mutexOptParam[0] = 0; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Invalid); CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0) == Ok); - CHECK(mutexOptParam[8] == 0 && mutexOptParam[15] == 0); CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam) == Null); CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Ok); CHECK(sceUltMutexLock(mutex) == Ok); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 3998c27c93..7ec281e84d 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -4,7 +4,7 @@ - [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. -- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. +- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. The nested context in `UltUlthreadInfo` still requires reconstruction. @@ -14,6 +14,7 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. +- [sceUltMutexLock](../../../core/libs/prx/libSceUlt/Export.cpp) permits recursive acquisition regardless of the recovered option attribute; native recursion limits are not enforced. - [sceUltMutexDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) returns `ULT_ERROR_BUSY` when the mutex is held or has waiting threads, tracked via owner and waiter counts in `UltMutexState`. Prosper (`hle_ult.cpp`) logs a destroy of a held mutex and returns `ULT_OK`; AnyPS5 returns `ULT_ERROR_BUSY` to prevent destroying active mutexes. - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). From 4c3c1345d7a08d0bf5ed0b5036598ff952066bdc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:45:12 +0300 Subject: [PATCH 0774/1360] fix(ult): restore runtime work area sizing --- core/libs/prx/libSceUlt/Export.cpp | 9 +++++---- core/libs/tests/GuestUltThread.cpp | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index b200b94561..6c21c1712d 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -35,9 +35,6 @@ std::unordered_map<void*, std::shared_ptr<UltQueueState>> gQueues; std::unordered_map<void*, UltRuntimeState> gRuntimes; std::unordered_map<void*, std::shared_ptr<UltUlthreadState>> gUlthreads; -std::uint64_t alignUp(std::uint64_t value, std::uint64_t alignment) { - return (value + alignment - 1u) & ~(alignment - 1u); -} int semaphoreGetState(void* semaphore, std::shared_ptr<UltSemaphoreState>* out) { std::lock_guard<std::mutex> lock(gMutex); @@ -743,7 +740,11 @@ int APS5_VABI sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam } std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread) { - return alignUp(static_cast<std::uint64_t>(maxNumUlthread) * 256u + static_cast<std::uint64_t>(numWorkerThread) * 16u * 1024u, 8u); + const std::uint32_t totalThreads = maxNumUlthread + numWorkerThread; + const auto threadNodes = 32ull * (2u * maxNumUlthread + 4u); + const auto threadArea = (threadNodes + 4ull * maxNumUlthread + 7u) & 0x3FFFFFFFF8ull; + const auto schedulerArea = 32ull * (2u * numWorkerThread + 4u) + 32ull * (2u * totalThreads + 4u); + return threadArea + schedulerArea + 512ull * numWorkerThread + 8ull * (static_cast<std::uint64_t>(maxNumUlthread) + totalThreads); } int APS5_VABI sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam, std::uint32_t buildVersion) { diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index ffa71f1597..7aa43d4dbd 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -115,7 +115,7 @@ static void Validation() { std::int32_t status = 0; CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); CHECK(sceUltFinalize() == Ok); - CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 1184); CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); CHECK(sceUltReaderWriterLockDestroy(nullptr) == ULT_ERROR_NULL); bool threwInfo = false; @@ -128,6 +128,10 @@ static void Validation() { } static void TryJoin() { + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(0, 0) == 384); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 1184); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(3, 2) == 2128); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(0x80000000u, 0) == 0xA00000180ull); Runtime runtime; Thread thread; std::uint8_t optParam[128]; From 574cf2943ae609d1db6956c6ece3c68ae3afe51f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:46:00 +0300 Subject: [PATCH 0775/1360] fix(ult): enforce mutex recursion and unlock ownership --- core/libs/prx/libSceUlt/Export.cpp | 8 +++++++- core/libs/prx/libSceUlt/SceUltTypes.hpp | 1 + core/libs/tests/GuestUltThread.cpp | 13 +++++++++++++ docs/dev/debt/libSceUlt.md | 3 --- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 6c21c1712d..ba2978be02 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -281,6 +281,9 @@ int APS5_VABI sceUltMutexLock(void* mutex) { return ULT_ERROR_STATE; } if (state->_owner == self) { + if (state->_attribute == 0) { + return ULT_ERROR_STATE; + } ++state->_lockCount; return ULT_OK; } @@ -341,9 +344,12 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { } const auto self = std::this_thread::get_id(); std::lock_guard<std::mutex> lock(state->_mutex); - if (!state->_alive || state->_owner != self) { + if (!state->_alive) { return ULT_ERROR_STATE; } + if (state->_owner != self) { + return ULT_ERROR_PERMISSION; + } --state->_lockCount; if (state->_lockCount == 0) { state->_owner = {}; diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index e02d67f2b5..35fa6b302c 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -19,6 +19,7 @@ constexpr int ULT_ERROR_NULL = -2139029503; constexpr int ULT_ERROR_ALIGNMENT = -2139029502; constexpr int ULT_ERROR_RANGE = -2139029501; constexpr int ULT_ERROR_INVALID = -2139029500; +constexpr int ULT_ERROR_PERMISSION = -2139029499; constexpr int ULT_ERROR_STATE = -2139029498; constexpr int ULT_ERROR_BUSY = -2139029497; constexpr int ULT_ERROR_AGAIN = -2139029496; diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 7aa43d4dbd..0057ef4d0e 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -49,6 +49,7 @@ static constexpr int Ok = 0; static constexpr int Null = -2139029503; static constexpr int Alignment = -2139029502; static constexpr int Invalid = -2139029500; +static constexpr int Permission = -2139029499; static constexpr int State = -2139029498; static constexpr int Busy = -2139029497; static constexpr int Again = -2139029496; @@ -215,6 +216,18 @@ static void UnderscoredCreate() { CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam) == Null); CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Ok); CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexLock(mutex) == State); + std::thread foreignUnlock([&] { CHECK(sceUltMutexUnlock(mutex) == Permission); }); + foreignUnlock.join(); + CHECK(sceUltMutexUnlock(mutex) == Ok); + CHECK(sceUltMutexUnlock(mutex) == Permission); + CHECK(sceUltMutexDestroy(mutex) == Ok); + mutexOptParam[16] = 1; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Ok); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexUnlock(mutex) == Ok); + CHECK(sceUltMutexDestroy(mutex) == Busy); CHECK(sceUltMutexUnlock(mutex) == Ok); alignas(8) static std::uint8_t dataPool[512]; alignas(8) static std::uint8_t queue[512]; diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 7ec281e84d..3e8b94f62a 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -14,7 +14,4 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. -- [sceUltMutexLock](../../../core/libs/prx/libSceUlt/Export.cpp) permits recursive acquisition regardless of the recovered option attribute; native recursion limits are not enforced. -- [sceUltMutexDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) returns `ULT_ERROR_BUSY` when the mutex is held or has waiting threads, tracked via owner and waiter counts in `UltMutexState`. Prosper (`hle_ult.cpp`) logs a destroy of a held mutex and returns `ULT_OK`; AnyPS5 returns `ULT_ERROR_BUSY` to prevent destroying active mutexes. - - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). From a41a9615815782968d19afce1af4c018e8e2b664 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:49:36 +0300 Subject: [PATCH 0776/1360] fix(dialog): restore g2p and player review parameter contracts --- core/libs/prx/libSceCdlgG2P/Export.cpp | 55 ++++++++++--------- .../prx/libSceCdlgPlayerReview/Export.cpp | 18 +++--- .../prx/libSceCommonDialog/CommonDialog.hpp | 42 ++++++++++++++ core/libs/tests/GuestG2PDialog.cpp | 34 ++++++++++-- core/libs/tests/GuestPlayerReviewDialog.cpp | 46 ++++++++++++---- docs/dev/debt/libSceCdlgG2P.md | 2 +- docs/dev/debt/libSceCdlgPlayerReview.md | 2 +- 7 files changed, 147 insertions(+), 52 deletions(-) diff --git a/core/libs/prx/libSceCdlgG2P/Export.cpp b/core/libs/prx/libSceCdlgG2P/Export.cpp index 082b4bb331..8a50061006 100644 --- a/core/libs/prx/libSceCdlgG2P/Export.cpp +++ b/core/libs/prx/libSceCdlgG2P/Export.cpp @@ -2,7 +2,8 @@ #include <cstdint> #include <cstddef> #include <cstring> -#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -12,7 +13,7 @@ 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<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); @@ -25,47 +26,47 @@ std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; extern "C" { int APS5_VABI sceG2PDialogInitialize(void) { - int expected = COMMON_DIALOG_STATUS_NONE; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; - return 0; + int 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 sceG2PDialogOpen(const void* param) { - const int status = g_status.load(); - if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; +int APS5_VABI sceG2PDialogOpen(const G2PDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + 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 (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceG2PDialogUpdateStatus(void) { - return g_status.load(); + return g_status.load(); } int APS5_VABI sceG2PDialogGetStatus(void) { - return g_status.load(); + return g_status.load(); } -int APS5_VABI sceG2PDialogGetResult(void* result) { - const int status = g_status.load(); - if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &canceled, sizeof(canceled)); - return 0; +int APS5_VABI sceG2PDialogGetResult(CommonDialogResultPrefix* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return 0; } int APS5_VABI sceG2PDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceG2PDialogTerminate(void) { - if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - return 0; + 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/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index 2e454c8741..d63a6ad02b 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -1,6 +1,7 @@ #include <atomic> #include <cstdint> -#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -10,8 +11,8 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); @@ -35,11 +36,12 @@ int APS5_VABI scePlayerReviewDialogTerminate(void) { return COMMON_DIALOG_ERROR_NOT_RUNNING; } -int APS5_VABI scePlayerReviewDialogOpen(const void* param) { +int APS5_VABI scePlayerReviewDialogOpen(const PlayerReviewDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; 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; + if (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; }) || param->mode > 1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -58,13 +60,13 @@ int APS5_VABI scePlayerReviewDialogUpdateStatus(void) { return g_status.load(); } -int APS5_VABI scePlayerReviewDialogGetResult(void* result) { +int APS5_VABI scePlayerReviewDialogGetResult(CommonDialogResultPrefix* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; 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; - *static_cast<std::int32_t*>(result) = COMMON_DIALOG_RESULT_USER_CANCELED; - return 0; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } } diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index c9eb03df55..b4d577c0db 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -1,6 +1,48 @@ #ifndef CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP #define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iterator> + +struct CommonDialogBaseParam { + std::uint64_t size; + std::uint8_t reserved[36]; + std::uint32_t magic; +}; + +struct G2PDialogParam { + CommonDialogBaseParam base; + std::int32_t userId; + std::uint32_t padding; + std::uint64_t size; + std::uint8_t reserved[64]; +}; + +struct PlayerReviewDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::int32_t userId; + std::uint8_t target[40]; + std::uint32_t mode; + std::uint8_t reserved[64]; +}; + +struct CommonDialogResultPrefix { + std::uint8_t untouched[8]; + std::int32_t result; +}; + +static_assert(sizeof(CommonDialogBaseParam) == 48); +static_assert(sizeof(G2PDialogParam) == 128); +static_assert(sizeof(PlayerReviewDialogParam) == 168); +static_assert(offsetof(CommonDialogResultPrefix, result) == 8); + +inline bool ValidateCommonDialogBaseParam(const CommonDialogBaseParam& param) { + return param.size == sizeof(param) && param.magic == static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u && std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; }); +} + constexpr int COMMON_DIALOG_OK = 0; constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004); diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index b3e0cb2b85..0afa1a12e7 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include <cstdlib> extern "C" { @@ -28,13 +29,19 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + G2PDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = 1; + std::int32_t result[4] = {-1, -1, -1, -1}; Require(sceG2PDialogTerminate() == kErrNotInitialized); Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); Require(sceG2PDialogClose() == kErrNotInitialized); Require(sceG2PDialogGetResult(result) == kErrNotInitialized); + Require(sceG2PDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); + Require(sceG2PDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); Require(sceG2PDialogGetStatus() == kStatusNone); Require(sceG2PDialogUpdateStatus() == kStatusNone); @@ -46,13 +53,29 @@ int main() { Require(sceG2PDialogOpen(nullptr) == kErrArgNull); Require(sceG2PDialogGetResult(result) == kErrNotFinished); + param.base.size = 0; + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.base.size = sizeof(param.base); + param.base.magic = 0; + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = 0; + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.size = sizeof(param); + param.reserved[63] = 1; + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.reserved[63] = 0; + param.userId = -1; + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.userId = 1; Require(sceG2PDialogOpen(¶m) == 0); Require(sceG2PDialogGetStatus() == kStatusFinished); Require(sceG2PDialogUpdateStatus() == kStatusFinished); Require(sceG2PDialogGetResult(nullptr) == kErrArgNull); Require(sceG2PDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == kResultUserCanceled); Require(result[1] == -1); Require(sceG2PDialogClose() == 0); @@ -64,8 +87,9 @@ int main() { Require(sceG2PDialogGetStatus() == kStatusInitialized); Require(sceG2PDialogClose() == 0); Require(sceG2PDialogGetStatus() == kStatusFinished); - result[0] = -1; + result[2] = -1; Require(sceG2PDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == kResultUserCanceled); Require(sceG2PDialogTerminate() == 0); } diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index c7917268a4..4c97608a99 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include <cstdlib> extern "C" { @@ -18,7 +19,6 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); @@ -28,11 +28,17 @@ void Require(bool value) { if (!value) std::abort(); } } int main() { - std::uint8_t param[64] = {}; - std::int32_t result[8] = {}; + PlayerReviewDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = 1; + std::int32_t result[4] = {-1, -1, -1, -1}; Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); + Require(scePlayerReviewDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); - Require(scePlayerReviewDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(¶m) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); @@ -48,12 +54,31 @@ int main() { Require(scePlayerReviewDialogInitialize() == 0); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogOpen(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); - Require(scePlayerReviewDialogOpen(param) == 0); + param.base.size = 0; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.base.size = sizeof(param.base); + param.base.magic = 0; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = 0; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.size = sizeof(param); + param.reserved[63] = 1; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.reserved[63] = 0; + param.userId = -1; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.userId = 1; + param.mode = 2; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.mode = 0; + Require(scePlayerReviewDialogOpen(¶m) == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); - Require(scePlayerReviewDialogGetResult(result) == 0); - Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogClose() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); @@ -64,9 +89,10 @@ int main() { Require(scePlayerReviewDialogClose() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); - result[0] = -1; - Require(scePlayerReviewDialogGetResult(result) == 0); - Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); + result[2] = -1; + Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); } diff --git a/docs/dev/debt/libSceCdlgG2P.md b/docs/dev/debt/libSceCdlgG2P.md index 2549e5ceee..f35f20d05d 100644 --- a/docs/dev/debt/libSceCdlgG2P.md +++ b/docs/dev/debt/libSceCdlgG2P.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceCdlgG2P](../../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The signatures (`Open` and `GetResult` take a pointer, the others none) and the leading `int32` result are assumed from the other common dialogs, not verified for G2P; PPSA01289 imports it +- [libSceCdlgG2P](../../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The 128-byte open parameters, common-header validation and result field at byte 8 are recovered. Active-user verification and native service state transitions remain unimplemented; PPSA01289 imports it diff --git a/docs/dev/debt/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md index 98ff1e48e6..2c193e11f2 100644 --- a/docs/dev/debt/libSceCdlgPlayerReview.md +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -2,7 +2,7 @@ ### Unknown function info -- [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes +- [scePlayerReviewDialogOpen](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) uses a recovered 168-byte parameter layout; the 40-byte target record at byte 60 and mode semantics remain unresolved. Active-user verification is not implemented. - [scePlayerReviewDialogClose](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - `scePlayerReviewDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED` even when called before Open, as Signin, NetCtlAp and PlayerSelection do; MsgDialog.native, SaveDataDialog.native and ErrorDialog return an error there instead. Unverified against real hardware ### Functional From 5e43f87d5945693d48224933e85311e42dbca657 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:50:10 +0300 Subject: [PATCH 0777/1360] fix(dialog): reject closing inactive g2p and player review dialogs --- core/libs/prx/libSceCdlgG2P/Export.cpp | 5 ++++- core/libs/prx/libSceCdlgPlayerReview/Export.cpp | 4 +++- core/libs/tests/GuestG2PDialog.cpp | 10 +++++----- core/libs/tests/GuestPlayerReviewDialog.cpp | 12 ++++++------ docs/dev/debt/libSceCdlgPlayerReview.md | 1 - 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceCdlgG2P/Export.cpp b/core/libs/prx/libSceCdlgG2P/Export.cpp index 8a50061006..6f402b651d 100644 --- a/core/libs/prx/libSceCdlgG2P/Export.cpp +++ b/core/libs/prx/libSceCdlgG2P/Export.cpp @@ -16,6 +16,7 @@ constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; @@ -59,7 +60,9 @@ int APS5_VABI sceG2PDialogGetResult(CommonDialogResultPrefix* result) { } int APS5_VABI sceG2PDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + 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_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index d63a6ad02b..2380fd4630 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -47,7 +47,9 @@ int APS5_VABI scePlayerReviewDialogOpen(const PlayerReviewDialogParam* param) { } int APS5_VABI scePlayerReviewDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + 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_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index 0afa1a12e7..9a655706a9 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -78,18 +78,18 @@ int main() { Require(result[2] == kResultUserCanceled); Require(result[1] == -1); - Require(sceG2PDialogClose() == 0); + Require(sceG2PDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); Require(sceG2PDialogGetStatus() == kStatusNone); Require(sceG2PDialogInitialize() == 0); Require(sceG2PDialogGetStatus() == kStatusInitialized); - Require(sceG2PDialogClose() == 0); - Require(sceG2PDialogGetStatus() == kStatusFinished); + Require(sceG2PDialogClose() == static_cast<int>(0x80B8000Bu)); + Require(sceG2PDialogGetStatus() == kStatusInitialized); result[2] = -1; - Require(sceG2PDialogGetResult(result) == 0); + Require(sceG2PDialogGetResult(result) == kErrNotFinished); Require(result[0] == -1 && result[1] == -1 && result[3] == -1); - Require(result[2] == kResultUserCanceled); + Require(result[2] == -1); Require(sceG2PDialogTerminate() == 0); } diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 4c97608a99..cb6c65a423 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -79,20 +79,20 @@ int main() { Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_RESULT_USER_CANCELED); Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); - Require(scePlayerReviewDialogClose() == 0); + Require(scePlayerReviewDialogClose() == static_cast<int>(0x80B8000Bu)); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); - Require(scePlayerReviewDialogClose() == 0); - Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); - Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(scePlayerReviewDialogClose() == static_cast<int>(0x80B8000Bu)); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); result[2] = -1; - Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_FINISHED); Require(result[0] == -1 && result[1] == -1 && result[3] == -1); - Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result[2] == -1); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); } diff --git a/docs/dev/debt/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md index 2c193e11f2..ae7ee87b57 100644 --- a/docs/dev/debt/libSceCdlgPlayerReview.md +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -3,7 +3,6 @@ ### Unknown function info - [scePlayerReviewDialogOpen](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) uses a recovered 168-byte parameter layout; the 40-byte target record at byte 60 and mode semantics remain unresolved. Active-user verification is not implemented. -- [scePlayerReviewDialogClose](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - `scePlayerReviewDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED` even when called before Open, as Signin, NetCtlAp and PlayerSelection do; MsgDialog.native, SaveDataDialog.native and ErrorDialog return an error there instead. Unverified against real hardware ### Functional From 024dddc6f41729e54bb9964d89f9d347096c7a70 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:53:04 +0300 Subject: [PATCH 0778/1360] fix(avplayer): restore extended start position parameters --- core/libs/prx/libSceAvPlayer/Export.cpp | 7 +++---- .../libs/prx/libSceAvPlayer/include/AvPlayer.hpp | 13 ++++++++++--- core/libs/prx/libSceAvPlayer/src/Player.cpp | 8 ++++---- core/libs/prx/libSceAvPlayer/src/Source.cpp | 5 +++-- core/libs/tests/GuestAvPlayer.cpp | 16 ++++++++++++++-- docs/dev/debt/libSceAvPlayer.md | 1 - 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index 2cdb89887d..93d3d8fb0f 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -175,10 +175,9 @@ int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) { 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 sceAvPlayerStartEx(AvPlayerInternal* handle, const StartInfo* startInfo) { + if (handle == nullptr || startInfo == nullptr || startInfo->size != sizeof(*startInfo)) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; + return ToPlayer(handle)->Start(startInfo->startTime); } int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) { diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 49bf029391..1c82453073 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -51,6 +51,13 @@ public: virtual void OnError() = 0; }; +struct StartInfo { + std::uint64_t size; + std::uint64_t startTime; +}; + +static_assert(sizeof(StartInfo) == 16); + struct SourceSettings { AvPlayerMemAllocator memory; AvPlayerFileReplacement file; @@ -66,7 +73,7 @@ public: virtual bool EnableStream(std::uint32_t index) = 0; virtual bool DisableStream(std::uint32_t index) = 0; virtual bool ChangeStream(std::uint32_t from, std::uint32_t to) = 0; - virtual int Start() = 0; + virtual int Start(std::uint64_t startTime) = 0; virtual void Stop() = 0; virtual void Pause() = 0; virtual void Resume() = 0; @@ -103,7 +110,7 @@ public: int EnableStream(std::uint32_t index); int DisableStream(std::uint32_t index); int ChangeStream(std::uint32_t from, std::uint32_t to); - int Start(); + int Start(std::uint64_t startTime = 0); int Stop(); int Pause(); int Resume(); @@ -136,7 +143,7 @@ private: void autoStart(); void checkEndOfFile(); bool readyLocked() const; - int startLocked(); + int startLocked(std::uint64_t startTime = 0); int stopLocked(); AvPlayerFileReplacement file{}; diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index 191a185847..90c3b57dba 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -265,15 +265,15 @@ int Player::ChangeStream(std::uint32_t from, std::uint32_t to) { return SCE_OK; } -int Player::Start() { +int Player::Start(std::uint64_t startTime) { std::lock_guard lock(mutex); - return startLocked(); + return startLocked(startTime); } -int Player::startLocked() { +int Player::startLocked(std::uint64_t startTime) { if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; if (state != State::Ready && state != State::Stop && stopLocked() != SCE_OK) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; - const int result = source->Start(); + const int result = source->Start(startTime); if (result != SCE_OK) { previous = state; state = State::Error; diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 70f7e7fd31..4d31c38f1d 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -327,7 +327,8 @@ public: return true; } - int Start() override { + int Start(std::uint64_t startTime) override { + if (duration != 0 && startTime > duration) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; if (video.stream < 0 && audio.stream < 0) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; for (auto* decoder : {&video, &audio}) { if (decoder->stream < 0) continue; @@ -338,7 +339,7 @@ public: } } addVideoDecodeAheadBuffers(); - const auto start = startOffset; + const auto start = startTime != 0 ? startTime : startOffset; startOffset = 0; seek(start); { diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index efa5ff1e00..59250e3be1 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -21,6 +21,7 @@ #include <thread> #include <vector> #include "SceTypes.hpp" +#include "prx/libSceAvPlayer/include/AvPlayer.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceAvPlayer/include/AvPlayer.hpp" @@ -37,7 +38,7 @@ int APS5_VABI sceAvPlayerEnableStream(AvPlayerInternal*, std::uint32_t); int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal*, std::uint32_t, std::uint32_t); int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal*, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAvPlayerStart(AvPlayerInternal*); -int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal*, const void*); +int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal*, const AvPlayer::StartInfo*); int APS5_VABI sceAvPlayerStop(AvPlayerInternal*); int APS5_VABI sceAvPlayerPause(AvPlayerInternal*); int APS5_VABI sceAvPlayerResume(AvPlayerInternal*); @@ -1072,11 +1073,22 @@ void TestExtendedExports() { CheckStreamInfoEx(player); Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); Check(sceAvPlayerStartEx(nullptr, nullptr) == InvalidParams, "start accepted a null player"); - Check(sceAvPlayerStartEx(player, nullptr) == 0, "extended start failed"); + Check(sceAvPlayerStartEx(player, nullptr) == InvalidParams, "start accepted null parameters"); + AvPlayer::StartInfo startInfo{0, 700}; + Check(sceAvPlayerStartEx(player, &startInfo) == InvalidParams, "start accepted an invalid parameter size"); + startInfo.size = sizeof(startInfo); + Check(sceAvPlayerStartEx(player, &startInfo) == 0, "extended start failed"); Check(WaitFor([&] { return events.Seen(EventPlay); }), "play event missing"); AvPlayerFrameInfoEx frame{}; Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame after the extended start"); CheckVideoFrame(frame); + Check(frame.timestamp >= 700 && frame.timestamp <= 767, "extended start ignored the requested position"); + Check(!events.Seen(EventWarning, JumpComplete), "extended start reported a jump"); + Check(sceAvPlayerStop(player) == 0, "extended stop failed"); + startInfo.startTime = 0; + Check(sceAvPlayerStartEx(player, &startInfo) == 0, "extended restart failed"); + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame after extended restart"); + Check(frame.timestamp <= 67, "zero-position start did not restart playback"); Check(sceAvPlayerClose(player) == 0, "close failed"); } diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 5a2f1941ed..fb24c0d166 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -10,7 +10,6 @@ - [AvPlayer loop warnings](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) are queued when the first audio or video frame of a loop is delivered, following [KytyPS5's playback boundary tracking](https://github.com/KytyPS5/KytyPS5/blob/a6f4e8e7f456b8697d3102823d3f16400c3e121e/src/libs/avPlayer.cpp#L1038-L1043). The console's exact callback timing is unverified. Decode-ahead does not announce a loop while the caller is consuming the previous pass. - [sceAvPlayerAddSource](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown -- [sceAvPlayerStartEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored - [AvPlayer auto start](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - the auto start that `sceAvPlayerAddSource` queues for a player with auto start or without an event callback starts the player only while it is still ready: a title that starts the player itself before the auto start runs keeps its playback and the frames it holds. Not confirmed on a title. From 3f3944a0ae7ccbed72692eb576bb1092fa018df1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:54:05 +0300 Subject: [PATCH 0779/1360] fix(ult): restore active object destruction error codes --- core/libs/prx/libSceUlt/Export.cpp | 10 +++++----- core/libs/tests/GuestUltQueue.cpp | 10 +++++----- core/libs/tests/GuestUltThread.cpp | 8 ++++---- docs/dev/debt/libSceUlt.md | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index ba2978be02..b2d575d929 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -258,7 +258,7 @@ int APS5_VABI sceUltMutexDestroy(void* mutex) { auto state = it->second; std::lock_guard<std::mutex> stateLock(state->_mutex); if (state->_lockCount != 0 || state->_waiters != 0 || state->_conditions != 0 || state->_conditionWaiters != 0) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } state->_alive = false; gMutexes.erase(it); @@ -483,7 +483,7 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { if (entry.second->_waitingPool == pool) { std::lock_guard<std::mutex> stateLock(entry.second->_mutex); if (entry.second->_waiters != 0) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } } } @@ -491,7 +491,7 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { if (entry.second->_waitingPool == pool) { std::lock_guard<std::mutex> stateLock(entry.second->_mutex); if (entry.second->_waiters != 0) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } } } @@ -499,7 +499,7 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { if (entry.second->_pool && entry.second->_pool->_waitingPool == pool) { std::lock_guard<std::mutex> poolLock(entry.second->_pool->_mutex); if (entry.second->_waiters != 0) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } } } @@ -618,7 +618,7 @@ int APS5_VABI sceUltQueueDestroy(void* queue) { auto& pool = *state->_pool; { std::lock_guard stateLock(pool._mutex); - if (state->_waiters != 0) return ULT_ERROR_BUSY; + if (state->_waiters != 0) return ULT_ERROR_STATE; ReleaseQueue(*state); gQueues.erase(it); } diff --git a/core/libs/tests/GuestUltQueue.cpp b/core/libs/tests/GuestUltQueue.cpp index 91fba11ef9..ffce3c241f 100644 --- a/core/libs/tests/GuestUltQueue.cpp +++ b/core/libs/tests/GuestUltQueue.cpp @@ -156,13 +156,13 @@ static void Blocking() { CHECK(sceUltQueuePush(&fixture.first, &first) == 0); auto producer = std::async(std::launch::async, [&] { return sceUltQueuePush(&fixture.first, &second); }); WaitUntilBlocked(producer); - CHECK(sceUltQueueDestroy(&fixture.first) == Busy); + CHECK(sceUltQueueDestroy(&fixture.first) == State); CHECK(sceUltQueuePop(&fixture.first, &output) == 0 && output == first); CHECK(producer.get() == 0); CHECK(sceUltQueuePop(&fixture.first, &output) == 0 && output == second); auto consumer = std::async(std::launch::async, [&] { return sceUltQueuePop(&fixture.first, &output); }); WaitUntilBlocked(consumer); - CHECK(sceUltQueueDestroy(&fixture.first) == Busy); + CHECK(sceUltQueueDestroy(&fixture.first) == State); CHECK(sceUltQueuePush(&fixture.first, &first) == 0); CHECK(consumer.get() == 0 && output == first); fixture.Destroy(); @@ -180,7 +180,7 @@ static void SharedPool() { CHECK(sceUltQueueTryPop(&fixture.second, &output) == Again); auto producer = std::async(std::launch::async, [&] { return sceUltQueuePush(&fixture.second, &second); }); WaitUntilBlocked(producer); - CHECK(sceUltQueueDestroy(&fixture.second) == Busy); + CHECK(sceUltQueueDestroy(&fixture.second) == State); CHECK(sceUltQueueDestroy(&fixture.first) == 0); CHECK(producer.get() == 0); CHECK(consumer.get() == 0 && output == second); @@ -201,8 +201,8 @@ static void Finalize() { auto consumer = std::async(std::launch::async, [&] { return sceUltQueuePop(&fixture.second, &output); }); WaitUntilBlocked(producer); WaitUntilBlocked(consumer); - CHECK(sceUltQueueDestroy(&fixture.first) == Busy); - CHECK(sceUltQueueDestroy(&fixture.second) == Busy); + CHECK(sceUltQueueDestroy(&fixture.first) == State); + CHECK(sceUltQueueDestroy(&fixture.second) == State); CHECK(sceUltFinalize() == 0); CHECK(producer.get() == State); CHECK(consumer.get() == State && output == 123); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 0057ef4d0e..70df002c12 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -227,7 +227,7 @@ static void UnderscoredCreate() { CHECK(sceUltMutexLock(mutex) == Ok); CHECK(sceUltMutexLock(mutex) == Ok); CHECK(sceUltMutexUnlock(mutex) == Ok); - CHECK(sceUltMutexDestroy(mutex) == Busy); + CHECK(sceUltMutexDestroy(mutex) == State); CHECK(sceUltMutexUnlock(mutex) == Ok); alignas(8) static std::uint8_t dataPool[512]; alignas(8) static std::uint8_t queue[512]; @@ -242,7 +242,7 @@ static void UnderscoredCreate() { CHECK(sceUltWaitingQueueResourcePoolDestroy(unknownPool) == State); CHECK(sceUltMutexDestroy(nullptr) == Null); CHECK(sceUltMutexLock(mutex) == Ok); - CHECK(sceUltMutexDestroy(mutex) == Busy); + CHECK(sceUltMutexDestroy(mutex) == State); std::atomic<bool> waiterStarted{false}; std::atomic<bool> waiterAcquired{false}; std::thread waiter([&] { @@ -254,8 +254,8 @@ static void UnderscoredCreate() { while (!waiterStarted.load(std::memory_order_acquire)) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(20)); CHECK(!waiterAcquired.load(std::memory_order_acquire)); - CHECK(sceUltMutexDestroy(mutex) == Busy); - CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == Busy); + CHECK(sceUltMutexDestroy(mutex) == State); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == State); CHECK(sceUltMutexUnlock(mutex) == Ok); waiter.join(); CHECK(waiterAcquired.load(std::memory_order_acquire)); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 3e8b94f62a..e569ec3ab6 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -5,13 +5,13 @@ - [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. -- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. -- [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction, allocation failure codes, alignment checks and shared slot limits still require comparison with the native node allocator. +- [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) accept and ignore option parameters. Native options contain a 16-byte header and 112 zero reserved bytes; header, alignment and reserved-field validation are not implemented. - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. The nested context in `UltUlthreadInfo` still requires reconstruction. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. -- [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. +- [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_STATE` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). From c86b797e9eaa93f431659286a0e4df016c1eb7b2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:57:08 +0300 Subject: [PATCH 0780/1360] fix(webbrowserdialog): restore cookie parameter layouts --- .../prx/libSceWebBrowserDialog/Export.cpp | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 8ccf318370..1d0dfbc4d4 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -4,6 +4,23 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct WebBrowserDialogCookieParam { + std::uint64_t size; + const char* address; + const char* cookie; + std::uint8_t reserved[256]; +}; + +struct WebBrowserDialogResetCookieParam { + std::uint64_t size; + std::uint8_t reserved[256]; +}; + +static_assert(sizeof(WebBrowserDialogCookieParam) == 280); +static_assert(offsetof(WebBrowserDialogCookieParam, address) == 8); +static_assert(offsetof(WebBrowserDialogCookieParam, cookie) == 16); +static_assert(sizeof(WebBrowserDialogResetCookieParam) == 264); + namespace { std::atomic<int> g_status{0}; @@ -68,7 +85,7 @@ int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { } -int APS5_VABI sceWebBrowserDialogSetCookie(void) { +int APS5_VABI sceWebBrowserDialogSetCookie(const WebBrowserDialogCookieParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -82,7 +99,7 @@ int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param) return 0; } -int APS5_VABI sceWebBrowserDialogResetCookie() { +int APS5_VABI sceWebBrowserDialogResetCookie(const WebBrowserDialogResetCookieParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } From 964fe1116862dfc743a804e8982620dc9662f9cf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:57:09 +0300 Subject: [PATCH 0781/1360] fix(appinstutil): restore package installation argument slots --- core/libs/prx/libSceAppInstUtil/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAppInstUtil/Export.cpp b/core/libs/prx/libSceAppInstUtil/Export.cpp index c3485fbe05..5925f41ddd 100644 --- a/core/libs/prx/libSceAppInstUtil/Export.cpp +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -39,7 +39,7 @@ int APS5_VABI sceAppInstUtilAppGetSize(const char* titleId, std::uint64_t* size) NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAppInstUtilAppInstallPkg() { +int APS5_VABI sceAppInstUtilAppInstallPkg(const char* packagePath, std::uint64_t serviceArgument) { NotImplemented_nid_no_patch(__func__); return 0; } From 6d318d168ae3e3b1dd29c651d4a9ff7bc80b6672 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:57:34 +0300 Subject: [PATCH 0782/1360] fix(font): restore graphics and word creation argument slots --- core/libs/prx/libSceFont/src/Unimplemented.cpp | 4 ++-- docs/dev/debt/libSceFont.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 49bdec210f..c98c47a93e 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -153,12 +153,12 @@ int APS5_VABI sceFontCreateGraphicsService(const FontMemory* memory, const FontG return 0; } -int APS5_VABI sceFontCreateGraphicsServiceWithEdition() { +int APS5_VABI sceFontCreateGraphicsServiceWithEdition(const FontMemory* memory, const FontGraphicsServiceParam* param, std::uint64_t unusedSlot, FontGraphicsService** service) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontCreateWords() { +int APS5_VABI sceFontCreateWords(const FontMemory* memory, FontTextSource* source, std::uint64_t unusedSlot, FontWords** words) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 585003d2a6..b387ddb5d3 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -36,3 +36,5 @@ Canvas, design, surface texture and fill-pattern construction signatures in [Uni [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) now defines the 32-byte fill method, 64-byte fill plot/rates and 96-byte device usage layouts, with size and field-offset assertions. Uninterpreted descriptor tails remain reserved; pool statistics expose capacity, current usage, available bytes, peak and signed change. The [glyph support registration and texture-image fill signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered from native argument use. The image constructor takes a byte span and four scalar coordinates; the decompiler omits its successful path. `CreateWords` and `CreateGraphicsServiceWithEdition` consume the output pointer in the fourth general-purpose argument slot but do not read the third slot, whose source type is not established. + +- [sceFontCreateGraphicsServiceWithEdition and sceFontCreateWords](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) take their handle output in the fourth integer argument slot. The third slot is not read by the supplied module and is represented as an opaque 64-bit value; its declared source type remains unknown. From 96ae2b5d5d294f5ac4796180f239383cada83354 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 10:58:34 +0300 Subject: [PATCH 0783/1360] fix(syscore): restore application info query arguments --- core/libs/prx/libSceSysCore/Export.cpp | 8 +++++++- docs/dev/debt/libSceSysCore.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSysCore/Export.cpp b/core/libs/prx/libSceSysCore/Export.cpp index 7c5cd4bbd6..f06a188fd8 100644 --- a/core/libs/prx/libSceSysCore/Export.cpp +++ b/core/libs/prx/libSceSysCore/Export.cpp @@ -3,9 +3,15 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct ApplicationInfo { + std::byte data[152]; +}; + +static_assert(sizeof(ApplicationInfo) == 152); + extern "C" { -int APS5_VABI sceApplicationGetAppInfoByAppId() { +int APS5_VABI sceApplicationGetAppInfoByAppId(std::uint32_t applicationId, ApplicationInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSysCore.md b/docs/dev/debt/libSceSysCore.md index 0ebd352ebd..969d61b1fe 100644 --- a/docs/dev/debt/libSceSysCore.md +++ b/docs/dev/debt/libSceSysCore.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceApplicationGetAppInfoByAppId](../../../core/libs/prx/libSceSysCore/Export.cpp) (libSceSysCore) - the name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysCore.c), but no credible prototype was found. The declaration has no parameters solely to resolve a known import and makes no ABI claim +- [sceApplicationGetAppInfoByAppId](../../../core/libs/prx/libSceSysCore/Export.cpp) takes a 32-bit application ID and a writable 152-byte record. The service fills the record through IPC; its complete field layout remains unresolved. The SystemService caller consumes a subrecord beginning at byte 48. From 991efec08663d5a3436d9527bd3ce15ea9a9173d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:03:05 +0300 Subject: [PATCH 0784/1360] fix(systemservice): restore tournament and daemon argument slots --- core/libs/prx/libSceSystemService/Export.cpp | 15 ++++++++++++++- .../prx/libSceSystemService/PendingExports.cpp | 8 ++++---- docs/dev/debt/libSceSystemService.md | 2 ++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index d0b5a7623b..08e4239f19 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -393,7 +393,20 @@ int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { return SYSTEM_SERVICE_OK; } -int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { +struct SystemServiceTournamentOccurrenceParam { + std::uint64_t size; + const char* occurrenceId; + std::int32_t userId; + std::uint32_t padding; + const char* context; +}; + +static_assert(sizeof(SystemServiceTournamentOccurrenceParam) == 32); +static_assert(offsetof(SystemServiceTournamentOccurrenceParam, occurrenceId) == 8); +static_assert(offsetof(SystemServiceTournamentOccurrenceParam, userId) == 16); +static_assert(offsetof(SystemServiceTournamentOccurrenceParam, context) == 24); + +int APS5_VABI sceSystemServiceOpenTournamentOccurrence(const SystemServiceTournamentOccurrenceParam* param) { return SYSTEM_SERVICE_OK; } diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 9841ee91cf..1c37a7a66c 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -286,7 +286,7 @@ int APS5_VABI sceLncUtilBlockAppSuspend(const char* reason) { return 0; } -int APS5_VABI sceLncUtilBlockingGetEventForDaemon() { +int APS5_VABI sceLncUtilBlockingGetEventForDaemon(LncDaemonEvent* event, std::uint64_t receiveArgument) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2157,7 +2157,7 @@ int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu(std::int32_t processId) return 0; } -int APS5_VABI sceSystemServiceLaunchEventDetails() { +int APS5_VABI sceSystemServiceLaunchEventDetails(std::uint64_t unusedSlot, const char* eventId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2172,7 +2172,7 @@ int APS5_VABI sceSystemServiceLaunchTournamentList() { return 0; } -int APS5_VABI sceSystemServiceLaunchTournamentsTeamProfile() { +int APS5_VABI sceSystemServiceLaunchTournamentsTeamProfile(std::uint64_t unusedSlot, const char* profile, const char* tournament, const char* context) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -2315,7 +2315,7 @@ int APS5_VABI sceSystemServiceShowEyeToEyeDistanceSetting() { return 0; } -int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu() { +int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu(std::uint8_t enabled, std::uint64_t reserved) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 8d9548f41d..8abccbf202 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -148,3 +148,5 @@ - [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. - [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 100–111, 200–213 and 300 throw because their settings are not modeled. Language results omit the console SDK-version compatibility remapping, and parameters 7, 400 and 1000 do not reproduce platform-dependent availability. + +- [Tournament launch parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): occurrence parameters occupy 32 bytes and contain two string pointers and a user ID. The final string semantics are unresolved. Event-details and team-profile launches retain an unused first argument slot of unknown declared type. The daemon blocking-event receive argument is passed unchanged as 64 bits to IPC; its meaning remains unresolved. Protected bodies in [PendingExports.cpp](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) remain unchanged. From 12223bf208f5ec6c80725799a386ef977ceb5926 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Wed, 21 Oct 2026 07:54:43 -0400 Subject: [PATCH 0785/1360] fix(agc): chain the twin format list only with VK_KHR_image_format_list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 6 ++++++ core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 55b1317658..60ede9e356 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -342,6 +342,7 @@ struct VulkanDevice::State { bool imageInt64Atomics = false; bool bufferInt64Atomics = false; bool viewportIndexLayer = false; + bool imageFormatList = false; bool primitiveListRestart = false; bool depthClipControl = false; bool graphicsPipelineLibrary = false; @@ -1313,6 +1314,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer"); state->viewportIndexLayer = true; } + if (hasExtension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + state->imageFormatList = true; + } if (state->demoteToHelperInvocation) { deviceExtensions.push_back(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityDemoteToHelperInvocation); @@ -3835,6 +3840,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); context.viewportIndexLayer = state->viewportIndexLayer; + context.imageFormatList = state->imageFormatList; context.demoteToHelperInvocation = state->demoteToHelperInvocation; context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting; context.primitiveListRestart = state->primitiveListRestart; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 1ac783d39e..4167955e8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -242,6 +242,7 @@ struct Context { bool maintenance5 = false; bool storageImageMultisample = false; bool storageImageWriteWithoutFormat = false; + bool imageFormatList = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool nullDescriptors = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 439edfd0a1..a8a3355769 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -155,7 +155,7 @@ public: const std::array<VkFormat, 2> viewFormats{target.format, twinFormat}; VkImageFormatListCreateInfo formatList{VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO}; if (twinFormat != VK_FORMAT_UNDEFINED) { - if ((flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { + if ((flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0 && context.imageFormatList) { formatList.viewFormatCount = static_cast<std::uint32_t>(viewFormats.size()); formatList.pViewFormats = viewFormats.data(); info.pNext = &formatList; From a9642f82419313a0a8c337060c2e1c39f298ec08 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:03:55 +0300 Subject: [PATCH 0786/1360] fix(vrtracker): restore processing and initialization contracts --- core/libs/prx/libSceVrTracker/Export.cpp | 69 +++++++++++++++++++++--- docs/dev/debt/libSceVrTracker.md | 2 +- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index b68062f19b..6ca29bfe49 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -3,29 +3,86 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct VrTrackerMemoryBlock { + void* address; + std::uint32_t size; + std::uint32_t alignment; +}; + +struct VrTrackerInitConfig { + std::uint32_t type; + std::uint32_t option; + std::uint32_t mode; + std::uint32_t parameter; + std::uint32_t reserved[4]; +}; + +struct VrTrackerInitParam { + std::uint32_t size; + std::int32_t mode; + std::uint32_t reserved[6]; + VrTrackerInitConfig config; + VrTrackerMemoryBlock memory[3]; + std::uint32_t option112; + std::uint32_t option116; + std::uint32_t reservedTail[6]; +}; + +static_assert(sizeof(VrTrackerMemoryBlock) == 16); +static_assert(sizeof(VrTrackerInitConfig) == 32); +static_assert(sizeof(VrTrackerInitParam) == 144); +static_assert(offsetof(VrTrackerInitParam, config) == 32); +static_assert(offsetof(VrTrackerInitParam, memory) == 64); +static_assert(offsetof(VrTrackerInitParam, option112) == 112); +struct VrTrackerGpuSubmitParam; +struct VrTrackerResult; + +struct VrTrackerCpuProcessParam { + std::uint32_t size; + std::uint32_t mode; + std::uint32_t argument; + std::uint32_t reserved[5]; +}; + +struct VrTrackerGpuWaitParam { + std::uint32_t size; + std::byte payload[28]; +}; + +struct VrTrackerGetResultParam { + std::uint32_t size; + std::byte payload[52]; +}; + +static_assert(sizeof(VrTrackerCpuProcessParam) == 32); +static_assert(offsetof(VrTrackerCpuProcessParam, mode) == 4); +static_assert(offsetof(VrTrackerCpuProcessParam, argument) == 8); +static_assert(sizeof(VrTrackerGpuWaitParam) == 32); +static_assert(sizeof(VrTrackerGetResultParam) == 56); + extern "C" { -int APS5_VABI sceVrTrackerCpuProcess() { +int APS5_VABI sceVrTrackerCpuProcess(const VrTrackerCpuProcessParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTrackerGetResult() { +int APS5_VABI sceVrTrackerGetResult(const VrTrackerGetResultParam* param, VrTrackerResult* result) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTrackerGetTime() { +int APS5_VABI sceVrTrackerGetTime(std::uint64_t* time) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTrackerGpuSubmit() { +int APS5_VABI sceVrTrackerGpuSubmit(const VrTrackerGpuSubmitParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTrackerGpuWait() { +int APS5_VABI sceVrTrackerGpuWait(const VrTrackerGpuWaitParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTrackerInit() { +int APS5_VABI sceVrTrackerInit(const VrTrackerInitParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceVrTracker.md b/docs/dev/debt/libSceVrTracker.md index 7180a455a7..da28022685 100644 --- a/docs/dev/debt/libSceVrTracker.md +++ b/docs/dev/debt/libSceVrTracker.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceVrTrackerCpuProcess, sceVrTrackerGetResult, sceVrTrackerGetTime, sceVrTrackerGpuSubmit, sceVrTrackerGpuWait and sceVrTrackerInit](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [VrTracker parameters](../../../core/libs/prx/libSceVrTracker/Export.cpp): argument slots are recovered. CPU processing and GPU waiting consume 32-byte records, result queries consume 56-byte records, and time queries write 64 bits. Initialization accepts 128-byte and 144-byte versions; the 144-byte layout includes three address/size/alignment records, but configuration selector semantics and the older field layout remain unresolved. GPU submission accepts 552-byte and 648-byte versions; submission and result records still require field recovery; result storage has 1520-byte and 3640-byte variants. CPU mode/argument semantics and the remaining query fields are unresolved. Protected bodies remain unchanged. - [sceVrTrackerCpuPopMarker, sceVrTrackerCpuPushMarker, sceVrTrackerGetLiveCaptureId, sceVrTrackerGpuWaitAndCpuProcess, sceVrTrackerResetAll, sceVrTrackerSaveInternalBuffers and sceVrTrackerSetDeviceRejection](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. - [sceVrTrackerSetExtendedMode, sceVrTrackerSetLEDBrightness, sceVrTrackerSetRestingMode, sceVrTrackerStartLiveCapture, sceVrTrackerStopLiveCapture and sceVrTrackerTerm](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. - [sceVrTrackerGetPlayAreaWarningInfo, sceVrTrackerNotifyEndOfCpuProcess, sceVrTrackerQueryMemory, sceVrTrackerRecalibrate, sceVrTrackerRegisterDevice and sceVrTrackerRegisterDevice2](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. From 7daeb8059ff27eba6c3119c00bc846a130356639 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:05:13 +0300 Subject: [PATCH 0787/1360] fix(movetracker): restore initialization argument slots --- core/libs/prx/libSceMoveTracker/Export.cpp | 2 +- docs/dev/debt/libSceMoveTracker.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceMoveTracker/Export.cpp b/core/libs/prx/libSceMoveTracker/Export.cpp index fb002273b5..ef780c7000 100644 --- a/core/libs/prx/libSceMoveTracker/Export.cpp +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -54,7 +54,7 @@ int APS5_VABI sceMoveTrackerGetWorkingMemorySize(std::uint32_t* totalSize, std:: return 0; } -int APS5_VABI sceMoveTrackerInit() { +int APS5_VABI sceMoveTrackerInit(void* workMemory, std::uint64_t requiredArgument, std::uint32_t pipeId, std::uint32_t queueId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMoveTracker.md b/docs/dev/debt/libSceMoveTracker.md index b6ddbd43a0..c261f9fcd9 100644 --- a/docs/dev/debt/libSceMoveTracker.md +++ b/docs/dev/debt/libSceMoveTracker.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libSceMoveTracker](../../../core/libs/prx/libSceMoveTracker/Export.cpp) remains unimplemented. Calibration direction, camera/controller input, capture initialization, state query, memory-size query and playback output signatures are recovered. Camera and controller records occupy 24 bytes, state records 160 bytes, and playback metadata 16 bytes; their complete field meanings still require reconstruction. Initialization arguments and capture/playback helper signatures need further checking against caller register use. +- [libSceMoveTracker](../../../core/libs/prx/libSceMoveTracker/Export.cpp) remains unimplemented. Calibration direction, camera/controller input, capture initialization, state query, memory-size query and playback output signatures are recovered. Camera and controller records occupy 24 bytes, state records 160 bytes, and playback metadata 16 bytes; their complete field meanings still require reconstruction. Initialization consumes a work-memory pointer, a required nonzero 64-bit argument and two 32-bit GPU queue selectors below eight. The second argument is only checked for zero; its declared type remains unresolved. Capture/playback helper signatures need further checking against caller register use. From 35e3c134c4f31d5bac847be4a4ee0d70b7fe0577 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:09:42 +0300 Subject: [PATCH 0788/1360] fix(mbus): restore by-value device state arguments --- core/libs/prx/libSceMbus/PendingExports.cpp | 9 +++++++-- docs/dev/debt/libSceMbus.md | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index 593546e9b6..76313b1a67 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -2,6 +2,11 @@ #include <cstddef> +struct MbusDeviceFunctionState { + std::byte data[180]; +}; + +static_assert(sizeof(MbusDeviceFunctionState) == 180); struct MbusDeviceDescription; struct MbusDeviceInfo; struct MbusSimulatedHandleStatus; @@ -340,7 +345,7 @@ int APS5_VABI sceMbusEventBusStatusChangeSubscribe(std::int32_t eventHandle, std return 0; } -int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe() { +int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe(std::uint64_t unusedSlot, std::int32_t subscription) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -505,7 +510,7 @@ int APS5_VABI sceMbusSetCameraAppModuleFocus(std::int32_t moduleId) { return 0; } -int APS5_VABI sceMbusSetDeviceFunctionState() { +int APS5_VABI sceMbusSetDeviceFunctionState(std::uint64_t deviceId, MbusDeviceFunctionState state) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index b1bb624f14..9078d4ebc6 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,7 +8,7 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): SetDeviceFunctionState still requires its by-value aggregate layout. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. +- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): SetDeviceFunctionState takes a 64-bit device identifier and a 180-byte aggregate by value on the stack. Its service-side fields and source alignment remain unresolved; the declaration preserves the observed stack ABI with opaque storage. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. From 07a181649c4e34574920b87202d491807aea0a1e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:11:32 +0300 Subject: [PATCH 0789/1360] fix(savedata): restore cancellation and transfer preparation arguments --- core/libs/prx/libSceSaveData.native/Export.cpp | 4 ++-- core/libs/tests/GuestSaveDataCancel.cpp | 5 +++-- docs/dev/debt/libSceSaveData.native.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index a95498f7d9..afe320f439 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -978,7 +978,7 @@ int APS5_VABI sceSaveDataGetConvertProgress(float* progress) { return rc; } -int APS5_VABI sceSaveDataCancel() { +int APS5_VABI sceSaveDataCancel(std::uint32_t cancelArgument) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -989,7 +989,7 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, S return rc; } -int APS5_VABI sceSaveDataPrepareForTransferring() { +int APS5_VABI sceSaveDataPrepareForTransferring(const SaveDataMountPoint* mountPoint, const SaveDataPrepareParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestSaveDataCancel.cpp b/core/libs/tests/GuestSaveDataCancel.cpp index 9b8087c031..ce9c2e8c20 100644 --- a/core/libs/tests/GuestSaveDataCancel.cpp +++ b/core/libs/tests/GuestSaveDataCancel.cpp @@ -1,13 +1,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdio> +#include <cstdint> #include <stdexcept> #include <string> -extern "C" int APS5_VABI sceSaveDataCancel(); +extern "C" int APS5_VABI sceSaveDataCancel(std::uint32_t cancelArgument); int main() { try { - sceSaveDataCancel(); + sceSaveDataCancel(0); } catch (const std::runtime_error& error) { if (std::string(error.what()) == "sceSaveDataCancel not implemented") return 0; std::fprintf(stderr, "sceSaveDataCancel threw: %s\n", error.what()); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index db7aeb7660..ba886396d2 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -3,11 +3,11 @@ ### Unknown function info - [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown -- [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. +- [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp): the native wrapper confirms the same 16-byte mount point and 40-byte preparation parameter transport as `sceSaveDataPrepare`. Transfer preparation still requires the save-data service backend; its protected stub is unchanged. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured -- [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown +- [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp): the native wrapper forwards one 32-bit argument to the save-data service. Its meaning and cancellation results require the service handler; the protected stub is unchanged. - [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware - [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `<dir>.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [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 From f80c7e047dd99ded6c968bf328d14d949c8d3b6e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:12:55 +0300 Subject: [PATCH 0790/1360] fix(webbrowserdialog): restore predetermined content allow-list validation --- .../prx/libSceCommonDialog/CommonDialog.hpp | 8 ++++++ .../prx/libSceWebBrowserDialog/Export.cpp | 18 ++++++++++-- core/libs/tests/GuestWebBrowserDialog.cpp | 28 +++++++++++++++---- docs/dev/debt/libSceWebBrowserDialog.md | 2 +- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index b4d577c0db..e0a19fe9e8 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -29,6 +29,14 @@ struct PlayerReviewDialogParam { std::uint8_t reserved[64]; }; +struct WebBrowserDialogAllowList { + std::uint64_t size; + const char* addresses[20]; + std::uint8_t reserved[256]; +}; + +static_assert(sizeof(WebBrowserDialogAllowList) == 424); + struct CommonDialogResultPrefix { std::uint8_t untouched[8]; std::int32_t result; diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 1d0dfbc4d4..97434d16e1 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -2,6 +2,7 @@ #include <cstdint> #include <cstddef> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" struct WebBrowserDialogCookieParam { @@ -29,9 +30,9 @@ constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_PARAMETER = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); } @@ -90,11 +91,22 @@ int APS5_VABI sceWebBrowserDialogSetCookie(const WebBrowserDialogCookieParam* pa return 0; } -int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param) { +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, const WebBrowserDialogAllowList* allowList) { 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 (allowList == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (allowList->size != sizeof(*allowList)) return COMMON_DIALOG_ERROR_PARAMETER; + bool hasAddress = false; + for (const char* address : allowList->addresses) { + if (address == nullptr) continue; + hasAddress = true; + std::size_t length = 0; + while (length < 256 && address[length] != '\0') ++length; + if (length == 0 || length == 256) return COMMON_DIALOG_ERROR_PARAMETER; + } + if (!hasAddress || std::any_of(std::begin(allowList->reserved), std::end(allowList->reserved), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index ea5d58c2cc..5f4802bef8 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include <cstdint> #include <cstdlib> @@ -6,7 +7,7 @@ extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void); int APS5_VABI sceWebBrowserDialogTerminate(void); int APS5_VABI sceWebBrowserDialogOpen(const void* param); -int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param); +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, const WebBrowserDialogAllowList* allowList); int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); @@ -20,7 +21,6 @@ constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); @@ -30,6 +30,9 @@ void Require(bool value) { if (!value) std::abort(); } int main() { std::uint8_t param[64] = {}; + WebBrowserDialogAllowList allowList{}; + allowList.size = sizeof(allowList); + allowList.addresses[0] = "test.invalid"; std::int32_t result = 0x5a5a5a5a; Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); @@ -65,10 +68,25 @@ int main() { Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); - Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogInitialize() == 0); - Require(sceWebBrowserDialogOpenForPredeterminedContent(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); - Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == 0); + Require(sceWebBrowserDialogOpenForPredeterminedContent(nullptr, &allowList) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + allowList.addresses[0] = nullptr; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast<int>(0x80B8000Au)); + allowList.addresses[19] = "test.invalid"; + allowList.reserved[255] = 1; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast<int>(0x80B8000Au)); + allowList.reserved[255] = 0; + allowList.addresses[19] = ""; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast<int>(0x80B8000Au)); + char longAddress[257]; + std::fill(std::begin(longAddress), std::end(longAddress), 'a'); + longAddress[256] = '\0'; + allowList.addresses[19] = longAddress; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast<int>(0x80B8000Au)); + longAddress[255] = '\0'; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index 3d44927711..8cd180fe3b 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The real export takes a second argument, the allow-list of up to twenty addresses (SharpProspero `docs/dialogs.md`); it is not read here, so no address filtering is modelled, and the exact validation order and codes with a null second argument are unverified. +- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The 424-byte allow-list parameter and its twenty optional address pointers are validated, but address filtering is not modelled without an interactive browser. Main open-parameter validation remains incomplete. - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogClose` transitions an initialized dialog directly to `COMMON_DIALOG_STATUS_FINISHED` even when called before `Open`, following the Signin / NetCtlAp / PlayerSelection convention; this behavior is unverified against real hardware. ### Unknown function info From fd6372e4f6295710a514ed276b040db664ea2c2b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:13:51 +0300 Subject: [PATCH 0791/1360] fix(webbrowserdialog): validate result buffers and inactive close --- .../prx/libSceCommonDialog/CommonDialog.hpp | 21 ++++++++ .../prx/libSceWebBrowserDialog/Export.cpp | 30 +++++++---- core/libs/tests/GuestWebBrowserDialog.cpp | 51 +++++++++++++------ docs/dev/debt/libSceWebBrowserDialog.md | 2 - 4 files changed, 77 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index e0a19fe9e8..d16c6ad73d 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -29,6 +29,27 @@ struct PlayerReviewDialogParam { std::uint8_t reserved[64]; }; +struct WebBrowserDialogExtendedResult { + std::uint64_t size; + std::uint32_t type; + char content[2048]; + std::uint8_t reserved0[4]; + char* extendedContent; + std::uint32_t extendedCapacity; + std::uint8_t reserved1[20]; +}; + +struct WebBrowserDialogResult { + std::int32_t result; + std::uint32_t padding; + WebBrowserDialogExtendedResult* extended; + std::uint8_t reserved[240]; +}; + +static_assert(sizeof(WebBrowserDialogExtendedResult) == 2096); +static_assert(offsetof(WebBrowserDialogExtendedResult, extendedContent) == 2064); +static_assert(sizeof(WebBrowserDialogResult) == 256); + struct WebBrowserDialogAllowList { std::uint64_t size; const char* addresses[20]; diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 97434d16e1..8c720fbab1 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -32,6 +32,7 @@ constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_PARAMETER = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); } @@ -54,18 +55,27 @@ int APS5_VABI sceWebBrowserDialogTerminate(void) { } int APS5_VABI sceWebBrowserDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; + 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_NOT_RUNNING; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } -int APS5_VABI sceWebBrowserDialogGetResult(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; - *static_cast<std::int32_t*>(result) = COMMON_DIALOG_RESULT_USER_CANCELED; - return 0; +int APS5_VABI sceWebBrowserDialogGetResult(WebBrowserDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (std::any_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + if (result->extended != nullptr) { + const auto& extended = *result->extended; + if (extended.size != sizeof(extended)) return COMMON_DIALOG_ERROR_PARAMETER; + if (std::any_of(std::begin(extended.reserved0), std::end(extended.reserved0), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + if (std::any_of(std::begin(extended.reserved1), std::end(extended.reserved1), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + } + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return 0; } int APS5_VABI sceWebBrowserDialogGetStatus(void) { diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 5f4802bef8..bb391077a8 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -11,7 +11,7 @@ int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); -int APS5_VABI sceWebBrowserDialogGetResult(void* result); +int APS5_VABI sceWebBrowserDialogGetResult(WebBrowserDialogResult* result); } namespace { @@ -33,21 +33,22 @@ int main() { WebBrowserDialogAllowList allowList{}; allowList.size = sizeof(allowList); allowList.addresses[0] = "test.invalid"; - std::int32_t result = 0x5a5a5a5a; + WebBrowserDialogResult result{}; + result.result = 0x5a5a5a5a; Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); - Require(result == 0x5a5a5a5a); + Require(result.result == 0x5a5a5a5a); Require(sceWebBrowserDialogInitialize() == 0); Require(sceWebBrowserDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_FINISHED); - Require(result == 0x5a5a5a5a); + Require(result.result == 0x5a5a5a5a); Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); @@ -61,9 +62,9 @@ int main() { Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetResult(&result) == 0); - Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result.result == COMMON_DIALOG_RESULT_USER_CANCELED); - Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); @@ -90,23 +91,43 @@ int main() { Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); - result = 0x5a5a5a5a; + result.result = 0x5a5a5a5a; Require(sceWebBrowserDialogGetResult(&result) == 0); - Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result.result == COMMON_DIALOG_RESULT_USER_CANCELED); - Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); - Require(sceWebBrowserDialogClose() == 0); - Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); - result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_FINISHED); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + result.reserved[239] = 1; + Require(sceWebBrowserDialogGetResult(&result) == static_cast<int>(0x80B8000Au)); + result.reserved[239] = 0; + WebBrowserDialogExtendedResult extended{}; + result.extended = &extended; + Require(sceWebBrowserDialogGetResult(&result) == static_cast<int>(0x80B8000Au)); + extended.size = sizeof(extended); + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpen(param) == 0); + extended.reserved0[3] = 1; + Require(sceWebBrowserDialogGetResult(&result) == static_cast<int>(0x80B8000Au)); + extended.reserved0[3] = 0; + extended.reserved1[19] = 1; + Require(sceWebBrowserDialogGetResult(&result) == static_cast<int>(0x80B8000Au)); + extended.reserved1[19] = 0; + extended.type = 0x5a5a5a5a; + extended.content[2047] = 'x'; + result.padding = 0x5a5a5a5a; Require(sceWebBrowserDialogGetResult(&result) == 0); - Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result.result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result.padding == 0x5a5a5a5a && result.extended == &extended); + Require(extended.type == 0x5a5a5a5a && extended.content[2047] == 'x'); Require(sceWebBrowserDialogTerminate() == 0); - Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); - return 0; } diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index 8cd180fe3b..f86ba5ae8f 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -3,8 +3,6 @@ ### Silent stubs - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The 424-byte allow-list parameter and its twenty optional address pointers are validated, but address filtering is not modelled without an interactive browser. Main open-parameter validation remains incomplete. -- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogClose` transitions an initialized dialog directly to `COMMON_DIALOG_STATUS_FINISHED` even when called before `Open`, following the Signin / NetCtlAp / PlayerSelection convention; this behavior is unverified against real hardware. - ### Unknown function info - [sceWebBrowserDialogGetEvent, sceWebBrowserDialogNavigate, sceWebBrowserDialogSetZoom](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceWebBrowserDialog.c); the no-parameter prototypes follow [shadPS4 `webbrowserdialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp#L23-L85) and are not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. From 218be372ea5b94dc81ad110a7a878a7ce6f7adb8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:16:42 +0300 Subject: [PATCH 0792/1360] fix(agc): restore protected export argument and return types --- core/libs/prx/libSceAgc/Unimplemented.cpp | 16 ++++++++-------- docs/dev/debt/libSceAgc.md | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 7a7d7c52cb..02dcc73aca 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -5,42 +5,42 @@ extern "C" { -int APS5_VABI sceAgcAcbPushMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcAcbSetMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcAcquireMemSetEngine() { +int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcDcbPushMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcDcbSetMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcSetSemaphoreMemory() { +int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcCbMemsetExclusive() { +std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcSetShaderInstrumentation() { +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 0bd4347123..eb2c126c2d 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -4,7 +4,6 @@ - [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - `SPI_SHADER_PGM_RSRC2_GS.OC_LDS_EN` is preserved if either half requests it, while user SGPR allocation comes from the front half and other state stays with the back half. Bit 18 is `OC_LDS_EN` in AMD's [GFX10 register definitions](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_sh_mask.h). Mesa's radeonsi sets it on a merged ES+GS program exactly when the ES stage is a tessellation evaluation shader ([`si_shader_gs_legacy`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L631), [`gfx10_shader_ngg`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L1144)), so it belongs to the front half; console fusion output has not been measured. Cocoon (PPSA08766) requests front `0x70004` and back `0xab0020`. Other unmatched hardware state still throws. -- PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../../core/libs/prx/libSceAgc/Unimplemented.cpp). Name from the shadPS4 aerolib NID list - [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 - [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 - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed From 2053461e93b414f7b1aa2473161ab4371806d546 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:19:22 +0300 Subject: [PATCH 0793/1360] fix(ult): validate creation options and work area contracts --- core/libs/prx/libSceUlt/Export.cpp | 38 ++++++++++++++++++++++++- core/libs/prx/libSceUlt/SceUltTypes.hpp | 8 ++++++ core/libs/tests/GuestUltQueue.cpp | 2 +- core/libs/tests/GuestUltThread.cpp | 29 ++++++++++++++++--- docs/dev/debt/libSceUlt.md | 3 +- 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index b2d575d929..5580ec5b6f 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -36,6 +36,16 @@ std::unordered_map<void*, UltRuntimeState> gRuntimes; std::unordered_map<void*, std::shared_ptr<UltUlthreadState>> gUlthreads; +int validateCommonOptParam(const void* optParam) { + if (optParam == nullptr) return ULT_OK; + if ((reinterpret_cast<std::uintptr_t>(optParam) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + UltCommonOptParam value; + std::memcpy(&value, optParam, sizeof(value)); + if (value.header[0] != 0x6C696275 || value.header[1] != 0x6C746D61 || value.header[2] != 0x67696300) return ULT_ERROR_INVALID; + if (std::any_of(std::begin(value.reserved), std::end(value.reserved), [](std::uint8_t byte) { return byte != 0; })) return ULT_ERROR_INVALID; + return ULT_OK; +} + int semaphoreGetState(void* semaphore, std::shared_ptr<UltSemaphoreState>* out) { std::lock_guard<std::mutex> lock(gMutex); auto it = gSemaphores.find(semaphore); @@ -586,7 +596,7 @@ int APS5_VABI sceUltQueueCreate(void* queue, const char* name, std::uint64_t dat auto& pool = *state->_pool; std::lock_guard stateLock(pool._mutex); if (dataSize > pool._dataSize || waitingQueueResourcePool != pool._waitingPool) return ULT_ERROR_INVALID; - if (pool._queues == pool._numQueueObject) return ULT_ERROR_AGAIN; + if (pool._queues == pool._numQueueObject) return ULT_ERROR_BUSY; gQueues.emplace(queue, std::move(state)); ++pool._queues; std::memset(queue, 0, 512); @@ -888,10 +898,20 @@ int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { } int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam) { + if (pool == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(pool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (name == nullptr) return ULT_ERROR_NULL; + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; + if (numThreads == 0 || numSyncObjects == 0) return ULT_ERROR_INVALID; + if ((reinterpret_cast<std::uintptr_t>(workArea) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (workArea == nullptr) return ULT_ERROR_NULL; return sceUltWaitingQueueResourcePoolCreate(pool, name, numThreads, numSyncObjects, workArea, optParam, 0); } int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam) { + if (semaphore == nullptr || name == nullptr) return ULT_ERROR_NULL; + if (((reinterpret_cast<std::uintptr_t>(semaphore) | reinterpret_cast<std::uintptr_t>(waitingQueueResourcePool)) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, 0); } @@ -913,10 +933,26 @@ int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQue } int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam) { + if (pool == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(pool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; + if (numData == 0 || dataSize == 0 || numQueueObject == 0) return ULT_ERROR_INVALID; + if ((reinterpret_cast<std::uintptr_t>(workArea) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (workArea == nullptr || name == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(waitingQueueResourcePool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, 0); } int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam) { + if (queue == nullptr || name == nullptr || queueDataResourcePool == nullptr) return ULT_ERROR_NULL; + if (((reinterpret_cast<std::uintptr_t>(queue) | reinterpret_cast<std::uintptr_t>(waitingQueueResourcePool) | reinterpret_cast<std::uintptr_t>(queueDataResourcePool)) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + { + std::lock_guard lock(gMutex); + const auto it = gQueueDataPools.find(queueDataResourcePool); + if (it == gQueueDataPools.end()) return ULT_ERROR_INVALID; + if (dataSize == 0 || dataSize > it->second->_dataSize) return ULT_ERROR_INVALID; + } + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, 0); } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 35fa6b302c..d8bff75850 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -36,6 +36,14 @@ inline int InitializeOptParam(UltOptParam* optParam) { return ULT_OK; } +struct alignas(8) UltCommonOptParam { + std::uint32_t header[3]; + std::uint32_t buildVersion; + std::uint8_t reserved[112]; +}; + +static_assert(sizeof(UltCommonOptParam) == 128); + struct UltWaitingThreadInfo { std::uint64_t size; void* thread; diff --git a/core/libs/tests/GuestUltQueue.cpp b/core/libs/tests/GuestUltQueue.cpp index ffce3c241f..4cfcf13881 100644 --- a/core/libs/tests/GuestUltQueue.cpp +++ b/core/libs/tests/GuestUltQueue.cpp @@ -82,7 +82,7 @@ static void Validation() { CHECK(sceUltQueueTryPop(&fixture.first, &saved) == 0 && saved == value); CHECK(sceUltQueueCreate(&extra, nullptr, 9, nullptr, &fixture.pool, nullptr, 0) == Invalid); CHECK(sceUltQueueCreate(&extra, nullptr, 8, &extra, &fixture.pool, nullptr, 0) == Invalid); - CHECK(sceUltQueueCreate(&extra, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Again); + CHECK(sceUltQueueCreate(&extra, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Busy); CHECK(sceUltQueueCreate(nullptr, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Null); CHECK(sceUltQueueCreate(extra.bytes.data() + 1, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Alignment); CHECK(sceUltQueueCreate(&extra, nullptr, 0, nullptr, &fixture.pool, nullptr, 0) == Range); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 70df002c12..41d47b2622 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -180,11 +180,24 @@ static void UnderscoredCreate() { alignas(8) static std::uint8_t semaphore[256]; alignas(8) static std::uint8_t unknownPool[256]; CHECK(_sceUltWaitingQueueResourcePoolCreate(nullptr, "pool", 1, 1, nullptr, nullptr) == Null); - CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, nullptr, nullptr) == Ok); + alignas(8) std::uint8_t workArea[1024]{}; + alignas(8) std::uint32_t commonOptions[32]{}; + commonOptions[0] = 0x6C696275; + commonOptions[1] = 0x6C746D61; + commonOptions[2] = 0x67696300; + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, nullptr, nullptr) == Null); + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea + 1, nullptr) == Alignment); + commonOptions[31] = 1; + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea, commonOptions) == Invalid); + commonOptions[31] = 0; + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea, commonOptions) == Ok); CHECK(_sceUltSemaphoreCreate(nullptr, "semaphore", 1, pool, nullptr) == Null); CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", -1, pool, nullptr) != Ok); CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, unknownPool, nullptr) != Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr) == Ok); + commonOptions[0] = 0; + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, commonOptions) == Invalid); + commonOptions[0] = 0x6C696275; + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, commonOptions) == Ok); CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr) == State); CHECK(sceUltSemaphoreAcquire(semaphore, 2) == Ok); CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); @@ -232,9 +245,17 @@ static void UnderscoredCreate() { alignas(8) static std::uint8_t dataPool[512]; alignas(8) static std::uint8_t queue[512]; CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr) == Null); - CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr) == Ok); + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr) == Null); + commonOptions[4] = 1; + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, workArea, commonOptions) == Invalid); + commonOptions[4] = 0; + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, workArea, commonOptions) == Ok); CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr) == Null); - CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, nullptr) == Ok); + commonOptions[31] = 1; + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, commonOptions) == Invalid); + commonOptions[31] = 0; + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, reinterpret_cast<std::uint8_t*>(commonOptions) + 1) == Alignment); + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, commonOptions) == Ok); std::uint64_t value = 0x1122334455667788ull, output = 0; CHECK(sceUltQueuePush(queue, &value) == Ok); CHECK(sceUltQueuePop(queue, &output) == Ok && output == value); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index e569ec3ab6..31ae1d7025 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -5,8 +5,7 @@ - [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. -- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction, allocation failure codes, alignment checks and shared slot limits still require comparison with the native node allocator. -- [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) accept and ignore option parameters. Native options contain a 16-byte header and 112 zero reserved bytes; header, alignment and reserved-field validation are not implemented. +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. The nested context in `UltUlthreadInfo` still requires reconstruction. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here From 1196f48d74abf11ec68e363eac470b645ee362e7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:20:44 +0300 Subject: [PATCH 0794/1360] fix(ult): restore semaphore count bits and validation order --- core/libs/prx/libSceUlt/Export.cpp | 23 +++++++++++++---------- core/libs/tests/GuestUltThread.cpp | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 5580ec5b6f..63344b81f8 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -1,5 +1,6 @@ #include <algorithm> #include <atomic> +#include <bit> #include <condition_variable> #include <cstddef> #include <cstdint> @@ -636,7 +637,7 @@ int APS5_VABI sceUltQueueDestroy(void* queue) { return ULT_OK; } -int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { +int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::uint32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { (void)name; (void)optParam; (void)buildVersion; @@ -646,11 +647,8 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int3 if ((reinterpret_cast<std::uintptr_t>(semaphore) & 7u) != 0) { return ULT_ERROR_ALIGNMENT; } - if (numInitialResource < 0) { - return ULT_ERROR_RANGE; - } auto state = std::make_shared<UltSemaphoreState>(); - state->_resources = numInitialResource; + state->_resources = std::bit_cast<std::int32_t>(numInitialResource); state->_waitingPool = waitingQueueResourcePool; std::lock_guard<std::mutex> lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { @@ -665,6 +663,8 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int3 } int APS5_VABI sceUltSemaphoreAcquire(void* semaphore, std::int32_t numResource) { + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numResource <= 0) { return ULT_ERROR_RANGE; } @@ -687,6 +687,8 @@ int APS5_VABI sceUltSemaphoreAcquire(void* semaphore, std::int32_t numResource) } int APS5_VABI sceUltSemaphoreTryAcquire(void* semaphore, std::int32_t numResource) { + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numResource <= 0) { return ULT_ERROR_RANGE; } @@ -706,6 +708,8 @@ int APS5_VABI sceUltSemaphoreTryAcquire(void* semaphore, std::int32_t numResourc } int APS5_VABI sceUltSemaphoreRelease(void* semaphore, std::int32_t numResource) { + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numResource <= 0) { return ULT_ERROR_RANGE; } @@ -728,9 +732,8 @@ int APS5_VABI sceUltSemaphoreRelease(void* semaphore, std::int32_t numResource) } int APS5_VABI sceUltSemaphoreDestroy(void* semaphore) { - if (semaphore == nullptr) { - return ULT_ERROR_NULL; - } + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast<std::uintptr_t>(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; std::lock_guard<std::mutex> globalLock(gMutex); auto it = gSemaphores.find(semaphore); if (it == gSemaphores.end()) { @@ -739,7 +742,7 @@ int APS5_VABI sceUltSemaphoreDestroy(void* semaphore) { auto state = it->second; std::lock_guard<std::mutex> stateLock(state->_mutex); if (state->_waiters != 0) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } state->_alive = false; gSemaphores.erase(it); @@ -908,7 +911,7 @@ int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name return sceUltWaitingQueueResourcePoolCreate(pool, name, numThreads, numSyncObjects, workArea, optParam, 0); } -int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam) { +int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::uint32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam) { if (semaphore == nullptr || name == nullptr) return ULT_ERROR_NULL; if (((reinterpret_cast<std::uintptr_t>(semaphore) | reinterpret_cast<std::uintptr_t>(waitingQueueResourcePool)) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 41d47b2622..e1fb8dbbba 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -24,7 +24,8 @@ int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(void*, std::uint32_t); int APS5_VABI _sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); int APS5_VABI _sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*); int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); -int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::int32_t, void*, const void*); +int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::uint32_t, void*, const void*); +int APS5_VABI sceUltSemaphoreTryAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreRelease(void*, std::int32_t); int APS5_VABI _sceUltMutexOptParamInitialize(void*, std::uint32_t); @@ -192,7 +193,18 @@ static void UnderscoredCreate() { commonOptions[31] = 0; CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea, commonOptions) == Ok); CHECK(_sceUltSemaphoreCreate(nullptr, "semaphore", 1, pool, nullptr) == Null); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", -1, pool, nullptr) != Ok); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", UINT32_MAX, pool, nullptr) == Ok); + CHECK(sceUltSemaphoreTryAcquire(semaphore, 1) == Again); + CHECK(sceUltSemaphoreRelease(semaphore, 1) == Ok); + CHECK(sceUltSemaphoreTryAcquire(semaphore, 1) == Again); + CHECK(sceUltSemaphoreDestroy(semaphore) == Ok); + CHECK(sceUltSemaphoreAcquire(nullptr, 0) == Null); + CHECK(sceUltSemaphoreTryAcquire(nullptr, 0) == Null); + CHECK(sceUltSemaphoreRelease(nullptr, 0) == Null); + CHECK(sceUltSemaphoreAcquire(semaphore + 1, 0) == Alignment); + CHECK(sceUltSemaphoreTryAcquire(semaphore + 1, 0) == Alignment); + CHECK(sceUltSemaphoreRelease(semaphore + 1, 0) == Alignment); + CHECK(sceUltSemaphoreDestroy(semaphore + 1) == Alignment); CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, unknownPool, nullptr) != Ok); commonOptions[0] = 0; CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, commonOptions) == Invalid); From 61a44c79e22377308181abdc4d9b08291cdc2afd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:22:01 +0300 Subject: [PATCH 0795/1360] fix(videoout): restore flip master handle arguments --- core/libs/prx/libSceVideoOut/Unimplemented.cpp | 4 ++-- docs/dev/debt/libSceVideoOut.md | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index dabea7e04a..91b75fbb31 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,12 +7,12 @@ extern "C" { -int APS5_VABI sceVideoOutSetFlipMaster() { +int APS5_VABI sceVideoOutSetFlipMaster(std::int32_t handle, std::int32_t masterHandle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoOutClearFlipMaster() { +int APS5_VABI sceVideoOutClearFlipMaster(std::int32_t handle) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 8b62870d99..56dce0b93f 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,7 +9,6 @@ ### Unknown function info -- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. From fdcf961f5c059eafb2979999409d06cfc208d443 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:23:27 +0300 Subject: [PATCH 0796/1360] fix(pad): restore output report argument registers --- core/libs/prx/libScePad/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index d66adfa338..34ff03cd2b 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -254,7 +254,7 @@ int APS5_VABI scePadGetFeatureReport(std::int32_t handle, std::uint8_t reportId, int APS5_VABI scePadSetFeatureReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadOutputReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; From 8de11f73eecd5398f3d276c3a41e2c9c182e1fd8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:27:03 +0300 Subject: [PATCH 0797/1360] fix(audiodec): validate aac sizes and extended mode ranges --- core/libs/prx/libSceAudiodec/Export.cpp | 18 ++++++++++--- core/libs/tests/GuestAudiodec.cpp | 34 +++++++++++++++++++++++++ docs/dev/debt/libSceAudiodec.md | 2 ++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index ffc1d6e1f7..5e2b911800 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -1,6 +1,8 @@ #include <cstdint> #include <cstddef> #include <array> +#include <cstring> +#include <stdexcept> #include <memory> #include <mutex> #include "SceTypes.hpp" @@ -88,7 +90,7 @@ std::int32_t validateCtrl(const AudiodecCtrl* ctrl, std::uint32_t codecType) { if (infoSize != sizeof(AudiodecMp3Info)) return ERROR_INVALID_BSI_INFO_SIZE; break; default: - if (paramSize < sizeof(AudiodecParamM4aac)) return ERROR_INVALID_PARAM_SIZE; + if (paramSize != sizeof(AudiodecParamM4aac) && paramSize != 28 && paramSize != 32) return ERROR_INVALID_PARAM_SIZE; if (infoSize != sizeof(AudiodecM4aacInfo)) return ERROR_INVALID_BSI_INFO_SIZE; break; } @@ -133,12 +135,20 @@ void fillMp3Info(AudiodecMp3Info* info, const Audiodec::Mp3Header& header) { } std::int32_t createAac(const AudiodecParamM4aac* param, AudiodecM4aacInfo* info, Instance& instance) { + std::uint32_t extensionMode = 0; + std::uint32_t outputMode = 0; + const auto* bytes = reinterpret_cast<const std::byte*>(param); + if (param->ui_size >= 28) std::memcpy(&extensionMode, bytes + 24, sizeof(extensionMode)); + if (extensionMode > 1) return static_cast<std::int32_t>(0x807F0305); + if (param->ui_size == 32) std::memcpy(&outputMode, bytes + 28, sizeof(outputMode)); + if (outputMode > 2) return static_cast<std::int32_t>(0x807F0306); + if (extensionMode != 0 || outputMode != 0) throw std::runtime_error("M4AAC extended decoding modes are not implemented"); if (param->ui_config_number != M4AAC_CONFIG_ADTS && param->ui_config_number != M4AAC_CONFIG_RAW) return ERROR_M4AAC_INVALID_CONFIG_NUMBER; const std::uint32_t sampleRate = Audiodec::AacSampleRate(param->ui_sampling_freq_index); if (param->ui_config_number == M4AAC_CONFIG_RAW && sampleRate == 0) return ERROR_M4AAC_INVALID_SAMPLING_FREQ; - if (param->ui_max_channels > 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; - if (param->ui_enable_heaac > 1) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; - const std::uint32_t channels = param->ui_max_channels == 0 ? 2 : param->ui_max_channels; + if (param->ui_max_channels != 1 && param->ui_max_channels != 2 && param->ui_max_channels != 6 && param->ui_max_channels != 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; + if (param->ui_enable_heaac > 1 || (param->ui_enable_heaac == 1 && param->ui_max_channels > 2)) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; + const std::uint32_t channels = param->ui_max_channels; instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels, param->ui_enable_heaac == 1); instance.channels = channels; instance.sampleRate = sampleRate; diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 404e7fce05..3eb7a370db 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -5,6 +5,7 @@ #include <cstdlib> #include <cstring> #include <numbers> +#include <stdexcept> #include <vector> extern "C" { @@ -410,6 +411,39 @@ int main() { aac.i_bw_pcm = 2; const std::int32_t floatAac = sceAudiodecCreateDecoder(&aacCtrl, 3); Require(floatAac > 0 && sceAudiodecDeleteDecoder(floatAac) == 0); + for (const std::uint32_t channels : {0u, 3u, 4u, 5u, 7u, 9u}) { + aac.ui_max_channels = channels; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast<std::int32_t>(0x807F0304)); + } + aac.ui_max_channels = 6; + aac.ui_enable_heaac = 1; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast<std::int32_t>(0x807F0302)); + struct ExtendedParam { + AudiodecParamM4aac base; + std::uint32_t extensionMode; + std::uint32_t outputMode; + } extended{{32, 1, 1, 3, 2, 0}, 0, 0}; + aacCtrl.pParam = &extended; + for (const std::uint32_t size : {25u, 27u, 29u, 31u, 33u}) { + extended.base.ui_size = size; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == errorInvalidParamSize); + } + for (const std::uint32_t size : {28u, 32u}) { + extended.base.ui_size = size; + extended.extensionMode = 2; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast<std::int32_t>(0x807F0305)); + extended.extensionMode = 1; + bool threw = false; + try { sceAudiodecCreateDecoder(&aacCtrl, 3); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + extended.extensionMode = 0; + const auto extendedHandle = sceAudiodecCreateDecoder(&aacCtrl, 3); + Require(extendedHandle > 0); + Require(sceAudiodecDeleteDecoder(extendedHandle) == 0); + } + extended.outputMode = 3; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast<std::int32_t>(0x807F0306)); const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 2); Require(handle > 0); diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 15c8c1cbe1..ac9b492ea4 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -2,6 +2,8 @@ ### Unknown function info +- [M4AAC extended parameters](../../../core/libs/prx/libSceAudiodec/Export.cpp): 28-byte and 32-byte parameter variants add fields at offsets 24 and 28. Their accepted ranges are 0..1 and 0..2; nonzero modes throw until the corresponding decoder configuration is implemented. Zero extensions use the base decoding configuration. + - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the HE-AAC handling follows one console measurement of an HE-AAC v2 ADTS stream: SBR output at 48 kHz in one channel with `ui_enable_heaac` 1, the 24 kHz mono core with 0. Not measured: whether the console, like FFmpeg, decodes a stream without SBR data in its first access unit as AAC-LC with `ui_enable_heaac` 1, and what it reports in `ui_sampling_freq` and `ui_heaac` with `ui_enable_heaac` 0 (here the rate of the returned PCM and 0) From 5a2274e2412e9ea5bef23d0d3a1810f4ccfd3871 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:41:45 +0300 Subject: [PATCH 0798/1360] fix(dialog): validate active users before opening --- core/libs/prx/libSceCdlgG2P/CMakeLists.txt | 3 ++- core/libs/prx/libSceCdlgG2P/Export.cpp | 5 +++++ .../prx/libSceCdlgPlayerReview/CMakeLists.txt | 3 ++- core/libs/prx/libSceCdlgPlayerReview/Export.cpp | 5 +++++ core/libs/tests/GuestG2PDialog.cpp | 14 ++++++++++++-- core/libs/tests/GuestPlayerReviewDialog.cpp | 15 +++++++++++++-- docs/dev/debt/libSceCdlgG2P.md | 2 +- docs/dev/debt/libSceCdlgPlayerReview.md | 2 +- 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceCdlgG2P/CMakeLists.txt b/core/libs/prx/libSceCdlgG2P/CMakeLists.txt index 9809109f69..f15414c733 100644 --- a/core/libs/prx/libSceCdlgG2P/CMakeLists.txt +++ b/core/libs/prx/libSceCdlgG2P/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceCdlgG2P SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceCdlgG2P PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceCdlgG2P) +target_link_libraries(libSceCdlgG2P PRIVATE libSceUserService) if(BUILD_TESTING) add_test_executable(guest_g2p_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestG2PDialog.cpp) target_include_directories(guest_g2p_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_g2p_dialog_tests PRIVATE libSceCdlgG2P) + target_link_libraries(guest_g2p_dialog_tests PRIVATE libSceCdlgG2P libSceUserService) add_test(NAME guest_g2p_dialog COMMAND guest_g2p_dialog_tests) if(WIN32) set_tests_properties(guest_g2p_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceCdlgG2P/Export.cpp b/core/libs/prx/libSceCdlgG2P/Export.cpp index 6f402b651d..43abff189e 100644 --- a/core/libs/prx/libSceCdlgG2P/Export.cpp +++ b/core/libs/prx/libSceCdlgG2P/Export.cpp @@ -1,3 +1,4 @@ +#include "SceTypes.hpp" #include <atomic> #include <cstdint> #include <cstddef> @@ -6,6 +7,8 @@ #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -38,6 +41,8 @@ int APS5_VABI sceG2PDialogOpen(const G2PDialogParam* param) { if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; if (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::none_of(std::begin(users.user_id), std::end(users.user_id), [param](std::int32_t userId) { return userId != -1 && userId == param->userId; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt b/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt index 7b4466af1a..f3aa2fabdc 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt +++ b/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceCdlgPlayerReview SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceCdlgPlayerReview PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceCdlgPlayerReview) +target_link_libraries(libSceCdlgPlayerReview PRIVATE libSceUserService) if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) @@ -11,7 +12,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_player_review_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerReviewDialog.cpp) target_include_directories(guest_player_review_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_player_review_dialog_tests PRIVATE libSceCdlgPlayerReview) + target_link_libraries(guest_player_review_dialog_tests PRIVATE libSceCdlgPlayerReview libSceUserService) add_test(NAME guest_player_review_dialog COMMAND guest_player_review_dialog_tests) if(WIN32) set_tests_properties(guest_player_review_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index 2380fd4630..4db4e1760e 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -1,9 +1,12 @@ +#include "SceTypes.hpp" #include <atomic> #include <cstdint> #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { std::atomic<int> g_status{0}; @@ -42,6 +45,8 @@ int APS5_VABI scePlayerReviewDialogOpen(const PlayerReviewDialogParam* param) { if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; if (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; }) || param->mode > 1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::none_of(std::begin(users.user_id), std::end(users.user_id), [param](std::int32_t userId) { return userId != -1 && userId == param->userId; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index 9a655706a9..a318e77f68 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -1,9 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> +#include "prx/libSceUserService/UserService.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include <cstdlib> extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceG2PDialogInitialize(void); int APS5_VABI sceG2PDialogOpen(const void* param); int APS5_VABI sceG2PDialogUpdateStatus(void); @@ -29,11 +32,12 @@ constexpr int kResultUserCanceled = 1; } int main() { + Require(sceUserServiceInitialize(nullptr) == 0); G2PDialogParam param{}; param.base.size = sizeof(param.base); param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; param.size = sizeof(param); - param.userId = 1; + param.userId = USER_SERVICE_INITIAL_USER_ID; std::int32_t result[4] = {-1, -1, -1, -1}; Require(sceG2PDialogTerminate() == kErrNotInitialized); @@ -67,7 +71,12 @@ int main() { param.reserved[63] = 0; param.userId = -1; Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); - param.userId = 1; + param.userId = 42; + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + Require(sceUserServiceTerminate() == 0); + Require(sceG2PDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + Require(sceUserServiceInitialize(nullptr) == 0); Require(sceG2PDialogOpen(¶m) == 0); Require(sceG2PDialogGetStatus() == kStatusFinished); Require(sceG2PDialogUpdateStatus() == kStatusFinished); @@ -92,4 +101,5 @@ int main() { Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == -1); Require(sceG2PDialogTerminate() == 0); + Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index cb6c65a423..45621694d4 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -1,9 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> +#include "prx/libSceUserService/UserService.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include <cstdlib> extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI scePlayerReviewDialogInitialize(void); int APS5_VABI scePlayerReviewDialogTerminate(void); int APS5_VABI scePlayerReviewDialogOpen(const void* param); @@ -28,11 +31,12 @@ void Require(bool value) { if (!value) std::abort(); } } int main() { + Require(sceUserServiceInitialize(nullptr) == 0); PlayerReviewDialogParam param{}; param.base.size = sizeof(param.base); param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; param.size = sizeof(param); - param.userId = 1; + param.userId = USER_SERVICE_INITIAL_USER_ID; std::int32_t result[4] = {-1, -1, -1, -1}; Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); @@ -68,10 +72,16 @@ int main() { param.reserved[63] = 0; param.userId = -1; Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); - param.userId = 1; + param.userId = USER_SERVICE_INITIAL_USER_ID; param.mode = 2; Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); param.mode = 0; + param.userId = 42; + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + Require(sceUserServiceTerminate() == 0); + Require(scePlayerReviewDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + Require(sceUserServiceInitialize(nullptr) == 0); Require(scePlayerReviewDialogOpen(¶m) == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); @@ -95,4 +105,5 @@ int main() { Require(result[2] == -1); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceUserServiceTerminate() == 0); } diff --git a/docs/dev/debt/libSceCdlgG2P.md b/docs/dev/debt/libSceCdlgG2P.md index f35f20d05d..ad18657b3d 100644 --- a/docs/dev/debt/libSceCdlgG2P.md +++ b/docs/dev/debt/libSceCdlgG2P.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceCdlgG2P](../../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The 128-byte open parameters, common-header validation and result field at byte 8 are recovered. Active-user verification and native service state transitions remain unimplemented; PPSA01289 imports it +- [libSceCdlgG2P](../../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The 128-byte open parameters, common-header validation and result field at byte 8 are recovered. Native service state transitions remain unimplemented; PPSA01289 imports it diff --git a/docs/dev/debt/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md index ae7ee87b57..0e97a3d72d 100644 --- a/docs/dev/debt/libSceCdlgPlayerReview.md +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -2,7 +2,7 @@ ### Unknown function info -- [scePlayerReviewDialogOpen](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) uses a recovered 168-byte parameter layout; the 40-byte target record at byte 60 and mode semantics remain unresolved. Active-user verification is not implemented. +- [scePlayerReviewDialogOpen](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) uses a recovered 168-byte parameter layout; the 40-byte target record at byte 60 and mode semantics remain unresolved. ### Functional From 266764fba740cf3ef302feb8e9043acf34856360 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:41:46 +0300 Subject: [PATCH 0799/1360] fix(vrtracker): restore result query parameter fields --- core/libs/prx/libSceVrTracker/Export.cpp | 16 +++++++++++++++- docs/dev/debt/libSceVrTracker.md | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index 6ca29bfe49..14776d3483 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -51,7 +51,17 @@ struct VrTrackerGpuWaitParam { struct VrTrackerGetResultParam { std::uint32_t size; - std::byte payload[52]; + std::int32_t handle; + std::uint32_t mode; + std::uint32_t extendedResult; + std::uint64_t time; + std::uint32_t statusMode; + std::uint32_t reserved0; + std::uint32_t option32; + std::byte outputValue[4]; + std::uint32_t option40; + std::uint32_t reserved1[2]; + std::byte unused[4]; }; static_assert(sizeof(VrTrackerCpuProcessParam) == 32); @@ -59,6 +69,10 @@ static_assert(offsetof(VrTrackerCpuProcessParam, mode) == 4); static_assert(offsetof(VrTrackerCpuProcessParam, argument) == 8); static_assert(sizeof(VrTrackerGpuWaitParam) == 32); static_assert(sizeof(VrTrackerGetResultParam) == 56); +static_assert(offsetof(VrTrackerGetResultParam, time) == 16); +static_assert(offsetof(VrTrackerGetResultParam, statusMode) == 24); +static_assert(offsetof(VrTrackerGetResultParam, outputValue) == 36); +static_assert(offsetof(VrTrackerGetResultParam, reserved1) == 44); extern "C" { diff --git a/docs/dev/debt/libSceVrTracker.md b/docs/dev/debt/libSceVrTracker.md index da28022685..138b88a370 100644 --- a/docs/dev/debt/libSceVrTracker.md +++ b/docs/dev/debt/libSceVrTracker.md @@ -2,7 +2,7 @@ ### Unknown function info -- [VrTracker parameters](../../../core/libs/prx/libSceVrTracker/Export.cpp): argument slots are recovered. CPU processing and GPU waiting consume 32-byte records, result queries consume 56-byte records, and time queries write 64 bits. Initialization accepts 128-byte and 144-byte versions; the 144-byte layout includes three address/size/alignment records, but configuration selector semantics and the older field layout remain unresolved. GPU submission accepts 552-byte and 648-byte versions; submission and result records still require field recovery; result storage has 1520-byte and 3640-byte variants. CPU mode/argument semantics and the remaining query fields are unresolved. Protected bodies remain unchanged. +- [VrTracker parameters](../../../core/libs/prx/libSceVrTracker/Export.cpp): argument slots are recovered. CPU processing and GPU waiting consume 32-byte records, result queries consume 56-byte records, and time queries write 64 bits. Initialization accepts 128-byte and 144-byte versions; the 144-byte layout includes three address/size/alignment records, but configuration selector semantics and the older field layout remain unresolved. GPU submission accepts 552-byte and 648-byte versions; submission and result records still require field recovery; result storage has 1520-byte and 3640-byte variants. Result-query records have a signed handle, 64-bit time, 32-bit selectors and an extended-result flag; selector meanings and the four-byte value copied unchanged to result offset 448 remain unresolved. CPU mode/argument semantics are unresolved. Protected bodies remain unchanged. - [sceVrTrackerCpuPopMarker, sceVrTrackerCpuPushMarker, sceVrTrackerGetLiveCaptureId, sceVrTrackerGpuWaitAndCpuProcess, sceVrTrackerResetAll, sceVrTrackerSaveInternalBuffers and sceVrTrackerSetDeviceRejection](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. - [sceVrTrackerSetExtendedMode, sceVrTrackerSetLEDBrightness, sceVrTrackerSetRestingMode, sceVrTrackerStartLiveCapture, sceVrTrackerStopLiveCapture and sceVrTrackerTerm](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. - [sceVrTrackerGetPlayAreaWarningInfo, sceVrTrackerNotifyEndOfCpuProcess, sceVrTrackerQueryMemory, sceVrTrackerRecalibrate, sceVrTrackerRegisterDevice and sceVrTrackerRegisterDevice2](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. From 464470f7ae1ea431955ec513f66bf66b7fcdbf45 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:41:47 +0300 Subject: [PATCH 0800/1360] fix(npcommerce): restore extended dialog parameter layout --- .../prx/libSceCommonDialog/CommonDialog.hpp | 20 +++++++++++++++++++ core/libs/prx/libSceNpCommerce/Export.cpp | 4 ++-- core/libs/tests/GuestNpCommerceDialog.cpp | 12 ++++++++--- docs/dev/debt/libSceNpCommerce.md | 2 +- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index d16c6ad73d..c2f5d66d63 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -29,6 +29,26 @@ struct PlayerReviewDialogParam { std::uint8_t reserved[64]; }; +struct NpCommerceDialogParam2 { + CommonDialogBaseParam base; + std::uint32_t size; + std::int32_t userId; + std::uint32_t mode; + std::uint32_t option; + std::uint32_t contentMode; + std::uint32_t padding0; + const char* const* products; + std::uint32_t productCount; + std::uint32_t padding1; + std::uint64_t argument; + std::uint64_t userData; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpCommerceDialogParam2) == 136); +static_assert(offsetof(NpCommerceDialogParam2, products) == 72); +static_assert(offsetof(NpCommerceDialogParam2, userData) == 96); + struct WebBrowserDialogExtendedResult { std::uint64_t size; std::uint32_t type; diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 2fd73aeb90..60f96cb34c 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -2,6 +2,7 @@ #include <cstddef> #include <cstring> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -11,7 +12,6 @@ constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80006u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B80009u); @@ -79,7 +79,7 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { return 0; } -int APS5_VABI sceNpCommerceDialogOpen2(void) { +int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param) { g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 7b7239959f..298022419c 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include <cstdint> #include <cstdlib> @@ -7,7 +8,7 @@ extern "C" { int APS5_VABI sceNpCommerceDialogInitialize(); int APS5_VABI sceNpCommerceDialogOpen(const void* param); -int APS5_VABI sceNpCommerceDialogOpen2(void); +int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param); int APS5_VABI sceNpCommerceDialogUpdateStatus(void); int APS5_VABI sceNpCommerceDialogGetStatus(void); int APS5_VABI sceNpCommerceDialogGetResult(void* result); @@ -38,11 +39,16 @@ void RequireCanceledResult() { } int main() { + NpCommerceDialogParam2 param2{}; + param2.base.size = sizeof(param2.base); + param2.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m2)) - 1060003575u; + param2.size = sizeof(param2); + param2.userId = 1; Require(sceNpCommerceDialogGetStatus() == statusNone); Require(sceNpCommerceDialogInitialize() == 0); Require(sceNpCommerceDialogGetStatus() == statusInitialized); - Require(sceNpCommerceDialogOpen2() == 0); + Require(sceNpCommerceDialogOpen2(¶m2) == 0); Require(sceNpCommerceDialogUpdateStatus() == statusFinished); Require(sceNpCommerceDialogGetStatus() == statusFinished); RequireCanceledResult(); @@ -55,7 +61,7 @@ int main() { alignas(8) std::uint8_t param[0x40] = {}; Require(sceNpCommerceDialogInitialize() == 0); Require(sceNpCommerceDialogOpen(param) == 0); - Require(sceNpCommerceDialogOpen2() == 0); + Require(sceNpCommerceDialogOpen2(¶m2) == 0); Require(sceNpCommerceDialogUpdateStatus() == statusFinished); RequireCanceledResult(); Require(sceNpCommerceDialogTerminate() == 0); diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index d9ac7be185..b918bd1717 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -7,4 +7,4 @@ ### Unknown function info - [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. -- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - declared without parameters; follows [prosper](https://github.com/mattias800/prosper/blob/73413cb445b0364bb49c4d2478e9ee1614b9f7d9/prosper/src/hle/np/np.cpp#L500-L504), which serves it with its `sceNpCommerceDialogOpen` handler: every argument is ignored, nothing is validated, it returns 0 and the dialog finishes at once, so `sceNpCommerceDialogGetResult` reports it cancelled. The parameters and the console's error results are unknown. PPSA34501 calls it at Zombies > Online +- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): the single pointer argument and 136-byte layout are recovered, including the product pointer array, count and result user-data value. User checks, mode validation, SDK-dependent restrictions and native error results remain unimplemented; the host dialog still finishes immediately as cancelled. Mode-specific meanings of the option and 64-bit argument remain unresolved. From 8ff0554ff4dc6431deae6fc2ab2a6c299225c9ad Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:42:45 +0300 Subject: [PATCH 0801/1360] fix(npcommerce): restore dialog lifecycle errors --- core/libs/prx/libSceNpCommerce/Export.cpp | 10 ++++++++-- core/libs/tests/GuestNpCommerceDialog.cpp | 10 +++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 60f96cb34c..245233008e 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -9,11 +9,14 @@ 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_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80006u); -constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B80009u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); + +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); int g_status = COMMON_DIALOG_STATUS_NONE; @@ -45,6 +48,7 @@ int APS5_VABI sceNpCommerceDialogGetStatus(void) { int APS5_VABI sceNpCommerceDialogGetResult(void* result) { if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (g_status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; // SceNpCommerceDialogResult: int32 result, bool authorized, then reserved bytes. std::int32_t value = COMMON_DIALOG_RESULT_USER_CANCELED; @@ -56,11 +60,13 @@ int APS5_VABI sceNpCommerceDialogGetResult(void* result) { int APS5_VABI sceNpCommerceDialogClose(void) { if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceNpCommerceDialogTerminate() { + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; g_status = COMMON_DIALOG_STATUS_NONE; return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 298022419c..687a158fa1 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -45,7 +45,15 @@ int main() { param2.size = sizeof(param2); param2.userId = 1; Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogTerminate() == static_cast<int>(0x80B80003u)); + Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B80003u)); + Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); + std::uint8_t result[48]{}; + Require(sceNpCommerceDialogGetResult(result) == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B8000Bu)); + Require(sceNpCommerceDialogGetResult(result) == static_cast<int>(0x80B80005u)); + Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); Require(sceNpCommerceDialogGetStatus() == statusInitialized); Require(sceNpCommerceDialogOpen2(¶m2) == 0); @@ -53,7 +61,7 @@ int main() { Require(sceNpCommerceDialogGetStatus() == statusFinished); RequireCanceledResult(); - Require(sceNpCommerceDialogClose() == 0); + Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceNpCommerceDialogGetStatus() == statusFinished); Require(sceNpCommerceDialogTerminate() == 0); Require(sceNpCommerceDialogGetStatus() == statusNone); From 878e5bdd5d549ca2b1a1dbdb6bdd794d219e4fbe Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:44:08 +0300 Subject: [PATCH 0802/1360] fix(npcommerce): validate dialog base parameters --- .../prx/libSceCommonDialog/CommonDialog.hpp | 18 +++++++++++++++ core/libs/prx/libSceNpCommerce/Export.cpp | 9 ++++++-- core/libs/tests/GuestNpCommerceDialog.cpp | 22 ++++++++++++++++--- docs/dev/debt/libSceNpCommerce.md | 2 +- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index c2f5d66d63..a9bea03571 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -29,6 +29,24 @@ struct PlayerReviewDialogParam { std::uint8_t reserved[64]; }; +struct NpCommerceDialogParam { + CommonDialogBaseParam base; + std::uint32_t size; + std::int32_t userId; + std::uint32_t mode; + std::uint32_t option; + const char* const* products; + std::uint32_t productCount; + std::uint32_t padding; + std::uint64_t argument; + std::uint64_t userData; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpCommerceDialogParam) == 128); +static_assert(offsetof(NpCommerceDialogParam, products) == 64); +static_assert(offsetof(NpCommerceDialogParam, userData) == 88); + struct NpCommerceDialogParam2 { CommonDialogBaseParam base; std::uint32_t size; diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 245233008e..23b6481670 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -16,6 +16,7 @@ constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); int g_status = COMMON_DIALOG_STATUS_NONE; @@ -31,9 +32,10 @@ int APS5_VABI sceNpCommerceDialogInitialize() { return 0; } -int APS5_VABI sceNpCommerceDialogOpen(const void* param) { - if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; +int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param) { if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -86,6 +88,9 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { } int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 687a158fa1..16f5bf3065 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -7,7 +7,7 @@ extern "C" { int APS5_VABI sceNpCommerceDialogInitialize(); -int APS5_VABI sceNpCommerceDialogOpen(const void* param); +int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param); int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param); int APS5_VABI sceNpCommerceDialogUpdateStatus(void); int APS5_VABI sceNpCommerceDialogGetStatus(void); @@ -45,6 +45,15 @@ int main() { param2.size = sizeof(param2); param2.userId = 1; Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); + Require(sceNpCommerceDialogOpen2(nullptr) == static_cast<int>(0x80B8000Du)); + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B80003u)); + param2.base.magic = 0; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); + param2.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m2)) - 1060003575u; + param2.base.reserved[35] = 1; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); + param2.base.reserved[35] = 0; Require(sceNpCommerceDialogTerminate() == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); @@ -66,9 +75,16 @@ int main() { Require(sceNpCommerceDialogTerminate() == 0); Require(sceNpCommerceDialogGetStatus() == statusNone); - alignas(8) std::uint8_t param[0x40] = {}; + NpCommerceDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = 1; Require(sceNpCommerceDialogInitialize() == 0); - Require(sceNpCommerceDialogOpen(param) == 0); + param.base.size = 0; + Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.base.size = sizeof(param.base); + Require(sceNpCommerceDialogOpen(¶m) == 0); Require(sceNpCommerceDialogOpen2(¶m2) == 0); Require(sceNpCommerceDialogUpdateStatus() == statusFinished); RequireCanceledResult(); diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index b918bd1717..ca3ca354d6 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -7,4 +7,4 @@ ### Unknown function info - [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. -- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): the single pointer argument and 136-byte layout are recovered, including the product pointer array, count and result user-data value. User checks, mode validation, SDK-dependent restrictions and native error results remain unimplemented; the host dialog still finishes immediately as cancelled. Mode-specific meanings of the option and 64-bit argument remain unresolved. +- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): the single pointer argument and 136-byte layout are recovered, including the product pointer array, count and result user-data value. User checks, mode validation, process-version-dependent restrictions and result-buffer semantics remain unimplemented; the host dialog still finishes immediately as cancelled. Mode-specific meanings of the option and 64-bit argument remain unresolved. From eeabc8d176892f2374c821f1f6527851e9557cb2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:45:44 +0300 Subject: [PATCH 0803/1360] fix(npcommerce): validate product modes and sdk constraints --- core/libs/prx/libSceNpCommerce/CMakeLists.txt | 3 +- core/libs/prx/libSceNpCommerce/Export.cpp | 37 +++++++++++ core/libs/tests/GuestNpCommerceDialog.cpp | 66 ++++++++++++++++++- docs/dev/debt/libSceNpCommerce.md | 2 + 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpCommerce/CMakeLists.txt b/core/libs/prx/libSceNpCommerce/CMakeLists.txt index ec34bf1230..1c59d83752 100644 --- a/core/libs/prx/libSceNpCommerce/CMakeLists.txt +++ b/core/libs/prx/libSceNpCommerce/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceNpCommerce SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNpCommerce PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNpCommerce) +target_link_libraries(libSceNpCommerce PRIVATE libc libSceUserService) if(BUILD_TESTING) add_test_executable(guest_np_commerce_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCommerceDialog.cpp) target_include_directories(guest_np_commerce_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_commerce_dialog_tests PRIVATE libSceNpCommerce libc) + target_link_libraries(guest_np_commerce_dialog_tests PRIVATE libSceNpCommerce libc libSceUserService) configure_windows_unwind(guest_np_commerce_dialog_tests) add_test(NAME guest_np_commerce_dialog COMMAND guest_np_commerce_dialog_tests) if(WIN32) diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 23b6481670..213732b38a 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -1,10 +1,13 @@ #include <cstdint> #include <cstddef> #include <cstring> +#include "prx/libc/include/ApplicationHeap.hpp" #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -21,6 +24,39 @@ constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); int g_status = COMMON_DIALOG_STATUS_NONE; +std::uint32_t compiledSdkVersion() { + const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version; +} + +bool isLoggedIn(std::int32_t userId) { + UserServiceLoginUserIdList users{}; + return sceUserServiceGetLoginUserIdList(&users) == 0 && userId != -1 && std::find(std::begin(users.user_id), std::end(users.user_id), userId) != std::end(users.user_id); +} + +bool validateParam(const NpCommerceDialogParam& param) { + const auto sdkVersion = compiledSdkVersion(); + if (sdkVersion >= 0x01700000 && (param.size != sizeof(param) || !std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; }) || !isLoggedIn(param.userId) || param.option > 7)) return false; + switch (param.mode) { + case 0: + return param.productCount <= 1 && (param.products != nullptr) == (param.productCount != 0); + case 1: + return param.productCount == 1 && param.products != nullptr; + case 2: + return param.productCount <= 1 && (param.productCount != 0 ? param.products != nullptr : sdkVersion < 0x01700000 || param.products == nullptr); + case 3: + return param.productCount >= 1 && param.productCount <= 10 && param.products != nullptr; + case 4: + return param.productCount <= 10 && (param.productCount != 0 ? param.products != nullptr : sdkVersion < 0x01700000 || param.products == nullptr); + case 5: + return param.productCount == 0 && (sdkVersion >= 0x03500000 ? param.argument == 1 : (param.argument & 3) != 0 && (sdkVersion < 0x01700000 || param.argument < 4)); + default: + return false; + } +} + } extern "C" { @@ -36,6 +72,7 @@ int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param) { if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (!validateParam(*param)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 16f5bf3065..bc0b204659 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -1,3 +1,6 @@ +#include "ProcessParametersFixture.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include <array> #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" @@ -6,6 +9,8 @@ #include <cstring> extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceNpCommerceDialogInitialize(); int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param); int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param); @@ -20,6 +25,8 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +alignas(16) std::array<std::byte, 64> process{}; + constexpr int statusNone = 0; constexpr int statusInitialized = 1; constexpr int statusFinished = 3; @@ -39,6 +46,10 @@ void RequireCanceledResult() { } int main() { + Require(ProcessParametersFixture::Install(process.data(), process.size())); + std::uint32_t sdkVersion = 0x03500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceUserServiceInitialize(nullptr) == 0); NpCommerceDialogParam2 param2{}; param2.base.size = sizeof(param2.base); param2.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m2)) - 1060003575u; @@ -79,16 +90,69 @@ int main() { param.base.size = sizeof(param.base); param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; param.size = sizeof(param); - param.userId = 1; + param.userId = USER_SERVICE_INITIAL_USER_ID; Require(sceNpCommerceDialogInitialize() == 0); param.base.size = 0; Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); param.base.size = sizeof(param.base); + param.size = 0; + Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.size = sizeof(param); + param.option = 8; + Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.option = 0; + param.reserved[31] = 1; + Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.reserved[31] = 0; + param.userId = -1; + Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + const char* products[11]{}; + for (std::uint32_t mode = 0; mode <= 6; ++mode) { + param.mode = mode; + for (std::uint32_t count = 0; count <= 11; ++count) { + param.productCount = count; + for (bool hasProducts : {false, true}) { + param.products = hasProducts ? products : nullptr; + bool valid = false; + if (mode == 0 || mode == 2) valid = count <= 1 && hasProducts == (count != 0); + if (mode == 1) valid = count == 1 && hasProducts; + if (mode == 3) valid = count >= 1 && count <= 10 && hasProducts; + if (mode == 4) valid = count <= 10 && hasProducts == (count != 0); + Require(sceNpCommerceDialogOpen(¶m) == (valid ? 0 : static_cast<int>(0x80B8000Au))); + } + } + } + for (std::uint32_t mode : {1000u, 1001u}) { + param.mode = mode; + param.productCount = 0; + param.argument = 1; + Require(sceNpCommerceDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + } + param.mode = 5; + param.productCount = 0; + param.products = nullptr; + for (std::uint64_t argument = 0; argument <= 4; ++argument) { + param.argument = argument; + Require(sceNpCommerceDialogOpen(¶m) == (argument == 1 ? 0 : static_cast<int>(0x80B8000Au))); + } + sdkVersion = 0x01700000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + param.argument = 3; + Require(sceNpCommerceDialogOpen(¶m) == 0); + sdkVersion = 0x01600000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + param.size = 0; + param.option = 100; + param.userId = -1; + param.reserved[31] = 1; + param.argument = 7; Require(sceNpCommerceDialogOpen(¶m) == 0); Require(sceNpCommerceDialogOpen2(¶m2) == 0); Require(sceNpCommerceDialogUpdateStatus() == statusFinished); RequireCanceledResult(); Require(sceNpCommerceDialogTerminate() == 0); Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceUserServiceTerminate() == 0); return 0; } diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index ca3ca354d6..9e423c84b4 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -8,3 +8,5 @@ - [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. - [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): the single pointer argument and 136-byte layout are recovered, including the product pointer array, count and result user-data value. User checks, mode validation, process-version-dependent restrictions and result-buffer semantics remain unimplemented; the host dialog still finishes immediately as cancelled. Mode-specific meanings of the option and 64-bit argument remain unresolved. + +- [sceNpCommerceDialogOpen](../../../core/libs/prx/libSceNpCommerce/Export.cpp): Product copying and service submission remain part of the cancelled UI stub. From 4e67d2380d41544587e3f0a3e95f8828daf983bc Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:47:44 +0300 Subject: [PATCH 0804/1360] fix(npcommerce): validate extended dialog modes --- core/libs/prx/libSceNpCommerce/Export.cpp | 34 +++++++++++++++++++++ core/libs/tests/GuestNpCommerceDialog.cpp | 37 ++++++++++++++++++++++- docs/dev/debt/libSceNpCommerce.md | 2 +- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 213732b38a..b77de4d0b5 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -1,6 +1,7 @@ #include <cstdint> #include <cstddef> #include <cstring> +#include <stdexcept> #include "prx/libc/include/ApplicationHeap.hpp" #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" @@ -57,6 +58,38 @@ bool validateParam(const NpCommerceDialogParam& param) { } } +bool validateParam2(const NpCommerceDialogParam2& param) { + if (!isLoggedIn(param.userId) || param.size != sizeof(param)) return false; + if (param.option >= 8 && (param.contentMode == 0 || (param.contentMode == 1 && param.option >= 100))) return false; + if (!std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; })) return false; + bool valid = false; + switch (param.mode) { + case 0: + valid = param.contentMode < 2 && param.productCount <= 1 && (param.products != nullptr) == (param.productCount != 0); + break; + case 1: + valid = param.contentMode < 2 && param.productCount == 1 && param.products != nullptr; + break; + case 2: + valid = param.productCount <= 1 && (param.products != nullptr) == (param.productCount != 0); + break; + case 3: + valid = param.contentMode < 2 && param.productCount >= 1 && param.productCount <= 10 && param.products != nullptr; + break; + case 4: + valid = param.contentMode < 2 && param.productCount <= 10 && (param.products != nullptr) == (param.productCount != 0); + break; + case 5: + valid = param.productCount == 0 && param.argument == 1; + break; + default: + return false; + } + if (!valid) return false; + if (param.contentMode != 0 && (param.contentMode != 1 || param.mode != 1)) throw std::runtime_error("commerce dialog: extended content process version is not implemented"); + return true; +} + } extern "C" { @@ -128,6 +161,7 @@ int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param) { if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (!validateParam2(*param)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index bc0b204659..1b2a6c9ed6 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -1,6 +1,7 @@ #include "ProcessParametersFixture.hpp" #include "prx/libSceUserService/UserService.hpp" #include <array> +#include <stdexcept> #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" @@ -54,7 +55,7 @@ int main() { param2.base.size = sizeof(param2.base); param2.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m2)) - 1060003575u; param2.size = sizeof(param2); - param2.userId = 1; + param2.userId = USER_SERVICE_INITIAL_USER_ID; Require(sceNpCommerceDialogGetStatus() == statusNone); Require(sceNpCommerceDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); Require(sceNpCommerceDialogOpen2(nullptr) == static_cast<int>(0x80B8000Du)); @@ -120,9 +121,43 @@ int main() { if (mode == 3) valid = count >= 1 && count <= 10 && hasProducts; if (mode == 4) valid = count <= 10 && hasProducts == (count != 0); Require(sceNpCommerceDialogOpen(¶m) == (valid ? 0 : static_cast<int>(0x80B8000Au))); + param2.mode = mode; + param2.productCount = count; + param2.products = param.products; + Require(sceNpCommerceDialogOpen2(¶m2) == (valid ? 0 : static_cast<int>(0x80B8000Au))); } } } + param2.mode = 1; + param2.contentMode = 1; + param2.productCount = 1; + param2.products = products; + param2.option = 99; + Require(sceNpCommerceDialogOpen2(¶m2) == 0); + param2.option = 100; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); + param2.option = 0; + param2.mode = 2; + bool threw = false; + try { + sceNpCommerceDialogOpen2(¶m2); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + param2.contentMode = 0; + param2.mode = 0; + param2.productCount = 0; + param2.products = nullptr; + param2.reserved[31] = 1; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); + param2.reserved[31] = 0; + param2.size = 0; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); + param2.size = sizeof(param2); + param2.userId = -1; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); + param2.userId = USER_SERVICE_INITIAL_USER_ID; for (std::uint32_t mode : {1000u, 1001u}) { param.mode = mode; param.productCount = 0; diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index 9e423c84b4..33da178d0b 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -7,6 +7,6 @@ ### Unknown function info - [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. -- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): the single pointer argument and 136-byte layout are recovered, including the product pointer array, count and result user-data value. User checks, mode validation, process-version-dependent restrictions and result-buffer semantics remain unimplemented; the host dialog still finishes immediately as cancelled. Mode-specific meanings of the option and 64-bit argument remain unresolved. +- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): extended content combinations outside content mode 0 and content mode 1 with dialog mode 1 throw when otherwise valid. The native restriction reads process sysctl field 68; its relation to host process metadata is unresolved. Result-buffer semantics and mode-specific meanings of the option and 64-bit argument remain unresolved. - [sceNpCommerceDialogOpen](../../../core/libs/prx/libSceNpCommerce/Export.cpp): Product copying and service submission remain part of the cancelled UI stub. From cb8ad7c7776bcfcb16b8271e91f6baefb96616ee Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:48:29 +0300 Subject: [PATCH 0805/1360] fix(npcommerce): restore dialog result data and return value --- .../prx/libSceCommonDialog/CommonDialog.hpp | 11 ++++++ core/libs/prx/libSceNpCommerce/Export.cpp | 17 +++++---- core/libs/tests/GuestNpCommerceDialog.cpp | 35 +++++++++++-------- docs/dev/debt/libSceNpCommerce.md | 2 +- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index a9bea03571..b430b8ec87 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -29,6 +29,17 @@ struct PlayerReviewDialogParam { std::uint8_t reserved[64]; }; +struct NpCommerceDialogResult { + std::int32_t result; + std::uint8_t authorized; + std::uint8_t padding[3]; + std::uint64_t userData; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpCommerceDialogResult) == 48); +static_assert(offsetof(NpCommerceDialogResult, userData) == 8); + struct NpCommerceDialogParam { CommonDialogBaseParam base; std::uint32_t size; diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index b77de4d0b5..e224383c46 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -24,6 +24,7 @@ constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000A constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); int g_status = COMMON_DIALOG_STATUS_NONE; +std::uint64_t g_userData = 0; std::uint32_t compiledSdkVersion() { const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); @@ -106,6 +107,7 @@ int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param) { if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (!validateParam(*param)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + g_userData = param->userData; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -118,16 +120,16 @@ int APS5_VABI sceNpCommerceDialogGetStatus(void) { return g_status; } -int APS5_VABI sceNpCommerceDialogGetResult(void* result) { +int APS5_VABI sceNpCommerceDialogGetResult(NpCommerceDialogResult* result) { if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (g_status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - // SceNpCommerceDialogResult: int32 result, bool authorized, then reserved bytes. - std::int32_t value = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &value, sizeof(value)); - std::uint8_t authorized = 0; - std::memcpy(static_cast<std::uint8_t*>(result) + sizeof(value), &authorized, sizeof(authorized)); - return 0; + const auto sdkVersion = compiledSdkVersion(); + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + result->authorized = 0; + result->userData = g_userData; + if (sdkVersion >= 0x01700000) std::fill(std::begin(result->reserved), std::end(result->reserved), 0); + return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI sceNpCommerceDialogClose(void) { @@ -162,6 +164,7 @@ int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param) { if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (!validateParam2(*param)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + g_userData = param->userData; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 1b2a6c9ed6..9cb602b2b0 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -17,7 +17,7 @@ int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param); int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param); int APS5_VABI sceNpCommerceDialogUpdateStatus(void); int APS5_VABI sceNpCommerceDialogGetStatus(void); -int APS5_VABI sceNpCommerceDialogGetResult(void* result); +int APS5_VABI sceNpCommerceDialogGetResult(NpCommerceDialogResult* result); int APS5_VABI sceNpCommerceDialogClose(void); int APS5_VABI sceNpCommerceDialogTerminate(); } @@ -33,15 +33,15 @@ constexpr int statusInitialized = 1; constexpr int statusFinished = 3; constexpr std::int32_t resultUserCanceled = 1; -void RequireCanceledResult() { - std::uint8_t result[0x20]; - std::memset(result, 0x33, sizeof(result)); - Require(sceNpCommerceDialogGetResult(result) == 0); - std::int32_t value = 0; - std::memcpy(&value, result, sizeof(value)); - Require(value == resultUserCanceled); - Require(result[4] == 0); - Require(result[5] == 0x33); +void RequireCanceledResult(std::uint64_t userData, bool clearsReserved) { + NpCommerceDialogResult result; + std::memset(&result, 0x33, sizeof(result)); + Require(sceNpCommerceDialogGetResult(&result) == resultUserCanceled); + Require(result.result == resultUserCanceled); + Require(result.authorized == 0); + Require(result.userData == userData); + for (const auto value : result.padding) Require(value == 0x33); + for (const auto value : result.reserved) Require(value == (clearsReserved ? 0 : 0x33)); } } @@ -56,6 +56,7 @@ int main() { param2.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m2)) - 1060003575u; param2.size = sizeof(param2); param2.userId = USER_SERVICE_INITIAL_USER_ID; + param2.userData = 0x123456789abcdef0; Require(sceNpCommerceDialogGetStatus() == statusNone); Require(sceNpCommerceDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); Require(sceNpCommerceDialogOpen2(nullptr) == static_cast<int>(0x80B8000Du)); @@ -69,18 +70,18 @@ int main() { Require(sceNpCommerceDialogTerminate() == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); - std::uint8_t result[48]{}; - Require(sceNpCommerceDialogGetResult(result) == static_cast<int>(0x80B80003u)); + NpCommerceDialogResult result{}; + Require(sceNpCommerceDialogGetResult(&result) == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogInitialize() == 0); Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B8000Bu)); - Require(sceNpCommerceDialogGetResult(result) == static_cast<int>(0x80B80005u)); + Require(sceNpCommerceDialogGetResult(&result) == static_cast<int>(0x80B80005u)); Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); Require(sceNpCommerceDialogGetStatus() == statusInitialized); Require(sceNpCommerceDialogOpen2(¶m2) == 0); Require(sceNpCommerceDialogUpdateStatus() == statusFinished); Require(sceNpCommerceDialogGetStatus() == statusFinished); - RequireCanceledResult(); + RequireCanceledResult(param2.userData, true); Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceNpCommerceDialogGetStatus() == statusFinished); @@ -158,6 +159,7 @@ int main() { param2.userId = -1; Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B8000Au)); param2.userId = USER_SERVICE_INITIAL_USER_ID; + param2.userData = 0x123456789abcdef0; for (std::uint32_t mode : {1000u, 1001u}) { param.mode = mode; param.productCount = 0; @@ -174,7 +176,9 @@ int main() { sdkVersion = 0x01700000; std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); param.argument = 3; + param.userData = 0xfedcba9876543210; Require(sceNpCommerceDialogOpen(¶m) == 0); + RequireCanceledResult(param.userData, true); sdkVersion = 0x01600000; std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); param.size = 0; @@ -183,9 +187,10 @@ int main() { param.reserved[31] = 1; param.argument = 7; Require(sceNpCommerceDialogOpen(¶m) == 0); + RequireCanceledResult(param.userData, false); Require(sceNpCommerceDialogOpen2(¶m2) == 0); Require(sceNpCommerceDialogUpdateStatus() == statusFinished); - RequireCanceledResult(); + RequireCanceledResult(param2.userData, false); Require(sceNpCommerceDialogTerminate() == 0); Require(sceNpCommerceDialogGetStatus() == statusNone); Require(sceUserServiceTerminate() == 0); diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index 33da178d0b..02559d2c86 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -7,6 +7,6 @@ ### Unknown function info - [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. -- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): extended content combinations outside content mode 0 and content mode 1 with dialog mode 1 throw when otherwise valid. The native restriction reads process sysctl field 68; its relation to host process metadata is unresolved. Result-buffer semantics and mode-specific meanings of the option and 64-bit argument remain unresolved. +- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): extended content combinations outside content mode 0 and content mode 1 with dialog mode 1 throw when otherwise valid. The native restriction reads process sysctl field 68; its relation to host process metadata is unresolved. Mode-specific meanings of the option and 64-bit argument remain unresolved. - [sceNpCommerceDialogOpen](../../../core/libs/prx/libSceNpCommerce/Export.cpp): Product copying and service submission remain part of the cancelled UI stub. From 51d788e87c506d4d82357fe49c962397912bb499 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:50:06 +0300 Subject: [PATCH 0806/1360] fix(agc): restore wait patch scalar argument widths --- core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp | 6 ++---- core/libs/prx/libSceAgc/tests/Commands.cpp | 9 ++------- docs/dev/debt/libSceAgc.md | 1 - 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp index 4da4af5783..2946f29377 100644 --- a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp @@ -55,16 +55,14 @@ int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile vo return 0; } -int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference) { +int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); - Agc::Command::CheckBits(reference, 0xffffffffu, __func__); wait[4] = static_cast<std::uint32_t>(reference); return 0; } -int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) { +int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_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<std::uint32_t>(mask); return 0; } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index c5ac16fa9c..c6fc01ae9c 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -28,8 +28,8 @@ extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t ver 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); 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 sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask); extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(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 sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); @@ -531,9 +531,6 @@ void testMemory() { check(packet[8] == 0x55667788u && packet[9] == 0x11223344u && packet[12] == 2u, "wait reference or poll interval mismatch"); sceAgcWaitRegMemPatchReference(packet, 7); check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word"); - const auto before = storage.words; - expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); }); - 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); @@ -541,8 +538,6 @@ void testMemory() { 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"); } diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index eb2c126c2d..4cc94d9004 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -4,7 +4,6 @@ - [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - `SPI_SHADER_PGM_RSRC2_GS.OC_LDS_EN` is preserved if either half requests it, while user SGPR allocation comes from the front half and other state stays with the back half. Bit 18 is `OC_LDS_EN` in AMD's [GFX10 register definitions](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_sh_mask.h). Mesa's radeonsi sets it on a merged ES+GS program exactly when the ES stage is a tessellation evaluation shader ([`si_shader_gs_legacy`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L631), [`gfx10_shader_ngg`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L1144)), so it belongs to the front half; console fusion output has not been measured. Cocoon (PPSA08766) requests front `0x70004` and back `0xab0020`. Other unmatched hardware state still throws. -- [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 - [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 - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table while `sceAgcGetRegisterDefaults` now returns the version 13 public one; which internal table a console returns is not known. [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 From d6966f9792ef300216c0f94d0ff6c44b54e31796 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:51:12 +0300 Subject: [PATCH 0807/1360] fix(agc): preserve wait packet control bits during patching --- .../libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp | 13 +++++-------- core/libs/prx/libSceAgc/tests/Commands.cpp | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp index 2946f29377..34ee4fd902 100644 --- a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp @@ -45,13 +45,11 @@ extern "C" { int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); const auto guestAddress = reinterpret_cast<std::uintptr_t>(address); - const auto alignment = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 4u : 8u; - Agc::Command::CheckAddress(guestAddress, alignment, __func__); - Agc::Command::CheckBits(guestAddress, 0xffffffffffffull, __func__); - cmd[2] = (cmd[2] & 0xffff0000u) | static_cast<std::uint32_t>(guestAddress >> 32u); + const auto lowMask = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 3u : 7u; + cmd[2] = (cmd[2] & 0xffff0000u) | (static_cast<std::uint32_t>(guestAddress >> 32u) & 0xffffu); cmd[3] = static_cast<std::uint32_t>(guestAddress); - wait[2] = static_cast<std::uint32_t>(guestAddress); - wait[3] = static_cast<std::uint32_t>(guestAddress >> 32u); + wait[2] = (static_cast<std::uint32_t>(guestAddress) & ~lowMask) | (wait[2] & lowMask); + wait[3] = (static_cast<std::uint32_t>(guestAddress >> 32u) & 0x3ffffu) | (wait[3] & 0xfffc0000u); return 0; } @@ -69,8 +67,7 @@ int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask) 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"); - wait[1] = (wait[1] & ~7u) | compareFunction; + wait[1] = (wait[1] & ~7u) | (compareFunction & 7u); return 0; } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index c6fc01ae9c..88badb2189 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -28,6 +28,8 @@ extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t ver 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); 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 sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction); extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference); extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask); extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type); @@ -537,6 +539,21 @@ void testMemory() { 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"); + for (auto* waitPacket : {packet, truncated}) { + const auto lowMask = waitPacket == packet ? 7u : 3u; + waitPacket[6] |= lowMask; + waitPacket[7] = 0xa5fc0000u; + const auto tag = waitPacket[2] & 0xffff0000u; + const std::uintptr_t address = 0x1236abcd12345679ull; + check(sceAgcWaitRegMemPatchAddress(waitPacket, reinterpret_cast<const volatile void*>(address)) == 0, "wait address patch failed"); + check(waitPacket[2] == (tag | 0xabcdu) && waitPacket[3] == 0x12345679u, "wait wrapper address patch mismatch"); + check(waitPacket[6] == ((0x12345679u & ~lowMask) | lowMask) && waitPacket[7] == 0xa5feabcdu, "wait address patch did not preserve control bits"); + for (std::uint32_t comparison = 0; comparison <= 255; ++comparison) { + const auto control = waitPacket[5] & ~7u; + check(sceAgcWaitRegMemPatchCompareFunction(waitPacket, static_cast<std::uint8_t>(comparison)) == 0, "wait comparison patch failed"); + check(waitPacket[5] == (control | (comparison & 7u)), "wait comparison patch changed control bits"); + } + } const auto beforeMask = storage.words; expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); }); check(storage.words == beforeMask, "invalid mask patch modified packet memory"); From faebf639e907b6ccdec424cbca555e285bda309e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:52:58 +0300 Subject: [PATCH 0808/1360] fix(agc): restore semaphore memory clearing and errors --- .../libs/prx/libSceAgc/Misc/src/Semaphore.cpp | 25 ++++---------- core/libs/prx/libSceAgc/tests/Semaphore.cpp | 33 +++++++------------ docs/dev/debt/libSceAgc.md | 2 +- 3 files changed, 19 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp index 7a6c802690..3ff95978d3 100644 --- a/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp @@ -13,20 +13,12 @@ namespace { constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u; constexpr std::uint64_t semaphoreLabelBytes = 32u; -constexpr std::uint64_t semaphoreCounterBytes = 8u; std::mutex semaphoreMutex; std::uint8_t* semaphoreMemory = nullptr; std::uint64_t semaphoreBytes = 0; -bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) { - for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { - for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) { - if (memory[label + byte] != 0) return false; - } - } - return true; -} + } @@ -35,15 +27,12 @@ extern "C" { int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) { const auto address = reinterpret_cast<std::uintptr_t>(memory); std::lock_guard lock(semaphoreMutex); - Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown"); + if (semaphoreBytes != 0) return static_cast<int>(0x8a6c0048u); + if (((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) != 0) return static_cast<int>(0x8a6c0002u); Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown"); - Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown"); Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space"); auto* labels = static_cast<std::uint8_t*>(memory); - Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown"); - for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { - std::memset(labels + label, 0, semaphoreCounterBytes); - } + std::memset(labels, 0, sizeInBytes); semaphoreMemory = labels; semaphoreBytes = sizeInBytes; return 0; @@ -51,9 +40,9 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInByte int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) { std::lock_guard lock(semaphoreMutex); - Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown"); - Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown"); - Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown"); + if (semaphoreBytes == 0) return static_cast<int>(0x8a6c0049u); + if (index >= semaphoreBytes / semaphoreLabelBytes) return static_cast<int>(0x8a6c000bu); + Agc::Command::Require(label != nullptr, __func__, "null semaphore label output"); *label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes; return 0; } diff --git a/core/libs/prx/libSceAgc/tests/Semaphore.cpp b/core/libs/prx/libSceAgc/tests/Semaphore.cpp index 021eb3e7c2..789a303ad8 100644 --- a/core/libs/prx/libSceAgc/tests/Semaphore.cpp +++ b/core/libs/prx/libSceAgc/tests/Semaphore.cpp @@ -70,8 +70,8 @@ void fillStorage() { void testUnregistered() { std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()}; - expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); }); - expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); + check(sceAgcGetSemaphoreLabel(0, &labels[1]) == static_cast<int>(0x8a6c0049u), "unregistered label error mismatch"); + check(sceAgcGetSemaphoreLabel(0, nullptr) == static_cast<int>(0x8a6c0049u), "unregistered null label error mismatch"); check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output"); } @@ -80,10 +80,10 @@ void testRegistrationRejections() { expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); }); expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); }); + check(sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch"); expectFailure([] { const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u); sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes); @@ -92,18 +92,8 @@ void testRegistrationRejections() { testUnregistered(); } -void testUnsettledClearing() { - for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) { - memory()[offset] = 0x77u; - const auto before = snapshot(); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); - checkUnchanged(before, "a registration with unsettled clearing changed memory"); - testUnregistered(); - memory()[offset] = 0; - } -} - void testRegistration() { + std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x77}); const auto before = snapshot(); check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed"); check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters"); @@ -123,7 +113,7 @@ void testLabels() { void testLabelRejections() { for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) { void* label = storage.get(); - expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); }); + check(sceAgcGetSemaphoreLabel(index, &label) == static_cast<int>(0x8a6c000bu), "out-of-range label error mismatch"); check(label == storage.get(), "a rejected semaphore label changed the output"); } expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); @@ -132,9 +122,9 @@ void testLabelRejections() { void testRepeatedRegistration() { std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c}); const auto before = snapshot(); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); + check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch"); checkUnchanged(before, "a repeated registration changed memory"); void* label = nullptr; check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory"); @@ -198,7 +188,6 @@ int main() { fillStorage(); testUnregistered(); testRegistrationRejections(); - testUnsettledClearing(); testRegistration(); testLabels(); testMemSemaphore(); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 4cc94d9004..196840a44a 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -42,7 +42,7 @@ ### Functional - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents -- [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. +- [sceAgcSetAmmSemaphoreMemory](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) - native registration first invokes a kernel ioctl and driver registration; host registration does not reproduce their memory-access checks or error propagation. Null, empty and address-wrapping ranges throw. The full range is cleared after successful registration; duplicate registration, alignment and label-range errors match the native wrapper. - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well From c311d6dc2a22597827238cb11ade2ffc8c7b591f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:54:17 +0300 Subject: [PATCH 0809/1360] fix(agc): take offchip lds enable from the front shader half --- core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp | 2 +- core/libs/prx/libSceAgc/tests/ShaderFusion.cpp | 2 +- docs/dev/debt/libSceAgc.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index ad00d374ce..7c1bfcc5e5 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -129,7 +129,7 @@ std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) { std::uint32_t MergeRsrc2(const char* function, std::uint32_t back, std::uint32_t front) { if ((front & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable) & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable"); - return (back & ~Rsrc2UserSgprMask) | (front & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)); + return (back & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | (front & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)); } const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) { diff --git a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp index 2f451acf50..6a37f831ff 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp @@ -208,7 +208,7 @@ void testOffchipLdsEnable() { Shader fused{}; check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, aligned(storage, 256)) == 0, "offchip LDS enable rejected by fusion"); const auto* resource = find(fused, 0x08bu); - check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable | backEnable), "fusion lost offchip LDS enable or changed the back-half state or front user SGPRs"); + check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable), "fusion did not take offchip LDS enable and user SGPRs from the front half"); } } Halves reported; diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 196840a44a..fa576d05c8 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - `SPI_SHADER_PGM_RSRC2_GS.OC_LDS_EN` is preserved if either half requests it, while user SGPR allocation comes from the front half and other state stays with the back half. Bit 18 is `OC_LDS_EN` in AMD's [GFX10 register definitions](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_sh_mask.h). Mesa's radeonsi sets it on a merged ES+GS program exactly when the ES stage is a tessellation evaluation shader ([`si_shader_gs_legacy`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L631), [`gfx10_shader_ngg`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L1144)), so it belongs to the front half; console fusion output has not been measured. Cocoon (PPSA08766) requests front `0x70004` and back `0xab0020`. Other unmatched hardware state still throws. +- [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) - front-half hardware state outside the user SGPR and offchip LDS enable fields still throws when the back half does not enable it. Native shared VGPR allocation and resource-field maxima remain to be reconciled with the host fusion path. - [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 - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed From df4d0beae67a1fd86813a00858f342552b1f98cf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 11:56:44 +0300 Subject: [PATCH 0810/1360] fix(agc): restore geometry shader resource merging --- .../prx/libSceAgc/Misc/src/ShaderFusion.cpp | 31 ++++++++------ .../libs/prx/libSceAgc/tests/ShaderFusion.cpp | 40 ++++++++++++++++--- docs/dev/debt/libSceAgc.md | 1 - 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 7c1bfcc5e5..a8fdedf664 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -35,7 +35,6 @@ constexpr std::size_t FusedCodeAlignment = 256; constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'}; constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t); constexpr std::uint32_t Rsrc1VgprMask = 0x3fu; -constexpr std::uint32_t Rsrc1SgprMask = 0xfu << 6u; constexpr std::uint32_t Rsrc2OffchipLdsEnable = 1u << 18u; constexpr std::uint32_t Rsrc2UserSgprMask = (0x1fu << 1u) | (1u << 27u); @@ -121,15 +120,19 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) { return static_cast<std::uint32_t>((end - 1) * 4); } -std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) { - const auto vgprs = std::max(back & Rsrc1VgprMask, front & Rsrc1VgprMask); - const auto sgprs = std::max(back & Rsrc1SgprMask, front & Rsrc1SgprMask); - return (back & ~(Rsrc1VgprMask | Rsrc1SgprMask)) | vgprs | sgprs; -} - -std::uint32_t MergeRsrc2(const char* function, std::uint32_t back, std::uint32_t front) { - if ((front & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable) & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable"); - return (back & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | (front & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)); +std::array<std::uint32_t, 2> mergeGeometryResources(std::uint32_t frontRsrc1, std::uint32_t frontRsrc2, std::uint32_t backRsrc1, std::uint32_t backRsrc2) { + const auto frontVgprs = ((frontRsrc1 & Rsrc1VgprMask) + 1u) * 4u; + const auto backVgprs = ((backRsrc1 & Rsrc1VgprMask) + 1u) * 4u; + const auto frontTotal = frontVgprs + (frontRsrc2 >> 28u) * 8u; + const auto backTotal = backVgprs + (backRsrc2 >> 28u) * 8u; + const auto maxTotal = std::max(frontTotal, backTotal); + const auto shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u; + const auto vgprs = std::max(frontRsrc1 & Rsrc1VgprMask, backRsrc1 & Rsrc1VgprMask); + const auto rsrc1Field = std::max((frontRsrc1 >> 29u) & 3u, (backRsrc1 >> 29u) & 3u); + const auto rsrc2Field = std::max((frontRsrc2 >> 16u) & 3u, (backRsrc2 >> 16u) & 3u); + const auto rsrc1 = (backRsrc1 & ~(Rsrc1VgprMask | (3u << 29u))) | vgprs | (rsrc1Field << 29u); + const auto rsrc2 = (backRsrc2 & ~(0xf0000000u | (3u << 16u) | Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | ((shared & 15u) << 28u) | (rsrc2Field << 16u) | (frontRsrc2 & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)); + return {rsrc1, rsrc2}; } const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) { @@ -149,10 +152,14 @@ GeometryLayout ComputeGeometryLayout(const char* function, const Shader* front, const auto* frontRsrc1 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC1_GS); const auto* frontRsrc2 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC2_GS); if (frontRsrc1 == nullptr || frontRsrc2 == nullptr) Fail(function, "front half has no GS resource registers"); + const auto* backRsrc1 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC1_GS); + const auto* backRsrc2 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC2_GS); + if (backRsrc1 == nullptr || backRsrc2 == nullptr) Fail(function, "back half has no GS resource registers"); + const auto resources = mergeGeometryResources(frontRsrc1->value, frontRsrc2->value, backRsrc1->value, backRsrc2->value); layout.sh.assign(back->sh_registers, back->sh_registers + back->num_sh_registers); for (auto& reg : layout.sh) { - if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = MergeRsrc1(reg.value, frontRsrc1->value); - else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = MergeRsrc2(function, reg.value, frontRsrc2->value); + if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = resources[0]; + else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = resources[1]; } for (std::uint32_t i = 0; i < front->num_sh_registers; ++i) { const auto& reg = front->sh_registers[i]; diff --git a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp index 6a37f831ff..7b66b5e57c 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp @@ -147,7 +147,7 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { const auto* frontLo = find(fused, 0x0C8u); check(backLo != nullptr && backLo->value == static_cast<std::uint32_t>((base + 256) >> 8u), "back program address was not patched"); check(frontLo != nullptr && frontLo->value == static_cast<std::uint32_t>(base >> 8u), "fused program address was not patched"); - check(find(fused, 0x08Au)->value == ((1u << 24u) | (2u << 6u) | 5u), "RSRC1 register counts were not merged"); + check(find(fused, 0x08Au)->value == ((1u << 24u) | (1u << 6u) | 5u), "RSRC1 register counts were not merged"); check(find(fused, 0x08Bu)->value == ((6u << 1u) | (1u << 7u) | (1u << 15u)), "RSRC2 user SGPRs were not taken from the front half"); check(find(fused, 0x0B2u) != nullptr && find(fused, 0x0B2u)->value == 0x55u, "front-only SH register was lost"); check(find(fused, 0x080u) == nullptr, "front program checksum was kept"); @@ -222,6 +222,38 @@ void testOffchipLdsEnable() { check(find(fused, 0x08bu)->value == 0x00af0004u, "reported shader resources did not retain the front offchip LDS enable"); } +void testResourceMerging() { + struct Case { + std::uint32_t front1; + std::uint32_t front2; + std::uint32_t back1; + std::uint32_t back2; + std::uint32_t expected1; + std::uint32_t expected2; + }; + const Case cases[] = { + {0u, 0x80000000u, 0u, 0u, 0u, 0x10000000u}, + {1u, 0xf0000000u, 3u, 0u, 3u, 0x10000000u}, + {0u, 0xf0000000u, 0u, 0xf0000000u, 0u, 0u}, + {0x400003c1u, 0x0006000cu, 0x61000043u, 0x00018001u, 0x61000043u, 0x0006800du}, + {0x60000007u, 0x00010000u, 0x21000041u, 0x00020000u, 0x61000047u, 0x00020000u} + }; + for (const auto& entry : cases) { + Halves halves; + halves.frontSh[1].value = entry.front1; + halves.frontSh[2].value = entry.front2; + halves.backSh[2].value = entry.back1; + halves.backSh[3].value = entry.back2; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "resource merge size query failed"); + std::vector<std::uint8_t> scratch(size.m_size); + Shader fused{}; + check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch.data()) == 0, "resource merge failed"); + check(find(fused, 0x08au)->value == entry.expected1, "resource register 1 merge mismatch"); + check(find(fused, 0x08bu)->value == entry.expected2, "resource register 2 merge mismatch"); + } +} + void testRejections() { Halves halves; SizeAlign size{}; @@ -249,11 +281,6 @@ void testRejections() { noTrailer.frontCode[5] = 0; expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); }); } - { - Halves hardware; - hardware.frontSh[2].value |= 1u << 16u; - expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); }); - } { Halves constants; constants.back.embedded_constant_buffer_size_dqw = 1; @@ -305,6 +332,7 @@ int main() { testFusion(0, SSwappcNullS6); testScratchReuse(); testOffchipLdsEnable(); + testResourceMerging(); testRejections(); testHullHalves(); LibcRunShutdown_nid_postfix(); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index fa576d05c8..ab5eaacf4e 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -2,7 +2,6 @@ ### Unknown function info -- [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) - front-half hardware state outside the user SGPR and offchip LDS enable fields still throws when the back half does not enable it. Native shared VGPR allocation and resource-field maxima remain to be reconciled with the host fusion path. - [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 - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed From 2ad07c61ee74fb4f4b6794c68c1f04b24dbf04a4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:03:00 +0300 Subject: [PATCH 0811/1360] fix(automounter): define usb device output layout --- .../prx/libSceAutoMounterClient/Export.cpp | 20 ++++++++++++++++++- docs/dev/debt/libSceAutoMounterClient.md | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAutoMounterClient/Export.cpp b/core/libs/prx/libSceAutoMounterClient/Export.cpp index e4845005e9..77af95c0ff 100644 --- a/core/libs/prx/libSceAutoMounterClient/Export.cpp +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -3,7 +3,25 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -struct AutoMounterUsbDeviceInfo; +struct AutoMounterUsbDeviceInfo { + char deviceName[128]; + std::byte metadata[22]; + std::byte data0[256]; + std::byte padding[2]; + std::uint64_t data0Size; + std::byte data1[256]; + std::uint64_t data1Size; + std::byte data2[256]; + std::uint64_t data2Size; +}; + +static_assert(sizeof(AutoMounterUsbDeviceInfo) == 944); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data0) == 150); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data0Size) == 408); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data1) == 416); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data1Size) == 672); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data2) == 680); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data2Size) == 936); extern "C" { diff --git a/docs/dev/debt/libSceAutoMounterClient.md b/docs/dev/debt/libSceAutoMounterClient.md index 1517732495..2996a7b44b 100644 --- a/docs/dev/debt/libSceAutoMounterClient.md +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -3,4 +3,4 @@ ### Unknown function info - [sceAutoMounterClientRegisterCallback, sceAutoMounterClientTerm and sceAutoMounterClientUnregisterCallback](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAutoMounterClient.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceAutoMounterClientGetUsbDeviceInfo](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp): the name string and output pointer are confirmed. The output occupies 944 bytes, including a 128-byte device name and three variable-length byte arrays with 64-bit lengths, but the metadata field meanings remain unresolved. +- [sceAutoMounterClientGetUsbDeviceInfo](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp): the 22 metadata bytes at offset 128 remain opaque. The 944-byte output layout, 128-byte device name and three 256-byte arrays with 64-bit lengths are recovered. From 886974fd6d15833227fb6a2cee33131c01ffd9f6 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:03:01 +0300 Subject: [PATCH 0812/1360] fix(bgft): define storage task parameter layout --- core/libs/prx/libSceBgft/Export.cpp | 27 ++++++++++++++++++++++++++- docs/dev/debt/libSceBgft.md | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp index 8480641566..483969400b 100644 --- a/core/libs/prx/libSceBgft/Export.cpp +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -9,7 +9,32 @@ struct BgftInitParam { }; struct BgftProgress; -struct BgftStorageTaskParam; +struct BgftStorageTaskParam { + std::uint32_t value0; + std::uint32_t value4; + const char* contentId; + const char* text16; + std::byte unused24[8]; + const char* text32; + const char* text40; + const char* text48; + std::uint32_t value56; + std::byte padding60[4]; + const char* text64; + const char* text72; + const char* text80; + const char* text88; + std::uint64_t value96; + std::uint32_t value104; +}; + +static_assert(sizeof(BgftStorageTaskParam) == 112); +static_assert(offsetof(BgftStorageTaskParam, contentId) == 8); +static_assert(offsetof(BgftStorageTaskParam, text32) == 32); +static_assert(offsetof(BgftStorageTaskParam, value56) == 56); +static_assert(offsetof(BgftStorageTaskParam, text64) == 64); +static_assert(offsetof(BgftStorageTaskParam, value96) == 96); +static_assert(offsetof(BgftStorageTaskParam, value104) == 104); static_assert(sizeof(BgftInitParam) == 16); static_assert(offsetof(BgftInitParam, heapSize) == 8); diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 839539717c..385f701d0f 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -3,5 +3,5 @@ ### Unknown function info - [sceBgftServiceDownloadGetProgress](../../../core/libs/prx/libSceBgft/Export.cpp), `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. -- [sceBgftServiceIntDownloadRegisterTaskByStorageEx](../../../core/libs/prx/libSceBgft/Export.cpp): confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. Scalar meanings and the field at offset 24 remain unresolved; the parameter type is forward-declared. +- [sceBgftServiceIntDownloadRegisterTaskByStorageEx](../../../core/libs/prx/libSceBgft/Export.cpp): confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. The typed layout preserves the unused eight bytes at offset 24; scalar meanings and several string roles remain unresolved. - [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. From 6aa2dd97b71c382c254e907dd96bcc7606da8d1f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:03:02 +0300 Subject: [PATCH 0813/1360] fix(npmanager): restore availability output and online id input --- core/libs/prx/libSceNpManager/Export.cpp | 8 ++++---- core/libs/tests/GuestNpManager.cpp | 6 +++--- core/libs/tests/GuestNpReachability.cpp | 14 +++++++++++--- docs/dev/debt/libSceNpManager.md | 6 +++--- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 1b45d38294..0035e68915 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -683,9 +683,9 @@ int APS5_VABI sceNpCheckCallback(void) { return 0; } -int APS5_VABI sceNpCheckNpAvailability(int req_id, const NpOnlineId* online_id) { - (void)req_id; - if (!online_id) return SCE_NP_ERROR_INVALID_ARGUMENT; +int APS5_VABI sceNpCheckNpAvailability(std::int32_t requestId, const NpOnlineId* onlineId, std::uint8_t* available) { + if (requestId == 0 || onlineId == nullptr) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (available != nullptr) *available = 0; return SCE_NP_ERROR_USER_NOT_FOUND; } @@ -961,7 +961,7 @@ int APS5_VABI sceNpGetSandboxInfo(void* sandbox_info) { } APS5_EXPORT("8weiQyIdI3A", sceNpUnknown_8weiQyIdI3A); -int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer) { +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, const NpOnlineId* buffer) { (void)userId; (void)buffer; NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 83a5566664..75393b1321 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -12,7 +12,7 @@ int APS5_VABI sceNpGetAccountCountryA(int user_id, void* country_code); int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id); int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id); int APS5_VABI sceNpGetUserIdByAccountId(std::uint64_t account_id, int* user_id); -int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, const NpOnlineId* buffer); int APS5_VABI sceNpUnknown_WxLcN76q19o(void); int APS5_VABI sceNpGetAccountCountry(const NpOnlineId* online_id, void* country_code); int APS5_VABI sceNpGetAccountDateOfBirth(const NpOnlineId* online_id, void* date_of_birth); @@ -306,9 +306,9 @@ int main() { Require(sceNpGetUserIdByAccountId(0x1234, nullptr) == InvalidArgument, "sceNpGetUserIdByAccountId must reject a null output"); Require(sceNpGetUserIdByAccountId(0x1234, &userId) == SignedOut, "sceNpGetUserIdByAccountId must report NP as signed out"); Require(userId == 0x5a5a, "sceNpGetUserIdByAccountId must leave the user id untouched"); - unsigned char buffer[0x11]{}; + NpOnlineId buffer{}; try { - sceNpUnknown_8weiQyIdI3A(0x10000, buffer); + sceNpUnknown_8weiQyIdI3A(0x10000, &buffer); Require(false, "sceNpUnknown_8weiQyIdI3A must report that it is not implemented"); } catch (const std::runtime_error& error) { Require(std::string(error.what()) == "sceNpUnknown_8weiQyIdI3A not implemented", diff --git a/core/libs/tests/GuestNpReachability.cpp b/core/libs/tests/GuestNpReachability.cpp index 4e0c1f2262..e6995ac80d 100644 --- a/core/libs/tests/GuestNpReachability.cpp +++ b/core/libs/tests/GuestNpReachability.cpp @@ -6,7 +6,7 @@ int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void*, void*); int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(); int APS5_VABI sceNpGetNpReachabilityState(int, uint32_t*); int APS5_VABI sceNpCheckCallback(); -int APS5_VABI sceNpCheckNpAvailability(int, const NpOnlineId*); +int APS5_VABI sceNpCheckNpAvailability(int, const NpOnlineId*, std::uint8_t*); } static constexpr int ErrorArgument = static_cast<int>(0x80550003); @@ -33,6 +33,14 @@ int main() { Require(sceNpUnregisterNpReachabilityStateCallback() == 0); NpOnlineId onlineId{}; onlineId.data[0] = 'p'; - Require(sceNpCheckNpAvailability(1, &onlineId) == ErrorUserNotFound); - Require(sceNpCheckNpAvailability(1, nullptr) == ErrorArgument); + Require(sceNpCheckNpAvailability(1, &onlineId, nullptr) == ErrorUserNotFound); + Require(sceNpCheckNpAvailability(1, nullptr, nullptr) == ErrorArgument); + std::uint8_t available[3]{0x55, 0x55, 0x55}; + Require(sceNpCheckNpAvailability(0, &onlineId, &available[1]) == ErrorArgument); + Require(available[1] == 0x55); + Require(sceNpCheckNpAvailability(1, nullptr, &available[1]) == ErrorArgument); + Require(available[1] == 0x55); + Require(sceNpCheckNpAvailability(1, &onlineId, &available[1]) == ErrorUserNotFound); + Require(available[0] == 0x55 && available[1] == 0 && available[2] == 0x55); + } diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 9cf1c6accd..b7d03f1c33 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -9,8 +9,8 @@ - [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 150 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. -- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory. -- [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `WxLcN76q19o`) - the name is not in aerolib. PPSA34015 imports it from `libSceNpManager.prx` as a strong `JUMP_SLOT` with no call found in the eboot; the loader still requires the export. Arguments, result and console errors are unknown, so every call throws. +- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp): the original name remains unknown. The native wrapper takes a user ID and a read-only 20-byte online-ID record, validates its first byte and sends the record through IPC. The implementation remains a throwing stub. +- [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp): the original name remains unknown. The native wrapper has no input arguments and returns a 32-bit status from an IPC request after initialization checks. The implementation remains a throwing stub. - [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored - [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user - [sceNpNotifyPlusFeature](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 titles import it; its parameter block and what the console does with it for a signed-out user are unknown ([shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1170) returns 0 without reading it), so every call throws. @@ -22,7 +22,7 @@ - [sceNpCheckPlus](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(request id, parameter, result)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L246): request id 0 or a null parameter or result returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and every other call returns `SCE_NP_ERROR_SIGNED_OUT` without writing the result, as `sceNpCheckPremium` does. The parameter's size and feature fields, which shadPS4 checks, are not read: the errors the console returns for them are not known - [sceNpGetAccountId, sceNpSetGamePresenceOnline](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms; the `(online id, account id output)` and `(request id, online id, presence)` signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1092-L1096). A null online id or output, or request id 0 for `sceNpSetGamePresenceOnline`, returns `SCE_NP_ERROR_INVALID_ARGUMENT`. `sceNpSetGamePresenceOnline` then returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` for a request that was never created or was deleted, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_manager.cpp#L1098-L1104) does. Otherwise both return `SCE_NP_ERROR_SIGNED_OUT` without writing, as `sceNpGetAccountIdA` does. The console's result for a signed-out user is not measured - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. -- [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests +- [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp): native NP initialization and online-ID lookup are not modeled. The offline host returns user-not-found for valid arguments after clearing the optional one-byte availability output; request ID 0 and a null online-ID pointer return invalid-argument before writing the output. - [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured - [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 From 73120899a97ac080bdcb9b9987fbff595f2427d0 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:05:14 +0300 Subject: [PATCH 0814/1360] fix(playerselection): restore dialog validation and result contract --- .../prx/libSceCommonDialog/CommonDialog.hpp | 40 ++++++++++++ .../CMakeLists.txt | 3 +- .../libScePlayerSelectionDialog/Export.cpp | 32 +++++++--- .../libs/tests/GuestPlayerSelectionDialog.cpp | 62 +++++++++++++++---- docs/dev/debt/libScePlayerSelectionDialog.md | 2 +- 5 files changed, 118 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index b430b8ec87..bb2d229e7c 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -29,6 +29,46 @@ struct PlayerReviewDialogParam { std::uint8_t reserved[64]; }; +struct PlayerSelectionDialogPlayer { + std::uint64_t accountId; + std::uint32_t option0; + std::uint32_t option1; + std::uint32_t mode; + std::uint8_t reserved[64]; + std::uint32_t padding; +}; + +struct PlayerSelectionDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::int32_t userId; + char title[64]; + std::uint32_t maxPlayers; + std::uint32_t mode; + std::uint32_t padding0; + const PlayerSelectionDialogPlayer* players; + std::uint32_t playerCount; + std::uint8_t reserved[64]; + std::uint32_t padding1; +}; + +struct PlayerSelectionDialogResult { + std::int32_t result; + std::uint32_t option; + std::uint64_t* accountIds; + std::uint32_t count; + std::uint32_t padding; +}; + +static_assert(sizeof(PlayerSelectionDialogPlayer) == 88); +static_assert(offsetof(PlayerSelectionDialogPlayer, reserved) == 20); +static_assert(sizeof(PlayerSelectionDialogParam) == 216); +static_assert(offsetof(PlayerSelectionDialogParam, maxPlayers) == 124); +static_assert(offsetof(PlayerSelectionDialogParam, players) == 136); +static_assert(offsetof(PlayerSelectionDialogParam, reserved) == 148); +static_assert(sizeof(PlayerSelectionDialogResult) == 24); +static_assert(offsetof(PlayerSelectionDialogResult, accountIds) == 8); + struct NpCommerceDialogResult { std::int32_t result; std::uint8_t authorized; diff --git a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt index 670b5b3b0a..0d85abead0 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libScePlayerSelectionDialog SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libScePlayerSelectionDialog PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libScePlayerSelectionDialog) +target_link_libraries(libScePlayerSelectionDialog PRIVATE libSceUserService) if(BUILD_TESTING) add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog libc) + target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog libSceUserService libc) configure_windows_unwind(guest_player_selection_dialog_tests) add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) if(WIN32) diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index 4f9c50b9a7..06df01e1c8 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -3,8 +3,11 @@ #include <cstddef> #include <cstring> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -12,9 +15,10 @@ 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<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; @@ -30,17 +34,30 @@ int APS5_VABI scePlayerSelectionDialogInitialize(void) { return 0; } -int APS5_VABI scePlayerSelectionDialogOpen(const void* param) { +int APS5_VABI scePlayerSelectionDialogOpen(const PlayerSelectionDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param->size != sizeof(*param) || param->userId == -1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::find(std::begin(users.user_id), std::end(users.user_id), param->userId) == std::end(users.user_id)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->maxPlayers > 256 || param->mode > 3 || param->playerCount > 256) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->playerCount != 0 && param->players == nullptr) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + for (std::uint32_t index = 0; index < param->playerCount; ++index) { + const auto& player = param->players[index]; + if (player.accountId == 0 || player.option0 > 1 || player.option1 > 1 || player.mode > 2 || !std::all_of(std::begin(player.reserved), std::end(player.reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->maxPlayers == 1 && player.option0 == 1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + } + if (!std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; 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 scePlayerSelectionDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -53,14 +70,13 @@ int APS5_VABI scePlayerSelectionDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { +int APS5_VABI scePlayerSelectionDialogGetResult(PlayerSelectionDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &canceled, sizeof(canceled)); - return 0; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param) { diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index 8814165e4f..4d3e163d12 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -2,13 +2,17 @@ #include <cstdint> #include <cstdlib> #include <stdexcept> +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceUserService/UserService.hpp" extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI scePlayerSelectionDialogInitialize(void); -int APS5_VABI scePlayerSelectionDialogOpen(const void* param); +int APS5_VABI scePlayerSelectionDialogOpen(const PlayerSelectionDialogParam* param); int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); int APS5_VABI scePlayerSelectionDialogGetStatus(void); -int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI scePlayerSelectionDialogGetResult(PlayerSelectionDialogResult* result); int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param); int APS5_VABI scePlayerSelectionDialogClose(void); int APS5_VABI scePlayerSelectionDialogTerminate(void); @@ -28,8 +32,13 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + Require(sceUserServiceInitialize(nullptr) == 0); + PlayerSelectionDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + PlayerSelectionDialogResult result{-1, 99, nullptr, 77, 55}; Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); @@ -42,8 +51,8 @@ int main() { threw = true; } Require(threw); - Require(param == 0); - Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); + Require(param.size == sizeof(param)); + Require(scePlayerSelectionDialogGetResult(&result) == kErrNotInitialized); Require(scePlayerSelectionDialogGetStatus() == 0); Require(scePlayerSelectionDialogUpdateStatus() == 0); @@ -53,20 +62,51 @@ int main() { Require(scePlayerSelectionDialogUpdateStatus() == 1); Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); - Require(scePlayerSelectionDialogGetResult(result) == kErrNotFinished); + Require(scePlayerSelectionDialogGetResult(&result) == kErrNotFinished); + param.size = 0; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.size = sizeof(param); + param.maxPlayers = 257; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.maxPlayers = 1; + param.mode = 4; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.mode = 0; + param.playerCount = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + PlayerSelectionDialogPlayer player{}; + param.players = &player; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + player.accountId = 1; + player.option0 = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.maxPlayers = 2; + player.option1 = 2; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + player.option1 = 1; + player.mode = 3; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + player.mode = 2; + player.reserved[63] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + player.reserved[63] = 0; + param.reserved[63] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.reserved[63] = 0; Require(scePlayerSelectionDialogOpen(¶m) == 0); Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); - Require(scePlayerSelectionDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + Require(scePlayerSelectionDialogGetResult(&result) == kResultUserCanceled); + Require(result.result == kResultUserCanceled); + Require(result.option == 99 && result.accountIds == nullptr && result.count == 77 && result.padding == 55); - Require(scePlayerSelectionDialogClose() == 0); + Require(scePlayerSelectionDialogClose() == static_cast<int>(0x80B8000Bu)); Require(scePlayerSelectionDialogTerminate() == 0); Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogGetStatus() == 0); Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(sceUserServiceTerminate() == 0); } diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md index c4936f8740..96b3ef7efc 100644 --- a/docs/dev/debt/libScePlayerSelectionDialog.md +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `Close`, `GetStatus`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) +- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) shows no selection UI and immediately reports cancellation. Account discovery, selection and successful-result list delivery are not implemented. ### Unknown function info From 6fdcac937c198e594b922155d72fa14cffa05e39 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:07:22 +0300 Subject: [PATCH 0815/1360] fix(playgodialog): restore lifecycle errors and result layout --- core/libs/SceTypes.hpp | 3 ++- core/libs/prx/libScePlayGoDialog/Export.cpp | 28 +++++++++++++-------- core/libs/tests/GuestPlayGoDialog.cpp | 26 ++++++++++++------- docs/dev/debt/libScePlayGoDialog.md | 2 +- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 38b336e42c..95443d9e12 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -2006,8 +2006,9 @@ union PlayGoOptionalChunk { }; struct PlayGoDialogResult { + std::int32_t errorCode; std::int32_t result; - std::int32_t reserved[9]; + std::uint8_t reserved[32]; }; struct RudpPollEvent { diff --git a/core/libs/prx/libScePlayGoDialog/Export.cpp b/core/libs/prx/libScePlayGoDialog/Export.cpp index 65b0b8eba1..e341d17b84 100644 --- a/core/libs/prx/libScePlayGoDialog/Export.cpp +++ b/core/libs/prx/libScePlayGoDialog/Export.cpp @@ -1,4 +1,6 @@ #include <atomic> +#include <algorithm> +#include <iterator> #include <cstdint> #include <cstddef> #include <cstring> @@ -6,6 +8,8 @@ #include "prx/libc/include/General.hpp" static_assert(sizeof(PlayGoDialogResult) == 40); +static_assert(offsetof(PlayGoDialogResult, result) == 4); +static_assert(offsetof(PlayGoDialogResult, reserved) == 8); namespace { @@ -15,11 +19,13 @@ constexpr std::int32_t STATUS_RUNNING = 2; constexpr std::int32_t STATUS_FINISHED = 3; constexpr std::int32_t RESULT_OK = 0; +constexpr std::int32_t ERROR_ARG_NULL = static_cast<std::int32_t>(0x80B8000Du); +constexpr std::int32_t ERROR_NOT_RUNNING = static_cast<std::int32_t>(0x80B8000Bu); -constexpr std::int32_t ERROR_NOT_INITIALIZED = static_cast<std::int32_t>(0x80ED0001); -constexpr std::int32_t ERROR_ALREADY_INITIALIZED = static_cast<std::int32_t>(0x80ED0002); -constexpr std::int32_t ERROR_PARAM_INVALID = static_cast<std::int32_t>(0x80ED0003); -constexpr std::int32_t ERROR_INVALID_STATE = static_cast<std::int32_t>(0x80ED0005); +constexpr std::int32_t ERROR_NOT_INITIALIZED = static_cast<std::int32_t>(0x80B80003); +constexpr std::int32_t ERROR_ALREADY_INITIALIZED = static_cast<std::int32_t>(0x80B80004); +constexpr std::int32_t ERROR_PARAM_INVALID = static_cast<std::int32_t>(0x80B8000A); +constexpr std::int32_t ERROR_INVALID_STATE = static_cast<std::int32_t>(0x80B80007); constexpr std::int32_t COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<std::int32_t>(0x80B80005u); std::atomic<int> g_status{STATUS_NONE}; @@ -44,7 +50,8 @@ int APS5_VABI scePlayGoDialogTerminate(void) { } int APS5_VABI scePlayGoDialogOpen(const void* param) { - if (!param) return ERROR_PARAM_INVALID; + if (!param) return ERROR_ARG_NULL; + if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; int expected = STATUS_INITIALIZED; if (!g_status.compare_exchange_strong(expected, STATUS_FINISHED)) { expected = STATUS_FINISHED; @@ -55,6 +62,7 @@ int APS5_VABI scePlayGoDialogOpen(const void* param) { int APS5_VABI scePlayGoDialogClose(void) { if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; + if (g_status.load() != STATUS_RUNNING) return ERROR_NOT_RUNNING; g_status.store(STATUS_FINISHED); return 0; } @@ -65,13 +73,13 @@ int APS5_VABI scePlayGoDialogUpdateStatus(void) { return g_status.load(); } -int APS5_VABI scePlayGoDialogGetResult(void* result) { +int APS5_VABI scePlayGoDialogGetResult(PlayGoDialogResult* result) { + if (result == nullptr) return ERROR_ARG_NULL; + if (!std::all_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value == 0; })) return ERROR_PARAM_INVALID; if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; - if (!result) return ERROR_PARAM_INVALID; if (g_status.load() != STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - auto* r = static_cast<PlayGoDialogResult*>(result); - std::memset(r, 0, sizeof(*r)); - r->result = RESULT_OK; + *result = {}; + result->result = RESULT_OK; return 0; } diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index 470cfee79d..0a8291f96c 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -11,27 +11,30 @@ int APS5_VABI scePlayGoDialogOpen(const void* param); int APS5_VABI scePlayGoDialogClose(void); int APS5_VABI scePlayGoDialogUpdateStatus(void); int APS5_VABI scePlayGoDialogGetStatus(void); -int APS5_VABI scePlayGoDialogGetResult(void* result); +int APS5_VABI scePlayGoDialogGetResult(PlayGoDialogResult* result); } static void Require(bool value) { if (!value) std::abort(); } int main() { - constexpr std::int32_t notInitialized = static_cast<std::int32_t>(0x80ED0001); - constexpr std::int32_t alreadyInitialized = static_cast<std::int32_t>(0x80ED0002); - constexpr std::int32_t paramInvalid = static_cast<std::int32_t>(0x80ED0003); - constexpr std::int32_t invalidState = static_cast<std::int32_t>(0x80ED0005); + constexpr std::int32_t notInitialized = static_cast<std::int32_t>(0x80B80003); + constexpr std::int32_t alreadyInitialized = static_cast<std::int32_t>(0x80B80004); + constexpr std::int32_t paramInvalid = static_cast<std::int32_t>(0x80B8000D); constexpr std::int32_t notFinished = static_cast<std::int32_t>(0x80B80005u); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); Require(scePlayGoDialogClose() == notInitialized); Require(scePlayGoDialogOpen(nullptr) == paramInvalid); - Require(scePlayGoDialogGetResult(nullptr) == notInitialized); + Require(scePlayGoDialogGetResult(nullptr) == paramInvalid); const int openParam = 1; - Require(scePlayGoDialogOpen(&openParam) == invalidState); + Require(scePlayGoDialogOpen(&openParam) == notInitialized); + PlayGoDialogResult early{}; + Require(scePlayGoDialogGetResult(&early) == notInitialized); + early.reserved[31] = 1; + Require(scePlayGoDialogGetResult(&early) == static_cast<std::int32_t>(0x80B8000Au)); Require(scePlayGoDialogInitialize() == 0); Require(scePlayGoDialogGetStatus() == 1); Require(scePlayGoDialogInitialize() == alreadyInitialized); @@ -47,8 +50,13 @@ int main() { PlayGoDialogResult result{}; Require(scePlayGoDialogGetResult(&result) == 0); - Require(result.result == 0); - Require(scePlayGoDialogClose() == 0); + Require(result.result == 0 && result.errorCode == 0); + result.errorCode = 7; + result.result = 9; + result.reserved[0] = 1; + Require(scePlayGoDialogGetResult(&result) == static_cast<std::int32_t>(0x80B8000Au)); + Require(result.errorCode == 7 && result.result == 9 && result.reserved[0] == 1); + Require(scePlayGoDialogClose() == static_cast<std::int32_t>(0x80B8000Bu)); Require(scePlayGoDialogTerminate() == 0); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); diff --git a/docs/dev/debt/libScePlayGoDialog.md b/docs/dev/debt/libScePlayGoDialog.md index 7e06e1309c..53b4cd2147 100644 --- a/docs/dev/debt/libScePlayGoDialog.md +++ b/docs/dev/debt/libScePlayGoDialog.md @@ -2,4 +2,4 @@ ### Unknown function info -- [scePlayGoDialogGetResult](../../../core/libs/prx/libScePlayGoDialog/Export.cpp) (libScePlayGoDialog) - checks that the dialog has reached STATUS_FINISHED and returns 0x80B80005 (COMMON_DIALOG_ERROR_NOT_FINISHED) when called before the dialog completes, following the other common dialog modules in the tree (MsgDialog, SigninDialog, PlayerSelectionDialog, SaveDataDialog). shadPS4's scePlayGoDialogGetResult is a dummy that always returns OK and provides no evidence for either value; the exact return code and intermediate state check have not been verified against a real hardware trace or title measurement. +- [scePlayGoDialogOpen](../../../core/libs/prx/libScePlayGoDialog/Export.cpp) does not validate or copy the native nested parameters or query download progress. The host UI stub immediately reports completion; service-driven failures and transitions remain unimplemented. From 4722e7d859712bef03e00ba0e2a9d0868a21885b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:07:23 +0300 Subject: [PATCH 0816/1360] docs(nkweb): close verified initialization signature debt --- docs/dev/debt/libSceNKWeb.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceNKWeb.md b/docs/dev/debt/libSceNKWeb.md index 4191fc13a3..5b311c43ed 100644 --- a/docs/dev/debt/libSceNKWeb.md +++ b/docs/dev/debt/libSceNKWeb.md @@ -1,5 +1,5 @@ # libSceNKWeb technical debt -### Unknown function info +### Functional -- [sceNKWebInitialize and sceNKWebTerminate](../../../core/libs/prx/libSceNKWeb/Export.cpp) (libSceNKWeb) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNKWeb.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNKWebInitialize and sceNKWebTerminate](../../../core/libs/prx/libSceNKWeb/Export.cpp) remain throwing stubs. Both native exports take no input arguments and return a 32-bit status; decompiler SIMD parameters are unused register propagation through initialization and cleanup helpers. From 2a8c0f82ab512e9a96d0d04efc7f820be3e6b170 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:12:08 +0300 Subject: [PATCH 0817/1360] fix(pad): define vr controller sample layout --- core/libs/prx/libScePad/Export.cpp | 30 ++++++++++++++++++++++++++++-- docs/dev/debt/libScePad.md | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 34ff03cd2b..3a6f40f658 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -26,7 +26,33 @@ struct PadRemoteSetting; struct PadVirtualTriggerSetting; struct PadProcessFocusParameter; struct PadDataExtended; -struct PadVrControllerData; +struct PadVrControllerData { + std::uint32_t buttons; + std::uint8_t stick[2]; + std::uint8_t unused6[2]; + std::uint8_t triggers[2]; + std::uint8_t unused10[2]; + std::uint32_t touchButtons; + std::uint8_t touchValues[4]; + float acceleration[3]; + float angularVelocity[3]; + float orientation[4]; + std::uint8_t unused60[4]; + std::uint8_t connected; + std::uint8_t connectedCount; + std::uint8_t unused66[6]; + std::uint64_t timestamp; + std::uint8_t unused80[24]; +}; + +static_assert(sizeof(PadVrControllerData) == 104); +static_assert(offsetof(PadVrControllerData, triggers) == 8); +static_assert(offsetof(PadVrControllerData, touchButtons) == 12); +static_assert(offsetof(PadVrControllerData, acceleration) == 20); +static_assert(offsetof(PadVrControllerData, angularVelocity) == 32); +static_assert(offsetof(PadVrControllerData, orientation) == 44); +static_assert(offsetof(PadVrControllerData, connected) == 64); +static_assert(offsetof(PadVrControllerData, timestamp) == 72); struct PadVrControllerDeviceInformation; struct PadVrTriggerEffect; struct PadVrTriggerEffects; @@ -363,7 +389,7 @@ int APS5_VABI scePadVrControllerGetTriggerEffectState(std::int32_t leftHandle, s return 0; } -int APS5_VABI scePadVrControllerReadState(int handle, void* state) { +int APS5_VABI scePadVrControllerReadState(int handle, PadVrControllerData* state) { (void)handle; (void)state; NotImplemented_nid_no_patch(__func__); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 10146571bc..3fdf755b89 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -9,7 +9,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw -- [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown +- [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false From a159f77523f4a018fcd11169bb96854720b10989 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:14:28 +0300 Subject: [PATCH 0818/1360] fix(npauthdialog): restore parameter validation and lifecycle --- .../prx/libSceCommonDialog/CommonDialog.hpp | 18 ++++++ .../CMakeLists.txt | 4 +- .../Export.cpp | 26 ++++++--- .../tests/GuestNpAuthAuthorizedAppDialog.cpp | 58 +++++++++++++++---- .../debt/libSceNpAuthAuthorizedAppDialog.md | 3 +- 5 files changed, 88 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index bb2d229e7c..a1f4710b79 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -12,6 +12,24 @@ struct CommonDialogBaseParam { std::uint32_t magic; }; +struct NpAuthAuthorizedAppDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::int32_t userId; + std::uint32_t padding60; + const char* application; + const char* text; + std::uint32_t mode; + std::uint32_t padding84; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpAuthAuthorizedAppDialogParam) == 120); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, application) == 64); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, text) == 72); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, mode) == 80); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, reserved) == 88); + struct G2PDialogParam { CommonDialogBaseParam base; std::int32_t userId; diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt index 3d65cfe796..15dcbb4f2e 100644 --- a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt @@ -2,10 +2,12 @@ add_library(libSceNpAuthAuthorizedAppDialog SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNpAuthAuthorizedAppDialog PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNpAuthAuthorizedAppDialog) +target_link_libraries(libSceNpAuthAuthorizedAppDialog PRIVATE libSceUserService) + if(BUILD_TESTING) add_test_executable(guest_np_auth_authorized_app_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppDialog.cpp) target_include_directories(guest_np_auth_authorized_app_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_auth_authorized_app_dialog_tests PRIVATE libSceNpAuthAuthorizedAppDialog) + target_link_libraries(guest_np_auth_authorized_app_dialog_tests PRIVATE libSceNpAuthAuthorizedAppDialog libSceUserService) add_test(NAME guest_np_auth_authorized_app_dialog COMMAND guest_np_auth_authorized_app_dialog_tests) if(WIN32) set_tests_properties(guest_np_auth_authorized_app_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp index f1cbb534c2..9244dedeea 100644 --- a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp @@ -3,8 +3,11 @@ #include <cstddef> #include <cstring> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -12,7 +15,8 @@ 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<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); @@ -30,11 +34,17 @@ int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void) { return 0; } -int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param) { +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const NpAuthAuthorizedAppDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->size != sizeof(*param) || param->userId == -1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::find(std::begin(users.user_id), std::end(users.user_id), param->userId) == std::end(users.user_id)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->application == nullptr || param->application[0] == 0 || param->text == nullptr || param->text[0] == 0) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (std::strlen(param->text) > 1024 || param->mode > 1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; 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; } @@ -47,18 +57,18 @@ int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result) { +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(std::int32_t* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &canceled, sizeof(canceled)); - return 0; + *result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp index 7eecd54c43..aedef7375a 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -1,13 +1,18 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> #include <cstdlib> +#include <string> +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceUserService/UserService.hpp" extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void); -int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param); +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const NpAuthAuthorizedAppDialogParam* param); int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void); int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void); -int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(std::int32_t* result); int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void); int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(void); } @@ -28,7 +33,16 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; + Require(sceUserServiceInitialize(nullptr) == 0); + NpAuthAuthorizedAppDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + char application[136] = "test"; + std::string text(1024, 'a'); + param.application = application; + param.text = text.c_str(); std::int32_t result[2] = {-1, -1}; Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); @@ -45,27 +59,51 @@ int main() { Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); + constexpr int invalidArgument = static_cast<int>(0x80B8000Au); + param.size = 119; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.size = sizeof(param); + param.userId = 42; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.userId = USER_SERVICE_INITIAL_USER_ID; + param.application = nullptr; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.application = application; + param.text = ""; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + text.push_back('a'); + param.text = text.c_str(); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + text.pop_back(); + param.text = text.c_str(); + param.mode = 2; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.mode = 1; + param.reserved[31] = 1; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.reserved[31] = 0; Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == 0); Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusFinished); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); - Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kResultUserCanceled); Require(result[0] == kResultUserCanceled); Require(result[1] == -1); - Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); - Require(sceNpAuthAuthorizedAppDialogClose() == 0); - Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); - result[0] = -1; - Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); + Require(sceNpAuthAuthorizedAppDialogClose() == static_cast<int>(0x80B8000Bu)); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceUserServiceTerminate() == 0); return 0; } diff --git a/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md index e6b5fa19a6..6f98749d82 100644 --- a/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md +++ b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md @@ -2,5 +2,4 @@ ### Silent stubs -- [libSceNpAuthAuthorizedAppDialog](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. The signatures follow the other common dialogs (`Open` takes a parameter block, `GetResult` writes a 32-bit result); Minecraft (PPSA17221) imports the seven functions and fails to load without them. -- [sceNpAuthAuthorizedAppDialogClose](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - `sceNpAuthAuthorizedAppDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED`, also when called before `Open`, following the Signin / NetCtlAp dialogs; MsgDialog / SaveDataDialog / ErrorDialog return NOT_RUNNING or INVALID_STATE for that case, and which one the real library does is unverified. +- [libSceNpAuthAuthorizedAppDialog](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. Parameter fields at offsets 64 and 72 feed a 136-byte application record and a text value of at most 1024 bytes; their service-side meaning remains incomplete. From 420e1aee0767a6bf7643e447dfb3df8b2c08ff3e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:15:31 +0300 Subject: [PATCH 0819/1360] fix(vrsetupdialog): restore parameter header and error codes --- .../prx/libSceCommonDialog/CommonDialog.hpp | 9 ++++++ core/libs/prx/libSceVrSetupDialog/Export.cpp | 23 +++++++------- core/libs/tests/GuestVrSetupDialog.cpp | 30 ++++++++++++------- docs/dev/debt/libSceVrSetupDialog.md | 2 +- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index a1f4710b79..2f2f6381de 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -30,6 +30,15 @@ static_assert(offsetof(NpAuthAuthorizedAppDialogParam, text) == 72); static_assert(offsetof(NpAuthAuthorizedAppDialogParam, mode) == 80); static_assert(offsetof(NpAuthAuthorizedAppDialogParam, reserved) == 88); +struct VrSetupDialogParam { + std::uint64_t size; + std::uint8_t reserved8; + std::uint8_t reserved9; + std::uint8_t data[46]; +}; + +static_assert(sizeof(VrSetupDialogParam) == 56); + struct G2PDialogParam { CommonDialogBaseParam base; std::int32_t userId; diff --git a/core/libs/prx/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp index f4b2718a30..73e247dafe 100644 --- a/core/libs/prx/libSceVrSetupDialog/Export.cpp +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -3,6 +3,7 @@ #include <cstddef> #include <cstring> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -12,9 +13,11 @@ 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<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x8A7C0001u); +constexpr int VR_SETUP_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x8A7C0002u); +constexpr int VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x8A7C0003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); -constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int VR_SETUP_DIALOG_ERROR_BUSY = static_cast<int>(0x8A7C0005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; @@ -26,15 +29,15 @@ extern "C" { int APS5_VABI sceVrSetupDialogInitialize(void) { int expected = COMMON_DIALOG_STATUS_NONE; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return VR_SETUP_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceVrSetupDialogOpen(const void* param) { +int APS5_VABI sceVrSetupDialogOpen(const VrSetupDialogParam* param) { + if (param == nullptr || param->size != sizeof(*param) || param->reserved8 != 0 || param->reserved9 != 0) return VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT; 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; + if (status == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return VR_SETUP_DIALOG_ERROR_BUSY; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -58,13 +61,13 @@ int APS5_VABI sceVrSetupDialogGetResult(void* result) { } int APS5_VABI sceVrSetupDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - g_status = COMMON_DIALOG_STATUS_FINISHED; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() == COMMON_DIALOG_STATUS_RUNNING) g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceVrSetupDialogTerminate(void) { - if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index 56a48ccdd0..b879875b89 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -1,10 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> #include <cstdlib> +#include "prx/libSceCommonDialog/CommonDialog.hpp" extern "C" { int APS5_VABI sceVrSetupDialogInitialize(void); -int APS5_VABI sceVrSetupDialogOpen(const void* param); +int APS5_VABI sceVrSetupDialogOpen(const VrSetupDialogParam* param); int APS5_VABI sceVrSetupDialogUpdateStatus(void); int APS5_VABI sceVrSetupDialogGetStatus(void); int APS5_VABI sceVrSetupDialogGetResult(void* result); @@ -19,8 +20,8 @@ void Require(bool value) { if (!value) std::abort(); } constexpr int kStatusNone = 0; constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; -constexpr int kErrNotInitialized = static_cast<int>(0x80B80003); -constexpr int kErrAlreadyInitialized = static_cast<int>(0x80B80004); +constexpr int kErrNotInitialized = static_cast<int>(0x8A7C0001); +constexpr int kErrAlreadyInitialized = static_cast<int>(0x8A7C0002); constexpr int kErrNotFinished = static_cast<int>(0x80B80005); constexpr int kErrArgNull = static_cast<int>(0x80B8000D); constexpr int kResultUserCanceled = 1; @@ -28,13 +29,14 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; + VrSetupDialogParam param{}; + param.size = sizeof(param); std::int32_t result[2] = {-1, -1}; Require(sceVrSetupDialogTerminate() == kErrNotInitialized); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); Require(sceVrSetupDialogClose() == kErrNotInitialized); - Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceVrSetupDialogGetResult(result) == static_cast<int>(0x80B80003u)); Require(sceVrSetupDialogGetStatus() == kStatusNone); Require(sceVrSetupDialogUpdateStatus() == kStatusNone); @@ -43,9 +45,19 @@ int main() { Require(sceVrSetupDialogGetStatus() == kStatusInitialized); Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); - Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceVrSetupDialogOpen(nullptr) == static_cast<int>(0x8A7C0003u)); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); + param.size = 55; + Require(sceVrSetupDialogOpen(¶m) == static_cast<int>(0x8A7C0003u)); + param.size = sizeof(param); + param.reserved8 = 1; + Require(sceVrSetupDialogOpen(¶m) == static_cast<int>(0x8A7C0003u)); + param.reserved8 = 0; + param.reserved9 = 1; + Require(sceVrSetupDialogOpen(¶m) == static_cast<int>(0x8A7C0003u)); + param.reserved9 = 0; + param.data[45] = 255; Require(sceVrSetupDialogOpen(¶m) == 0); Require(sceVrSetupDialogGetStatus() == kStatusFinished); Require(sceVrSetupDialogUpdateStatus() == kStatusFinished); @@ -64,9 +76,7 @@ int main() { Require(sceVrSetupDialogInitialize() == 0); Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); Require(sceVrSetupDialogClose() == 0); - Require(sceVrSetupDialogUpdateStatus() == kStatusFinished); - result[0] = -1; - Require(sceVrSetupDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); + Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); Require(sceVrSetupDialogTerminate() == 0); } diff --git a/docs/dev/debt/libSceVrSetupDialog.md b/docs/dev/debt/libSceVrSetupDialog.md index 336e3361d1..138686c11e 100644 --- a/docs/dev/debt/libSceVrSetupDialog.md +++ b/docs/dev/debt/libSceVrSetupDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports the library. `sceVrSetupDialogGetStatus` (NID `qRUACYyUNFA`) is present in the firmware export database (aerolib), but direct import by PPSA12544 has not been explicitly confirmed. +- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). Initialization does not check the native process eligibility record, and the 46-byte setup payload remains opaque. Open validates the recovered 56-byte parameter header. GetResult is absent from the supplied module, so its signature and cancellation result remain inferred. From 39cb8593f331d8e96b300d96b7c364b206a3ee4a Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:17:58 +0300 Subject: [PATCH 0820/1360] fix(usbstoragedialog): restore parameters and result contract --- core/libs/CMakeLists.txt | 4 +- .../prx/libSceCommonDialog/CommonDialog.hpp | 21 ++++++ .../prx/libSceUsbStorageDialog/Export.cpp | 40 ++++++++--- core/libs/tests/GuestUsbStorageDialog.cpp | 69 ++++++++++++++++--- docs/dev/debt/libSceUsbStorageDialog.md | 3 +- 5 files changed, 118 insertions(+), 19 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..84952285be 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -186,6 +186,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() +target_link_libraries(libSceUsbStorageDialog PRIVATE libSceUserService) + if(APS5_ENABLE_TRACY) set_target_properties(TracyClient PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} @@ -406,7 +408,7 @@ if(BUILD_TESTING) add_test_executable(guest_usb_storage_dialog_tests tests/GuestUsbStorageDialog.cpp) target_include_directories(guest_usb_storage_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libc) + target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libSceUserService libc) configure_windows_unwind(guest_usb_storage_dialog_tests) add_test(NAME guest_usb_storage_dialog COMMAND guest_usb_storage_dialog_tests) if(WIN32) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index 2f2f6381de..3138618de7 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -39,6 +39,27 @@ struct VrSetupDialogParam { static_assert(sizeof(VrSetupDialogParam) == 56); +struct UsbStorageDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + char path[1024]; + std::int32_t userId; + std::uint32_t padding; +}; + +struct UsbStorageDialogResult { + std::uint64_t size; + std::int32_t result; + std::int32_t status; + std::uint8_t reserved[64]; +}; + +static_assert(offsetof(UsbStorageDialogParam, path) == 56); +static_assert(offsetof(UsbStorageDialogParam, userId) == 1080); +static_assert(sizeof(UsbStorageDialogResult) == 80); +static_assert(offsetof(UsbStorageDialogResult, result) == 8); +static_assert(offsetof(UsbStorageDialogResult, status) == 12); + struct G2PDialogParam { CommonDialogBaseParam base; std::int32_t userId; diff --git a/core/libs/prx/libSceUsbStorageDialog/Export.cpp b/core/libs/prx/libSceUsbStorageDialog/Export.cpp index 78a410fe58..612ac95a57 100644 --- a/core/libs/prx/libSceUsbStorageDialog/Export.cpp +++ b/core/libs/prx/libSceUsbStorageDialog/Export.cpp @@ -3,8 +3,12 @@ #include <cstddef> #include <cstring> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -12,7 +16,7 @@ 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<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); @@ -20,22 +24,36 @@ constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; +std::uint32_t g_sdkVersion = 0; +std::atomic_bool g_closed{false}; } extern "C" { int APS5_VABI sceUsbStorageDialogInitialize(void) { + if (g_status.load() != COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 16, sizeof(sdkVersion)); int expected = COMMON_DIALOG_STATUS_NONE; if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + g_sdkVersion = sdkVersion; + g_closed = false; return 0; } -int APS5_VABI sceUsbStorageDialogOpen(const void* param) { +int APS5_VABI sceUsbStorageDialogOpen(const UsbStorageDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + UserServiceLoginUserIdList users{}; + if (param->userId == -1 || sceUserServiceGetLoginUserIdList(&users) != 0 || std::find(std::begin(users.user_id), std::end(users.user_id), param->userId) == std::end(users.user_id)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->path[1023] != 0 || param->path[0] == 0) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (g_sdkVersion >= 0x04500000 && (std::strpbrk(param->path, ",*?\"<>|") != nullptr || std::strstr(param->path, "..") != nullptr)) return static_cast<int>(0x812D0003u); if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_closed = false; g_status = COMMON_DIALOG_STATUS_RUNNING; return 0; } @@ -50,20 +68,26 @@ int APS5_VABI sceUsbStorageDialogUpdateStatus(void) { return g_status.load(); } -int APS5_VABI sceUsbStorageDialogGetResult(void* result) { +int APS5_VABI sceUsbStorageDialogGetResult(UsbStorageDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; 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 (g_closed.load()) { + result->result = COMMON_DIALOG_ERROR_NOT_FINISHED; + result->status = 0; + return 0; + } if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - const std::int32_t dialogResult = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &dialogResult, sizeof(dialogResult)); - return 0; + if (g_sdkVersion >= 0x04500000 && (result->size != sizeof(*result) || result->result != COMMON_DIALOG_ERROR_BUSY)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI sceUsbStorageDialogClose(void) { 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_NOT_RUNNING; + g_closed = true; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestUsbStorageDialog.cpp b/core/libs/tests/GuestUsbStorageDialog.cpp index 5217f66be0..e21bcc50eb 100644 --- a/core/libs/tests/GuestUsbStorageDialog.cpp +++ b/core/libs/tests/GuestUsbStorageDialog.cpp @@ -2,13 +2,20 @@ #include <cstdint> #include <cstdlib> #include <cstring> +#include <array> +#include <string> +#include "ProcessParametersFixture.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceUserService/UserService.hpp" extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceUsbStorageDialogInitialize(void); -int APS5_VABI sceUsbStorageDialogOpen(const void* param); +int APS5_VABI sceUsbStorageDialogOpen(const UsbStorageDialogParam* param); int APS5_VABI sceUsbStorageDialogUpdateStatus(void); int APS5_VABI sceUsbStorageDialogGetStatus(void); -int APS5_VABI sceUsbStorageDialogGetResult(void* result); +int APS5_VABI sceUsbStorageDialogGetResult(UsbStorageDialogResult* result); int APS5_VABI sceUsbStorageDialogClose(void); int APS5_VABI sceUsbStorageDialogTerminate(void); } @@ -32,8 +39,21 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; - std::int32_t result = 0x5a5a5a5a; + static std::array<std::byte, 64> process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + std::uint32_t sdkVersion = 0x04500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceUserServiceInitialize(nullptr) == 0); + UsbStorageDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + std::strcpy(param.path, "test"); + UsbStorageDialogResult result; + std::memset(&result, 0x5a, sizeof(result)); + result.size = sizeof(result); + result.result = kErrBusy; Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); Require(sceUsbStorageDialogOpen(¶m) == kErrNotInitialized); @@ -49,6 +69,17 @@ int main() { Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); Require(sceUsbStorageDialogGetResult(&result) == kErrNotFinished); + param.path[1023] = 'a'; + Require(sceUsbStorageDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.path[1023] = 0; + std::strcpy(param.path, "a..b"); + Require(sceUsbStorageDialogOpen(¶m) == static_cast<int>(0x812D0003u)); + for (const char invalid : std::string(",*?\"<>|")) { + param.path[0] = invalid; + param.path[1] = 0; + Require(sceUsbStorageDialogOpen(¶m) == static_cast<int>(0x812D0003u)); + } + std::strcpy(param.path, "test"); Require(sceUsbStorageDialogOpen(¶m) == 0); Require(sceUsbStorageDialogOpen(¶m) == kErrBusy); Require(sceUsbStorageDialogGetStatus() == kStatusRunning); @@ -58,8 +89,16 @@ int main() { Require(sceUsbStorageDialogGetStatus() == kStatusFinished); Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); - Require(sceUsbStorageDialogGetResult(&result) == 0); - Require(result == kResultUserCanceled); + result.size = 79; + Require(sceUsbStorageDialogGetResult(&result) == static_cast<int>(0x80B8000Au)); + result.size = sizeof(result); + result.result = 0; + Require(sceUsbStorageDialogGetResult(&result) == static_cast<int>(0x80B8000Au)); + result.result = kErrBusy; + Require(sceUsbStorageDialogGetResult(&result) == kResultUserCanceled); + Require(result.result == kResultUserCanceled); + Require(result.size == sizeof(result) && result.status == 0x5a5a5a5a); + for (const auto value : result.reserved) Require(value == 0x5a); Require(sceUsbStorageDialogClose() == kErrNotRunning); Require(sceUsbStorageDialogTerminate() == 0); @@ -71,9 +110,23 @@ int main() { Require(sceUsbStorageDialogGetStatus() == kStatusRunning); Require(sceUsbStorageDialogClose() == 0); Require(sceUsbStorageDialogGetStatus() == kStatusFinished); - result = 0; + result.result = 0; Require(sceUsbStorageDialogGetResult(&result) == 0); - Require(result == kResultUserCanceled); + Require(result.result == kErrNotFinished && result.status == 0); Require(sceUsbStorageDialogTerminate() == 0); + sdkVersion = 0x04400000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceUsbStorageDialogInitialize() == 0); + std::strcpy(param.path, "a..b"); + Require(sceUsbStorageDialogOpen(¶m) == 0); + Require(sceUsbStorageDialogUpdateStatus() == kStatusFinished); + result.size = 0; + result.result = 0; + Require(sceUsbStorageDialogGetResult(&result) == kResultUserCanceled); + Require(result.size == 0 && result.result == kResultUserCanceled); + Require(sceUsbStorageDialogTerminate() == 0); + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); + Require(sceUserServiceTerminate() == 0); return 0; } diff --git a/docs/dev/debt/libSceUsbStorageDialog.md b/docs/dev/debt/libSceUsbStorageDialog.md index 93445b0d4a..58151aaf70 100644 --- a/docs/dev/debt/libSceUsbStorageDialog.md +++ b/docs/dev/debt/libSceUsbStorageDialog.md @@ -6,5 +6,4 @@ ### Unknown function info -- [sceUsbStorageDialogGetResult](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) (libSceUsbStorageDialog) - `SceUsbStorageDialogResult` full layout, size and additional fields are unverified across open references (KytyPS5, shadPS4, fpPS4, SharpProspero); only the 4-byte `result` field (`COMMON_DIALOG_RESULT_USER_CANCELED = 1`) is written to avoid guest memory corruption -- [sceUsbStorageDialogOpen, sceUsbStorageDialogClose](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) (libSceUsbStorageDialog) - the relative validation precedence between uninitialized state, busy/running state, and null parameter checks is inferred from standard common dialog conventions and unverified against real console firmware +- [libSceUsbStorageDialog](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) - the service-specific result status at offset 12 is not produced because the UI always cancels. The 80-byte result header and SDK-dependent validation are recovered; the remaining 64 bytes are untouched by the native wrapper. The purpose of the Open parameter size field remains unverified; the native wrapper does not validate it. From b688c4044bf9bdb860a84e438c4397860088c145 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:24:45 +0300 Subject: [PATCH 0821/1360] fix(share): validate extended capture parameters --- core/libs/prx/libSceShare/Export.cpp | 49 +++++++++++++++++++--------- core/libs/tests/GuestShare.cpp | 33 +++++++++++++------ docs/dev/debt/libSceShare.md | 6 ++-- 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index a313edc364..375682887e 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -6,6 +6,28 @@ // Capture and sharing are not emulated; policy and parameter calls are accepted. +struct ShareScreenshotParamExtended { + std::uint64_t size; + std::uint32_t source; + std::uint32_t mode; + const char* text; +}; + +struct ShareVideoClipParamExtended { + std::uint64_t size; + std::uint64_t value8; + std::uint64_t value16; + std::uint32_t mode; + std::uint32_t reserved; + const char* text; +}; + +static_assert(sizeof(ShareScreenshotParamExtended) == 24); +static_assert(offsetof(ShareScreenshotParamExtended, text) == 16); +static_assert(sizeof(ShareVideoClipParamExtended) == 40); +static_assert(offsetof(ShareVideoClipParamExtended, mode) == 24); +static_assert(offsetof(ShareVideoClipParamExtended, text) == 32); + namespace { constexpr std::int32_t ERROR_INVALID_PARAM = static_cast<std::int32_t>(0x81960002); @@ -96,20 +118,16 @@ int APS5_VABI sceShareUnregisterContentEventCallback(void* callback) { return 0; } -int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, int32_t* req_id) { - (void)extended_param; - if (req_id != nullptr) { - *req_id = REQUEST_ID_INVALID; - } +int APS5_VABI sceShareCaptureScreenshotExtended(const ShareScreenshotParamExtended* param, std::int32_t* requestId) { + if (param == nullptr || param->size != sizeof(*param)) return ERROR_INVALID_PARAM; + if (requestId != nullptr) *requestId = REQUEST_ID_INVALID; return ERROR_NOT_SUPPORTED; } - -int APS5_VABI sceShareCaptureVideoClipExtended(const void* extended_param, int32_t* req_id) { - (void)extended_param; - if (req_id != nullptr) { - *req_id = REQUEST_ID_INVALID; - } +int APS5_VABI sceShareCaptureVideoClipExtended(const ShareVideoClipParamExtended* param, std::int32_t* requestId) { + if (param == nullptr || param->size != sizeof(*param)) return ERROR_INVALID_PARAM; + if (requestId != nullptr) *requestId = REQUEST_ID_INVALID; + if (param->reserved != 0 || (param->mode == 0 && param->value16 != 0)) return ERROR_INVALID_PARAM; return ERROR_NOT_SUPPORTED; } @@ -121,10 +139,11 @@ int APS5_VABI sceShareGetRunningStatus(uint32_t* status) { return 0; } -int APS5_VABI sceShareSetContentParamForApplicationTitle(const char* application_title) { - if (application_title == nullptr) { - return ERROR_INVALID_PARAM; - } +int APS5_VABI sceShareSetContentParamForApplicationTitle(const char* applicationTitle) { + if (applicationTitle == nullptr) return ERROR_INVALID_PARAM; + std::size_t length = 0; + while (length < 5120 && applicationTitle[length] != 0) ++length; + if (length == 0 || length == 5120) return ERROR_INVALID_PARAM; return 0; } diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 698b357e1e..0ef5a51bc5 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -3,6 +3,7 @@ #include <cstdlib> #include <cstring> #include <initializer_list> +#include <string> extern "C" { int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, std::int32_t* req_id); @@ -27,17 +28,25 @@ int main() { using Capture = int (APS5_VABI*)(const void*, std::int32_t*); std::int32_t reqId = 7; + constexpr std::int32_t invalidParam = static_cast<std::int32_t>(0x81960002); for (Capture capture : {sceShareCaptureScreenshotExtended, sceShareCaptureVideoClipExtended}) { + std::uint64_t fields[5]{}; + const std::uint64_t size = capture == sceShareCaptureScreenshotExtended ? 24 : 40; reqId = 7; - Require(capture(param, &reqId) == notSupported); - Require(reqId == -1); - - reqId = 7; - Require(capture(nullptr, &reqId) == notSupported); - Require(reqId == -1); - - Require(capture(param, nullptr) == notSupported); - Require(capture(nullptr, nullptr) == notSupported); + Require(capture(fields, &reqId) == invalidParam && reqId == 7); + Require(capture(nullptr, &reqId) == invalidParam && reqId == 7); + fields[0] = size; + Require(capture(fields, &reqId) == notSupported && reqId == -1); + Require(capture(fields, nullptr) == notSupported); + if (size == 40) { + fields[2] = 1; + reqId = 7; + Require(capture(fields, &reqId) == invalidParam && reqId == -1); + fields[3] = 1; + Require(capture(fields, nullptr) == notSupported); + fields[3] |= std::uint64_t{1} << 32; + Require(capture(fields, nullptr) == invalidParam); + } } for (Capture capture : {sceShareCaptureScreenshot, sceShareCaptureVideoClip}) { @@ -53,7 +62,6 @@ int main() { Require(sceShareOpenMenuForContent(param) == notSupported); Require(sceShareOpenMenuForContent(nullptr) == notSupported); - constexpr std::int32_t invalidParam = static_cast<std::int32_t>(0x81960002); std::uint8_t status[18]; std::memset(status, 0x5a, sizeof(status)); Require(sceShareGetCurrentStatus(1, status) == 0); @@ -71,5 +79,10 @@ int main() { Require(sceShareGetRunningStatus(nullptr) == invalidParam); Require(sceShareSetContentParamForApplicationTitle("title") == 0); Require(sceShareSetContentParamForApplicationTitle(nullptr) == invalidParam); + Require(sceShareSetContentParamForApplicationTitle("") == invalidParam); + std::string title(5119, 'a'); + Require(sceShareSetContentParamForApplicationTitle(title.c_str()) == 0); + title.push_back('a'); + Require(sceShareSetContentParamForApplicationTitle(title.c_str()) == invalidParam); return 0; } diff --git a/docs/dev/debt/libSceShare.md b/docs/dev/debt/libSceShare.md index 8ad04d2333..e9062c9e03 100644 --- a/docs/dev/debt/libSceShare.md +++ b/docs/dev/debt/libSceShare.md @@ -3,9 +3,9 @@ ### Unknown function info - [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 -- [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - only the first 32-bit word of the status is known: PPSA01317 treats bits 0x3c of it as a running share feature and gives the call a 40-byte buffer. The host never records or broadcasts, so it stores 0 in that word and leaves the rest untouched; a null status returns `INVALID_PARAM` by analogy with `sceShareGetCurrentStatus` -- [sceShareSetContentParamForApplicationTitle](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Unity plugins; the signature follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libShare.cpp), not confirmed on a PS5 title. The application title is only checked for null, as KytyPS5 does, since nothing is captured +- [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) - the native wrapper writes only the first 32-bit word, populated from a kernel event flag. The host reports zero because capture and broadcast services are absent; native initialization checks and event-flag failures are not modeled. +- [sceShareSetContentParamForApplicationTitle](../../../core/libs/prx/libSceShare/Export.cpp) - the recovered string length limit is 1 through 5119 bytes. The title is validated but not sent to the capture service; native initialization and service failures are not modeled. ### Functional -- [sceShareCaptureVideoClipExtended](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Share plugin; the signature (extended parameter, request id) and the answer follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/147188b143f67c27d8da396e759c19336f64c105/src/libs/libShare.cpp), not confirmed on a PS5 title. Nothing is captured, so, like `sceShareCaptureScreenshotExtended`, it stores the invalid request id -1 when the pointer is not null and returns `NOT_SUPPORTED` without reading the extended parameter, whose layout is unknown +- [sceShareCaptureVideoClipExtended, sceShareCaptureScreenshotExtended](../../../core/libs/prx/libSceShare/Export.cpp) - the 40-byte video and 24-byte screenshot parameters and request-ID write precedence are recovered. Video values at offsets 8 and 16 and screenshot mode semantics remain incomplete. Valid requests still return NOT_SUPPORTED without capture; screenshot mode 2 platform checks, throttling, initialization and the backend text limit are not modeled. From 340429d151ff9294e8b7fc5aba52b7263aff0fef Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:24:46 +0300 Subject: [PATCH 0822/1360] fix(videorecording): restore parameter layouts and handle widths --- .../libs/prx/libSceVideoRecordingP/Export.cpp | 53 ++++++++++++++++--- docs/dev/debt/libSceVideoRecordingP.md | 15 +++--- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index a5cdee3d3a..93cf413444 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -9,6 +9,43 @@ #include <string> #include <vector> +struct VideoRecordingContext; + +struct VideoRecordingSupportInfo { + std::uint8_t data[18]; +}; + +static_assert(sizeof(VideoRecordingSupportInfo) == 18); + +struct VideoRecordingParam { + std::uint64_t size; + std::int32_t threadPriority; + std::uint32_t duration; + std::uint32_t mode; + std::int32_t outputMode; + std::uint32_t option; + std::uint32_t format; +}; + +struct VideoRecordingParam2 { + std::uint64_t size; + std::uint64_t affinityMask; + std::int32_t threadPriority; + std::uint32_t duration; + std::uint32_t mode; + std::int32_t outputMode; + std::uint32_t option; + std::uint32_t format; +}; + +static_assert(sizeof(VideoRecordingParam) == 32); +static_assert(offsetof(VideoRecordingParam, duration) == 12); +static_assert(offsetof(VideoRecordingParam, format) == 28); +static_assert(sizeof(VideoRecordingParam2) == 40); +static_assert(offsetof(VideoRecordingParam2, threadPriority) == 16); +static_assert(offsetof(VideoRecordingParam2, duration) == 20); +static_assert(offsetof(VideoRecordingParam2, format) == 36); + namespace { std::mutex g_infoMutex; std::map<int32_t, std::vector<unsigned char>> g_info; @@ -28,7 +65,7 @@ int APS5_VABI sceVideoRecordingClose(int discard) { return 0; } -int APS5_VABI sceVideoRecordingCloseByHandle(int handle, int discard) { +int APS5_VABI sceVideoRecordingCloseByHandle(VideoRecordingContext* handle, int discard) { (void)handle; (void)discard; NotImplemented_nid_no_patch(__func__); @@ -54,7 +91,7 @@ int APS5_VABI sceVideoRecordingGetInfo(int32_t info, void* data, size_t size) { return 0; } -int APS5_VABI sceVideoRecordingGetInfoByHandle(int handle, int32_t info, void* data, size_t size) { +int APS5_VABI sceVideoRecordingGetInfoByHandle(VideoRecordingContext* handle, int32_t info, void* data, size_t size) { (void)handle; (void)info; (void)data; @@ -67,7 +104,7 @@ int APS5_VABI sceVideoRecordingGetStatus(void) { return SceVideoRecordingStatusNone; } -int APS5_VABI sceVideoRecordingGetSupportInfo(void* info) { +int APS5_VABI sceVideoRecordingGetSupportInfo(VideoRecordingSupportInfo* info) { (void)info; NotImplemented_nid_no_patch(__func__); return 0; @@ -89,7 +126,7 @@ int APS5_VABI sceVideoRecordingInitialize(void* param) { return 0; } -int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* heap, int heapSize) { +int APS5_VABI sceVideoRecordingOpen(const char* path, const VideoRecordingParam* param, void* heap, int heapSize) { (void)path; (void)param; (void)heap; @@ -98,7 +135,7 @@ int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* h return 0; } -int APS5_VABI sceVideoRecordingOpen2(const char* path, const void* param, void* heap, int heapSize) { +int APS5_VABI sceVideoRecordingOpen2(const char* path, const VideoRecordingParam2* param, void* heap, int heapSize) { (void)path; (void)param; (void)heap; @@ -107,19 +144,19 @@ int APS5_VABI sceVideoRecordingOpen2(const char* path, const void* param, void* return 0; } -int APS5_VABI sceVideoRecordingQueryHddSize2(const void* param) { +std::int64_t APS5_VABI sceVideoRecordingQueryHddSize2(const VideoRecordingParam2* param) { (void)param; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoRecordingQueryMemSize(const void* param) { +int APS5_VABI sceVideoRecordingQueryMemSize(const VideoRecordingParam* param) { (void)param; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoRecordingQueryMemSize2(const void* param) { +int APS5_VABI sceVideoRecordingQueryMemSize2(const VideoRecordingParam2* param) { (void)param; NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md index 50c98e9fc0..ec40ae9fd8 100644 --- a/docs/dev/debt/libSceVideoRecordingP.md +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -3,17 +3,14 @@ ### Unknown function info - [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - declared with `void(void* param)` signature following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L67-L69); unimplemented and throws `NotImplemented_nid_no_patch` -- [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - inferred handle-based recording control signatures taking an integer recording handle; unimplemented and throw `NotImplemented_nid_no_patch` -- [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - inferred pixel buffer conversion signature `(void* dst, const void* src, uint32_t width, uint32_t height)`; unimplemented and throws `NotImplemented_nid_no_patch` -- [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - query signatures taking an output pointer; exact structure layouts and return values are unverified and throw `NotImplemented_nid_no_patch` +- [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. +- [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. +- [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode is absent from the supplied recording modules, so its signature remains unverified. Both exports remain unimplemented. - [sceVideoRecordingInit, sceVideoRecordingTerm](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - parameterless initialization and termination stubs; unimplemented and throw `NotImplemented_nid_no_patch` - [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - initialization stub taking an optional parameter structure pointer; unverified and throws `NotImplemented_nid_no_patch` -- [sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - extended recording open signature; parameters inferred from `sceVideoRecordingOpen` and [fpPS4](https://github.com/red-prig/fpPS4); unimplemented and throws `NotImplemented_nid_no_patch` -- [sceVideoRecordingQueryHddSize2, sceVideoRecordingQueryMemSize2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - memory and disk size query signatures taking parameter pointer; unverified and throw `NotImplemented_nid_no_patch` -- [sceVideoRecordingQueryMemSize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - signature `int(const SceVideoRecordingParam*)` from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L75-L77), which says only that it reports the heap bytes needed for the settings in the parameter; how the size follows from `ringSec` and the format is not documented. [`MaxMemSize` (4096)](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L64-L65) is the largest heap the recorder needs, an upper bound and not the size for given settings, so returning it would be invented. Throws `NotImplemented_nid_no_patch` -- [sceVideoRecordingOpen, sceVideoRecordingStart, sceVideoRecordingStop, sceVideoRecordingClose](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - signatures from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L79-L101). The recorder is not modelled, so these throw `NotImplemented_nid_no_patch` instead of pretending to record. SharpProspero documents no error code for an unavailable recorder; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/video_recording/video_recording_error.h#L9-L18) lists codes such as `0x80A80008` (UNSUPPORTED), but nothing shows the console returns one here -- [sceVideoRecordingRestModeProcessGetStatus, sceVideoRecordingRestModeProcessStart](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - rest mode recording interfaces; unverified and throw `NotImplemented_nid_no_patch` -- [sceVideoRecordingSendCommand](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - command dispatch signature taking integer command ID and parameter pointer; unverified and throws `NotImplemented_nid_no_patch` +- [sceVideoRecordingOpen, sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the 32-byte and 40-byte parameter variants, four argument registers and 32-bit heap size are recovered. Mode, output mode, option and format semantics remain incomplete. QueryHddSize2 returns a signed 64-bit size or error; recording and sizing exports remain unimplemented. + +- [Handle and support queries](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native recording handles are context pointers, and GetSupportInfo writes 18 bytes. Support payload field semantics remain incomplete. RestModeProcessStart, RestModeProcessGetStatus and SendCommand consume no caller arguments in the supplied native module; their existing unused parameter slots remain because the protected bodies reference those parameter names. ### Functional From f414adb3d6501f7ac696985d7c68427e93caf526 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:24:47 +0300 Subject: [PATCH 0823/1360] docs(kernel): close thread sanitizer signature debt --- docs/dev/debt/libkernel.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 77f9284967..6a4136fc3c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -8,7 +8,7 @@ - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) -- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) returns 0, as `sceKernelIsAddressSanitizerEnabled` does: a converted title never runs under the thread sanitizer, so a title that checks it skips the [libSceDbgThreadSanitizer](../../../core/libs/prx/libSceDbgThreadSanitizer/Export.cpp) annotation calls, which throw. PPSA23226's bundled `libSceJobManager_nosubmission.prx` makes those calls only when it reports nonzero +- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. ### Unknown function info @@ -109,7 +109,6 @@ - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws From d109d51f43196806b932d8e71308d8ce8171d8a1 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:27:03 +0300 Subject: [PATCH 0824/1360] fix(opus): restore decoder validation errors and precedence --- core/libs/prx/libSceOpusDec/Export.cpp | 4 +-- .../prx/libSceOpusDec/include/OpusDecoder.hpp | 29 ++++++++++++------- core/libs/tests/GuestOpusCeltDec.cpp | 25 +++++++++++++--- core/libs/tests/GuestOpusDec.cpp | 29 +++++++++++++++---- docs/dev/debt/libSceOpusCeltDec.md | 2 +- docs/dev/debt/libSceOpusDec.md | 2 +- 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index a02f1d05be..ebf78206f4 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -13,11 +13,11 @@ extern "C" { int APS5_VABI sceOpusDecInitialize(std::uint32_t* context) { return library.Initialize(context); } -int APS5_VABI sceOpusDecTerminate(std::uint32_t* context) { return library.Terminate(context); } +int APS5_VABI sceOpusDecTerminate(const std::uint32_t* context) { return library.Terminate(context); } int APS5_VABI sceOpusDecGetSize(int channels) { return library.GetSize(channels); } -int APS5_VABI sceOpusDecCreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { +int APS5_VABI sceOpusDecCreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { return library.CreateEx(context, state, sampleRate, channels); } diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp index 818381ea7c..fee4c48957 100644 --- a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -27,7 +27,7 @@ public: OpusDecoderLibrary(const char* libraryName, bool celtPacketsOnly) : name(libraryName), celtOnly(celtPacketsOnly) {} int Initialize(std::uint32_t* context) { - if (!context) fail("null context output"); + if (!context) return error(13); std::lock_guard lock(mutex); for (const auto& [id, owner] : contexts) if (owner == context) fail("context already initialized"); @@ -49,15 +49,15 @@ public: } int GetSize(int channels) { - if (channels != 1 && channels != 2) fail("only mono and stereo are supported"); + if (channels != 1 && channels != 2) return error(3); return 640; } int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { - if (!context || !state) fail("null context or state"); - GetSize(channels); - if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) - fail("sample rates other than 8000, 12000, 16000, 24000 and 48000 Hz are not supported"); + if (!context || !state) return error(0); + if (celtOnly && channels != 1 && channels != 2) return error(3); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(2); + if (channels != 1 && channels != 2) return error(3); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); if (decoders.contains(state)) fail("state already owns a decoder"); @@ -84,11 +84,15 @@ public: } int Decode(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity) { - if (!data || bytes <= 0 || bytes > std::numeric_limits<int>::max() - AV_INPUT_BUFFER_PADDING_SIZE || !pcm || capacity < 0) - fail("invalid packet or PCM buffer; packet loss concealment is not implemented"); + if (!state) return error(0); + if (bytes != 0 && !data) return error(6); + if (bytes < 0) return error(7); + if (!pcm) return error(8); + if (capacity < 0) return error(9); std::lock_guard lock(mutex); const auto found = decoders.find(state); - if (found == decoders.end()) fail("unknown decoder state"); + if (found == decoders.end()) return error(1); + if (bytes == 0 || bytes > std::numeric_limits<int>::max() - AV_INPUT_BUFFER_PADDING_SIZE) fail("packet loss concealment or oversized packet is not implemented"); auto& decoder = *found->second; if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); const int samples = packetSamples(data, bytes) / (48000 / decoder.sampleRate); @@ -135,8 +139,9 @@ public: } int Destroy(void* state) { + if (!state) return error(0); std::lock_guard lock(mutex); - if (decoders.erase(state) == 0) fail("unknown decoder state"); + if (decoders.erase(state) == 0) return error(1); return 0; } @@ -161,6 +166,10 @@ private: std::unordered_map<void*, std::unique_ptr<Decoder>> decoders; std::uint32_t nextContext = 1; + int error(std::uint32_t offset) const { + return static_cast<int>((celtOnly ? 0x81400000u : 0x81740000u) + offset); + } + [[noreturn]] void fail(const char* message) const { throw std::runtime_error(std::string(name) + ": " + message); } diff --git a/core/libs/tests/GuestOpusCeltDec.cpp b/core/libs/tests/GuestOpusCeltDec.cpp index d6422e910b..3245cc824e 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -75,11 +75,28 @@ void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { } // namespace int main() { + constexpr std::uint32_t errorBase = 0x81400000u; + Require(sceOpusCeltDecInitialize(nullptr) == static_cast<int>(errorBase + 13)); + Require(sceOpusCeltDecDestroy(nullptr) == static_cast<int>(errorBase)); std::uint32_t context = 0; Require(sceOpusCeltDecInitialize(&context) == 0); + std::array<std::uint8_t, 640> invalidState{}; + std::int16_t output = 12345; + const std::uint8_t packet = 0; + Require(sceOpusCeltDecCreateEx(nullptr, invalidState.data(), 0, 0) == static_cast<int>(errorBase)); + Require(sceOpusCeltDecCreateEx(&context, invalidState.data(), 0, 0) == static_cast<int>(errorBase + 3)); + Require(sceOpusCeltDecCreateEx(&context, invalidState.data(), 48000, 0) == static_cast<int>(errorBase + 3)); + Require(sceOpusCeltDecDecode(nullptr, nullptr, -1, nullptr, -1) == static_cast<int>(errorBase)); + Require(sceOpusCeltDecDecode(invalidState.data(), nullptr, -1, nullptr, -1) == static_cast<int>(errorBase + 6)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, -1, nullptr, -1) == static_cast<int>(errorBase + 7)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, 1, nullptr, -1) == static_cast<int>(errorBase + 8)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, 1, &output, -1) == static_cast<int>(errorBase + 9)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, 1, &output, 2) == static_cast<int>(errorBase + 1)); + Require(output == 12345); + RequireThrows([&] { sceOpusCeltDecInitialize(&context); }); Require(sceOpusCeltDecGetSize(1) == 640 && sceOpusCeltDecGetSize(2) == 640); - RequireThrows([&] { sceOpusCeltDecGetSize(3); }); + Require(sceOpusCeltDecGetSize(3) == static_cast<int>(errorBase + 3)); std::array<std::uint8_t, 640> state{}, monoState{}; Require(sceOpusCeltDecCreateEx(&context, state.data(), 48000, 2) == 0); Require(sceOpusCeltDecCreateEx(&context, monoState.data(), 48000, 1) == 0); @@ -109,7 +126,7 @@ int main() { std::array<std::uint8_t, 640> unsupported{}; for (int rate : {0, 22050, 44100, 96000}) - RequireThrows([&] { sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2); }); + Require(sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast<int>(errorBase + 2)); for (int rate : {8000, 12000, 16000, 24000, 48000}) { std::array<std::uint8_t, 640> toneState{}; Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0); @@ -126,8 +143,8 @@ int main() { } Require(sceOpusCeltDecDestroy(state.data()) == 0); - RequireThrows([&] { sceOpusCeltDecDestroy(state.data()); }); - RequireThrows([&] { sceOpusCeltDecDecode(state.data(), CELT_STEREO + 2, CELT_STEREO[0], pcm.data(), 3840); }); + Require(sceOpusCeltDecDestroy(state.data()) == static_cast<int>(errorBase + 1)); + Require(sceOpusCeltDecDecode(state.data(), CELT_STEREO + 2, CELT_STEREO[0], pcm.data(), 3840) == static_cast<int>(errorBase + 1)); Require(sceOpusCeltDecDestroy(monoState.data()) == 0); Require(sceOpusCeltDecTerminate(&context) == 0); RequireThrows([&] { sceOpusCeltDecTerminate(&context); }); diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index a5824ee855..e19fb78141 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -8,9 +8,9 @@ extern "C" { int APS5_VABI sceOpusDecInitialize(std::uint32_t*); -int APS5_VABI sceOpusDecTerminate(std::uint32_t*); +int APS5_VABI sceOpusDecTerminate(const std::uint32_t*); int APS5_VABI sceOpusDecGetSize(int); -int APS5_VABI sceOpusDecCreateEx(std::uint32_t*, void*, int, int); +int APS5_VABI sceOpusDecCreateEx(const std::uint32_t*, void*, int, int); int APS5_VABI sceOpusDecDecode(void*, const std::uint8_t*, int, std::int16_t*, int); int APS5_VABI sceOpusDecDestroy(void*); } @@ -49,8 +49,25 @@ const std::uint8_t OPUS_NARROWBAND[] = { } // namespace int main() { + constexpr std::uint32_t errorBase = 0x81740000u; + Require(sceOpusDecInitialize(nullptr) == static_cast<int>(errorBase + 13)); + Require(sceOpusDecDestroy(nullptr) == static_cast<int>(errorBase)); std::uint32_t context = 0, otherContext = 0; Require(sceOpusDecInitialize(&context) == 0); + std::array<std::uint8_t, 640> invalidState{}; + std::int16_t output = 12345; + const std::uint8_t packet = 0; + Require(sceOpusDecCreateEx(nullptr, invalidState.data(), 0, 0) == static_cast<int>(errorBase)); + Require(sceOpusDecCreateEx(&context, invalidState.data(), 0, 0) == static_cast<int>(errorBase + 2)); + Require(sceOpusDecCreateEx(&context, invalidState.data(), 48000, 0) == static_cast<int>(errorBase + 3)); + Require(sceOpusDecDecode(nullptr, nullptr, -1, nullptr, -1) == static_cast<int>(errorBase)); + Require(sceOpusDecDecode(invalidState.data(), nullptr, -1, nullptr, -1) == static_cast<int>(errorBase + 6)); + Require(sceOpusDecDecode(invalidState.data(), &packet, -1, nullptr, -1) == static_cast<int>(errorBase + 7)); + Require(sceOpusDecDecode(invalidState.data(), &packet, 1, nullptr, -1) == static_cast<int>(errorBase + 8)); + Require(sceOpusDecDecode(invalidState.data(), &packet, 1, &output, -1) == static_cast<int>(errorBase + 9)); + Require(sceOpusDecDecode(invalidState.data(), &packet, 1, &output, 2) == static_cast<int>(errorBase + 1)); + Require(output == 12345); + Require(sceOpusDecInitialize(&otherContext) == 0 && context != otherContext); RequireThrows([&] { sceOpusDecInitialize(&context); }); Require(sceOpusDecGetSize(1) == 640 && sceOpusDecGetSize(2) == 640); @@ -60,10 +77,10 @@ int main() { Require(sceOpusDecCreateEx(&context, monoState.data(), 48000, 1) == 0); RequireThrows([&] { sceOpusDecTerminate(&context); }); RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); }); - RequireThrows([&] { sceOpusDecGetSize(3); }); + Require(sceOpusDecGetSize(3) == static_cast<int>(errorBase + 3)); std::array<std::uint8_t, 640> unsupported{}; for (int rate : {0, 22050, 44100, 96000}) - RequireThrows([&] { sceOpusDecCreateEx(&context, unsupported.data(), rate, 2); }); + Require(sceOpusDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast<int>(errorBase + 2)); std::array<std::int16_t, 1922> pcm{}, reference{}; pcm.fill(12345); RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data() + 1, 3839); }); @@ -119,8 +136,8 @@ int main() { } Require(sceOpusDecDestroy(state.data()) == 0); - RequireThrows([&] { sceOpusDecDestroy(state.data()); }); - RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840); }); + Require(sceOpusDecDestroy(state.data()) == static_cast<int>(errorBase + 1)); + Require(sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840) == static_cast<int>(errorBase + 1)); Require(sceOpusDecCreateEx(&context, state.data(), 48000, 2) == 0); Require(sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840) == 3840); std::array<std::int16_t, 3842> rejected; diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 0c94aeb226..2b3827fa86 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -7,5 +7,5 @@ ### Functional -- [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. Original error codes, AJM resource ownership and termination with live decoders are not implemented. +- [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. AJM error propagation, resource ownership and termination with live decoders are not implemented. Wrapper validation errors and their precedence are recovered. - [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; these inputs still throw. Packet loss concealment, asynchronous AJM execution, exact PCM conversion and resampler buffering remain outside the verified behavior of the FFmpeg implementation in [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp). diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index fa6aa1dfd1..6dbf108315 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -6,6 +6,6 @@ ### Functional -- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. +- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout. - [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap - [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured From ff128a51a10f250f2f83a3a73db2e1f0cc6ea880 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:29:33 +0300 Subject: [PATCH 0825/1360] fix(logindialog): restore validation and result layout --- .../prx/libSceCommonDialog/CommonDialog.hpp | 19 +++++ core/libs/prx/libSceLoginDialog/Export.cpp | 58 ++++++------- core/libs/tests/GuestLoginDialog.cpp | 82 +++++++++---------- docs/dev/debt/libSceLoginDialog.md | 2 +- 4 files changed, 84 insertions(+), 77 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index 3138618de7..0a7890e949 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -60,6 +60,25 @@ static_assert(sizeof(UsbStorageDialogResult) == 80); static_assert(offsetof(UsbStorageDialogResult, result) == 8); static_assert(offsetof(UsbStorageDialogResult, status) == 12); +struct LoginDialogParam { + std::uint32_t size; + std::uint32_t mode; + std::int32_t userIds[9]; + std::uint32_t reserved[5]; +}; + +struct LoginDialogResult { + std::int32_t result; + std::int32_t errorCode; + std::uint8_t data[8]; +}; + +static_assert(sizeof(LoginDialogParam) == 64); +static_assert(offsetof(LoginDialogParam, userIds) == 8); +static_assert(offsetof(LoginDialogParam, reserved) == 44); +static_assert(sizeof(LoginDialogResult) == 16); +static_assert(offsetof(LoginDialogResult, errorCode) == 4); + struct G2PDialogParam { CommonDialogBaseParam base; std::int32_t userId; diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index 7ca5a3626c..8fcd0f4daf 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -2,8 +2,8 @@ #include <cstdint> #include <cstddef> #include <cstring> -#include <iterator> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -12,29 +12,13 @@ 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<int>(0x81340001u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x81340002u); -constexpr int COMMON_DIALOG_ERROR_PARAM_INVALID = static_cast<int>(0x81340003u); -constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x81340005u); -constexpr int COMMON_DIALOG_ERROR_NO_SERVICE = static_cast<int>(0x81340007u); +constexpr int LOGIN_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x81340001u); +constexpr int LOGIN_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x81340002u); +constexpr int LOGIN_DIALOG_ERROR_BUSY = static_cast<int>(0x81340005u); +constexpr int LOGIN_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x81340003u); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -constexpr std::uint32_t LOGIN_DIALOG_PARAM_SIZE = 0x40; -constexpr std::uint32_t LOGIN_DIALOG_SELECTOR_LIMIT = 2; -constexpr std::size_t LOGIN_DIALOG_FIRST_RESERVED_WORD = 0x2C / 4; -bool ValidOpenParam(const void* param) { - if (param == nullptr) return false; - std::uint32_t size = 0; - std::memcpy(&size, param, sizeof(size)); - if (size != LOGIN_DIALOG_PARAM_SIZE) return false; - std::uint32_t words[LOGIN_DIALOG_PARAM_SIZE / sizeof(std::uint32_t)]; - std::memcpy(words, param, sizeof(words)); - if (words[1] >= LOGIN_DIALOG_SELECTOR_LIMIT) return false; - for (std::size_t i = LOGIN_DIALOG_FIRST_RESERVED_WORD; i < std::size(words); ++i) { - if (words[i] != 0) return false; - } - return true; -} +constexpr int LOGIN_DIALOG_ERROR_INVALID_USER = static_cast<int>(0x81340007u); std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; @@ -44,15 +28,18 @@ extern "C" { int APS5_VABI sceLoginDialogInitialize(void) { int expected = COMMON_DIALOG_STATUS_NONE; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return LOGIN_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceLoginDialogOpen(const void* param) { - if (!ValidOpenParam(param)) return COMMON_DIALOG_ERROR_PARAM_INVALID; +int APS5_VABI sceLoginDialogOpen(const LoginDialogParam* param) { + if (param == nullptr || param->size != sizeof(*param) || param->mode > 1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint32_t value) { return value == 0; })) return LOGIN_DIALOG_ERROR_INVALID_ARGUMENT; + for (const auto userId : param->userIds) { + if (userId != -1 && (static_cast<std::uint32_t>(userId) < 0x10000000u || static_cast<std::uint32_t>(userId) >= 0x40000000u)) return LOGIN_DIALOG_ERROR_INVALID_USER; + } 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 (status == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return LOGIN_DIALOG_ERROR_BUSY; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -65,23 +52,24 @@ int APS5_VABI sceLoginDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceLoginDialogGetResult(void* result) { - if (result == nullptr) return COMMON_DIALOG_ERROR_PARAM_INVALID; +int APS5_VABI sceLoginDialogGetResult(LoginDialogResult* result) { + if (result == nullptr) return LOGIN_DIALOG_ERROR_INVALID_ARGUMENT; const int status = g_status.load(); - if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - const std::int32_t words[] = {COMMON_DIALOG_RESULT_USER_CANCELED, COMMON_DIALOG_ERROR_NO_SERVICE}; - std::memcpy(result, words, sizeof(words)); - return status == COMMON_DIALOG_STATUS_FINISHED ? 0 : COMMON_DIALOG_ERROR_BUSY; + if (status == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + result->errorCode = LOGIN_DIALOG_ERROR_INVALID_USER; + if (status != COMMON_DIALOG_STATUS_FINISHED) return LOGIN_DIALOG_ERROR_BUSY; + return 0; } int APS5_VABI sceLoginDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; g_status = COMMON_DIALOG_STATUS_FINISHED; 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; + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 0289d86fda..09e5d52663 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -1,16 +1,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include <cstddef> #include <cstdint> #include <cstdlib> -#include <cstring> -#include <initializer_list> +#include "prx/libSceCommonDialog/CommonDialog.hpp" extern "C" { int APS5_VABI sceLoginDialogInitialize(void); -int APS5_VABI sceLoginDialogOpen(const void* param); +int APS5_VABI sceLoginDialogOpen(const LoginDialogParam* param); int APS5_VABI sceLoginDialogUpdateStatus(void); int APS5_VABI sceLoginDialogGetStatus(void); -int APS5_VABI sceLoginDialogGetResult(void* result); +int APS5_VABI sceLoginDialogGetResult(LoginDialogResult* result); int APS5_VABI sceLoginDialogClose(void); int APS5_VABI sceLoginDialogTerminate(void); } @@ -24,35 +22,22 @@ constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast<int>(0x81340001); constexpr int kErrAlreadyInitialized = static_cast<int>(0x81340002); -constexpr int kErrParamInvalid = static_cast<int>(0x81340003); -constexpr int kErrBusy = static_cast<int>(0x81340005); -constexpr int kErrNoService = static_cast<int>(0x81340007); +constexpr int kErrNotFinished = static_cast<int>(0x81340005); +constexpr int kErrArgNull = static_cast<int>(0x81340003); constexpr int kResultUserCanceled = 1; -void RequireRejected(const std::uint32_t (&valid)[16], std::size_t word, std::uint32_t value) { - std::uint32_t param[16]; - std::memcpy(param, valid, sizeof(param)); - param[word] = value; - Require(sceLoginDialogOpen(param) == kErrParamInvalid); -} - -void RequireResult(const std::int32_t (&result)[4]) { - Require(result[0] == kResultUserCanceled); - Require(result[1] == kErrNoService); - Require(result[2] == -1 && result[3] == -1); -} - } int main() { - const std::uint32_t param[16] = {0x40}; - std::int32_t result[4] = {-1, -1, -1, -1}; + LoginDialogParam param{}; + param.size = sizeof(param); + std::fill(std::begin(param.userIds), std::end(param.userIds), -1); + LoginDialogResult result{-1, -1, {11, 22, 33, 44, 55, 66, 77, 88}}; Require(sceLoginDialogTerminate() == kErrNotInitialized); - Require(sceLoginDialogOpen(param) == kErrNotInitialized); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); Require(sceLoginDialogClose() == kErrNotInitialized); - Require(sceLoginDialogGetResult(result) == kErrNotInitialized); - Require(result[0] == -1 && result[1] == -1); + Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); Require(sceLoginDialogGetStatus() == kStatusNone); Require(sceLoginDialogUpdateStatus() == kStatusNone); @@ -61,22 +46,37 @@ int main() { Require(sceLoginDialogGetStatus() == kStatusInitialized); Require(sceLoginDialogUpdateStatus() == kStatusInitialized); - Require(sceLoginDialogOpen(nullptr) == kErrParamInvalid); - for (std::uint32_t size : {0u, 0x3Cu, 0x3Fu, 0x41u}) RequireRejected(param, 0, size); - for (std::uint32_t selector : {2u, 0xFFFFFFFFu}) RequireRejected(param, 1, selector); - for (std::size_t word = 0x2C / 4; word < 16; ++word) RequireRejected(param, word, 1); - Require(sceLoginDialogGetStatus() == kStatusInitialized); - Require(sceLoginDialogGetResult(result) == kErrBusy); - RequireResult(result); - std::memset(result, 0xFF, sizeof(result)); + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == kErrNotFinished); + Require(result.result == 1 && result.errorCode == static_cast<int>(0x81340007u)); - Require(sceLoginDialogOpen(param) == 0); + param.size = 63; + Require(sceLoginDialogOpen(¶m) == kErrArgNull); + param.size = sizeof(param); + param.mode = 2; + Require(sceLoginDialogOpen(¶m) == kErrArgNull); + param.mode = 1; + param.reserved[4] = 1; + Require(sceLoginDialogOpen(¶m) == kErrArgNull); + param.reserved[4] = 0; + for (auto& userId : param.userIds) { + for (const auto invalid : {0, 0x0fffffff, 0x40000000, -2}) { + userId = invalid; + Require(sceLoginDialogOpen(¶m) == static_cast<int>(0x81340007u)); + } + userId = -1; + } + param.userIds[0] = 0x10000000; + param.userIds[8] = 0x3fffffff; + Require(sceLoginDialogOpen(¶m) == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); Require(sceLoginDialogUpdateStatus() == kStatusFinished); - Require(sceLoginDialogGetResult(nullptr) == kErrParamInvalid); - Require(sceLoginDialogGetResult(result) == 0); - RequireResult(result); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == 0); + Require(result.result == kResultUserCanceled); + Require(result.errorCode == static_cast<int>(0x81340007u)); + Require(result.data[0] == 11 && result.data[7] == 88); Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); @@ -87,8 +87,8 @@ int main() { Require(sceLoginDialogGetStatus() == kStatusInitialized); Require(sceLoginDialogClose() == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); - result[0] = -1; - Require(sceLoginDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); + result.result = -1; + Require(sceLoginDialogGetResult(&result) == 0); + Require(result.result == kResultUserCanceled); Require(sceLoginDialogTerminate() == 0); } diff --git a/docs/dev/debt/libSceLoginDialog.md b/docs/dev/debt/libSceLoginDialog.md index 07a38ff414..c9408a3d58 100644 --- a/docs/dev/debt/libSceLoginDialog.md +++ b/docs/dev/debt/libSceLoginDialog.md @@ -7,4 +7,4 @@ ### Unknown function info -- [sceLoginDialogOpen, sceLoginDialogGetResult](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the 0x8134xxxx codes, the Open param checks (size 0x40, word 1 below 2, words 0x2C to 0x3C zero) and the `{1, 0x81340007}` result words are the ones [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/service/hle_service.cpp#L805-L874) reads from the shipped module. The order of the param and state checks follows prosper's handlers and is not stated separately, the meaning of word 1 and of the second result word is unknown, words 2 to 10 are not checked, and Open's busy result is never reached because the dialog never runs. Closes when: the module's check order is read or measured on a PS5 +- [sceLoginDialogOpen, sceLoginDialogGetResult](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the 64-byte input and 16-byte result layouts are recovered. The roles of the nine user-ID slots and the eight trailing result bytes require service-side analysis. The UI stub returns the native disconnected-service cancellation result and does not populate successful login data. From 94e579a37aceb0613b68a47f26fab2d7978c1f0e Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:30:44 +0300 Subject: [PATCH 0826/1360] fix(netctlap): restore option and result validation order --- core/libs/prx/libSceNetCtlAp/Export.cpp | 11 ++++++++--- core/libs/tests/GuestNetCtlAp.cpp | 9 +++++++-- docs/dev/debt/libSceNetCtlAp.md | 5 ++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNetCtlAp/Export.cpp b/core/libs/prx/libSceNetCtlAp/Export.cpp index d0bcb19845..9da3953097 100644 --- a/core/libs/prx/libSceNetCtlAp/Export.cpp +++ b/core/libs/prx/libSceNetCtlAp/Export.cpp @@ -7,6 +7,7 @@ namespace { +constexpr int NET_CTL_AP_ERROR_INVALID_OPT_STR = static_cast<int>(0x80412901); constexpr int NET_CTL_AP_ERROR_NOT_STARTED = static_cast<int>(0x8041298F); constexpr int NET_CTL_AP_ERROR_INVALID_TYPE = static_cast<int>(0x80412990); constexpr int NET_CTL_AP_ERROR_INVALID_ADDR = static_cast<int>(0x80412991); @@ -91,8 +92,8 @@ int APS5_VABI sceNetCtlApUnregisterCallback(int cid) { int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode) { std::lock_guard lock(g_lock); if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; - if (!ValidEventType(eventType)) return NET_CTL_AP_ERROR_INVALID_TYPE; if (!errorCode) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (!ValidEventType(eventType)) return NET_CTL_AP_ERROR_INVALID_TYPE; *errorCode = 0; return 0; } @@ -118,10 +119,14 @@ int APS5_VABI sceNetCtlApStop() { return g_initialized ? NET_CTL_AP_ERROR_NOT_STARTED : NET_CTL_AP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceNetCtlApGetConnectInfo(std::uint8_t* optStr, void* connectInfo) { - (void)optStr; +int APS5_VABI sceNetCtlApGetConnectInfo(const char* optStr, void* connectInfo) { std::lock_guard lock(g_lock); if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (optStr != nullptr) { + std::size_t length = 0; + while (length < 9 && optStr[length] != 0) ++length; + if (length > 8) return NET_CTL_AP_ERROR_INVALID_OPT_STR; + } if (!connectInfo) return NET_CTL_AP_ERROR_INVALID_ADDR; if (*static_cast<const std::uint64_t*>(connectInfo) != NET_CTL_AP_CONNECT_INFO_SIZE) return NET_CTL_AP_ERROR_INVALID_SIZE; return NET_CTL_AP_ERROR_NOT_STARTED; diff --git a/core/libs/tests/GuestNetCtlAp.cpp b/core/libs/tests/GuestNetCtlAp.cpp index fbb8b0f01d..3689260ee4 100644 --- a/core/libs/tests/GuestNetCtlAp.cpp +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -15,7 +15,7 @@ int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode); int APS5_VABI sceNetCtlApGetInfo(void* info); int APS5_VABI sceNetCtlApGetState(int* state); int APS5_VABI sceNetCtlApStop(void); -int APS5_VABI sceNetCtlApGetConnectInfo(std::uint8_t* optStr, void* connectInfo); +int APS5_VABI sceNetCtlApGetConnectInfo(const char* optStr, void* connectInfo); } namespace { @@ -74,9 +74,13 @@ int main() { std::memset(connect, 0x5a, sizeof(connect)); const std::uint64_t connectSize = sizeof(connect); std::memcpy(connect, &connectSize, sizeof(connectSize)); - std::uint8_t option[] = "opt"; + const char option[] = "opt"; Require(sceNetCtlApGetConnectInfo(option, connect) == kErrNotStarted); Require(sceNetCtlApGetConnectInfo(nullptr, connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo("", connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo("12345678", connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo("123456789", nullptr) == static_cast<int>(0x80412901u)); + Require(sceNetCtlApGetConnectInfo("123456789", connect) == static_cast<int>(0x80412901u)); for (std::size_t i = sizeof(connectSize); i < sizeof(connect); ++i) Require(connect[i] == 0x5a); Require(sceNetCtlApGetConnectInfo(option, nullptr) == kErrInvalidAddr); const std::uint64_t wrongConnectSize = sizeof(info); @@ -95,6 +99,7 @@ int main() { int code = 0x5a5a; Require(sceNetCtlApClearEvent(type) == kErrInvalidType); Require(sceNetCtlApGetResult(type, &code) == kErrInvalidType && code == 0x5a5a); + Require(sceNetCtlApGetResult(type, nullptr) == kErrInvalidAddr); } int cids[8]; diff --git a/docs/dev/debt/libSceNetCtlAp.md b/docs/dev/debt/libSceNetCtlAp.md index 012a82c476..94b54af330 100644 --- a/docs/dev/debt/libSceNetCtlAp.md +++ b/docs/dev/debt/libSceNetCtlAp.md @@ -7,6 +7,5 @@ ### Unknown function info - [sceNetCtlApInit, sceNetCtlApTerm](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - every other call returns `NOT_INITIALIZED` (`0x80412994`) before `sceNetCtlApInit` and after `sceNetCtlApTerm`, which also drops the registered callbacks. A second `sceNetCtlApInit` throws, as no error for it is documented -- [sceNetCtlApGetInfo, sceNetCtlApGetConnectInfo, sceNetCtlApStop](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - checks run in this order, which is not verified: a null structure returns `INVALID_ADDR` (`0x80412991`), a size field other than 152 or 184 `INVALID_SIZE` (`0x80412998`), then `NOT_STARTED`. The option string is not read, since the accepted strings (`INVALID_OPT_STR`, `0x80412901`) are unknown. Whether `sceNetCtlApStop` of a stopped access point returns `NOT_STARTED` or 0 is unverified -- [sceNetCtlApClearEvent, sceNetCtlApGetResult](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - event types 1 to 4 are accepted and others return `INVALID_TYPE` (`0x80412990`), checked before the null `errorCode` (`INVALID_ADDR`). With no event ever delivered, `sceNetCtlApGetResult` writes 0; what the console writes before the first event is unverified -- [sceNetCtlApRegisterCallback, sceNetCtlApUnregisterCallback](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - a null function or id pointer returns `INVALID_ADDR`, a ninth registration `CALLBACK_MAX` (`0x80412995`), an id outside 0 to 7 `INVALID_ID` (`0x80412996`) and an unregistered id `ID_NOT_FOUND` (`0x80412997`). Ids are the lowest free slot, as in libSceNetCtl; the console's id assignment is unverified +- [sceNetCtlApGetInfo, sceNetCtlApGetConnectInfo, sceNetCtlApStop](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) - stopped-state results come from a service on the console and remain unverified. Wrapper validation is recovered: initialization precedes arguments, the optional connection string is at most eight bytes, then output pointers and exact 152/184-byte sizes are checked. The service interpretation of the option string remains unresolved. +- [sceNetCtlApGetResult](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) - with no event delivered, the host writes zero; the service result before the first event remains unknown. Wrapper validation checks initialization, output pointer and event type in that order. Event types 1 through 4 and lowest-free callback IDs 0 through 7 are confirmed. From dd5b81e1dfe0beaa3872e6d0a2cd51a92318fe27 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:32:12 +0300 Subject: [PATCH 0827/1360] fix(netctlapdialog): restore parameter and lifecycle validation --- .../prx/libSceCommonDialog/CommonDialog.hpp | 19 +++++++++ core/libs/prx/libSceNetCtlApDialog/Export.cpp | 28 ++++++------- core/libs/tests/GuestNetCtlApDialog.cpp | 42 +++++++++++++------ docs/dev/debt/libSceNetCtlApDialog.md | 2 +- 4 files changed, 62 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index 0a7890e949..f1949f6eeb 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -79,6 +79,25 @@ static_assert(offsetof(LoginDialogParam, reserved) == 44); static_assert(sizeof(LoginDialogResult) == 16); static_assert(offsetof(LoginDialogResult, errorCode) == 4); +struct NetCtlApDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::uint32_t value56; + std::uint32_t reserved60; + char option[9]; + std::uint8_t reserved[31]; +}; + +struct NetCtlApDialogResult { + std::int32_t result; + std::uint8_t reserved[36]; +}; + +static_assert(sizeof(NetCtlApDialogParam) == 104); +static_assert(offsetof(NetCtlApDialogParam, option) == 64); +static_assert(offsetof(NetCtlApDialogParam, reserved) == 73); +static_assert(sizeof(NetCtlApDialogResult) == 40); + struct G2PDialogParam { CommonDialogBaseParam base; std::int32_t userId; diff --git a/core/libs/prx/libSceNetCtlApDialog/Export.cpp b/core/libs/prx/libSceNetCtlApDialog/Export.cpp index e1d2bc4a27..64a0d3086e 100644 --- a/core/libs/prx/libSceNetCtlApDialog/Export.cpp +++ b/core/libs/prx/libSceNetCtlApDialog/Export.cpp @@ -3,6 +3,7 @@ #include <cstddef> #include <cstring> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -12,19 +13,13 @@ 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<int>(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -struct SceNetCtlApDialogResult { - std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; - std::uint8_t reserved[36] = {}; -}; - -static_assert(sizeof(SceNetCtlApDialogResult) == 40); - std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; } @@ -42,17 +37,20 @@ int APS5_VABI sceNetCtlApDialogTerminate(void) { return 0; } -int APS5_VABI sceNetCtlApDialogOpen(const void* param) { +int APS5_VABI sceNetCtlApDialogOpen(const NetCtlApDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->size != sizeof(*param) || param->reserved60 != 0 || std::find(std::begin(param->option), std::end(param->option), '\0') == std::end(param->option) || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; 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 sceNetCtlApDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -65,14 +63,14 @@ int APS5_VABI sceNetCtlApDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceNetCtlApDialogGetResult(void* result) { +int APS5_VABI sceNetCtlApDialogGetResult(NetCtlApDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (!std::all_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - const SceNetCtlApDialogResult dialogResult{}; - std::memcpy(result, &dialogResult, sizeof(dialogResult)); - return 0; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } } diff --git a/core/libs/tests/GuestNetCtlApDialog.cpp b/core/libs/tests/GuestNetCtlApDialog.cpp index bdce6ca102..f17c171e56 100644 --- a/core/libs/tests/GuestNetCtlApDialog.cpp +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -2,11 +2,12 @@ #include <cstdint> #include <cstdlib> #include <cstring> +#include "prx/libSceCommonDialog/CommonDialog.hpp" extern "C" { int APS5_VABI sceNetCtlApDialogInitialize(void); int APS5_VABI sceNetCtlApDialogTerminate(void); -int APS5_VABI sceNetCtlApDialogOpen(const void* param); +int APS5_VABI sceNetCtlApDialogOpen(const NetCtlApDialogParam* param); int APS5_VABI sceNetCtlApDialogClose(void); int APS5_VABI sceNetCtlApDialogUpdateStatus(void); int APS5_VABI sceNetCtlApDialogGetStatus(void); @@ -37,12 +38,15 @@ void RequireCanceled(const std::uint8_t (&result)[44]) { } int main() { - std::uint8_t param[104] = {}; - std::uint8_t result[44]; + NetCtlApDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; + param.size = sizeof(param); + alignas(NetCtlApDialogResult) std::uint8_t result[44]; std::memset(result, 0x5a, sizeof(result)); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); - Require(sceNetCtlApDialogOpen(param) == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); Require(sceNetCtlApDialogClose() == kErrNotInitialized); Require(sceNetCtlApDialogGetResult(result) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); @@ -55,27 +59,39 @@ int main() { Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogGetResult(result) == static_cast<int>(0x80B8000Au)); + std::memset(result + 4, 0, 36); Require(sceNetCtlApDialogGetResult(result) == kErrNotFinished); - Require(sceNetCtlApDialogOpen(param) == 0); + param.size = 103; + Require(sceNetCtlApDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.size = sizeof(param); + param.reserved60 = 1; + Require(sceNetCtlApDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.reserved60 = 0; + std::memset(param.option, 'a', sizeof(param.option)); + Require(sceNetCtlApDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.option[8] = 0; + param.reserved[30] = 1; + Require(sceNetCtlApDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); + param.reserved[30] = 0; + Require(sceNetCtlApDialogOpen(¶m) == 0); Require(sceNetCtlApDialogGetStatus() == kStatusFinished); Require(sceNetCtlApDialogUpdateStatus() == kStatusFinished); Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); - Require(sceNetCtlApDialogGetResult(result) == 0); + Require(sceNetCtlApDialogGetResult(result) == kResultUserCanceled); RequireCanceled(result); - Require(sceNetCtlApDialogOpen(param) == 0); - Require(sceNetCtlApDialogClose() == 0); + Require(sceNetCtlApDialogOpen(¶m) == 0); + Require(sceNetCtlApDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceNetCtlApDialogTerminate() == 0); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); Require(sceNetCtlApDialogInitialize() == 0); - Require(sceNetCtlApDialogClose() == 0); - Require(sceNetCtlApDialogGetStatus() == kStatusFinished); - std::memset(result, 0x5a, sizeof(result)); - Require(sceNetCtlApDialogGetResult(result) == 0); - RequireCanceled(result); + Require(sceNetCtlApDialogClose() == static_cast<int>(0x80B8000Bu)); + Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogGetResult(result) == kErrNotFinished); Require(sceNetCtlApDialogTerminate() == 0); } diff --git a/docs/dev/debt/libSceNetCtlApDialog.md b/docs/dev/debt/libSceNetCtlApDialog.md index aefc800699..faeb9b3cb6 100644 --- a/docs/dev/debt/libSceNetCtlApDialog.md +++ b/docs/dev/debt/libSceNetCtlApDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceNetCtlApDialog](../../../core/libs/prx/libSceNetCtlApDialog/Export.cpp) - the access-point dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no access-point connection is brought up. `Open` takes the 104-byte parameter block and `GetResult` writes a 40-byte result (int32 result and 36 zeroed reserved bytes), following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/NetCtlApDialog.cs); the parameter block is not validated and the 0x80B8xxxx error codes follow the other common dialogs, neither verified on hardware. Deathloop imports it +- [libSceNetCtlApDialog](../../../core/libs/prx/libSceNetCtlApDialog/Export.cpp) - the access-point dialog shows no UI and reports user cancellation without bringing up a connection. Parameter size, reserved-byte checks, option-string limit and lifecycle errors are recovered from the supplied module. The meaning of the input word at offset 56 and service-side connection behavior remain unresolved. From 20a2488b4b446f49710324bef4605b78993763bd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:37:50 +0300 Subject: [PATCH 0828/1360] fix(appcontent): restore initialization guards and validation --- core/libs/prx/libSceAppContent/Export.cpp | 42 ++++++++++++++++++++--- core/libs/tests/GuestAppContent.cpp | 42 ++++++++++++++++++++++- docs/dev/debt/libSceAppContent.md | 2 +- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 30a0b1bc1c..24f6127080 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -3,14 +3,17 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/RemoveTree.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include <algorithm> +#include <atomic> #include <cstring> #include <filesystem> #include <mutex> #include <stdexcept> #include <string> +static constexpr int SCE_APP_CONTENT_ERROR_NOT_INITIALIZED = static_cast<int>(0x80D90001); static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast<int>(0x80D90002); static constexpr int SCE_APP_CONTENT_ERROR_BUSY = static_cast<int>(0x80D90003); static constexpr int SCE_APP_CONTENT_ERROR_NOT_MOUNTED = static_cast<int>(0x80D90004); @@ -30,6 +33,16 @@ struct AppContentAddcontInfo { namespace { +std::atomic_bool initialized{false}; +std::mutex initializationMutex; + +std::uint32_t compiledSdkVersion() { + const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version; +} + struct TemporaryData { std::mutex mutex; const std::filesystem::path directory = ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); @@ -57,23 +70,27 @@ static void ClearDirectory(const std::filesystem::path& directory) { extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentMountPoint* mount_point) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint* mount_point) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentAddcontInfo* info) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (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 serviceLabel, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)serviceLabel; if (!hitNum) return SCE_APP_CONTENT_ERROR_PARAMETER; *hitNum = 0; @@ -81,6 +98,7 @@ int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t serviceLabel, AppContentA } int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!value) return SCE_APP_CONTENT_ERROR_PARAMETER; switch (param_id) { case APPPARAM_ID_SKU_FLAG: @@ -95,6 +113,7 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { } int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); @@ -104,6 +123,7 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_ } int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER; if (std::strncmp(mountPoint->data, DOWNLOAD_MOUNT_POINT, sizeof(mountPoint->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; @@ -122,15 +142,23 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount return 0; } -int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) { - (void)init_param; - if (!boot_param) return SCE_APP_CONTENT_ERROR_PARAMETER; +int APS5_VABI sceAppContentInitialize(const AppContentInitParam* initParam, AppContentBootParam* bootParam) { + std::lock_guard lock(initializationMutex); + const auto sdkVersion = compiledSdkVersion(); + if (sdkVersion >= 0x01500000) { + if (initialized.load()) return SCE_APP_CONTENT_ERROR_BUSY; + if (initParam == nullptr || bootParam == nullptr) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!std::all_of(std::begin(initParam->reserved), std::end(initParam->reserved), [](char value) { return value == 0; }) || !std::all_of(std::begin(bootParam->reserved1), std::end(bootParam->reserved1), [](char value) { return value == 0; }) || !std::all_of(std::begin(bootParam->reserved2), std::end(bootParam->reserved2), [](char value) { return value == 0; })) return SCE_APP_CONTENT_ERROR_PARAMETER; + } + if (bootParam == nullptr) return SCE_APP_CONTENT_ERROR_PARAMETER; Temporary(); - std::memset(boot_param, 0, sizeof(*boot_param)); + std::memset(bootParam, 0, sizeof(*bootParam)); + initialized = true; return 0; } int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -140,6 +168,7 @@ int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount } int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -149,6 +178,7 @@ int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMoun } int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint || option > TEMPORARY_DATA_OPTION_FORMAT) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -163,6 +193,7 @@ int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountP } int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -174,18 +205,21 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, void* progress) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 4cf0721f55..f8c3cc5427 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -5,6 +5,8 @@ #include <cstring> #include <filesystem> #include <fstream> +#include <array> +#include "ProcessParametersFixture.hpp" extern "C" { int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBootParam*); @@ -34,6 +36,25 @@ static constexpr int ErrorNotMounted = static_cast<int>(0x80D90004); static void Require(bool value) { if (!value) std::abort(); } int main() { + static std::array<std::byte, 64> process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + std::uint32_t sdkVersion = 0x01500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + constexpr int notInitialized = static_cast<int>(0x80D90001u); + Require(sceAppContentAddcontMount(0, nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontUnmount(nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfo(0, nullptr, nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == notInitialized); + Require(sceAppContentAppParamGetInt(0, nullptr) == notInitialized); + Require(sceAppContentTemporaryDataMount2(0, nullptr) == notInitialized); + Require(sceAppContentTemporaryDataUnmount(nullptr) == notInitialized); + Require(sceAppContentTemporaryDataFormat(nullptr) == notInitialized); + Require(sceAppContentTemporaryDataGetAvailableSpaceKb(nullptr, nullptr) == notInitialized); + Require(sceAppContentDownloadDataFormat(nullptr) == notInitialized); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontDelete(0, nullptr) == notInitialized); + Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == notInitialized); + Require(sceAppContentGetAddcontDownloadProgress(0, nullptr, nullptr) == notInitialized); const auto original = std::filesystem::current_path(); const auto directory = original / ("appcontent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(directory)); @@ -45,6 +66,26 @@ int main() { R"("userDefinedParam1":23566,"userDefinedParam2":-7,"userDefinedParam3":0,"userDefinedParam4":2147483647})"; Require(static_cast<bool>(param)); } + AppContentInitParam init{}; + AppContentBootParam boot{}; + Require(sceAppContentInitialize(nullptr, &boot) == ErrorParameter); + Require(sceAppContentInitialize(&init, nullptr) == ErrorParameter); + init.reserved[31] = 1; + Require(sceAppContentInitialize(&init, &boot) == ErrorParameter); + init.reserved[31] = 0; + boot.reserved1[3] = 1; + Require(sceAppContentInitialize(&init, &boot) == ErrorParameter); + boot.reserved1[3] = 0; + boot.reserved2[31] = 1; + Require(sceAppContentInitialize(&init, &boot) == ErrorParameter); + boot.reserved2[31] = 0; + boot.attr = 0xffffffffu; + Require(sceAppContentInitialize(&init, &boot) == 0); + Require(boot.attr == 0); + Require(sceAppContentInitialize(nullptr, nullptr) == ErrorBusy); + sdkVersion = 0x01400000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceAppContentInitialize(nullptr, &boot) == 0); AppContentMountPoint download{}; std::memcpy(download.data, "/download0", 11); size_t availableKb = 12345; @@ -110,7 +151,6 @@ int main() { std::filesystem::create_directory(directory / "temp0"); { std::ofstream file(directory / "temp0" / "old.txt"); file << "previous run"; } - AppContentBootParam boot{}; Require(sceAppContentInitialize(nullptr, &boot) == 0); Require(access_nid_postfix("/temp0/old.txt", 0) == -1); Require(sceAppContentTemporaryDataUnmount(nullptr) == ErrorParameter); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 373aa6fdba..93d5099c7b 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -9,7 +9,7 @@ - [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 17 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional -- [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) does not track native service readiness or initialization. Native operations reject uninitialized use with `0x80D90001`; SDK 0x01500000 and later also reject repeated initialization and require zeroed initialization fields. Native entitlement operations additionally validate label bytes 17 through 19 from SDK 0x03500000. These checks and SDK-dependent service-error translation are not implemented. +- [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. Native entitlement-label validation and SDK-dependent service-error translation remain incomplete. - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. From f55e936f92fd381e67988b15294a0725594ed7e7 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:38:12 +0300 Subject: [PATCH 0829/1360] fix(appcontent): validate entitlement padding by sdk version --- core/libs/prx/libSceAppContent/Export.cpp | 9 +++++++++ core/libs/tests/GuestAppContent.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceAppContent.md | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 24f6127080..8bfb24b850 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -43,6 +43,10 @@ std::uint32_t compiledSdkVersion() { return version; } +bool validEntitlementLabel(const NpUnifiedEntitlementLabel& label, std::uint32_t minimumSdkVersion) { + return compiledSdkVersion() < minimumSdkVersion || std::all_of(std::begin(label.padding), std::end(label.padding), [](char value) { return value == 0; }); +} + struct TemporaryData { std::mutex mutex; const std::filesystem::path directory = ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); @@ -73,6 +77,7 @@ int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedE if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } @@ -86,6 +91,7 @@ int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifie if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } @@ -208,6 +214,7 @@ int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } @@ -215,6 +222,7 @@ int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnified if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } @@ -222,6 +230,7 @@ int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, co if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void) { diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index f8c3cc5427..468467e521 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -200,6 +200,24 @@ int main() { Require(sceAppContentGetAddcontInfo(0, nullptr, info) == ErrorParameter); Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + for (const std::uint32_t version : {0x014fffffu, 0x01500000u, 0x034fffffu, 0x03500000u}) { + std::memcpy(process.data() + 16, &version, sizeof(version)); + for (char& padding : label.padding) { + padding = 1; + const int earlyError = version >= 0x01500000 ? ErrorParameter : ErrorNotFound; + const int infoError = version >= 0x01500000 ? ErrorParameter : ErrorDrmNoEntitlement; + const int lateError = version >= 0x03500000 ? ErrorParameter : ErrorNotFound; + Require(sceAppContentAddcontMount(0, &label, &mountPoint) == earlyError); + Require(sceAppContentGetAddcontInfo(0, &label, info) == infoError); + Require(sceAppContentAddcontEnqueueDownload(0, &label) == earlyError); + Require(sceAppContentAddcontDelete(0, &label) == lateError); + Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == lateError); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); + padding = 0; + } + } + uint32_t hitNum = 0x5a5a5a5a; Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, &hitNum) == 0); Require(hitNum == 0); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 93d5099c7b..371a027f51 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -9,7 +9,7 @@ - [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 17 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional -- [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. Native entitlement-label validation and SDK-dependent service-error translation remain incomplete. +- [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. SDK-dependent service-error translation remains incomplete. - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. From 5f980378c42836650b70dd77e40949cf2e755a19 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:39:41 +0300 Subject: [PATCH 0830/1360] fix(font): restore device cache ownership and detach outputs --- core/libs/prx/libSceFont/src/Library.cpp | 41 +++++++++++++++--------- core/libs/tests/GuestFont.cpp | 10 ++++-- docs/dev/debt/libSceFont.md | 1 - 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Library.cpp b/core/libs/prx/libSceFont/src/Library.cpp index a3e01002c3..4d8adbac7e 100644 --- a/core/libs/prx/libSceFont/src/Library.cpp +++ b/core/libs/prx/libSceFont/src/Library.cpp @@ -378,26 +378,37 @@ int APS5_VABI sceFontClearDeviceCache(FontLibrary library) { } int APS5_VABI sceFontDettachDeviceCacheBuffer(FontLibrary library, void** buffer, std::uint32_t* size) { + const auto clearOutputs = [&] { + if (buffer) *buffer = nullptr; + if (size) *size = 0; + }; auto* lib = static_cast<FontLibNative*>(library); - if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; std::uint32_t previous = 0; - if (!AcquireLibraryLock(lib, previous)) return SCE_FONT_ERROR_INVALID_LIBRARY; + if (!lib || lib->magic != LIBRARY_MAGIC || !AcquireLibraryLock(lib, previous)) { + clearOutputs(); + return SCE_FONT_ERROR_INVALID_LIBRARY; + } std::uint32_t* current = AcquireDeviceCache(lib); - if (!current) { - ReleaseDeviceCache(lib, nullptr); - ReleaseLibraryLock(lib, previous); - return SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER; - } - if (size) *size = current[0]; - if (buffer) { - *buffer = current; - } else if ((lib->flags & 1u) != 0) { - lib->iface->dealloc(lib->alloc_ctx, current); - } - lib->flags &= ~1u; ReleaseDeviceCache(lib, nullptr); + int result = SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER; + if (current) { + const std::uint32_t capacity = current[0]; + std::memset(current + 1, 0, 5 * sizeof(std::uint32_t)); + if ((lib->flags & 1u) == 0) { + if (buffer) *buffer = current; + if (size) *size = capacity; + ReleaseLibraryLock(lib, previous); + return SCE_FONT_OK; + } + result = SCE_FONT_ERROR_FATAL; + if (lib->iface && lib->iface->dealloc) { + lib->iface->dealloc(lib->alloc_ctx, current); + result = SCE_FONT_OK; + } + } + clearOutputs(); ReleaseLibraryLock(lib, previous); - return SCE_FONT_OK; + return result; } int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, std::uint32_t fontMax, std::uint32_t formats) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index afa5e93d2c..54880a4991 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -257,9 +257,11 @@ int main() { Require(static_cast<Font::FontLibNative*>(coarseLibrary)->lock_word == 0); Require(sceFontDestroyLibrary(&coarseLibrary) == SCE_FONT_OK && coarseLibrary == nullptr); - void* cacheBuffer = nullptr; + void* cacheBuffer = ¬ALibrary; std::uint32_t cacheSize = 0; + cacheSize = 123; Require(sceFontDettachDeviceCacheBuffer(nullptr, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(cacheBuffer == nullptr && cacheSize == 0); Require(sceFontDettachDeviceCacheBuffer(¬ALibrary, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_INVALID_LIBRARY); Require(sceFontClearDeviceCache(library) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); @@ -270,13 +272,15 @@ int main() { Require(sceFontAttachDeviceCacheBuffer(library, callerCache.data(), static_cast<std::uint32_t>(callerCache.size())) == SCE_FONT_ERROR_ALREADY_ATTACHED); Require(sceFontClearDeviceCache(library) == SCE_FONT_OK); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_OK && cacheBuffer == callerCache.data() && cacheSize == callerCache.size()); + for (std::size_t offset = 4; offset < 24; ++offset) Require(callerCache[offset] == 0); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); Require(sceFontAttachDeviceCacheBuffer(library, nullptr, 0x2000) == SCE_FONT_OK); cacheBuffer = nullptr; cacheSize = 0; - Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_OK && cacheBuffer != nullptr && cacheSize == 0x2000); - Release(nullptr, cacheBuffer); + const int allocationsBeforeDetach = allocations; + Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_OK && cacheBuffer == nullptr && cacheSize == 0); + Require(allocations == allocationsBeforeDetach - 1); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); Require(sceFontAttachDeviceCacheBuffer(library, nullptr, 0x2000) == SCE_FONT_OK); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index b387ddb5d3..ced51def02 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -5,7 +5,6 @@ - [sceFontSetTypographicDesign and sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp): signatures are confirmed by the supplied PS5 library. Native feature ranges, defaults and driver-dependent availability are not implemented; the host still accepts arbitrary values and stores them without affecting shaping or rendering. - [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [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 -- [sceFontDettachDeviceCacheBuffer](../../../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. It detaches the cache buffer attached by `sceFontAttachDeviceCacheBuffer`, returns the buffer and its size, and reports `SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER` when no cache buffer is attached - [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` - [Writing-line ABI and allocation](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 layouts, creation forms, memory validation, allocator callbacks and error handling are recovered. The 2208-byte line and 64-byte orders use caller memory. Host registry bookkeeping still allocates separately; memory lifetime and concurrent destruction require caller synchronization. From 512ae7f80f7558a74471248f2af1011f50de3074 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:41:42 +0300 Subject: [PATCH 0831/1360] fix(font): use 32-bit resolution in driver callbacks --- core/libs/prx/libSceFont/include/FontDriver.hpp | 2 +- core/libs/prx/libSceFont/src/Open.cpp | 5 +++-- core/libs/prx/libSceFontFt/src/Driver.cpp | 2 +- core/libs/tests/GuestFont.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceFont.md | 2 +- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index 889c367dda..35b8d3d533 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -202,7 +202,7 @@ using DriverTermFunction = int (APS5_VABI*)(FontLibNative* library); using DriverSupportFunction = int (APS5_VABI*)(FontLibNative* library, std::uint32_t formats); using DriverOpenFunction = int (APS5_VABI*)(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj); using DriverCloseFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t flags); -using DriverScaleFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint16_t* outUnitsPerEm, float* outScale); +using DriverScaleFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* outUnitsPerEm, float* outScale); using DriverMetricFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t metricId, std::uint16_t* outMetric); using DriverGlyphsCountFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* outCount); using DriverGlyphIndexFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex); diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 21e0272990..8b8480ddd8 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -82,7 +82,8 @@ void InitializeOpenedHandle(FontHandleNative* font, FontLibNative* library, Font driver->metric(obj, 0xEA00, metric); font->metricB = metric[0]; float scale = 0.0f; - driver->scale(obj, metric, &scale); + std::uint32_t resolution = 0; + driver->scale(obj, &resolution, &scale); font->style = {}; font->style.dpi_x = 0x48; font->style.dpi_y = 0x48; @@ -515,7 +516,7 @@ int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, } int APS5_VABI sceFontGetFontResolution(FontHandle fontHandle, std::uint32_t* pResolution, float* pScalePixel) { - std::uint16_t unitsPerEm = 0; + std::uint32_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; diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 152a72f8d7..6ed7e5203e 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -269,7 +269,7 @@ int APS5_VABI LibraryCloseFont(FontObj* obj, std::uint32_t flags) { return SCE_FONT_OK; } -int APS5_VABI LibraryGetFaceScale(FontObj* obj, std::uint16_t* outUnitsPerEm, float* outScale) { +int APS5_VABI LibraryGetFaceScale(FontObj* obj, std::uint32_t* outUnitsPerEm, float* outScale) { if (!obj || !outUnitsPerEm || !outScale) return SCE_FONT_ERROR_FATAL; const FT_Face face = FaceOf(obj); if (!face) return SCE_FONT_ERROR_FATAL; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 54880a4991..b466265979 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -108,6 +108,12 @@ static std::uint32_t APS5_VABI CoarsePixelResolution() { return 16; } +static int APS5_VABI WideFontResolution(Font::FontObj*, std::uint32_t* resolution, float* scale) { + *resolution = 0x12345678; + *scale = 12.5f; + return SCE_FONT_OK; +} + using FontFixture::BuildFont; using FontFixture::Words; @@ -355,6 +361,16 @@ int main() { Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 7); Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); Require(sceFontCloseFont(otherFont) == SCE_FONT_OK); + auto* resolutionLibrary = static_cast<Font::FontLibNative*>(library); + const Font::SysDriver* originalDriver = resolutionLibrary->sys_driver; + Font::SysDriver wideResolutionDriver = *originalDriver; + wideResolutionDriver.scale = WideFontResolution; + resolutionLibrary->sys_driver = &wideResolutionDriver; + std::uint32_t wideResolution = 0; + float wideScale = 0; + Require(sceFontGetFontResolution(font, &wideResolution, &wideScale) == SCE_FONT_OK); + Require(wideResolution == 0x12345678 && wideScale == 12.5f); + resolutionLibrary->sys_driver = originalDriver; std::uint32_t hDpi = 1; std::uint32_t vDpi = 1; Require(sceFontGetResolutionDpi(font, &hDpi, &vDpi) == SCE_FONT_OK && hDpi == 72 && vDpi == 72); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index ced51def02..b75f406bbe 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -6,7 +6,7 @@ - [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [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 - [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` +- [sceFontGetFontResolution](../../../core/libs/prx/libSceFont/src/Open.cpp): the 32-bit resolution output, float scale and nullable outputs are confirmed by the PS5 library and FontFt driver. Native system font sets return resolution 1000; the host still uses its selected face resolution because multi-face system font sets are not modeled. - [Writing-line ABI and allocation](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 layouts, creation forms, memory validation, allocator callbacks and error handling are recovered. The 2208-byte line and 64-byte orders use caller memory. Host registry bookkeeping still allocates separately; memory lifetime and concurrent destruction require caller synchronization. - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 confirms output order, calculations and zeroed outputs on invalid handles. A non-null head output with a null tail output causes an unchecked native write; the host throws instead. With no head output, the tail output remains untouched. From 34123d4e290ff498915f0891b4c8b8aef5ee9225 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:42:44 +0300 Subject: [PATCH 0832/1360] fix(font): restore encoded glyph identifiers and validation --- core/libs/prx/libSceFont/src/Open.cpp | 25 +++++++++++++++++++++---- core/libs/tests/GuestFont.cpp | 12 ++++++++++-- docs/dev/debt/libSceFont.md | 2 +- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 8b8480ddd8..36e7e2263d 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -493,6 +493,11 @@ int APS5_VABI sceFontGetLibrary(FontHandle fontHandle, FontLibrary* pLibrary) { } int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* glyphsCount) { + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) { + if (glyphsCount) *glyphsCount = 0; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } if (!glyphsCount) return SCE_FONT_ERROR_INVALID_PARAMETER; *glyphsCount = 0; const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { @@ -504,14 +509,26 @@ int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* gl } int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, std::uint32_t* glyphCode) { + if (glyphCode) *glyphCode = 0; + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; if (!glyphCode) return SCE_FONT_ERROR_INVALID_PARAMETER; - *glyphCode = 0; + if ((code & 0x80000000u) != 0 && (code & 0x3fff0000u) != 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + std::uint32_t index = 0; const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if ((code & 0x80000000u) != 0) { + if (!driver.glyphs_count) return SCE_FONT_ERROR_FATAL; + std::uint32_t count = 0; + const int result = driver.glyphs_count(obj, &count); + if (result != SCE_FONT_OK) return result; + index = code & 0x3fffffffu; + return index < count ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_GLYPH; + } 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); + return driver.glyph_index(obj, code, &index); }); - if (rc != SCE_FONT_OK) *glyphCode = 0; + if (rc == SCE_FONT_OK) *glyphCode = index | 0x80000000u; return rc; } diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index b466265979..cd0085e158 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -423,8 +423,16 @@ int main() { font = nullptr; Require(sceFontOpenFontMemory(library, kerningData.data(), static_cast<std::uint32_t>(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); + Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000001u); + Require(sceFontGetCharGlyphCode(font, 'V', &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000002u); + Require(sceFontGetCharGlyphCode(nullptr, 0, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetFontGlyphsCount(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetCharGlyphCode(font, 0, nullptr) == SCE_FONT_ERROR_NO_SUPPORT_CODE); + Require(sceFontGetCharGlyphCode(font, 0x80000000u, &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000000u); + Require(sceFontGetCharGlyphCode(font, 0x80000002u, &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000002u); + Require(sceFontGetCharGlyphCode(font, 0xc0000001u, &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000001u); + Require(sceFontGetCharGlyphCode(font, 0x80000003u, &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); + Require(sceFontGetCharGlyphCode(font, 0x80010001u, &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_CODE && glyphCode == 0); glyphCode = 1; Require(sceFontGetCharGlyphCode(font, 'B', &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); glyphCode = 1; diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index b75f406bbe..bd2a0098c2 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -5,7 +5,7 @@ - [sceFontSetTypographicDesign and sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp): signatures are confirmed by the supplied PS5 library. Native feature ranges, defaults and driver-dependent availability are not implemented; the host still accepts arbitrary values and stores them without affecting shaping or rendering. - [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [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 -- [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` +- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../../core/libs/prx/libSceFont/src/Open.cpp): signatures, error precedence, failed-output clearing and external-font glyph-code encoding are confirmed by the PS5 library. Multi-face system-font glyph counts, face-selection bits and typographic glyph substitutions are not modeled. - [sceFontGetFontResolution](../../../core/libs/prx/libSceFont/src/Open.cpp): the 32-bit resolution output, float scale and nullable outputs are confirmed by the PS5 library and FontFt driver. Native system font sets return resolution 1000; the host still uses its selected face resolution because multi-face system font sets are not modeled. - [Writing-line ABI and allocation](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 layouts, creation forms, memory validation, allocator callbacks and error handling are recovered. The 2208-byte line and 64-byte orders use caller memory. Host registry bookkeeping still allocates separately; memory lifetime and concurrent destruction require caller synchronization. - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. From 2ee602e66f18bd52f1b81636eb4485d73705ccaf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:44:06 +0300 Subject: [PATCH 0833/1360] fix(ssl): restore alpn clearing and validation --- core/libs/prx/libSceSsl/Export.cpp | 22 ++++++++++++++++++---- core/libs/tests/GuestSsl.cpp | 14 +++++++++++--- docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 4f697ed914..874dad4779 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1040,7 +1040,9 @@ const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { std::shared_ptr<Session> session; { std::lock_guard lock(State().mutex); - session = RequireConnection(__func__, sslConnectionId).session; + const auto* connection = FindConnection(sslConnectionId); + if (!connection) return nullptr; + session = connection->session; } if (!session) return nullptr; std::lock_guard sessionLock(session->mutex); @@ -1049,12 +1051,24 @@ const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { } int APS5_VABI sceSslSetAlpn(int sslConnectionId, const char* proto) { - if (!proto) throw std::invalid_argument("sceSslSetAlpn: null protocol list"); std::lock_guard lock(State().mutex); auto* connection = FindConnection(sslConnectionId); if (!connection) return ErrorInvalidId; - if (!*proto) return ErrorBadLength; - connection->alpn = proto; + connection->alpn.clear(); + if (!proto) return 0; + std::size_t length = 0; + while (length < 65535 && proto[length] != 0) ++length; + if (length >= 65535) return ErrorBadLength; + std::size_t tokenLength = 0; + for (std::size_t index = 0; index <= length; ++index) { + if (index == length || proto[index] == ',') { + if (tokenLength == 0 || tokenLength > 255) return ErrorBadLength; + tokenLength = 0; + } else { + ++tokenLength; + } + } + connection->alpn.assign(proto, length); return 0; } diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 4becc68d8c..12c079d7eb 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -3,6 +3,7 @@ #include <cstdint> #include <cstdlib> #include <exception> +#include <string> extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::size_t); @@ -129,7 +130,14 @@ int main() { Require(sceSslGetAlpnSelected(connection) == nullptr); Require(sceSslSetAlpn(connection, "http/1.1") == 0); Require(sceSslSetAlpn(connection, "") == badLength); - Require(Throws([&] { sceSslSetAlpn(connection, nullptr); })); + Require(sceSslSetAlpn(connection, ",h2") == badLength); + Require(sceSslSetAlpn(connection, "h2,") == badLength); + Require(sceSslSetAlpn(connection, "h2,,http/1.1") == badLength); + Require(sceSslSetAlpn(connection, std::string(255, 'a').c_str()) == 0); + Require(sceSslSetAlpn(connection, std::string(256, 'a').c_str()) == badLength); + Require(sceSslSetAlpn(connection, std::string(65535, 'a').c_str()) == badLength); + Require(sceSslSetAlpn(0, nullptr) == invalidId); + Require(sceSslSetAlpn(connection, nullptr) == 0); Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; Require(sceSslConnect(connection) == badDescriptor); @@ -144,7 +152,7 @@ int main() { Require(sceSslDisableVerifyOption(unknown, serverVerify) == invalidId); Require(sceSslSetMinSslVersion(unknown, 5) == invalidId); Require(sceSslSetAlpn(unknown, "http/1.1") == invalidId); - Require(Throws([&] { sceSslGetAlpnSelected(unknown); })); + Require(sceSslGetAlpnSelected(unknown) == nullptr); Require(sceSslConnect(unknown) == invalidId); Require(sceSslRead(unknown, buffer, sizeof(buffer), 0) == invalidId); Require(sceSslWrite(unknown, buffer, sizeof(buffer), 0) == invalidId); @@ -158,7 +166,7 @@ int main() { Require(sceSslDisableVerifyOption(connection, serverVerify) == invalidId); Require(sceSslSetMinSslVersion(connection, 5) == invalidId); Require(sceSslSetAlpn(connection, "http/1.1") == invalidId); - Require(Throws([&] { sceSslGetAlpnSelected(connection); })); + Require(sceSslGetAlpnSelected(connection) == nullptr); Require(sceSslConnect(connection) == invalidId); Require(sceSslClose(connection) == invalidId); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 71ef1ff74e..f704fcef79 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -10,5 +10,5 @@ - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate -- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list throws (not measured). `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and throw from `sceSslGetAlpnSelected`, whose console return was not measured +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1–255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged From 83f1f63e85ba8451c77723737d4cbcd50676ece9 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:46:06 +0300 Subject: [PATCH 0834/1360] fix(ssl): restore serial number truncation and errors --- core/libs/prx/libSceSsl/Export.cpp | 16 +++++++--------- core/libs/tests/GuestSslLoad.cpp | 15 ++++++++------- core/libs/tests/GuestSslPs5Readers.cpp | 4 ++-- docs/dev/debt/libSceSsl.md | 2 +- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 874dad4779..cb6bf5ad97 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -991,19 +991,17 @@ int APS5_VABI sceSslClose(int sslConnectionId) { } int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* sboData, std::size_t* sboLen) { - if (!sboLen) return ErrorInvalidArg; std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert) return ErrorInvalidValue; const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); - const auto reqLen = crt->serial.len; - if (!sboData || *sboLen == 0) { - *sboLen = reqLen; + if (!sboData) { + if (sboLen) *sboLen = crt->serial.len; return 0; } - if (*sboLen < reqLen) { - *sboLen = reqLen; - return ErrorInvalidArg; - } - std::memcpy(sboData, crt->serial.p, reqLen); + if (!sboLen) throw std::invalid_argument("sceSslGetSerialNumber: output buffer requires a length pointer"); + const auto copyLength = std::min(*sboLen, crt->serial.len); + if (copyLength != 0) std::memcpy(sboData, crt->serial.p, copyLength); return 0; } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 28b894ddb4..30a01dddce 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -182,20 +182,21 @@ void CertificateReaders() { Check(sceSslGetSerialNumber(id, cert, nullptr, &querySize) == 0 && querySize == 3, "query serial size failed"); querySize = 0; unsigned char shortBuf[2]{}; - Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 3, "zero-len serial query failed"); + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 0, "zero-len serial copy changed length"); querySize = sizeof(shortBuf); - Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == static_cast<int>(0x8095177a) && querySize == 3, - "short serial buffer accepted"); + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 2 && shortBuf[0] == 0 && shortBuf[1] == 0x80, + "short serial copy mismatch"); unsigned char serialData[8]{}; std::size_t serialLen = sizeof(serialData); Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); Check(serialLen == sizeof(serialData) && serialData[3] == 0, "a large enough serial buffer changed the caller's length"); Check(serialData[0] == 0x00 && serialData[1] == 0x80 && serialData[2] == 0xa1, "serial bytes mismatch"); - Check(sceSslGetSerialNumber(id, cert, serialData, nullptr) == static_cast<int>(0x8095177a), "null serial length accepted"); - Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); + Throws([&] { sceSslGetSerialNumber(id, cert, serialData, nullptr); }); + Check(sceSslGetSerialNumber(id, cert, nullptr, nullptr) == 0, "empty serial query failed"); + Check(sceSslGetSerialNumber(id, nullptr, serialData, &serialLen) == static_cast<int>(0x8095f007), "null certificate error mismatch"); void* fake = &serialLen; Throws([&] { sceSslGetSerialNumber(id, fake, serialData, &serialLen); }); - Throws([&] { sceSslGetSerialNumber(9999, cert, serialData, &serialLen); }); + Check(sceSslGetSerialNumber(9999, cert, serialData, &serialLen) == InvalidId, "unknown context error mismatch"); void* subject = sceSslGetSubjectName(id, cert); Check(subject != nullptr, "get subject failed"); @@ -301,7 +302,7 @@ void CertificateReaders() { Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); - Throws([&] { sceSslGetSerialNumber(id, cert, serialData, &serialLen); }); + Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == InvalidId, "terminated context error mismatch"); Throws([&] { sceSslGetSubjectName(id, cert); }); Throws([&] { sceSslGetIssuerName(id, cert); }); Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index caffe1cf3e..17d445a3dd 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -55,8 +55,8 @@ void Readers() { Check(serial == std::array<unsigned char, 5>{0x00, 0x80, 0xa1, 0xa5, 0xa5}, "serial copy changed its bytes or guards"); serial.fill(0xa5); length = 2; - Check(sceSslGetSerialNumber(context, certificate, serial.data(), &length) == static_cast<int>(0x8095177a) && length == 3, "short serial buffer did not report its required size"); - Check(std::all_of(serial.begin(), serial.end(), [](auto byte) { return byte == 0xa5; }), "short serial buffer was modified"); + Check(sceSslGetSerialNumber(context, certificate, serial.data(), &length) == 0 && length == 2, "short serial copy changed length"); + Check(serial == std::array<unsigned char, 5>{0x00, 0x80, 0xa5, 0xa5, 0xa5}, "short serial copy changed guard bytes"); Throws([&] { sceSslGetSerialNumber(other, certificate, nullptr, &length); }); auto* subject = sceSslGetSubjectName(context, certificate); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index f704fcef79..280c7e4589 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -8,7 +8,7 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. Native `sceSslGetSerialNumber` copies at most the caller capacity without changing it and queries the required length only when the buffer is null. Unknown contexts return `SSL_ERROR_INVALID_ID`; null certificates return `SSL_ERROR_INVALID_VALUE`. A non-null buffer with a null length would cause a native unchecked read; the host throws. Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. Except for the recovered serial-reader context check, an unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The remaining readers' error codes, the NUL-terminated truncation of name entry strings, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1–255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged From 060a3b6bf9cb1e1aff8f0cf869bb4f4cd25c301f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:47:09 +0300 Subject: [PATCH 0835/1360] fix(ssl): restore certificate name copy bounds and errors --- core/libs/prx/libSceSsl/Export.cpp | 27 ++++++++++++-------------- core/libs/tests/GuestSslLoad.cpp | 27 +++++++++++++++++--------- core/libs/tests/GuestSslPs5Readers.cpp | 2 +- docs/dev/debt/libSceSsl.md | 4 ++-- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index cb6bf5ad97..62fff3dcc2 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1084,37 +1084,34 @@ void* APS5_VABI sceSslGetIssuerName(int sslCtxId, void* sslCert) { } int APS5_VABI sceSslGetNameEntryCount(int sslCtxId, void* certName) { - if (!certName) return ErrorInvalidArg; auto& state = State(); std::lock_guard lock(state.mutex); + if (!state.contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!certName) return ErrorInvalidValue; auto& context = GetContext(sslCtxId); const auto found = context.certNames.find(certName); if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); return static_cast<int>(found->second->entries.size()); } -int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, char* oidname, std::size_t maxOidnameLen, - char* value, std::size_t maxValueLen, std::size_t* valueLen) { - if (!certName) return ErrorInvalidArg; +int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, char* oidname, std::size_t maxOidnameLen, char* value, std::size_t maxValueLen, std::size_t* valueLen) { auto& state = State(); std::lock_guard lock(state.mutex); + if (!state.contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!certName || !oidname || !value || !valueLen) return ErrorInvalidValue; auto& context = GetContext(sslCtxId); const auto found = context.certNames.find(certName); if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); const auto& entries = found->second->entries; - if (entryNum < 0 || static_cast<std::size_t>(entryNum) >= entries.size()) return ErrorInvalidArg; + if (entryNum < 0 || static_cast<std::size_t>(entryNum) >= entries.size()) return ErrorInvalidValue; const auto& entry = entries[static_cast<std::size_t>(entryNum)]; - if (oidname && maxOidnameLen > 0) { - const auto copyLen = std::min<std::size_t>(entry.oid.size(), maxOidnameLen - 1); - std::memcpy(oidname, entry.oid.data(), copyLen); - oidname[copyLen] = '\0'; + const auto copyLength = std::min(entry.value.size(), maxValueLen); + if (copyLength != 0) std::memcpy(value, entry.value.data(), copyLength); + *valueLen = copyLength; + if (maxOidnameLen != 0) { + std::memset(oidname, 0, maxOidnameLen); + std::memcpy(oidname, entry.oid.data(), std::min(entry.oid.size(), maxOidnameLen)); } - if (value && maxValueLen > 0) { - const auto copyLen = std::min<std::size_t>(entry.value.size(), maxValueLen - 1); - std::memcpy(value, entry.value.data(), copyLen); - value[copyLen] = '\0'; - } - if (valueLen) *valueLen = entry.value.size(); return 0; } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 30a01dddce..be75ff9102 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -211,9 +211,9 @@ void CertificateReaders() { Check(sceSslGetNameEntryCount(id, subject) == 3, "subject entry count mismatch"); Check(sceSslGetNameEntryCount(id, issuer) == 3, "issuer entry count mismatch"); - Check(sceSslGetNameEntryCount(id, nullptr) == static_cast<int>(0x8095177a), "null cert name count accepted"); + Check(sceSslGetNameEntryCount(id, nullptr) == static_cast<int>(0x8095f007), "null cert name count accepted"); Throws([&] { sceSslGetNameEntryCount(id, fake); }); - Throws([&] { sceSslGetNameEntryCount(9999, subject); }); + Check(sceSslGetNameEntryCount(9999, subject) == InvalidId, "unknown name context error mismatch"); char oid[32]{}; char val[64]{}; @@ -227,14 +227,23 @@ void CertificateReaders() { char smallVal[4]{}; std::size_t fullLen = 0; - Check(sceSslGetNameEntryInfo(id, subject, 1, nullptr, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); - Check(std::strcmp(smallVal, "Any") == 0 && fullLen == 12, "buffer truncation mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 1, oid, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); + Check(std::memcmp(smallVal, "AnyP", 4) == 0 && fullLen == 4, "buffer truncation mismatch"); + char shortOid[5] = {'x', 'x', 'x', 'x', 'x'}; + char shortValue[5] = {'y', 'y', 'y', 'y', 'y'}; + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 4, shortValue, 4, &fullLen) == 0, "bounded entry query failed"); + Check(std::memcmp(shortOid, "orga", 4) == 0 && shortOid[4] == 'x', "oid copy or guard mismatch"); + Check(std::memcmp(shortValue, "AnyP", 4) == 0 && shortValue[4] == 'y' && fullLen == 4, "value copy or guard mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 0, shortValue, 0, &fullLen) == 0 && fullLen == 0, "zero capacity query failed"); + Check(sceSslGetNameEntryInfo(id, subject, 1, nullptr, 0, shortValue, 0, &fullLen) == static_cast<int>(0x8095f007), "null oid accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 0, nullptr, 0, &fullLen) == static_cast<int>(0x8095f007), "null value accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 0, shortValue, 0, nullptr) == static_cast<int>(0x8095f007), "null value length accepted"); - Check(sceSslGetNameEntryInfo(id, subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast<int>(0x8095177a), "negative index accepted"); - Check(sceSslGetNameEntryInfo(id, subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast<int>(0x8095177a), "out of bounds index accepted"); - Check(sceSslGetNameEntryInfo(id, nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast<int>(0x8095177a), "null certName accepted"); + Check(sceSslGetNameEntryInfo(id, subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast<int>(0x8095f007), "negative index accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast<int>(0x8095f007), "out of bounds index accepted"); + Check(sceSslGetNameEntryInfo(id, nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast<int>(0x8095f007), "null certName accepted"); Throws([&] { sceSslGetNameEntryInfo(id, fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); - Throws([&] { sceSslGetNameEntryInfo(9999, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + Check(sceSslGetNameEntryInfo(9999, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == InvalidId, "unknown entry context error mismatch"); Check(sceSslFreeSslCertName(id, subject) == 0, "free subject name failed"); Check(sceSslFreeSslCertName(id, issuer) == 0, "free issuer name failed"); @@ -327,7 +336,7 @@ void CertificateReaders() { Throws([&] { sceSslGetFingerprint(id2, cert2, 3, sha256); }); Throws([&] { sceSslGetPem(id2, cert2, pemBuf.data(), &writtenLen); }); Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); - Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); + Check(sceSslGetNameEntryCount(id2, subject2) == InvalidId, "terminated name context error mismatch"); Throws([&] { sceSslFreeSslCertName(id2, subject2); }); } diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index 17d445a3dd..c6e302579c 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -89,7 +89,7 @@ void Readers() { Check(sceSslFreeSslCertName(context, subject) == 0, "name free failed"); Throws([&] { sceSslGetNameEntryCount(context, subject); }); Check(sceSslTerm_nid_postfix(context) == 0, "context term failed"); - Throws([&] { sceSslGetNameEntryCount(context, issuer); }); + Check(sceSslGetNameEntryCount(context, issuer) == static_cast<int>(0x8095f006), "terminated context count error mismatch"); Check(sceSslTerm_nid_postfix(other) == 0, "other context term failed"); } diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 280c7e4589..18e5b1ac6e 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -8,7 +8,7 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. Native `sceSslGetSerialNumber` copies at most the caller capacity without changing it and queries the required length only when the buffer is null. Unknown contexts return `SSL_ERROR_INVALID_ID`; null certificates return `SSL_ERROR_INVALID_VALUE`. A non-null buffer with a null length would cause a native unchecked read; the host throws. Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. Except for the recovered serial-reader context check, an unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The remaining readers' error codes, the NUL-terminated truncation of name entry strings, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. Native `sceSslGetSerialNumber` copies at most the caller capacity without changing it and queries the required length only when the buffer is null. Unknown contexts return `SSL_ERROR_INVALID_ID`; null certificates return `SSL_ERROR_INVALID_VALUE`. A non-null buffer with a null length would cause a native unchecked read; the host throws. Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. Except for the recovered serial and name-reader context checks, an unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. Native name readers require a valid context and non-null name and output pointers; invalid indices return `SSL_ERROR_INVALID_VALUE`. Entry values copy up to capacity without adding a terminator and report the copied length. OID names are zero-padded when shorter than capacity and can fill it without a terminator. The remaining readers' error codes and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate -- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1–255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged From e37638505d17311c5d5e87297d6a9651b03c2a49 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:48:46 +0300 Subject: [PATCH 0836/1360] fix(ssl): restore native pem encoding and buffer semantics --- core/libs/prx/libSceSsl/Export.cpp | 37 +++++++++++++++----------- core/libs/tests/GuestSslLoad.cpp | 12 ++++----- core/libs/tests/GuestSslPs5Readers.cpp | 16 +++++++---- docs/dev/debt/libSceSsl.md | 2 +- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 62fff3dcc2..4c22b2f5b4 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1152,25 +1152,30 @@ int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, un } int APS5_VABI sceSslGetPem(int sslCtxId, void* sslCert, char* buf, std::size_t* bufLen) { - if (!bufLen) return ErrorInvalidArg; + if (!sslCert || !bufLen) return ErrorInvalidValue; std::lock_guard lock(State().mutex); - auto& context = GetContext(sslCtxId); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); - PoolScope scope(context.pool); - std::size_t required = 0; - const auto encode = [&](unsigned char* output, std::size_t capacity, std::size_t* written) { - return mbedtls_pem_write_buffer("-----BEGIN CERTIFICATE-----\n", "-----END CERTIFICATE-----\n", crt->raw.p, crt->raw.len, output, capacity, written); - }; - const int sizing = encode(nullptr, 0, &required); - if (sizing != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) CheckBackend(sizing, "SSL PEM sizing failed"); - if (!buf) { - *bufLen = required; - return 0; + const std::size_t encodedLength = Add(crt->raw.len, 2) / 3 * 4; + const std::size_t lineCount = Add(encodedLength, 63) / 64; + const std::size_t required = Add(57, Add(encodedLength, lineCount * 2)); + const std::size_t capacity = *bufLen; + if (buf && capacity != 0) std::memset(buf, 0, capacity); + *bufLen = required; + if (!buf) return 0; + if (capacity < required) return static_cast<int>(0x8095f008); + constexpr char header[] = "-----BEGIN CERTIFICATE-----\r\n"; + constexpr char footer[] = "-----END CERTIFICATE-----\r\n"; + std::memcpy(buf, header, sizeof(header) - 1); + std::size_t offset = sizeof(header) - 1; + for (std::size_t position = 0; position < crt->raw.len; position += 48) { + std::size_t written = 0; + CheckBackend(mbedtls_base64_encode(reinterpret_cast<unsigned char*>(buf + offset), capacity - offset, &written, crt->raw.p + position, std::min<std::size_t>(48, crt->raw.len - position)), "SSL PEM encoding failed"); + offset += written; + buf[offset++] = '\r'; + buf[offset++] = '\n'; } - if (*bufLen < required) throw std::length_error("SSL PEM buffer is too small"); - std::size_t written = 0; - CheckBackend(encode(reinterpret_cast<unsigned char*>(buf), required, &written), "SSL PEM encoding failed"); - *bufLen = written; + std::memcpy(buf + offset, footer, sizeof(footer)); return 0; } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index be75ff9102..a0b79105d1 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -297,14 +297,14 @@ void CertificateReaders() { std::size_t writtenLen = pemBuf.size(); Check(sceSslGetPem(id, cert, pemBuf.data(), &writtenLen) == 0, "write pem failed"); Check(writtenLen == pemLen, "written pem length mismatch"); - Check(std::strstr(pemBuf.data(), "-----BEGIN CERTIFICATE-----\n") != nullptr, "pem header missing"); - Check(std::strstr(pemBuf.data(), "-----END CERTIFICATE-----\n") != nullptr, "pem footer missing"); + Check(std::strstr(pemBuf.data(), "-----BEGIN CERTIFICATE-----\r\n") != nullptr, "pem header missing"); + Check(std::strstr(pemBuf.data(), "-----END CERTIFICATE-----\r\n") != nullptr, "pem footer missing"); char smallPem[8]{}; std::size_t smallPemLen = sizeof(smallPem); - Throws([&] { sceSslGetPem(id, cert, smallPem, &smallPemLen); }); - Check(sceSslGetPem(id, cert, pemBuf.data(), nullptr) == static_cast<int>(0x8095177a), "null pem len accepted"); - Throws([&] { sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen); }); + Check(sceSslGetPem(id, cert, smallPem, &smallPemLen) == static_cast<int>(0x8095f008) && smallPemLen == pemLen, "short PEM size error mismatch"); + Check(sceSslGetPem(id, cert, pemBuf.data(), nullptr) == static_cast<int>(0x8095f007), "null pem len accepted"); + Check(sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen) == static_cast<int>(0x8095f007), "null PEM certificate error mismatch"); Throws([&] { sceSslGetPem(id, fake, pemBuf.data(), &writtenLen); }); Check(sceSslClose(1) == InvalidId, "closing an unknown connection did not report an invalid id"); @@ -317,7 +317,7 @@ void CertificateReaders() { Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); Throws([&] { sceSslGetNotAfter(id, cert, &limit); }); Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); }); - Throws([&] { sceSslGetPem(id, cert, pemBuf.data(), &writtenLen); }); + Check(sceSslGetPem(id, cert, pemBuf.data(), &writtenLen) == InvalidId, "terminated PEM context error mismatch"); const int id2 = sceSslInit_nid_postfix(1024 * 1024); Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index c6e302579c..a9feda8d5a 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -9,6 +9,7 @@ #include <exception> #include <stdexcept> #include <vector> +#include <string> extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); @@ -71,15 +72,20 @@ void Readers() { Check(sceSslGetNameEntryInfo(context, subject, 1, oid.data(), oid.size(), value.data(), value.size(), &length) == 0, "PS5 eight-argument name query failed"); Check(std::strcmp(oid.data(), "organizationName") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed"); + std::string expectedPem; + for (const char byte : std::string(RootPem)) { + if (byte == '\n') expectedPem += '\r'; + expectedPem += byte; + } length = SIZE_MAX; - Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM size query failed"); + Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == expectedPem.size() + 1, "PS5 PEM size query failed"); std::vector<char> pem(length + 2, static_cast<char>(0x5a)); - Check(sceSslGetPem(context, certificate, pem.data(), &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM copy failed"); - Check(std::strcmp(pem.data(), RootPem) == 0 && pem[length] == 0x5a && pem[length + 1] == 0x5a, "PEM copy changed bytes or guards"); + Check(sceSslGetPem(context, certificate, pem.data(), &length) == 0 && length == expectedPem.size() + 1, "PS5 PEM copy failed"); + Check(std::strcmp(pem.data(), expectedPem.c_str()) == 0 && pem[length] == 0x5a && pem[length + 1] == 0x5a, "PEM copy changed bytes or guards"); std::array<char, 4> small{'x', 'x', 'x', 'x'}; length = 2; - Throws([&] { sceSslGetPem(context, certificate, small.data(), &length); }); - Check(std::all_of(small.begin(), small.end(), [](char byte) { return byte == 'x'; }), "short PEM buffer was modified"); + Check(sceSslGetPem(context, certificate, small.data(), &length) == static_cast<int>(0x8095f008) && length == expectedPem.size() + 1, "short PEM error or size mismatch"); + Check(small == std::array<char, 4>{0, 0, 'x', 'x'}, "short PEM clearing or guards mismatch"); Throws([&] { sceSslGetPem(other, certificate, nullptr, &length); }); Check(sceSslUnloadCert(context) == 0, "unload failed"); Check(sceSslGetSerialNumber(context, certificate, nullptr, &length) == 0 && length == 3, "CA snapshot lost its certificate on unload"); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 18e5b1ac6e..bec37f0eab 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -11,4 +11,4 @@ - [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. Native `sceSslGetSerialNumber` copies at most the caller capacity without changing it and queries the required length only when the buffer is null. Unknown contexts return `SSL_ERROR_INVALID_ID`; null certificates return `SSL_ERROR_INVALID_VALUE`. A non-null buffer with a null length would cause a native unchecked read; the host throws. Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. Except for the recovered serial and name-reader context checks, an unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. Native name readers require a valid context and non-null name and output pointers; invalid indices return `SSL_ERROR_INVALID_VALUE`. Entry values copy up to capacity without adding a terminator and report the copied length. OID names are zero-padded when shorter than capacity and can fill it without a terminator. The remaining readers' error codes and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module -- [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged +- [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. From d33bde8ed37fb91fe868de92e632103a472d10d8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:49:56 +0300 Subject: [PATCH 0837/1360] fix(ssl): restore validity and fingerprint validation order --- core/libs/prx/libSceSsl/Export.cpp | 11 +++++++---- core/libs/tests/GuestSslLoad.cpp | 27 +++++++++++++++------------ docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 4c22b2f5b4..b41820f8b5 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1116,23 +1116,27 @@ int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, } int APS5_VABI sceSslGetNotBefore(int sslCtxId, void* sslCert, SceSsl::RtcTick* begin) { - if (!begin) return ErrorInvalidArg; std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert || !begin) return ErrorInvalidValue; const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); begin->tick = TimeToTick(crt->valid_from); return 0; } int APS5_VABI sceSslGetNotAfter(int sslCtxId, void* sslCert, SceSsl::RtcTick* limit) { - if (!limit) return ErrorInvalidArg; std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert || !limit) return ErrorInvalidValue; const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); limit->tick = TimeToTick(crt->valid_to); return 0; } int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, unsigned char* fingerprint) { - if (!fingerprint) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert || !fingerprint) return ErrorInvalidValue; mbedtls_md_type_t mdType; switch (hashType) { case 0: mdType = MBEDTLS_MD_MD5; break; @@ -1145,7 +1149,6 @@ int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, un } const auto* info = mbedtls_md_info_from_type(mdType); if (!info) return ErrorInvalidArg; - std::lock_guard lock(State().mutex); const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); if (mbedtls_md(info, crt->raw.p, crt->raw.len, fingerprint) != 0) return ErrorInvalidArg; return 0; diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index a0b79105d1..3c29135d77 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -253,20 +253,23 @@ void CertificateReaders() { Throws([&] { sceSslFreeSslCertName(9999, subject); }); SceSsl::RtcTick begin{}; + Check(sceSslGetNotBefore(0, nullptr, nullptr) == InvalidId, "date validation precedence mismatch"); + Check(sceSslGetNotAfter(0, nullptr, nullptr) == InvalidId, "date validation precedence mismatch"); + Check(sceSslGetFingerprint(0, nullptr, -1, nullptr) == InvalidId, "fingerprint validation precedence mismatch"); Check(sceSslGetNotBefore(id, cert, &begin) == 0, "get not before failed"); Check(begin.tick == 63927094442000000ULL, "not before tick mismatch"); - Check(sceSslGetNotBefore(id, cert, nullptr) == static_cast<int>(0x8095177a), "null begin accepted"); - Throws([&] { sceSslGetNotBefore(id, nullptr, &begin); }); + Check(sceSslGetNotBefore(id, cert, nullptr) == static_cast<int>(0x8095f007), "null begin accepted"); + Check(sceSslGetNotBefore(id, nullptr, &begin) == static_cast<int>(0x8095f007), "null certificate date error mismatch"); Throws([&] { sceSslGetNotBefore(id, fake, &begin); }); - Throws([&] { sceSslGetNotBefore(9999, cert, &begin); }); + Check(sceSslGetNotBefore(9999, cert, &begin) == InvalidId, "invalid date context error mismatch"); SceSsl::RtcTick limit{}; Check(sceSslGetNotAfter(id, cert, &limit) == 0, "get not after failed"); Check(limit.tick == 64242454442000000ULL, "not after tick mismatch"); - Check(sceSslGetNotAfter(id, cert, nullptr) == static_cast<int>(0x8095177a), "null limit accepted"); - Throws([&] { sceSslGetNotAfter(id, nullptr, &limit); }); + Check(sceSslGetNotAfter(id, cert, nullptr) == static_cast<int>(0x8095f007), "null limit accepted"); + Check(sceSslGetNotAfter(id, nullptr, &limit) == static_cast<int>(0x8095f007), "null certificate date error mismatch"); Throws([&] { sceSslGetNotAfter(id, fake, &limit); }); - Throws([&] { sceSslGetNotAfter(9999, cert, &limit); }); + Check(sceSslGetNotAfter(9999, cert, &limit) == InvalidId, "invalid date context error mismatch"); unsigned char md5[16]{}; Check(sceSslGetFingerprint(id, cert, 0, md5) == 0, "get md5 fingerprint failed"); @@ -283,12 +286,12 @@ void CertificateReaders() { constexpr unsigned char expectedSha256[32] = {0x3d, 0x7d, 0xac, 0xd3, 0x04, 0x90, 0x42, 0xe1, 0x57, 0x19, 0x87, 0x27, 0x54, 0x45, 0xae, 0xed, 0xc4, 0xea, 0xd6, 0xd4, 0x20, 0xa0, 0x7f, 0xc4, 0xf5, 0x26, 0xc1, 0xa0, 0x85, 0x87, 0x5a, 0x4c}; Check(std::memcmp(sha256, expectedSha256, sizeof(sha256)) == 0, "sha256 digest mismatch"); - Check(sceSslGetFingerprint(id, cert, 3, nullptr) == static_cast<int>(0x8095177a), "null fingerprint buffer accepted"); + Check(sceSslGetFingerprint(id, cert, 3, nullptr) == static_cast<int>(0x8095f007), "null fingerprint buffer accepted"); Check(sceSslGetFingerprint(id, cert, -1, sha256) == static_cast<int>(0x8095177a), "negative hash type accepted"); Check(sceSslGetFingerprint(id, cert, 99, sha256) == static_cast<int>(0x8095177a), "invalid hash type accepted"); - Throws([&] { sceSslGetFingerprint(id, nullptr, 3, sha256); }); + Check(sceSslGetFingerprint(id, nullptr, 3, sha256) == static_cast<int>(0x8095f007), "null fingerprint certificate error mismatch"); Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); }); - Throws([&] { sceSslGetFingerprint(9999, cert, 3, sha256); }); + Check(sceSslGetFingerprint(9999, cert, 3, sha256) == InvalidId, "invalid fingerprint context error mismatch"); std::size_t pemLen = 0; Check(sceSslGetPem(id, cert, nullptr, &pemLen) == 0, "query pem size failed"); @@ -314,9 +317,9 @@ void CertificateReaders() { Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == InvalidId, "terminated context error mismatch"); Throws([&] { sceSslGetSubjectName(id, cert); }); Throws([&] { sceSslGetIssuerName(id, cert); }); - Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); - Throws([&] { sceSslGetNotAfter(id, cert, &limit); }); - Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); }); + Check(sceSslGetNotBefore(id, cert, &begin) == InvalidId, "invalid date context error mismatch"); + Check(sceSslGetNotAfter(id, cert, &limit) == InvalidId, "invalid date context error mismatch"); + Check(sceSslGetFingerprint(id, cert, 3, sha256) == InvalidId, "invalid fingerprint context error mismatch"); Check(sceSslGetPem(id, cert, pemBuf.data(), &writtenLen) == InvalidId, "terminated PEM context error mismatch"); const int id2 = sceSslInit_nid_postfix(1024 * 1024); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index bec37f0eab..8e7603a4a1 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -8,7 +8,7 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. Native `sceSslGetSerialNumber` copies at most the caller capacity without changing it and queries the required length only when the buffer is null. Unknown contexts return `SSL_ERROR_INVALID_ID`; null certificates return `SSL_ERROR_INVALID_VALUE`. A non-null buffer with a null length would cause a native unchecked read; the host throws. Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. Except for the recovered serial and name-reader context checks, an unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. Native name readers require a valid context and non-null name and output pointers; invalid indices return `SSL_ERROR_INVALID_VALUE`. Entry values copy up to capacity without adding a terminator and report the copied length. OID names are zero-padded when shorter than capacity and can fill it without a terminator. The remaining readers' error codes and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name handles still use host-owned copies; native ownership and release semantics are not modeled. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. From 31925119d51a84e17a8b8b3a249313daec0158f3 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:51:00 +0300 Subject: [PATCH 0838/1360] fix(ssl): bind certificate name views to certificate lifetime --- core/libs/prx/libSceSsl/Export.cpp | 25 +++++++++++++++---------- core/libs/tests/GuestSslLoad.cpp | 24 +++++++++++++----------- core/libs/tests/GuestSslPs5Readers.cpp | 4 ++-- docs/dev/debt/libSceSsl.md | 2 +- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index b41820f8b5..9d6e172d16 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -222,6 +222,9 @@ struct CertNameEntry { }; struct CertName { + void* certificate = nullptr; + bool subject = false; + std::weak_ptr<CertificateChain> owner; std::vector<CertNameEntry> entries; }; @@ -582,10 +585,17 @@ const char* LongAttributeName(const char* shortName) { void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { std::lock_guard lock(State().mutex); + if (!sslCert || !State().contexts.contains(sslCtxId)) return nullptr; auto& context = GetContext(sslCtxId); const auto* crt = RequireCertificate(function, sslCtxId, sslCert); + for (const auto& [handle, stored] : context.certNames) { + if (stored->certificate == sslCert && stored->subject == subject) return handle; + } const auto* name = subject ? &crt->subject : &crt->issuer; auto certName = std::make_unique<CertName>(); + certName->certificate = sslCert; + certName->subject = subject; + certName->owner = static_cast<CertificateHandle*>(sslCert)->owner; for (const auto* node = name; node; node = node->next) { if (!node->oid.p || node->oid.len == 0) continue; std::string oid; @@ -1006,14 +1016,9 @@ int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* } int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { - if (!certName) return ErrorInvalidArg; - auto& state = State(); - std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); - const auto found = context.certNames.find(certName); - if (found == context.certNames.end()) throw std::invalid_argument("sceSslFreeSslCertName: unknown cert name"); - context.certNames.erase(found); - return 0; + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + return certName ? 0 : ErrorInvalidValue; } int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { @@ -1090,7 +1095,7 @@ int APS5_VABI sceSslGetNameEntryCount(int sslCtxId, void* certName) { if (!certName) return ErrorInvalidValue; auto& context = GetContext(sslCtxId); const auto found = context.certNames.find(certName); - if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); + if (found == context.certNames.end() || found->second->owner.expired()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown or expired cert name"); return static_cast<int>(found->second->entries.size()); } @@ -1101,7 +1106,7 @@ int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, if (!certName || !oidname || !value || !valueLen) return ErrorInvalidValue; auto& context = GetContext(sslCtxId); const auto found = context.certNames.find(certName); - if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); + if (found == context.certNames.end() || found->second->owner.expired()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown or expired cert name"); const auto& entries = found->second->entries; if (entryNum < 0 || static_cast<std::size_t>(entryNum) >= entries.size()) return ErrorInvalidValue; const auto& entry = entries[static_cast<std::size_t>(entryNum)]; diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 3c29135d77..904b4764a7 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -200,14 +200,15 @@ void CertificateReaders() { void* subject = sceSslGetSubjectName(id, cert); Check(subject != nullptr, "get subject failed"); + Check(sceSslGetSubjectName(id, cert) == subject, "subject handle identity changed"); void* issuer = sceSslGetIssuerName(id, cert); Check(issuer != nullptr, "get issuer failed"); - Throws([&] { sceSslGetSubjectName(id, nullptr); }); - Throws([&] { sceSslGetIssuerName(id, nullptr); }); + Check(sceSslGetSubjectName(id, nullptr) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetIssuerName(id, nullptr) == nullptr, "invalid name query returned a handle"); Throws([&] { sceSslGetSubjectName(id, fake); }); Throws([&] { sceSslGetIssuerName(id, fake); }); - Throws([&] { sceSslGetSubjectName(9999, cert); }); - Throws([&] { sceSslGetIssuerName(9999, cert); }); + Check(sceSslGetSubjectName(9999, cert) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetIssuerName(9999, cert) == nullptr, "invalid name query returned a handle"); Check(sceSslGetNameEntryCount(id, subject) == 3, "subject entry count mismatch"); Check(sceSslGetNameEntryCount(id, issuer) == 3, "issuer entry count mismatch"); @@ -247,10 +248,11 @@ void CertificateReaders() { Check(sceSslFreeSslCertName(id, subject) == 0, "free subject name failed"); Check(sceSslFreeSslCertName(id, issuer) == 0, "free issuer name failed"); - Check(sceSslFreeSslCertName(id, nullptr) == static_cast<int>(0x8095177a), "null cert name free accepted"); - Throws([&] { sceSslFreeSslCertName(id, subject); }); - Throws([&] { sceSslFreeSslCertName(id, fake); }); - Throws([&] { sceSslFreeSslCertName(9999, subject); }); + Check(sceSslGetNameEntryCount(id, subject) == 3, "name release invalidated a live certificate name"); + Check(sceSslFreeSslCertName(id, nullptr) == static_cast<int>(0x8095f007), "null cert name free accepted"); + Check(sceSslFreeSslCertName(id, subject) == 0, "non-null name release failed"); + Check(sceSslFreeSslCertName(id, fake) == 0, "non-null name release failed"); + Check(sceSslFreeSslCertName(9999, subject) == InvalidId, "invalid name release context error mismatch"); SceSsl::RtcTick begin{}; Check(sceSslGetNotBefore(0, nullptr, nullptr) == InvalidId, "date validation precedence mismatch"); @@ -315,8 +317,8 @@ void CertificateReaders() { Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == InvalidId, "terminated context error mismatch"); - Throws([&] { sceSslGetSubjectName(id, cert); }); - Throws([&] { sceSslGetIssuerName(id, cert); }); + Check(sceSslGetSubjectName(id, cert) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetIssuerName(id, cert) == nullptr, "invalid name query returned a handle"); Check(sceSslGetNotBefore(id, cert, &begin) == InvalidId, "invalid date context error mismatch"); Check(sceSslGetNotAfter(id, cert, &limit) == InvalidId, "invalid date context error mismatch"); Check(sceSslGetFingerprint(id, cert, 3, sha256) == InvalidId, "invalid fingerprint context error mismatch"); @@ -340,7 +342,7 @@ void CertificateReaders() { Throws([&] { sceSslGetPem(id2, cert2, pemBuf.data(), &writtenLen); }); Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); Check(sceSslGetNameEntryCount(id2, subject2) == InvalidId, "terminated name context error mismatch"); - Throws([&] { sceSslFreeSslCertName(id2, subject2); }); + Check(sceSslFreeSslCertName(id2, subject2) == InvalidId, "invalid name release context error mismatch"); } } diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index a9feda8d5a..3075a601e9 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -66,7 +66,7 @@ void Readers() { Check(sceSslGetNameEntryCount(context, subject) == 3 && sceSslGetNameEntryCount(context, issuer) == 3, "PS5 name entry count failed"); Throws([&] { sceSslGetSubjectName(other, certificate); }); Throws([&] { sceSslGetNameEntryCount(other, subject); }); - Throws([&] { sceSslFreeSslCertName(other, subject); }); + Check(sceSslFreeSslCertName(other, subject) == 0, "non-null name release checked ownership"); std::array<char, 32> oid{}, value{}; length = SIZE_MAX; Check(sceSslGetNameEntryInfo(context, subject, 1, oid.data(), oid.size(), value.data(), value.size(), &length) == 0, "PS5 eight-argument name query failed"); @@ -91,7 +91,7 @@ void Readers() { Check(sceSslGetSerialNumber(context, certificate, nullptr, &length) == 0 && length == 3, "CA snapshot lost its certificate on unload"); Check(sceSslFreeCaList(context, &list) == 0, "CA list free failed"); Throws([&] { sceSslGetSerialNumber(context, certificate, nullptr, &length); }); - Check(sceSslGetNameEntryCount(context, subject) == 3, "owned name depended on freed certificate storage"); + Throws([&] { sceSslGetNameEntryCount(context, subject); }); Check(sceSslFreeSslCertName(context, subject) == 0, "name free failed"); Throws([&] { sceSslGetNameEntryCount(context, subject); }); Check(sceSslTerm_nid_postfix(context) == 0, "context term failed"); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 8e7603a4a1..e6423c45be 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -8,7 +8,7 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown -- [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name handles still use host-owned copies; native ownership and release semantics are not modeled. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. +- [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. From b4ac752769504360b60830632e06212eb3998308 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:52:46 +0300 Subject: [PATCH 0839/1360] fix(ssl): implement native peek reads and flag validation --- core/libs/prx/libSceSsl/Export.cpp | 27 ++++++++++++++++++++++++++- core/libs/tests/GuestSslTransport.cpp | 4 ++++ docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 9d6e172d16..a1bab7e2af 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -296,6 +296,9 @@ struct Session { std::vector<std::string> alpnStrings; std::vector<const char*> alpnList; std::string alpnSelected; + Block peekBuffer; + std::size_t peekOffset = 0; + std::size_t peekSize = 0; bool handshaken = false; bool verifying = false; bool verified = false; @@ -915,6 +918,7 @@ int APS5_VABI sceSslConnect(int sslConnectionId) { } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { + if (static_cast<unsigned int>(flags) > 3) return ErrorInvalidValue; if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); std::shared_ptr<Session> session; if (const int code = Establish(__func__, sslConnectionId, session)) { @@ -932,7 +936,28 @@ int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) const auto total = std::min<std::size_t>(len, INT_MAX); std::size_t received = 0; while (received < total) { - const int result = mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + if (session->peekOffset < session->peekSize) { + const auto count = std::min(total - received, session->peekSize - session->peekOffset); + std::memcpy(bytes + received, session->peekBuffer.get() + session->peekOffset, count); + received += count; + if (flags == 3) break; + session->peekOffset += count; + if (session->peekOffset == session->peekSize) { + session->peekOffset = 0; + session->peekSize = 0; + session->peekBuffer.reset(); + } + if (flags != 0) break; + continue; + } + if (flags == 3 && !session->peekBuffer) session->peekBuffer = Allocate(MBEDTLS_SSL_IN_CONTENT_LEN); + const int result = flags == 3 + ? mbedtls_ssl_read(&session->ssl, session->peekBuffer.get(), MBEDTLS_SSL_IN_CONTENT_LEN) + : mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + if (result > 0 && flags == 3) { + session->peekSize = static_cast<std::size_t>(result); + continue; + } if (result > 0) { received += static_cast<std::size_t>(result); if (flags != 0) break; diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 93fcb1b901..927dc47414 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -459,6 +459,10 @@ int main(int argc, char** argv) { Require(sceSslWrite(connection, buffer.data(), 1, 1) == ErrorInvalidValue, "unknown write flags were accepted"); char request[] = "ping"; Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 4) == ErrorInvalidValue, "unknown read flags were accepted"); + Require(sceSslRead(0, nullptr, 1, -1) == ErrorInvalidValue, "read flags did not precede other validation"); + Require(sceSslRead(connection, buffer.data(), 2, 3) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "short peek failed"); + Require(sceSslRead(connection, buffer.data(), 4, 3) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "repeated larger peek consumed bytes"); Require(sceSslRead(connection, buffer.data(), 2, 1) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "a partial read did not return the available bytes"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "a full read did not stop at the end of the stream"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index e6423c45be..877af46e33 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -6,7 +6,7 @@ - [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslRecv, sceSslReuseConnection and sceSslSend](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Their parameter shapes follow [SharpProspero's pinned bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L222-L262), and are not measured on a PS5. -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate From b35e344585442798da921cdeab560500cb39c17c Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:55:13 +0300 Subject: [PATCH 0840/1360] fix(systemservice): restore browser launch signature and validation --- core/libs/prx/libSceSystemService/Export.cpp | 11 +++++++---- core/libs/tests/GuestSystemService.cpp | 20 ++++++++++++-------- docs/dev/debt/libSceSystemService.md | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 08e4239f19..65eae0e1e4 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -371,10 +371,13 @@ int APS5_VABI sceSystemServiceLaunchPlayerDialog(const void* param) { return SYSTEM_SERVICE_OK; } -int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param) { - (void)uri; - (void)param; - return SCE_KERNEL_ERROR_EOPNOTSUPP; +int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri) { + if (!uri) return static_cast<int>(0x80a10003); + std::size_t length = 0; + while (length < 6016 && uri[length] != 0) ++length; + if (length > 6015) return static_cast<int>(0x80a10007); + if (std::strncmp(uri, "http:", 5) != 0 && std::strncmp(uri, "https:", 6) != 0 && std::strncmp(uri, "psno:", 5) != 0) return static_cast<int>(0x80a10008); + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceSystemServiceLaunchApp(const char* titleId, const char* const* arguments, const void* param) { diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 3913b7dc1c..36e6e11853 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -3,6 +3,7 @@ #include <cstddef> #include <cstdlib> #include <cstring> +#include <string> #include <initializer_list> #include <stdexcept> @@ -45,7 +46,7 @@ extern "C" int APS5_VABI sceSystemServiceDisableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceDisableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceReenableMediaPlay(void); -extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param); +extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri); extern "C" int APS5_VABI sceSystemServiceShowControllerSettings(void); extern "C" int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void); extern "C" int APS5_VABI sceSystemServiceOpenChallengeActivity(void); @@ -96,13 +97,16 @@ int main() { Require(sceSystemServiceShowControllerSettings() == SYSTEM_SERVICE_OK); Require(sceSystemServiceShowDisplaySafeAreaSettings() == SYSTEM_SERVICE_OK); Require(sceSystemServiceOpenChallengeActivity() == SYSTEM_SERVICE_OK); - Require(sceSystemServiceLaunchWebBrowser("http://127.0.0.1:8780/video?v=0", nullptr) == browserUnavailable); - unsigned char browserParam[64]; - std::memset(browserParam, 0x5a, sizeof(browserParam)); - Require(sceSystemServiceLaunchWebBrowser("https://example.com/", browserParam) == browserUnavailable); - for (unsigned char byte : browserParam) Require(byte == 0x5a); - Require(sceSystemServiceLaunchWebBrowser("", nullptr) == browserUnavailable); - Require(sceSystemServiceLaunchWebBrowser(nullptr, nullptr) == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("http:") == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("https:") == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("psno:") == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("") == static_cast<int>(0x80a10008)); + Require(sceSystemServiceLaunchWebBrowser("HTTP:") == static_cast<int>(0x80a10008)); + Require(sceSystemServiceLaunchWebBrowser(nullptr) == static_cast<int>(0x80a10003)); + std::string browserUri = "http:" + std::string(6010, 'a'); + Require(sceSystemServiceLaunchWebBrowser(browserUri.c_str()) == browserUnavailable); + browserUri += 'a'; + Require(sceSystemServiceLaunchWebBrowser(browserUri.c_str()) == static_cast<int>(0x80a10007)); Require(LaunchAppThrows("PPSA99109", nullptr, nullptr)); Require(LaunchAppThrows(nullptr, nullptr, nullptr)); const char* launchArguments[] = {"--backup", nullptr}; diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 8abccbf202..e697b107ae 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -13,7 +13,7 @@ - [sceSystemServiceAddLocalProcessForPsmKit, sceSystemServiceKillLocalProcessForPsmKit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one process and starts or stops no other, so both throw without reading their arguments. The parameters follow Forever Launcher's own firmware 5.50 probes (`ps5/probes/localproc` in [bizkut/ps5-forever](https://github.com/bizkut/ps5-forever)): a path, a null-terminated argv and a 0x90-byte parameter block in, a handle out; the kill takes that handle. Their results on the console are unknown. Forever Launcher (PPSA99030) imports both and stops at load without them. - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown -- [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown +- [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp): the native export takes one URI argument. Null input, the 6015-byte length limit and supported scheme checks are recovered. The host still returns kernel `EOPNOTSUPP` for valid input; a missing browser service's native error is unresolved. - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC From 2ee0131006bc97239c98d4f4fdbd1084769891e4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:55:51 +0300 Subject: [PATCH 0841/1360] fix(vrsetupdialog): restore result type and error precedence --- core/libs/prx/libSceVrSetupDialog/Export.cpp | 18 +++++++----------- core/libs/tests/GuestVrSetupDialog.cpp | 12 ++++++++---- docs/dev/debt/libSceVrSetupDialog.md | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp index 73e247dafe..2aa6aad360 100644 --- a/core/libs/prx/libSceVrSetupDialog/Export.cpp +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -12,13 +12,10 @@ 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<int>(0x80B80003u); constexpr int VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x8A7C0001u); constexpr int VR_SETUP_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x8A7C0002u); constexpr int VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x8A7C0003u); -constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); constexpr int VR_SETUP_DIALOG_ERROR_BUSY = static_cast<int>(0x8A7C0005u); -constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; @@ -50,14 +47,13 @@ int APS5_VABI sceVrSetupDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceVrSetupDialogGetResult(void* result) { - const int status = g_status.load(); - if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &canceled, sizeof(canceled)); - return 0; +int APS5_VABI sceVrSetupDialogGetResult(std::int32_t* result) { + if (result == nullptr) return VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; + *result = COMMON_DIALOG_RESULT_USER_CANCELED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return VR_SETUP_DIALOG_ERROR_BUSY; + return 0; } int APS5_VABI sceVrSetupDialogClose(void) { diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index b879875b89..decf0c81e2 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -8,7 +8,7 @@ int APS5_VABI sceVrSetupDialogInitialize(void); int APS5_VABI sceVrSetupDialogOpen(const VrSetupDialogParam* param); int APS5_VABI sceVrSetupDialogUpdateStatus(void); int APS5_VABI sceVrSetupDialogGetStatus(void); -int APS5_VABI sceVrSetupDialogGetResult(void* result); +int APS5_VABI sceVrSetupDialogGetResult(std::int32_t* result); int APS5_VABI sceVrSetupDialogClose(void); int APS5_VABI sceVrSetupDialogTerminate(void); } @@ -22,8 +22,8 @@ constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast<int>(0x8A7C0001); constexpr int kErrAlreadyInitialized = static_cast<int>(0x8A7C0002); -constexpr int kErrNotFinished = static_cast<int>(0x80B80005); -constexpr int kErrArgNull = static_cast<int>(0x80B8000D); +constexpr int kErrNotFinished = static_cast<int>(0x8A7C0005); +constexpr int kErrArgNull = static_cast<int>(0x8A7C0003); constexpr int kResultUserCanceled = 1; } @@ -36,7 +36,9 @@ int main() { Require(sceVrSetupDialogTerminate() == kErrNotInitialized); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); Require(sceVrSetupDialogClose() == kErrNotInitialized); - Require(sceVrSetupDialogGetResult(result) == static_cast<int>(0x80B80003u)); + Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); + Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(result[0] == -1 && result[1] == -1); Require(sceVrSetupDialogGetStatus() == kStatusNone); Require(sceVrSetupDialogUpdateStatus() == kStatusNone); @@ -47,6 +49,7 @@ int main() { Require(sceVrSetupDialogOpen(nullptr) == static_cast<int>(0x8A7C0003u)); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kResultUserCanceled && result[1] == -1); param.size = 55; Require(sceVrSetupDialogOpen(¶m) == static_cast<int>(0x8A7C0003u)); @@ -78,5 +81,6 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kResultUserCanceled && result[1] == -1); Require(sceVrSetupDialogTerminate() == 0); } diff --git a/docs/dev/debt/libSceVrSetupDialog.md b/docs/dev/debt/libSceVrSetupDialog.md index 138686c11e..6890166ca4 100644 --- a/docs/dev/debt/libSceVrSetupDialog.md +++ b/docs/dev/debt/libSceVrSetupDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). Initialization does not check the native process eligibility record, and the 46-byte setup payload remains opaque. Open validates the recovered 56-byte parameter header. GetResult is absent from the supplied module, so its signature and cancellation result remain inferred. +- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). Initialization does not check the native process eligibility record, and the 46-byte setup payload remains opaque. Open validates the recovered 56-byte parameter header. GetResult writes one 32-bit result after initialization, including when returning the not-finished error; its error codes and validation order are recovered. From 2411c836ced99b6410ed06b5636ba1ec03c3be5f Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:58:31 +0300 Subject: [PATCH 0842/1360] fix(videorecording): type native video output mode query --- core/libs/prx/libSceVideoRecordingP/Export.cpp | 2 +- docs/dev/debt/libSceVideoRecordingP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 93cf413444..621cce7d32 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -110,7 +110,7 @@ int APS5_VABI sceVideoRecordingGetSupportInfo(VideoRecordingSupportInfo* info) { return 0; } -int APS5_VABI sceVideoRecordingGetVideoOutMode(void* mode) { +int APS5_VABI sceVideoRecordingGetVideoOutMode(std::uint32_t* mode) { (void)mode; NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md index ec40ae9fd8..5d8096130d 100644 --- a/docs/dev/debt/libSceVideoRecordingP.md +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -5,7 +5,7 @@ - [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - declared with `void(void* param)` signature following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L67-L69); unimplemented and throws `NotImplemented_nid_no_patch` - [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. - [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. -- [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode is absent from the supplied recording modules, so its signature remains unverified. Both exports remain unimplemented. +- [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode takes a four-byte output confirmed by the native recording assembly and Avcap2 IPC descriptor; the mode values remain unresolved. Both exports remain unimplemented. - [sceVideoRecordingInit, sceVideoRecordingTerm](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - parameterless initialization and termination stubs; unimplemented and throw `NotImplemented_nid_no_patch` - [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - initialization stub taking an optional parameter structure pointer; unverified and throws `NotImplemented_nid_no_patch` - [sceVideoRecordingOpen, sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the 32-byte and 40-byte parameter variants, four argument registers and 32-bit heap size are recovered. Mode, output mode, option and format semantics remain incomplete. QueryHddSize2 returns a signed 64-bit size or error; recording and sizing exports remain unimplemented. From f2ee3065f1aa71384eabebb5cbd13b7758bac209 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 12:59:49 +0300 Subject: [PATCH 0843/1360] fix(videorecording): define query parameter output layout --- core/libs/prx/libSceVideoRecordingP/Export.cpp | 16 +++++++++++++++- docs/dev/debt/libSceVideoRecordingP.md | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 621cce7d32..9a930b3476 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -11,6 +11,20 @@ struct VideoRecordingContext; +struct VideoRecordingQueryParam { + std::uint64_t size; + std::uint32_t reserved8; + std::int32_t mode; + std::uint8_t reserved16[12]; + std::uint32_t option; + std::uint8_t reserved32[32]; +}; + +static_assert(sizeof(VideoRecordingQueryParam) == 64); +static_assert(offsetof(VideoRecordingQueryParam, mode) == 12); +static_assert(offsetof(VideoRecordingQueryParam, option) == 28); + + struct VideoRecordingSupportInfo { std::uint8_t data[18]; }; @@ -54,7 +68,7 @@ constexpr int SceVideoRecordingStatusNone = 0; extern "C" { -void APS5_VABI _sceVideoRecordingQueryParam_nid_postfix(void* param) { +void APS5_VABI _sceVideoRecordingQueryParam_nid_postfix(VideoRecordingQueryParam* param) { (void)param; NotImplemented_nid_no_patch("_sceVideoRecordingQueryParam"); } diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md index 5d8096130d..484e8d00ae 100644 --- a/docs/dev/debt/libSceVideoRecordingP.md +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -2,12 +2,12 @@ ### Unknown function info -- [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - declared with `void(void* param)` signature following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L67-L69); unimplemented and throws `NotImplemented_nid_no_patch` +- [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the supplied module reads a 64-bit size and writes a 32-bit mode at offset 12 for sizes at least 16, plus a 32-bit option at offset 28 for sizes at least 64. Mode/option meanings remain unresolved; the protected implementation is unchanged. - [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. - [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. - [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode takes a four-byte output confirmed by the native recording assembly and Avcap2 IPC descriptor; the mode values remain unresolved. Both exports remain unimplemented. - [sceVideoRecordingInit, sceVideoRecordingTerm](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - parameterless initialization and termination stubs; unimplemented and throw `NotImplemented_nid_no_patch` -- [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - initialization stub taking an optional parameter structure pointer; unverified and throws `NotImplemented_nid_no_patch` +- [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the native initializer consumes no caller arguments; the former optional parameter is not read by the Avcap2 initializer or recording setup. Its unused parameter slot is retained because the protected body references it. - [sceVideoRecordingOpen, sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the 32-byte and 40-byte parameter variants, four argument registers and 32-bit heap size are recovered. Mode, output mode, option and format semantics remain incomplete. QueryHddSize2 returns a signed 64-bit size or error; recording and sizing exports remain unimplemented. - [Handle and support queries](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native recording handles are context pointers, and GetSupportInfo writes 18 bytes. Support payload field semantics remain incomplete. RestModeProcessStart, RestModeProcessGetStatus and SendCommand consume no caller arguments in the supplied native module; their existing unused parameter slots remain because the protected bodies reference those parameter names. From 90e4e930aeca7d2847b4e667d6dd962fe7740b2b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:04:51 +0300 Subject: [PATCH 0844/1360] fix(audiopropagation): define source and system diagnostic layouts --- .../AudioPropagation.hpp | 45 ++++++++++++++++++- docs/dev/debt/libSceAudioPropagation.md | 4 +- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 99db7ef6e3..8323b813c9 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -47,8 +47,49 @@ static_assert(sizeof(SystemOptions) == SystemOptionsSize); static_assert(offsetof(SystemOptions, frameSamples) == 32); static_assert(offsetof(SystemOptions, flags) == 44); -struct SourceInfo; -struct SystemInfo; +struct SourceInfo { + AudioPropagationHandle system; + float position[3]; + std::uint32_t reserved20; + float direction[3]; + std::uint32_t reserved36; + std::uint32_t attribute2Mode; + float attribute2Values[3]; + float attribute3Values[4]; + std::uint32_t pathCapacity; + std::uint32_t parameter76; +}; + +struct SystemInfo { + void* cpuMemory; + std::size_t cpuMemorySize; + std::uint32_t index; + std::uint32_t frameSamples; + std::uint32_t parameter24; + std::uint32_t maxSources; + std::uint32_t maxMaterials; + std::uint32_t pathCapacityPerSource; + float propagationSpeed; + std::uint32_t reserved44; +}; + +static_assert(sizeof(SourceInfo) == 80); +static_assert(offsetof(SourceInfo, position) == 8); +static_assert(offsetof(SourceInfo, direction) == 24); +static_assert(offsetof(SourceInfo, attribute2Mode) == 40); +static_assert(offsetof(SourceInfo, attribute2Values) == 44); +static_assert(offsetof(SourceInfo, attribute3Values) == 56); +static_assert(offsetof(SourceInfo, pathCapacity) == 72); +static_assert(offsetof(SourceInfo, parameter76) == 76); +static_assert(sizeof(SystemInfo) == 48); +static_assert(offsetof(SystemInfo, cpuMemorySize) == 8); +static_assert(offsetof(SystemInfo, index) == 16); +static_assert(offsetof(SystemInfo, frameSamples) == 20); +static_assert(offsetof(SystemInfo, parameter24) == 24); +static_assert(offsetof(SystemInfo, maxSources) == 28); +static_assert(offsetof(SystemInfo, maxMaterials) == 32); +static_assert(offsetof(SystemInfo, pathCapacityPerSource) == 36); +static_assert(offsetof(SystemInfo, propagationSpeed) == 40); struct ReportRegistration; struct ReportArguments { diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 501b37adab..cff417667f 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. Source/system diagnostic fields still need reconstruction. -- [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; source/system diagnostic fields still need reconstruction. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. +- [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. From c54adfb288f522ae076f34bd44469cac385a14ea Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:08:56 +0300 Subject: [PATCH 0845/1360] fix(ult): restore thread diagnostic context and fiber callback type --- core/libs/SceTypes.hpp | 2 +- core/libs/prx/libSceFiber/Export.cpp | 4 ++-- core/libs/prx/libSceUlt/SceUltTypes.hpp | 26 ++++++++++++++++++++++++- core/libs/tests/Fiber.cpp | 1 + docs/dev/debt/libSceUlt.md | 2 +- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 95443d9e12..85bb99bedf 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -333,7 +333,7 @@ struct VaList { }; #pragma pack(pop) -using FiberEntry = void* (*)(std::uint64_t, void*); +using FiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); struct FiberOptParam { std::uint32_t magic; diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 81aec33202..54b0930521 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -37,7 +37,7 @@ static constexpr std::uint64_t FIBER_CONTEXT_FILL = 0xdeadbeefdeadbeefull; static constexpr std::size_t FIBER_HOST_STACK_MARGIN = 1u << 20; static constexpr std::size_t FIBER_GUEST_STACK_MIN = 64u << 10; -using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); +using GuestFiberEntry = FiberEntry; // A fiber that is switching away stays Suspending until its context is saved; whoever runs next // on that host thread publishes Suspended. Fibers migrate between threads, so a resumer must never @@ -515,7 +515,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { auto* fiber = AsFiber(object); if (!fiber || !fiber_info) return object && fiber_info ? SCE_FIBER_ERROR_INVALID : SCE_FIBER_ERROR_NULL; if (fiber_info->size != sizeof(FiberInfo)) return SCE_FIBER_ERROR_INVALID; - fiber_info->entry = reinterpret_cast<FiberEntry>(fiber->entry); + fiber_info->entry = fiber->entry; fiber_info->arg_on_initialize = fiber->argOnInitialize; fiber_info->addr_context = SizedWindow(fiber); fiber_info->size_context = fiber->contextSize; diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index d8bff75850..4c77af1649 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -239,7 +239,31 @@ static_assert(offsetof(UltUlthreadRuntimeInfo, reserved60) == 60); static_assert(offsetof(UltUlthreadRuntimeInfo, workerStackSize) == 64); static_assert(offsetof(UltUlthreadRuntimeInfo, reserved) == 72); -struct UltUlthreadInfo; +struct UltUlthreadInfo { + std::uint64_t size; + void* object; + char name[32]; + std::uint32_t contextAttribute; + std::uint32_t reserved52; + void* runtime; + FiberInfo fiber; + std::byte reserved[64]; +}; + +static_assert(sizeof(FiberInfo) == 128); +static_assert(offsetof(FiberInfo, entry) == 8); +static_assert(offsetof(FiberInfo, arg_on_initialize) == 16); +static_assert(offsetof(FiberInfo, addr_context) == 24); +static_assert(offsetof(FiberInfo, size_context) == 32); +static_assert(offsetof(FiberInfo, name) == 40); +static_assert(offsetof(FiberInfo, size_context_margin) == 72); +static_assert(sizeof(UltUlthreadInfo) == 256); +static_assert(offsetof(UltUlthreadInfo, object) == 8); +static_assert(offsetof(UltUlthreadInfo, name) == 16); +static_assert(offsetof(UltUlthreadInfo, contextAttribute) == 48); +static_assert(offsetof(UltUlthreadInfo, runtime) == 56); +static_assert(offsetof(UltUlthreadInfo, fiber) == 64); +static_assert(offsetof(UltUlthreadInfo, reserved) == 192); struct UltMutexState { std::mutex _mutex; diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 73b6a9afb1..2267ee2497 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -270,6 +270,7 @@ int main() { Require(sceFiberStartContextSizeCheck(1) == FiberErrorInvalid, "start with flags"); Require(_sceFiberInitializeImpl_nid_postfix(checked, "unchecked", CheckedEntry, 0, g_checkedContext.data(), g_checkedContext.size(), nullptr, 0) == 0, "initialize unchecked"); Require(sceFiberGetInfo(checked, &info) == 0 && info.size_context_margin == ~0ull, "no margin without size check"); + Require(info.entry == CheckedEntry, "info preserves the guest entry callback type"); Require(sceFiberFinalize(checked) == 0, "finalize unchecked"); Require(sceFiberStartContextSizeCheck(0) == 0, "start size check"); Require(sceFiberStartContextSizeCheck(0) == FiberErrorState, "start twice"); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 31ae1d7025..12002369d2 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -6,7 +6,7 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. -- [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. The nested context in `UltUlthreadInfo` still requires reconstruction. +- [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional From cf717cd80f96dd8e8eb9f4423671357698e66cbd Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:10:21 +0300 Subject: [PATCH 0846/1360] fix(videorecording): restore info selectors and unopened errors --- core/libs/CMakeLists.txt | 2 +- .../libs/prx/libSceVideoRecordingP/Export.cpp | 47 ++++++++++--------- core/libs/tests/GuestOfflineImports.cpp | 26 ++++++++++ docs/dev/debt/libSceVideoRecordingP.md | 2 + 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 84952285be..f84fa93f40 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -384,7 +384,7 @@ if(BUILD_TESTING) add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) + target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc) configure_windows_unwind(guest_offline_imports_tests) add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests) if(Python3_Interpreter_FOUND) diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 9a930b3476..c3b67583f6 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -2,12 +2,6 @@ #include <cstddef> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include <cstring> -#include <map> -#include <mutex> -#include <stdexcept> -#include <string> -#include <vector> struct VideoRecordingContext; @@ -61,8 +55,6 @@ static_assert(offsetof(VideoRecordingParam2, duration) == 20); static_assert(offsetof(VideoRecordingParam2, format) == 36); namespace { -std::mutex g_infoMutex; -std::map<int32_t, std::vector<unsigned char>> g_info; constexpr int SceVideoRecordingStatusNone = 0; } @@ -95,14 +87,19 @@ int APS5_VABI sceVideoRecordingCopyBGRAtoNV12(void* dst, const void* src, uint32 return 0; } -int APS5_VABI sceVideoRecordingGetInfo(int32_t info, void* data, size_t size) { - if (data == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(g_infoMutex); - const auto found = g_info.find(info); - if (found == g_info.end()) throw std::logic_error(std::string(__func__) + ": info " + std::to_string(info) + " was never set"); - if (size < found->second.size()) APS5_INVALID_ARG_EX; - std::memcpy(data, found->second.data(), found->second.size()); - return 0; +int APS5_VABI sceVideoRecordingGetInfo(std::int32_t info, void*, std::size_t) { + switch (info) { + case 0xa001: + case 0xa002: + case 0xa003: + case 0xa005: + case 0xa00a: + case 0xa00b: + case 0xa00c: + return static_cast<int>(0x80a80006); + default: + return static_cast<int>(0x80a80003); + } } int APS5_VABI sceVideoRecordingGetInfoByHandle(VideoRecordingContext* handle, int32_t info, void* data, size_t size) { @@ -194,12 +191,18 @@ int APS5_VABI sceVideoRecordingSendCommand(int cmd, const void* param) { return 0; } -int APS5_VABI sceVideoRecordingSetInfo(int32_t info, const void* data, size_t size) { - if (data == nullptr && size != 0) APS5_INVALID_ARG_EX; - const auto* bytes = static_cast<const unsigned char*>(data); - std::lock_guard lock(g_infoMutex); - g_info[info].assign(bytes, bytes + size); - return 0; +int APS5_VABI sceVideoRecordingSetInfo(std::int32_t info, const void* data, std::size_t size) { + switch (info) { + case 0x8002: + case 0x8003: + if (!data || size < sizeof(float)) return static_cast<int>(0x80a80003); + return static_cast<int>(0x80a80002); + case 0xa004: + case 0xa006: + return static_cast<int>(0x80a80006); + default: + return static_cast<int>(0x80a80003); + } } int APS5_VABI sceVideoRecordingStart(void) { diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index f89a850b63..54acdb9e0c 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -9,6 +9,8 @@ #include <string_view> extern "C" { +int APS5_VABI sceVideoRecordingGetInfo(std::int32_t info, void* data, std::size_t size); +int APS5_VABI sceVideoRecordingSetInfo(std::int32_t info, const void* data, std::size_t size); int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t); int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t); int APS5_VABI sceHttpSetCookieEnabled(int, int); @@ -34,6 +36,30 @@ int APS5_VABI sceRudpEnd(); static void Require(bool value) { if (!value) std::abort(); } int main() { + constexpr int recordingInvalid = static_cast<int>(0x80a80003); + constexpr int recordingNotOpen = static_cast<int>(0x80a80006); + constexpr int recordingUnavailable = static_cast<int>(0x80a80002); + std::uint32_t recordingOutput = 0x12345678; + for (std::int32_t selector = 0; selector <= 0x10000; ++selector) { + const bool query = selector == 0xa001 || selector == 0xa002 || selector == 0xa003 || selector == 0xa005 || selector == 0xa00a || selector == 0xa00b || selector == 0xa00c; + const bool control = selector == 0xa004 || selector == 0xa006; + Require(sceVideoRecordingGetInfo(selector, &recordingOutput, sizeof(recordingOutput)) == (query ? recordingNotOpen : recordingInvalid)); + Require(recordingOutput == 0x12345678); + Require(sceVideoRecordingGetInfo(selector, nullptr, 0) == (query ? recordingNotOpen : recordingInvalid)); + Require(sceVideoRecordingSetInfo(selector, nullptr, 0) == (control ? recordingNotOpen : recordingInvalid)); + } + for (std::int32_t selector : {-1, INT32_MIN, INT32_MAX}) { + Require(sceVideoRecordingGetInfo(selector, nullptr, 0) == recordingInvalid); + Require(sceVideoRecordingSetInfo(selector, nullptr, 0) == recordingInvalid); + } + const float recordingVolume = 0.5f; + for (std::int32_t selector : {0x8002, 0x8003}) { + for (std::size_t size : {0u, 1u, 3u, 4u, 8u}) { + Require(sceVideoRecordingSetInfo(selector, nullptr, size) == recordingInvalid); + Require(sceVideoRecordingSetInfo(selector, &recordingVolume, size) == (size < sizeof(float) ? recordingInvalid : recordingUnavailable)); + } + } + for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { bool abortFailed = false; bool deleteFailed = false; diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md index 484e8d00ae..0b82a50648 100644 --- a/docs/dev/debt/libSceVideoRecordingP.md +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceVideoRecordingGetInfo, sceVideoRecordingSetInfo](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) support native validation and errors before a recording context exists. Open and initialization remain protected unimplemented exports; live recording queries, control and volume changes are unavailable. + - [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the supplied module reads a 64-bit size and writes a 32-bit mode at offset 12 for sizes at least 16, plus a 32-bit option at offset 28 for sizes at least 64. Mode/option meanings remain unresolved; the protected implementation is unchanged. - [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. - [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. From ee76c0cd98841ab4453a6e0a7f2344655a4343db Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:22:45 +0300 Subject: [PATCH 0847/1360] fix(kernel): restore coredump buffer validation and data type argument --- core/libs/prx/libkernel/System/src/Coredump.cpp | 12 ++++-------- core/libs/tests/GuestCoredumpAttach.cpp | 17 +++++++++-------- core/libs/tests/GuestCoredumpUserData.cpp | 10 +++++++--- docs/dev/debt/libkernel.md | 6 +++--- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 3bc229914c..05cca26f98 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -69,7 +69,7 @@ int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpSetUserDataType(void) { +int APS5_VABI sceCoredumpSetUserDataType(std::int32_t type) { NotImplemented_nid_no_patch("Uxqkdta7wEg"); return 0; } @@ -83,21 +83,17 @@ void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len) { } int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size) { - requireRegionOutsideHandler(__func__, data, size); + if (!data || size > INT32_MAX) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { - const auto address = reinterpret_cast<uintptr_t>(info); - if (info == nullptr || size != 16 || address % alignof(uint64_t) != 0 || size > UINTPTR_MAX - address) - throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported CPU stop-info buffer outside the core dump handler"); + if (!info || (size != 8 && size != 16)) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { - const auto address = reinterpret_cast<uintptr_t>(threadContextInfo); - if (threadContextInfo == nullptr || threadContextInfoSize != 144 || address % alignof(uint64_t) != 0 || threadContextInfoSize > UINTPTR_MAX - address) - throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported thread context buffer outside the core dump handler"); + if (!threadContextInfo || threadContextInfoSize != 144) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index 43e32238e0..ab03f0b21c 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -52,6 +52,7 @@ int main() { Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpAttachUserMemoryFile(UserFileTruncateIfExceedLimit | 2, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 8) == NotInCoredumpHandler); Require(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler); Require(region == original && context == originalContext); @@ -76,16 +77,16 @@ int main() { Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, nullptr, region.size()); }, "sceCoredumpAttachUserMemoryFile")); Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), 0); }, "sceCoredumpAttachUserMemoryFile")); Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachUserMemoryFile")); - Require(Throws([&] { sceCoredumpGetStopInfoCpu(nullptr, 16); }, "sceCoredumpGetStopInfoCpu")); - Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 1, 16); }, "sceCoredumpGetStopInfoCpu")); - Require(Throws([&] { sceCoredumpGetStopInfoCpu(reinterpret_cast<void*>(UINTPTR_MAX - 7u), 16); }, "sceCoredumpGetStopInfoCpu")); + Require(sceCoredumpGetStopInfoCpu(nullptr, 16) == InvalidArgument); + Require(sceCoredumpGetStopInfoCpu(region.data() + 1, 16) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(reinterpret_cast<void*>(UINTPTR_MAX - 7u), 16) == NotInCoredumpHandler); for (const size_t size : {size_t{0}, size_t{1}, size_t{15}, size_t{17}, SIZE_MAX}) - Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 16, size); }, "sceCoredumpGetStopInfoCpu")); - Require(Throws([&] { sceCoredumpGetThreadContextInfo(nullptr, 144); }, "sceCoredumpGetThreadContextInfo")); - Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data() + 1, 144); }, "sceCoredumpGetThreadContextInfo")); - Require(Throws([&] { sceCoredumpGetThreadContextInfo(reinterpret_cast<void*>(UINTPTR_MAX - 7u), 144); }, "sceCoredumpGetThreadContextInfo")); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, size) == InvalidArgument); + Require(sceCoredumpGetThreadContextInfo(nullptr, 144) == InvalidArgument); + Require(sceCoredumpGetThreadContextInfo(context.data() + 1, 144) == NotInCoredumpHandler); + Require(sceCoredumpGetThreadContextInfo(reinterpret_cast<void*>(UINTPTR_MAX - 7u), 144) == NotInCoredumpHandler); for (const size_t size : {size_t{0}, size_t{1}, size_t{143}, size_t{145}, SIZE_MAX}) - Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data(), size); }, "sceCoredumpGetThreadContextInfo")); + Require(sceCoredumpGetThreadContextInfo(context.data(), size) == InvalidArgument); Require(sceCoredumpUnregisterCoredumpHandler() == 0); Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index 30776ebbcc..3578510707 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -17,6 +17,7 @@ int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const } namespace { +constexpr int64_t InvalidArgument = static_cast<int32_t>(0x81180000u); constexpr int64_t NotInCoredumpHandler = static_cast<int32_t>(0x81180003u); bool handlerRan = false; int handlerContext = 0; @@ -74,9 +75,12 @@ int main() { sceCoredumpDebugTextOut(text, static_cast<int>(sizeof(text) - 1)); Require(!handlerRan); - Require(Throws([&] { sceCoredumpWriteUserData(nullptr, region.size()); }, "sceCoredumpWriteUserData")); - Require(Throws([&] { sceCoredumpWriteUserData(region.data(), 0); }, "sceCoredumpWriteUserData")); - Require(Throws([&] { sceCoredumpWriteUserData(region.data(), SIZE_MAX); }, "sceCoredumpWriteUserData")); + Require(sceCoredumpWriteUserData(nullptr, region.size()) == InvalidArgument); + Require(sceCoredumpWriteUserData(nullptr, 0) == InvalidArgument); + Require(sceCoredumpWriteUserData(region.data(), 0) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), INT32_MAX) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), std::size_t{INT32_MAX} + 1) == InvalidArgument); + Require(sceCoredumpWriteUserData(region.data(), SIZE_MAX) == InvalidArgument); Require(Throws([&] { sceCoredumpDebugTextOut(nullptr, 4); }, "sceCoredumpDebugTextOut")); Require(Throws([&] { sceCoredumpDebugTextOut(text, -1); }, "sceCoredumpDebugTextOut")); Require(Throws([&] { sceCoredumpAttachUserFile(1, nullptr); }, "sceCoredumpAttachUserFile")); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6a4136fc3c..3f835e75f8 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -83,10 +83,10 @@ - [seteuid, setegid, setuid, setgid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw -- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported -- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) takes `(void* threadContextInfo, size_t threadContextInfoSize)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 144-byte block of 18 64-bit registers follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs#L120-L141). It answers as `sceCoredumpGetStopInfoCpu`: the handler is never invoked here, so an aligned 144-byte request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block, and null, unaligned or wrapping buffers and other sizes throw, as the console validation order is unverified. Planet Coaster (PPSA01736) imports it +- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) has no active coredump context. Native null/size validation is recovered, including both 8-byte and 16-byte records; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. Active-handler data retrieval remains unimplemented. +- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) validates a nonnull 144-byte output using the native checks. Active-handler register capture remains unimplemented; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants -- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: PPSA11386 imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) have no active coredump report. WriteUserData validates a nonnull buffer and size at most `INT32_MAX`, including zero; accepted requests return the sign-extended outside-handler error. DebugTextOut still prints to stderr instead of a report; its validation and report integration remain unverified. - [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either - [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 - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed From fe4016f612fa310852096af149d967308a16601d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:22:46 +0300 Subject: [PATCH 0848/1360] fix(voice): restore initialization and thread parameter validation --- core/libs/SceTypes.hpp | 51 +++++++++++++++++++++++++- core/libs/prx/libSceVoice/Export.cpp | 20 ++++++---- core/libs/tests/GuestVoice.cpp | 30 ++++++++++++++- core/libs/tests/GuestVoicePortAttr.cpp | 2 +- docs/dev/debt/libSceVoice.md | 4 +- 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 85bb99bedf..8b3eaa3f89 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -975,13 +975,60 @@ struct AudiodecM4aacInfo { std::int32_t i_result; }; +struct VoiceEvent { + std::int32_t type; + std::uint32_t reserved; + void* userData; + union { + struct { + std::uint16_t count; + std::uint16_t ports[7]; + } dataReady; + struct { + std::uint16_t port; + std::uint8_t state; + std::uint8_t reserved[13]; + } deviceState; + }; +}; + +using VoiceEventHandler = void (APS5_VABI*)(const VoiceEvent* event); + +struct VoiceThreadsParams { + std::uint32_t mode; + std::int32_t firstPriority; + std::uint64_t firstAffinity; + std::int32_t secondPriority; + std::uint32_t reserved20; + std::uint64_t secondAffinity; + std::int32_t thirdPriority; + std::uint32_t reserved36; + std::uint64_t thirdAffinity; +}; + +static_assert(sizeof(VoiceEvent) == 32); +static_assert(offsetof(VoiceEvent, userData) == 8); +static_assert(offsetof(VoiceEvent, dataReady) == 16); +static_assert(offsetof(VoiceEvent, deviceState) == 16); +static_assert(sizeof(VoiceThreadsParams) == 48); +static_assert(offsetof(VoiceThreadsParams, firstAffinity) == 8); +static_assert(offsetof(VoiceThreadsParams, secondPriority) == 16); +static_assert(offsetof(VoiceThreadsParams, secondAffinity) == 24); +static_assert(offsetof(VoiceThreadsParams, thirdPriority) == 32); +static_assert(offsetof(VoiceThreadsParams, thirdAffinity) == 40); + struct VoiceInitParam { std::int32_t app_type; - std::uint64_t on_event; + VoiceEventHandler on_event; void* user_data; - std::uint8_t reserved[32 - sizeof(std::int32_t) - sizeof(std::uint64_t) - sizeof(void*)]; + std::uint8_t reserved[16]; }; +static_assert(sizeof(VoiceInitParam) == 40); +static_assert(offsetof(VoiceInitParam, on_event) == 8); +static_assert(offsetof(VoiceInitParam, user_data) == 16); +static_assert(offsetof(VoiceInitParam, reserved) == 24); + struct VoicePortParam { std::int32_t port_type; std::uint16_t threshold; diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index b89fea488a..c070175dc4 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -68,20 +68,26 @@ bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInP extern "C" { -int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { - (void)version; - if (param == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceInit(const VoiceInitParam* param, std::int32_t version) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (voice.initialized) return VoiceErrorAlreadyInitialized; + if (voice.initialized) return static_cast<std::int32_t>(0x804e0802u); + if (!param || version != 100) return InvalidArgument; + if (param->app_type != 4 && param->app_type != 0x08000000 && param->app_type != 0x10000000 && param->app_type != 0x20000000) return InvalidArgument; voice.appType = param->app_type; voice.initialized = true; return 0; } -int APS5_VABI sceVoiceSetThreadsParams(void* params) { - if (params == nullptr) APS5_INVALID_ARG_EX; - return 0; +int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams* params) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (!params) return InvalidArgument; + if (voice.started) return static_cast<std::int32_t>(0x804e080bu); + if (params->mode == 0) return 0; + if (params->mode != 1) return InvalidArgument; + Fail(__func__, "worker priority and affinity are not implemented"); } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index ac56c3d949..4f18783667 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -7,7 +7,8 @@ #include <stdexcept> extern "C" { -int APS5_VABI sceVoiceInit(VoiceInitParam*, std::int32_t); +int APS5_VABI sceVoiceInit(const VoiceInitParam*, std::int32_t); +int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams*); int APS5_VABI sceVoiceStart(const VoiceStartParam*); int APS5_VABI sceVoiceEnd_nid_postfix(); int APS5_VABI sceVoiceCreatePort(std::uint32_t*, const VoicePortParam*); @@ -89,12 +90,37 @@ int main() { Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); Require(sceVoiceEnableChat() == notInitialized); Require(sceVoiceDisableChat() == notInitialized); + Require(sceVoiceSetThreadsParams(nullptr) == notInitialized); + VoiceThreadsParams threads{}; + Require(sceVoiceSetThreadsParams(&threads) == notInitialized); VoiceInitParam init{}; + Require(sceVoiceInit(nullptr, 100) == invalidArgument); + for (const auto mode : {-1, 0, 1, 3, 5, 0x07ffffff, 0x08000001, 0x10000001, 0x20000001, INT32_MAX}) { + init.app_type = mode; + Require(sceVoiceInit(&init, 100) == invalidArgument); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + } init.app_type = 0x10000000; + for (const auto version : {-1, 0, 99, 101, INT32_MAX}) { + Require(sceVoiceInit(&init, version) == invalidArgument); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + } VoiceStartParam start{}; Require(sceVoiceInit(&init, 100) == 0); - Require(sceVoiceInit(&init, 100) == AlreadyInitialized); + Require(sceVoiceInit(nullptr, 0) == static_cast<std::int32_t>(0x804e0802u)); + Require(sceVoiceInit(&init, 100) == static_cast<std::int32_t>(0x804e0802u)); + Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); + Require(sceVoiceSetThreadsParams(&threads) == 0); + threads.mode = 2; + Require(sceVoiceSetThreadsParams(&threads) == invalidArgument); + threads.mode = UINT32_MAX; + Require(sceVoiceSetThreadsParams(&threads) == invalidArgument); + threads.mode = 1; + RequireThrows([&] { sceVoiceSetThreadsParams(&threads); }); + threads.mode = 0; Require(sceVoiceStart(&start) == 0); + Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); + Require(sceVoiceSetThreadsParams(&threads) == static_cast<std::int32_t>(0x804e080bu)); Require(sceVoiceGetResourceInfo(nullptr) == invalidArgument); struct ResourceOutput { VoiceResourceInfo info; diff --git a/core/libs/tests/GuestVoicePortAttr.cpp b/core/libs/tests/GuestVoicePortAttr.cpp index 082c828ec5..e7d05b55d9 100644 --- a/core/libs/tests/GuestVoicePortAttr.cpp +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -9,7 +9,7 @@ #include <string_view> extern "C" { -int APS5_VABI sceVoiceInit(VoiceInitParam*, int32_t); +int APS5_VABI sceVoiceInit(const VoiceInitParam*, int32_t); int APS5_VABI sceVoiceEnd_nid_postfix(void); int APS5_VABI sceVoiceCreatePort(uint32_t*, const VoicePortParam*); int APS5_VABI sceVoiceDeletePort(uint32_t); diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index df5a8e4c45..db5a284c09 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -2,7 +2,7 @@ ### Functional -- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization still lacks native mode/version validation, allocator callbacks and backend creation; port creation does not enforce native resource limits. +- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization validates native modes and version 100, but still lacks event callback delivery and backend creation; port creation does not enforce native resource limits. - [sceVoiceEnableChat and sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp): zero-argument signatures and the not-initialized error are confirmed. Initialized calls throw because the required audio chat backend is not implemented; they no longer report success for a local flag change. ### Unknown function info @@ -20,3 +20,5 @@ - [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. + +- [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the 48-byte mode/priority/affinity layout and initialization, null, started-state and mode validation are recovered. Mode 1 throws because worker scheduling is not implemented; mode 0 is the native no-op. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. From f76db36d9f50f4a5ccf37fc0cfe2be3ac0306b53 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:23:01 +0300 Subject: [PATCH 0849/1360] fix(hmd2): restore buffer size query pair return type --- core/libs/prx/libSceHmd2/Export.cpp | 2 +- core/libs/prx/libSceHmd2/Hmd2.hpp | 5 +++++ core/libs/tests/GuestHmd2.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceHmd2.md | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index d348fcefe5..bcc53524e0 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -293,7 +293,7 @@ int APS5_VABI sceHmd2ReprojectionInitialize(const Hmd2::ReprojectionInitializePa return 0; } -int APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index c5ae98b060..4623331f91 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -60,9 +60,13 @@ static_assert(sizeof(FieldOfView) == 16); struct SizeAlign { std::uint64_t size; std::uint64_t align; + + constexpr SizeAlign(std::uint64_t size = 0, std::uint64_t align = 0) noexcept : size(size), align(align) {} }; static_assert(sizeof(SizeAlign) == 16); +static_assert(offsetof(SizeAlign, size) == 0); +static_assert(offsetof(SizeAlign, align) == 8); } @@ -76,6 +80,7 @@ std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView); std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView); std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign(); Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 4f05d57f02..e1bd7b8c52 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -10,6 +10,10 @@ #include <cstring> #include <stdexcept> +namespace Hmd2 { +struct ReprojectionOutput; +} + extern "C" { std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0); std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1); @@ -55,6 +59,9 @@ std::int32_t APS5_VABI Hmd2BeforeInitializeError1003(); std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne(); std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400(); std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000(); +int APS5_VABI sceHmd2GazeActivateGazeTracking(std::int32_t mode); +int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(Hmd2::ReprojectionOutput* output); +int APS5_VABI sceHmd2SetFieldOfView(const Hmd2::FieldOfView* fieldOfView); } namespace { @@ -299,6 +306,16 @@ int main() { InstallProcessParameters(); TestBeforeInitialize(); TestWithoutHeadset(); + bool threw = false; + try { + static_cast<void>(sceHmd2ReprojectionQueryBufferSizeAlign()); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "buffer size query did not throw"); + Require(sceHmd2GazeActivateGazeTracking(0) == 0, "gaze activation before initialization"); + Require(sceHmd2ReprojectionGetCurrentOutput(nullptr) == Hmd2::ReprojectionNotInitialized, "reprojection before initialization"); + Require(sceHmd2SetFieldOfView(nullptr) == Hmd2::ParameterNull, "null field of view"); std::puts("HMD2 tests passed"); return 0; } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 20fdeb5810..25f1bc36a6 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -32,7 +32,7 @@ ### Functional - [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled -- [Reprojection size/alignment queries](../../../core/libs/prx/libSceHmd2/Export.cpp) - all five exports consume no arguments and return two uint64 values in RAX:RDX. Four host queries return values measured without a headset; native display sizes are calculated through the graphics layout service and see-through size follows internal configuration. QueryBufferSizeAlign changes calculation with runtime state; its protected body still throws and its declared scalar return type does not represent the native pair. Correcting that return type also requires changing its return statement, outside the permitted body edits. +- [Reprojection size/alignment queries](../../../core/libs/prx/libSceHmd2/Export.cpp) - all five exports consume no arguments and return two uint64 values in RAX:RDX. Four host queries return values measured without a headset; native display sizes are calculated through the graphics layout service and see-through size follows internal configuration. QueryBufferSizeAlign changes calculation with runtime state; its native pair return type is restored, while its protected body remains unchanged and throws. - [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720004` to these 15 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `kAMsjYHBbNQ` (sceHmd2InternalSetVolume), `5m73zmLpsQ0` (sceHmd2InternalResetViewPosition), `EPeH4xuQ2XQ` (sceHmd2InternalGetStatus), `TJ8lyRsjFY4` (sceHmd2InternalClearTrackerFts), `-XQVcLnyX68` (sceHmd2InternalSetSlamCommand), `giQwQoHhYYA` (sceHmd2InternalDfuStart), `zNc9IkgawMI`, `-tnG5UJPvAc` (sceHmd2GazeStartUserCalibration), `FPhFmG+jXuQ` (sceHmd2GazeStopUserCalibration), `CKlkvNngofQ`, `yL1CZaciFWE` (sceHmd2GazeSetUserCalibrationPoint), `qlzgyG3s0RY`, `kTIDrDLzMv0`, `9Gee0ef8a+U`, `ecZVjQdwmBI` (sceHmd2GazeSetEnabledEye). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded From b9fb0d289759aef378ae74da9111a629b06a1d52 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:24:25 +0300 Subject: [PATCH 0850/1360] fix(kernel): restore signed coredump attachment lengths --- .../prx/libkernel/System/src/Coredump.cpp | 15 +++++------- core/libs/tests/GuestCoredumpAttach.cpp | 23 ++++++++++++------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 05cca26f98..07cb01cbe8 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -19,11 +19,6 @@ static void requireRegionOutsideHandler(const char* function, const void* mem, s throw std::runtime_error(std::string(function) + ": null, empty or wrapping region outside the core dump handler"); } -static int attachOutsideCoredumpHandler(const char* function, const void* mem, size_t size) { - requireRegionOutsideHandler(function, mem, size); - return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; -} - static void requireTextOutsideHandler(const char* function, const char* argument, const char* text) { if (text == nullptr) throw std::runtime_error(std::string(function) + ": null " + argument + " outside the core dump handler"); } @@ -106,13 +101,15 @@ int APS5_VABI sceCoredumpConfigDumpMode(int mode) { return mode == 1 ? 0 : SCE_COREDUMP_ERROR_INVALID_ARGUMENT; } -int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) { +int APS5_VABI sceCoredumpAttachUserMemoryFile(std::uint32_t userValue, const void* mem, std::int32_t size) { (void)userValue; - return attachOutsideCoredumpHandler(__func__, mem, size); + (void)mem; + return size < 0 ? 0 : SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size) { +int APS5_VABI sceCoredumpAttachMemoryRegion(std::uint32_t userValue, const void* mem, std::int32_t size) { (void)userValue; - return attachOutsideCoredumpHandler(__func__, mem, size); + (void)mem; + return size < 0 ? 0 : SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } } diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index ab03f0b21c..d3d5466b57 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -10,8 +10,8 @@ extern "C" { int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); -int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size); -int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size); +int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, std::int32_t size); +int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, std::int32_t size); int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size); int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize); int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size); @@ -71,12 +71,12 @@ int main() { Require(region == original && context == originalContext); Require(!handlerRan); - Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, nullptr, region.size()); }, "sceCoredumpAttachMemoryRegion")); - Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, region.data(), 0); }, "sceCoredumpAttachMemoryRegion")); - Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachMemoryRegion")); - Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, nullptr, region.size()); }, "sceCoredumpAttachUserMemoryFile")); - Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), 0); }, "sceCoredumpAttachUserMemoryFile")); - Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachUserMemoryFile")); + Require(sceCoredumpAttachMemoryRegion(1, nullptr, region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegion(1, region.data(), 0) == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegion(1, region.data(), -1) == 0); + Require(sceCoredumpAttachUserMemoryFile(1, nullptr, region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(1, region.data(), 0) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(1, region.data(), -1) == 0); Require(sceCoredumpGetStopInfoCpu(nullptr, 16) == InvalidArgument); Require(sceCoredumpGetStopInfoCpu(region.data() + 1, 16) == NotInCoredumpHandler); Require(sceCoredumpGetStopInfoCpu(reinterpret_cast<void*>(UINTPTR_MAX - 7u), 16) == NotInCoredumpHandler); @@ -88,6 +88,13 @@ int main() { for (const size_t size : {size_t{0}, size_t{1}, size_t{143}, size_t{145}, SIZE_MAX}) Require(sceCoredumpGetThreadContextInfo(context.data(), size) == InvalidArgument); + for (const auto length : {INT32_MIN, -1, 0, 1, INT32_MAX}) { + const int expected = length < 0 ? 0 : NotInCoredumpHandler; + Require(sceCoredumpAttachMemoryRegion(0, nullptr, length) == expected); + Require(sceCoredumpAttachUserMemoryFile(0, nullptr, length) == expected); + Require(sceCoredumpAttachMemoryRegion(0xffffffffu, region.data(), length) == expected); + Require(sceCoredumpAttachUserMemoryFile(0xffffffffu, region.data(), length) == expected); + } Require(sceCoredumpUnregisterCoredumpHandler() == 0); Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpAttachUserMemoryFile(1, region.data(), region.size()) == NotInCoredumpHandler); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 3f835e75f8..d8c3a355a9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -85,7 +85,7 @@ - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) has no active coredump context. Native null/size validation is recovered, including both 8-byte and 16-byte records; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. Active-handler data retrieval remains unimplemented. - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) validates a nonnull 144-byte output using the native checks. Active-handler register capture remains unimplemented; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. -- [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants +- [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot attach memory to an active coredump. The native signed 32-bit length is recovered: negative lengths are ignored successfully; other calls return the outside-handler error without accessing memory. Capture and kernel-side record limits remain unimplemented. - [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) have no active coredump report. WriteUserData validates a nonnull buffer and size at most `INT32_MAX`, including zero; accepted requests return the sign-extended outside-handler error. DebugTextOut still prints to stderr instead of a report; its validation and report integration remain unverified. - [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either - [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 From f4447aa2520b2a2e286f424d8c8668ef30eabddf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:26:44 +0300 Subject: [PATCH 0851/1360] fix(kernel): restore coredump registration and file flag validation --- .../prx/libkernel/System/src/Coredump.cpp | 35 ++++++++------- core/libs/tests/GuestCoredumpAttach.cpp | 9 ++++ core/libs/tests/GuestCoredumpUserData.cpp | 44 ++++++++++++++++--- docs/dev/debt/libkernel.md | 4 +- 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 07cb01cbe8..f618d1e2ea 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -2,37 +2,41 @@ #include <cstddef> #include <atomic> #include <cstdio> +#include <cstring> #include <stdexcept> #include <string> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" // The handler is recorded but never invoked: host crashes are not turned into guest core dumps. static std::atomic<uint64_t> g_coredumpHandler{0}; static std::atomic<uint64_t> g_coredumpContext{0}; +static std::atomic<std::uint32_t> coredumpSdkVersion{0}; static constexpr int SCE_COREDUMP_ERROR_INVALID_ARGUMENT = static_cast<int>(0x81180000u); static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast<int>(0x81180003u); -static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) { - if (mem == nullptr || size == 0 || size > UINTPTR_MAX - reinterpret_cast<uintptr_t>(mem)) - throw std::runtime_error(std::string(function) + ": null, empty or wrapping region outside the core dump handler"); -} - -static void requireTextOutsideHandler(const char* function, const char* argument, const char* text) { - if (text == nullptr) throw std::runtime_error(std::string(function) + ": null " + argument + " outside the core dump handler"); +static bool validUserFileFlags(std::uint32_t userValue) { + return coredumpSdkVersion.load(std::memory_order_relaxed) < 0x06500000 || (userValue & 0xc0000000u) != 0x40000000u; } extern "C" { int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stack_size, uint64_t context) { - (void)stack_size; + if (g_coredumpHandler.load(std::memory_order_relaxed)) return static_cast<int>(0x81180002u); + if (!handler || stack_size < 0x4000 || stack_size >= 0x20000000) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + const auto* parameters = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, parameters + 16, sizeof(sdkVersion)); + coredumpSdkVersion.store(sdkVersion, std::memory_order_relaxed); g_coredumpHandler.store(handler, std::memory_order_relaxed); g_coredumpContext.store(context, std::memory_order_relaxed); return 0; } int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void) { + if (!g_coredumpHandler.load(std::memory_order_relaxed)) return static_cast<int>(0x81180001u); g_coredumpHandler.store(0, std::memory_order_relaxed); g_coredumpContext.store(0, std::memory_order_relaxed); return 0; @@ -45,9 +49,9 @@ int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_ } -int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) { - (void)userValue; - requireTextOutsideHandler(__func__, "path", path); +int APS5_VABI sceCoredumpAttachUserFile(std::uint32_t userValue, const char* path) { + if (!path || !validUserFileFlags(userValue)) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + if (static_cast<std::int32_t>(std::strlen(path)) < 0) return 0; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } @@ -57,11 +61,10 @@ int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size) { return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) { - (void)userValue; - requireRegionOutsideHandler(__func__, mem, size); - requireTextOutsideHandler(__func__, "name", name); - return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(std::uint32_t userValue, const void* mem, std::int32_t size, const char* name) { + (void)mem; + if (!name || !validUserFileFlags(userValue)) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + return size < 0 ? 0 : SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpSetUserDataType(std::int32_t type) { diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index d3d5466b57..5d3b7ebb5e 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -1,3 +1,4 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include <array> #include <cstddef> @@ -20,6 +21,7 @@ int APS5_VABI sceCoredumpConfigDumpMode(int mode); } namespace { +alignas(16) std::array<std::byte, 0x40> process{}; constexpr int InvalidArgument = static_cast<int>(0x81180000u); constexpr int NotInCoredumpHandler = static_cast<int>(0x81180003u); constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u; @@ -42,6 +44,13 @@ bool Throws(TAttach attach, const char* function) { } int main() { + const std::uint64_t processSize = process.size(); + const std::uint32_t processMagic = 0x4942524f; + const std::uint32_t sdkVersion = 0x06500000; + std::memcpy(process.data(), &processSize, sizeof(processSize)); + std::memcpy(process.data() + 8, &processMagic, sizeof(processMagic)); + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size())); alignas(uint64_t) std::array<unsigned char, 64> region{}; for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast<unsigned char>(index * 7 + 3); const auto original = region; diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index 3578510707..3b7c6c3817 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -1,3 +1,4 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include <array> #include <cstddef> @@ -13,10 +14,11 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); -int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name); +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, std::int32_t size, const char* name); } namespace { +alignas(16) std::array<std::byte, 0x40> process{}; constexpr int64_t InvalidArgument = static_cast<int32_t>(0x81180000u); constexpr int64_t NotInCoredumpHandler = static_cast<int32_t>(0x81180003u); bool handlerRan = false; @@ -56,10 +58,23 @@ void RequireAttachesRefused(const std::array<unsigned char, 64>& region) { } int main() { + const std::uint64_t processSize = process.size(); + const std::uint32_t processMagic = 0x4942524f; + const std::uint32_t sdkVersion = 0x06500000; + std::memcpy(process.data(), &processSize, sizeof(processSize)); + std::memcpy(process.data() + 8, &processMagic, sizeof(processMagic)); + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size())); std::array<unsigned char, 64> region{}; for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast<unsigned char>(index * 5 + 1); const auto original = region; const char text[] = "coredump text\n"; + Require(sceCoredumpUnregisterCoredumpHandler() == static_cast<int>(0x81180001u)); + Require(sceCoredumpRegisterCoredumpHandler(0, 0x4000, 0) == InvalidArgument); + for (const std::size_t stackSize : {std::size_t{0}, std::size_t{0x3fff}, std::size_t{0x20000000}, SIZE_MAX}) { + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast<std::uint64_t>(&Handler), stackSize, 0) == InvalidArgument); + } + Require(sceCoredumpAttachUserFile(0x40000000u, "") == NotInCoredumpHandler); Require(NotInCoredumpHandler < 0); RequireWritesRefused(region); @@ -70,6 +85,9 @@ int main() { sceCoredumpDebugTextOut(nullptr, 0); Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast<uint64_t>(&Handler), 16 * 1024, reinterpret_cast<uint64_t>(&handlerContext)) == 0); + Require(sceCoredumpRegisterCoredumpHandler(0, 0, 0) == static_cast<int>(0x81180002u)); + Require(sceCoredumpAttachUserFile(0x40000000u, "") == InvalidArgument); + Require(sceCoredumpAttachMemoryRegionAsUserFile(0x40000000u, nullptr, -1, "") == InvalidArgument); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast<int>(sizeof(text) - 1)); @@ -83,16 +101,30 @@ int main() { Require(sceCoredumpWriteUserData(region.data(), SIZE_MAX) == InvalidArgument); Require(Throws([&] { sceCoredumpDebugTextOut(nullptr, 4); }, "sceCoredumpDebugTextOut")); Require(Throws([&] { sceCoredumpDebugTextOut(text, -1); }, "sceCoredumpDebugTextOut")); - Require(Throws([&] { sceCoredumpAttachUserFile(1, nullptr); }, "sceCoredumpAttachUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, nullptr, region.size(), "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), 0, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), SIZE_MAX, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), region.size(), nullptr); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(sceCoredumpAttachUserFile(1, nullptr) == InvalidArgument); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, nullptr, region.size(), "a.txt") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), 0, "a.txt") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), -1, "a.txt") == 0); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), region.size(), nullptr) == InvalidArgument); Require(sceCoredumpUnregisterCoredumpHandler() == 0); RequireWritesRefused(region); RequireAttachesRefused(region); Require(region == original); + Require(sceCoredumpUnregisterCoredumpHandler() == static_cast<int>(0x81180001u)); + Require(sceCoredumpAttachUserFile(0x40000000u, "") == InvalidArgument); + for (const std::uint32_t version : {0x064fffffu, 0x06500000u, 0x12000000u}) { + std::memcpy(process.data() + 16, &version, sizeof(version)); + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast<std::uint64_t>(&Handler), 0x4000, 0) == 0); + for (const std::uint32_t flags : {0u, 0xffffu, 0x10000u, 0x40000000u, 0x40000001u, 0x80000000u, 0xc0000000u, UINT32_MAX}) { + const bool invalid = version >= 0x06500000 && (flags & 0xc0000000u) == 0x40000000u; + Require(sceCoredumpAttachUserFile(flags, "") == (invalid ? InvalidArgument : NotInCoredumpHandler)); + Require(sceCoredumpAttachMemoryRegionAsUserFile(flags, nullptr, 0, "") == (invalid ? InvalidArgument : NotInCoredumpHandler)); + Require(sceCoredumpAttachMemoryRegionAsUserFile(flags, nullptr, -1, "") == (invalid ? InvalidArgument : 0)); + Require(sceCoredumpAttachMemoryRegionAsUserFile(flags, nullptr, -1, nullptr) == InvalidArgument); + } + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + } Require(!handlerRan); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d8c3a355a9..52eea8e8bb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -12,6 +12,8 @@ ### Unknown function info +- [sceCoredumpRegisterCoredumpHandler, sceCoredumpUnregisterCoredumpHandler](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) retain registration metadata but do not create the native report thread or deliver callbacks. Native registration-state precedence, stack bounds and SDK capture are recovered; thread-creation failures and active-report synchronization remain unimplemented. + - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. @@ -87,7 +89,7 @@ - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) validates a nonnull 144-byte output using the native checks. Active-handler register capture remains unimplemented; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot attach memory to an active coredump. The native signed 32-bit length is recovered: negative lengths are ignored successfully; other calls return the outside-handler error without accessing memory. Capture and kernel-side record limits remain unimplemented. - [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) have no active coredump report. WriteUserData validates a nonnull buffer and size at most `INT32_MAX`, including zero; accepted requests return the sign-extended outside-handler error. DebugTextOut still prints to stderr instead of a report; its validation and report integration remain unverified. -- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either +- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot write files to an active coredump. Native null-name checks, the signed 32-bit memory length and SDK 6.50 flag validation are recovered. Nonnegative requests return the outside-handler error; negative memory lengths are ignored after name/flag validation. Kernel-side path handling and capture limits remain unimplemented. - [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 - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed - [AC1FtjqMCL0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature From 0f668a2ea86c7c7c92c4c53ae8c7f267153cf6e5 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:27:45 +0300 Subject: [PATCH 0852/1360] fix(voice): restore start memory checks and lifecycle errors --- core/libs/prx/libSceVoice/Export.cpp | 12 ++++++---- core/libs/tests/GuestVoice.cpp | 33 ++++++++++++++++++++++++++++ docs/dev/debt/libSceVoice.md | 2 ++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index c070175dc4..6573ceaffd 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -75,6 +75,7 @@ int APS5_VABI sceVoiceInit(const VoiceInitParam* param, std::int32_t version) { if (!param || version != 100) return InvalidArgument; if (param->app_type != 4 && param->app_type != 0x08000000 && param->app_type != 0x10000000 && param->app_type != 0x20000000) return InvalidArgument; voice.appType = param->app_type; + voice.nextId = 0; voice.initialized = true; return 0; } @@ -93,8 +94,11 @@ int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams* params) { int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) return VoiceErrorNotInitialized; - if (param == nullptr) APS5_INVALID_ARG_EX; + if (!voice.initialized) return NotInitialized; + if (!param || !param->container) return InvalidArgument; + if (voice.started) return 0; + const std::uint32_t memorySize = voice.appType == 0x10000000 || voice.appType == 0x20000000 ? 0x40000 : 0xc0000; + if (param->mem_size != memorySize) return static_cast<std::int32_t>(0x804e0806u); voice.started = true; return 0; } @@ -102,7 +106,7 @@ int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { int APS5_VABI sceVoiceStop(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return NotInitialized; voice.started = false; return 0; } @@ -110,7 +114,7 @@ int APS5_VABI sceVoiceStop(void) { int APS5_VABI sceVoiceEnd_nid_postfix(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return NotInitialized; voice.ports.clear(); voice.connections.clear(); voice.started = false; diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 4f18783667..7bf4ab9a1c 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -5,11 +5,13 @@ #include <cstring> #include <initializer_list> #include <stdexcept> +#include <vector> extern "C" { int APS5_VABI sceVoiceInit(const VoiceInitParam*, std::int32_t); int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams*); int APS5_VABI sceVoiceStart(const VoiceStartParam*); +int APS5_VABI sceVoiceStop(); int APS5_VABI sceVoiceEnd_nid_postfix(); int APS5_VABI sceVoiceCreatePort(std::uint32_t*, const VoicePortParam*); int APS5_VABI sceVoiceDeletePort(std::uint32_t); @@ -106,6 +108,11 @@ int main() { Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); } VoiceStartParam start{}; + Require(sceVoiceStart(nullptr) == notInitialized); + Require(sceVoiceStart(&start) == notInitialized); + Require(sceVoiceStop() == notInitialized); + Require(sceVoiceEnd_nid_postfix() == notInitialized); + std::vector<std::byte> voiceMemory(0xc0000); Require(sceVoiceInit(&init, 100) == 0); Require(sceVoiceInit(nullptr, 0) == static_cast<std::int32_t>(0x804e0802u)); Require(sceVoiceInit(&init, 100) == static_cast<std::int32_t>(0x804e0802u)); @@ -118,7 +125,19 @@ int main() { threads.mode = 1; RequireThrows([&] { sceVoiceSetThreadsParams(&threads); }); threads.mode = 0; + Require(sceVoiceStart(nullptr) == invalidArgument); + Require(sceVoiceStart(&start) == invalidArgument); + start.container = voiceMemory.data(); + Require(sceVoiceStart(&start) == static_cast<std::int32_t>(0x804e0806u)); + start.mem_size = 0x40000; Require(sceVoiceStart(&start) == 0); + start.mem_size = 0; + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceStart(nullptr) == invalidArgument); + start.container = nullptr; + Require(sceVoiceStart(&start) == invalidArgument); + start.container = voiceMemory.data(); + start.mem_size = 0x40000; Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); Require(sceVoiceSetThreadsParams(&threads) == static_cast<std::int32_t>(0x804e080bu)); Require(sceVoiceGetResourceInfo(nullptr) == invalidArgument); @@ -192,6 +211,20 @@ int main() { for (const auto mode : {4, 0x08000000, 0x10000000, 0x20000000}) { init.app_type = mode; Require(sceVoiceInit(&init, 100) == 0); + const std::uint32_t memorySize = mode == 4 || mode == 0x08000000 ? 0xc0000 : 0x40000; + start.mem_size = memorySize - 1; + Require(sceVoiceStart(&start) == static_cast<std::int32_t>(0x804e0806u)); + start.mem_size = memorySize + 1; + Require(sceVoiceStart(&start) == static_cast<std::int32_t>(0x804e0806u)); + start.mem_size = memorySize; + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceStop() == 0); + Require(sceVoiceStop() == 0); + Require(sceVoiceSetThreadsParams(&threads) == 0); + VoicePortParam portParam{}; + portParam.port_type = 0; + std::uint32_t port = UINT32_MAX; + Require(sceVoiceCreatePort(&port, &portParam) == 0 && port == 0); Require(sceVoiceGetResourceInfo(&resourceOutput.info) == 0); Require(resourceOutput.info.maxVoiceInputPorts == (mode == 4 ? 0 : mode == 0x20000000 ? 7 : 15)); Require(resourceOutput.info.maxVoiceOutputPorts == (mode == 4 ? 0 : 4)); diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index db5a284c09..9cea739a9c 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -22,3 +22,5 @@ - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. - [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the 48-byte mode/priority/affinity layout and initialization, null, started-state and mode validation are recovered. Mode 1 throws because worker scheduling is not implemented; mode 0 is the native no-op. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. + +- [sceVoiceStart, sceVoiceStop and sceVoiceEnd](../../../core/libs/prx/libSceVoice/Export.cpp) validate initialization, container presence and native mode-dependent memory sizes, but do not partition guest working memory or run the audio workers. Start/stop currently change software state only; backend failures and active-stream shutdown are not reproduced. From 09fb692fa1e184f7435151e2b3f2aab6e0d9b84d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:33:18 +0300 Subject: [PATCH 0853/1360] fix(voice): restore port format validation and resource limits --- core/libs/prx/libSceVoice/Export.cpp | 76 +++++++++++++--- core/libs/tests/GuestVoice.cpp | 120 +++++++++++++++++++++++++ core/libs/tests/GuestVoicePortAttr.cpp | 7 ++ docs/dev/debt/libSceVoice.md | 2 +- 4 files changed, 194 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 6573ceaffd..fc13a6b73a 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,6 +1,7 @@ #include <cstdint> #include <cstddef> #include <cstring> +#include <deque> #include <map> #include <mutex> #include <set> @@ -32,6 +33,8 @@ struct Port { std::int32_t type; float volume; std::uint32_t bitrate; + std::uint16_t bufferSize; + std::uint16_t frameSize; bool muted = false; bool paused = false; }; @@ -41,7 +44,7 @@ struct Voice { bool initialized = false; bool started = false; std::int32_t appType = 0; - std::uint32_t nextId = 0; + std::deque<std::uint32_t> freePorts; std::map<std::uint32_t, Port> ports; std::set<std::pair<std::uint32_t, std::uint32_t>> connections; }; @@ -75,7 +78,8 @@ int APS5_VABI sceVoiceInit(const VoiceInitParam* param, std::int32_t version) { if (!param || version != 100) return InvalidArgument; if (param->app_type != 4 && param->app_type != 0x08000000 && param->app_type != 0x10000000 && param->app_type != 0x20000000) return InvalidArgument; voice.appType = param->app_type; - voice.nextId = 0; + voice.freePorts.clear(); + for (std::uint32_t id = 0; id < 128; ++id) voice.freePorts.push_back(id); voice.initialized = true; return 0; } @@ -123,16 +127,67 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { return 0; } -int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { +int APS5_VABI sceVoiceCreatePort(std::uint32_t* portId, const VoicePortParam* param) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) return VoiceErrorNotInitialized; - if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; - if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); - const bool voicePort = param->port_type == PortInVoice || param->port_type == PortOutVoice; - const auto id = voice.nextId++; - voice.ports.emplace(id, Port{param->port_type, param->volume, voicePort ? static_cast<std::uint32_t>(param->voice.bitrate) : 0u}); - *port_id = id; + if (!voice.initialized) return NotInitialized; + if (!portId || !param) return InvalidArgument; + *portId = 255; + std::uint16_t frameSize = 512; + std::uint16_t frameDuration = 16; + const auto type = param->port_type; + if (type < PortInDevice || type > PortOutDevice) return InvalidArgument; + if (type == PortInPcm || type == PortOutPcm) { + if (param->pcmaudio.sample_rate != 16000 || param->pcmaudio.data_type != 0) return InvalidArgument; + } else if (type == PortInVoice || type == PortOutVoice) { + switch (param->voice.bitrate) { + case 3850: frameSize = 20; frameDuration = 40; break; + case 4650: frameSize = 24; frameDuration = 40; break; + case 5700: frameSize = 15; frameDuration = 20; break; + case 7300: frameSize = 19; frameDuration = 20; break; + case 11800: frameSize = 15; frameDuration = 10; break; + case 12200: frameSize = 16; frameDuration = 10; break; + case 14400: frameSize = 27; frameDuration = 15; break; + case 16000: frameSize = 20; frameDuration = 10; break; + case 22533: frameSize = 43; frameDuration = 15; break; + default: return InvalidArgument; + } + } + constexpr auto resourceFull = static_cast<std::int32_t>(0x804e0808u); + if (voice.freePorts.empty()) return resourceFull; + std::uint32_t count = 0; + std::uint32_t pcmCount = 0; + std::uint32_t pcmBytes = 0; + for (const auto& [id, port] : voice.ports) { + if (port.type == type) ++count; + if (port.type == PortInPcm || port.type == PortOutPcm) { + ++pcmCount; + pcmBytes += 2u * port.bufferSize; + } + } + std::uint16_t bufferSize = 2048; + if (type == PortInDevice || type == PortOutDevice) { + const std::uint32_t limit = type == PortOutDevice && voice.appType == 4 ? 5 : 4; + if (count == limit) return resourceFull; + if (type == PortInDevice ? param->device.type != 0 : param->device.type != 2 && param->device.type != 14) return InvalidArgument; + bufferSize = 4096; + } else if (type == PortInPcm || type == PortOutPcm) { + bufferSize = static_cast<std::uint16_t>(param->pcmaudio.buffer_size - param->pcmaudio.buffer_size % frameSize); + if (bufferSize < 1024 || bufferSize > 64512) return InvalidArgument; + if (type == PortInPcm && bufferSize / frameSize * frameDuration < param->threshold) return resourceFull; + const std::uint32_t limit = voice.appType == 4 ? 128 : voice.appType == 0x08000000 ? 30 : 14; + const std::uint32_t byteLimit = voice.appType == 4 || voice.appType == 0x08000000 ? 0x80000 : voice.appType == 0x10000000 ? 0x5000 : 0x10000; + if (pcmCount == limit || pcmBytes + 2u * bufferSize > byteLimit) return resourceFull; + } else { + if (type == PortInVoice && bufferSize / frameSize * frameDuration < param->threshold) return resourceFull; + const std::uint32_t limit = voice.appType == 4 ? 0 : type == PortOutVoice ? 4 : voice.appType == 0x20000000 ? 7 : 15; + if (count == limit) return resourceFull; + } + const auto id = voice.freePorts.front(); + const bool voicePort = type == PortInVoice || type == PortOutVoice; + voice.ports.emplace(id, Port{type, param->volume, voicePort ? static_cast<std::uint32_t>(param->voice.bitrate) : 0u, bufferSize, frameSize, param->mute != 0}); + voice.freePorts.pop_front(); + *portId = id; return 0; } @@ -142,6 +197,7 @@ int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { RequirePort(voice, port_id, __func__); std::erase_if(voice.connections, [port_id](const auto& link) { return link.first == port_id || link.second == port_id; }); voice.ports.erase(port_id); + voice.freePorts.push_back(port_id); return 0; } diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 7bf4ab9a1c..3cd5219949 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -73,6 +73,11 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { param.port_type = type; param.volume = volume; if (type == 2 || type == 4) param.voice.bitrate = 16000; + if (type == 1 || type == 3) { + param.pcmaudio.buffer_size = 1024; + param.pcmaudio.sample_rate = 16000; + } + if (type == 5) param.device.type = 2; std::uint32_t id = 0; Require(sceVoiceCreatePort(&id, ¶m) == 0); return id; @@ -234,4 +239,119 @@ int main() { Require(sceVoiceEnd_nid_postfix() == 0); } + constexpr auto resourceFull = static_cast<std::int32_t>(0x804e0808u); + std::uint32_t portId = 123; + VoicePortParam param{}; + Require(sceVoiceCreatePort(nullptr, nullptr) == notInitialized); + Require(sceVoiceCreatePort(&portId, ¶m) == notInitialized && portId == 123); + for (const auto mode : {4, 0x08000000, 0x10000000, 0x20000000}) { + init.app_type = mode; + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceCreatePort(&portId, nullptr) == invalidArgument && portId == 123); + Require(sceVoiceCreatePort(nullptr, ¶m) == invalidArgument); + for (const auto type : {-1, 6, INT32_MAX}) { + param = {}; + param.port_type = type; + Require(sceVoiceCreatePort(&portId, ¶m) == invalidArgument && portId == 255); + } + for (const auto type : {0, 2, 4, 5}) { + param = {}; + param.port_type = type; + if (type == 2 || type == 4) param.voice.bitrate = 16000; + if (type == 5) param.device.type = 14; + const auto limit = type == 0 ? 4 : type == 5 ? (mode == 4 ? 5 : 4) : mode == 4 ? 0 : type == 4 ? 4 : mode == 0x20000000 ? 7 : 15; + std::vector<std::uint32_t> ports; + for (int i = 0; i < limit; ++i) { + Require(sceVoiceCreatePort(&portId, ¶m) == 0 && portId < 128); + ports.push_back(portId); + } + Require(sceVoiceCreatePort(&portId, ¶m) == resourceFull && portId == 255); + if (type == 0 || type == 5) { + param.device.type = 1; + Require(sceVoiceCreatePort(&portId, ¶m) == resourceFull); + param.device.type = type == 0 ? 0 : 2; + } + for (auto id : ports) Require(sceVoiceDeletePort(id) == 0); + if (limit != 0) { + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + Require(sceVoiceDeletePort(portId) == 0); + } + } + param = {}; + param.port_type = 3; + param.pcmaudio.sample_rate = 16000; + param.pcmaudio.buffer_size = 1024; + const auto limit = mode == 4 ? 128 : mode == 0x08000000 ? 30 : mode == 0x10000000 ? 10 : 14; + std::vector<std::uint32_t> ports; + for (int i = 0; i < limit; ++i) { + Require(sceVoiceCreatePort(&portId, ¶m) == 0 && portId < 128); + for (auto id : ports) Require(id != portId); + ports.push_back(portId); + } + Require(sceVoiceCreatePort(&portId, ¶m) == resourceFull && portId == 255); + const auto released = ports.front(); + Require(sceVoiceDeletePort(released) == 0); + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + if (mode == 4) Require(portId == released); + Require(sceVoiceEnd_nid_postfix() == 0); + portId = 123; + } + init.app_type = 0x08000000; + Require(sceVoiceInit(&init, 100) == 0); + for (const auto type : {0, 5}) { + param = {}; + param.port_type = type; + for (const auto deviceType : {-1, 0, 1, 2, 3, 13, 14, 15, INT32_MAX}) { + param.device.type = deviceType; + const bool valid = type == 0 ? deviceType == 0 : deviceType == 2 || deviceType == 14; + Require(sceVoiceCreatePort(&portId, ¶m) == (valid ? 0 : invalidArgument)); + if (valid) Require(sceVoiceDeletePort(portId) == 0); + else Require(portId == 255); + } + } + for (const auto type : {1, 3}) { + param = {}; + param.port_type = type; + param.pcmaudio.buffer_size = 1024; + Require(sceVoiceCreatePort(&portId, ¶m) == invalidArgument); + param.pcmaudio.sample_rate = 16000; + param.pcmaudio.data_type = 1; + Require(sceVoiceCreatePort(&portId, ¶m) == invalidArgument); + param.pcmaudio.data_type = 0; + for (const std::uint32_t bytes : {0u, 511u, 512u, 1023u, 1024u, 1535u, 64512u, 65023u, 65024u, 65535u, 65536u, 66560u, UINT32_MAX}) { + param.pcmaudio.buffer_size = bytes; + const auto rounded = static_cast<std::uint16_t>(bytes - bytes % 512); + const bool valid = rounded >= 1024 && rounded <= 64512; + Require(sceVoiceCreatePort(&portId, ¶m) == (valid ? 0 : invalidArgument)); + if (valid) Require(sceVoiceDeletePort(portId) == 0); + } + param.pcmaudio.buffer_size = 1024; + param.threshold = 33; + Require(sceVoiceCreatePort(&portId, ¶m) == (type == 1 ? resourceFull : 0)); + if (type == 3) Require(sceVoiceDeletePort(portId) == 0); + param.threshold = 32; + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + Require(sceVoiceDeletePort(portId) == 0); + } + for (const auto type : {2, 4}) { + param = {}; + param.port_type = type; + for (const auto bitrate : {-1, 0, 3849, 3850, 4650, 5700, 7300, 11800, 12200, 14400, 16000, 16001, 22533, 22534}) { + param.voice.bitrate = bitrate; + const bool valid = bitrate == 3850 || bitrate == 4650 || bitrate == 5700 || bitrate == 7300 || bitrate == 11800 || bitrate == 12200 || bitrate == 14400 || bitrate == 16000 || bitrate == 22533; + Require(sceVoiceCreatePort(&portId, ¶m) == (valid ? 0 : invalidArgument)); + if (valid) Require(sceVoiceDeletePort(portId) == 0); + } + param.voice.bitrate = 16000; + param.threshold = 1021; + Require(sceVoiceCreatePort(&portId, ¶m) == (type == 2 ? resourceFull : 0)); + if (type == 4) Require(sceVoiceDeletePort(portId) == 0); + param.threshold = 1020; + param.mute = 1; + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + Require(sceVoiceGetMuteFlag(portId, &muted) == 0 && muted); + Require(sceVoiceDeletePort(portId) == 0); + } + Require(sceVoiceEnd_nid_postfix() == 0); + } diff --git a/core/libs/tests/GuestVoicePortAttr.cpp b/core/libs/tests/GuestVoicePortAttr.cpp index e7d05b55d9..ceaa036b65 100644 --- a/core/libs/tests/GuestVoicePortAttr.cpp +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -43,7 +43,14 @@ int main() { Require(sceVoiceGetPortAttr(128, 1001, nullptr, 1) == invalidPort); Require(sceVoiceGetPortAttr(0, 1001, nullptr, 1) == invalidArgument); for (std::int32_t type = 0; type <= 5; ++type) { + param = {}; param.port_type = type; + if (type == 1 || type == 3) { + param.pcmaudio.buffer_size = 1024; + param.pcmaudio.sample_rate = 16000; + } + if (type == 2 || type == 4) param.voice.bitrate = 16000; + if (type == 5) param.device.type = 2; std::uint32_t port = 0; Require(sceVoiceCreatePort(&port, ¶m) == 0); for (const auto attr : {1000, 1001, 1002, 1003, 2000, 2001, 2002}) { diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 9cea739a9c..32ccc598d9 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -2,7 +2,7 @@ ### Functional -- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization validates native modes and version 100, but still lacks event callback delivery and backend creation; port creation does not enforce native resource limits. +- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization validates native modes and version 100, but still lacks event callback delivery and backend creation. Port creation enforces recovered format and resource limits, but does not allocate device or codec backends. - [sceVoiceEnableChat and sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp): zero-argument signatures and the not-initialized error are confirmed. Initialized calls throw because the required audio chat backend is not implemented; they no longer report success for a local flag change. ### Unknown function info From bfbaf72142a68fb2037d8aebff05c97721f44b08 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:35:15 +0300 Subject: [PATCH 0854/1360] fix(voice): restore port parameter errors and volume normalization --- core/libs/prx/libSceVoice/Export.cpp | 53 ++++++++++++++++++++-------- core/libs/tests/GuestVoice.cpp | 46 +++++++++++++++++++++++- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index fc13a6b73a..d4f1d3f8a5 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,6 +1,7 @@ #include <cstdint> #include <cstddef> #include <cstring> +#include <cmath> #include <deque> #include <map> #include <mutex> @@ -194,7 +195,9 @@ int APS5_VABI sceVoiceCreatePort(std::uint32_t* portId, const VoicePortParam* pa int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); + if (!voice.initialized) return NotInitialized; + if (port_id > 127) return InvalidPort; + if (!voice.ports.contains(port_id)) return PortNotFound; std::erase_if(voice.connections, [port_id](const auto& link) { return link.first == port_id || link.second == port_id; }); voice.ports.erase(port_id); voice.freePorts.push_back(port_id); @@ -219,28 +222,40 @@ int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t return 0; } -int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { - if (bitrate == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceGetBitRate(std::uint32_t portId, std::uint32_t* bitrate) { auto& voice = State(); std::lock_guard lock(voice.mutex); - const auto& port = RequirePort(voice, port_id, __func__); - if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!bitrate) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + const auto& port = found->second; + if (port.type != PortInVoice && port.type != PortOutVoice) return PortNotFound; *bitrate = port.bitrate; return 0; } -int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { +int APS5_VABI sceVoiceSetVolume(std::uint32_t portId, float volume) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__).volume = volume; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.volume = std::isfinite(volume) ? volume : 0.0f; return 0; } -int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { - if (volume == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceGetVolume(std::uint32_t portId, float* volume) { auto& voice = State(); std::lock_guard lock(voice.mutex); - *volume = RequirePort(voice, port_id, __func__).volume; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!volume) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + *volume = found->second.volume; return 0; } @@ -307,18 +322,26 @@ int APS5_VABI sceVoiceWriteToIPort(uint32_t input_port_id, const void* data, uin return 0; } -int APS5_VABI sceVoiceSetMuteFlag(uint32_t port_id, bool muted) { +int APS5_VABI sceVoiceSetMuteFlag(std::uint32_t portId, std::uint8_t muted) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__).muted = muted; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.muted = muted != 0; return 0; } -int APS5_VABI sceVoiceGetMuteFlag(uint32_t port_id, bool* muted) { - if (muted == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t portId, bool* muted) { auto& voice = State(); std::lock_guard lock(voice.mutex); - *muted = RequirePort(voice, port_id, __func__).muted; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!muted) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + *muted = found->second.muted; return 0; } diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 3cd5219949..27e9cb31e2 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -4,6 +4,8 @@ #include <cstdlib> #include <cstring> #include <initializer_list> +#include <cmath> +#include <limits> #include <stdexcept> #include <vector> @@ -18,7 +20,9 @@ int APS5_VABI sceVoiceDeletePort(std::uint32_t); int APS5_VABI sceVoiceConnectIPortToOPort(std::uint32_t, std::uint32_t); int APS5_VABI sceVoiceDisconnectIPortFromOPort(std::uint32_t, std::uint32_t); int APS5_VABI sceVoiceGetVolume(std::uint32_t, float*); -int APS5_VABI sceVoiceSetMuteFlag(std::uint32_t, bool); +int APS5_VABI sceVoiceSetMuteFlag(std::uint32_t, std::uint8_t); +int APS5_VABI sceVoiceSetVolume(std::uint32_t, float); +int APS5_VABI sceVoiceGetBitRate(std::uint32_t, std::uint32_t*); int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t, bool*); int APS5_VABI sceVoiceGetPortInfo(std::uint32_t, VoicePortInfo*); int APS5_VABI sceVoiceReadFromOPort(std::uint32_t, void*, std::uint32_t*); @@ -91,6 +95,12 @@ int main() { constexpr auto invalidPort = static_cast<std::int32_t>(0x804e0804u); constexpr auto invalidArgument = static_cast<std::int32_t>(0x804e0805u); constexpr auto portNotFound = static_cast<std::int32_t>(0x804e0807u); + Require(sceVoiceGetVolume(128, nullptr) == notInitialized); + Require(sceVoiceGetBitRate(128, nullptr) == notInitialized); + Require(sceVoiceGetMuteFlag(128, nullptr) == notInitialized); + Require(sceVoiceSetVolume(128, 1.0f) == notInitialized); + Require(sceVoiceSetMuteFlag(128, 1) == notInitialized); + Require(sceVoiceDeletePort(128) == notInitialized); Require(sceVoiceResetPort(0) == notInitialized); Require(sceVoicePausePort(0) == notInitialized); Require(sceVoiceResumePort(0) == notInitialized); @@ -352,6 +362,40 @@ int main() { Require(sceVoiceGetMuteFlag(portId, &muted) == 0 && muted); Require(sceVoiceDeletePort(portId) == 0); } + Require(sceVoiceGetVolume(128, nullptr) == invalidPort); + Require(sceVoiceGetBitRate(128, nullptr) == invalidPort); + Require(sceVoiceGetMuteFlag(128, nullptr) == invalidPort); + Require(sceVoiceSetVolume(128, 1.0f) == invalidPort); + Require(sceVoiceSetMuteFlag(128, 1) == invalidPort); + Require(sceVoiceDeletePort(128) == invalidPort); + Require(sceVoiceGetVolume(127, nullptr) == invalidArgument); + Require(sceVoiceGetBitRate(127, nullptr) == invalidArgument); + Require(sceVoiceGetMuteFlag(127, nullptr) == invalidArgument); + volume = 123.0f; + std::uint32_t bitrate = 123; + muted = true; + Require(sceVoiceGetVolume(127, &volume) == portNotFound && volume == 123.0f); + Require(sceVoiceGetBitRate(127, &bitrate) == portNotFound && bitrate == 123); + Require(sceVoiceGetMuteFlag(127, &muted) == portNotFound && muted); + Require(sceVoiceSetVolume(127, 1.0f) == portNotFound); + Require(sceVoiceSetMuteFlag(127, 1) == portNotFound); + Require(sceVoiceDeletePort(127) == portNotFound); + for (const auto type : {0, 1, 2, 3, 4, 5}) { + const auto id = CreatePort(type, -2.0f); + Require(sceVoiceGetBitRate(id, &bitrate) == (type == 2 || type == 4 ? 0 : portNotFound)); + if (type == 2 || type == 4) Require(bitrate == 16000); + for (const float value : {-2.0f, -0.0f, 0.25f, 2.0f, std::numeric_limits<float>::max(), std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity(), std::numeric_limits<float>::quiet_NaN()}) { + Require(sceVoiceSetVolume(id, value) == 0); + Require(sceVoiceGetVolume(id, &volume) == 0); + Require(volume == (std::isfinite(value) ? value : 0.0f)); + if (value == 0.0f) Require(std::signbit(volume) == std::signbit(value)); + } + for (const std::uint8_t value : {0, 1, 2, 128, 255}) { + Require(sceVoiceSetMuteFlag(id, value) == 0); + Require(sceVoiceGetMuteFlag(id, &muted) == 0 && muted == (value != 0)); + } + Require(sceVoiceDeletePort(id) == 0); + } Require(sceVoiceEnd_nid_postfix() == 0); } From 89567a597f750913ac006fe921003627cb57bf40 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:36:58 +0300 Subject: [PATCH 0855/1360] fix(hmd2): define gaze object selection abi layouts --- core/libs/prx/libSceHmd2/Export.cpp | 58 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index bcc53524e0..d2458b815c 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -89,8 +89,62 @@ struct GazeUserCalibrationStatus { float point[3]; }; struct GazeRawData; -struct GazeObjectSelectionParameter; -struct GazeObjectSelectionResult; +struct GazeObjectSelectionTransform { + float values[16]; +}; + +struct GazeObjectSelectionObject { + std::uint64_t id; + std::uint32_t enabled; + std::uint32_t reserved12; + GazeObjectSelectionTransform transform; + const GazeObjectSelectionTransform* boundingBoxes; + std::uint32_t boundingBoxCount; + std::uint32_t reserved92; +}; + +struct GazeObjectSelectionParameter { + std::uint32_t size; + std::uint32_t reserved4; + std::uint64_t reserved8; + std::uint64_t gazeTimestamp; + GazeObjectSelectionTransform transform; + std::uint32_t selectionMode; + std::uint32_t filterMode; + std::uint64_t timestamp; + const GazeObjectSelectionObject* objects; + std::uint32_t objectCount; + std::uint32_t reserved116; +}; + +struct GazeObjectSelectionResult { + std::uint32_t size; + std::uint32_t selected; + std::uint64_t objectId; + std::uint64_t gazeTimestamp; + std::uint32_t gazeValid; + float origin[3]; + float direction[3]; + float distance; +}; + +static_assert(sizeof(GazeObjectSelectionTransform) == 64); +static_assert(sizeof(GazeObjectSelectionObject) == 96); +static_assert(offsetof(GazeObjectSelectionObject, transform) == 16); +static_assert(offsetof(GazeObjectSelectionObject, boundingBoxes) == 80); +static_assert(offsetof(GazeObjectSelectionObject, boundingBoxCount) == 88); +static_assert(sizeof(GazeObjectSelectionParameter) == 120); +static_assert(offsetof(GazeObjectSelectionParameter, transform) == 24); +static_assert(offsetof(GazeObjectSelectionParameter, selectionMode) == 88); +static_assert(offsetof(GazeObjectSelectionParameter, timestamp) == 96); +static_assert(offsetof(GazeObjectSelectionParameter, objects) == 104); +static_assert(offsetof(GazeObjectSelectionParameter, objectCount) == 112); +static_assert(sizeof(GazeObjectSelectionResult) == 56); +static_assert(offsetof(GazeObjectSelectionResult, objectId) == 8); +static_assert(offsetof(GazeObjectSelectionResult, gazeTimestamp) == 16); +static_assert(offsetof(GazeObjectSelectionResult, origin) == 28); +static_assert(offsetof(GazeObjectSelectionResult, direction) == 40); +static_assert(offsetof(GazeObjectSelectionResult, distance) == 52); struct GazeObjectSelectionInitializeParameter { std::uint32_t size; std::uint32_t maxObjects; From d47b4d729d031f29708c8feec9850ca7e20a32e5 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:39:20 +0300 Subject: [PATCH 0856/1360] fix(hmd2): define reprojection initialization and output layouts --- core/libs/prx/libSceHmd2/Export.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index d2458b815c..a0dd351bdf 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -14,8 +14,31 @@ namespace Hmd2 { struct ReprojectionStatus; -struct ReprojectionOutput; -struct ReprojectionInitializeParameter; +struct ReprojectionOutput { + std::uint32_t textureDescriptor[8]; +}; + +struct ReprojectionInitializeParameter { + void* workMemory; + void* displayMemory; + std::int32_t threadPriority; + std::uint32_t threadAffinity; + std::uint32_t queueParameter24; + std::uint32_t queueParameter28; + std::uint32_t renderingMode; + std::uint32_t reserved36; + void* auxiliaryMemory; + std::uint32_t timingInterval; + std::uint32_t reserved52[5]; +}; + +static_assert(sizeof(ReprojectionOutput) == 32); +static_assert(sizeof(ReprojectionInitializeParameter) == 72); +static_assert(offsetof(ReprojectionInitializeParameter, threadPriority) == 16); +static_assert(offsetof(ReprojectionInitializeParameter, threadAffinity) == 20); +static_assert(offsetof(ReprojectionInitializeParameter, renderingMode) == 32); +static_assert(offsetof(ReprojectionInitializeParameter, auxiliaryMemory) == 40); +static_assert(offsetof(ReprojectionInitializeParameter, timingInterval) == 48); struct ReprojectionLayer; struct ReprojectionBufferedLayer; From 870376c65faef22c37a3eadd8702ce21d5681f93 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:48:29 +0300 Subject: [PATCH 0857/1360] fix(abi): apply guest calling convention to system callbacks --- core/libs/SceTypes.hpp | 14 +++++++------- core/libs/prx/libSceRudp/Export.cpp | 2 +- core/libs/tests/GuestHttp.cpp | 2 +- core/libs/tests/GuestImeOpen.cpp | 4 ++++ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 8b3eaa3f89..4a61c38ed7 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1260,8 +1260,8 @@ struct ImeKeycode { std::uint64_t timestamp; }; -using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length); -using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved); +using ImeTextFilter = std::int32_t (APS5_VABI*)(char16_t* outText, std::uint32_t* outTextLength, const char16_t* sourceText, std::uint32_t sourceTextLength); +using ImeExtKeyboardFilter = int (APS5_VABI*)(const ImeKeycode* sourceKeycode, std::uint16_t* outKeycode, std::uint32_t* outStatus, void* reserved); struct ImeExtendedParam { std::uint32_t option; @@ -1443,10 +1443,10 @@ using NetCtlCallback = void (APS5_VABI *)(int, void*); struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; -using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*); -using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*); -using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*); -using HttpAuthInfoCallback = int (*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*); +using HttpsCallback = int (APS5_VABI*)(int, unsigned int, void* const*, int, void*); +using HttpRedirectCallback = int (APS5_VABI*)(int, std::int32_t, std::int32_t*, const char*, void*); +using HttpCookieRecvCallback = int (APS5_VABI*)(int, const char*, const char*, std::uint64_t, void*); +using HttpAuthInfoCallback = int (APS5_VABI*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*); struct HttpNBEvent { std::uint8_t opaque[64]; }; @@ -2068,7 +2068,7 @@ static_assert(sizeof(RudpPollEvent) == 8); static_assert(offsetof(RudpPollEvent, events) == 4); static_assert(offsetof(RudpPollEvent, returnedEvents) == 6); -using RudpEventHandler = void (*)(int ctx_id, int event_id, int error_code, void* arg); +using RudpEventHandler = void (APS5_VABI*)(int ctxId, int eventId, int errorCode, void* arg); struct SystemServiceStatus { std::int32_t event_num = 0; diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 68afe5a022..dcfb2c25be 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -58,7 +58,7 @@ enum RudpOption { RUDP_OPTION_KEEP_ALIVE_TIMEOUT = 19, }; -using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +using GuestRudpEventHandler = RudpEventHandler; std::mutex g_mutex; bool g_inited = false; diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 2d0060ef46..b70b78bba2 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -96,7 +96,7 @@ static void CheckUriEscapeBytes() { } } -static int AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } +static int APS5_VABI AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } static void APS5_VABI RequestStatusCallback(int, int, void* argument) { ++*static_cast<int*>(argument); } diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index ce516d5530..39c7278c1e 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -69,6 +69,10 @@ void APS5_VABI Record(void* arg, const ImeEvent* event) { if (g_closeOnPressClose && event->id == EventPressClose) g_reentrantClose = sceImeClose_nid_postfix(); } +std::int32_t APS5_VABI TextFilter(char16_t*, std::uint32_t*, const char16_t*, std::uint32_t) { + std::abort(); +} + void APS5_VABI Other(void*, const ImeEvent*) { std::abort(); } From 1e6077d1faf1dd294f0991d69096e41230752edf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:48:31 +0300 Subject: [PATCH 0858/1360] fix(voice): restore port info frame sizes and output bounds --- core/libs/prx/libSceVoice/Export.cpp | 7 ++----- core/libs/tests/GuestVoice.cpp | 8 ++++++++ docs/dev/debt/libSceVoice.md | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index d4f1d3f8a5..707d112ea4 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -21,10 +21,7 @@ constexpr std::int32_t PortOutPcm = 3; constexpr std::int32_t PortOutVoice = 4; constexpr std::int32_t PortOutDevice = 5; constexpr std::int32_t PortStateReady = 1; -constexpr std::uint32_t VoiceFrameMs = 20; -constexpr std::int32_t PortAttrVoiceActivity = 1001; constexpr int VoiceErrorNotInitialized = static_cast<int>(0x804E0801u); -constexpr int VoiceErrorAlreadyInitialized = static_cast<int>(0x804E0802u); constexpr auto NotInitialized = static_cast<std::int32_t>(0x804e0801u); constexpr auto InvalidPort = static_cast<std::int32_t>(0x804e0804u); constexpr auto InvalidArgument = static_cast<std::int32_t>(0x804e0805u); @@ -265,10 +262,10 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { if (!voice.initialized) return VoiceErrorNotInitialized; if (info == nullptr) APS5_INVALID_ARG_EX; const auto& port = RequirePort(voice, port_id, __func__); - *info = VoicePortInfo{}; info->port_type = port.type; info->state = PortStateReady; - if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u; + info->byte_count = 0; + info->frame_size = port.bitrate != 0 ? port.frameSize : 2; return 0; } diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 27e9cb31e2..cb12940acb 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -172,8 +172,16 @@ int main() { const auto output = CreatePort(3, 0.25f); Require(sceVoiceConnectIPortToOPort(input, output) == 0); Require(sceVoiceSetMuteFlag(output, true) == 0); + std::uint32_t portIdForInfo = 0; VoicePortInfo before{}; + before.edge = &portIdForInfo; + before.edge_count = 0x1234; + before.reserved = 0x5678; Require(sceVoiceGetPortInfo(input, &before) == 0); + Require(before.frame_size == 20 && before.byte_count == 0); + Require(before.edge == &portIdForInfo && before.edge_count == 0x1234 && before.reserved == 0x5678); + VoicePortInfo pcmInfo{}; + Require(sceVoiceGetPortInfo(output, &pcmInfo) == 0 && pcmInfo.frame_size == 2); Require(sceVoiceResetPort(input) == 0); Require(sceVoiceResetPort(output) == 0); diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 32ccc598d9..9e8d8fc46e 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -2,7 +2,7 @@ ### Functional -- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization validates native modes and version 100, but still lacks event callback delivery and backend creation. Port creation enforces recovered format and resource limits, but does not allocate device or codec backends. +- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. GetPortInfo preserves unused output fields and reports native frame sizes, but still reports a constant ready state and does not reproduce stopped-state or snapshot-slot errors. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization validates native modes and version 100, but still lacks event callback delivery and backend creation. Port creation enforces recovered format and resource limits, but does not allocate device or codec backends. - [sceVoiceEnableChat and sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp): zero-argument signatures and the not-initialized error are confirmed. Initialized calls throw because the required audio chat backend is not implemented; they no longer report success for a local flag change. ### Unknown function info From e7a1e5594a319318da016e2bc150796da314117d Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:48:31 +0300 Subject: [PATCH 0859/1360] fix(ssl): restore ca query errors and sdk unload boundary --- core/libs/prx/libSceSsl/Export.cpp | 23 +++++++++++++++-------- core/libs/tests/GuestSsl.cpp | 30 ++++++++++++++++++++++++------ docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index a1bab7e2af..923f584821 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -6,6 +6,7 @@ #endif #include "SslTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" @@ -701,17 +702,26 @@ int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, int APS5_VABI sceSslUnloadCert(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) { + const auto* parameters = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, parameters + 16, sizeof(sdkVersion)); + return sdkVersion < 0x07500000 ? 0 : ErrorInvalidId; + } + auto& context = *found->second; context.authorities.clear(); context.identity.reset(); return 0; } int APS5_VABI sceSslGetCaList(int sslCtxId, SceSsl::CaList* output) { - if (!output) return ErrorInvalidArg; + if (!output) return ErrorInvalidValue; auto& state = State(); std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) return ErrorInvalidId; + auto& context = *found->second; PoolScope scope(context.pool); const auto count = CertificateCount(context); if (!count) { @@ -743,14 +753,11 @@ int APS5_VABI sceSslFreeCaList(int sslCtxId, SceSsl::CaList* list) { } int APS5_VABI sceSslGetCaCerts(int sslCtxId, SceSsl::CaCerts* output) { - if (!output) return ErrorInvalidArg; auto& state = State(); std::lock_guard lock(state.mutex); const auto found = state.contexts.find(sslCtxId); - if (found == state.contexts.end()) { - *output = {}; - return ErrorNotFound; - } + if (found == state.contexts.end()) return ErrorInvalidId; + if (!output) return ErrorInvalidValue; auto& context = *found->second; PoolScope scope(context.pool); const auto count = CertificateCount(context); diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 12c079d7eb..4aa00482b6 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -1,4 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" +#include <array> +#include <cstring> #include <cstddef> #include <cstdint> #include <cstdlib> @@ -62,6 +65,13 @@ struct SslCaList { }; int main() { + static std::array<std::byte, 64> parameters{}; + const std::uint64_t processSize = parameters.size(); + const std::uint32_t processMagic = 0x4942524f; + std::memcpy(parameters.data(), &processSize, sizeof(processSize)); + std::memcpy(parameters.data() + 8, &processMagic, sizeof(processMagic)); + Require(ProcessParametersFixture::Install(parameters.data(), parameters.size())); + constexpr int invalidValue = static_cast<int>(0x8095F007); constexpr int notFound = static_cast<int>(0x8095F004); constexpr int invalidArg = static_cast<int>(0x8095177A); int marker = 0; @@ -70,7 +80,7 @@ int main() { SslMemoryPoolStats stats{1, 1, 1, 1}; Require(sceSslGetMemoryPoolStats(context, &stats) == 0); Require(stats.pool_size == 0x10000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); - Require(sceSslGetCaCerts(context, nullptr) == invalidArg); + Require(sceSslGetCaCerts(context, nullptr) == invalidValue); Require(sceSslFreeCaCerts(context, nullptr) == invalidArg); SslCaCerts certs{&marker, 3, &marker}; @@ -81,7 +91,7 @@ int main() { Require(sceSslFreeCaCerts(context, &certs) == 0); Require(certs.certs == nullptr && certs.num == 0 && certs.pool == nullptr); - Require(sceSslGetCaList(context, nullptr) == invalidArg); + Require(sceSslGetCaList(context, nullptr) == invalidValue); Require(sceSslFreeCaList(context, nullptr) == invalidArg); void* handle = ▮ @@ -103,7 +113,6 @@ int main() { Require(list.certs == nullptr && list.num == 0); constexpr int invalidId = static_cast<int>(0x8095F006); - constexpr int invalidValue = static_cast<int>(0x8095F007); constexpr int badLength = static_cast<int>(0x80951773); constexpr int badDescriptor = static_cast<int>(0x80410109); Require(sceSslInit_nid_postfix(0) == static_cast<int>(0x80020016)); @@ -172,10 +181,19 @@ int main() { Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); Require(sceSslDeleteConnection(connection) == invalidId); - Require(sceSslGetCaList(context, nullptr) == invalidArg); + Require(sceSslGetCaList(context, nullptr) == invalidValue); Require(sceSslFreeCaList(context, nullptr) == invalidArg); list = {&handle, 3}; - Require(Throws([&] { sceSslGetCaList(context, &list); })); + Require(sceSslGetCaList(context, &list) == invalidId); + Require(list.certs == &handle && list.num == 3); + Require(sceSslGetCaCerts(context, nullptr) == invalidId); + certs = {&marker, 3, &marker}; + Require(sceSslGetCaCerts(context, &certs) == invalidId); + Require(certs.certs == &marker && certs.num == 3 && certs.pool == &marker); Require(Throws([&] { sceSslFreeCaList(context, &list); })); - Require(Throws([&] { sceSslUnloadCert(context); })); + for (const std::uint32_t sdkVersion : {0u, 0x074fffffu, 0x07500000u, 0x09000000u}) { + std::memcpy(parameters.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceSslUnloadCert(context) == (sdkVersion < 0x07500000 ? 0 : invalidId)); + Require(sceSslUnloadCert(0) == (sdkVersion < 0x07500000 ? 0 : invalidId)); + } } diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 877af46e33..736ec71de5 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -9,6 +9,6 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. -- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. From 682165a9290b2d344fcaf5ab09a703ea69036633 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:55:54 +0300 Subject: [PATCH 0860/1360] fix(agc): restore occlusion query size argument --- core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp index b3ae56e199..2681834e0b 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp @@ -21,7 +21,7 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c return Agc::Command::Emit(buf, 0x10u, {bufferSizeInBytes, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), control}, __func__); } -std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize() { +std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) { NotImplemented_nid_no_patch(__func__); return 0; } From 00bb8aa40911bf7546f8be6cf17fca0546795740 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:49:22 +0200 Subject: [PATCH 0861/1360] fix(libkernel): fail lseek with EOVERFLOW when the new offset exceeds the 64-bit range FreeBSD 11.4's vn_seek fails SEEK_CUR and SEEK_END with EOVERFLOW when the base offset plus a positive offset does not fit in an off_t, and with EINVAL only when the result is negative. The host lseek reports EINVAL for the overflow, so the guest saw EINVAL. When the host call fails with EINVAL for a positive offset on SEEK_CUR or SEEK_END, the base position is read back and the call fails with EOVERFLOW if the sum would overflow; the file position is left unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/File/src/Open.cpp | 18 +++++++++++++++++- core/libs/tests/GuestFilesystem.cpp | 8 ++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0cd332ef01..04b914b2cf 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -352,6 +352,18 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes return static_cast<std::int64_t>(n); } +static bool SeekOverflows(int d, std::int64_t offset, int whence) { + const std::int64_t current = NativeLseek(d, 0, 1); + if (current < 0) return false; + std::int64_t base = current; + if (whence == 2) { + base = NativeLseek(d, 0, 2); + NativeLseek(d, current, 0); + if (base < 0) return false; + } + return base > std::numeric_limits<std::int64_t>::max() - offset; +} + std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (GuestSockets::IsOpen(d)) return SCE_KERNEL_ERROR_ESPIPE; if (whence < 0 || whence > 2) { @@ -365,7 +377,11 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { } #endif std::int64_t result = NativeLseek(d, offset, whence); - if (result < 0) return SceIoErrorFromErrno(errno); + if (result < 0) { + const int error = errno; + if (error == EINVAL && offset > 0 && (whence == 1 || whence == 2) && SeekOverflows(d, offset, whence)) return SceIoErrorFromErrno(EOVERFLOW); + return SceIoErrorFromErrno(error); + } return result; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 4d7f42b692..4b473fb6a8 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1109,6 +1109,14 @@ int main() { Require(pread_nid_postfix(locked, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(close_nid_postfix(transferPipe[0]) == 0 && close_nid_postfix(transferPipe[1]) == 0); } + Require(lseek_nid_postfix(locked, 4, 0) == 4); + Require(lseek_nid_postfix(locked, INT64_MAX, 1) == -1 && *__error_nid_postfix() == 84); + Require(lseek_nid_postfix(locked, 0, 1) == 4); + Require(lseek_nid_postfix(locked, -1, 0) == -1 && *__error_nid_postfix() == 22); + const std::int64_t lockedSize = lseek_nid_postfix(locked, 0, 2); + Require(lockedSize >= 0); + Require(lseek_nid_postfix(locked, INT64_MAX - lockedSize + 1, 2) == -1 && *__error_nid_postfix() == 84); + Require(lseek_nid_postfix(locked, 0, 1) == lockedSize); Require(flock_nid_postfix(-1, 2 | 4) == -1 && *__error_nid_postfix() == 9); Require(flock_nid_postfix(0x7fffffff, 1) == -1 && *__error_nid_postfix() == 9); int flockPipe[2] = {-1, -1}; From 350c6eb32396e786bddb1e55e4b2a199fd689c12 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:57:02 +0300 Subject: [PATCH 0862/1360] fix(font): restore typography validation and reject unsupported shaping --- .../prx/libSceFont/include/FontInternal.hpp | 1 - core/libs/prx/libSceFont/src/Style.cpp | 56 ++++++++++++++---- core/libs/tests/GuestFont.cpp | 59 +++++++++++++++++-- docs/dev/debt/libSceFont.md | 2 +- 4 files changed, 99 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 8c68010f10..4e057a93e1 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -37,7 +37,6 @@ struct FontState { float scaleW = 16.0f; float scaleH = 16.0f; std::map<int, int> scriptLanguages; - std::map<int, int> typographicFeatures; int attribute = 0; FontState() = default; diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index 22291654ee..01ae245867 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -60,20 +60,38 @@ int AccessFontSettings(FontHandle fontHandle, Access&& access) { }); } -int SetFontSetting(FontHandle fontHandle, std::map<int, int> FontState::*table, int key, int value) { - return AccessFontSettings(fontHandle, [&](FontState& state) { - (state.*table)[key] = value; - return SCE_FONT_OK; - }); +bool HasTypographyRecordFeature(const void* record, int typographic) { + const auto* bytes = static_cast<const std::byte*>(record); + std::uint32_t count; + std::memcpy(&count, bytes + 32, sizeof(count)); + for (std::uint32_t index = 0; index < count; ++index) { + std::int32_t fontId; + std::memcpy(&fontId, bytes + 36 + sizeof(fontId) * index, sizeof(fontId)); + if (count == 1) { + if (typographic == 65538) return fontId == 43 || fontId == 12; + return static_cast<std::uint32_t>(fontId) <= 29 && (0x30000c00u & (1u << fontId)) != 0; + } + if ((std::to_integer<unsigned>(bytes[100 + index]) & 0x80u) != 0 && fontId >= 0) return index == (typographic == 65538 ? 2u : 1u); + } + return false; } -int GetFontSetting(FontHandle fontHandle, std::map<int, int> FontState::*table, int key, int* value) { - if (!value) return SCE_FONT_ERROR_INVALID_PARAMETER; - return AccessFontSettings(fontHandle, [&](FontState& state) { - const auto found = (state.*table).find(key); - *value = found == (state.*table).end() ? 0 : found->second; +int CheckTypography(FontHandleNative* font, int typographic, bool setting) { + const auto* record = font->open_info.fontset_record; + switch (typographic) { + case 1: + case 17: return SCE_FONT_OK; - }); + case 65537: + if (setting) return !record || HasTypographyRecordFeature(record, typographic) ? SCE_FONT_ERROR_INVALID_PARAMETER : SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY; + return !record || HasTypographyRecordFeature(record, typographic) ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY; + case 65538: + return !record || HasTypographyRecordFeature(record, typographic) ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY; + case 65539: + return record ? SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY : SCE_FONT_OK; + default: + return SCE_FONT_ERROR_INVALID_PARAMETER; + } } bool IsScriptLanguageValid(int script, int language) { @@ -161,11 +179,23 @@ int APS5_VABI sceFontGetScriptLanguage(FontHandle fontHandle, int fontScript, in } int APS5_VABI sceFontSetTypographicDesign(FontHandle fontHandle, int typographic, int feature) { - return SetFontSetting(fontHandle, &FontState::typographicFeatures, typographic, feature); + const int result = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { + const int validation = CheckTypography(font, typographic, true); + if (validation != SCE_FONT_OK) return validation; + return feature == 0 || feature == (typographic == 1 ? 1 : 32) ? SCE_FONT_OK : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontSetTypographicDesign: driver typography selection is not implemented"); } int APS5_VABI sceFontGetTypographicDesign(FontHandle fontHandle, int typographic, int* feature) { - return GetFontSetting(fontHandle, &FontState::typographicFeatures, typographic, feature); + const int result = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { + const int validation = CheckTypography(font, typographic, false); + return feature ? validation : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (feature) *feature = 0; + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontGetTypographicDesign: driver typography query is not implemented"); } int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index cd0085e158..1ca28f3228 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,5 +1,6 @@ #include "prx/libSceFont/include/FontDriver.hpp" #include "tests/FontFixture.hpp" +#include <array> #include <cstddef> #include <cstdint> #include <cstdio> @@ -335,10 +336,60 @@ int main() { } Require(setThrew && getThrew && setting == 0); } - Require(sceFontSetTypographicDesign(font, 1, 2) == SCE_FONT_OK); - Require(sceFontGetTypographicDesign(font, 1, &setting) == SCE_FONT_OK && setting == 2); - Require(sceFontGetTypographicDesign(font, 5, &setting) == SCE_FONT_OK && setting == 0); - Require(sceFontGetTypographicDesign(nullptr, 1, &setting) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetTypographicDesign(font, 1, 2) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetTypographicDesign(font, 17, 1) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (const auto selector : {-1, 0, 2, 5, 16, 18, 65536, 65540}) { + Require(sceFontSetTypographicDesign(font, selector, 0) == SCE_FONT_ERROR_INVALID_PARAMETER); + setting = 123; + Require(sceFontGetTypographicDesign(font, selector, &setting) == SCE_FONT_ERROR_INVALID_PARAMETER && setting == 0); + } + Require(sceFontGetTypographicDesign(nullptr, 1, &setting) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && setting == 0); + Require(sceFontGetTypographicDesign(nullptr, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetTypographicDesign(nullptr, 1, 2) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetTypographicDesign(font, 65537, 0) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (const auto selector : {1, 17, 65537, 65538, 65539}) { + Require(sceFontGetTypographicDesign(font, selector, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + bool getThrew = false; + try { + sceFontGetTypographicDesign(font, selector, &setting); + } catch (const std::logic_error&) { + getThrew = true; + } + Require(getThrew && setting == 0); + if (selector == 65537) continue; + for (const auto feature : {0, selector == 1 ? 1 : 32}) { + bool setThrew = false; + try { + sceFontSetTypographicDesign(font, selector, feature); + } catch (const std::logic_error&) { + setThrew = true; + } + Require(setThrew); + } + } + std::array<std::byte, 116> typographyRecord{}; + auto* nativeFont = reinterpret_cast<Font::FontHandleNative*>(font); + nativeFont->open_info.fontset_record = typographyRecord.data(); + for (const auto selector : {65537, 65538, 65539}) { + Require(sceFontSetTypographicDesign(font, selector, 0) == SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY); + setting = 123; + Require(sceFontGetTypographicDesign(font, selector, &setting) == SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY && setting == 0); + Require(sceFontGetTypographicDesign(font, selector, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + } + const std::uint32_t typographyCount = 1; + const std::int32_t typographyFontId = 10; + std::memcpy(typographyRecord.data() + 32, &typographyCount, sizeof(typographyCount)); + std::memcpy(typographyRecord.data() + 36, &typographyFontId, sizeof(typographyFontId)); + Require(sceFontSetTypographicDesign(font, 65537, 0) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetTypographicDesign(font, 65538, &setting) == SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY); + bool typographyQueryThrew = false; + try { + sceFontGetTypographicDesign(font, 65537, &setting); + } catch (const std::logic_error&) { + typographyQueryThrew = true; + } + Require(typographyQueryThrew && setting == 0); + nativeFont->open_info.fontset_record = nullptr; int attribute = -1; Require(sceFontGetAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); Require(sceFontDefineAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index bd2a0098c2..b1b5e56398 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceFontSetTypographicDesign and sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp): signatures are confirmed by the supplied PS5 library. Native feature ranges, defaults and driver-dependent availability are not implemented; the host still accepts arbitrary values and stores them without affecting shaping or rendering. +- [Typographic design](../../../core/libs/prx/libSceFont/src/Style.cpp): native selector, feature, font-set eligibility and output-clearing rules are recovered. Driver typography selection and queries, feature-cache updates and glyph substitution remain unimplemented and throw instead of storing arbitrary selector/value pairs. - [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [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 - [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../../core/libs/prx/libSceFont/src/Open.cpp): signatures, error precedence, failed-output clearing and external-font glyph-code encoding are confirmed by the PS5 library. Multi-face system-font glyph counts, face-selection bits and typographic glyph substitutions are not modeled. From 77835951cd01aa053ed679c5e559d5d9e024eb24 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 13:58:31 +0300 Subject: [PATCH 0863/1360] fix(ssl): defer private key parsing and allow separate identity loads --- core/libs/prx/libSceSsl/Export.cpp | 58 +++++++++++++++++++----------- core/libs/tests/GuestSslLoad.cpp | 24 +++++++++---- docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 923f584821..bfc982dcdc 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -206,9 +206,14 @@ struct Random { } }; +struct EncodedPrivateKey { + Block bytes; + std::size_t size; +}; + struct Identity { std::shared_ptr<CertificateChain> chain; - PrivateKey key; + std::shared_ptr<EncodedPrivateKey> key; }; struct Snapshot { @@ -309,6 +314,7 @@ struct Session { int verifyResult = 0; unsigned int failedOptions = 0; Random random; + PrivateKey privateKey; mbedtls_x509_crt authorities; mbedtls_ssl_config config; mbedtls_ssl_context ssl; @@ -339,8 +345,18 @@ struct Session { mbedtls_ssl_conf_min_tls_version(&config, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_ca_chain(&config, &authorities, nullptr); mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &random.drbg); - if (identity) - CheckBackend(mbedtls_ssl_conf_own_cert(&config, &identity->chain->root, &identity->key.key), "SSL client certificate setup failed"); + if (identity) { + if (!identity->chain || !identity->key) throw std::logic_error("Incomplete SSL client identity is not supported"); + const auto& encoded = *identity->key; + auto bytes = Allocate(Add(encoded.size, 1)); + std::memcpy(bytes.get(), encoded.bytes.get(), encoded.size); + std::size_t first = 0; + while (first < encoded.size && (bytes.get()[first] == ' ' || bytes.get()[first] == '\r' || bytes.get()[first] == '\n' || bytes.get()[first] == '\t')) ++first; + const auto size = first < encoded.size && bytes.get()[first] == '-' ? Add(encoded.size, 1) : encoded.size; + CheckBackend(mbedtls_pk_parse_key(&privateKey.key, bytes.get(), size, nullptr, 0, mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); + CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &privateKey.key, mbedtls_ctr_drbg_random, &random.drbg), "SSL client certificate and private key do not match"); + CheckBackend(mbedtls_ssl_conf_own_cert(&config, &identity->chain->root, &privateKey.key), "SSL client certificate setup failed"); + } for (std::size_t start = 0; start <= alpn.size();) { const auto end = alpn.find(',', start); const auto protocol = alpn.substr(start, end == std::string::npos ? std::string::npos : end - start); @@ -661,15 +677,15 @@ int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* st } int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, SceSsl::Data* cert, SceSsl::Data* privateKey) { - if (caCertNum < 0 || (caCertNum && !caList) || (!!cert != !!privateKey)) - throw std::invalid_argument("Invalid SSL certificate load arguments"); auto& state = State(); std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) return ErrorInvalidId; + auto& context = *found->second; PoolScope scope(context.pool); std::vector<std::shared_ptr<CertificateChain>> authorities; std::vector<std::unique_ptr<CertificateHandle>> handles; - for (int index = 0; index < caCertNum; ++index) { + for (int index = 0; caList && index < caCertNum; ++index) { if (!caList[index]) throw std::invalid_argument("Missing SSL certificate descriptor"); auto chain = ParseCertificate(*caList[index]); for (auto* entry = &chain->root; entry; entry = entry->next) { @@ -679,23 +695,25 @@ int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, } authorities.push_back(std::move(chain)); } - std::shared_ptr<Identity> identity; - if (cert) { - identity = std::make_shared<Identity>(); - identity->chain = ParseCertificate(*cert); - const auto key = CopyInput(*privateKey); - Random random; - random.Seed(); - CheckBackend(mbedtls_pk_parse_key(&identity->key.key, key.bytes.get(), key.size, nullptr, 0, - mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); - CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &identity->key.key, mbedtls_ctr_drbg_random, &random.drbg), - "SSL client certificate and private key do not match"); - } context.authorities.reserve(Add(context.authorities.size(), authorities.size())); context.certificateHandles.reserve(Add(context.certificateHandles.size(), handles.size())); for (auto& chain : authorities) context.authorities.push_back(std::move(chain)); for (auto& handle : handles) context.certificateHandles.push_back(std::move(handle)); - if (identity) context.identity = std::move(identity); + if (cert) { + if (!cert->ptr) return static_cast<int>(0x80951771); + context.identity = std::make_shared<Identity>(); + context.identity->chain = ParseCertificate(*cert); + } + if (privateKey) { + if (!privateKey->ptr) return static_cast<int>(0x80951771); + if (!context.identity) context.identity = std::make_shared<Identity>(); + auto identity = std::make_shared<Identity>(*context.identity); + context.identity = identity; + identity->key.reset(); + auto bytes = Allocate(privateKey->size); + std::memcpy(bytes.get(), privateKey->ptr, privateKey->size); + identity->key = std::make_shared<EncodedPrivateKey>(EncodedPrivateKey{std::move(bytes), privateKey->size}); + } return 0; } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 904b4764a7..b59ad7c6c5 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -141,14 +141,14 @@ void AtomicFailures() { Check(sceSslGetCaCerts(id, &bytes) == 0, "CA vanished after failure"); CheckRoot(bytes); Check(sceSslFreeCaCerts(id, &bytes) == 0, "snapshot free failed"); - Throws([&] { sceSslLoadCert(id, -1, nullptr, nullptr, nullptr); }); - Throws([&] { sceSslLoadCert(id, 1, nullptr, nullptr, nullptr); }); + Check(sceSslLoadCert(id, -1, nullptr, nullptr, nullptr) == 0, "negative CA count was not ignored"); + Check(sceSslLoadCert(id, 1, nullptr, nullptr, nullptr) == 0, "null CA list was not ignored"); SceSsl::Data* absent = nullptr; Throws([&] { sceSslLoadCert(id, 1, &absent, nullptr, nullptr); }); - Throws([&] { sceSslLoadCert(id, 0, nullptr, &der, nullptr); }); + Check(Stats(id).currentInuseSize == before, "argument failure changed retained CA"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); - Throws([&] { sceSslLoadCert(id, 1, &pointer, nullptr, nullptr); }); + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == InvalidId, "invalid load context error mismatch"); const int tiny = sceSslInit_nid_postfix(32); Throws([&] { sceSslLoadCert(tiny, 1, &pointer, nullptr, nullptr); }); Check(Stats(tiny).currentInuseSize == 0, "failed small-pool load leaked"); @@ -163,8 +163,20 @@ void ClientIdentity() { Check(sceSslLoadCert(id, 0, nullptr, &client, &key) == 0, "client identity load failed"); const auto before = Stats(id).currentInuseSize; Check(before > 0, "identity not retained"); - Throws([&] { sceSslLoadCert(id, 0, nullptr, &client, &wrong); }); - Check(Stats(id).currentInuseSize == before, "mismatched key changed identity"); + Check(sceSslLoadCert(id, 0, nullptr, &client, &wrong) == 0, "key pair was checked during loading"); + Check(sceSslLoadCert(id, 0, nullptr, &client, nullptr) == 0, "certificate-only load failed"); + const auto certificateBytes = Stats(id).currentInuseSize; + Check(certificateBytes > 0 && certificateBytes < before, "certificate replacement retained the old key"); + Check(sceSslLoadCert(id, 0, nullptr, nullptr, &key) == 0, "independent key load failed"); + Check(Stats(id).currentInuseSize == before, "separate identity load retained different data"); + auto arbitraryKey = Data("invalid key encoding"); + Check(sceSslLoadCert(id, 0, nullptr, nullptr, &arbitraryKey) == 0, "key was parsed during loading"); + Check(Stats(id).currentInuseSize == certificateBytes + arbitraryKey.size, "raw key bytes were not retained"); + SceSsl::Data missing{}; + Check(sceSslLoadCert(id, 0, nullptr, &missing, nullptr) == static_cast<int>(0x80951771), "null certificate payload error mismatch"); + Check(sceSslLoadCert(id, 0, nullptr, nullptr, &missing) == static_cast<int>(0x80951771), "null key payload error mismatch"); + Check(Stats(id).currentInuseSize == certificateBytes + arbitraryKey.size, "null payload cleared the identity"); + Check(sceSslLoadCert(-1, -1, nullptr, &missing, &missing) == InvalidId, "load context validation precedence mismatch"); Check(sceSslUnloadCert(id) == 0 && Stats(id).currentInuseSize == 0, "identity release failed"); Check(sceSslTerm_nid_postfix(id) == 0, "identity term failed"); } diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 736ec71de5..23190fe3ef 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -9,6 +9,6 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. -- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS parses DER/PEM chains. CA imports still commit together instead of preserving successful entries before a later failure. Client certificate replacement clears the previous identity; raw private keys load independently and are parsed when configuring TLS. Partial client chains on decode failure, native key parse errors and incomplete identities during TLS setup remain unsupported. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. From 8b75d497c0f5048fc5d786738cf013a5db81bb38 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 14:02:24 +0300 Subject: [PATCH 0864/1360] fix(kernel): reject unimplemented internal pool statistics --- core/libs/prx/libkernel/DirectMemory/Export.cpp | 7 +++---- core/libs/tests/GuestMemory.cpp | 10 ++++++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 79d07266cb..bd93f135d1 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -465,10 +465,9 @@ int APS5_VABI sceKernelConfiguredFlexibleMemorySize(size_t* size) { return 0; } -int APS5_VABI sceKernelInternalMemoryGetAvailableSize(size_t* size) { - if (!size) return SCE_KERNEL_ERROR_EINVAL; - *size = 0; - return 0; +int APS5_VABI sceKernelInternalMemoryGetAvailableSize(std::size_t* size) { + if (!size) return -1; + throw std::logic_error("sceKernelInternalMemoryGetAvailableSize: internal pool statistics are not implemented"); } int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const char* name) { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 7bd76d6dad..d03a35de70 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -157,8 +157,14 @@ static void CheckPageTableStats() { static void CheckInternalMemoryAvailableSize() { std::size_t available = 1; - Require(sceKernelInternalMemoryGetAvailableSize(&available) == 0 && available == 0); - Require(sceKernelInternalMemoryGetAvailableSize(nullptr) == static_cast<int>(0x80020016u)); + bool threw = false; + try { + sceKernelInternalMemoryGetAvailableSize(&available); + } catch (const std::logic_error&) { + threw = true; + } + Require(threw && available == 1); + Require(sceKernelInternalMemoryGetAvailableSize(nullptr) == -1); } static void CheckNamedAndHintedMappings() { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 52eea8e8bb..7779dc8dc6 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -7,7 +7,7 @@ - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified -- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) +- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. ### Unknown function info From 6036bc651219e0a46ac27b5e10596635822b13e6 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 14:04:29 +0300 Subject: [PATCH 0865/1360] fix(zlib): restore validation errors and finalization behavior --- core/libs/prx/libSceZlib/Export.cpp | 25 ++++++++-------- core/libs/tests/GuestZlib.cpp | 46 ++++++++++++++++++++--------- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 22135e465c..093a5401d2 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -13,6 +13,8 @@ static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof constexpr std::int32_t NOT_INITIALIZED = static_cast<std::int32_t>(0x81120032); constexpr std::int32_t ALREADY_INITIALIZED = static_cast<std::int32_t>(0x81120033); constexpr std::int32_t NO_SPACE = static_cast<std::int32_t>(0x8112001C); +constexpr std::int32_t INVALID_ARGUMENT = static_cast<std::int32_t>(0x81120016); +constexpr std::int32_t NOT_FOUND = static_cast<std::int32_t>(0x81120002); constexpr std::int32_t FATAL = static_cast<std::int32_t>(0x811200FF); struct Result { @@ -34,7 +36,8 @@ extern "C" { std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t length) { std::lock_guard lock(serviceMutex); if (initialized) return ALREADY_INITIALIZED; - if ((buffer == nullptr) != (length == 0)) throw std::runtime_error("sceZlibInitialize: invalid work buffer"); + if ((buffer == nullptr) != (length == 0)) return INVALID_ARGUMENT; + if (buffer) throw std::runtime_error("sceZlibInitialize: work memory registration is not implemented"); initialized = true; return 0; } @@ -42,20 +45,18 @@ std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t lengt std::int32_t APS5_VABI sceZlibFinalize() { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (result && !result->retrieved) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); result.reset(); initialized = false; return 0; } -std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLength, - void* destination, std::uint32_t destinationLength, - std::uint64_t* requestId) { +std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLength, void* destination, std::uint32_t destinationLength, std::uint64_t* requestId) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; if (!source || !sourceLength || !destination || !requestId || !destinationLength || - destinationLength > 65536 || reinterpret_cast<std::uintptr_t>(destination) % 2048 != 0) - throw std::runtime_error("sceZlibInflate: invalid buffer or size"); + destinationLength > 65536 || destinationLength % 2048 != 0) return INVALID_ARGUMENT; + if (reinterpret_cast<std::uintptr_t>(destination) % 2048 != 0) + throw std::runtime_error("sceZlibInflate: unaligned destination driver behavior is not implemented"); if (result && !result->retrieved) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); if (nextId == std::numeric_limits<std::uint64_t>::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); z_stream stream{}; @@ -78,18 +79,18 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_t* timeout) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; + if (!requestId) return INVALID_ARGUMENT; if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); - if (!requestId || !result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + if (!result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); *requestId = result->id; return 0; } -std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* destinationLength, - std::int32_t* status) { +std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* destinationLength, std::int32_t* status) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (!destinationLength || !status || !result || result->id != requestId) - throw std::runtime_error("sceZlibGetResult: invalid request or output"); + if (!destinationLength || !status) return INVALID_ARGUMENT; + if (!result || result->id != requestId) return NOT_FOUND; if (result->retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); *destinationLength = result->produced; *status = result->status; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 50a9688420..e893ab95e5 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -3,6 +3,8 @@ #include <array> #include <cstdint> #include <cstdlib> +#include <cstdio> +#include <source_location> #include <cstring> #include <stdexcept> @@ -14,7 +16,11 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t*, std::uint32_t*); std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t, std::uint32_t*, std::int32_t*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "Zlib assertion failed at line %u\n", location.line()); + std::abort(); +} template<typename TAction> static void RequireThrows(TAction action) { @@ -33,12 +39,20 @@ int main() { std::int32_t status = 0; Require(sceZlibFinalize() == static_cast<std::int32_t>(0x81120032)); Require(sceZlibInflate(nullptr, 0, nullptr, 0, nullptr) == static_cast<std::int32_t>(0x81120032)); - RequireThrows([&] { sceZlibInitialize(nullptr, 1); }); + Require(sceZlibInitialize(nullptr, 1) == static_cast<std::int32_t>(0x81120016)); + Require(sceZlibInitialize(output, 0) == static_cast<std::int32_t>(0x81120016)); Require(sceZlibInitialize(nullptr, 0) == 0); Require(sceZlibInitialize(nullptr, 0) == static_cast<std::int32_t>(0x81120033)); - RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output + 1, expectedLength, &id); }); - RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output, 65537, &id); }); - RequireThrows([&] { sceZlibInflate(nullptr, compressed.size(), output, expectedLength, &id); }); + RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output + 1, 8192, &id); }); + for (const auto capacity : {0u, 1u, 2047u, 2049u, 65535u, 65537u}) { + Require(sceZlibInflate(compressed.data(), compressed.size(), output, capacity, &id) == static_cast<std::int32_t>(0x81120016)); + Require(id == 0); + } + Require(sceZlibInflate(nullptr, compressed.size(), output, 8192, &id) == static_cast<std::int32_t>(0x81120016)); + Require(sceZlibWaitForDone(nullptr, nullptr) == static_cast<std::int32_t>(0x81120016)); + Require(sceZlibGetResult(0, nullptr, &status) == static_cast<std::int32_t>(0x81120016)); + Require(sceZlibGetResult(0, &produced, nullptr) == static_cast<std::int32_t>(0x81120016)); + Require(sceZlibGetResult(0, &produced, &status) == static_cast<std::int32_t>(0x81120002)); std::uint64_t previous = 0; auto decode = [&](const void* input, std::uint32_t inputLength, std::uint32_t capacity, std::int32_t expectedStatus) { storage.fill(0xA5); @@ -46,34 +60,38 @@ int main() { Require(id > previous); previous = id; RequireThrows([&] { sceZlibInflate(input, inputLength, output, capacity, &id); }); - RequireThrows([&] { sceZlibFinalize(); }); std::uint32_t timeout = 0; RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); std::uint64_t completed; Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); - RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); + Require(sceZlibGetResult(id + 1, &produced, &status) == static_cast<std::int32_t>(0x81120002)); Require(sceZlibGetResult(id, &produced, &status) == 0); + if (status != expectedStatus) std::fprintf(stderr, "input=%u capacity=%u status=%x expected=%x produced=%u\n", inputLength, capacity, static_cast<unsigned>(status), static_cast<unsigned>(expectedStatus), produced); Require(status == expectedStatus && produced <= capacity); RequireThrows([&] { sceZlibGetResult(id, &produced, &status); }); Require(std::all_of(storage.begin(), storage.begin() + 2048, [](auto byte) { return byte == 0xA5; })); Require(std::all_of(storage.begin() + 2048 + capacity, storage.end(), [](auto byte) { return byte == 0xA5; })); }; - decode(compressed.data(), compressed.size(), expectedLength, 0); + decode(compressed.data(), compressed.size(), 8192, 0); Require(produced == expectedLength); for (std::uint32_t offset = 0; offset < produced; offset += sizeof(text) - 1) Require(std::memcmp(output + offset, text, sizeof(text) - 1) == 0); - decode(compressed.data(), compressed.size(), expectedLength - 1, static_cast<std::int32_t>(0x8112001C)); + decode(compressed.data(), compressed.size(), 2048, static_cast<std::int32_t>(0x8112001C)); auto corrupt = compressed; corrupt.back() ^= 1; - decode(corrupt.data(), corrupt.size(), expectedLength, static_cast<std::int32_t>(0x811200FF)); + decode(corrupt.data(), corrupt.size(), 8192, static_cast<std::int32_t>(0x811200FF)); decode(compressed.data(), compressed.size() - 1, 65536, static_cast<std::int32_t>(0x811200FF)); - decode(compressed.data(), compressed.size() - 4, expectedLength, static_cast<std::int32_t>(0x811200FF)); - decode(compressed.data(), compressed.size() - 1, expectedLength, static_cast<std::int32_t>(0x811200FF)); + decode(compressed.data(), compressed.size() - 4, 8192, static_cast<std::int32_t>(0x811200FF)); + decode(compressed.data(), compressed.size() - 1, 8192, static_cast<std::int32_t>(0x811200FF)); Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast<std::int32_t>(0x81120032)); std::array<std::uint8_t, 65536> scratch{}; - Require(sceZlibInitialize(scratch.data(), scratch.size()) == 0); - decode(compressed.data(), compressed.size(), 65536, 0); + RequireThrows([&] { sceZlibInitialize(scratch.data(), scratch.size()); }); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibInflate(compressed.data(), compressed.size(), output, 65536, &id) == 0); + Require(sceZlibFinalize() == 0); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibGetResult(id, &produced, &status) == static_cast<std::int32_t>(0x81120002)); Require(sceZlibFinalize() == 0); } diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 5d289d575b..8cbfd2864e 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) decompresses synchronously and supports one outstanding request. Timed waits, multiple outstanding requests and repeated retrieval throw. Work-memory registration and driver-side destination alignment validation remain unsupported; non-null work memory and unaligned destinations throw. Native user-space validation confirms the 64-bit initialization length, 2 KiB output-length granularity up to 64 KiB, null-argument errors and initialization precedence. Finalization discards the host's completed result. Driver scheduling, completion consumption, cancellation, scratch-memory requirements and decompression error details remain unverified; host decoding maps malformed streams to 0x811200FF and exhausted output to 0x8112001C. From 2f678e904639b2d4d13ee999fe34b152962ee025 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 15:04:02 +0300 Subject: [PATCH 0866/1360] docs: record retained activation and pad no-op behavior --- docs/dev/debt/libScePad.md | 4 ++++ docs/dev/debt/libSceSystemService.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 3fdf755b89..d533ccb116 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -1,5 +1,9 @@ # libScePad technical debt +### Silent stubs + +- [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing extension-port state. The boolean argument is recovered; connected-device behavior is not modeled. + ### Unknown function info - [libScePad profile and device-setting exports](../../../core/libs/prx/libScePad/Export.cpp) - profile getters, tuning, saved settings, USB state, shared output and virtual-device setting arguments are recovered. Hunt and Leonardo reset each consume only a handle; physical VR USB status consumes only an output pointer. Remote settings output is 64 bytes and license-controller information is 1224 bytes. Kernel ioctl payload fields remain incomplete. diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index e697b107ae..a5b01ed7ab 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -2,6 +2,8 @@ ### Silent stubs +- [sceShellCoreUtilActivateAbort, sceShellCoreUtilActivateStart, sceShellCoreUtilActivateStartAsync, sceShellCoreUtilActivateTerm](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) return zero with the recovered handle argument. Activation service state is not modeled. + - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it From 5888cf3cb295e42d0df6d0c22ba1381bccfae26b Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 15:10:04 +0300 Subject: [PATCH 0867/1360] fix(net): align merged ipv6 and rudp argument types --- core/libs/prx/libSceNetCtl/Export.cpp | 2 +- core/libs/tests/GuestNetCtl.cpp | 10 ++++++---- core/libs/tests/GuestRudpPoll.cpp | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index aea249a719..30be5f6e85 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -527,7 +527,7 @@ int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* c return RegisterCallback(g_callbacksV6, func, arg, cid); } -int APS5_VABI sceNetCtlGetInfoV6(int code, void* info) { +int APS5_VABI sceNetCtlGetInfoV6(std::uint32_t code, NetCtlInfoV6* info) { if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; if (code < NET_CTL_INFO_V6_IP_ADDRESS || code > NET_CTL_INFO_V6_SECONDARY_DNS) return SCE_NET_CTL_ERROR_INVALID_CODE; if (!HostHasAddress(AF_INET6)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index aa2c54300b..1b7ce582b2 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -31,6 +31,8 @@ struct NetCtlDefaultRoute { }; #endif +union NetCtlInfoV6; + extern "C" { int APS5_VABI sceNetCtlInit(void); int APS5_VABI sceNetCtlCheckCallback(void); @@ -38,7 +40,7 @@ int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetStateV6(int*); -int APS5_VABI sceNetCtlGetInfoV6(int, void*); +int APS5_VABI sceNetCtlGetInfoV6(std::uint32_t, NetCtlInfoV6*); int APS5_VABI sceNetCtlGetResult(int, int*); int APS5_VABI sceNetCtlGetResultV6(int, int*); #ifdef __APPLE__ @@ -316,10 +318,10 @@ int main() { Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); std::uint8_t infoV6[256] = {}; - Require(sceNetCtlGetInfoV6(0, infoV6) == kErrInvalidCode); - Require(sceNetCtlGetInfoV6(6, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(0, reinterpret_cast<NetCtlInfoV6*>(infoV6)) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(6, reinterpret_cast<NetCtlInfoV6*>(infoV6)) == kErrInvalidCode); Require(sceNetCtlGetInfoV6(1, nullptr) == kErrInvalidAddr); - if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, infoV6) == kErrNotConnected); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, reinterpret_cast<NetCtlInfoV6*>(infoV6)) == kErrNotConnected); const int result = sceNetCtlGetInfo(14, &info); if (result == kErrNotConnected) { diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index d797fad444..4ff92c482f 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -2,6 +2,7 @@ #include <array> #include <chrono> +#include <cstddef> #include <cstdint> #include <cstdlib> #include <limits> @@ -18,9 +19,9 @@ int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); int APS5_VABI sceRudpTerminate(int); int APS5_VABI sceRudpEnd(); -int APS5_VABI sceRudpPollCreate(std::uint32_t); +int APS5_VABI sceRudpPollCreate(std::size_t); int APS5_VABI sceRudpPollControl(int, int, int, std::uint16_t); -int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::uint32_t, std::uint64_t); +int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::size_t, std::uint64_t); int APS5_VABI sceRudpPollDestroy(int); } From 9301f2ffeb03680626c4b884dd9d7046ec0de873 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 15:17:14 +0300 Subject: [PATCH 0868/1360] test: align merged cases with recovered library behavior --- core/libs/tests/GuestUltCondition.cpp | 23 +++++++++++++---------- core/libs/tests/GuestUltSync.cpp | 22 +++++++++++++--------- core/libs/tests/GuestUltThread.cpp | 2 +- core/libs/tests/GuestVoiceQoS.cpp | 1 - 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp index 51741e4aa2..5bec09da1b 100644 --- a/core/libs/tests/GuestUltCondition.cpp +++ b/core/libs/tests/GuestUltCondition.cpp @@ -23,6 +23,7 @@ int APS5_VABI sceUltConditionVariableDestroy(void*); constexpr int Null = -2139029503; constexpr int Invalid = -2139029500; +constexpr int Permission = -2139029499; constexpr int State = -2139029498; constexpr int Busy = -2139029497; @@ -41,8 +42,10 @@ struct Fixture { Object mutex; Object condition; - Fixture() { - CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0x12000000) == 0); + explicit Fixture(bool recursive = false) { + UltMutexOptParam options{}; + options.attribute = recursive ? 1 : 0; + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, &options, 0x12000000) == 0); CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0x12000000) == 0); } @@ -58,7 +61,7 @@ struct Fixture { }; static void Validation() { - Fixture f; + Fixture f(true); Object unused; CHECK(sceUltMutexDestroy(nullptr) == Null); CHECK(sceUltMutexDestroy(&unused) == State); @@ -73,16 +76,16 @@ static void Validation() { CHECK(sceUltConditionVariableWait(&unused) == State); CHECK(_sceUltConditionVariableCreate(&unused, nullptr, nullptr, nullptr, 0) == Null); CHECK(sceUltConditionVariableWait(&f.condition) == State); - CHECK(sceUltMutexUnlock(&f.mutex) == State); + CHECK(sceUltMutexUnlock(&f.mutex) == Permission); bool unsupported = false; try { _sceUltConditionVariableCreate(&unused, nullptr, &f.mutex, &unused, 0); } catch (const std::runtime_error&) { unsupported = true; } CHECK(unsupported); f.Lock(); - CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == State); CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == State); auto other = std::async(std::launch::async, [&] { - CHECK(sceUltMutexUnlock(&f.mutex) == State); + CHECK(sceUltMutexUnlock(&f.mutex) == Permission); CHECK(sceUltConditionVariableWait(&f.condition) == State); }); other.get(); @@ -91,9 +94,9 @@ static void Validation() { try { f.Wait(); } catch (const std::runtime_error&) { unsupported = true; } CHECK(unsupported); f.Unlock(); - CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == State); f.Unlock(); - CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == State); CHECK(sceUltConditionVariableDestroy(&f.condition) == 0); CHECK(sceUltMutexDestroy(&f.mutex) == 0); CHECK(sceUltMutexLock(&f.mutex) == State); @@ -121,13 +124,13 @@ static void Handoff() { entered = 1; f.Wait(); CHECK(payload == 42); - CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == State); f.Unlock(); }); Await(entered, 1); f.Lock(); CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); - CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == State); CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); f.Unlock(); f.Lock(); diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 9695ac0263..2a368aa57b 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -12,7 +12,7 @@ extern "C" { int APS5_VABI sceUltInitialize(); int APS5_VABI sceUltFinalize(); -int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptParam*, std::uint32_t); int APS5_VABI sceUltMutexLock(void*); int APS5_VABI sceUltMutexTryLock(void*); int APS5_VABI sceUltMutexUnlock(void*); @@ -22,7 +22,7 @@ int APS5_VABI sceUltQueueOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); -int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); @@ -49,6 +49,8 @@ namespace { constexpr int Ok = 0; constexpr int Null = -2139029503; constexpr int Invalid = -2139029500; +constexpr int Permission = -2139029499; +constexpr int Again = -2139029496; constexpr int State = -2139029498; constexpr int Busy = -2139029497; @@ -68,7 +70,9 @@ void MutexTryLock() { Object mutex{}; CHECK(sceUltMutexTryLock(nullptr) == Null); CHECK(sceUltMutexTryLock(&mutex) == State); - CHECK(_sceUltMutexCreate(&mutex, "trylock", nullptr, nullptr, 0) == Ok); + UltMutexOptParam options{}; + options.attribute = 1; + CHECK(sceUltMutexCreate(&mutex, "trylock", nullptr, &options, 0) == Ok); CHECK(sceUltMutexTryLock(&mutex) == Ok); CHECK(sceUltMutexTryLock(&mutex) == Ok); std::atomic<int> other{0}; @@ -78,14 +82,14 @@ void MutexTryLock() { std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); CHECK(other == Busy); CHECK(sceUltMutexUnlock(&mutex) == Ok); - CHECK(sceUltMutexUnlock(&mutex) == State); + CHECK(sceUltMutexUnlock(&mutex) == Permission); std::thread([&] { other = sceUltMutexTryLock(&mutex); if (other == Ok) other = sceUltMutexUnlock(&mutex); }).join(); CHECK(other == Ok); CHECK(sceUltMutexLock(&mutex) == Ok); - CHECK(sceUltMutexDestroy(&mutex) == Busy); + CHECK(sceUltMutexDestroy(&mutex) == State); CHECK(sceUltMutexUnlock(&mutex) == Ok); CHECK(sceUltMutexDestroy(&mutex) == Ok); CHECK(sceUltMutexTryLock(&mutex) == State); @@ -210,7 +214,7 @@ void ReaderWriterPool() { Object pool{}; Object rwlock{}; std::uint8_t workArea[2048]; - CHECK(_sceUltWaitingQueueResourcePoolCreate(&pool, "pool", 2, 2, workArea, nullptr, 0) == Ok); + CHECK(_sceUltWaitingQueueResourcePoolCreate(&pool, "pool", 2, 2, workArea, nullptr) == Ok); CHECK(sceUltReaderWriterLockCreate(&rwlock, "pooled", &pool, nullptr, 0) == Ok); CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); std::atomic<bool> started{false}; @@ -289,10 +293,10 @@ void Ulthreads() { gSelf = nullptr; CHECK(sceUltUlthreadCreate(&second, "tryjoin", ExitingEntry, 7, context, sizeof(context), &runtime, nullptr, 0) == Ok); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); - int joined = Busy; - while (joined == Busy && std::chrono::steady_clock::now() < deadline) { + int joined = Again; + while (joined == Again && std::chrono::steady_clock::now() < deadline) { joined = sceUltUlthreadTryJoin(&second, &status); - if (joined == Busy) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (joined == Again) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } CHECK(joined == Ok && status == 7 && gSelf.load() == &second); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index e1fb8dbbba..32b8be2972 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -119,7 +119,7 @@ static void Validation() { CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 1184); CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); - CHECK(sceUltReaderWriterLockDestroy(nullptr) == ULT_ERROR_NULL); + CHECK(sceUltReaderWriterLockDestroy(nullptr) == Null); bool threwInfo = false; try { sceUltGetConditionVariableInfo(nullptr, nullptr, nullptr, 0); diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 8db920a44f..b4d318548e 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -139,7 +139,6 @@ int main(int argc, char** argv) { Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); - Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == notInitialized); Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == notInitialized); Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == notInitialized); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); From dfe41f1f4596d03df0458162e919e9208adc19bf Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 15:45:18 +0300 Subject: [PATCH 0869/1360] fix: align merged hmd declarations and webapi tests --- core/libs/prx/libSceHmd/Hmd.hpp | 13 +++++++++---- core/libs/tests/GuestHmd.cpp | 8 ++++---- core/libs/tests/GuestNpWebApi2.cpp | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceHmd/Hmd.hpp b/core/libs/prx/libSceHmd/Hmd.hpp index eb2f77625f..31cf038dbb 100644 --- a/core/libs/prx/libSceHmd/Hmd.hpp +++ b/core/libs/prx/libSceHmd/Hmd.hpp @@ -51,6 +51,11 @@ static_assert(offsetof(DeviceInformation, hmuMount) == 24); } +struct HmdFieldOfView; +struct HmdEyeOffset; +struct HmdInertialSensorData; +struct HmdAssemblyError; + extern "C" { std::int32_t APS5_VABI sceHmdInitialize(const Hmd::InitializeParam* param); std::int32_t APS5_VABI sceHmdInitialize315(const Hmd::InitializeParam* param); @@ -69,10 +74,10 @@ int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void); int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void); int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void); int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void); -int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, void* fieldOfView); -int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, void* leftOffset, void* rightOffset); -int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, void* data, std::int32_t count); -int APS5_VABI sceHmdGetAssyError(void* data); +int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, HmdFieldOfView* fieldOfView); +int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, HmdEyeOffset* leftOffset, HmdEyeOffset* rightOffset); +int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, HmdInertialSensorData* data, std::uint32_t count); +int APS5_VABI sceHmdGetAssyError(HmdAssemblyError* data); } #endif diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index 203775d1fd..230395afb9 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -42,10 +42,10 @@ void CheckHandleQueries(std::int32_t expected) { std::memset(sensor, 0x5a, sizeof(sensor)); for (const auto handle : {0, -1, 1, 0x0f000000}) { Require(sceHmdGetFieldOfView(handle, nullptr) == Hmd::ParameterNull, "null field of view accepted"); - Require(sceHmdGetFieldOfView(handle, fieldOfView) == expected, "field of view without a headset"); - Require(sceHmdGet2DEyeOffset(handle, left, right) == expected, "eye offset without a headset"); + Require(sceHmdGetFieldOfView(handle, reinterpret_cast<HmdFieldOfView*>(fieldOfView)) == expected, "field of view without a headset"); + Require(sceHmdGet2DEyeOffset(handle, reinterpret_cast<HmdEyeOffset*>(left), reinterpret_cast<HmdEyeOffset*>(right)) == expected, "eye offset without a headset"); Require(sceHmdGet2DEyeOffset(handle, nullptr, nullptr) == expected, "eye offset with null outputs"); - Require(sceHmdGetInertialSensorData(handle, sensor, 1) == expected, "sensor data without a headset"); + Require(sceHmdGetInertialSensorData(handle, reinterpret_cast<HmdInertialSensorData*>(sensor), 1) == expected, "sensor data without a headset"); } for (const auto* bytes : {left, right, fieldOfView}) { for (std::size_t index = 0; index < 16; ++index) Require(bytes[index] == 0x5a, "a refused query wrote its output"); @@ -57,7 +57,7 @@ void CheckAssyError(std::int32_t expected) { unsigned char data[32]; std::memset(data, 0x5a, sizeof(data)); Require(sceHmdGetAssyError(nullptr) == Hmd::ParameterNull, "null assembly error output accepted"); - Require(sceHmdGetAssyError(data) == expected, "assembly error query without a headset"); + Require(sceHmdGetAssyError(reinterpret_cast<HmdAssemblyError*>(data)) == expected, "assembly error query without a headset"); for (const auto byte : data) Require(byte == 0x5a, "a refused assembly error query wrote its output"); } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 7bd8327e2b..1adccd989d 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -92,7 +92,7 @@ int main() { Require(presence > 0 && sceNpWebApi2GetMemoryPoolStats(presence, &stats) == 0 && stats.pool_size == 0x2000); for (const int created : {internal, internal2, presence}) { Require(sceNpWebApi2Terminate(created) == 0); - Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(created, &stats); })); + Require(sceNpWebApi2GetMemoryPoolStats(created, &stats) == invalidContext); } return 0; } From 68aeef249af0e36e8c0f749a300316f79850a6a2 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 21:36:22 +0300 Subject: [PATCH 0870/1360] fix(tools): scan committed sources for duplicate exports --- tools/check_conventions.py | 29 ++++++++++--------- .../tests/test_check_conventions_comments.py | 1 + 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 132f275710..872ae1dd6c 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -206,21 +206,22 @@ class Check: def duplicates(self, exports): if not exports: return - result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nF", "--full-name", "-f", "-", self.head, "--", ":(top)core/libs/prx"], input="\n".join(exports) + "\n", capture_output=True, encoding="utf-8", errors="replace") - hits = [line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))] - self.read(path for path, _, _ in hits) - sources = {} - for path, number, text in hits: - if path not in sources: - sources[path] = cpp_text(self.show(path)).split("\n") - source = sources[path] - match = next((match for match in EXPORT.finditer(source[int(number) - 1]) if match.group(1) in exports), None) - if match is None: + paths = sorted(path for path in self.objects if path.startswith("core/libs/prx/") and + "tests" not in PurePosixPath(path).parts) + self.read(paths) + for path in paths: + text = self.show(path) + if "APS5_VABI" not in text: continue - name = match.group(1) - own, own_number = exports[name] - if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(source, int(number) - 1) is not None: - self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") + source = cpp_text(text).split("\n") + for number, line in enumerate(source): + for match in EXPORT.finditer(line): + name = match.group(1) + if name not in exports: + continue + own, own_number = exports[name] + if library(path) != library(own) and function_body(source, number) is not None: + self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def unknown_nids(self, unknowns, symbols): if not unknowns and not symbols: diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index c86fcdd05a..95aa949321 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -715,6 +715,7 @@ class RepositoryChecksTests(unittest.TestCase): second.parent.mkdir(parents=True) second.write_text("".join(f"int APS5_VABI sceLargeExportSet{index}(int value) {{ return value; }}\n" for index in range(2000)), encoding="utf-8") self.commit() + first.write_text("", encoding="utf-8") check = Check("HEAD~1", "HEAD") check.code() self.assertEqual(check.findings, [("duplicate-export", "core/libs/prx/libSceB/Export.cpp", 2000, "sceLargeExportSet1999 is also in libSceA")]) From 949737625b446ed35e8c12824985ac31112795b4 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:00:50 +0200 Subject: [PATCH 0871/1360] docs(input): list the F10 performance overlay in the default bindings table Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/user/INPUT_MAPPING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 7ab80c3d4f..7fd7a8d16d 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -42,6 +42,7 @@ Without an `anyps5-input.ini`, or for actions the file does not mention, these b | `Shake` | X (reports a swinging acceleration and angular velocity while held) | | `ToggleMouse` | Middle mouse button, F7 | | `ToggleFullscreen` | F11 | +| `TogglePerformanceOverlay` | F10 | | `Screenshot` | F12 | For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: From 023139d12ce4aa15935e7bd8e3178964977e4224 Mon Sep 17 00:00:00 2001 From: MarcZX10 <marc@example.com> Date: Wed, 21 Oct 2026 19:05:18 +0000 Subject: [PATCH 0872/1360] feat(libSceAudioOut): support mastering InitEx flags --- core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp | 5 +++++ core/libs/prx/libSceAudioOut/src/Unimplemented.cpp | 5 ----- core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp | 5 +++++ docs/dev/debt/libSceAudioOut.md | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp index e48537db95..fa03043577 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp @@ -22,6 +22,11 @@ int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) { return 0; } +int APS5_VABI sceAudioOut2MasteringInitEx(uint32_t flags) { + if ((flags & 0x7FFFFFFFu) != 0) NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAudioOut2MasteringTerm(void) { return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 02abcf4fd9..4119c896d7 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -365,11 +365,6 @@ int APS5_VABI sceAudioOut2GetSystemStateEx(void) { return 0; } -int APS5_VABI sceAudioOut2MasteringInitEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { 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 9a09ec4fba..ff8bca81a3 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceAudioOut2LoInit(); int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); +int APS5_VABI sceAudioOut2MasteringInitEx(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioOut2MasteringGetState(void*, std::uint32_t, std::uint64_t); @@ -65,6 +66,10 @@ void TestSet3DLatency() { void TestMasteringInit() { Require(sceAudioOut2MasteringInit(0) == 0, "flags 0 must be accepted"); Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInit(1); }), "flags 1 must throw"); + Require(sceAudioOut2MasteringInitEx(0) == 0, "InitEx flags 0 must be accepted"); + Require(sceAudioOut2MasteringInitEx(0x80000000u) == 0, "InitEx flag bit 31 must be accepted"); + Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(1); }), "InitEx flag bit 0 must throw"); + Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(0x40000000u); }), "InitEx flag bit 30 must throw"); } void TestMasteringTerm() { diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index f6e6901f1f..41beba6f42 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -97,7 +97,7 @@ - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) accepts only flag bit 31, following the shared Init/InitEx validation routine in the decrypted PS5 12.00 `libSceAudioOut` analyzed for #9027. The mode is not applied because this implementation has no mastering stage; other firmware versions are unverified. - [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From ca8b3da6fb040f4c518371477230a58d6d06dc4e Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Thu, 22 Oct 2026 02:07:26 +0700 Subject: [PATCH 0873/1360] test(libSceAudioOut): check that an audio port whose buffer size overflows throws Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 15 +++ .../tests/AudioOutBufferSize.cpp | 100 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index af79a20cf1..d13571aad4 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -47,6 +47,21 @@ if(BUILD_TESTING) endif() endforeach() + add_test_executable(audio_out_buffer_size_tests tests/AudioOutBufferSize.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) + target_compile_definitions(audio_out_buffer_size_tests PRIVATE SDL_MAIN_HANDLED) + target_include_directories(audio_out_buffer_size_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_buffer_size_tests PRIVATE libc libkernel libScePad) + configure_windows_unwind(audio_out_buffer_size_tests) + foreach(bufferSizeMode IN ITEMS pass_through wrap mix threshold) + set(bufferSizeTest "audio_out_buffer_size_${bufferSizeMode}") + add_test(NAME ${bufferSizeTest} COMMAND audio_out_buffer_size_tests ${bufferSizeMode}) + set_tests_properties(${bufferSizeTest} PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(${bufferSizeTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() + add_test_executable(audio_out_vibration_tests tests/AudioOutVibration.cpp) target_include_directories(audio_out_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out_vibration_tests PRIVATE libSceAudioOut libScePad libc) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp new file mode 100644 index 0000000000..d51eda399a --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp @@ -0,0 +1,100 @@ +#include "SDL.h" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <stdexcept> +#include <string_view> +#include <vector> + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +} + +namespace { +constexpr SDL_AudioDeviceID device = 7; +int opened = 0; +int closed = 0; +int queued = 0; +std::uint32_t queuedBytes = 0; +const void* supplied = nullptr; +std::uint32_t suppliedBytes = 0; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void RunPassThrough() { + std::vector<float> samples(512u * 8u); + for (std::size_t i = 0; i < samples.size(); ++i) samples[i] = static_cast<float>(i % 97u) / 97.0f; + const int handle = sceAudioOutOpen(0, 0, 0, 512, 48000, 5); + Require(handle > 0 && opened == 1, "a 512-frame F32 8-channel port did not open its device"); + supplied = samples.data(); + suppliedBytes = static_cast<std::uint32_t>(samples.size() * sizeof(float)); + Require(sceAudioOutOutput(handle, samples.data()) == 512, "a 512-frame output did not return 512"); + Require(queued == 1 && queuedBytes == 16384, "a 512-frame F32 8-channel output did not queue 16384 bytes"); + Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close its device"); +} + +void RunOverflow(std::uint32_t frames, std::uint32_t format, std::uint32_t frameBytes) { + std::vector<std::uint8_t> frame(frameBytes); + const int handle = sceAudioOutOpen(0, 0, 0, frames, 48000, format); + Require(handle > 0 && opened == 1, "a port with an oversized length did not open its device"); + supplied = frame.data(); + suppliedBytes = frameBytes; + bool threw = false; + try { + static_cast<void>(sceAudioOutOutput(handle, frame.data())); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "an output whose buffer size overflows did not throw"); + Require(queued == 0, "an output whose buffer size overflows queued audio"); + Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close after the refused output"); +} +} + +int InitializeSdlAudio_nid_no_patch() { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return 1; } +extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; } +extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = 2; return 0; } +extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return "dummy"; } +extern "C" const char* SDLCALL SDL_GetError() { return "test error"; } +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + Require(name == nullptr && capture == 0 && desired->callback == nullptr, "the main port did not open the default device"); + ++opened; + *obtained = *desired; + return device; +} +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID id, int) { Require(id == device, "wrong device paused"); } +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID id) { Require(id == device, "wrong queue queried"); return 0; } +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID id, const void* data, Uint32 bytes) { + Require(id == device, "wrong queue fed"); + ++queued; + queuedBytes = bytes; + if (bytes <= suppliedBytes) Require(std::memcmp(data, supplied, bytes) == 0, "the queued audio is not the supplied samples"); + return 0; +} +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID id) { Require(id == device, "wrong queue cleared"); } +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID id) { Require(id == device, "wrong device closed"); ++closed; } + +int main(int argc, char** argv) { + try { + Require(argc == 2, "invalid test arguments"); + const std::string_view mode = argv[1]; + if (mode == "pass_through") RunPassThrough(); + else if (mode == "wrap") RunOverflow(0x80000000u, 5, 8u * sizeof(float)); + else if (mode == "mix") RunOverflow(0x80000000u, 7, 8u * sizeof(float)); + else if (mode == "threshold") RunOverflow(0x40000000u, 0, sizeof(std::int16_t)); + else Require(false, "invalid buffer size mode"); + std::puts("audio out buffer size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From 478947f71f4a5de01374e23e060110acebbe690e Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Wed, 21 Oct 2026 20:05:22 +0100 Subject: [PATCH 0874/1360] feat(libkernel_sys): implement the POSIX aio calls --- core/libs/CMakeLists.txt | 11 + core/libs/prx/libkernel_sys/Aio.cpp | 345 ++++++++++++++++++ core/libs/prx/libkernel_sys/CMakeLists.txt | 2 +- .../prx/libkernel_sys/Export.Discovery.cpp | 40 -- core/libs/tests/GuestKernelSysAio.cpp | 237 ++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 6 files changed, 595 insertions(+), 41 deletions(-) create mode 100644 core/libs/prx/libkernel_sys/Aio.cpp create mode 100644 core/libs/tests/GuestKernelSysAio.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..907d7ed1c3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -633,6 +633,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>") endif() + add_test_executable(guest_kernel_sys_aio_tests tests/GuestKernelSysAio.cpp) + target_include_directories(guest_kernel_sys_aio_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_aio_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_aio_tests) + add_test(NAME guest_kernel_sys_aio COMMAND guest_kernel_sys_aio_tests) + set_tests_properties(guest_kernel_sys_aio PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_aio PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") + endif() + add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp) target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit) diff --git a/core/libs/prx/libkernel_sys/Aio.cpp b/core/libs/prx/libkernel_sys/Aio.cpp new file mode 100644 index 0000000000..789b0c72d3 --- /dev/null +++ b/core/libs/prx/libkernel_sys/Aio.cpp @@ -0,0 +1,345 @@ +#include <algorithm> +#include <chrono> +#include <condition_variable> +#include <cstddef> +#include <cstdint> +#include <deque> +#include <limits> +#include <list> +#include <memory> +#include <mutex> +#include <optional> +#include <thread> + +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); +extern "C" std::int64_t APS5_VABI _fstat_nid_postfix(int descriptor, FileStat* status); +extern "C" std::int64_t APS5_VABI pread_nid_postfix(int descriptor, void* buffer, std::size_t bytes, std::int64_t offset); +extern "C" std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int descriptor, const void* buffer, std::size_t bytes, std::int64_t offset); +extern "C" int APS5_VABI fsync_nid_postfix(int descriptor); + +namespace { + +constexpr int GuestEbadf = 9; +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEagain = 35; +constexpr int GuestEinprogress = 36; +constexpr int GuestEopnotsupp = 45; +constexpr int GuestEcanceled = 85; +constexpr int LioWrite = 1; +constexpr int LioRead = 2; +constexpr int LioSync = 3; +constexpr int AioCanceled = 1; +constexpr int AioNotCanceled = 2; +constexpr int AioAllDone = 3; +constexpr int SigevNone = 0; +constexpr int SigevSignal = 1; +constexpr int SigevKevent = 3; +constexpr int SigevThreadId = 4; +constexpr int SignalMax = 128; +constexpr int GuestOSync = 0x80; +constexpr int GetFlags = 3; +constexpr int AccessMode = 3; +constexpr int WriteOnly = 1; +constexpr int ReadWrite = 2; +constexpr std::uint16_t TypeMask = 0170000; +constexpr std::uint16_t TypeFifo = 0010000; +constexpr std::uint16_t TypeDirectory = 0040000; +constexpr std::uint16_t TypeRegular = 0100000; +constexpr std::uint16_t TypeSocket = 0140000; +constexpr std::size_t QueuePerProcess = 256; +constexpr std::uint64_t IoSizeMax = std::numeric_limits<int>::max(); +constexpr std::int64_t NanosecondsPerSecond = 1000000000; +constexpr std::int64_t TickNanoseconds = 1000000; + +struct GuestSigevent { + int notify; + int signo; + std::int64_t value; + std::int64_t extra[8]; +}; + +struct GuestAiocb { + int fildes; + std::int64_t offset; + void* buf; + std::uint64_t nbytes; + int spare[2]; + void* spare2; + int lioOpcode; + int reqprio; + std::int64_t status; + std::int64_t error; + void* kernelinfo; + GuestSigevent sigevent; +}; + +static_assert(offsetof(GuestAiocb, lioOpcode) == 48 && offsetof(GuestAiocb, status) == 56 && offsetof(GuestAiocb, sigevent) == 80); +static_assert(sizeof(GuestAiocb) == 160); + +struct GuestTimespec { + std::int64_t tv_sec; + std::int64_t tv_nsec; +}; + +enum class State { Queued, Running, Done }; + +struct Job { + GuestAiocb* user; + int descriptor; + int opcode; + std::int64_t offset; + void* buffer; + std::uint64_t bytes; + State state = State::Queued; + std::int64_t status = -1; + int error = 0; +}; + +struct Aio { + std::mutex lock; + std::condition_variable changed; + bool used = false; + bool held = false; + bool started = false; + std::intptr_t nextId = 0; + std::deque<std::shared_ptr<Job>> queued; + std::list<std::shared_ptr<Job>> all; + std::list<std::shared_ptr<Job>> done; +}; + +Aio& Instance() { + static auto* aio = new Aio; + return *aio; +} + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +void Run(Job& job) { + std::int64_t result = 0; + if (job.opcode == LioRead) result = job.bytes == 0 ? 0 : pread_nid_postfix(job.descriptor, job.buffer, job.bytes, job.offset); + else if (job.opcode == LioWrite) result = pwrite_nid_disambig1_nid_postfix(job.descriptor, job.buffer, job.bytes, job.offset); + else result = fsync_nid_postfix(job.descriptor); + job.status = result < 0 ? -1 : result; + job.error = result < 0 ? *__error_nid_postfix() : 0; +} + +void Worker() { + auto& aio = Instance(); + std::unique_lock lock(aio.lock); + for (;;) { + aio.changed.wait(lock, [&] { return !aio.held && !aio.queued.empty(); }); + auto job = aio.queued.front(); + aio.queued.pop_front(); + job->state = State::Running; + lock.unlock(); + Run(*job); + lock.lock(); + job->state = State::Done; + aio.done.push_back(job); + aio.changed.notify_all(); + } +} + +void Complete(Aio& aio, const std::shared_ptr<Job>& job, std::int64_t status, int error) { + job->status = status; + job->error = error; + job->state = State::Done; + aio.done.push_back(job); + aio.changed.notify_all(); +} + +std::optional<int> ReadyError(const std::shared_ptr<Job>& job) { + if (job->state != State::Done) return std::nullopt; + return job->error; +} + +std::int64_t Consume(Aio& aio, std::list<std::shared_ptr<Job>>::iterator found) { + const auto job = *found; + aio.done.erase(found); + aio.all.remove(job); + job->user->error = job->error; + job->user->status = job->status; + return job->error != 0 ? Fail(job->error) : job->status; +} + +std::optional<std::chrono::nanoseconds> WaitTime(const GuestTimespec& time) { + if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return std::nullopt; + const auto microseconds = time.tv_nsec / 1000; + const std::int64_t whole = microseconds * 1000; + const std::int64_t rounded = time.tv_sec == 0 && whole < TickNanoseconds ? TickNanoseconds : whole; + const auto seconds = std::min<std::int64_t>(time.tv_sec, std::chrono::duration_cast<std::chrono::seconds>(std::chrono::hours(24 * 365)).count()); + return std::chrono::seconds(seconds) + std::chrono::nanoseconds(rounded); +} + +int Queue(GuestAiocb* user, int opcode) { + if (user == nullptr) return Fail(GuestEfault); + auto& aio = Instance(); + std::unique_lock lock(aio.lock); + aio.used = true; + user->status = -1; + user->error = 0; + user->kernelinfo = reinterpret_cast<void*>(std::intptr_t{-1}); + const auto fail = [&](int error) { + user->error = error; + return Fail(error); + }; + if (aio.all.size() >= QueuePerProcess) return fail(GuestEagain); + const GuestAiocb request = *user; + if (request.nbytes > IoSizeMax) return Fail(GuestEinval); + const int notify = request.sigevent.notify; + if (notify != SigevKevent && notify != SigevSignal && notify != SigevThreadId && notify != SigevNone) return fail(GuestEinval); + if ((notify == SigevSignal || notify == SigevThreadId) && (request.sigevent.signo <= 0 || request.sigevent.signo > SignalMax)) + return Fail(GuestEinval); + const int flags = fcntl_nid_postfix(request.fildes, GetFlags); + if (flags < 0) return fail(GuestEbadf); + const int access = flags & AccessMode; + if (opcode == LioRead && access != 0 && access != ReadWrite) return fail(GuestEbadf); + if (opcode == LioWrite && access != WriteOnly && access != ReadWrite) return fail(GuestEbadf); + FileStat status{}; + if (_fstat_nid_postfix(request.fildes, &status) != 0) return fail(GuestEbadf); + const auto type = static_cast<std::uint16_t>(status.st_mode & TypeMask); + if (type == TypeSocket) NotImplemented_nid_no_patch("aio on a socket"); + const bool vnode = type != 0 && type != TypeFifo; + if (opcode == LioSync && !vnode) return fail(GuestEinval); + if (opcode != LioSync && request.offset < 0) return fail(GuestEinval); + user->kernelinfo = reinterpret_cast<void*>(aio.nextId++); + if (notify != SigevNone) NotImplemented_nid_no_patch("aio completion delivering a signal or a kevent"); + user->error = GuestEinprogress; + if (type != TypeRegular && type != TypeDirectory) return fail(GuestEopnotsupp); + auto job = std::make_shared<Job>(Job{user, request.fildes, opcode, request.offset, request.buf, request.nbytes}); + aio.all.push_back(job); + aio.queued.push_back(job); + if (!aio.started) { + aio.started = true; + std::thread(Worker).detach(); + } + aio.changed.notify_all(); + return 0; +} + +} + +extern "C" { + +void AioHoldWorkerForTest_nid_no_patch(bool hold) { + auto& aio = Instance(); + std::lock_guard lock(aio.lock); + aio.held = hold; + aio.changed.notify_all(); +} + +int APS5_VABI aio_read_nid_postfix(GuestAiocb* aiocb) { + return Queue(aiocb, LioRead); +} + +int APS5_VABI aio_write_nid_postfix(GuestAiocb* aiocb) { + return Queue(aiocb, LioWrite); +} + +int APS5_VABI aio_fsync_nid_postfix(int operation, GuestAiocb* aiocb) { + if (operation != GuestOSync) return Fail(GuestEinval); + return Queue(aiocb, LioSync); +} + +int APS5_VABI aio_error_nid_postfix(const GuestAiocb* aiocb) { + auto& aio = Instance(); + std::lock_guard lock(aio.lock); + if (!aio.used || aiocb == nullptr) return GuestEinval; + for (const auto& job : aio.all) { + if (job->user == aiocb) return ReadyError(job).value_or(GuestEinprogress); + } + if (aiocb->status == -1) return static_cast<int>(aiocb->error); + return GuestEinval; +} + +std::int64_t APS5_VABI aio_return_nid_postfix(GuestAiocb* aiocb) { + auto& aio = Instance(); + std::lock_guard lock(aio.lock); + if (!aio.used) return Fail(GuestEinval); + for (auto job = aio.done.begin(); job != aio.done.end(); ++job) { + if ((*job)->user == aiocb) return Consume(aio, job); + } + return Fail(GuestEinval); +} + +int APS5_VABI aio_suspend_nid_postfix(const GuestAiocb* const* list, int count, const GuestTimespec* timeout) { + if (count < 0 || count > static_cast<int>(QueuePerProcess)) return Fail(GuestEinval); + std::optional<std::chrono::nanoseconds> wait; + if (timeout != nullptr) { + wait = WaitTime(*timeout); + if (!wait) return Fail(GuestEinval); + } + auto& aio = Instance(); + std::unique_lock lock(aio.lock); + if (!aio.used) return Fail(GuestEagain); + if (count == 0) return 0; + for (;;) { + bool listed = false; + for (const auto& job : aio.all) { + for (int index = 0; index < count; ++index) { + if (job->user != list[index]) continue; + listed = true; + if (job->state == State::Done) return 0; + } + } + if (!listed) return 0; + if (!wait) aio.changed.wait(lock); + else if (aio.changed.wait_for(lock, *wait) == std::cv_status::timeout) return Fail(GuestEagain); + } +} + +std::int64_t APS5_VABI aio_waitcomplete_nid_postfix(GuestAiocb** result, const GuestTimespec* timeout) { + if (result != nullptr) *result = nullptr; + std::optional<std::chrono::nanoseconds> wait; + bool poll = false; + if (timeout != nullptr) { + poll = timeout->tv_sec == 0 && timeout->tv_nsec == 0; + if (!poll) { + wait = WaitTime(*timeout); + if (!wait) return Fail(GuestEinval); + } + } + auto& aio = Instance(); + std::unique_lock lock(aio.lock); + aio.used = true; + while (aio.done.empty()) { + if (poll) return Fail(GuestEagain); + if (!wait) aio.changed.wait(lock); + else if (aio.changed.wait_for(lock, *wait) == std::cv_status::timeout && aio.done.empty()) return Fail(GuestEagain); + } + if (result != nullptr) *result = aio.done.front()->user; + return Consume(aio, aio.done.begin()); +} + +int APS5_VABI aio_cancel_nid_postfix(int descriptor, GuestAiocb* aiocb) { + if (fcntl_nid_postfix(descriptor, GetFlags) < 0) return Fail(GuestEbadf); + auto& aio = Instance(); + std::lock_guard lock(aio.lock); + int cancelled = 0; + int notCancelled = 0; + for (const auto& job : std::list<std::shared_ptr<Job>>(aio.all)) { + if (job->state == State::Done || job->descriptor != descriptor || (aiocb != nullptr && job->user != aiocb)) continue; + if (job->state == State::Queued) { + std::erase(aio.queued, job); + Complete(aio, job, -1, GuestEcanceled); + ++cancelled; + } else { + ++notCancelled; + } + if (aiocb != nullptr) break; + } + if (aiocb != nullptr && cancelled != 0) return AioCanceled; + if (notCancelled != 0) return AioNotCanceled; + if (cancelled != 0) return AioCanceled; + return AioAllDone; +} + +} diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt index 6938d0c456..8aead464a0 100644 --- a/core/libs/prx/libkernel_sys/CMakeLists.txt +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -1,3 +1,3 @@ -add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp) +add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp Aio.cpp) target_include_directories(${LIB_NAME} PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(${LIB_NAME}) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..a9a06aaadf 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -3,46 +3,6 @@ extern "C" { -int APS5_VABI aio_cancel_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_error_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_fsync_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_read_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_return_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_suspend_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_waitcomplete_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI aio_write_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI amd64_set_fsbase_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestKernelSysAio.cpp b/core/libs/tests/GuestKernelSysAio.cpp new file mode 100644 index 0000000000..7eb23d13ad --- /dev/null +++ b/core/libs/tests/GuestKernelSysAio.cpp @@ -0,0 +1,237 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <chrono> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <filesystem> +#include <limits> +#include <source_location> +#include <string> + +namespace { + +struct Sigevent { + int notify; + int signo; + std::int64_t value; + std::int64_t extra[8]; +}; + +struct Aiocb { + int fildes; + std::int64_t offset; + void* buf; + std::uint64_t nbytes; + int spare[2]; + void* spare2; + int lioOpcode; + int reqprio; + std::int64_t status; + std::int64_t error; + void* kernelinfo; + Sigevent sigevent; +}; + +struct Timespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +} + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); +int APS5_VABI close_nid_postfix(int descriptor); +int APS5_VABI pipe_nid_postfix(int* descriptors); +int APS5_VABI aio_read_nid_postfix(Aiocb* aiocb); +int APS5_VABI aio_write_nid_postfix(Aiocb* aiocb); +int APS5_VABI aio_fsync_nid_postfix(int operation, Aiocb* aiocb); +int APS5_VABI aio_error_nid_postfix(const Aiocb* aiocb); +std::int64_t APS5_VABI aio_return_nid_postfix(Aiocb* aiocb); +int APS5_VABI aio_suspend_nid_postfix(const Aiocb* const* list, int count, const Timespec* timeout); +std::int64_t APS5_VABI aio_waitcomplete_nid_postfix(Aiocb** result, const Timespec* timeout); +int APS5_VABI aio_cancel_nid_postfix(int descriptor, Aiocb* aiocb); +void AioHoldWorkerForTest_nid_no_patch(bool hold); +} + +namespace { + +constexpr int Ebadf = 9; +constexpr int Efault = 14; +constexpr int Einval = 22; +constexpr int Eagain = 35; +constexpr int Einprogress = 36; +constexpr int Eopnotsupp = 45; +constexpr int Ecanceled = 85; +constexpr int ORdonly = 0; +constexpr int ORdwr = 2; +constexpr int OCreat = 0x200; +constexpr int OSync = 0x80; +constexpr int ODsync = 0x1000000; +constexpr int AioCanceled = 1; +constexpr int AioAllDone = 3; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast<unsigned>(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +Aiocb Request(int descriptor, void* buffer, std::uint64_t bytes, std::int64_t offset = 0) { + Aiocb request{}; + request.fildes = descriptor; + request.buf = buffer; + request.nbytes = bytes; + request.offset = offset; + return request; +} + +void Wait(Aiocb& request) { + const Aiocb* list[] = {&request}; + Require(aio_suspend_nid_postfix(list, 1, nullptr) == 0); +} + +void BeforeUse() { + Aiocb request{}; + const Aiocb* list[] = {&request}; + Require(aio_error_nid_postfix(&request) == Einval); + Require(aio_return_nid_postfix(&request) == -1 && Error() == Einval); + Require(aio_suspend_nid_postfix(list, 1, nullptr) == -1 && Error() == Eagain); +} + +void Validation(int file, int readOnly) { + char buffer[8] = {}; + Require(aio_read_nid_postfix(nullptr) == -1 && Error() == Efault); + + auto bad = Request(-1, buffer, sizeof(buffer)); + Require(aio_read_nid_postfix(&bad) == -1 && Error() == Ebadf); + Require(bad.status == -1 && bad.error == Ebadf && aio_error_nid_postfix(&bad) == Ebadf); + + auto readOnlyWrite = Request(readOnly, buffer, sizeof(buffer)); + Require(aio_write_nid_postfix(&readOnlyWrite) == -1 && Error() == Ebadf); + + auto negative = Request(file, buffer, sizeof(buffer), -1); + Require(aio_read_nid_postfix(&negative) == -1 && Error() == Einval && negative.error == Einval); + + auto huge = Request(file, buffer, std::uint64_t{std::numeric_limits<int>::max()} + 1); + Require(aio_read_nid_postfix(&huge) == -1 && Error() == Einval && huge.error == 0); + + auto notify = Request(file, buffer, sizeof(buffer)); + notify.sigevent.notify = 2; + Require(aio_read_nid_postfix(¬ify) == -1 && Error() == Einval && notify.error == Einval); + notify.sigevent = {1, 0, 0, {}}; + Require(aio_read_nid_postfix(¬ify) == -1 && Error() == Einval); + + int pipe[2] = {-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + auto piped = Request(pipe[0], buffer, sizeof(buffer)); + Require(aio_read_nid_postfix(&piped) == -1 && Error() == Eopnotsupp && piped.error == Eopnotsupp); + auto pipeSync = Request(pipe[1], nullptr, 0); + Require(aio_fsync_nid_postfix(OSync, &pipeSync) == -1 && Error() == Einval); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + + auto sync = Request(file, nullptr, 0); + Require(aio_fsync_nid_postfix(0, &sync) == -1 && Error() == Einval); + Require(aio_fsync_nid_postfix(ODsync, &sync) == -1 && Error() == Einval); +} + +void ReadAndWrite(int file) { + char text[] = "asynchronous"; + auto write = Request(file, text, 12, 4); + Require(aio_write_nid_postfix(&write) == 0); + Wait(write); + Require(aio_error_nid_postfix(&write) == 0); + Require(aio_return_nid_postfix(&write) == 12 && write.status == 12 && write.error == 0); + Require(aio_return_nid_postfix(&write) == -1 && Error() == Einval); + Require(aio_error_nid_postfix(&write) == Einval); + + char read[16] = {}; + auto request = Request(file, read, sizeof(read), 6); + Require(aio_read_nid_postfix(&request) == 0); + Wait(request); + Require(aio_return_nid_postfix(&request) == 10 && std::memcmp(read, "ynchronous", 10) == 0); + + auto empty = Request(file, read, 0, 0); + Require(aio_read_nid_postfix(&empty) == 0); + Wait(empty); + Require(aio_return_nid_postfix(&empty) == 0); + + const Aiocb* none[] = {&request}; + Require(aio_suspend_nid_postfix(none, 1, nullptr) == 0); + Require(aio_suspend_nid_postfix(none, 0, nullptr) == 0); + Require(aio_suspend_nid_postfix(none, -1, nullptr) == -1 && Error() == Einval); + Require(aio_suspend_nid_postfix(none, 257, nullptr) == -1 && Error() == Einval); + const Timespec badTime{0, 1000000000}; + Require(aio_suspend_nid_postfix(none, 1, &badTime) == -1 && Error() == Einval); +} + +void Cancel(int file) { + char buffer[4] = {}; + AioHoldWorkerForTest_nid_no_patch(true); + auto first = Request(file, buffer, sizeof(buffer)); + auto second = Request(file, buffer, sizeof(buffer)); + Require(aio_read_nid_postfix(&first) == 0 && aio_read_nid_postfix(&second) == 0); + Require(first.error == Einprogress && aio_error_nid_postfix(&first) == Einprogress); + const Aiocb* list[] = {&first}; + const Timespec brief{0, 5000000}; + Require(aio_suspend_nid_postfix(list, 1, &brief) == -1 && Error() == Eagain); + + Require(aio_cancel_nid_postfix(file, &first) == AioCanceled); + Require(aio_error_nid_postfix(&first) == Ecanceled && aio_error_nid_postfix(&second) == Einprogress); + Require(aio_suspend_nid_postfix(list, 1, &brief) == 0); + Require(aio_return_nid_postfix(&first) == -1 && Error() == Ecanceled); + Require(first.status == -1 && first.error == Ecanceled); + Require(aio_return_nid_postfix(&first) == -1 && Error() == Einval); + Require(aio_cancel_nid_postfix(file, &first) == AioAllDone); + + Require(aio_cancel_nid_postfix(file, nullptr) == AioCanceled); + Require(aio_return_nid_postfix(&second) == -1 && Error() == Ecanceled); + Require(aio_cancel_nid_postfix(file, nullptr) == AioAllDone); + Require(aio_cancel_nid_postfix(-1, nullptr) == -1 && Error() == Ebadf); + AioHoldWorkerForTest_nid_no_patch(false); +} + +void SyncOrder(int file) { + char text[] = "ordered"; + AioHoldWorkerForTest_nid_no_patch(true); + auto write = Request(file, text, 7, 0); + auto sync = Request(file, nullptr, 0); + Require(aio_write_nid_postfix(&write) == 0 && aio_fsync_nid_postfix(OSync, &sync) == 0); + Require(aio_error_nid_postfix(&sync) == Einprogress); + AioHoldWorkerForTest_nid_no_patch(false); + + Aiocb* done = nullptr; + Require(aio_waitcomplete_nid_postfix(&done, nullptr) == 7 && done == &write); + Require(aio_waitcomplete_nid_postfix(&done, nullptr) == 0 && done == &sync && sync.status == 0); + const Timespec zero{0, 0}; + Require(aio_waitcomplete_nid_postfix(&done, &zero) == -1 && Error() == Eagain && done == nullptr); + const Timespec brief{0, 5000000}; + Require(aio_waitcomplete_nid_postfix(&done, &brief) == -1 && Error() == Eagain); + const Timespec badTime{0, -1}; + Require(aio_waitcomplete_nid_postfix(&done, &badTime) == -1 && Error() == Einval); +} + +} + +int main() { + const auto path = "anyps5-kernel-sys-aio-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"; + BeforeUse(); + const int file = open_nid_postfix(path.c_str(), ORdwr | OCreat, 0600); + const int readOnly = open_nid_postfix(path.c_str(), ORdonly, 0); + Require(file >= 0 && readOnly >= 0); + Validation(file, readOnly); + ReadAndWrite(file); + Cancel(file); + SyncOrder(file); + Require(close_nid_postfix(readOnly) == 0 && close_nid_postfix(file) == 0); + Require(std::filesystem::remove(path)); + std::puts("guest_kernel_sys_aio passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..15239cf62b 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -11,6 +11,7 @@ - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified From f6ae37bb3f51952f56d3ccb834d0d874cf747b2b Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 22:19:52 +0300 Subject: [PATCH 0875/1360] fix(tools): handle errors cleanly and decode archive with utf-8-sig in test_web Wrap test_web entrypoint in main(argv=None) with UTF-8 stream reconfiguration using backslashreplace error handling. Check existence of the archive module before reading, decode with utf-8-sig to tolerate leading BOMs, validate the zip library URL match, and catch subprocess and network errors cleanly via parser.error. --- tools/test_web.py | 49 +++++++++++++++++++---------- tools/tests/test_web_release.py | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/tools/test_web.py b/tools/test_web.py index 245e6b4eac..9da700335f 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -5,6 +5,7 @@ import re import subprocess import sys import tempfile +import urllib.error import urllib.request ROOT = Path(__file__).resolve().parent.parent @@ -13,25 +14,39 @@ from test_guest_intel_trampolines import main_fixture from test_guest_module_directories import module_with_symbol -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) - args = parser.parse_args() - subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) - with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: - fixture = Path(directory) - (fixture / "main.elf").write_bytes(main_fixture()) - (fixture / "module.elf").write_bytes(module_with_symbol(True)) - source = (args.build / "web/archive.mjs").read_text(encoding="utf-8") - url = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source).group(1) - library = fixture / "zip.mjs" - with urllib.request.urlopen(url, timeout=30) as response: - library.write_bytes(response.read()) - environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + args = parser.parse_args(argv) + archive = args.build / "web/archive.mjs" + if not archive.is_file(): + parser.error(f"archive module does not exist: {archive}") + try: + subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) + with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: + fixture = Path(directory) + (fixture / "main.elf").write_bytes(main_fixture()) + (fixture / "module.elf").write_bytes(module_with_symbol(True)) + source = archive.read_text(encoding="utf-8-sig") + match = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source) + if not match: + parser.error(f"zip library URL not found in {archive}") + url = match.group(1) + library = fixture / "zip.mjs" + with urllib.request.urlopen(url, timeout=30) as response: + library.write_bytes(response.read()) + environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + except (subprocess.CalledProcessError, urllib.error.URLError, OSError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index 5fbc00dc65..e48e3fd3c7 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -1,3 +1,4 @@ +import contextlib import hashlib import io import json @@ -12,6 +13,7 @@ from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import build_web import package_release +import test_web FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") @@ -375,5 +377,58 @@ class WebPackageTests(unittest.TestCase): self.assertEqual(self.asset.read_bytes(), b"previous asset") +class TestWebRunnerTests(unittest.TestCase): + def test_main_rejects_missing_archive_file_without_traceback(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + build.mkdir() + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("archive module does not exist", err.getvalue()) + + def test_main_rejects_archive_missing_zip_url(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_text("const x = 1;\n", encoding="utf-8") + with patch("subprocess.run"): + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("zip library URL not found", err.getvalue()) + + def test_main_decodes_archive_with_utf8_bom(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_text('\ufefffrom "https://cdn.jsdelivr.net/example/zip.mjs";\n', encoding="utf-8") + with patch("subprocess.run"), \ + patch("urllib.request.urlopen") as mock_url: + mock_resp = mock_url.return_value.__enter__.return_value + mock_resp.read.return_value = b"zip-content" + result = test_web.main(["--build", str(build)]) + self.assertEqual(result, 0) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + out, err = Stream(), Stream() + with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): + with self.assertRaises(SystemExit): + test_web.main([]) + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From 76178efc322935d7820efc294b09ccc21a6eb3da Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:54:44 +0000 Subject: [PATCH 0876/1360] feat(libkernel_sys): implement sched_rr_get_interval --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 12 ++++++++++++ core/libs/tests/GuestSchedPriority.cpp | 15 ++++++++++++++- docs/dev/debt/libkernel_sys.md | 2 ++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 635db8b212..83e2b3312f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1159,7 +1159,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_sched_priority_tests ${LIBS_INCLUDE_DIR}/tests/GuestSchedPriority.cpp) target_include_directories(guest_sched_priority_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_sched_priority_tests PRIVATE libkernel libc) + target_link_libraries(guest_sched_priority_tests PRIVATE libkernel libkernel_sys libc) configure_windows_unwind(guest_sched_priority_tests) add_test(NAME guest_sched_priority COMMAND guest_sched_priority_tests) set_tests_properties(guest_sched_priority PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..26bc5f7765 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -2473,11 +2473,6 @@ int APS5_VABI sched_getscheduler_nid_postfix(void) { return 0; } -int APS5_VABI sched_rr_get_interval_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sched_setparam_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 82158449b6..e094365558 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -11,6 +11,7 @@ #include <string_view> #include <system_error> #include <sys/stat.h> +#include <ctime> #ifndef _WIN32 #include <cerrno> #include <fcntl.h> @@ -23,6 +24,7 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +extern "C" int APS5_VABI getpid_nid_postfix(void); #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -545,6 +547,16 @@ int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { return 0; } +int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, struct timespec* interval) { + constexpr int GuestEsrch = 3; + const int self = getpid_nid_postfix(); + if (pid != 0 && pid != self) return static_cast<int>(Fail(GuestEsrch)); + if (interval == nullptr) return static_cast<int>(Fail(GuestEfault)); + interval->tv_sec = 0; + interval->tv_nsec = 94000000; + return 0; +} + int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 5c175d9f37..59ff1c29e6 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -3,6 +3,7 @@ #include <cstdlib> #include <initializer_list> #include <stdexcept> +#include <ctime> extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { int APS5_VABI sched_get_priority_max_nid_postfix(int policy); @@ -16,6 +17,7 @@ struct GuestRtPrio { int APS5_VABI rtprio_thread_nid_postfix(int function, int lwpid, GuestRtPrio* priority); int APS5_VABI pthread_setconcurrency_nid_postfix(int newLevel); int APS5_VABI pthread_getconcurrency_nid_postfix(void); +int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, struct timespec* interval); int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } @@ -47,12 +49,23 @@ static void RequireSetError(int which, int who, int nice, int error) { } int main() { Require(Rejects(-1) && Rejects(2) && Rejects(5)); + const int self = getpid_nid_postfix(); + struct timespec interval{}; + *__error_nid_postfix() = 13; + Require(sched_rr_get_interval_nid_postfix(0, &interval) == 0 && interval.tv_sec == 0 && interval.tv_nsec == 94000000 && *__error_nid_postfix() == 13); + interval = {}; + Require(sched_rr_get_interval_nid_postfix(self, &interval) == 0 && interval.tv_sec == 0 && interval.tv_nsec == 94000000); + *__error_nid_postfix() = 0; + Require(sched_rr_get_interval_nid_postfix(-1, &interval) == -1 && *__error_nid_postfix() == 3); + *__error_nid_postfix() = 0; + Require(sched_rr_get_interval_nid_postfix(self + 1, &interval) == -1 && *__error_nid_postfix() == 3); + *__error_nid_postfix() = 0; + Require(sched_rr_get_interval_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == 14); for (int policy : {1, 3}) { Require(sched_get_priority_max_nid_postfix(policy) == 767); Require(sched_get_priority_min_nid_postfix(policy) == 256); } Require(Rejects(0) && Rejects(4)); - const int self = getpid_nid_postfix(); RequireNice(PrioProcess, 0); RequireNice(PrioProcess, self); RequireNice(PrioGroup, 0); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..11835ec5ad 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -5,6 +5,8 @@ ### Functional +- [sched_rr_get_interval](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) accepts pid 0 or the host-backed guest pid and reports the FreeBSD 11 ULE default time slice for the usual AMD64 clock rates (94 ms); other pids return `ESRCH`, and a null interval returns `EFAULT`. AnyPS5 does not run a guest ULE scheduler or track a per-process configured quantum, so this reports the modeled default and does not reflect runtime scheduling. Console-specific clock rates and quantum configuration are unverified + - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified From 20201779b849e403a7ca2268e3610daf26813b4c Mon Sep 17 00:00:00 2001 From: juanpimg <juanpimg@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:41:24 +0200 Subject: [PATCH 0877/1360] test(agc): register the recorder's flag-scoped runs with ctest --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4a6440642d..53579bd451 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1653,9 +1653,17 @@ if(BUILD_TESTING) ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_completion_labels COMMAND agc_driver_recorder_tests --completion-labels-only) + set_tests_properties(agc_recorder_completion_labels PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_cmask COMMAND agc_driver_recorder_tests --cmask-only) + set_tests_properties(agc_recorder_cmask PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_cube COMMAND agc_driver_recorder_tests --cube-only) + set_tests_properties(agc_recorder_cube PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + set_tests_properties(agc_recorder_completion_labels agc_recorder_cmask agc_recorder_cube PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") @@ -2043,9 +2051,20 @@ if(BUILD_TESTING) add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity) set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_store_idle_results_wake COMMAND agc_driver_store_idle_results_tests --idle-epoch-wake) + set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_stencil_ref_export_unused_keep COMMAND agc_driver_stencil_ref_export_unused_tests --case keep) + add_test(NAME agc_driver_stencil_ref_export_unused_invert COMMAND agc_driver_stencil_ref_export_unused_tests --case invert) + set_tests_properties(agc_driver_stencil_ref_export_unused_keep agc_driver_stencil_ref_export_unused_invert PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + set_tests_properties(agc_driver_stencil_ref_export_unused_keep agc_driver_stencil_ref_export_unused_invert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() endif() From 79a9a5f3c21e33ec48d22ca2178c1a7c16485237 Mon Sep 17 00:00:00 2001 From: juanpimg <juanpimg@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:18:09 +0200 Subject: [PATCH 0878/1360] fix(agc): drop the stencil reference registrations that repeat an existing run The agcTest loop already registers a no-argument ctest entry for every executable built from tests/execution/*.cpp, and the no-argument path of StencilRefExportUnused.cpp runs IndependentReference(false) and IndependentReference(true), which is what --case keep and --case invert run. A registration for either of them would only repeat an existing run. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 53579bd451..c17fd92f8e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2054,17 +2054,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_store_idle_results_wake COMMAND agc_driver_store_idle_results_tests --idle-epoch-wake) set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") - add_test(NAME agc_driver_stencil_ref_export_unused_keep COMMAND agc_driver_stencil_ref_export_unused_tests --case keep) - add_test(NAME agc_driver_stencil_ref_export_unused_invert COMMAND agc_driver_stencil_ref_export_unused_tests --case invert) - set_tests_properties(agc_driver_stencil_ref_export_unused_keep agc_driver_stencil_ref_export_unused_invert PROPERTIES - ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") - set_tests_properties(agc_driver_stencil_ref_export_unused_keep agc_driver_stencil_ref_export_unused_invert PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() endif() From 6de0666ce57a030e6b36729b894a7568cb2e9602 Mon Sep 17 00:00:00 2001 From: Johan David <johancarras16@gmail.com> Date: Mon, 19 Oct 2026 16:19:34 -0500 Subject: [PATCH 0879/1360] perf(agc): read a sampled texture's DCC keys once per lookup cachedTexture read the keys of one range up to three times in a lookup with nothing written in between: the ClearSingle check over a pending image dropped its scan, the staged-clear test scanned even without a pending image and kept nothing, and the final read scanned again. On Windows the key ranges sit in compared host imports, so no proof stands in for these scans; in PPSA31327's gameplay they took 12 % of the draw committer's samples (#8068). The lookup now keeps the keys it read until it does something that can change them: the storage source it looks up and may refresh, the pending source's Refresh, or a pending flush, which can publish unit shadows over the surface even when it flushes no image. The staged-clear test reads the keys only with a pending source. Which keys a lookup acts on is unchanged; only repeated reads with nothing between them are gone. DccKeyPresync checks that a dispatch's lookup with no key writer reads the keys at most once under the GPU mutex; main reads them twice. --- .../Graphics/src/ShaderResources.cpp | 18 +++++++++++----- .../tests/execution/DccKeyPresync.cpp | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d84cd4aa..43178ec182 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -720,11 +720,13 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + std::optional<DccKeys> scanned; const auto scanKeys = [&] { + if (scanned.has_value()) return *scanned; const auto scanStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const auto keys = TextureClearKeys(resource, guestBytes); + scanned = TextureClearKeys(resource, guestBytes); if (profile && resource.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, scanStart); - return keys; + return *scanned; }; auto& counters = TextureCounts(); const auto address = resource.baseAddress; @@ -780,6 +782,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s auto candidate = sampledStorageSource(context, resource, guestBytes); if (candidate != nullptr && candidate->Descriptor().dccAddress == resource.dccAddress) source = std::move(candidate); } + scanned.reset(); // The lookup's Refresh may have flushed another image's results over the memory (which // marks the keys uncompressed): a surface that stays a snapshot re-reads them behind that // flush, as it does behind its own. APS5_NO_KEYS_RESCAN=1 keeps the keys scanned above. @@ -795,15 +798,19 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) { // The CPU wrote the memory while results were pending: Refresh merges them the usual way. refreshForView(*source, resource); + scanned.reset(); } // Results stored to guest memory just now are stamped newer than `generation`; a snapshot read // after them is current at the generation of a second (memoized) collect. The store marks the // surface's DCC keys uncompressed, so the keys are read after it. const auto flushStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const auto observedKeys = keys.has_value() ? *keys : scanKeys(); - const bool stagedClear = source != nullptr && resource.dccAddress != 0 && (IsDccClear(observedKeys) || observedKeys == DccKeys::ClearSingle); + const bool stagedClear = source != nullptr && resource.dccAddress != 0 && [&] { + const auto observedKeys = keys.has_value() ? *keys : scanKeys(); + return IsDccClear(observedKeys) || observedKeys == DccKeys::ClearSingle; + }(); bool stagedFlushed = false; - if ((source == nullptr || stagedClear) && !clearThroughKeys && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) { + const bool flushAsked = (source == nullptr || stagedClear) && !clearThroughKeys; + if (flushAsked && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) { if (profile) LookupOutcomes::Add(LookupOutcomes::PendingFlush, flushStart); generation = GuestMemory::CollectWrites(address, bytes); if (stagedClear) { @@ -814,6 +821,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s } if (stagedFlushed) source.reset(); } + if (flushAsked) scanned.reset(); if (!keys.has_value()) keys = scanKeys(); if (disabled) { if (source != nullptr && depthCompare) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp index bed49aed8f..a749c607b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp @@ -234,6 +234,22 @@ void RunKnownKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { RequireSampledClear(scenario, "a dispatch behind a pending fill of the keys"); } +void RunOneKeyReadPerLookup(AgcDriver::VulkanDevice& device, const Scenario& scenario, const char* what) { + Reset(scenario); + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data())); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); + } + const auto reads = AgcDriver::Graphics::KeyWaitCounts().calls - waits.calls; + char message[192]; + std::snprintf(message, sizeof(message), "%s read the keys of its sampled texture %llu times under the GPU mutex with nothing written between the reads, expected at most once", what, static_cast<unsigned long long>(reads)); + Require(reads <= 1u, message); +} + void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); const auto before = AgcDriver::VulkanDevice::PresyncCounts(); @@ -273,6 +289,11 @@ int main() { RunLockedSync(*device, control); RunPresync(*device, changed); RunNoWriter(*device, changed); + GuestBlock single; + GuestBlock singleOutput; + const auto reads = Prepare(single.Data(), singleOutput.Data()); + RunOneKeyReadPerLookup(*device, reads, "a dispatch's first lookup"); + RunOneKeyReadPerLookup(*device, reads, "a dispatch's repeated lookup"); RunKnownKeys(*device, fills); std::puts("DCC key pre-sync tests passed"); return 0; From 2e7494bbab8311610b8cb8dc8afbada1e753a493 Mon Sep 17 00:00:00 2001 From: Johan David <johancarras16@gmail.com> Date: Wed, 21 Oct 2026 00:38:29 -0500 Subject: [PATCH 0880/1360] test(agc): count only the lookup's own key scans in DccKeyPresync RunOneKeyReadPerLookup compared the process-wide KeyWaitCounts().calls around a dispatch. The repeated lookup sometimes refreshes the surface's storage image, and its upload and ProvedKeys read the keys three more times on the same thread, so the test failed in about one run of four. cachedTexture now counts the key scans it makes on the calling thread (SampledLookupKeyScans()), and the test compares that count. --- .../libSceAgcDriver/Graphics/include/ShaderResources.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 7 +++++++ .../prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp | 8 ++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5321104605..cd2d3deb23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -146,6 +146,7 @@ struct ImageTableReuseCounts { std::uint64_t writesMade = 0; }; ImageTableReuseCounts ImageTableReuses(); +std::uint64_t SampledLookupKeyScans(); struct PresyncSurface { std::uint64_t address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 43178ec182..9394e9cc41 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -394,6 +394,8 @@ TextureCounters& TextureCounts() { return counters; } +thread_local std::uint64_t sampledLookupKeyScans = 0; + void reportTextureCounters() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -725,6 +727,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s if (scanned.has_value()) return *scanned; const auto scanStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; scanned = TextureClearKeys(resource, guestBytes); + if (resource.dccAddress != 0) ++sampledLookupKeyScans; if (profile && resource.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, scanStart); return *scanned; }; @@ -5099,6 +5102,10 @@ void ShaderResources::resolveImageTables() { reportTextureCounters(); } +std::uint64_t SampledLookupKeyScans() { + return sampledLookupKeyScans; +} + ImageTableReuseCounts ImageTableReuses() { const auto& counters = TextureCounts(); return {counters.tablesReused.load(std::memory_order_relaxed) + counters.tablesReplayed.load(std::memory_order_relaxed), counters.tablesResolved.load(std::memory_order_relaxed), counters.tablesReplayed.load(std::memory_order_relaxed), counters.tableWritesKept.load(std::memory_order_relaxed), counters.tableWritesMade.load(std::memory_order_relaxed)}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp index a749c607b1..6fcb25ce6f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp @@ -238,16 +238,16 @@ void RunOneKeyReadPerLookup(AgcDriver::VulkanDevice& device, const Scenario& sce Reset(scenario); const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data())); - const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + const auto before = AgcDriver::Graphics::SampledLookupKeyScans(); { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()), std::move(prepared)); device.WaitIdle(); } - const auto reads = AgcDriver::Graphics::KeyWaitCounts().calls - waits.calls; + const auto scans = AgcDriver::Graphics::SampledLookupKeyScans() - before; char message[192]; - std::snprintf(message, sizeof(message), "%s read the keys of its sampled texture %llu times under the GPU mutex with nothing written between the reads, expected at most once", what, static_cast<unsigned long long>(reads)); - Require(reads <= 1u, message); + std::snprintf(message, sizeof(message), "%s scanned the keys of its sampled texture %llu times with nothing written between the scans, expected at most once", what, static_cast<unsigned long long>(scans)); + Require(scans <= 1u, message); } void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { From e3c8b51ac287d89aae11cfded9eb0815c7009600 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Thu, 22 Oct 2026 00:32:57 +0400 Subject: [PATCH 0881/1360] docs(debt): list the GPUs the rect-list barycentric test passed on --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e93aa7a9e1..def0a04455 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -96,7 +96,7 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages, checked on MoltenVK 1.4.2 (Apple M2) only. Tessellation and mesh pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages; its test passes on MoltenVK 1.4.2 (Apple M2), RADV (Radeon RX 7900 XT, Mesa 25.2.8), lavapipe, and on Windows on an RTX 3080 (NVIDIA 617.42) and Radeon RDNA 2 iGPUs (AMD 32.0.21045 and 32.0.21030); no title was run through it except Ruffle PPSA68091 on the M2. Tessellation and mesh pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. From a96cfda9e6a6d0c7185ba0992d8e9640963931ab Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Thu, 22 Oct 2026 00:36:08 +0400 Subject: [PATCH 0882/1360] test(agc): check flat inputs of a triangle strip with the last provoking vertex --- .../execution/PixelBarycentricEmulation.cpp | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index f937947739..f364c83039 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -353,6 +353,26 @@ void CheckStrip(AgcDriver::VulkanDevice& device, Emulation emulation, const std: std::printf("barycentric emulation through the %s passed for a triangle strip: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); } +void CheckStripFlatLast(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + Draw(device, emulation, FlatBarycentricPixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, true); + const auto positions = StripPositions(); + std::array<std::uint32_t, 2> checked{}; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto triangle = expected.order[0]; + const auto provoking = triangle == 0u ? 2u : 3u; + for (std::uint32_t channel = 2; channel < 4; ++channel) { + Require(Pixel(x, y, channel) == positions[provoking][0], name + " flat last-provoking strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") flat channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected vertex " + std::to_string(provoking) + " value " + std::to_string(positions[provoking][0])); + } + ++checked[triangle]; + } + } + Require(checked[0] > 200u && checked[1] > 200u, name + " flat last-provoking strip: too few pixels of each triangle were checked"); + std::printf("flat last-provoking strip through %s passed: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); +} + void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); @@ -376,6 +396,7 @@ void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { if (emulation == Emulation::Tessellation) { Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT); CheckFlat(name + " flat input, last provoking vertex", 2u); + CheckStripFlatLast(device, emulation, name); } CheckStrip(device, emulation, name); std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling, flat inputs and triangle strips\n", name.c_str(), smooth, noPerspective); From 428e9cb5f3e52f6756521065ca70b9266fd736ab Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:45:04 -0700 Subject: [PATCH 0883/1360] fix(libc): size the num_get<char> vtable to the console's 17 slots mazen320 read KfcTPbeaOqg from the console's libSceLibcInternal.sprx symbol table: an object of 136 bytes, 17 pointer slots. Export that size instead of the 15 derived from Dinkumware's crt.vc12 xlocnum, and say in the debt entry that which two virtual functions are added is not known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/LocaleSupport.cpp | 2 +- core/libs/tests/GuestLocale.cpp | 2 +- docs/dev/debt/libc.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index f50ca75eea..fb1f8042ab 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -299,7 +299,7 @@ std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {}; std::uint64_t _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[12] {}; std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; -std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[15] {}; +std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17] {}; std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index ae8292b410..78aaee27be 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -36,7 +36,7 @@ std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t d int* APS5_VABI __error_nid_postfix(); extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; -extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[15]; +extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17]; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2910431c59..2827dc2170 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -134,4 +134,4 @@ - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered -- [num_get<char>::id, time_get<char>::id and the num_get<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 15 zeroed slots (two header entries, two destructor entries and Dinkumware's eleven `do_get`), so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. +- [num_get<char>::id, time_get<char>::id and the num_get<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal (two more than the two header entries, two destructor entries and eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`; which two virtual functions the console's adds is not known), so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. From baf01b724eb3a285980fbbb74c7b8cb10d80827c Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:01:13 +0300 Subject: [PATCH 0884/1360] perf(shader): mark GetSrtResource and ReadConst as recomputable in RVE --- .../src/RedundantValueEliminator.cpp | 79 +++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp index 6e8c4e269d..5244bf70f9 100644 --- a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -122,12 +122,21 @@ bool Recomputable(IrOpcode opcode) { case IrOpcode::FPTrunc32: case IrOpcode::FPFract32: case IrOpcode::FPRoundEven32: + case IrOpcode::GetSrtResource: + case IrOpcode::ReadConst: return true; default: return false; } } +bool ClobbersReadConst(IrOpcode opcode) { + const auto buffer = BufferAccessOf(opcode); + if (buffer == BufferAccess::Write || buffer == BufferAccess::Atomic) return true; + const auto address = AddressOpcodeInfoOf(opcode).access; + return address == AddressAccess::Write || address == AddressAccess::Atomic || opcode == IrOpcode::Barrier; +} + struct Operand { const IrValue* value; IrType type; @@ -145,11 +154,12 @@ struct ValueKey { IrOpcode opcode; IrType type; std::uint64_t flags; + std::uint64_t memoryEpoch = 0; std::array<const IrValue*, 4> operands{}; std::size_t operandCount = 0; bool operator==(const ValueKey& other) const { - if (opcode != other.opcode || type != other.type || flags != other.flags || operandCount != other.operandCount) return false; + if (opcode != other.opcode || type != other.type || flags != other.flags || memoryEpoch != other.memoryEpoch || operandCount != other.operandCount) return false; for (std::size_t index = 0; index < operandCount; ++index) { if (operands[index] != other.operands[index] && OperandOf(operands[index]) != OperandOf(other.operands[index])) return false; } @@ -159,7 +169,7 @@ struct ValueKey { struct ValueKeyHash { std::size_t operator()(const ValueKey& key) const { - std::size_t hash = static_cast<std::size_t>(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast<std::size_t>(key.type) ^ std::hash<std::uint64_t>{}(key.flags) * 31u; + std::size_t hash = static_cast<std::size_t>(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast<std::size_t>(key.type) ^ std::hash<std::uint64_t>{}(key.flags) * 31u ^ std::hash<std::uint64_t>{}(key.memoryEpoch) * 131u; for (const auto* argument : std::span(key.operands).first(key.operandCount)) { const auto operand = OperandOf(argument); hash = hash * 1099511628211ull ^ std::hash<const void*>{}(operand.value) ^ std::hash<std::uint64_t>{}(operand.immediate) * 7u ^ static_cast<std::size_t>(operand.type); @@ -222,13 +232,61 @@ std::vector<std::vector<IrBlock*>> DominatorChildren(const std::vector<IrBlock*> RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& program) const { RedundantValueEliminationStats stats; + if (program.BlockOrder().empty()) return stats; + const auto order = ReversePostOrder(*program.BlockOrder().front()); + std::unordered_map<const IrBlock*, std::size_t> index; + const auto children = DominatorChildren(order, index); + for (const auto* block : program.BlockOrder()) { + if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); + } + std::uint64_t nextClobberEpoch = order.size() + 2u; + std::vector<std::uint64_t> firstClobber(order.size(), 0u); + std::vector<std::uint64_t> lastClobber(order.size(), 0u); + for (std::size_t position = 0; position < order.size(); ++position) { + firstClobber[position] = nextClobberEpoch; + for (const auto* inst : order[position]->Instructions()) { + if (ClobbersReadConst(inst->Opcode())) lastClobber[position] = nextClobberEpoch++; + } + } + std::vector<std::uint64_t> entryEpoch(order.size(), 0u); + const auto exitEpoch = [&](std::size_t position) -> std::uint64_t { + if (entryEpoch[position] == 0u) return 0u; + return lastClobber[position] != 0u ? lastClobber[position] : entryEpoch[position]; + }; + for (bool changed = true; changed;) { + changed = false; + for (std::size_t position = 0; position < order.size(); ++position) { + std::uint64_t candidate = position == 0u ? 1u : 0u; + for (const IrBlock* predecessor : order[position]->Predecessors()) { + const auto found = index.find(predecessor); + if (found == index.end()) continue; + const auto incoming = exitEpoch(found->second); + if (incoming == 0u) continue; + if (candidate == 0u) { + candidate = incoming; + } else if (candidate != incoming) { + candidate = position + 2u; + } + } + if (candidate != entryEpoch[position]) { + entryEpoch[position] = candidate; + changed = true; + } + } + } std::unordered_map<ValueKey, IrValue*, ValueKeyHash> available; std::vector<const ValueKey*> added; std::unordered_map<const IrValue*, IrValue*> replacements; - const auto visit = [&](IrBlock& block) { - for (auto* inst : block.Instructions()) { + const auto visit = [&](std::size_t position) { + std::uint64_t epoch = entryEpoch[position]; + std::uint64_t nextClobber = firstClobber[position]; + for (auto* inst : order[position]->Instructions()) { + if (ClobbersReadConst(inst->Opcode())) { + epoch = nextClobber++; + continue; + } if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; - ValueKey key{inst->Opcode(), inst->Type(), inst->Flags<std::uint64_t>()}; + ValueKey key{inst->Opcode(), inst->Type(), inst->Flags<std::uint64_t>(), inst->Opcode() == IrOpcode::ReadConst ? epoch : 0u}; key.operandCount = inst->ArgumentCount(); if (key.operandCount > key.operands.size()) continue; for (std::size_t index = 0; index < key.operandCount; ++index) key.operands[index] = inst->Argument(index)->Resolve(); @@ -242,22 +300,15 @@ RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& pr ++stats.replacedValues; } }; - if (program.BlockOrder().empty()) return stats; - const auto order = ReversePostOrder(*program.BlockOrder().front()); - std::unordered_map<const IrBlock*, std::size_t> index; - const auto children = DominatorChildren(order, index); - for (const auto* block : program.BlockOrder()) { - if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); - } std::vector<std::pair<std::size_t, std::size_t>> stack{{0u, 0u}}; std::vector<std::size_t> scopes{added.size()}; - visit(*order[0]); + visit(0u); while (!stack.empty()) { auto& [position, next] = stack.back(); if (next < children[position].size()) { const auto child = index.at(children[position][next++]); scopes.push_back(added.size()); - visit(*order[child]); + visit(child); stack.emplace_back(child, 0u); continue; } From f2a0dc0b746b825159fc10ddf58592b65e099e0e Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:01:19 +0300 Subject: [PATCH 0885/1360] test(shader): cover GetSrtResource and ReadConst deduplication in RVE --- .../tests/RedundantValueElimination.cpp | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index a17b7cf87d..cc88c42cfe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -324,6 +324,149 @@ int main() { return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged}); }); + passed &= run("GetSrtResource and ReadConst with the same operands merge in a dominated block, while another slot or a sibling block stays", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& srtHead = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(0u)}); + auto& readHead = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtHead, &b.constant(2u)}); + b.keep(readHead); + b.block = &then; + auto& srtDominated = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(0u)}); + auto& readDominated = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtDominated, &b.constant(2u)}); + auto& readOtherSlot = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtDominated, &b.constant(3u)}); + auto& srtThenOnly = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(1u)}); + auto& readThenOnly = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtThenOnly, &b.constant(4u)}); + b.keep(readDominated); + b.keep(readOtherSlot); + b.keep(readThenOnly); + b.block = &otherwise; + auto& srtSibling = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(1u)}); + auto& readSibling = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtSibling, &b.constant(4u)}); + b.keep(readSibling); + b.block = &merge; + auto& srtMerge = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(0u)}); + auto& readMerge = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtMerge, &b.constant(2u)}); + b.keep(readMerge); + return b.eliminate() == 4u && !srtDominated.HasUses() && !readDominated.HasUses() && !srtMerge.HasUses() && !readMerge.HasUses() && srtHead.UseCount() == 4u && readHead.UseCount() == 3u && AllUsed({&readOtherSlot, &srtThenOnly, &readThenOnly, &srtSibling, &readSibling}); + }); + + passed &= run("two ReadConst instructions of the same slot separated by a store to the table address stay", [] { + Builder b; + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& first = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + static_cast<void>(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + auto& second = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("a store on one branch keeps ReadConst in the merge block while the clean sibling branch reuses the head read", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& readHead = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readHead); + b.block = &then; + static_cast<void>(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + auto& readThenAfterStore = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readThenAfterStore); + b.block = &otherwise; + auto& readOtherwise = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readOtherwise); + b.block = &merge; + auto& readMerge = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readMerge); + return b.eliminate() == 1u && !readOtherwise.HasUses() && readHead.UseCount() == 2u && AllUsed({&readHead, &readThenAfterStore, &readMerge}); + }); + + passed &= run("a store on a loop backedge prevents reusing a pre-loop ReadConst in the loop header", [] { + Builder b; + IrBlock& header = b.newBlock(); + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&header); + header.AddBranch(&body); + header.AddBranch(&exit); + body.AddBranch(&header); + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& beforeLoop = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(beforeLoop); + b.block = &header; + auto& inHeader = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inHeader); + b.block = &body; + auto& inBodyBeforeStore = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + static_cast<void>(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + auto& inBodyAfterStore = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inBodyBeforeStore); + b.keep(inBodyAfterStore); + b.block = &exit; + auto& afterLoop = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(afterLoop); + return b.eliminate() == 2u && !inBodyBeforeStore.HasUses() && !afterLoop.HasUses() && beforeLoop.UseCount() == 1u && inHeader.UseCount() == 3u && AllUsed({&beforeLoop, &inHeader, &inBodyAfterStore}); + }); + + passed &= run("a store in an inner loop propagates to the outer loop header across fixed-point iterations", [] { + Builder b; + IrBlock& outerHeader = b.newBlock(); + IrBlock& innerHeader = b.newBlock(); + IrBlock& innerBody = b.newBlock(); + IrBlock& outerLatch = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&outerHeader); + outerHeader.AddBranch(&innerHeader); + outerHeader.AddBranch(&exit); + innerHeader.AddBranch(&innerBody); + innerHeader.AddBranch(&outerLatch); + innerBody.AddBranch(&innerHeader); + outerLatch.AddBranch(&outerHeader); + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& beforeOuter = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(beforeOuter); + b.block = &outerHeader; + auto& inOuterHeader = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inOuterHeader); + b.block = &innerHeader; + auto& inInnerHeader = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inInnerHeader); + b.block = &innerBody; + static_cast<void>(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + b.block = &outerLatch; + auto& inOuterLatch = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inOuterLatch); + b.block = &exit; + auto& inExit = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inExit); + return b.eliminate() == 2u && !inOuterLatch.HasUses() && !inExit.HasUses() && beforeOuter.UseCount() == 1u && inOuterHeader.UseCount() == 2u && inInnerHeader.UseCount() == 2u && AllUsed({&beforeOuter, &inOuterHeader, &inInnerHeader}); + }); + passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] { Builder b; IrBlock& header = b.newBlock(); From 7820a5353a09607a252c32a1e20520ad392d889f Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Wed, 21 Oct 2026 22:22:16 +0100 Subject: [PATCH 0886/1360] refactor(libkernel): move the ktimer calls into libkernel The PS5 3.20 export lists have ktimer_* in libkernel as well as libkernel_sys, and on Windows a libkernel import never reaches libkernel_sys. The implementation moves unchanged to libkernel/Time/Ktimer.cpp. --- core/libs/prx/libkernel/CMakeLists.txt | 1 + core/libs/prx/libkernel/Time/Ktimer.cpp | 165 ++++++++++++++++++++++++ core/libs/prx/libkernel_sys/Export.cpp | 152 ---------------------- docs/dev/debt/libkernel.md | 1 + docs/dev/debt/libkernel_sys.md | 1 - 5 files changed, 167 insertions(+), 153 deletions(-) create mode 100644 core/libs/prx/libkernel/Time/Ktimer.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..2785bbe6e5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -74,6 +74,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/Ktimer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp diff --git a/core/libs/prx/libkernel/Time/Ktimer.cpp b/core/libs/prx/libkernel/Time/Ktimer.cpp new file mode 100644 index 0000000000..6dd8b16800 --- /dev/null +++ b/core/libs/prx/libkernel/Time/Ktimer.cpp @@ -0,0 +1,165 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include <cstdint> +#include <mutex> + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp); + +namespace { + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEagain = 35; + +std::int64_t Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +constexpr int ClockRealtime = 0; +constexpr int ClockMonotonic = 4; +constexpr int SigevNone = 0; +constexpr int SigevSignal = 1; +constexpr int SigevThreadId = 4; +constexpr int SignalMax = 128; +constexpr int TimerAbstime = 1; +constexpr int FirstTimer = 3; +constexpr int TimerMax = 32; +constexpr long long NanosecondsPerSecond = 1000000000; +constexpr long long TickNanoseconds = 1000000; + +struct GuestSigevent { + int notify; + int signo; +}; + +struct GuestTimespec { + std::int64_t tv_sec; + std::int64_t tv_nsec; +}; + +struct GuestItimerspec { + GuestTimespec it_interval; + GuestTimespec it_value; +}; + +using Nanoseconds = __int128; + +struct Ktimer { + bool live = false; + int clock = ClockRealtime; + Nanoseconds value = 0; + Nanoseconds interval = 0; +}; + +std::mutex g_ktimerLock; +Ktimer g_ktimers[TimerMax]; + +Nanoseconds ToNanoseconds(const GuestTimespec& time) { + return static_cast<Nanoseconds>(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec; +} + +GuestTimespec ToTimespec(Nanoseconds time) { + return {static_cast<std::int64_t>(time / NanosecondsPerSecond), + static_cast<std::int64_t>(time % NanosecondsPerSecond)}; +} + +Nanoseconds ClockNow(int clock) { + GuestTimespec now{}; + clock_gettime_nid_postfix(clock, &now); + return ToNanoseconds(now); +} + +bool FixTimespec(GuestTimespec& time) { + if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false; + if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds; + return true; +} + +Ktimer* FindTimer(int id) { + if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr; + return &g_ktimers[id]; +} + +void Expire(Ktimer& timer, Nanoseconds now) { + if (timer.value == 0 || now < timer.value) return; + if (timer.interval == 0) { + timer.value = 0; + return; + } + timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval; +} + +GuestItimerspec Remaining(Ktimer& timer) { + const Nanoseconds now = ClockNow(timer.clock); + Expire(timer, now); + GuestItimerspec result{ToTimespec(timer.interval), {}}; + if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1}; + return result; +} + +} + +extern "C" { + +int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) { + if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast<int>(Fail(GuestEinval)); + if (event != nullptr) { + if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId) + return static_cast<int>(Fail(GuestEinval)); + if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax)) + return static_cast<int>(Fail(GuestEinval)); + } + std::lock_guard lock(g_ktimerLock); + int id = FirstTimer; + while (id < TimerMax && g_ktimers[id].live) ++id; + if (id == TimerMax) return static_cast<int>(Fail(GuestEagain)); + if (timerid == nullptr) return static_cast<int>(Fail(GuestEfault)); + if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal"); + g_ktimers[id] = {true, clock, 0, 0}; + *timerid = id; + return 0; +} + +int APS5_VABI ktimer_delete_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); + *timer = {}; + return 0; +} + +int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { + if (value == nullptr) return static_cast<int>(Fail(GuestEfault)); + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); + GuestItimerspec next = *value; + if (!FixTimespec(next.it_value)) return static_cast<int>(Fail(GuestEinval)); + const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0; + if (armed && !FixTimespec(next.it_interval)) return static_cast<int>(Fail(GuestEinval)); + if (!armed) next.it_interval = {}; + if (oldValue != nullptr) *oldValue = Remaining(*timer); + timer->interval = ToNanoseconds(next.it_interval); + timer->value = ToNanoseconds(next.it_value); + if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock); + return 0; +} + +int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); + if (value == nullptr) return static_cast<int>(Fail(GuestEfault)); + *value = Remaining(*timer); + return 0; +} + +int APS5_VABI ktimer_getoverrun_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + if (FindTimer(id) == nullptr) return static_cast<int>(Fail(GuestEinval)); + return 0; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 0ac387a960..9c7d6fc2b6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -6,7 +6,6 @@ #include <cstdlib> #include <filesystem> #include <limits> -#include <mutex> #include <stdexcept> #include <string> #include <string_view> @@ -692,154 +691,3 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const } } - -extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp); - -namespace { - -constexpr int ClockRealtime = 0; -constexpr int ClockMonotonic = 4; -constexpr int SigevNone = 0; -constexpr int SigevSignal = 1; -constexpr int SigevThreadId = 4; -constexpr int SignalMax = 128; -constexpr int TimerAbstime = 1; -constexpr int FirstTimer = 3; -constexpr int TimerMax = 32; -constexpr long long NanosecondsPerSecond = 1000000000; -constexpr long long TickNanoseconds = 1000000; - -struct GuestSigevent { - int notify; - int signo; -}; - -struct GuestTimespec { - std::int64_t tv_sec; - std::int64_t tv_nsec; -}; - -struct GuestItimerspec { - GuestTimespec it_interval; - GuestTimespec it_value; -}; - -using Nanoseconds = __int128; - -struct Ktimer { - bool live = false; - int clock = ClockRealtime; - Nanoseconds value = 0; - Nanoseconds interval = 0; -}; - -std::mutex g_ktimerLock; -Ktimer g_ktimers[TimerMax]; - -Nanoseconds ToNanoseconds(const GuestTimespec& time) { - return static_cast<Nanoseconds>(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec; -} - -GuestTimespec ToTimespec(Nanoseconds time) { - return {static_cast<std::int64_t>(time / NanosecondsPerSecond), - static_cast<std::int64_t>(time % NanosecondsPerSecond)}; -} - -Nanoseconds ClockNow(int clock) { - GuestTimespec now{}; - clock_gettime_nid_postfix(clock, &now); - return ToNanoseconds(now); -} - -bool FixTimespec(GuestTimespec& time) { - if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false; - if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds; - return true; -} - -Ktimer* FindTimer(int id) { - if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr; - return &g_ktimers[id]; -} - -void Expire(Ktimer& timer, Nanoseconds now) { - if (timer.value == 0 || now < timer.value) return; - if (timer.interval == 0) { - timer.value = 0; - return; - } - timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval; -} - -GuestItimerspec Remaining(Ktimer& timer) { - const Nanoseconds now = ClockNow(timer.clock); - Expire(timer, now); - GuestItimerspec result{ToTimespec(timer.interval), {}}; - if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1}; - return result; -} - -} - -extern "C" { - -int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) { - if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast<int>(Fail(GuestEinval)); - if (event != nullptr) { - if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId) - return static_cast<int>(Fail(GuestEinval)); - if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax)) - return static_cast<int>(Fail(GuestEinval)); - } - std::lock_guard lock(g_ktimerLock); - int id = FirstTimer; - while (id < TimerMax && g_ktimers[id].live) ++id; - if (id == TimerMax) return static_cast<int>(Fail(GuestEagain)); - if (timerid == nullptr) return static_cast<int>(Fail(GuestEfault)); - if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal"); - g_ktimers[id] = {true, clock, 0, 0}; - *timerid = id; - return 0; -} - -int APS5_VABI ktimer_delete_nid_postfix(int id) { - std::lock_guard lock(g_ktimerLock); - Ktimer* timer = FindTimer(id); - if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); - *timer = {}; - return 0; -} - -int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { - if (value == nullptr) return static_cast<int>(Fail(GuestEfault)); - std::lock_guard lock(g_ktimerLock); - Ktimer* timer = FindTimer(id); - if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); - GuestItimerspec next = *value; - if (!FixTimespec(next.it_value)) return static_cast<int>(Fail(GuestEinval)); - const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0; - if (armed && !FixTimespec(next.it_interval)) return static_cast<int>(Fail(GuestEinval)); - if (!armed) next.it_interval = {}; - if (oldValue != nullptr) *oldValue = Remaining(*timer); - timer->interval = ToNanoseconds(next.it_interval); - timer->value = ToNanoseconds(next.it_value); - if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock); - return 0; -} - -int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) { - std::lock_guard lock(g_ktimerLock); - Ktimer* timer = FindTimer(id); - if (timer == nullptr) return static_cast<int>(Fail(GuestEinval)); - if (value == nullptr) return static_cast<int>(Fail(GuestEfault)); - *value = Remaining(*timer); - return 0; -} - -int APS5_VABI ktimer_getoverrun_nid_postfix(int id) { - std::lock_guard lock(g_ktimerLock); - if (FindTimer(id) == nullptr) return static_cast<int>(Fail(GuestEinval)); - return 0; -} - -} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..e5afde5a0a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -13,6 +13,7 @@ ### Unknown function info - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. +- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 1bceba3297..25499ae3d1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -7,7 +7,6 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified -- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified From 642c0d56ac9f7112ee60f56c7a055c3b577ee362 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Wed, 21 Oct 2026 22:27:24 +0100 Subject: [PATCH 0887/1360] docs(debt): list the ktimer calls under Functional --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e5afde5a0a..f970d5bfb2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -13,7 +13,6 @@ ### Unknown function info - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. -- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. @@ -147,6 +146,7 @@ - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store +- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `/<sandbox word>/common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them From 375b53d91851689681101f951ce4ee7423d09326 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Wed, 21 Oct 2026 22:30:43 +0100 Subject: [PATCH 0888/1360] refactor(libkernel): move pselect into libkernel The PS5 3.20 export lists have pselect in libkernel as well as libkernel_sys, and on Windows a libkernel import never reaches libkernel_sys. The implementation moves unchanged to libkernel/Socket/src/Pselect.cpp, beside select. --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libs/prx/libkernel/Socket/src/Pselect.cpp | 33 +++++++++++++++++++ .../prx/libkernel_sys/Export.Discovery.cpp | 27 --------------- docs/dev/debt/libkernel.md | 1 + docs/dev/debt/libkernel_sys.md | 1 - 5 files changed, 35 insertions(+), 28 deletions(-) create mode 100644 core/libs/prx/libkernel/Socket/src/Pselect.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..338210fb77 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Pselect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp diff --git a/core/libs/prx/libkernel/Socket/src/Pselect.cpp b/core/libs/prx/libkernel/Socket/src/Pselect.cpp new file mode 100644 index 0000000000..931cc83a54 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/Pselect.cpp @@ -0,0 +1,33 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> + +extern "C" { + +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); + +int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { + constexpr int SetMask = 3; + struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; + } limit{}; + const Timeval* wait = nullptr; + if (timeout != nullptr) { + const auto* spec = static_cast<const std::int64_t*>(timeout); + limit = {spec[0], spec[1] / 1000}; + wait = &limit; + } + std::uint32_t previous[4]{}; + if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + if (sigmask != nullptr) { + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, previous, nullptr); + *__error_nid_postfix() = error; + } + return result; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index dcf727762f..2ebe4db765 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -353,33 +353,6 @@ int APS5_VABI profil_nid_postfix(void) { return 0; } -int* APS5_VABI __error_nid_postfix(); -int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); -int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); - -int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { - constexpr int SetMask = 3; - struct Timeval { - std::int64_t seconds; - std::int64_t microseconds; - } limit{}; - const Timeval* wait = nullptr; - if (timeout != nullptr) { - const auto* spec = static_cast<const std::int64_t*>(timeout); - limit = {spec[0], spec[1] / 1000}; - wait = &limit; - } - std::uint32_t previous[4]{}; - if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; - const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); - if (sigmask != nullptr) { - const int error = *__error_nid_postfix(); - sigprocmask_nid_postfix(SetMask, previous, nullptr); - *__error_nid_postfix() = error; - } - return result; -} - int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..65c312cd2b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -147,6 +147,7 @@ - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `/<sandbox word>/common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified +- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index ff16c7e9af..25499ae3d1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -14,7 +14,6 @@ - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified -- [pselect](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) From f04d16d702a1a0f902fe79cf4ba4bf101b6c0fae Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:05:38 +0300 Subject: [PATCH 0889/1360] feat: PS4 NpTrophy, save data v1, NpWebApi offline and Limbo's other imports NpTrophy reads the trophy count and icons from the title's trophy package (names are encrypted and stay not implemented), PS4 v1 save data calls wrap the native v2/v3 ones, and NpWebApi library contexts, handles and push event filters work offline (service filters fail without a signed-in user). libSceAjm is built from the libSceAjm.native sources for PS4 titles. Limbo's (CUSA01664) remaining PS4 imports, including the synchronous PS4 TUS calls and the Facebook dialog calls next to the existing libraries, are exported as not implemented. --- .../prx/libSceAjm.native/AjmLibrary.cmake | 15 + core/libs/prx/libSceAjm.native/CMakeLists.txt | 14 +- core/libs/prx/libSceAjm/CMakeLists.txt | 2 + core/libs/prx/libSceAjm/Export.cpp | 15 +- .../prx/libSceGameCustomDataDialog/Export.cpp | 31 ++ .../prx/libSceInvitationDialog/Export.cpp | 31 ++ .../prx/libSceNpCommon/PendingExports.cpp | 5 + core/libs/prx/libSceNpManager/Export.cpp | 49 ++++ core/libs/prx/libSceNpMatching2/Export.cpp | 151 ++++++++++ core/libs/prx/libSceNpScoreRanking/Export.cpp | 81 ++++++ core/libs/prx/libSceNpSignaling/Export.cpp | 46 +++ .../prx/libSceNpSnsFacebookDialog/Export.cpp | 25 ++ core/libs/prx/libSceNpTrophy/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpTrophy/Export.cpp | 268 ++++++++++++++++++ core/libs/prx/libSceNpTus/Export.cpp | 50 ++++ core/libs/prx/libSceNpUtility/Export.cpp | 79 ++++++ core/libs/prx/libSceNpWebApi/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpWebApi/Export.cpp | 181 ++++++++++++ .../libs/prx/libSceSaveData.native/Export.cpp | 74 +++++ core/libs/prx/libkernel/Unimplemented.cpp | 14 + core/libs/tests/GuestNpTrophy.cpp | 119 ++++++++ core/libs/tests/GuestNpWebApi.cpp | 67 +++++ core/libs/tests/GuestSaveDataPs4.cpp | 24 ++ docs/dev/debt/libSceNpTrophy.md | 5 + docs/dev/debt/libSceNpWebApi.md | 6 + 25 files changed, 1371 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAjm.native/AjmLibrary.cmake create mode 100644 core/libs/prx/libSceAjm/CMakeLists.txt create mode 100644 core/libs/prx/libSceGameCustomDataDialog/Export.cpp create mode 100644 core/libs/prx/libSceInvitationDialog/Export.cpp create mode 100644 core/libs/prx/libSceNpMatching2/Export.cpp create mode 100644 core/libs/prx/libSceNpScoreRanking/Export.cpp create mode 100644 core/libs/prx/libSceNpSignaling/Export.cpp create mode 100644 core/libs/prx/libSceNpTrophy/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpTrophy/Export.cpp create mode 100644 core/libs/prx/libSceNpWebApi/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpWebApi/Export.cpp create mode 100644 core/libs/tests/GuestNpTrophy.cpp create mode 100644 core/libs/tests/GuestNpWebApi.cpp create mode 100644 docs/dev/debt/libSceNpTrophy.md create mode 100644 docs/dev/debt/libSceNpWebApi.md diff --git a/core/libs/prx/libSceAjm.native/AjmLibrary.cmake b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake new file mode 100644 index 0000000000..ec9f5b9a81 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake @@ -0,0 +1,15 @@ +function(add_sce_ajm_library target) + set(ajmDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${ajmDir}/Export.cpp + ${ajmDir}/src/Ajm.cpp + ${ARGN} + ) + target_include_directories(${target} PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src + ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs + ) + target_link_libraries(${target} PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 51b7c2507f..04c4242ad8 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -29,18 +29,8 @@ add_library(atrac9_vibration STATIC EXCLUDE_FROM_ALL ) set_target_properties(atrac9_vibration PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) -add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp -) - -target_include_directories(libSceAjm.native PRIVATE - ${LIBS_INCLUDE_DIR} - ${LIBATRAC9_DIR} - ${VGMSTREAM_ATRAC9_DIR} -) -target_link_libraries(libSceAjm.native PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) -configure_windows_unwind(libSceAjm.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm.native) if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) diff --git a/core/libs/prx/libSceAjm/CMakeLists.txt b/core/libs/prx/libSceAjm/CMakeLists.txt new file mode 100644 index 0000000000..7a9c708eae --- /dev/null +++ b/core/libs/prx/libSceAjm/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAjm.native/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) diff --git a/core/libs/prx/libSceAjm/Export.cpp b/core/libs/prx/libSceAjm/Export.cpp index fb34206c21..508f721ede 100644 --- a/core/libs/prx/libSceAjm/Export.cpp +++ b/core/libs/prx/libSceAjm/Export.cpp @@ -5,6 +5,19 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAjmBatchJobControlBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchJobRunSplitBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchStartBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceGameCustomDataDialog/Export.cpp b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp new file mode 100644 index 0000000000..5b647ad000 --- /dev/null +++ b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceGameCustomDataDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp new file mode 100644 index 0000000000..8ae801bc26 --- /dev/null +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceInvitationDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index c9d6e53ffd..d2daed1a92 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,6 +115,11 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } +int APS5_VABI sceNpCmpNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpCmpNpIdInOrder(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 1b45d38294..1efce7122e 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -981,3 +981,52 @@ int APS5_VABI sceNpCheckCallbackForLib() { } } + +extern "C" { + +int APS5_VABI sceNpInGameMessageCreateHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageDeleteHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessagePrepare() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookGetAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp new file mode 100644 index 0000000000..2746971c77 --- /dev/null +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -0,0 +1,151 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpMatching2ContextStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2ContextStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateJoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2DestroyContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataExternalList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetServerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetUserInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetWorldInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2JoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2KickoutRoomMember() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2LeaveRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterContextCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomMessageCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterSignalingCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SearchRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomChatMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetDefaultRequestOptParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataExternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetUserInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp new file mode 100644 index 0000000000..1bb8bc6df6 --- /dev/null +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpScoreAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetBoardInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRanking() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRankingAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpIdAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRangeAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScorePollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScoreAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp new file mode 100644 index 0000000000..8ab2a25170 --- /dev/null +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -0,0 +1,46 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSignalingActivateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingSetContextOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp index 58bb27df19..9a4fe0e8cd 100644 --- a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -15,4 +15,29 @@ int APS5_VABI sceNpSnsFacebookDialogGetStatus(void) { return 0; } +int APS5_VABI sceNpSnsFacebookDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy/CMakeLists.txt b/core/libs/prx/libSceNpTrophy/CMakeLists.txt new file mode 100644 index 0000000000..7301fa20ab --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpTrophy SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpTrophy PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpTrophy) + +if(BUILD_TESTING) + add_test_executable(guest_np_trophy_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophy.cpp) + target_include_directories(guest_np_trophy_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_tests PRIVATE libSceNpTrophy) + configure_windows_unwind(guest_np_trophy_tests) + add_test(NAME guest_np_trophy COMMAND guest_np_trophy_tests) + set_tests_properties(guest_np_trophy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_trophy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpTrophy>") + endif() +endif() diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp new file mode 100644 index 0000000000..aad30b5af0 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -0,0 +1,268 @@ +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <filesystem> +#include <fstream> +#include <map> +#include <mutex> +#include <optional> +#include <set> +#include <string> +#include <vector> +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_TROPHY_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80551604u); +constexpr int NP_TROPHY_ERROR_INSUFFICIENT_BUFFER = static_cast<int>(0x80551605u); +constexpr int NP_TROPHY_ERROR_INVALID_HANDLE = static_cast<int>(0x80551608u); +constexpr int NP_TROPHY_ERROR_INVALID_CONTEXT = static_cast<int>(0x80551609u); +constexpr int NP_TROPHY_ERROR_INVALID_TROPHY_ID = static_cast<int>(0x8055160au); +constexpr int NP_TROPHY_ERROR_INVALID_GROUP_ID = static_cast<int>(0x8055160bu); +constexpr int NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED = static_cast<int>(0x8055160cu); +constexpr int NP_TROPHY_ERROR_NOT_REGISTERED = static_cast<int>(0x8055160fu); +constexpr int NP_TROPHY_ERROR_ALREADY_REGISTERED = static_cast<int>(0x80551610u); +constexpr int NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS = static_cast<int>(0x80551613u); +constexpr int NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND = static_cast<int>(0x80551614u); +constexpr int NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT = static_cast<int>(0x80551616u); +constexpr int NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED = static_cast<int>(0x8055161au); +constexpr int NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED = static_cast<int>(0x80551626u); +constexpr int NP_TROPHY_INVALID_ID = -1; +constexpr int NP_TROPHY_NUM_MAX = 128; +constexpr std::uint32_t TrpMagic = 0xdca24d00u; +constexpr std::size_t TrpHeaderBytes = 0x60; +constexpr std::size_t TrpEntryBytes = 0x40; + +struct TrophyFlagArray { + std::uint32_t bits[NP_TROPHY_NUM_MAX / 32]; +}; + +struct TrpEntry { + std::uint64_t offset; + std::uint64_t size; +}; + +struct Context { + int userId; + std::uint32_t serviceLabel; + bool registered = false; + std::filesystem::path package; + std::map<std::string, TrpEntry> entries; + int trophies = 0; + std::filesystem::path state; + std::set<int> unlocked; +}; + +std::mutex g_mutex; +std::map<int, Context> g_contexts; +std::set<int> g_handles; +int g_nextContext = 1; +int g_nextHandle = 1; + +std::uint32_t BigEndian32(const std::uint8_t* bytes) { + return (std::uint32_t{bytes[0]} << 24) | (std::uint32_t{bytes[1]} << 16) | (std::uint32_t{bytes[2]} << 8) | bytes[3]; +} + +std::uint64_t BigEndian64(const std::uint8_t* bytes) { + return (std::uint64_t{BigEndian32(bytes)} << 32) | BigEndian32(bytes + 4); +} + +std::optional<std::map<std::string, TrpEntry>> ReadPackage(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::array<std::uint8_t, TrpHeaderBytes> header{}; + if (!file.read(reinterpret_cast<char*>(header.data()), header.size()) || BigEndian32(header.data()) != TrpMagic) return std::map<std::string, TrpEntry>{}; + const auto count = BigEndian32(header.data() + 0x10); + if (BigEndian32(header.data() + 0x14) != TrpEntryBytes) return std::map<std::string, TrpEntry>{}; + std::map<std::string, TrpEntry> entries; + for (std::uint32_t i = 0; i < count; ++i) { + std::array<std::uint8_t, TrpEntryBytes> entry{}; + if (!file.read(reinterpret_cast<char*>(entry.data()), entry.size())) return std::map<std::string, TrpEntry>{}; + const std::string name(reinterpret_cast<const char*>(entry.data()), strnlen(reinterpret_cast<const char*>(entry.data()), 32)); + entries[name] = {BigEndian64(entry.data() + 32), BigEndian64(entry.data() + 40)}; + } + return entries; +} + +void LoadState(Context& context) { + std::ifstream file(context.state); + int id = 0; + while (file >> id) { + if (id >= 0 && id < context.trophies) context.unlocked.insert(id); + } +} + +void SaveState(const Context& context) { + std::filesystem::create_directories(context.state.parent_path()); + const auto temporary = context.state.string() + ".tmp"; + { + std::ofstream file(temporary, std::ios::trunc); + for (const auto id : context.unlocked) file << id << '\n'; + } + std::filesystem::rename(temporary, context.state); +} + +int Lookup(int contextId, int handle, Context*& context) { + if (contextId == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (handle == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_HANDLE; + const auto found = g_contexts.find(contextId); + if (found == g_contexts.end()) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (!g_handles.contains(handle)) return NP_TROPHY_ERROR_INVALID_HANDLE; + context = &found->second; + return 0; +} + +int CopyIcon(const Context& context, const std::string& name, void* buffer, std::uint64_t* size) { + const auto entry = context.entries.find(name); + if (entry == context.entries.end()) return NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + if (buffer == nullptr) { + *size = entry->second.size; + return 0; + } + if (*size < entry->second.size) return NP_TROPHY_ERROR_INSUFFICIENT_BUFFER; + std::ifstream file(context.package, std::ios::binary); + file.seekg(static_cast<std::streamoff>(entry->second.offset)); + if (!file.read(static_cast<char*>(buffer), static_cast<std::streamsize>(entry->second.size))) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + *size = entry->second.size; + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options) { + if (!context || options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + for (const auto& [id, existing] : g_contexts) { + if (existing.userId == userId && existing.serviceLabel == serviceLabel) return NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS; + } + const int id = g_nextContext++; + g_contexts[id] = Context{userId, serviceLabel}; + *context = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyContext(int context) { + std::lock_guard lock(g_mutex); + return g_contexts.erase(context) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_CONTEXT; +} + +int APS5_VABI sceNpTrophyCreateHandle(int* handle) { + if (!handle) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + const int id = g_nextHandle++; + g_handles.insert(id); + *handle = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyHandle(int handle) { + std::lock_guard lock(g_mutex); + return g_handles.erase(handle) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_HANDLE; +} + +int APS5_VABI sceNpTrophyRegisterContext(int contextId, int handle, std::uint64_t options) { + if (options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (context->registered) return NP_TROPHY_ERROR_ALREADY_REGISTERED; + char name[32]; + std::snprintf(name, sizeof(name), "trophy%02u.trp", context->serviceLabel); + context->package = ResolvePath_nid_no_patch((std::string("/app0/sce_sys/trophy/") + name).c_str()); + const auto entries = ReadPackage(context->package); + if (!entries) return NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED; + if (entries->empty()) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + context->entries = *entries; + for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", id); + if (!context->entries.contains(icon)) break; + context->trophies = id + 1; + } + std::snprintf(name, sizeof(name), "trophy%02u.%d", context->serviceLabel, context->userId); + context->state = std::filesystem::path("_trophy") / name; + LoadState(*context); + context->registered = true; + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int contextId, int handle, TrophyFlagArray* flags, std::uint32_t* count) { + if (!flags || !count) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + std::memset(flags, 0, sizeof(*flags)); + for (const auto id : context->unlocked) flags->bits[id >> 5] |= 1u << (id & 31); + *count = static_cast<std::uint32_t>(context->trophies); + return 0; +} + +int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, int* platinumId) { + if (!platinumId) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; + context->unlocked.insert(trophyId); + SaveState(*context); + *platinumId = NP_TROPHY_INVALID_ID; + return 0; +} + +int APS5_VABI sceNpTrophyGetGameIcon(int contextId, int handle, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + return CopyIcon(*context, "ICON0.PNG", buffer, size); +} + +int APS5_VABI sceNpTrophyGetGroupIcon(int contextId, int handle, int groupId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (groupId < 0) return NP_TROPHY_ERROR_INVALID_GROUP_ID; + char icon[16]; + std::snprintf(icon, sizeof(icon), "GR%03d.PNG", groupId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (!context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", trophyId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetGameInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index a69e6cb5a6..54f32438da 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -782,4 +782,54 @@ int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { return 0; } +int APS5_VABI sceNpTssGetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 79f5389752..2247cdf62b 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -38,3 +38,82 @@ int APS5_VABI sceNpBandwidthTestShutdown(int context_id, void* result) { } } + +extern "C" { + +int APS5_VABI sceNpBandwidthTestInitStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCensorComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateAsyncRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterPollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterSanitizeComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpWebApi/CMakeLists.txt b/core/libs/prx/libSceNpWebApi/CMakeLists.txt new file mode 100644 index 0000000000..0e3e193000 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpWebApi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpWebApi) + +if(BUILD_TESTING) + add_test_executable(guest_np_web_api_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpWebApi.cpp) + target_include_directories(guest_np_web_api_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_web_api_tests PRIVATE libSceNpWebApi) + configure_windows_unwind(guest_np_web_api_tests) + add_test(NAME guest_np_web_api COMMAND guest_np_web_api_tests) + set_tests_properties(guest_np_web_api PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_web_api PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpWebApi>") + endif() +endif() diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp new file mode 100644 index 0000000000..1aece4e185 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -0,0 +1,181 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include <cstddef> +#include <cstdint> +#include <mutex> +#include <unordered_map> +#include <unordered_set> + +static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80552902); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND = static_cast<int>(0x80552904); +static constexpr int SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND = static_cast<int>(0x8055290B); +static constexpr int SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND = static_cast<int>(0x8055290D); +static constexpr int SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND = static_cast<int>(0x8055290E); +static constexpr int SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND = static_cast<int>(0x80552910); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX = static_cast<int>(0x8055291A); + +namespace { + +struct LibraryContext { + int libHttpCtxId; + std::size_t poolSize; + std::unordered_set<int> handles; + std::unordered_set<int> pushEventFilters; +}; + +std::mutex contextsMutex; +std::unordered_map<int, LibraryContext> contexts; +int lastContext = 0; +int lastObject = 0; + +int nextObject() { + lastObject = lastObject % 0xefffffff + 1; + return lastObject; +} + +} + +extern "C" { + +int APS5_VABI sceNpWebApiAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto handle = nextObject(); + context->second.handles.insert(handle); + return handle; +} + +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum) { + if (!filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto filter = nextObject(); + context->second.pushEventFilters.insert(filter); + return filter; +} + +int APS5_VABI sceNpWebApiCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, std::uint32_t npServiceLabel, const void* filterParam, std::size_t filterParamNum) { + (void)npServiceLabel; + if (!npServiceName || !filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + if (!context->second.handles.contains(handleId)) return SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.handles.erase(handleId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.pushEventFilters.erase(filterId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId) { + (void)filterId; + std::lock_guard lock(contextsMutex); + if (!contexts.contains(libCtxId)) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpStatusCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { + std::lock_guard lock(contextsMutex); + for (int attempt = 0; attempt < 0x7fff; ++attempt) { + lastContext = lastContext % 0x7fff + 1; + if (contexts.contains(lastContext)) continue; + contexts[lastContext] = {libHttpCtxId, poolSize, {}, {}}; + return lastContext; + } + return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; +} + +int APS5_VABI sceNpWebApiReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiSendRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiTerminate(int libCtxId) { + std::lock_guard lock(contextsMutex); + return contexts.erase(libCtxId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiUnregisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUtilityParseNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index a95498f7d9..9549157996 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -1031,3 +1031,77 @@ int APS5_VABI sceSaveDataDebugTarget() { } } + +namespace { + +struct SaveDataMountPs4 { + std::int32_t user_id; + std::int32_t pad; + const void* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; +}; + +} + +extern "C" { + +int APS5_VABI sceSaveDataInitialize(const void* init) { + return sceSaveDataInitialize3(init); +} + +int APS5_VABI sceSaveDataMount(const SaveDataMountPs4* mount, SaveDataMountResult* mount_result) { + if (mount == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; + if (mount->title_id != nullptr) NotImplemented_nid_no_patch("sceSaveDataMount of another title's save data"); + SaveDataMount3 converted{}; + converted.user_id = mount->user_id; + converted.dir_name = mount->dir_name; + converted.blocks = mount->blocks; + converted.mount_mode = mount->mount_mode; + return sceSaveDataMount3(&converted, mount_result); +} + +int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint* mount_point) { + return sceSaveDataUmount2(0, mount_point); +} + +int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t user_id, std::size_t memory_size, const SaveDataParam* param) { + SaveDataMemorySetup2 setup{}; + setup.user_id = user_id; + setup.memory_size = memory_size; + SaveDataMemorySetupResult result{}; + if (const int rc = sceSaveDataSetupSaveDataMemory2(&setup, &result); rc != SAVE_DATA_OK) return rc; + if (param == nullptr) return SAVE_DATA_OK; + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.param = param; + return sceSaveDataSetSaveDataMemory2(&set); +} + +int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast<std::size_t>(offset); + SaveDataMemoryGet2 get{}; + get.user_id = user_id; + get.data = &data; + return sceSaveDataGetSaveDataMemory2(&get); +} + +int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast<std::size_t>(offset); + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.data = &data; + set.data_num = 1; + return sceSaveDataSetSaveDataMemory2(&set); +} + +} diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 3dd75f72d8..617dc506f1 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -133,3 +133,17 @@ int APS5_VABI sceKernelGetProcessAbiType() { } } + +extern "C" { + +int APS5_VABI __Ux86_64_setcontext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigreturn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp new file mode 100644 index 0000000000..e8a9b8749f --- /dev/null +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -0,0 +1,119 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <filesystem> +#include <fstream> +#include <random> +#include <string> +#include <vector> + +extern "C" { +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); +int APS5_VABI sceNpTrophyDestroyContext(int context); +int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyDestroyHandle(int handle); +int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int context, int handle, std::uint32_t* flags, std::uint32_t* count); +int APS5_VABI sceNpTrophyUnlockTrophy(int context, int handle, int trophyId, int* platinumId); +int APS5_VABI sceNpTrophyGetGameIcon(int context, int handle, void* buffer, std::uint64_t* size); +int APS5_VABI sceNpTrophyGetTrophyIcon(int context, int handle, int trophyId, void* buffer, std::uint64_t* size); +} + +namespace { + +constexpr int InvalidArgument = static_cast<int>(0x80551604u); +constexpr int InsufficientBuffer = static_cast<int>(0x80551605u); +constexpr int InvalidContext = static_cast<int>(0x80551609u); +constexpr int InvalidTrophyId = static_cast<int>(0x8055160au); +constexpr int AlreadyUnlocked = static_cast<int>(0x8055160cu); +constexpr int NotRegistered = static_cast<int>(0x8055160fu); +constexpr int AlreadyRegistered = static_cast<int>(0x80551610u); +constexpr int ContextAlreadyExists = static_cast<int>(0x80551613u); +constexpr int TitleConfNotInstalled = static_cast<int>(0x80551626u); +constexpr int TrophyNotUnlocked = static_cast<int>(0x8055161au); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpTrophy: %s\n", message); + std::abort(); + } +} + +void PutBigEndian(std::vector<std::uint8_t>& bytes, std::size_t at, std::uint64_t value, int width) { + for (int i = 0; i < width; ++i) bytes[at + i] = static_cast<std::uint8_t>(value >> (8 * (width - 1 - i))); +} + +void WritePackage(const std::filesystem::path& path, const std::vector<std::pair<std::string, std::string>>& files) { + const std::size_t header = 0x60, entry = 0x40; + std::vector<std::uint8_t> bytes(header + entry * files.size()); + PutBigEndian(bytes, 0, 0xdca24d00u, 4); + PutBigEndian(bytes, 0x10, files.size(), 4); + PutBigEndian(bytes, 0x14, entry, 4); + for (std::size_t i = 0; i < files.size(); ++i) { + const auto at = header + entry * i; + std::memcpy(bytes.data() + at, files[i].first.data(), files[i].first.size()); + PutBigEndian(bytes, at + 32, bytes.size(), 8); + PutBigEndian(bytes, at + 40, files[i].second.size(), 8); + bytes.insert(bytes.end(), files[i].second.begin(), files[i].second.end()); + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary).write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size())); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-np-trophy-" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + WritePackage(root / "app0/sce_sys/trophy/trophy00.trp", {{"TROPCONF.ESFM", "conf"}, {"ICON0.PNG", "game-icon"}, {"TROP000.PNG", "first"}, {"TROP001.PNG", "second"}}); + std::filesystem::current_path(root); + + int context = 0, handle = 0, missing = 0, platinum = 0; + Require(sceNpTrophyCreateContext(nullptr, 1, 0, 0) == InvalidArgument, "null context"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 1) == InvalidArgument, "context options"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 0) == 0, "create the context"); + Require(sceNpTrophyCreateContext(&missing, 1, 0, 0) == ContextAlreadyExists, "second context for the same user and label"); + Require(sceNpTrophyCreateHandle(&handle) == 0, "create the handle"); + + std::uint32_t flags[4] = {}, count = 0; + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == NotRegistered, "state before registration"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == 0, "register the context"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == AlreadyRegistered, "register twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && count == 2 && flags[0] == 0, "two trophies, none unlocked"); + + std::uint64_t size = 0; + Require(sceNpTrophyGetGameIcon(context, handle, nullptr, &size) == 0 && size == 9, "game icon size"); + char icon[16] = {}; + size = 4; + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == InsufficientBuffer, "game icon into a short buffer"); + size = sizeof(icon); + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == 0 && size == 9 && std::memcmp(icon, "game-icon", 9) == 0, "game icon bytes"); + + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == TrophyNotUnlocked, "icon of a locked trophy"); + Require(sceNpTrophyUnlockTrophy(context, handle, 2, &platinum) == InvalidTrophyId, "unlock a trophy past the count"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == 0 && platinum == -1, "unlock trophy 1"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == AlreadyUnlocked, "unlock trophy 1 twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && flags[0] == 2u, "trophy 1 unlocked"); + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == 0 && size == 6 && std::memcmp(icon, "second", 6) == 0, "icon of the unlocked trophy"); + int saved = -1; + { + std::ifstream state(root / "_trophy/trophy00.1"); + state >> saved; + } + Require(saved == 1, "unlock state saved for the user"); + + Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); + Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); + Require(sceNpTrophyDestroyContext(missing) == 0, "destroy the second context"); + Require(sceNpTrophyDestroyHandle(handle) == 0 && sceNpTrophyDestroyContext(context) == 0, "destroy the handle and context"); + Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); + + std::filesystem::current_path(root.parent_path()); + std::error_code error; + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestNpWebApi.cpp b/core/libs/tests/GuestNpWebApi.cpp new file mode 100644 index 0000000000..9281e58632 --- /dev/null +++ b/core/libs/tests/GuestNpWebApi.cpp @@ -0,0 +1,67 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstddef> +#include <cstdio> +#include <cstdlib> + +extern "C" { +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); +int APS5_VABI sceNpWebApiTerminate(int libCtxId); +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId); +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId); +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId); +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, unsigned npServiceLabel, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); +} + +namespace { + +constexpr int InvalidArgument = static_cast<int>(0x80552902u); +constexpr int LibContextNotFound = static_cast<int>(0x80552904u); +constexpr int PushEventFilterNotFound = static_cast<int>(0x8055290Bu); +constexpr int HandleNotFound = static_cast<int>(0x8055290Du); +constexpr int ServicePushEventFilterNotFound = static_cast<int>(0x8055290Eu); +constexpr int SignedInUserNotFound = static_cast<int>(0x80552910u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpWebApi: %s\n", message); + std::abort(); + } +} + +} + +int main() { + const int context = sceNpWebApiInitialize(1, 0x10000); + Require(context > 0 && context <= 0x7fff, "library context id out of range"); + const int other = sceNpWebApiInitialize(1, 0x10000); + Require(other > 0 && other != context, "a second library context must get its own id"); + Require(sceNpWebApiCreateHandle(0x7fff + 1) == LibContextNotFound, "handle of an unknown context"); + + const int handle = sceNpWebApiCreateHandle(context); + Require(handle > 0 && static_cast<unsigned>(handle) < 0xf0000000u, "handle id out of range"); + const char filterParam[16] = {}; + Require(sceNpWebApiCreatePushEventFilter(context, nullptr, 1) == InvalidArgument, "push event filter without its parameter"); + Require(sceNpWebApiCreatePushEventFilter(context, filterParam, 0) == InvalidArgument, "push event filter with no parameters"); + const int filter = sceNpWebApiCreatePushEventFilter(context, filterParam, 1); + Require(filter > 0 && filter != handle, "push event filter id"); + + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, nullptr, 0, filterParam, 1) == InvalidArgument, "service filter without a service name"); + Require(sceNpWebApiCreateServicePushEventFilter(0x7fff + 1, handle, "service", 0, filterParam, 1) == LibContextNotFound, "service filter of an unknown context"); + Require(sceNpWebApiCreateServicePushEventFilter(other, handle, "service", 0, filterParam, 1) == HandleNotFound, "service filter with another context's handle"); + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0, filterParam, 1) == SignedInUserNotFound, "service filter without a signed-in user"); + Require(sceNpWebApiDeleteServicePushEventFilter(context, 1) == ServicePushEventFilterNotFound, "no service filter exists"); + + Require(sceNpWebApiDeletePushEventFilter(other, filter) == PushEventFilterNotFound, "filter deleted through another context"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == 0, "delete the push event filter"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == PushEventFilterNotFound, "push event filter deleted twice"); + Require(sceNpWebApiDeleteHandle(context, handle) == 0, "delete the handle"); + Require(sceNpWebApiDeleteHandle(context, handle) == HandleNotFound, "handle deleted twice"); + + Require(sceNpWebApiTerminate(context) == 0, "terminate the library context"); + Require(sceNpWebApiTerminate(context) == LibContextNotFound, "library context terminated twice"); + Require(sceNpWebApiCreateHandle(context) == LibContextNotFound, "handle of a terminated context"); + Require(sceNpWebApiTerminate(other) == 0, "terminate the second library context"); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 39e7b14d1e..6ee6868bd4 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -3,6 +3,7 @@ #include <cstdio> #include <cstring> #include <filesystem> +#include <fstream> #include <random> #include <stdexcept> #include <string> @@ -10,10 +11,16 @@ extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +extern "C" int APS5_VABI sceSaveDataInitialize(const void*); +extern "C" int APS5_VABI sceSaveDataTerminate(); +extern "C" int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t, std::size_t, const SaveDataParam*); +extern "C" int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); +extern "C" int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); namespace { constexpr int SaveDataErrorNotFound = -2137063416; +constexpr int SaveDataErrorNotInitialized = -2137063423; int failures = 0; @@ -73,6 +80,22 @@ void TestDirNameSearchPs4() { Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); } +void TestSaveDataMemoryV1() { + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == SaveDataErrorNotInitialized, "v1 SetupSaveDataMemory before v1 Initialize"); + Check(sceSaveDataInitialize(nullptr) == 0, "v1 Initialize"); + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == 0, "v1 SetupSaveDataMemory"); + char written[16] = "settings-v1"; + Check(sceSaveDataSetSaveDataMemory(7, written, sizeof(written), 0x100) == 0, "v1 SetSaveDataMemory at an offset"); + char read[16] = {}; + Check(sceSaveDataGetSaveDataMemory(7, read, sizeof(read), 0x100) == 0, "v1 GetSaveDataMemory"); + Check(std::memcmp(read, written, sizeof(read)) == 0, "v1 GetSaveDataMemory returns the bytes v1 set"); + std::ifstream slot("_sd_mem/u7/slot0.bin", std::ios::binary); + slot.seekg(0x100); + char stored[16] = {}; + Check(static_cast<bool>(slot.read(stored, sizeof(stored))) && std::memcmp(stored, written, sizeof(stored)) == 0, "v1 SetSaveDataMemory stores into slot 0 of the user"); + Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); +} + } int main() { @@ -83,6 +106,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); + TestSaveDataMemoryV1(); std::filesystem::current_path(previous); std::error_code error; diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md new file mode 100644 index 0000000000..c445f4e8d8 --- /dev/null +++ b/docs/dev/debt/libSceNpTrophy.md @@ -0,0 +1,5 @@ +# libSceNpTrophy technical debt + +### Unknown function info + +- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`, and the calls that return names and details (`sceNpTrophyGetGameInfo`, `sceNpTrophyGetGroupInfo`, `sceNpTrophyGetTrophyInfo`) throw. Unlocks are kept per user and service label in `_trophy/trophyNN.<user id>` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. diff --git a/docs/dev/debt/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md new file mode 100644 index 0000000000..95fb314c74 --- /dev/null +++ b/docs/dev/debt/libSceNpWebApi.md @@ -0,0 +1,6 @@ +# libSceNpWebApi technical debt + +### Unknown function info + +- [sceNpWebApiInitialize, sceNpWebApiTerminate, sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiCreatePushEventFilter, sceNpWebApiDeletePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - signatures, id ranges (library contexts 1 to 0x7fff, handles and filters below 0xf0000000) and errors (`LIB_CONTEXT_NOT_FOUND`, `HANDLE_NOT_FOUND`, `PUSH_EVENT_FILTER_NOT_FOUND`, `INVALID_ARGUMENT` for a missing filter parameter) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp); they are not checked on a console. The libHttp context and pool size are kept but not used, since no request is ever sent. Limbo (CUSA01664) creates a context, a handle and filters when its game starts. +- [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. shadPS4 also rejects an invalid service label for titles built with SDK 2.00 or later; AnyPS5 has no compiled-SDK version for libraries yet, so such a call returns `SIGNED_IN_USER_NOT_FOUND` instead of `INVALID_ARGUMENT`. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. From 173603428de261f7fcdc85f2855b5993a352b2e8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:44:31 +0000 Subject: [PATCH 0890/1360] feat(libSceFontFt): report TrueType GX support --- core/libs/prx/libSceFontFt/Export.cpp | 3 +-- core/libs/tests/GuestFont.cpp | 5 +++-- docs/dev/debt/libSceFontFt.md | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceFontFt/Export.cpp b/core/libs/prx/libSceFontFt/Export.cpp index b0a213d62f..628a572b94 100644 --- a/core/libs/prx/libSceFontFt/Export.cpp +++ b/core/libs/prx/libSceFontFt/Export.cpp @@ -78,8 +78,7 @@ int APS5_VABI sceFontFtSupportTrueType(FontLibrary library) { } int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library) { - NotImplemented_nid_no_patch(__func__); - return 0; + return FontFt::SupportFormats(library, {"truetype"}); } int APS5_VABI sceFontFtSupportType1(FontLibrary library) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 1ca28f3228..b4f1a80ff2 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -70,6 +70,7 @@ int APS5_VABI sceFontFtSupportOpenTypeTtf(FontLibrary library); int APS5_VABI sceFontFtSupportPcf(FontLibrary library); int APS5_VABI sceFontFtSupportPfr(FontLibrary library); int APS5_VABI sceFontFtSupportTrueType(FontLibrary library); +int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library); int APS5_VABI sceFontFtSupportType1(FontLibrary library); int APS5_VABI sceFontFtSupportType42(FontLibrary library); int APS5_VABI sceFontFtSupportWinFonts(FontLibrary library); @@ -220,10 +221,10 @@ int main() { FontLibrary library = nullptr; Require(sceFontCreateLibrary(&memory, nullptr, &library) == SCE_FONT_ERROR_INVALID_PARAMETER && library == nullptr); Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK && library != nullptr); - Require(sceFontFtSupportBdf(library) == 0 && sceFontFtSupportCid(library) == 0 && sceFontFtSupportOpenType(library) == 0 && sceFontFtSupportOpenTypeOtf(library) == 0 && sceFontFtSupportOpenTypeTtf(library) == 0 && sceFontFtSupportPcf(library) == 0 && sceFontFtSupportPfr(library) == 0 && sceFontFtSupportTrueType(library) == 0 && sceFontFtSupportType1(library) == 0 && sceFontFtSupportType42(library) == 0 && sceFontFtSupportWinFonts(library) == 0); + Require(sceFontFtSupportBdf(library) == 0 && sceFontFtSupportCid(library) == 0 && sceFontFtSupportOpenType(library) == 0 && sceFontFtSupportOpenTypeOtf(library) == 0 && sceFontFtSupportOpenTypeTtf(library) == 0 && sceFontFtSupportPcf(library) == 0 && sceFontFtSupportPfr(library) == 0 && sceFontFtSupportTrueType(library) == 0 && sceFontFtSupportTrueTypeGx(library) == 0 && sceFontFtSupportType1(library) == 0 && sceFontFtSupportType42(library) == 0 && sceFontFtSupportWinFonts(library) == 0); const auto* nativeLibrary = static_cast<const Font::FontLibNative*>(library); const auto savedLock = nativeLibrary->lock_word; - for (auto support : {sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportType1, sceFontFtSupportType42, sceFontFtSupportWinFonts}) { + for (auto support : {sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportTrueTypeGx, sceFontFtSupportType1, sceFontFtSupportType42, sceFontFtSupportWinFonts}) { Font::FontLibNative invalidLibrary{}; Require(support(nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); Require(support(&invalidLibrary) == SCE_FONT_ERROR_INVALID_LIBRARY); diff --git a/docs/dev/debt/libSceFontFt.md b/docs/dev/debt/libSceFontFt.md index 3aa05e385d..c5c609e43f 100644 --- a/docs/dev/debt/libSceFontFt.md +++ b/docs/dev/debt/libSceFontFt.md @@ -3,6 +3,7 @@ ### Functional - [Font format support](../../../core/libs/prx/libSceFontFt/Export.cpp): format queries now take the font library, validate its magic and lock it while checking its actual FreeType modules. The host registers its default modules at creation rather than enabling formats individually, so format-selection state and guest allocator failures during format registration are not reproduced. Unsupported foreign drivers throw. +- [TrueType GX support](../../../core/libs/prx/libSceFontFt/Export.cpp): the GX query checks the bundled FreeType TrueType module, whose configuration enables GX variation support. The console's response for a FreeType build without GX variation support has not been measured. ### Unknown function info From 6287af96574a7607479c8f783c459ceb0b964333 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:56:43 -0700 Subject: [PATCH 0891/1360] docs(libc): name the num_get<char> vtable's slots from the console mazen320 mapped the relocated slots of KfcTPbeaOqg in the console's libSceLibcInternal: offset-to-top, typeinfo, the two destructors, locale::facet::_Incref and _Decref, and the eleven do_get. The debt entry now says so instead of leaving the two extra entries unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2827dc2170..d32a332f40 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -134,4 +134,4 @@ - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered -- [num_get<char>::id, time_get<char>::id and the num_get<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal (two more than the two header entries, two destructor entries and eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`; which two virtual functions the console's adds is not known), so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. +- [num_get<char>::id, time_get<char>::id and the num_get<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. From 25b7991c6ab694a5b3ccf54250bb66f670eb6254 Mon Sep 17 00:00:00 2001 From: hugodearaujo <hugoaraujocamara@gmail.com> Date: Wed, 21 Oct 2026 18:56:52 -0300 Subject: [PATCH 0892/1360] fix(netctl): start the library initialized, as titles find it Titles register and check NetCtl callbacks without importing sceNetCtlInit: libSceNetCtl is an internal sysmodule the system has started before the title runs. 007 First Light traps at start-up when sceNetCtlRegisterCallback answers NOT_INITIALIZED. --- core/libs/prx/libSceNetCtl/Export.cpp | 2 +- core/libs/tests/GuestNetCtl.cpp | 25 +++++++++++-------------- docs/dev/debt/libSceNetCtl.md | 3 ++- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 30be5f6e85..519bcf4ce1 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -95,7 +95,7 @@ struct CallbackSlot { int lastEvent = 0; }; -static std::atomic_bool g_initialized{false}; +static std::atomic_bool g_initialized{true}; static std::mutex g_callbackLock; static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 1b7ce582b2..f45a66dc77 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -272,20 +272,17 @@ int main() { #ifdef __APPLE__ CheckDefaultRoutes(); #endif - constexpr int notInitialized = static_cast<int>(0x80412101); - Require(sceNetCtlCheckCallback() == notInitialized); - Require(sceNetCtlGetState(nullptr) == notInitialized); - Require(sceNetCtlGetStateV6(nullptr) == notInitialized); - Require(sceNetCtlGetInfo(0, nullptr) == notInitialized); - Require(sceNetCtlGetResult(0, nullptr) == notInitialized); - Require(sceNetCtlGetNatInfo(nullptr) == notInitialized); - Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == notInitialized); - Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == notInitialized); - Require(sceNetCtlUnregisterCallback(-1) == notInitialized); - Require(sceNetCtlUnregisterCallbackV6(-1) == notInitialized); - sceNetCtlTerm(); - Require(sceNetCtlCheckCallback() == notInitialized); - Require(sceNetCtlInit() == 0); + constexpr int invalidId = static_cast<int>(0x80412105); + Require(sceNetCtlCheckCallback() == 0); + Require(sceNetCtlGetState(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetInfo(0, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlUnregisterCallback(-1) == invalidId); + Require(sceNetCtlUnregisterCallbackV6(-1) == invalidId); CheckCallbacks(); Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 2e936ee5bc..96b0e7ed58 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -15,6 +15,7 @@ ### Functional -- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for the native service event queues. IPv4 callbacks receive connected/disconnected transitions and an initial event; intermediate connection states and errors are not represented. Native CheckCallback drains both IPv4 and IPv6 queues, but the host backend never delivers IPv6 events. Dispatch copies slots before invoking callbacks, so unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. Initialization, slot limits, invalid/deleted IDs and the no-op Term follow the supplied module. +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for the native service event queues. IPv4 callbacks receive connected/disconnected transitions and an initial event; intermediate connection states and errors are not represented. Native CheckCallback drains both IPv4 and IPv6 queues, but the host backend never delivers IPv6 events. Dispatch copies slots before invoking callbacks, so unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. Slot limits, invalid/deleted IDs and the no-op Term follow the supplied module. +- [libSceNetCtl initialization](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the library starts initialized, as titles find it: it is the internal sysmodule `0x80000009`, and PPSA11386, PPSA20049, PPSA02530, PPSA14632 and PPSA15878 register and check NetCtl callbacks without importing `sceNetCtlInit`. PPSA11386 executes `int 0x42` at start-up unless `sceNetCtlRegisterCallback` returns 0. So the supplied module's `SCE_NET_CTL_ERROR_NOT_INITIALIZED` answers are unreachable, since Term is a no-op. Whether the console's process start or the module's start sets that state is not measured. - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB From 3b5a99c20fd53f2e10d3524861698d32e2b3c36c Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:13:09 +0200 Subject: [PATCH 0893/1360] fix(tools): count assembly exports in the duplicate-export check --- tools/check_conventions.py | 25 +++++++++--- .../tests/test_check_conventions_comments.py | 39 ++++++++++++++++++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index bc985e1480..09cb309937 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -171,6 +171,7 @@ class Check: core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts source = self.show(path).split("\n") masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") + assembly = asm_exports("\n".join(source)) if core and path.startswith("core/libs/prx/") else {} for number, text in lines: bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): @@ -182,6 +183,7 @@ class Check: if core and path.startswith("core/libs/prx/"): unknowns.extend((nid, name, path, number) for nid, name in source_aliases(text) if "unknown" in name.lower()) symbols.extend((symbol, path, number) for symbol in nid_symbols(text)) + exports.update((name, (path, number)) for name in assembly.get(number, ()) if not name.endswith(("_nid_no_patch", "_nid_no_patch_cut"))) match = EXPORT.search(bare) if match and core and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): source = source or self.show(path).splitlines() @@ -212,16 +214,17 @@ class Check: self.read(paths) for path in paths: text = self.show(path) - if "APS5_VABI" not in text: + if "APS5_VABI" not in text and "ASM_FUNCTION" not in text: continue source = cpp_text(text).split("\n") + assembly = asm_exports(text) for number, line in enumerate(source): - for match in EXPORT.finditer(line): - name = match.group(1) - if name not in exports: - continue + names = [match.group(1) for match in EXPORT.finditer(line) + if match.group(1) in exports and function_body(source, number) is not None] + names += [name for name in assembly.get(number + 1, ()) if name in exports] + for name in names: own, own_number = exports[name] - if library(path) != library(own) and function_body(source, number) is not None: + if library(path) != library(own): self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def unknown_nids(self, unknowns, symbols): @@ -517,6 +520,16 @@ def symbol_nid(name): return compute_nid(base) if base else None +def asm_exports(text): + tokens = [match for match in CPP_TOKEN.finditer(text) if match.group("comment") is None] + exports = defaultdict(list) + for index, match in enumerate(tokens[:-2]): + name = tokens[index + 2].group()[1:-1] + if match.group() in ("APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION") and tokens[index + 1].group() == "(" and tokens[index + 2].group("string") is not None and IDENTIFIER.fullmatch(name): + exports[text.count("\n", 0, tokens[index + 2].start()) + 1].append(name) + return exports + + def nid_symbols(text): tokens = [match for match in CPP_TOKEN.finditer(text) if match.group("comment") is None] names = [] diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 95aa949321..b5755fb281 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -399,7 +399,7 @@ class SilentStubSectionTests(unittest.TestCase): class TestFixtureExportTests(unittest.TestCase): - def check(self, path, text=None): + def check(self, path, text=None, base=None): files = path if text is None else {path: text} checker = Path(__file__).resolve().parents[1] / "check_conventions.py" with tempfile.TemporaryDirectory() as directory: @@ -420,6 +420,8 @@ class TestFixtureExportTests(unittest.TestCase): git("init", "-q") write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") + for relative, content in (base or {}).items(): + write(relative, content) git("add", ".") git("commit", "-qm", "feat: add libraries") for relative, content in files.items(): @@ -451,6 +453,41 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_export_shadowing_an_assembly_export_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_assembly_export_in_another_library_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", + 'asm(".text\\n"\n APS5_SIGNAL_ASM_FUNCTION("sceKernelGetProcessTime")\n " ret\\n");\n') + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:2: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + def test_assembly_export_after_a_raw_string_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Jump.cpp", + 'asm(R"(\n .text\n)" APS5_ASM_FUNCTION("setjmp_nid_postfix") R"(\n ret\n)");\n', + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Jump.cpp:3: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_assembly_export_in_a_block_comment_is_not_an_export(self): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": '/*\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n*/\n'}) + self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) + + def test_assembly_name_that_is_not_an_identifier_keeps_the_check(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", + 'asm(".text\\n"\n APS5_ASM_FUNCTION("x(y")\n " ret\\n");\nstd::uint64_t APS5_VABI sceKernelGetProcessTime() {\n return 1;\n}\n') + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_comment_in_a_shader_is_reported(self): shaders = {} for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag", "core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.glsl"): From 5c3556a0b91c9fa4543311212f5a77401fc9338e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:20:18 +0000 Subject: [PATCH 0894/1360] test(tools): find a duplicate export among more than 32K characters of export names --- tools/tests/test_check_conventions_comments.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index b5755fb281..36dfed40a7 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -453,6 +453,16 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_large_export_set_uses_a_file_backed_pattern(self): + names = [f"ConventionExport{index:08d}LongName" for index in range(1100)] + self.assertGreater(len("|".join(names)), 32767) + source = "\n".join(f"int APS5_VABI {name}(void) {{ return 0; }}" for name in names) + source += "\nstd::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n" + result = self.check("core/libs/prx/libSceAudioOut/src/Exports.cpp", source) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Exports.cpp:1101: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_export_shadowing_an_assembly_export_is_a_duplicate(self): result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", From f066bd61b3da25c3a7fef7d6d01f6be9bff0c3d3 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Wed, 14 Oct 2026 04:39:33 -0400 Subject: [PATCH 0895/1360] feat(audio-propagation): track source-owned configurable path slots --- .../AudioPropagation.hpp | 10 ++ .../prx/libSceAudioPropagation/Export.cpp | 91 +++++++++++++++---- core/libs/tests/GuestAudioPropagation.cpp | 70 ++++++++++++-- docs/dev/debt/libSceAudioPropagation.md | 4 +- 4 files changed, 149 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 8323b813c9..f00b2ea48a 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOPROPAGATION_AUDIOPROPAGATION_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOPROPAGATION_AUDIOPROPAGATION_HPP +#include <array> #include <cstddef> #include <cstdint> @@ -128,6 +129,14 @@ static_assert(offsetof(ReportInfo, function) == 32); static_assert(offsetof(ReportInfo, format) == 40); static_assert(offsetof(ReportInfo, arguments) == 48); +inline constexpr std::uint32_t MaxPaths = 36; +inline constexpr std::uint32_t MaxPathPoints = 4; +inline constexpr std::uint32_t PathId = 0x010107d3; +inline constexpr std::uint32_t PathPointId = 0x010107d2; +inline constexpr std::uint32_t PathInvalid = 1; +inline constexpr std::uint32_t PathPortal = 2; + + struct Attribute { std::uint32_t id; std::uint32_t reserved0; @@ -209,6 +218,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceDestroy(AudioPropagationHandle s std::int32_t APS5_VABI sceAudioPropagationSourceSetAttributes(AudioPropagationHandle source, const AudioPropagation::Attribute* attributes, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagationHandle source, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath); +std::int32_t APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index fb835aca4d..3458ff2530 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -1,8 +1,8 @@ #include "AudioPropagation.hpp" #include "prx/libc/include/General.hpp" -#include <cstdint> #include <cmath> +#include <cstdint> #include <cstring> #include <mutex> #include <stdexcept> @@ -27,18 +27,25 @@ struct SystemState { std::uint32_t maxSources; }; -enum class Kind { Room, Portal, Source, Material }; +enum class Kind { Room, Portal, Source, Material, Path }; struct Object { Kind kind; AudioPropagationHandle system; }; +struct PathState { + AudioPath data{}; + float gain = 0.0f; +}; + struct Registry { std::mutex mutex; std::unordered_set<AudioPropagationHandle> systems; std::unordered_map<AudioPropagationHandle, Object> objects; std::unordered_map<AudioPropagationHandle, Object> orphans; + std::unordered_map<AudioPropagationHandle, std::array<AudioPropagationHandle, MaxPaths>> sourcePaths; + std::unordered_map<AudioPropagationHandle, PathState> paths; AudioPropagationHandle next = 1; }; @@ -101,6 +108,13 @@ void destroyObject(AudioPropagationHandle system, AudioPropagationHandle handle, return; } require(requireObject(state, handle, kind, function).system == system, function); + if (kind == Kind::Source) { + for (const auto path : state.sourcePaths.at(handle)) { + state.paths.erase(path); + state.objects.erase(path); + } + state.sourcePaths.erase(handle); + } state.objects.erase(handle); } @@ -169,9 +183,11 @@ std::int32_t APS5_VABI sceAudioPropagationSystemDestroy(AudioPropagationHandle s auto& state = registry(); std::lock_guard lock(state.mutex); requireSystem(state, system, __func__); + std::erase_if(state.paths, [&](const auto& entry) { return state.objects.at(entry.first).system == system; }); + std::erase_if(state.sourcePaths, [&](const auto& entry) { return state.objects.at(entry.first).system == system; }); std::erase_if(state.objects, [&](const auto& entry) { if (entry.second.system != system) return false; - state.orphans.insert(entry); + if (entry.second.kind != Kind::Path) state.orphans.insert(entry); return true; }); reinterpret_cast<SystemState*>(system)->magic = 0; @@ -257,7 +273,30 @@ std::int32_t APS5_VABI sceAudioPropagationPortalSetAttributes(AudioPropagationHa } std::int32_t APS5_VABI sceAudioPropagationSourceCreate(AudioPropagationHandle system, AudioPropagationHandle* outSource) { - createObject(system, Kind::Source, outSource, __func__); + require(outSource != nullptr, __func__); + auto& state = registry(); + std::lock_guard lock(state.mutex); + AudioPropagationHandle source = 0; + createObjectLocked(state, system, Kind::Source, &source, __func__); + std::array<AudioPropagationHandle, MaxPaths> handles{}; + try { + for (auto& handle : handles) { + createObjectLocked(state, system, Kind::Path, &handle, __func__); + PathState path{}; + path.data.desc = {PathId, sizeof(AudioPath)}; + path.data.flags = PathInvalid; + state.paths.emplace(handle, path); + } + state.sourcePaths.emplace(source, handles); + } catch (...) { + for (const auto handle : handles) { + state.paths.erase(handle); + state.objects.erase(handle); + } + state.objects.erase(source); + throw; + } + *outSource = source; return 0; } @@ -279,7 +318,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagati std::lock_guard lock(state.mutex); requireObject(state, source, Kind::Source, __func__); require(count != nullptr, __func__); - *count = 0; + *count = MaxPaths; return 0; } @@ -289,8 +328,18 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHan const auto found = state.objects.find(source); if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; if (!outPath) return ErrorNullPointer; - static_cast<void>(index); - return ErrorInvalidArgument; + if (index >= MaxPaths) return ErrorInvalidArgument; + *outPath = state.sourcePaths.at(source)[index]; + return 0; +} + +std::int32_t APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { + auto& state = registry(); + std::lock_guard lock(state.mutex); + requireObject(state, path, Kind::Path, __func__); + require(count != nullptr, __func__); + *count = state.paths.at(path).data.pointCount; + return 0; } std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count) { @@ -322,10 +371,19 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa } std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPath* data, float gain) { - static_cast<void>(path); - static_cast<void>(data); - static_cast<void>(gain); - NotImplemented_nid_no_patch(__func__); + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto system = requireObject(state, path, Kind::Path, __func__).system; + requireDescriptor(data != nullptr ? &static_cast<const AudioPath*>(data)->desc : nullptr, PathId, sizeof(AudioPath), __func__); + const auto replacement = *static_cast<const AudioPath*>(data); + require(std::isfinite(gain) && gain >= 0.0f && replacement.pointCount <= MaxPathPoints && (replacement.flags & ~(PathInvalid | PathPortal)) == 0, __func__); + for (std::uint32_t index = 0; index < replacement.pointCount; ++index) { + const auto& point = replacement.points[index]; + requireDescriptor(&point.desc, PathPointId, sizeof(PathPoint), __func__); + require(std::isfinite(point.gain) && point.gain >= 0.0f && std::isfinite(point.position[0]) && std::isfinite(point.position[1]) && std::isfinite(point.position[2]), __func__); + if (point.room != 0) require(requireObject(state, point.room, Kind::Room, __func__).system == system, __func__); + } + state.paths.at(path) = PathState{replacement, gain}; return 0; } @@ -343,6 +401,12 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); require(info.output != nullptr && info.outputSize >= requiredBytes, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + for (const auto handle : state.sourcePaths.at(info.source)) { + const auto& path = state.paths.at(handle); + if (path.gain > 0.0f && path.data.pointCount != 0 && (path.data.flags & PathInvalid) == 0) { + throw std::runtime_error(std::string(__func__) + ": nonempty audio path rendering is not implemented"); + } + } } for (std::uint32_t index = 0; index < count; ++index) { infos[index].channelCount = 0; @@ -350,11 +414,6 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy return 0; } -int APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioPropagationReportApi(std::uint32_t category, AudioPropagationHandle handle, std::int32_t result, std::int32_t operation, const char* function, const char* format, ...) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 0f060fd0a5..ebf9b6874a 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -5,10 +5,10 @@ #include <cstdio> #include <cstdlib> #include <cstring> +#include <limits> #include <stdexcept> extern "C" { -int APS5_VABI sceAudioPropagationPathGetNumPoints(); int APS5_VABI sceAudioPropagationSystemLock(); int APS5_VABI sceAudioPropagationMaterialInit(); } @@ -152,13 +152,48 @@ int main() { RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceCreate(system, nullptr); }, "source without output"); Require(sceAudioPropagationSourceSetAttributes(source, portalAttributes, 3) == 0, "source attributes failed"); std::uint32_t count = 7; - Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == 0, "source reports audio paths"); + Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == AudioPropagation::MaxPaths, "source path pool size"); AudioPropagationHandle path = 0; - path = 0x1234; - Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path without paths"); - Require(sceAudioPropagationSourceGetAudioPath(source, UINT32_MAX, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path index range"); - Require(sceAudioPropagationSourceGetAudioPath(source, 0, nullptr) == AudioPropagation::ErrorNullPointer, "audio path null output"); - Require(sceAudioPropagationSourceGetAudioPath(0, 0, nullptr) == AudioPropagation::ErrorInvalidHandle, "audio path handle precedence"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == 0 && path != 0, "path lookup immediately after source creation"); + AudioPropagationHandle repeated = 0; + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &repeated) == 0 && repeated == path, "path handle is unstable"); + AudioPropagationHandle lastPath = 0; + Require(sceAudioPropagationSourceGetAudioPath(source, AudioPropagation::MaxPaths - 1, &lastPath) == 0 && lastPath != path, "last path slot"); + Require(sceAudioPropagationSourceGetAudioPath(source, AudioPropagation::MaxPaths, &repeated) == AudioPropagation::ErrorInvalidArgument, "out of range path slot"); + Require(repeated == path, "rejected path lookup changed the output"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, nullptr) == AudioPropagation::ErrorNullPointer, "null path output"); + Require(sceAudioPropagationSourceGetAudioPath(0, 0, nullptr) == AudioPropagation::ErrorInvalidHandle, "path lookup handle precedence"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 0, "initial path has points"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationPathGetNumPoints(source, &count); }, "source used as a path"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationPathGetNumPoints(path, nullptr); }, "null point-count output"); + AudioPropagation::AudioPath configured{}; + configured.desc = {AudioPropagation::PathId, sizeof(configured)}; + configured.pointCount = 2; + configured.flags = AudioPropagation::PathInvalid; + for (auto& point : configured.points) { + point.desc = {AudioPropagation::PathPointId, sizeof(point)}; + point.gain = 1.0f; + } + configured.points[0].position[0] = 3.0f; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "path configuration"); + configured.pointCount = 0; + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "path configuration aliases caller memory"); + configured.pointCount = 2; + auto badPath = configured; + badPath.desc.size--; + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "wrong path size"); + badPath = configured; + badPath.pointCount = AudioPropagation::MaxPathPoints + 1; + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "too many path points"); + badPath = configured; + badPath.points[0].room = materialHandle; + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "material used as a path room"); + badPath = configured; + badPath.points[0].position[0] = std::numeric_limits<float>::infinity(); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "nonfinite point position"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &configured, std::numeric_limits<float>::quiet_NaN()); }, "nonfinite path gain"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, nullptr, 1.0f); }, "null path data"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "rejected update changed path state"); std::uint8_t rays[AudioPropagation::RaySize * 2]{}; count = 7; @@ -186,6 +221,13 @@ int main() { Require(sceAudioPropagationSystemCreate(&options, &otherMemory, &otherSystem) == 0, "second system create failed"); AudioPropagationHandle otherSource = 0; Require(sceAudioPropagationSourceCreate(otherSystem, &otherSource) == 0, "second system source create failed"); + AudioPropagationHandle foreignRoom = 0; + Require(sceAudioPropagationRoomCreate(otherSystem, &foreignRoom) == 0, "foreign room create failed"); + badPath = configured; + badPath.points[0].room = foreignRoom; + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "room from another system"); + Require(sceAudioPropagationRoomDestroy(otherSystem, foreignRoom) == 0, "foreign room cleanup"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "cross-system rejection changed path state"); AudioPropagationHandle secondSource = 0; Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); @@ -265,6 +307,14 @@ int main() { RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); Require(untouched(), "render with an unknown format wrote an output"); + reset(); + configured.flags = 0; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "active path configuration"); + RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "active path silently rendered"); + Require(untouched(), "unsupported active-path render modified output"); + configured.flags = AudioPropagation::PathInvalid; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "invalidating path"); + Require(sceAudioPropagationSourceDestroy(system, secondSource) == 0, "second source destroy failed"); reset(); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render of a destroyed source"); @@ -274,13 +324,18 @@ int main() { RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceDestroy(system + 16, source); }, "source destroy with another system"); Require(sceAudioPropagationPortalDestroy(system, portal) == 0, "portal destroy failed"); Require(sceAudioPropagationSourceDestroy(system, source) == 0, "source destroy failed"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationPathGetNumPoints(path, &count); }, "path survived source destruction"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(lastPath, &configured, 1.0f); }, "path update after source destruction"); Require(sceAudioPropagationRoomDestroy(system, rooms[0]) == 0, "room destroy failed"); AudioPropagationHandle lateSource = 0; AudioPropagationHandle lateMaterial = 0; Require(sceAudioPropagationSourceCreate(system, &lateSource) == 0, "late source create failed"); + AudioPropagationHandle latePath = 0; + Require(sceAudioPropagationSourceGetAudioPath(lateSource, 0, &latePath) == 0 && latePath != path, "path handle reuse"); Require(sceAudioPropagationSystemRegisterMaterial(system, material, &lateMaterial) == 0, "late material register failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "system destroy failed"); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationPathGetNumPoints(latePath, &count); }, "path survived system destruction"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAttributes(lateSource, &attribute, 1); }, "source used after its system"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationRoomDestroy(system, lateSource); }, "orphan destroyed as the wrong kind"); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceDestroy(system + 16, lateSource); }, "orphan destroyed with another system"); @@ -292,7 +347,6 @@ int main() { Require(sceAudioPropagationSystemCreate(&options, &memory, &system) == 0, "memory reuse after destroy failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "second destroy failed"); - RequireThrows<std::runtime_error>([&] { sceAudioPropagationPathGetNumPoints(); }, "path get num points throw"); RequireThrows<std::runtime_error>([&] { sceAudioPropagationSystemLock(); }, "system lock throw"); RequireThrows<std::runtime_error>([&] { sceAudioPropagationMaterialInit(); }, "material init throw"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index cff417667f..33f0ca3d13 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,10 +2,10 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose no paths or rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose 36 initially empty, invalid path slots and no rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. ### Unknown function info -- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. +- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): native evidence establishes a 64-bit path handle, a read-only 224-byte path description and a float gain. The four 48-byte points contain a room handle, gain and position; trailing eight-byte fields remain opaque. Updates copy the description and validate room ownership. Each source owns 36 initially invalid, empty slots, following public SharpProspero MaxPaths and SAROS's immediate lookup of slot 0. Allocation before calculation, the count of 36 and update validation precedence are inferred, not measured on a console. Native lookup error codes and their handle/null/index precedence are retained. Source/system destruction invalidates owned slots. Active nonempty path rendering still throws. - [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. From 7bfcc9d07aa1a5b57af1659a3c475d4a5f85a689 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Wed, 21 Oct 2026 05:29:56 -0400 Subject: [PATCH 0896/1360] fix(audio-propagation): accept the public render output formats sceAudioPropagationSourceRender accepted only format 2. The RenderInfo word at +0x28 is SharpProspero's AudioPropagationOutputFormat: 0, 1 and 2 are first- to third-order Ambisonics (4, 9 and 16 float channels), 4 and 5 are 8 interleaved float channels. SAROS (PPSA07631) renders format 0. Other values still throw, and an output that is not a whole number of frames is rejected. Based on db91769497 from the JJenkx fork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAudioPropagation/Export.cpp | 15 +++++++-- core/libs/tests/GuestAudioPropagation.cpp | 32 ++++++++++++++++--- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 3458ff2530..2d5b0afba2 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -394,13 +394,22 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy requireSystem(state, system, __func__); const auto& configuration = *reinterpret_cast<const SystemState*>(system); require(count <= configuration.maxSources, __func__); - const auto requiredBytes = static_cast<std::size_t>(configuration.frameSamples) * 16 * sizeof(float); for (std::uint32_t index = 0; index < count; ++index) { const auto& info = infos[index]; requireDescriptor(&info.desc, RenderInfoId, RenderInfoSize, __func__); require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); - require(info.output != nullptr && info.outputSize >= requiredBytes, __func__); - if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + require(info.output != nullptr && info.outputSize != 0, __func__); + std::uint32_t channels = 0; + switch (info.format) { + case 0: channels = 4; break; + case 1: channels = 9; break; + case 2: channels = 16; break; + case 4: + case 5: channels = 8; break; + default: throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + } + const auto requiredBytes = static_cast<std::size_t>(configuration.frameSamples) * channels * sizeof(float); + require(info.outputSize >= requiredBytes && info.outputSize % (channels * sizeof(float)) == 0, __func__); for (const auto handle : state.sourcePaths.at(info.source)) { const auto& path = state.paths.at(handle); if (path.gain > 0.0f && path.data.pointCount != 0 && (path.data.flags & PathInvalid) == 0) { diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index ebf9b6874a..b98463d30c 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -174,6 +174,9 @@ int main() { point.desc = {AudioPropagation::PathPointId, sizeof(point)}; point.gain = 1.0f; } + configured.points[0].room = rooms[0]; + configured.points[0].opaque[0] = std::byte{0x7f}; + configured.opaque[0] = std::byte{0x55}; configured.points[0].position[0] = 3.0f; Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "path configuration"); configured.pointCount = 0; @@ -186,8 +189,11 @@ int main() { badPath.pointCount = AudioPropagation::MaxPathPoints + 1; RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "too many path points"); badPath = configured; - badPath.points[0].room = materialHandle; + AudioPropagationHandle pathMaterial = 0; + Require(sceAudioPropagationSystemRegisterMaterial(system, material, &pathMaterial) == 0, "path material creation"); + badPath.points[0].room = pathMaterial; RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "material used as a path room"); + Require(sceAudioPropagationSystemUnregisterMaterial(pathMaterial) == 0, "path material cleanup"); badPath = configured; badPath.points[0].position[0] = std::numeric_limits<float>::infinity(); RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "nonfinite point position"); @@ -302,10 +308,28 @@ int main() { reset(); infos[0].outputSize = 0; RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "render with an empty output"); + for (const std::uint32_t format : {0u, 1u, 2u, 4u, 5u}) { + reset(); + infos[0].format = format; + const std::size_t channels = format == 0 ? 4 : format == 1 ? 9 : format == 2 ? 16 : 8; + infos[0].outputSize = sizeof(float) * options.frameSamples * channels; + Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render in a known format failed"); + Require(untouched(), "empty source modified a mix in a known format"); + infos[0].outputSize -= sizeof(float) * channels; + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "known format accepted an undersized frame"); + Require(untouched(), "undersized known-format output modified a mix"); + } reset(); - infos[1].format = 1; - RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); - Require(untouched(), "render with an unknown format wrote an output"); + infos[0].format = 1; + infos[0].outputSize = sizeof(float) * (options.frameSamples * 9 + 1); + RequireThrows<std::invalid_argument>([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "render of a partial nine-channel frame"); + Require(untouched(), "render of a partial frame wrote an output"); + for (const std::uint32_t format : {3u, 6u}) { + reset(); + infos[1].format = format; + RequireThrows<std::runtime_error>([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); + Require(untouched(), "render with an unknown format wrote an output"); + } reset(); configured.flags = 0; diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 33f0ca3d13..31f184e618 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): native evidence establishes a 64-bit path handle, a read-only 224-byte path description and a float gain. The four 48-byte points contain a room handle, gain and position; trailing eight-byte fields remain opaque. Updates copy the description and validate room ownership. Each source owns 36 initially invalid, empty slots, following public SharpProspero MaxPaths and SAROS's immediate lookup of slot 0. Allocation before calculation, the count of 36 and update validation precedence are inferred, not measured on a console. Native lookup error codes and their handle/null/index precedence are retained. Source/system destruction invalidates owned slots. Active nonempty path rendering still throws. -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host accepts public formats 0/1/2/4/5 (4/9/16/8/8 float channels), preserves the output mix without path contributions and clears the returned channel count; non-format-2 behavior remains unverified on a console. Frame capacity and source-count limits follow the recovered options. Other formats throw; native validation precedence remains unimplemented. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. From e5dc85e483dacae3a8b93a12690081bd06778470 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:44:34 +0200 Subject: [PATCH 0897/1360] test(tools): find a C export shadowing a spaced or split assembly export --- tools/tests/test_check_conventions_comments.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 36dfed40a7..6bc843e5d2 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -471,6 +471,18 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + def test_export_shadowing_a_spaced_or_split_assembly_export_is_a_duplicate(self): + for macro, call in (("APS5_ASM_FUNCTION", ' ("setjmp_nid_postfix")'), + ("APS5_ASM_FUNCTION", '(\n "setjmp_nid_postfix")'), + ("APS5_SIGNAL_ASM_FUNCTION", ' (\n "setjmp_nid_postfix")')): + with self.subTest(macro=macro, call=call): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n ' + macro + call + '\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + def test_assembly_export_in_another_library_is_a_duplicate(self): result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", 'asm(".text\\n"\n APS5_SIGNAL_ASM_FUNCTION("sceKernelGetProcessTime")\n " ret\\n");\n') From 6b80e0db0668665c42925ec24c403dcbca246169 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Wed, 21 Oct 2026 07:38:48 -0500 Subject: [PATCH 0898/1360] test(libSceRtc): cover sceRtcInit and sceRtcEnd lifecycle --- core/libs/tests/GuestRtc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a5585f843d..f5ab7ac79b 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -67,6 +67,8 @@ int APS5_VABI sceRtcSetCurrentTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick*); +int APS5_VABI sceRtcInit(void); +int APS5_VABI sceRtcEnd(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -891,4 +893,6 @@ int main() { Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } + Require(sceRtcInit() == 0); + Require(sceRtcEnd() == 0); } From c3bd7c5f638f6e690fc47a9828642e79c8cd2ca9 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:24:10 +0200 Subject: [PATCH 0899/1360] docs(debt): note that a title call of sceAjmBatchJobSetResampleParametersEx agrees with the argument registers Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAjm.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 2f7798c679..a349de83cc 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -11,7 +11,7 @@ - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` -- [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. +- [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`, and PPSA02225's one call agrees with that layout (`rdi`, `esi`, `xmm0` and `xmm1` as two single floats, `edx`, `rcx`) without showing `flags` or the float roles. With a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 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 From 78bfaba736ea2739d3fe9a32091765ad800b55f5 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:01:33 -0300 Subject: [PATCH 0900/1360] test(relinker): exercise kernel variant imports --- core/relinker/CMakeLists.txt | 8 ++ .../test_windows_kernel_variant_imports.py | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 core/relinker/relinker/tests/test_windows_kernel_variant_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa78fbaa42..6f95673303 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -666,6 +666,14 @@ if(BUILD_TESTING) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $<TARGET_FILE:relinker> ${windows_stdio_host_arguments}) + if(WIN32) + foreach(kernelVariant IN ITEMS sys web) + add_test(NAME windows_kernel_variant_${kernelVariant} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_kernel_variant_imports.py + $<TARGET_FILE:relinker> libkernel_${kernelVariant}.prx) + set_tests_properties(windows_kernel_variant_${kernelVariant} PROPERTIES TIMEOUT 30) + endforeach() + endif() add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $<TARGET_FILE:relinker>) add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py b/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py new file mode 100644 index 0000000000..2335566e12 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py @@ -0,0 +1,77 @@ +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from guest_import_fixtures import executable +from test_windows_import_modules import provider + + +def main(): + assert os.name == 'nt', 'This test executes Windows startup stubs' + relinker = Path(sys.argv[1]).resolve() + variant = sys.argv[2] + assert variant in ('libkernel_sys.prx', 'libkernel_web.prx') + with tempfile.TemporaryDirectory(prefix='anyps5-kernel-variant-') as directory: + work = Path(directory) + modules = work / 'helpers' / 'prx' + modules.mkdir(parents=True) + for name, value in (('variant', 11), ('canonical', 22), ('runtime', 33), ('guest', 44)): + (modules / (name + '.prx')).write_bytes(provider(value)) + empty = provider(0) + empty[0x800:0x80c] = b'\0other#A#B\0\0' + (modules / 'empty.prx').write_bytes(empty) + source = modules.parent / 'input.elf' + source.write_bytes(executable('variant.prx', 'shared#A#B')) + result = subprocess.run([str(relinker), '--windows', str(source), str(modules.parent / 'output.exe')], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, result.stderr + converted = modules.parent / 'app0' / 'prx' + images = {name: (converted / (name + '.prx.guest.prx')).read_bytes() + for name in ('variant', 'canonical', 'runtime', 'guest', 'empty')} + cases = ( + ('runtime-fallback', 'empty', 'empty', 'runtime', 33), + ('variant-first', 'variant', 'canonical', 'runtime', 11), + ('canonical-before-runtime', 'empty', 'canonical', 'runtime', 22), + ('missing', 'empty', 'empty', 'empty', None), + ('guest-isolation', 'empty', 'empty', 'runtime', 33), + ('guest-cannot-fill-missing', 'empty', 'empty', 'empty', None), + ('unrelated-host', 'empty', 'empty', 'empty', None), + ('guest-requested-variant', 'variant', 'canonical', 'runtime', 44), + ('unqualified', 'variant', 'canonical', 'runtime', 11), + ) + for label, requested, canonical, runtime, expected in cases: + case = work / label + libraries = case / 'libs' + libraries.mkdir(parents=True) + (case / 'prx').mkdir() + for name, image in ((variant, requested), ('libkernel.prx', canonical), ('libc.prx', runtime)): + (libraries / name).write_bytes(images[image]) + if label.startswith('guest-'): + guests = case / 'sce_module' + guests.mkdir() + names = (variant,) if label == 'guest-requested-variant' else ('libkernel.prx', 'libc.prx') + for name in names: + (guests / name).write_bytes(provider(44)) + extra = ('unrelated.prx',) if label == 'unrelated-host' else () + if extra: + (libraries / extra[0]).write_bytes(images['guest']) + source = case / 'input.elf' + symbol = 'shared' if label == 'unqualified' else 'shared#A#B' + source.write_bytes(executable(variant, symbol, extra_dependencies=extra)) + output = case / 'output.exe' + result = subprocess.run([str(relinker), '--windows', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (variant, label, result.stderr) + result = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert result.returncode & 0xffffffff == 0xc0000139, (label, result.returncode, result.stderr) + assert 'unresolved ELF import shared' in result.stderr, (label, result.stderr) + else: + assert result.returncode == expected, (variant, label, result.returncode, result.stderr) + print(variant + ': runtime, precedence and isolation checks passed') + + +if __name__ == '__main__': + main() From c9b80f0304520f4f66c14047fb8aed94b4ab52fb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:38:29 +0000 Subject: [PATCH 0901/1360] feat(libc): implement _Cosh --- core/libs/prx/libc/src/Math.cpp | 4 ++++ core/libs/tests/GuestMath.cpp | 6 ++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index bfed7a8a4d..a97b43ba03 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -750,6 +750,10 @@ double APS5_VABI _Sinh_nid_postfix(double x, double y) { return SonySinh(x, y); } +double APS5_VABI _Cosh_nid_postfix(double x, double y) { + return y * std::cosh(x); +} + int APS5_VABI _Fltrounds_nid_postfix(void) { return SonyFltrounds(); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index d7a52ec814..b18de304b3 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -129,6 +129,7 @@ std::int64_t APS5_VABI lround_nid_postfix(double); std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); +double APS5_VABI _Cosh_nid_postfix(double, double); float APS5_VABI coshf_nid_postfix(float); float APS5_VABI sinhf_nid_postfix(float); float APS5_VABI _FSinh_nid_postfix(float, float); @@ -839,6 +840,11 @@ static void CheckExtendedMath() { static void CheckHyperbolic() { const double infinity = std::numeric_limits<double>::infinity(); + Require(_Cosh_nid_postfix(0.0, 2.0) == 2.0); + Require(Near(_Cosh_nid_postfix(1.0, 2.0), 3.0861612696304874, 1e-15)); + Require(Near(_Cosh_nid_postfix(-1.0, -2.0), -3.0861612696304874, 1e-15)); + Require(std::isinf(_Cosh_nid_postfix(infinity, -1.0)) && std::signbit(_Cosh_nid_postfix(infinity, -1.0))); + Require(std::isnan(_Cosh_nid_postfix(infinity, 0.0))); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); Require(cosh_nid_postfix(0.) == 1. && coshf_nid_postfix(0.f) == 1.f); Require(asinh_nid_postfix(0.) == 0. && std::signbit(asinh_nid_postfix(-0.)) && std::signbit(asinhf_nid_postfix(-0.f))); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ef9b20680c..ec33dc2245 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -54,6 +54,7 @@ ### Functional +- [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) returns `y * cosh(x)` through the host math library, following the two-argument Dinkumware CRT helper used by `std::complex`; the signature and result are not verified against the console, including its `errno` and floating-point exception behavior. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. From 72316ad6185bd74cef624cd6f87def0c2c8bd22d Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:59:10 +0000 Subject: [PATCH 0902/1360] fix(libc): scale large finite cosh inputs --- core/libs/prx/libc/src/Math.cpp | 18 +++++++++++++++++- core/libs/tests/GuestMath.cpp | 3 +++ docs/dev/debt/libc.md | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index a97b43ba03..37616b7be3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -751,7 +751,23 @@ double APS5_VABI _Sinh_nid_postfix(double x, double y) { } double APS5_VABI _Cosh_nid_postfix(double x, double y) { - return y * std::cosh(x); + if (!std::isfinite(x) || !std::isfinite(y)) return y * std::cosh(x); + if (y == 0.0) return std::copysign(0.0, y); + constexpr double Ln2High = 0x1.62e42fefa0000p-1; + constexpr double Ln2Low = 0x1.cf79abc9e3b3ap-40; + constexpr double InverseLn2 = 0x1.71547652b82fep+0; + const double magnitude = std::abs(x); + if (magnitude <= (std::numeric_limits<double>::max_exponent - 1) * (Ln2High + Ln2Low)) + return y * std::cosh(x); + int scaleExponent; + const double scale = std::frexp(y, &scaleExponent); + constexpr int MaximumExponentSpan = std::numeric_limits<double>::max_exponent - + std::numeric_limits<double>::min_exponent + std::numeric_limits<double>::digits + 4; + if (magnitude > MaximumExponentSpan * (Ln2High + Ln2Low)) + return std::scalbn(scale, INT_MAX); + const int exponent = static_cast<int>(magnitude * InverseLn2); + const double reduced = std::fma(-exponent, Ln2Low, std::fma(-exponent, Ln2High, magnitude)); + return std::scalbn(scale * (0.5 * std::exp(reduced)), scaleExponent + exponent); } int APS5_VABI _Fltrounds_nid_postfix(void) { diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index b18de304b3..04541e7c79 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -845,6 +845,9 @@ static void CheckHyperbolic() { Require(Near(_Cosh_nid_postfix(-1.0, -2.0), -3.0861612696304874, 1e-15)); Require(std::isinf(_Cosh_nid_postfix(infinity, -1.0)) && std::signbit(_Cosh_nid_postfix(infinity, -1.0))); Require(std::isnan(_Cosh_nid_postfix(infinity, 0.0))); + Require(Near(_Cosh_nid_postfix(711.0, 1e-300), 0x1.2190c08e2f1a4p+28, 1e-15)); + Require(_Cosh_nid_postfix(720.0, 0.0) == 0.0 && !std::signbit(_Cosh_nid_postfix(720.0, 0.0))); + Require(_Cosh_nid_postfix(720.0, -0.0) == 0.0 && std::signbit(_Cosh_nid_postfix(720.0, -0.0))); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); Require(cosh_nid_postfix(0.) == 1. && coshf_nid_postfix(0.f) == 1.f); Require(asinh_nid_postfix(0.) == 0. && std::signbit(asinh_nid_postfix(-0.)) && std::signbit(asinhf_nid_postfix(-0.f))); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ec33dc2245..31dd1eae36 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -54,7 +54,7 @@ ### Functional -- [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) returns `y * cosh(x)` through the host math library, following the two-argument Dinkumware CRT helper used by `std::complex`; the signature and result are not verified against the console, including its `errno` and floating-point exception behavior. +- [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. From 2236680340b3de46e51eb2283e23fa2c84b3de97 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:07:41 +0200 Subject: [PATCH 0903/1360] fix(libSceNpAuth): reject completion queries before request start --- core/libs/prx/libSceNpAuth/Export.cpp | 6 ++-- core/libs/tests/GuestNpAuth.cpp | 41 +++++++++++++++++++++++++-- docs/dev/debt/libSceNpAuth.md | 2 ++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 265c3fd4df..3289f920f0 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -84,7 +84,6 @@ int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, voi if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; if (it->second.aborted) { - it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; return ORBIS_NP_AUTH_ERROR_ABORTED; } it->second.processed = true; @@ -102,7 +101,6 @@ int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_toke if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; if (it->second.aborted) { - it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; return ORBIS_NP_AUTH_ERROR_ABORTED; } it->second.processed = true; @@ -125,7 +123,7 @@ int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; - if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.isAsync || !it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ID; if (!it->second.polled) { it->second.polled = true; return NP_POLL_ASYNC_RUNNING; @@ -154,7 +152,7 @@ int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; - if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.isAsync || !it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ID; *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 95b11eddc5..695d37a67d 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -2,6 +2,7 @@ #include <cstdint> #include <cstdlib> +#include <initializer_list> extern "C" { int APS5_VABI sceNpAuthAbortRequest(int req_id); @@ -26,7 +27,43 @@ static void Require(bool condition) { } } +static void RequireUnstarted(int request) { + constexpr int invalidId = static_cast<int>(0x80550307); + constexpr int sentinel = 0x13579bdf; + for (int attempt = 0; attempt != 3; ++attempt) { + int result = sentinel; + Require(sceNpAuthPollAsync(request, &result) == invalidId); + Require(result == sentinel); + Require(sceNpAuthWaitAsync(request, &result) == invalidId); + Require(result == sentinel); + } +} + +static void CheckUnstartedAborts() { + constexpr int aborted = static_cast<int>(0x80550304); + alignas(8) char parameter[16]{}; + alignas(8) char output[16]{}; + for (const bool async : {false, true}) { + const int request = async ? sceNpAuthCreateAsyncRequest(nullptr) : sceNpAuthCreateRequest(); + Require(request > 0); + RequireUnstarted(request); + Require(sceNpAuthAbortRequest(request) == 0); + Require(sceNpAuthAbortRequest(request) == 0); + RequireUnstarted(request); + for (auto getter : {sceNpAuthGetIdToken, sceNpAuthGetIdTokenV3, sceNpAuthGetAuthorizationCode}) { + Require(getter(request, parameter, output) == aborted); + RequireUnstarted(request); + } + int issuer = 0x2468ace; + Require(sceNpAuthGetAuthorizationCodeV3(request, parameter, output, &issuer) == aborted); + Require(issuer == 0x2468ace); + RequireUnstarted(request); + Require(sceNpAuthDeleteRequest(request) == 0); + } +} + int main() { + CheckUnstartedAborts(); constexpr int invalidArgument = static_cast<int>(0x80550301); constexpr int signedOut = static_cast<int>(0x80550006); constexpr int aborted = static_cast<int>(0x80550304); @@ -130,8 +167,8 @@ int main() { Require(sceNpAuthGetIdTokenV3(abortedReq, param, token) == aborted); Require(sceNpAuthGetAuthorizationCodeV3(abortedReq, param, authCode, &issuerId) == aborted); int abortedWait = 0; - Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == 0); - Require(abortedWait == aborted); + Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == invalidId); + Require(abortedWait == 0); const int syncReqA = sceNpAuthCreateRequest(); Require(sceNpAuthGetIdTokenA(syncReqA, nullptr, token) == invalidArgument); diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 9f41ec750a..98b7bde5c9 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -4,6 +4,8 @@ - [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified +- [sceNpAuthPollAsync and sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - PS5 abort-before-start behavior is unmeasured. Poll/Wait keep returning `ORBIS_NP_AUTH_ERROR_INVALID_ID` without changing output after a pre-start abort, including subsequent rejected getter calls, following [shadPS4 StartRequest and FindAsyncRequest](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp). + ### Functional - [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp): native validation and storage are implemented: retry must be nonnegative, the all-zero tuple is invalid, resolve timeout is zero or at least 1000000, and other timeouts are zero or at least 10000000. Invalid arguments precede request lookup. Stored settings currently have no transport to configure because authentication remains offline. From c13218735e4cb1ddc4050c1f1e995e93125da160 Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:25:26 +0500 Subject: [PATCH 0904/1360] feat(libSceNpTus): create TSS title contexts in the TUS context table sceNpTssCreateNpTitleCtxA creates a context like sceNpTusCreateNpTitleCtxA, and sceNpTssCreateNpTitleCtx checks its NpId and service label first, as shadPS4 does; both share the TUS table, its 32-context limit and its requests. --- core/libs/prx/libSceNpTus/Export.cpp | 28 +++++++++++++++---------- core/libs/tests/GuestNpTus.cpp | 31 ++++++++++++++++++++++++++++ docs/dev/debt/libSceNpTus.md | 3 +-- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index a69e6cb5a6..f0bdbbe955 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -75,6 +75,16 @@ int Complete(int reqId, int asyncResult) { if (const int error = WaitOffline(reqId, &result)) return error; return result; } + +constexpr std::uint32_t INVALID_SERVICE_LABEL = 0xFFFFFFFF; + +int CreateTitleContext() { + std::lock_guard lock(tusMutex); + if (titleContexts.size() >= MAX_TITLE_CONTEXTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int contextId = nextTitleContext++; + titleContexts.insert(contextId); + return contextId; +} } extern "C" { @@ -82,11 +92,7 @@ extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { (void)serviceLabel; (void)userId; - std::lock_guard lock(tusMutex); - if (titleContexts.size() >= MAX_TITLE_CONTEXTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; - const int contextId = nextTitleContext++; - titleContexts.insert(contextId); - return contextId; + return CreateTitleContext(); } int APS5_VABI sceNpTusCreateRequest(int titleCtxId) { @@ -164,14 +170,14 @@ int APS5_VABI sceNpTusSetThreadParam(void) { return 0; } -int APS5_VABI sceNpTssCreateNpTitleCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTssCreateNpTitleCtx(std::uint32_t serviceLabel, const NpId* npId) { + if (!npId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (serviceLabel == INVALID_SERVICE_LABEL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return CreateTitleContext(); } -int APS5_VABI sceNpTssCreateNpTitleCtxA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTssCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { + return sceNpTusCreateNpTitleCtxA(serviceLabel, userId); } int APS5_VABI sceNpTusCreateTitleCtx(void) { diff --git a/core/libs/tests/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp index 11d9cf5cf5..d252efcfaa 100644 --- a/core/libs/tests/GuestNpTus.cpp +++ b/core/libs/tests/GuestNpTus.cpp @@ -8,6 +8,8 @@ extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId); +int APS5_VABI sceNpTssCreateNpTitleCtx(std::uint32_t serviceLabel, const NpId* npId); +int APS5_VABI sceNpTssCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); int APS5_VABI sceNpTusCreateRequest(int titleCtxId); int APS5_VABI sceNpTusDeleteRequest(int reqId); int APS5_VABI sceNpTusAbortRequest(int reqId); @@ -51,6 +53,34 @@ static void RequireFinished(int reqId, int expected) { Require(sceNpTusPollAsync(reqId, &result) == 0 && result == expected); } +static void CheckTssTitleContexts() { + const int shared = sceNpTssCreateNpTitleCtxA(0, 1); + Require(shared > 0); + const int request = sceNpTusCreateRequest(shared); + Require(request > 0); + Require(sceNpTusDeleteNpTitleCtx(shared) == 0); + Require(sceNpTusDeleteNpTitleCtx(shared) == INVALID_ID); + Require(sceNpTusDeleteRequest(request) == INVALID_ID); + + const NpId npId{}; + Require(sceNpTssCreateNpTitleCtx(0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, &npId) == INVALID_ARGUMENT); + + std::array<int, 32> contexts{}; + for (std::size_t index = 0; index < contexts.size(); ++index) { + const std::size_t form = index % 3; + contexts[index] = form == 0 ? sceNpTusCreateNpTitleCtxA(0, 1) : form == 1 ? sceNpTssCreateNpTitleCtxA(0, 1) : sceNpTssCreateNpTitleCtx(0, &npId); + Require(contexts[index] > 0); + } + Require(sceNpTssCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + Require(sceNpTssCreateNpTitleCtx(0, &npId) == TOO_MANY_OBJECTS); + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + Require(sceNpTssCreateNpTitleCtx(0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, &npId) == INVALID_ARGUMENT); + for (const int context : contexts) Require(sceNpTusDeleteNpTitleCtx(context) == 0); +} + static void CheckMultiSlotVariables() { const int context = sceNpTusCreateNpTitleCtxA(0, 1); Require(context > 0); @@ -271,4 +301,5 @@ int main() { Require(sceNpTusCreateRequest(contexts[1]) > 0); for (const int child : children) Require(sceNpTusPollAsync(child, &result) == INVALID_ID); for (std::size_t index = 1; index < contexts.size(); ++index) Require(sceNpTusDeleteNpTitleCtx(contexts[index]) == 0); + CheckTssTitleContexts(); } diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 141e50a1cc..6532df0037 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -5,8 +5,6 @@ - [TUS variable outputs](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layouts of the variable arrays of the `sceNpTusGetMultiSlotVariable*` calls and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` are not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked; shadPS4, and RPCS3 in its PS3 `sceNpTusGetMultiSlotVariable*` and `sceNpTusAddAndGetVariable*` calls, return `INVALID_ALIGNMENT` unless the size is `arrayLen` variables of the call's type, or one variable for `outVariable`. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. - [Multi-slot variable calls](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the multi-slot get, set and delete calls are not measured on a console; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_tus/np_tus.cpp), where they differ between functions: `arrayLen < 1` is `INSUFFICIENT_ARGUMENT` in the NpId and virtual-user delete calls and in `sceNpTusGetMultiSlotVariableVUserAsync`, and `INVALID_ARGUMENT` in the others; a null NpId is accepted except in delete; an empty virtual user ID is `INVALID_ARGUMENT`. RPCS3's [PS3 module](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/Cell/Modules/sceNpTus.cpp) has only the NpId and virtual-user calls; it returns `INVALID_ARGUMENT` for an empty array in all of them, rejects a null target in get and set and does not check for an empty virtual user ID. - [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature @@ -113,4 +111,5 @@ ### Functional - [sceNpTusCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - creates a title context without checking sign-in, up to 32 contexts (`TOO_MANY_OBJECTS` beyond); shadPS4 returns the `sceNpGetNpId` error instead. Requests are limited to 256, and the limits are not measured on a console. +- [sceNpTssCreateNpTitleCtx and sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - that TSS title contexts share the TUS context table and its 32-context limit is inferred from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L146-L168), which redirects both to the TUS creators, and from TSS requests coming from `sceNpTusCreateRequest`; nothing is measured on a console. The `A` form is `sceNpTusCreateNpTitleCtxA`. The `NpId` form returns `INSUFFICIENT_ARGUMENT` for a null `npId` and `INVALID_ARGUMENT` for service label `0xFFFFFFFF` before the limit, as shadPS4's [sceNpTusCreateNpTitleCtx](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L122-L143) does; the signatures are shadPS4's and fpPS4's, and Orbital's for the `A` form. - [sceNpTusPollAsync, sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - there is no PSN, so every implemented `*Async` call whose arguments pass the checks starts a request that finishes at once with `SCE_NP_ERROR_SIGNED_OUT`, which poll and wait report and the blocking calls return; shadPS4 returns `SIGNED_OUT` from the `*Async` call itself when no shadNet client is signed in, and RPCS3 returns `INVALID_ONLINE_ID` while offline, marked there as probable behaviour. An abort stays set, so every later operation on the request, blocking ones included, finishes with `ABORTED`; in shadPS4 an abort only ends the operation in progress. Until an operation has started, poll returns 1 and wait returns `INVALID_ARGUMENT` as in shadPS4; RPCS3 [blocks until a result is set](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/NP/np_contexts.cpp#L40-L52). From 23e59095e89a8cccb22caacf904c413cf71ca81d Mon Sep 17 00:00:00 2001 From: Joacoorandom <62301477+Joacoorandom@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:30:41 -0300 Subject: [PATCH 0905/1360] test(tools): cover repeated pad timeline capacity boundaries --- tools/tests/test_pad_timeline.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py index cc40a5b7d8..887310714f 100644 --- a/tools/tests/test_pad_timeline.py +++ b/tools/tests/test_pad_timeline.py @@ -74,6 +74,23 @@ class TimelineTests(unittest.TestCase): with self.assertRaises(ValueError): pad_timeline.compile_timeline(self.document, 1, repeat, gap) + def test_repeated_event_count_at_capacity(self): + rows = pad_timeline.compile_timeline(self.document, 42, 2048, 0).splitlines() + self.assertEqual(rows[0], "APS5_PAD_TIMELINE 1 42 4096") + self.assertEqual(len(rows) - 1, 4096) + self.assertEqual(rows[1:3], ["200 100 18432", "500 200 64"]) + self.assertEqual(rows[-2:], ["1433100 100 18432", "1433400 200 64"]) + with self.assertRaisesRegex(ValueError, "1 to 4096 events after repetition"): + pad_timeline.compile_timeline(self.document, 42, 2049, 0) + + def test_repeated_duration_includes_inter_iteration_gaps_only(self): + document = {"version": 1, "events": [{"at_ms": 0, "hold_ms": 5000, "buttons": ["cross"]}]} + self.assertEqual( + pad_timeline.compile_timeline(document, 42, 2, 3590000), + "APS5_PAD_TIMELINE 1 42 2\n0 5000 16384\n3595000 5000 16384\n") + with self.assertRaisesRegex(ValueError, "repeated timeline exceeds one hour"): + pad_timeline.compile_timeline(document, 42, 2, 3590001) + def test_sequence_bounds(self): for sequence in [0, -1, True, 2**64]: with self.assertRaises(ValueError): From 6f9b6bad1f351b8cee82be604116ff0d59465aa9 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:41:48 +0200 Subject: [PATCH 0906/1360] feat(libSceFreeType): implement outline orientation --- core/libs/prx/libSceFreeType/Export.cpp | 39 ++++++++ core/libs/tests/GuestFreeType.cpp | 119 ++++++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 3 files changed, 160 insertions(+) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0c4f8d79b1..adcdc01533 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -1,7 +1,10 @@ #include "prx/libSceFreeType/include/GuestObjects.hpp" +#include FT_OUTLINE_H #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" +#include <algorithm> +#include <bit> #include <cstdint> #include <filesystem> #include <fstream> @@ -195,6 +198,42 @@ int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec* face, GuestCharMapRec* ch return FT_Err_Invalid_Argument; } +int APS5_VABI FT_Outline_Get_Orientation_nid_postfix(GuestOutline* outline) { + if (!outline || outline->n_points == 0) return FT_ORIENTATION_TRUETYPE; + const auto initial = outline->points[0]; + GuestBBox bounds{initial.x, initial.y, initial.x, initial.y}; + for (unsigned index = 1; index < outline->n_points; ++index) { + const auto point = outline->points[index]; + bounds.xMin = std::min(bounds.xMin, point.x); + bounds.yMin = std::min(bounds.yMin, point.y); + bounds.xMax = std::max(bounds.xMax, point.x); + bounds.yMax = std::max(bounds.yMax, point.y); + } + if (bounds.xMin == bounds.xMax || bounds.yMin == bounds.yMax || + bounds.xMin < -0x1000000 || bounds.yMin < -0x1000000 || + bounds.xMax > 0x1000000 || bounds.yMax > 0x1000000) return FT_ORIENTATION_NONE; + const auto xMagnitude = static_cast<std::uint32_t>((bounds.xMin < 0 ? -bounds.xMin : bounds.xMin) | + (bounds.xMax < 0 ? -bounds.xMax : bounds.xMax)); + const int xShift = std::max(0, static_cast<int>(std::bit_width(xMagnitude)) - 15); + const int yShift = std::max(0, static_cast<int>(std::bit_width(static_cast<std::uint32_t>(bounds.yMax - bounds.yMin))) - 15); + std::int64_t area = 0; + unsigned first = 0; + for (unsigned contour = 0; contour < outline->n_contours; ++contour) { + const unsigned last = outline->contours[contour]; + auto previousX = outline->points[last].x >> xShift; + auto previousY = outline->points[last].y >> yShift; + for (unsigned index = first; index <= last; ++index) { + const auto x = outline->points[index].x >> xShift; + const auto y = outline->points[index].y >> yShift; + area += (y - previousY) * (x + previousX); + previousX = x; + previousY = y; + } + first = last + 1; + } + return area > 0 ? FT_ORIENTATION_POSTSCRIPT : area < 0 ? FT_ORIENTATION_TRUETYPE : FT_ORIENTATION_NONE; +} + std::uint64_t APS5_VABI FT_Get_First_Char_nid_postfix(GuestFaceRec* face, std::uint32_t* glyphIndex) { const GuestFace* owner = FaceOf(face); return FT_Get_First_Char(owner ? owner->host : nullptr, glyphIndex); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 8e5c7e7d0f..61dd827276 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -6,6 +6,7 @@ #include FT_FREETYPE_H #include FT_OUTLINE_H +#include <algorithm> #include <array> #include <cstdint> #include <cstdio> @@ -67,6 +68,123 @@ static void FaceMatches(const GuestFaceRec* face, FT_Face host, const void* libr Require(face->glyph != nullptr && face->glyph->face == face && face->glyph->library == library && face->size != nullptr && face->size->face == face); } +extern "C" int APS5_VABI FT_Outline_Get_Orientation_nid_postfix(GuestOutline*); + +static unsigned orientationCases = 0; + +static void CheckOrientationCase(std::vector<GuestVector> points, std::vector<std::uint16_t> contours, + int expected, bool compareNative = true) { + ++orientationCases; + const auto originalPoints = points; + const auto originalContours = contours; + std::vector<unsigned char> tags(points.size(), FT_CURVE_TAG_ON); + GuestOutline outline{static_cast<std::uint16_t>(contours.size()), static_cast<std::uint16_t>(points.size()), + points.data(), tags.data(), contours.data(), FT_OUTLINE_EVEN_ODD_FILL | FT_OUTLINE_REVERSE_FILL}; + const auto original = outline; + Require(FT_Outline_Get_Orientation_nid_postfix(&outline) == expected); + Require(outline.n_points == original.n_points && outline.n_contours == original.n_contours && outline.flags == original.flags); + Require(outline.points == original.points && outline.tags == original.tags && outline.contours == original.contours); + for (std::size_t index = 0; index < points.size(); ++index) { + Require(points[index].x == originalPoints[index].x && points[index].y == originalPoints[index].y); + Require(tags[index] == FT_CURVE_TAG_ON); + } + Require(contours == originalContours); + if (compareNative) { + std::vector<FT_Vector> nativePoints; + for (const auto& point : points) nativePoints.push_back({static_cast<FT_Pos>(point.x), static_cast<FT_Pos>(point.y)}); + FT_Outline native{outline.n_contours, outline.n_points, nativePoints.data(), tags.data(), contours.data(), outline.flags}; + Require(FT_Outline_Get_Orientation(&native) == expected); + } +} + +static void CheckOutlineOrientation() { + Require(FT_Outline_Get_Orientation_nid_postfix(nullptr) == FT_ORIENTATION_TRUETYPE); + CheckOrientationCase({}, {}, FT_ORIENTATION_TRUETYPE); + CheckOrientationCase({{0, 0}}, {0}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, 0}, {1, 1}}, {1}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, 0}, {1, 1}, {2, 2}}, {2}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, 0}, {40, 0}, {40, 40}, {0, 40}}, {3}, FT_ORIENTATION_POSTSCRIPT); + CheckOrientationCase({{0, 0}, {0, 40}, {40, 40}, {40, 0}}, {3}, FT_ORIENTATION_TRUETYPE); + CheckOrientationCase({{0, 0}, {40, 0}, {40, 40}, {0, 40}, {10, 10}, {10, 30}, {30, 30}, {30, 10}}, + {3, 7}, FT_ORIENTATION_POSTSCRIPT); + CheckOrientationCase({{0, 0}, {40, 0}, {40, 40}, {0, 40}, {0, 0}, {0, 40}, {40, 40}, {40, 0}}, + {3, 7}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, 0}, {40, 0}, {40, 40}, {0, 40}}, {}, FT_ORIENTATION_NONE); + CheckOrientationCase({{1048577, 0}, {1048578, 1}, {1048577, 2}}, {2}, FT_ORIENTATION_NONE); + CheckOrientationCase({{-0x1000000, -0x1000000}, {0x1000000, -0x1000000}, + {0x1000000, 0x1000000}, {-0x1000000, 0x1000000}}, {3}, FT_ORIENTATION_POSTSCRIPT); + CheckOrientationCase({{-0x1000001, 0}, {0, 100}, {100, 0}}, {2}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, -0x1000001}, {100, 0}, {0, 100}}, {2}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, 0}, {0x1000001, 100}, {100, 0}}, {2}, FT_ORIENTATION_NONE); + CheckOrientationCase({{0, 0}, {100, 0x1000001}, {0, 100}}, {2}, FT_ORIENTATION_NONE); + CheckOrientationCase({{INT64_MIN, INT64_MIN}, {INT64_MAX, INT64_MAX}, {0, 1}}, {2}, FT_ORIENTATION_NONE, false); + std::vector<GuestVector> repeated{{0, 0}, {32767, 0}, {32767, 32767}, {0, 32767}, + {0, 0}, {32767, 0}, {32767, 32767}, {0, 32767}}; + CheckOrientationCase(repeated, {3, 7}, FT_ORIENTATION_POSTSCRIPT, sizeof(FT_Pos) == 8); + if constexpr (sizeof(FT_Pos) == 4) { + FT_Vector nativePoints[]{{0, 0}, {32767, 0}, {32767, 32767}, {0, 32767}, + {0, 0}, {32767, 0}, {32767, 32767}, {0, 32767}}; + unsigned short contours[]{3, 7}; + FT_Outline native{2, 8, nativePoints, nullptr, contours, 0}; + Require(FT_Outline_Get_Orientation(&native) == FT_ORIENTATION_TRUETYPE); + } + std::vector<GuestVector> maximum(65535); + const GuestVector square[]{{0, 0}, {32767, 0}, {32767, 32767}, {0, 32767}}; + for (std::size_t index = 0; index < maximum.size(); ++index) maximum[index] = square[index % 4]; + CheckOrientationCase(maximum, {65534}, FT_ORIENTATION_POSTSCRIPT, sizeof(FT_Pos) == 8); + std::vector<std::uint16_t> singletons(65535); + for (std::size_t index = 0; index < singletons.size(); ++index) singletons[index] = static_cast<std::uint16_t>(index); + CheckOrientationCase(maximum, singletons, FT_ORIENTATION_NONE); + std::mt19937 random(0xF7013); + for (unsigned test = 0; test < 512; ++test) { + std::vector<GuestVector> points(random() % 10 + 3); + for (auto& point : points) point = {static_cast<int>(random() % 1024) - 512, static_cast<int>(random() % 1024) - 512}; + std::int64_t twiceArea = 0; + auto previous = points.back(); + for (const auto& current : points) { + twiceArea += previous.x * current.y - current.x * previous.y; + previous = current; + } + const int expected = twiceArea > 0 ? FT_ORIENTATION_POSTSCRIPT : twiceArea < 0 ? FT_ORIENTATION_TRUETYPE : FT_ORIENTATION_NONE; + const std::vector<std::uint16_t> contours{static_cast<std::uint16_t>(points.size() - 1)}; + CheckOrientationCase(points, contours, expected); + for (auto& point : points) { point.x += 1000; point.y -= 2000; } + CheckOrientationCase(points, contours, expected); + std::reverse(points.begin(), points.end()); + CheckOrientationCase(points, contours, twiceArea == 0 ? FT_ORIENTATION_NONE : expected == FT_ORIENTATION_POSTSCRIPT ? FT_ORIENTATION_TRUETYPE : FT_ORIENTATION_POSTSCRIPT); + } + const auto font = SquareGlyphFont('A', 700); + void* library = nullptr; + GuestFaceRec* face = nullptr; + FT_Library nativeLibrary = nullptr; + FT_Face native = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face(nativeLibrary, font.data(), static_cast<FT_Long>(font.size()), 0, &native) == 0); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 32) == 0 && FT_Set_Pixel_Sizes(native, 0, 32) == 0); + for (const bool reflect : {false, true}) { + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) == 0); + Require(FT_Load_Char(native, 'A', FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) == 0); + if (reflect) { + for (unsigned index = 0; index < face->glyph->outline.n_points; ++index) { + face->glyph->outline.points[index].x = -face->glyph->outline.points[index].x; + native->glyph->outline.points[index].x = -native->glyph->outline.points[index].x; + } + } + Require(FT_Outline_Get_Orientation_nid_postfix(&face->glyph->outline) == (reflect ? FT_ORIENTATION_POSTSCRIPT : FT_ORIENTATION_TRUETYPE)); + Require(FT_Outline_Get_Orientation(&native->glyph->outline) == (reflect ? FT_ORIENTATION_POSTSCRIPT : FT_ORIENTATION_TRUETYPE)); + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == 0 && FT_Render_Glyph(native->glyph, FT_RENDER_MODE_NORMAL) == 0); + const auto& actual = face->glyph->bitmap; + const auto& expected = native->glyph->bitmap; + Require(actual.width > 0 && actual.rows > 0 && actual.width == expected.width && actual.rows == expected.rows && actual.pitch == expected.pitch); + Require(actual.pixel_mode == expected.pixel_mode && face->glyph->bitmap_left == native->glyph->bitmap_left && face->glyph->bitmap_top == native->glyph->bitmap_top); + Require(std::memcmp(actual.buffer, expected.buffer, static_cast<std::size_t>(actual.rows) * std::abs(actual.pitch)) == 0); + } + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(nativeLibrary) == 0); + std::printf("FreeType outline orientation: %u polygon cases and 2 rendered glyphs passed\n", orientationCases); +} + static void SlotMatches(const GuestGlyphSlotRec* slot, FT_GlyphSlot host) { Require(slot->glyph_index == host->glyph_index && slot->format == static_cast<std::uint32_t>(host->format) && slot->next == nullptr); const auto& metrics = host->metrics; @@ -144,6 +262,7 @@ static void SetFinalizers(GuestFaceRec* face, Finalizers& events) { int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); + CheckOutlineOrientation(); const std::vector<unsigned char> font = SquareGlyphFont('A', 700); { std::ofstream file(root / "app0" / "font.ttf", std::ios::binary); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..3073be6b08 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -13,3 +13,5 @@ - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read + +- [FT_Outline_Get_Orientation](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI/version and large-outline equivalence are unverified. The adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with the existing unsigned guest counts and an LP64 area accumulator, including on Windows. From 29eab421d93bf3383b7385940d9f5e21e731867b Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:45:51 +0200 Subject: [PATCH 0907/1360] chore(libSceFreeType): isolate orientation integration points --- core/libs/prx/libSceFreeType/Export.cpp | 54 ++++++++++++------------- core/libs/tests/GuestFreeType.cpp | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index adcdc01533..14a1a26717 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -171,33 +171,6 @@ int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec* face, std::uint64_t charCod return error; } -int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec* slot, std::int32_t render_mode) { - GuestFace* owner = FaceOf(slot); - if (owner) WriteBackOutline(*owner); - const FT_Error error = FT_Render_Glyph(owner ? owner->host->glyph : nullptr, static_cast<FT_Render_Mode>(render_mode)); - if (owner) SyncSlot(*owner); - return error; -} - -int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec* face, std::int32_t encoding) { - GuestFace* owner = FaceOf(face); - const FT_Error error = FT_Select_Charmap(owner ? owner->host : nullptr, static_cast<FT_Encoding>(encoding)); - if (error == 0) owner->rec.charmap = owner->charmapPointers.at(FT_Get_Charmap_Index(owner->host->charmap)); - return error; -} - -int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec* face, GuestCharMapRec* charmap) { - GuestFace* owner = FaceOf(face); - if (!owner || !charmap || owner->charmapPointers.empty()) return FT_Set_Charmap(owner ? owner->host : nullptr, nullptr); - for (std::size_t index = 0; index < owner->charmapPointers.size(); ++index) { - if (owner->charmapPointers[index] != charmap) continue; - const FT_Error error = FT_Set_Charmap(owner->host, owner->host->charmaps[index]); - if (error == 0) owner->rec.charmap = charmap; - return error; - } - return FT_Err_Invalid_Argument; -} - int APS5_VABI FT_Outline_Get_Orientation_nid_postfix(GuestOutline* outline) { if (!outline || outline->n_points == 0) return FT_ORIENTATION_TRUETYPE; const auto initial = outline->points[0]; @@ -234,6 +207,33 @@ int APS5_VABI FT_Outline_Get_Orientation_nid_postfix(GuestOutline* outline) { return area > 0 ? FT_ORIENTATION_POSTSCRIPT : area < 0 ? FT_ORIENTATION_TRUETYPE : FT_ORIENTATION_NONE; } +int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec* slot, std::int32_t render_mode) { + GuestFace* owner = FaceOf(slot); + if (owner) WriteBackOutline(*owner); + const FT_Error error = FT_Render_Glyph(owner ? owner->host->glyph : nullptr, static_cast<FT_Render_Mode>(render_mode)); + if (owner) SyncSlot(*owner); + return error; +} + +int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec* face, std::int32_t encoding) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Select_Charmap(owner ? owner->host : nullptr, static_cast<FT_Encoding>(encoding)); + if (error == 0) owner->rec.charmap = owner->charmapPointers.at(FT_Get_Charmap_Index(owner->host->charmap)); + return error; +} + +int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec* face, GuestCharMapRec* charmap) { + GuestFace* owner = FaceOf(face); + if (!owner || !charmap || owner->charmapPointers.empty()) return FT_Set_Charmap(owner ? owner->host : nullptr, nullptr); + for (std::size_t index = 0; index < owner->charmapPointers.size(); ++index) { + if (owner->charmapPointers[index] != charmap) continue; + const FT_Error error = FT_Set_Charmap(owner->host, owner->host->charmaps[index]); + if (error == 0) owner->rec.charmap = charmap; + return error; + } + return FT_Err_Invalid_Argument; +} + std::uint64_t APS5_VABI FT_Get_First_Char_nid_postfix(GuestFaceRec* face, std::uint32_t* glyphIndex) { const GuestFace* owner = FaceOf(face); return FT_Get_First_Char(owner ? owner->host : nullptr, glyphIndex); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 61dd827276..b90c7abd75 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -262,7 +262,6 @@ static void SetFinalizers(GuestFaceRec* face, Finalizers& events) { int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); - CheckOutlineOrientation(); const std::vector<unsigned char> font = SquareGlyphFont('A', 700); { std::ofstream file(root / "app0" / "font.ttf", std::ios::binary); @@ -270,6 +269,7 @@ int main() { } const auto previous = std::filesystem::current_path(); std::filesystem::current_path(root); + CheckOutlineOrientation(); FT_Library hostLibrary = nullptr; Require(FT_Init_FreeType(&hostLibrary) == 0); From 4e7559ae9a7af136af65f8c7f76a8acbee62150b Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:03:06 -0300 Subject: [PATCH 0908/1360] fix(libSceNet): reject nonpositive socket buffers --- core/libs/prx/libSceNet/CMakeLists.txt | 11 +++ core/libs/prx/libSceNet/Export.cpp | 1 + core/libs/tests/GuestNetBufferValidation.cpp | 81 ++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 4 files changed, 94 insertions(+) create mode 100644 core/libs/tests/GuestNetBufferValidation.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 95faf4584b..6179dca866 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -8,6 +8,17 @@ if(BUILD_TESTING) target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) configure_windows_unwind(net_dontwait_tests) + add_test_executable(guest_net_buffer_validation_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetBufferValidation.cpp) + target_include_directories(guest_net_buffer_validation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_buffer_validation_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_buffer_validation_tests) + add_test(NAME guest_net_buffer_validation COMMAND guest_net_buffer_validation_tests) + set_tests_properties(guest_net_buffer_validation PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_buffer_validation_tests PRIVATE ws2_32) + set_tests_properties(guest_net_buffer_validation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNet>") + endif() foreach(api IN ITEMS send sendto sendmsg recv recvfrom recvmsg duplex blocking) add_test(NAME net_dontwait_${api} COMMAND net_dontwait_tests ${api}) set_tests_properties(net_dontwait_${api} PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 808cdc7364..8b4af062da 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1507,6 +1507,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (value < 0) return fail(NET_EINVAL); if (set_native_timeout(socket.native->value, optname, value) != 0) return fail(native_error()); } else if (optname == NET_SO_REUSEADDR || optname == NET_SO_BROADCAST || optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) { + if ((optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) && value < 1) return fail(NET_EINVAL); const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast<const char*>(&value), sizeof(value)) != 0) return fail(native_error()); diff --git a/core/libs/tests/GuestNetBufferValidation.cpp b/core/libs/tests/GuestNetBufferValidation.cpp new file mode 100644 index 0000000000..4b2c66402d --- /dev/null +++ b/core/libs/tests/GuestNetBufferValidation.cpp @@ -0,0 +1,81 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <winsock2.h> +#else +#include <sys/socket.h> +#endif +#include <climits> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <source_location> +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("buffer-validation", family, type, type == 1 ? 6 : 17); + Require(socket >= 0); + std::shared_ptr<void> owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + for (const int option : {0x1001, 0x1002}) { + const auto nativeBuffer = [&] { + int value = 0; +#ifdef _WIN32 + int size = sizeof(value); +#else + socklen_t size = sizeof(value); +#endif + Require(::getsockopt(native, SOL_SOCKET, option == 0x1001 ? SO_SNDBUF : SO_RCVBUF, + reinterpret_cast<char*>(&value), &size) == 0); + return value; + }; + const int initial = 32768; + Require(sceNetSetsockopt(socket, 0xffff, option, &initial, sizeof(initial)) == 0); + const int originalNative = nativeBuffer(); + for (const int invalid : {0, -1, -4096, INT_MIN}) { + Require(sceNetSetsockopt(socket, 0xffff, option, &invalid, sizeof(invalid)) == static_cast<int>(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(setsockopt_nid_postfix(socket, 0xffff, option, &invalid, sizeof(invalid)) == -1); + Require(*__error_nid_postfix() == 22); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == initial); + Require(nativeBuffer() == originalNative); + } + Require(sceNetSetsockopt(socket, 0xffff, option, nullptr, sizeof(initial)) == static_cast<int>(0x80410116u)); + Require(sceNetSetsockopt(socket, 0xffff, option, &initial, 3) == static_cast<int>(0x80410116u)); + const int valid = 8192; + Require(sceNetSetsockopt(socket, 0xffff, option, &valid, sizeof(valid)) == 0); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == valid); + } + Require(sceNetSocketClose(socket) == 0); + } + } + const int zero = 0; + Require(sceNetSetsockopt(-1, 0xffff, 0x1001, &zero, sizeof(zero)) == static_cast<int>(0x80410109u)); + Require(*sceNetErrnoLoc() == 9); +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f10d1eb873..dbc123082b 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -36,6 +36,7 @@ - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. +- [SO_SNDBUF and SO_RCVBUF validation](../../../core/libs/prx/libSceNet/Export.cpp) remains unverified on PS5, and positive sizes remain subject to host buffer limits. Nonpositive sizes fail with `EINVAL` before changing the native socket, following FreeBSD 11.4 [sosetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2494-L2509). - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before From 962b88a9e1ae43c07742270971ccdee0975fe067 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 14:18:58 +1300 Subject: [PATCH 0909/1360] fix(libSceNpCommon): parse sceNpStrToInt32 through a 64-bit integer on every host Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceNpCommon/Strings.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp index d5f70019e9..ce17739d6f 100644 --- a/core/libs/prx/libSceNpCommon/Strings.cpp +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -357,11 +357,11 @@ int APS5_VABI sceNpInt64ToStr(char* buf, std::size_t size, std::int64_t value, s } int APS5_VABI sceNpStrToInt32(const char* str, std::int32_t* value, int base, std::size_t* parsedLength) { - long parsed = 0; + long long parsed = 0; bool saturated = false; const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { - parsed = std::strtol(s, end, base); - saturated = parsed == LONG_MIN || parsed == LONG_MAX; + parsed = std::strtoll(s, end, base); + saturated = parsed == LLONG_MIN || parsed == LLONG_MAX; }, &saturated); if (result < 0) return result; if (parsed != static_cast<std::int32_t>(parsed)) return NP_ERROR_OUT_OF_RANGE; From 8e462a6bcc48f0430f0146218c7995feb00c5fe3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:33:53 +0000 Subject: [PATCH 0910/1360] feat(hmd): refuse distortion and 2D VR command queries without a headset sceHmdOpen never issues a handle without a headset, so the distortion correction, wide-near correction and 2D VR command queries, and their sceHmdDistortion* export aliases, answer as the other handle queries: NOT_INITIALIZED before sceHmdInitialize and HANDLE_INVALID after it, leaving the command buffer unwritten. --- core/libs/prx/libSceHmd/Export.cpp | 36 ++++++++++++++++++++---------- core/libs/prx/libSceHmd/Hmd.hpp | 10 +++++++++ core/libs/tests/GuestHmd.cpp | 15 ++++++++++++- docs/dev/debt/libSceHmd.md | 4 ++-- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 3ede810dfe..d0d055fb93 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -438,8 +438,10 @@ int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void* buffer, std::uint } int APS5_VABI sceHmdDistortionGetCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)parameter; + (void)commandBuffer; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { @@ -572,8 +574,10 @@ int APS5_VABI sceHmdInternalGetHmuSerialNumber(std::uint8_t* serialNumber) { } int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)parameter; + (void)commandBuffer; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(bool* available) { @@ -727,8 +731,10 @@ int APS5_VABI sceHmdInternalMmapGetModeId(std::uint32_t* modeId) { } int APS5_VABI sceHmdDistortionGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)parameter; + (void)commandBuffer; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { @@ -737,8 +743,10 @@ int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { } int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)parameter; + (void)commandBuffer; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayerParameter* layers, std::uint32_t layerCount, const HmdReprojectionSubmissionParameter* submission, std::uint64_t flipArg, const void* reserved) { @@ -747,8 +755,10 @@ int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayer } int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)parameter; + (void)commandBuffer; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } int APS5_VABI sceHmdInternalSetDeviceConnection(std::uint64_t connection) { @@ -962,8 +972,10 @@ int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { } int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)parameter; + (void)commandBuffer; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; } } diff --git a/core/libs/prx/libSceHmd/Hmd.hpp b/core/libs/prx/libSceHmd/Hmd.hpp index 31cf038dbb..7ec132928e 100644 --- a/core/libs/prx/libSceHmd/Hmd.hpp +++ b/core/libs/prx/libSceHmd/Hmd.hpp @@ -55,6 +55,10 @@ struct HmdFieldOfView; struct HmdEyeOffset; struct HmdInertialSensorData; struct HmdAssemblyError; +struct HmdDistortionCommandParameter; +struct HmdWideNearDistortionCommandParameter; +struct Hmd2dVrCommandParameter; +struct HmdCommandBuffer; extern "C" { std::int32_t APS5_VABI sceHmdInitialize(const Hmd::InitializeParam* param); @@ -78,6 +82,12 @@ int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, HmdFieldOfView* fieldOfV int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, HmdEyeOffset* leftOffset, HmdEyeOffset* rightOffset); int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, HmdInertialSensorData* data, std::uint32_t count); int APS5_VABI sceHmdGetAssyError(HmdAssemblyError* data); +int APS5_VABI sceHmdDistortionGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdDistortionGetCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); } #endif diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index 230395afb9..ce8db4929d 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -36,18 +36,31 @@ void CheckHandleQueries(std::int32_t expected) { unsigned char right[16]; unsigned char fieldOfView[16]; unsigned char sensor[64]; + unsigned char descriptor[64] = {}; + unsigned char commands[16]; std::memset(left, 0x5a, sizeof(left)); std::memset(right, 0x5a, sizeof(right)); std::memset(fieldOfView, 0x5a, sizeof(fieldOfView)); std::memset(sensor, 0x5a, sizeof(sensor)); + std::memset(commands, 0x5a, sizeof(commands)); + const auto* distortion = reinterpret_cast<const HmdDistortionCommandParameter*>(descriptor); + const auto* wideNear = reinterpret_cast<const HmdWideNearDistortionCommandParameter*>(descriptor); + const auto* vr2d = reinterpret_cast<const Hmd2dVrCommandParameter*>(descriptor); + auto* commandBuffer = reinterpret_cast<HmdCommandBuffer*>(commands); for (const auto handle : {0, -1, 1, 0x0f000000}) { Require(sceHmdGetFieldOfView(handle, nullptr) == Hmd::ParameterNull, "null field of view accepted"); Require(sceHmdGetFieldOfView(handle, reinterpret_cast<HmdFieldOfView*>(fieldOfView)) == expected, "field of view without a headset"); Require(sceHmdGet2DEyeOffset(handle, reinterpret_cast<HmdEyeOffset*>(left), reinterpret_cast<HmdEyeOffset*>(right)) == expected, "eye offset without a headset"); Require(sceHmdGet2DEyeOffset(handle, nullptr, nullptr) == expected, "eye offset with null outputs"); Require(sceHmdGetInertialSensorData(handle, reinterpret_cast<HmdInertialSensorData*>(sensor), 1) == expected, "sensor data without a headset"); + Require(sceHmdGetDistortionCorrectionCommand(handle, distortion, commandBuffer) == expected, "distortion command without a headset"); + Require(sceHmdDistortionGetCorrectionCommand(handle, distortion, commandBuffer) == expected, "aliased distortion command without a headset"); + Require(sceHmdGetWideNearDistortionCorrectionCommand(handle, wideNear, commandBuffer) == expected, "wide-near distortion command without a headset"); + Require(sceHmdDistortionGetWideNearCorrectionCommand(handle, wideNear, commandBuffer) == expected, "aliased wide-near distortion command without a headset"); + Require(sceHmdGet2dVrCommand(handle, vr2d, commandBuffer) == expected, "2D VR command without a headset"); + Require(sceHmdDistortionGet2dVrCommand(handle, vr2d, commandBuffer) == expected, "aliased 2D VR command without a headset"); } - for (const auto* bytes : {left, right, fieldOfView}) { + for (const auto* bytes : {left, right, fieldOfView, commands}) { for (std::size_t index = 0; index < 16; ++index) Require(bytes[index] == 0x5a, "a refused query wrote its output"); } for (const auto byte : sensor) Require(byte == 0x5a, "a refused sensor query wrote its output"); diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 3af366507e..70ea790534 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -20,6 +20,6 @@ [Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. -[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Protected bodies are unchanged. +[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Without a headset `sceHmdOpen` never issues a handle, so the queries answer as the other handle queries do: `NOT_INITIALIZED` before initialization and `HANDLE_INVALID` after it, without reading the descriptor or writing the output. Like `sceHmdGet2DEyeOffset` they do not check their pointers; whether the console checks a null descriptor or output before the handle is not measured. -The ELF export table aliases the [Distortion correction/2D command and work-memory queries](../../../core/libs/prx/libSceHmd/Export.cpp) to their corresponding HMD queries, confirming identical signatures. Distortion initialization consumes only a work-memory pointer. No shader code or calibration constants are copied; protected bodies remain unchanged. +The ELF export table aliases the [Distortion correction/2D command and work-memory queries](../../../core/libs/prx/libSceHmd/Export.cpp) to their corresponding HMD queries, confirming identical signatures. Distortion initialization consumes only a work-memory pointer. No shader code or calibration constants are copied; each aliased command query answers as its twin, and distortion initialization keeps its protected body. From a6444591140b7a86516bcfa23bbde79e4b6b6623 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:34:36 +0000 Subject: [PATCH 0911/1360] feat(hmd): report no VR2D session without a headset Nothing runs in VR2D without a headset, so sceHmdInternalIsGameVr2d, IsMiniAppVr2d, IsCommonDlgVr2d and IsCommonDlgMiniAppVr2d return false, the answer the console gives to the same four queries in libSceHmd2. --- core/libs/prx/libSceHmd/Export.cpp | 12 ++++-------- core/libs/prx/libSceHmd/Hmd.hpp | 4 ++++ core/libs/tests/GuestHmd.cpp | 4 ++++ docs/dev/debt/libSceHmd.md | 1 + 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index d0d055fb93..bbce694977 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -428,8 +428,7 @@ int APS5_VABI sceHmdReprojectionStopCapture(HmdCaptureInfo* info) { } bool APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return false; } int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void* buffer, std::uint32_t size) { @@ -454,13 +453,11 @@ int APS5_VABI sceHmdInternalGetDebugMode(std::uint8_t* mode) { } bool APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return false; } bool APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return false; } int APS5_VABI sceHmdReprojectionSetOutputMinColor(float red, float green, float blue) { @@ -601,8 +598,7 @@ int APS5_VABI sceHmdReprojectionStartLiveCapture(const HmdLiveCaptureParameter* } bool APS5_VABI sceHmdInternalIsGameVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return false; } int APS5_VABI sceHmdGetAssyError(HmdAssemblyError* data) { diff --git a/core/libs/prx/libSceHmd/Hmd.hpp b/core/libs/prx/libSceHmd/Hmd.hpp index 7ec132928e..439c94a5af 100644 --- a/core/libs/prx/libSceHmd/Hmd.hpp +++ b/core/libs/prx/libSceHmd/Hmd.hpp @@ -88,6 +88,10 @@ int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer); int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +bool APS5_VABI sceHmdInternalIsGameVr2d(void); +bool APS5_VABI sceHmdInternalIsMiniAppVr2d(void); +bool APS5_VABI sceHmdInternalIsCommonDlgVr2d(void); +bool APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void); } #endif diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index ce8db4929d..a7b8179b1c 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -20,6 +20,10 @@ void CheckDisconnected() { Require(status == Hmd::DeviceStatus::NotDetected, "headset reported connected"); Require(sceHmdInternalGetDeviceStatus(nullptr) == Hmd::ParameterNull, "null status"); Require(sceHmdInternalMmapIsConnect() == 0, "connection reported present"); + Require(!sceHmdInternalIsGameVr2d(), "game reported in VR2D without a headset"); + Require(!sceHmdInternalIsMiniAppVr2d(), "mini app reported in VR2D without a headset"); + Require(!sceHmdInternalIsCommonDlgVr2d(), "common dialog reported in VR2D without a headset"); + Require(!sceHmdInternalIsCommonDlgMiniAppVr2d(), "common dialog mini app reported in VR2D without a headset"); } void CheckBufferQueries() { diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 70ea790534..7cd2c2872d 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -9,6 +9,7 @@ - [libSceHmd internal and capture exports](../../../core/libs/prx/libSceHmd/Export.cpp) - input/output directions and argument registers are recovered for shared VR2D data, device binding, optical and serial queries, screen status and capture operations. VR2D records are 128 bytes, optical output 16, serial output 16, PU position output 20, crash-dump output 68, audio/video status 32, and reprojection debug outputs 216 or 1136. Payload field meanings and capture callback contracts remain incomplete; these structures use forward declarations. - [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count +- [libSceHmd VR2D queries](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - `sceHmdInternalIsGameVr2d`, `sceHmdInternalIsMiniAppVr2d`, `sceHmdInternalIsCommonDlgVr2d` and `sceHmdInternalIsCommonDlgMiniAppVr2d` return false: nothing runs in VR2D without a headset, and the console answers 0 to the same four queries in libSceHmd2 (`Hmd2BeforeInitializeZero` in [libSceHmd2](libSceHmd2.md)). The libSceHmd answers themselves are not measured. - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types remain unresolved. - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) takes an unused handle, a 32-bit mask output and a nine-byte LED output. Output sizes match the paired setter; mask bits and LED payload semantics remain unresolved. The protected body is unchanged. From 789a5f686ffd1c26ba83c6de412ebbb27d23608f Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin <evgkaz0@gmail.com> Date: Sun, 18 Oct 2026 06:28:39 +0300 Subject: [PATCH 0912/1360] perf(agc): defer small host imports until first GPU use --- .../Graphics/src/GuestBufferMemory.cpp | 5 ++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 2 +- .../tests/execution/HostImportWindows.cpp | 44 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 074681e607..053fb10c01 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -3441,10 +3441,11 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& } else if (state.refreshedGeneration > generation) { return true; } - if (!adopting) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); for (const auto& mapped : ranges) { const auto range = mapped.lock(); - if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; + if (range == nullptr || range->bytes <= 0x10000) continue; + if (current.empty()) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + if (leasedRangeOwner(current, range->address) != range) continue; try { if (!GuestMemory::GpuMutex().HeldByThisThread()) { const auto run = importRun(current, range->address, std::numeric_limits<std::uint64_t>::max(), effectiveBudget(context, state)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 97f02e7f2f..45df34ee97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2371,7 +2371,7 @@ void remappedImportTests(const Device& device) { void hostImportRetirementTests(const Device& device) { const auto& context = device.GetContext(); - constexpr std::size_t UnitBytes = 65536; + constexpr std::size_t UnitBytes = 2 * 65536; constexpr std::size_t AllocationBytes = 4 * UnitBytes; constexpr std::size_t CopyBytes = 256; #ifdef _WIN32 diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp index e6d9bd6b30..35e4f777cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -21,6 +21,14 @@ #include <mutex> #include <string> +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { namespace Graphics = AgcDriver::Graphics; @@ -163,6 +171,41 @@ Outcome ResidentMappingsImportAfterCollection() { return Outcome::Passed; } +Outcome SmallMappingsImportOnFirstUse() { + GuestBlock small(GuestAlignment); + GuestBlock large(2 * GuestAlignment); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (context.hostImportAlignment == 0) return Outcome::NoImport; + for (auto* guest : {&small, &large}) { + Graphics::ClearHostImports(device->Device()); + GuestAllocations::Mapped ranges; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == guest->Address()) ranges.push_back(range); + } + Require(ranges.size() == 1, "mapped import test lost its registered range"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(Graphics::ImportMappedRanges(context, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "mapped import test could not adopt the device"); + } + Require(Graphics::HostImportCovers(context, guest->Address(), ProbeBytes) == (guest == &large), "mapping did not defer exactly the small import"); + Require(Fill(*device, guest->Address()), "first GPU use could not import the mapped buffer"); + Require(Graphics::HostImportCovers(context, guest->Address(), ProbeBytes), "first GPU use left the mapped buffer unimported"); + constexpr std::array<std::uint32_t, 4> pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + for (std::size_t offset = 0; offset < ProbeBytes; offset += sizeof(std::uint32_t)) { + std::uint32_t actual = 0; + std::memcpy(&actual, guest->Data() + offset, sizeof(actual)); + Require(actual == pattern[(offset / sizeof(actual)) % pattern.size()], "first GPU use changed the fill result"); + } + for (std::size_t offset = ProbeBytes; offset < GuestAlignment; ++offset) { + Require(guest->Data()[offset] == 0, "first GPU use overwrote bytes outside the fill"); + } + } + Graphics::ClearHostImports(device->Device()); + return Outcome::Passed; +} + } int main() { @@ -176,6 +219,7 @@ int main() { const auto windows = WindowsFollowTheirRange(); Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); Require(ResidentMappingsImportAfterCollection() == Outcome::Passed, "resident import test failed"); + Require(SmallMappingsImportOnFirstUse() == Outcome::Passed, "mapped import test lost its Vulkan device or host import support"); std::puts("host import windows tests passed"); return 0; } catch (const std::exception& error) { From e0764b9e14cb9eece34c9807c0098b75d7613766 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin <evgkaz0@gmail.com> Date: Wed, 21 Oct 2026 10:18:44 +0800 Subject: [PATCH 0913/1360] test(agc): map-time import tests for ranges of 64 KiB left to first GPU use The import-requester test now counts the map requester on a 128 KiB range and asserts that a 64 KiB range is not imported at map time and is imported, for the access requester, at its first use. The over-budget map test maps 128 KiB so it still reaches the budget check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/BdaResources.cpp | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index e4ea60da83..324c9bf90e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -979,7 +979,7 @@ void importNeighbouringEdgesTests(const Context& context) { // A range mapped while the imports are at the budget is refused at map time without retiring an import: the map // observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit). void importAtMapOverBudgetTests(const Context& context) { - constexpr std::size_t bytes = 65536; + constexpr std::size_t bytes = 2 * 65536; auto importing = context; importing.hostImportAlignment = bytes; void* kept = ::operator new(bytes, std::align_val_t{bytes}); @@ -1039,17 +1039,32 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) { const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast<std::size_t>(requester)]; }; const auto made = [&](ImportRequester requester) { return counted(requester).made; }; Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment", "an import requester has the wrong name"); - change(0, true); - { + const auto mappedRange = [&](std::size_t index) { GuestAllocations::Mapped ranges; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); for (const auto& range : lease) { - if (range->address == address(0)) ranges.push_back(range); + if (range->address == address(index)) ranges.push_back(range); } Require(ranges.size() == 1, "the requester test's mapped range is not registered"); + return ranges; + }; + { + GuestAllocations::Mutation mutation; + mutation.Add(block + 6 * piece, 2 * piece, true, true, true); + } + { const auto before = made(ImportRequester::Map); - static_cast<void>(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); - Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(0), piece), "a range imported at map time was not counted for the map requester"); + static_cast<void>(ImportMappedRanges(importing, mappedRange(6), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(6), 2 * piece), "a range imported at map time was not counted for the map requester"); + } + change(0, true); + { + const auto map = made(ImportRequester::Map); + const auto access = made(ImportRequester::Access); + static_cast<void>(ImportMappedRanges(importing, mappedRange(0), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + Require(made(ImportRequester::Map) == map && !HostImportCovers(importing, address(0), 16), "a range of 64 KiB was imported at map time"); + Require(HostImportFor(importing, address(0), 16) != nullptr && HostImportCovers(importing, address(0), piece) && made(ImportRequester::Access) == access + 1 && made(ImportRequester::Map) == map, + "a range of 64 KiB left to its first GPU use was not imported there, or not counted for that use's requester"); } change(1, true); { @@ -1127,6 +1142,10 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) { const auto counts = HostImportCounters(); Require(counts.live >= 4 && counts.liveBytes == HostImportLiveBytes(), "the live import counts disagree with the live bytes"); for (std::size_t index = 0; index < 6; ++index) change(index, false); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block + 6 * piece); + } ClearHostImports(importing.device); ::operator delete(block, std::align_val_t{piece}); } From a1cee90f52ccfa025b6a2b152b4795f7a310fe67 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin <evgkaz0@gmail.com> Date: Thu, 22 Oct 2026 08:57:01 +0800 Subject: [PATCH 0914/1360] test(agc): keep the map-failure memo test on a range imported at map time importMappedFailureGenerationTests (#8802) maps a single 64 KiB range and expects ImportMappedRanges to try its import. Ranges of 64 KiB are now left to their first GPU use, so the probe saw no allocation. The fixture maps 128 KiB as one window, which is still imported at map time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 324c9bf90e..0f28c680cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -809,7 +809,7 @@ private: void importMappedFailureGenerationTests(const Context& context) { ClearHostImports(context.device); - WindowedRange range(context, WindowedRange::piece, 1); + WindowedRange range(context, 2 * WindowedRange::piece, 1); ImportAllocationProbe probe(range.importing); probe.failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; GuestAllocations::Mapped mapped; From d06dfb1cafc0443d0e83d89546977228bd2ee475 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 14:41:50 +1300 Subject: [PATCH 0915/1360] fix(libc): stage wctomb_s and wcrtomb_s through _Getmbcurmax and pin their UTF-8 results Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/BoundsChecking.cpp | 6 +++--- core/libs/tests/GuestBoundsChecking.cpp | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/BoundsChecking.cpp b/core/libs/prx/libc/src/BoundsChecking.cpp index a29df4a74f..2acc1aa95c 100644 --- a/core/libs/prx/libc/src/BoundsChecking.cpp +++ b/core/libs/prx/libc/src/BoundsChecking.cpp @@ -11,7 +11,7 @@ extern "C" { std::size_t APS5_VABI mbrtowc_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state); std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value); -int APS5_VABI ___mb_cur_max_nid_postfix(); +int APS5_VABI _Getmbcurmax_nid_postfix(); int APS5_VABI getchar_nid_postfix(); int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); int APS5_VABI fputc_nid_postfix(int value, FileStream* stream); @@ -127,7 +127,7 @@ std::uint16_t* APS5_VABI wcstok_s_nid_postfix(std::uint16_t* str, std::size_t* r int APS5_VABI wctomb_s_nid_postfix(int* status, char* destination, std::size_t capacity, std::uint16_t wide) { if (!status || (!destination && capacity) || (destination && capacity > rsizeMax)) return violation("wctomb_s: bad argument", guestEinval); - if (destination && static_cast<std::size_t>(___mb_cur_max_nid_postfix()) > capacity) { + if (destination && static_cast<std::size_t>(_Getmbcurmax_nid_postfix()) > capacity) { char bytes[MB_LEN_MAX]; const int length = wctomb_nid_postfix(bytes, wide); if (length > 0 && static_cast<std::size_t>(length) > capacity) return violation("wctomb_s: buffer too small", guestErange); @@ -154,7 +154,7 @@ int APS5_VABI wcrtomb_s_nid_postfix(std::size_t* result, char* destination, std: std::size_t length = 0; if (!destination) { length = wcrtomb_nid_postfix(bytes, 0, state); - } else if (static_cast<std::size_t>(___mb_cur_max_nid_postfix()) > capacity) { + } else if (static_cast<std::size_t>(_Getmbcurmax_nid_postfix()) > capacity) { length = wcrtomb_nid_postfix(bytes, wide, state); if (length != failed && length > capacity) { *result = failed; diff --git a/core/libs/tests/GuestBoundsChecking.cpp b/core/libs/tests/GuestBoundsChecking.cpp index 4021e99e48..2fe88a5685 100644 --- a/core/libs/tests/GuestBoundsChecking.cpp +++ b/core/libs/tests/GuestBoundsChecking.cpp @@ -130,7 +130,14 @@ void CheckWideToMultibyte() { int status = 7; char bytes[4] = {'x', 'x', 'x', 'x'}; Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 'A') == 0 && status == 1 && bytes[0] == 'A', "wctomb_s did not convert"); - Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 0x263a) == 86 && status == -1, "wctomb_s converted an unrepresentable character"); + Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 0x263a) == 0 && status == 3 && std::memcmp(bytes, "\xE2\x98\xBA", 3) == 0, + "wctomb_s did not convert U+263A to UTF-8"); + std::memset(bytes, 'x', sizeof(bytes)); + status = 7; + int smallBefore = constraintCalls; + Require(wctomb_s_nid_postfix(&status, bytes, 2, 0x263a) == 34 && status == 7 && std::memcmp(bytes, "xxxx", 4) == 0, + "wctomb_s wrote past a two-byte buffer"); + RequireViolation(smallBefore, "wctomb_s: buffer too small", 34); Require(wctomb_s_nid_postfix(&status, nullptr, 0, 'A') == 0 && status == 0, "wctomb_s reported state dependence"); status = 7; int before = constraintCalls; @@ -144,7 +151,13 @@ void CheckWideToMultibyte() { std::size_t result = 7; Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 'B', state) == 0 && result == 1 && bytes[0] == 'B', "wcrtomb_s did not convert"); Require(wcrtomb_s_nid_postfix(&result, nullptr, 0, 'B', state) == 0 && result == 1, "wcrtomb_s did not measure the reset sequence"); - Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 0x263a, state) == 86 && result == failed, "wcrtomb_s converted an unrepresentable character"); + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 0x263a, state) == 0 && result == 3 && std::memcmp(bytes, "\xE2\x98\xBA", 3) == 0, + "wcrtomb_s did not convert U+263A to UTF-8"); + std::memset(bytes, 'x', sizeof(bytes)); + before = constraintCalls; + Require(wcrtomb_s_nid_postfix(&result, bytes, 2, 0x263a, state) == 34 && result == failed && bytes[0] == 0 && std::memcmp(bytes + 1, "xxx", 3) == 0, + "wcrtomb_s wrote past a two-byte buffer"); + RequireViolation(before, "wcrtomb_s: buffer too small", 34); before = constraintCalls; bytes[0] = 'x'; Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 'B', nullptr) == 22 && result == failed && bytes[0] == 0, "wcrtomb_s accepted a null state"); From d1e66e95ca11876c039d1e13230bfe84eb64c818 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 14:42:24 +1300 Subject: [PATCH 0916/1360] docs(libc): shorten the char16/char32 conversion entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 341883e5ed..a42735f105 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -107,7 +107,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1), as this libc does. Titles bundle their own `libc.prx`, whose locale was not measured +- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (SDK 4.5x, 0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, sequences of up to six bytes, and no check for surrogates or values above 0x10FFFF when encoding. The state holds the value in its first eight bytes and the count of bytes still expected (0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at 0xA. As on the console, `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without setting `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (firmware 13.60, homebrew payload) the system library's default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; the locale of a title's own `libc.prx` was not measured - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not go through the guest's `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate<char> and collate<wchar_t> _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits<wchar_t>::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. From e8abcba78fb6cf9c9f4dd3585ca7d020b34a0774 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 14:43:14 +1300 Subject: [PATCH 0917/1360] docs(agc): add the console's measured workload packet details Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libSceAgc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 01dd1bfcf3..d02567c947 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -44,6 +44,6 @@ - [sceAgcSetAmmSemaphoreMemory](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) - native registration first invokes a kernel ioctl and driver registration; host registration does not reproduce their memory-access checks or error propagation. Null, empty and address-wrapping ranges throw. The full range is cleared after successful registration; duplicate registration, alignment and label-range errors match the native wrapper. - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting -- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or per-stream mask table. In `libSceAgc.sprx` (firmware 12.00, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`, and the pointer given to `sceAgcDriverRegisterWorkloadStream` is never written +- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 12.00, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From b27a4bea5aeb131553653b20ef7b396ae745108f Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 16:16:10 +1300 Subject: [PATCH 0918/1360] test(libSceWebBrowserDialog): expect Close of a finished dialog to report it is not running Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestWebBrowserDialogCookies.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestWebBrowserDialogCookies.cpp b/core/libs/tests/GuestWebBrowserDialogCookies.cpp index 608f054c97..723bada25d 100644 --- a/core/libs/tests/GuestWebBrowserDialogCookies.cpp +++ b/core/libs/tests/GuestWebBrowserDialogCookies.cpp @@ -31,6 +31,7 @@ namespace { constexpr int NOT_INITIALIZED = static_cast<int>(0x80B80003u); constexpr int INVALID_STATE = static_cast<int>(0x80B80006u); +constexpr int NOT_RUNNING = static_cast<int>(0x80B8000Bu); constexpr int PARAM_INVALID = static_cast<int>(0x80B8000Au); constexpr int ARG_NULL = static_cast<int>(0x80B8000Du); @@ -116,7 +117,7 @@ void CheckOpenDialog() { Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after UpdateStatus"); Require(sceWebBrowserDialogOpen(open) == 0, "second Open"); - Require(sceWebBrowserDialogClose() == 0, "Close"); + Require(sceWebBrowserDialogClose() == NOT_RUNNING, "Close of a finished dialog"); Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after Close"); Require(sceWebBrowserDialogOpen(open) == 0, "third Open"); From 400d333961a00fc6c9f1a88ff4f3d7bcd3b6f900 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 16:18:02 +1300 Subject: [PATCH 0919/1360] fix(libSceNpSessionSignaling): check initialization and the context before each lookup Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestNpSessionSignaling.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index ef74401272..74240e67b1 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -27,7 +27,6 @@ namespace { constexpr int InvalidArgument = static_cast<int>(0x80553303u); constexpr int Unavailable = static_cast<int>(0x80552D06u); -constexpr int GroupNotFound = static_cast<int>(0x8055330Au); constexpr int ConnectionNotFound = static_cast<int>(0x8055330Cu); constexpr int NotInitialized = static_cast<int>(0x80553301u); constexpr int AlreadyInitialized = static_cast<int>(0x80553302u); From 726edc503bacc313648283ca0f649aa8242fc1ab Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" <harry.z.kang@outlook.com> Date: Mon, 19 Oct 2026 13:10:21 -0700 Subject: [PATCH 0920/1360] feat(windows): support MSYS2 GCC runtime builds --- .github/workflows/windows-msys2.yml | 117 ++++++++++++++++++ core/libs/CMakeLists.txt | 16 ++- core/libs/prx/libSceAjm.native/CMakeLists.txt | 1 + .../prx/libSceLibcInternal/UserContext.cpp | 39 ++++++ core/libs/prx/libc/CMakeLists.txt | 2 +- core/libs/prx/libc/src/exception/Unwind.cpp | 49 ++++++-- .../src/specifics/windows/CrashReport.cpp | 14 ++- .../specifics/windows/SehPersonalities.cpp | 11 ++ core/libs/prx/libkernel/Pthread/src/Cond.cpp | 11 +- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 11 +- core/libs/tests/GuestMessageDestructorAbi.S | 26 ++++ core/libs/tests/GuestMessageDestructorAbi.cpp | 38 ++++++ core/libs/tests/GuestModuleInfo.cpp | 17 ++- docs/dev/BUILD.md | 10 +- docs/dev/debt/libc.md | 2 +- 15 files changed, 332 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/windows-msys2.yml create mode 100644 core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp diff --git a/.github/workflows/windows-msys2.yml b/.github/workflows/windows-msys2.yml new file mode 100644 index 0000000000..def52370d2 --- /dev/null +++ b/.github/workflows/windows-msys2.yml @@ -0,0 +1,117 @@ +name: Windows MSYS2 + +on: + schedule: + - cron: '23 8 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: windows-msys2-${{ github.ref }} + cancel-in-progress: true + +env: + CONFIGURE: >- + -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + +jobs: + windows-msys2: + runs-on: windows-latest + timeout-minutes: 45 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + - id: submodules + name: Key the submodules on their commits + run: | + echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT" + - id: submodule-cache + uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Check out the submodules + run: | + if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi + git submodule update --init --recursive --depth 1 --jobs 8 + - if: steps.submodule-cache.outputs.cache-hit != 'true' + run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules + - if: steps.submodule-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Use the CA bundle of Git for Windows + run: | + for prefix in ucrt64 mingw64; do + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 + - id: msys2 + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + - shell: pwsh + env: + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + run: Add-Content -LiteralPath $env:GITHUB_PATH -Value "$env:MSYS2_LOCATION\ucrt64\bin" + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: windows-msys2 + save: ${{ github.ref == 'refs/heads/main' }} + evict-old-files: job + - id: ffmpeg-cache + uses: actions/cache/restore@v6 + with: + path: build/externals + key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + - shell: cmd + run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + - if: steps.ffmpeg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: build/externals + key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }} + - name: Check that every test executable is registered with ctest + shell: cmd + run: python tools/check_test_registration.py build + - shell: cmd + run: cmake --build build + - shell: cmd + run: cmake --build build --target libs + - shell: cmd + working-directory: build + run: ctest --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml + - run: python tools/check_skips.py build/ctest.xml --job windows + - name: Report tests slower than 30 s + if: ${{ !cancelled() }} + run: | + [ -f build/ctest.log ] || exit 0 + awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t } + END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' build/ctest.log + - uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: windows-msys2-tests + path: | + build/ctest.log + build/ctest.xml + if-no-files-found: ignore diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f84fa93f40..d2a614da5e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -39,9 +39,16 @@ if(MINGW) check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) unset(CMAKE_REQUIRED_FLAGS) if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS) - message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.") + check_cxx_source_compiles("#if !defined(__SEH__) || defined(__USING_SJLJ_EXCEPTIONS__)\n#error\n#endif\n[[gnu::sysv_abi, gnu::noinline]] void Throw() { throw 1; }\n[[gnu::sysv_abi, gnu::noinline]] int Catch() { try { Throw(); } catch (int value) { return value; } return 0; }\nint main() { return Catch() != 1; }" ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS) + if(NOT ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS) + message(FATAL_ERROR "The Windows libraries require DWARF exceptions or GCC with System V ABI SEH support. Use the toolchains listed in docs/dev/BUILD.md.") + endif() + add_library(windows_seh_personalities OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/src/specifics/windows/SehPersonalities.cpp) + target_link_options(windows_seh_personalities INTERFACE LINKER:--undefined=LibcSehPersonalities_nid_no_patch) + link_libraries(windows_seh_personalities) + else() + add_compile_options(-fno-asynchronous-unwind-tables) endif() - add_compile_options(-fno-asynchronous-unwind-tables) endif() function(configure_windows_unwind target) @@ -50,7 +57,7 @@ function(configure_windows_unwind target) message(FATAL_ERROR "Windows DWARF unwinding requires objcopy") endif() add_custom_command(TARGET ${target} POST_BUILD - COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram "$<TARGET_FILE:${target}>" + COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram --rename-section .eh_fram=.ehfram "$<TARGET_FILE:${target}>" VERBATIM ) endif() @@ -327,6 +334,9 @@ if(BUILD_TESTING) if(WIN32) add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) + if(MINGW) + target_compile_options(undeliverable_host_exception_tests PRIVATE -fno-asynchronous-unwind-tables -fno-exceptions) + endif() add_dependencies(undeliverable_host_exception_tests libc) add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $<TARGET_FILE:libc>) set_tests_properties(undeliverable_host_exception PROPERTIES diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 51b7c2507f..de55e6792c 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -45,6 +45,7 @@ configure_windows_unwind(libSceAjm.native) if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR}) + target_link_libraries(atrac9_gradient_tests PRIVATE atrac9) target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections) if(APPLE) target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip) diff --git a/core/libs/prx/libSceLibcInternal/UserContext.cpp b/core/libs/prx/libSceLibcInternal/UserContext.cpp index f5f6c7eae8..3f743af8f4 100644 --- a/core/libs/prx/libSceLibcInternal/UserContext.cpp +++ b/core/libs/prx/libSceLibcInternal/UserContext.cpp @@ -20,12 +20,51 @@ constexpr std::uint64_t MinimumStackSize = 2048; extern "C" { +#if defined(_WIN32) && defined(__SEH__) +[[noreturn]] void APS5_VABI Aps5MakecontextStart_nid_no_patch(UserUcontext* context, ContextFunction function, const std::uint64_t* arguments); + +[[noreturn]] void APS5_VABI Aps5MakecontextFinish_nid_no_patch(UserUcontext* context) { + if (context->link == nullptr) LibcExit_nid_no_patch(0); + setcontext_nid_postfix(context->link); + std::abort(); +} + +asm(R"( +.text +.globl Aps5MakecontextStart_nid_no_patch +.def Aps5MakecontextStart_nid_no_patch; .scl 2; .type 32; .endef +.seh_proc Aps5MakecontextStart_nid_no_patch +Aps5MakecontextStart_nid_no_patch: + pushq %rbp + .seh_pushreg %rbp + subq $48, %rsp + .seh_stackalloc 48 + leaq 48(%rsp), %rbp + .seh_setframe %rbp, 48 + .seh_endprologue + movq %rdi, -8(%rbp) + movq %rsi, %rax + movq %rdx, %r10 + movq (%r10), %rdi + movq 8(%r10), %rsi + movq 16(%r10), %rdx + movq 24(%r10), %rcx + movq 32(%r10), %r8 + movq 40(%r10), %r9 + call *%rax + movq -8(%rbp), %rdi + call Aps5MakecontextFinish_nid_no_patch + ud2 +.seh_endproc +)"); +#else [[noreturn]] void APS5_VABI Aps5MakecontextStart_nid_no_patch(UserUcontext* context, ContextFunction function, const std::uint64_t* arguments) { function(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]); if (context->link == nullptr) LibcExit_nid_no_patch(0); setcontext_nid_postfix(context->link); std::abort(); } +#endif void APS5_VABI makecontext_nid_postfix(UserUcontext* context, void (APS5_VABI* start)(), int count, ...) { if (context == nullptr || context->mcontext.len != sizeof(UserMcontext)) return; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 98ff1daf52..d53c2d0461 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1052,7 +1052,7 @@ if(MINGW) target_include_directories(guest_message_destructor_abi_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_message_destructor_abi_tests PRIVATE libc) configure_windows_unwind(guest_message_destructor_abi_tests) - foreach(mode IN ITEMS control decoy zero release regex-decoy regex-zero + foreach(mode IN ITEMS control hybrid decoy zero release regex-decoy regex-zero runtime-decoy runtime-zero doraise-decoy doraise-zero unshared-decoy unshared-zero regex-unshared-decoy regex-unshared-zero) add_test(NAME guest_message_destructor_${mode} COMMAND guest_message_destructor_abi_tests ${mode}) diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index c86389adfe..94226226e0 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -105,14 +105,21 @@ extern "C" void _Unwind_DeleteException(_Unwind_Exception*); #endif namespace LibcUnwind { -_Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _Unwind_Exception* exception, _Unwind_Context* context) { #ifdef _WIN32 - const auto* code = reinterpret_cast<const Byte*>(personality); +static Word ResolveImport(Word address) { + const auto* code = reinterpret_cast<const Byte*>(address); if (code[0] == 0xff && code[1] == 0x25) { std::int32_t displacement; - std::memcpy(&displacement, code + 2, 4); - std::memcpy(&personality, code + 6 + displacement, sizeof(personality)); + std::memcpy(&displacement, code + 2, sizeof(displacement)); + std::memcpy(&address, code + 6 + displacement, sizeof(address)); } + return address; +} +#endif + +_Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _Unwind_Exception* exception, _Unwind_Context* context) { +#ifdef _WIN32 + personality = ResolveImport(personality); #endif if (personality == reinterpret_cast<Word>(__gxx_personality_v0_nid_postfix) || personality == reinterpret_cast<Word>(__gxx_personality_v0)) return __gxx_personality_v0_nid_postfix(1, actions, exception->exception_class, exception, context); @@ -257,14 +264,20 @@ const IMAGE_NT_HEADERS64* ImageHeaders(Word pc, const Byte*& base) { return nt->Signature == IMAGE_NT_SIGNATURE ? nt : nullptr; } -bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { +bool VirtualUnwind(_Unwind_Context& context, Frame& frame) { const Word pc = context.registers[16]; const Byte* base = nullptr; const auto* nt = ImageHeaders(pc, base); if (!nt) return false; const auto* sections = IMAGE_FIRST_SECTION(nt); - for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) - if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) return false; + const PEXCEPTION_ROUTINE* personalities = nullptr; + bool dwarf = false; + for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) { + if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) dwarf = true; + if (std::memcmp(sections[i].Name, ".sehgcc", 8) == 0 && sections[i].Misc.VirtualSize >= 2 * sizeof(PEXCEPTION_ROUTINE)) + personalities = reinterpret_cast<const PEXCEPTION_ROUTINE*>(base + sections[i].VirtualAddress); + } + if (dwarf && !personalities) return false; DWORD64 imageBase = 0; auto* function = RtlLookupFunctionEntry(pc, &imageBase, nullptr); if (!function) return false; @@ -275,11 +288,27 @@ bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { for (unsigned i = 0; i < 16; ++i) std::memcpy(&state.FltSave.XmmRegisters[i], context.vectorRegisters[i], 16); void* handlerData = nullptr; DWORD64 establisher = 0; - if (RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr)) return false; + const auto handler = RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr); + frame.start = imageBase + function->BeginAddress; + if (handler) { + if (!personalities) return false; + const Word resolved = ResolveImport(reinterpret_cast<Word>(handler)); + if (resolved == ResolveImport(reinterpret_cast<Word>(personalities[0]))) frame.personality = reinterpret_cast<Word>(__gxx_personality_v0_nid_postfix); + else if (resolved == ResolveImport(reinterpret_cast<Word>(personalities[1]))) frame.personality = reinterpret_cast<Word>(__gcc_personality_v0_nid_postfix); + else return false; + frame.lsda = reinterpret_cast<Word>(handlerData); + } + auto& caller = frame.caller; caller = context; for (unsigned i = 0; i < 17; ++i) caller.registers[i] = *registers[i]; for (unsigned i = 0; i < 16; ++i) std::memcpy(caller.vectorRegisters[i], &state.FltSave.XmmRegisters[i], 16); caller.signalFrame = false; + context.cfa = caller.registers[7]; + context.region = frame.start; + context.lsda = frame.lsda; + context.personality = frame.personality; + context.textBase = imageBase; + context.dataBase = 0; return true; } #endif @@ -414,10 +443,8 @@ bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { if (!DecodeFrame(context, frame)) { #ifdef _WIN32 frame = {}; - if (!VirtualUnwind(context, frame.caller)) return false; + if (!VirtualUnwind(context, frame)) return false; frame.foreign = true; - context.cfa = frame.caller.registers[7]; - context.region = context.lsda = context.personality = context.textBase = context.dataBase = 0; return true; #else return false; diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 7b4cc729ed..1d031c17a1 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -235,6 +235,14 @@ constexpr DWORD GccException = 0x20474343u; constexpr std::uint64_t GccExceptionClass = 0x474e5543432b2b00ull; constexpr std::uint64_t CxaExceptionTypeOffset = 80; +PWSTR ThreadDescription(HANDLE thread) { + using GetDescription = HRESULT (WINAPI *)(HANDLE, PWSTR*); + static const auto getDescription = reinterpret_cast<GetDescription>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "GetThreadDescription")); + PWSTR description = nullptr; + if (!getDescription || FAILED(getDescription(thread, &description))) return nullptr; + return description; +} + std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { const HMODULE runtime[] = {GetModuleHandleA("KERNELBASE.dll"), GetModuleHandleA("libgcc_s_seh-1.dll"), GetModuleHandleA("libstdc++-6.dll")}; CONTEXT context = start; @@ -363,8 +371,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { const auto* context = info->ContextRecord; char line[MAX_PATH + 64]; char threadName[128] = ""; - PWSTR description = nullptr; - if (SUCCEEDED(GetThreadDescription(GetCurrentThread(), &description)) && description) { + if (PWSTR description = ThreadDescription(GetCurrentThread())) { WideCharToMultiByte(CP_UTF8, 0, description, -1, threadName, sizeof(threadName), nullptr, nullptr); LocalFree(description); } @@ -582,8 +589,7 @@ void ReportAllThreads() { const std::size_t candidateCount = haveContext && frameCount < 2 ? CaptureStackCandidates(context.Rsp, candidates, sizeof(candidates) / sizeof(candidates[0])) : 0; ResumeThread(thread); char name[128] = ""; - PWSTR description = nullptr; - if (SUCCEEDED(GetThreadDescription(thread, &description)) && description) { + if (PWSTR description = ThreadDescription(thread)) { WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr); LocalFree(description); } diff --git a/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp b/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp new file mode 100644 index 0000000000..cd4c856063 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp @@ -0,0 +1,11 @@ +#include <windows.h> + +extern "C" { +EXCEPTION_DISPOSITION __gxx_personality_seh0(EXCEPTION_RECORD*, void*, CONTEXT*, void*); +EXCEPTION_DISPOSITION __gcc_personality_seh0(EXCEPTION_RECORD*, void*, CONTEXT*, void*); + +[[gnu::section(".sehgcc"), gnu::used]] extern const PEXCEPTION_ROUTINE LibcSehPersonalities_nid_no_patch[] { + __gxx_personality_seh0, + __gcc_personality_seh0, +}; +} diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 609acd98aa..a3a3087bf7 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -19,7 +19,10 @@ namespace { constexpr int sceTimedOut = static_cast<int>(0x8002003cu); -std::mutex condInitializationMutex; +std::mutex& InitializationMutex() { + static std::mutex mutex; + return mutex; +} PthreadCond destroyedCond() { return reinterpret_cast<PthreadCond>(std::uintptr_t{2}); @@ -36,7 +39,7 @@ PthreadMutex adaptiveInitializer() { std::shared_ptr<TimedWait::Condition> resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); std::atomic_ref<PthreadCond> slot(*cond); auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) @@ -161,7 +164,7 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, auto replacement = std::make_unique<PthreadCondPrivate>(); if (attr && *attr) replacement->_clockid = (*attr)->_clockid; - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); std::atomic_ref<PthreadCond>(*cond).store(replacement.release(), std::memory_order_release); return 0; } @@ -169,7 +172,7 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); if (*cond == destroyedCond()) return SCE_KERNEL_ERROR_EINVAL; delete *cond; diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index dccc979442..d7bc4013f8 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -16,7 +16,10 @@ constexpr int sceNotPermitted = static_cast<int>(0x80020001u); constexpr int sceDeadlock = static_cast<int>(0x8002000bu); constexpr int sceBusy = static_cast<int>(0x80020010u); constexpr int sceTimedOut = static_cast<int>(0x8002003cu); -std::mutex initializationMutex; +std::mutex& InitializationMutex() { + static std::mutex mutex; + return mutex; +} PthreadMutex destroyedMutex() { return reinterpret_cast<PthreadMutex>(std::uintptr_t{2}); @@ -36,7 +39,7 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { std::atomic_ref<PthreadMutex> slot(*mutex); if (const auto current = slot.load(std::memory_order_acquire); isInitializedMutex(current)) return current; - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); const auto current = slot.load(std::memory_order_acquire); if (current == destroyedMutex()) return nullptr; @@ -162,7 +165,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a auto replacement = std::make_unique<PthreadMutexPrivate>(); if (attr && *attr) replacement->SetType((*attr)->type); - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); std::atomic_ref<PthreadMutex>(*mutex).store(replacement.release(), std::memory_order_release); return 0; } @@ -170,7 +173,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); if (*mutex == destroyedMutex()) return SCE_KERNEL_ERROR_EINVAL; if (*mutex == adaptiveInitializer()) { diff --git a/core/libs/tests/GuestMessageDestructorAbi.S b/core/libs/tests/GuestMessageDestructorAbi.S index 57ac11e827..7330b86885 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.S +++ b/core/libs/tests/GuestMessageDestructorAbi.S @@ -141,7 +141,33 @@ ProbeDestroy: .cfi_def_cfa %rsp, 8 ret .cfi_endproc +.globl GuestCleanup +GuestCleanup: + .cfi_startproc + .cfi_personality 0x00, __gcc_personality_v0_nid_postfix + .cfi_lsda 0x00, .Lcleanup_lsda + PROLOGUE +.Lcleanup_call: + call FixtureHostThrow +.Lcleanup_call_end: + ud2 +.Lcleanup_landing: + incl FixtureGuardDestroyed(%rip) + movq %rax, %rdi + call _Unwind_Resume_nid_postfix + ud2 + .cfi_endproc + .section .rdata,"dr" +.Lcleanup_lsda: + .byte 0xff, 0xff, 0x01 + .uleb128 .Lcleanup_calls_end-.Lcleanup_calls +.Lcleanup_calls: + .uleb128 .Lcleanup_call-GuestCleanup + .uleb128 .Lcleanup_call_end-.Lcleanup_call + .uleb128 .Lcleanup_landing-GuestCleanup + .uleb128 0 +.Lcleanup_calls_end: .Linner_lsda: .byte 0xff, 0x00 .uleb128 .Linner_types_end-.Linner_types_offset diff --git a/core/libs/tests/GuestMessageDestructorAbi.cpp b/core/libs/tests/GuestMessageDestructorAbi.cpp index 68e398ef4d..8c55dbc490 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.cpp +++ b/core/libs/tests/GuestMessageDestructorAbi.cpp @@ -32,6 +32,7 @@ void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(Object*); [[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const Object*); [[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type); int APS5_VABI GuestOuter(); +void APS5_VABI GuestCleanup(); void APS5_VABI GuestStdCapture(); void APS5_VABI ProbeDestroy(GuestDestroy, void*, void*); const char* APS5_VABI ProbeWhat(GuestWhat, const void*, const void*); @@ -202,9 +203,46 @@ extern "C" void APS5_VABI FixtureInspect(void* pointer) { if (!regularRelease) header->destructor = reinterpret_cast<HostDestroy>(ObserveDestroy); } +static unsigned hybridDestroyed; +struct HybridGuard { ~HybridGuard() { ++hybridDestroyed; } }; + +extern "C" [[gnu::noinline, noreturn]] void APS5_VABI FixtureHostThrow() { + HybridGuard guard; + __asm__ volatile("pxor %%xmm12, %%xmm12" ::: "xmm12"); + throw 73; +} + +[[gnu::noinline]] static void HybridMiddle() { + HybridGuard guard; + GuestCleanup(); +} + +static int TestHybridUnwind() { + const std::uint64_t expected[2] {0x123456789abcdef0, 0xfedcba9876543210}; + std::uint64_t restored[2] {}; + __asm__ volatile("movdqu %0, %%xmm12" :: "m"(expected) : "xmm12"); + unsigned caught = 0; + try { + HybridGuard guard; + try { HybridMiddle(); } + catch (int value) { + if (value != 73 || hybridDestroyed != 2 || FixtureGuardDestroyed != 1) return 63; + ++caught; + throw; + } + } catch (int value) { + if (value != 73) return 64; + ++caught; + } + __asm__ volatile("movdqu %%xmm12, %0" : "=m"(restored)); + return caught == 2 && hybridDestroyed == 3 && FixtureGuardDestroyed == 1 && + std::memcmp(restored, expected, sizeof(expected)) == 0 ? 0 : 65; +} + int main(int argc, char** argv) { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); if (argc != 2) return 2; + if (std::strcmp(argv[1], "hybrid") == 0) return TestHybridUnwind(); if (std::strncmp(argv[1], "vtable-", 7) == 0) return TestVtable(argv[1]); if (std::strcmp(argv[1], "control") == 0) { if (GuestOuter() != 0 || FixtureDestroyed != 1 || FixtureGuardDestroyed != 1 || diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index 6c70763d2a..d596bf0280 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -3,10 +3,12 @@ #include <algorithm> #include <cstddef> #include <cstdint> +#include <cstdio> #include <cstdlib> #include <cstring> #include <iterator> #include <stdexcept> +#include <source_location> #include <string> #include <vector> extern "C" { @@ -21,7 +23,12 @@ int APS5_VABI sceKernelGetModuleInfo2(KernelModule, ModuleInfo*); std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void**, std::size_t*); std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "module info check failed at line %u\n", location.line()); + std::abort(); + } +} template<typename TFunction> static bool ThrowsInvalidArgument(TFunction function) { try { @@ -55,7 +62,11 @@ int main(int argc, char** argv) { executable = (segment.prot & 5) == 5; } Require(executable); +#if defined(_WIN32) && defined(__SEH__) + Require(info.eh_frame_addr == 0 && info.eh_frame_size == 0); +#else Require(info.eh_frame_addr != 0 && info.eh_frame_size != 0); +#endif #ifndef _WIN32 Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0); #endif @@ -172,6 +183,8 @@ int main(int argc, char** argv) { Require(reloadedAdd != nullptr); const auto fresh = Query(reloadedAdd, 0); Require(fresh.eh_frame_size == info.eh_frame_size && fresh.eh_frame_hdr_size == info.eh_frame_hdr_size); - Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + if (info.eh_frame_addr) + Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + else Require(fresh.eh_frame_addr == 0); Require(dlclose_nid_postfix(reloaded) == 0); } diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 4cebf79a20..1b1a1287ac 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -12,7 +12,7 @@ ctest --test-dir build-relinker --output-on-failure Python must be found during CMake configuration for Python tests to be registered. List registered tests with `ctest --test-dir build-relinker -N` and look for `input_magic`, a Python-backed relinker test. If automatic discovery fails, add `-DPython3_EXECUTABLE="C:/path/to/python.exe"` to the configure command, using the path to your Python interpreter, then list the tests again. -This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the WinLibs requirement below applies to the full build. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). +This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. @@ -30,7 +30,7 @@ git submodule update --init --recursive - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. -- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. +- Windows: x86-64 MinGW-w64 GCC with POSIX threads. The compiler must provide DWARF exceptions with `-fno-asynchronous-unwind-tables`, or SEH exceptions for System V ABI functions; CMake checks these capabilities. WinLibs GCC 15.2.0 (`x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7)) uses DWARF. [MSYS2 UCRT64 GCC](https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-gcc) 16.2.0-6 uses SEH. Add the selected toolchain's `bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. - Bitdefender Advanced Threat Defense hooks Windows calls that `sceKernelRaiseException` makes while its target thread is suspended. When the target is suspended inside ntdll's exception dispatch while it holds an ntdll lock, the hook waits for that lock and `guest_raise_exception` hangs until its timeout. A scanning exception for the build directory does not prevent it; add `guest_raise_exception_tests.exe` to the Advanced Threat Defense exceptions. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. @@ -64,6 +64,12 @@ therefore tests the previous binaries and can show no effect at all. The full build also requires Python 3 for glslang and Git for Windows with Bash for libusb's version-header generation. A minimal Git installation without Bash is insufficient for the full build. +For MSYS2, install `mingw-w64-ucrt-x86_64-gcc` with `pacman -S mingw-w64-ucrt-x86_64-gcc` and use `C:\msys64\ucrt64\bin` in PowerShell. Adjust the prefix for a Scoop installation of MSYS2. Use a separate build directory for each toolchain, and select both `gcc` and `g++` from the same installation. + +The [Windows MSYS2 workflow](../../.github/workflows/windows-msys2.yml) builds and tests the full runtime nightly at 08:23 UTC and can be started manually with `workflow_dispatch`. The regular Windows build uses WinLibs. + +Scoop's standalone GCC 15.2.0 package cannot build the runtime: disabling SEH selects SjLj exceptions, and its SEH backend cannot compile System V ABI catch handlers. Use MSYS2 UCRT64 or a DWARF-capable WinLibs distribution. This restriction does not apply to relinker-only builds. + In PowerShell, prepend the supported WinLibs toolchain and Git for Windows to the current session's `PATH`. Adjust the paths for the installed locations; a Portable Git installation can be used in place of `C:\Program Files\Git`. ```powershell diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ef9b20680c..d0c049c16c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -101,7 +101,7 @@ - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way - [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked -- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled +- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) cannot run arbitrary Windows exception or termination handlers. It accepts GCC C and C++ handlers identified by a project's `.sehgcc` section and interprets their LSDA with the guest personalities. A handler in an unmarked module, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), still stops unwinding and the exception is reported as unhandled. SjLj exceptions are unsupported; Scoop GCC 15.2.0 selects them when SEH is disabled and lacks the System V ABI SEH compiler fix carried by MSYS2. - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if<index>` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. From 4e9854ea2b5023c900afa492c4c006e350267402 Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" <harry.z.kang@outlook.com> Date: Wed, 21 Oct 2026 10:05:10 -0700 Subject: [PATCH 0921/1360] test(relinker): report SHA-1 preservation failures precisely --- core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 08d527462d..a03384cea7 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1811,7 +1811,13 @@ void sha1Execution() { else require(run.Xmm[reg][0] == xmmIn[reg][0] && run.Xmm[reg][1] == xmmIn[reg][1], "SHA-1 stub clobbered an xmm register"); } - require(run.Rax == rax && run.Rcx == 0x1122334455667788ull && (run.Flags & 0x8D5) == (0x8D7 & 0x8D5), "SHA-1 stub changed a general register or RFLAGS"); + if (run.Rax != rax || run.Rcx != 0x1122334455667788ull || (run.Flags & 0x8D5) != (0x8D7 & 0x8D5)) { + std::cerr << "SHA-1 sample=" << sample << " site=" << std::hex; + for (const auto byte : site) + std::cerr << static_cast<unsigned>(byte) << ' '; + std::cerr << "rax=" << run.Rax << " expected=" << rax << " rcx=" << run.Rcx << " flags=" << run.Flags << std::dec << '\n'; + throw std::runtime_error("SHA-1 stub changed a general register or RFLAGS"); + } } } } From e4de56f3ce528ea30a3e42794282d883c1f81114 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Thu, 22 Oct 2026 10:39:26 +0700 Subject: [PATCH 0922/1360] fix(shader): clamp a domain read at an immediate address to the last control point The dynamic path of TessellationPointer passes the control point through inPatch(), which clamps it with UMin to inputControlPoints - 1 for hull inputs and to outputControlPoints - 1 for domain inputs. The immediate path clamped only hull inputs, so a domain read at an immediate address past the last control point indexed the input array past its end. It now clamps domain inputs to outputControlPoints - 1 too. Co-Authored-By: Claude <noreply@anthropic.com> --- .../tests/TessellationLowering.cpp | 55 +++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp index 1b1ec2d0da..2ccb9129ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -1,11 +1,16 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" #include "Optimization/TessellationLowering.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include <spirv/unified1/spirv.hpp> #include <cstdint> #include <cstdio> #include <exception> #include <functional> +#include <stdexcept> #include <string> +#include <unordered_map> using namespace ShaderRecompiler; @@ -135,6 +140,52 @@ bool RefusedBecause(IrProgram& program, const char* reason) { return !outcome.lowered && outcome.reason.find(reason) != std::string::npos; } +std::uint32_t EmittedEvaluationControlPoint(std::uint32_t address) { + constexpr std::uint32_t ControlStride = 16u; + IrProgram program; + IrBuilder builder{program}; + program.SetWaveSize(64); + program.Resources().stage = IrShaderStage::TessellationEvaluation; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + program.Metadata().tessellationControlStride = ControlStride; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = block.Id(); + program.Metadata().blockInfo.push_back(info); + builder.SetInsertionPoint(block); + auto& read = builder.Emit(IrOpcode::GetTessellationAttribute, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(TessellationAttribute::EvaluationInput)), &builder.Constant(address), &builder.ConstantBool(true)}); + static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&read})); + ShaderVertexInputInfo vertex; + vertex.tess.inputControlPoints = InputControlPoints; + vertex.tess.outputControlPoints = InputControlPoints; + vertex.tess.domain = 1u; + vertex.tess.partitioning = 2u; + vertex.tess.outputTopology = 2u; + ShaderStageInputInfo inputs; + inputs.vertex = &vertex; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 64; + const auto words = SpirvEmitter().Emit(program, inputs, bindings, target); + std::unordered_map<std::uint32_t, std::uint32_t> constants; + for (std::size_t at = 5; at < words.size(); at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + if (count == 0u || at + count > words.size()) throw std::runtime_error("the domain program compiled to a malformed SPIR-V instruction"); + const auto opcode = words[at] & 0xffffu; + if (opcode == spv::OpConstant && count == 4u) constants[words[at + 2u]] = words[at + 3u]; + if (opcode == spv::OpAccessChain && count == 7u) return constants.at(words[at + 4u]); + } + throw std::runtime_error("the domain program reads no control point attribute"); +} + bool run(const char* name, const std::function<bool()>& body) { bool passed = false; try { @@ -206,5 +257,9 @@ int main() { return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride; }); + passed &= run("a domain read at an immediate address past the last control point reads the last control point", [] { + return EmittedEvaluationControlPoint(InputControlPoints * 16u) == InputControlPoints - 1u; + }); + return passed ? 0 : 1; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 0ef03a1698..87ed5233c6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -821,7 +821,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins return pointer; } const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); - const auto controlPoint = kind == TessellationAttribute::ControlInput ? std::min(value / stride, state.inputInfo.vertex->tess.inputControlPoints - 1u) : value / stride; + const auto controlPoint = kind == TessellationAttribute::ControlInput ? std::min(value / stride, state.inputInfo.vertex->tess.inputControlPoints - 1u) : kind == TessellationAttribute::EvaluationInput ? std::min(value / stride, state.inputInfo.vertex->tess.outputControlPoints - 1u) : value / stride; state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, own ? EmitLaneId(state) : ConstantU32(state, controlPoint), attribute, component); return pointer; } From 89548eb4afe9d641bcaf292bb473032d4d2ac6d5 Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" <harry.z.kang@outlook.com> Date: Wed, 21 Oct 2026 20:45:15 -0700 Subject: [PATCH 0923/1360] fix(tests): retry locked Windows TLS fixture cleanup --- core/relinker/relinker/tests/test_tls_function_coverage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 7de43350f9..8325f6bac5 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -5,8 +5,8 @@ import platform import struct import subprocess import sys -import tempfile +from guest_import_fixtures import fixture_directory from test_optional_plt import fixture @@ -538,7 +538,7 @@ def main(): parser.add_argument("group", nargs="?", default="all", choices=("all",) + groups) args = parser.parse_args() relinker = args.relinker.resolve() - with tempfile.TemporaryDirectory(prefix=f"anyps5-tls-{args.group}-") as directory: + with fixture_directory(prefix=f"anyps5-tls-{args.group}-") as directory: work = Path(directory) def convert(name, image, error=None, tls_address=0x1240, displacement=0, error_offset=None): From 0efe28a83959f834b74154a18edbb51f193d5c02 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Wed, 21 Oct 2026 00:33:02 +0700 Subject: [PATCH 0924/1360] fix(agc): draw line lists and strips into a multisampled target with MSAA_ENABLE clear A line list or strip drawn with PA_SC_AA_CONFIG above 1x and PA_SC_MODE_CNTL_0.MSAA_ENABLE clear threw, and METAL GEAR SOLID: Peace Walker (PPSA18216) draws such lines into its 4x target. They now take the path triangles already take in this state: the target's sample count with every sample at the pixel center, or one sample under a one-sample color target. The refusal now reads the primitive the draw rasterizes (RasterPrimitiveOf) instead of VGT_PRIMITIVE_TYPE: a point list that a geometry program turns into triangles or lines is drawn as those, and rasterized points and alpha-to-coverage still throw. No draw on main rasterizes points in this state, because a vertex-path point list and a geometry program's point output throw earlier; the point term takes effect once they are drawable, as with #6801. The new GPU test decodes a line list and a line strip into a 4x target in this state with DecodeState, draws the decoded state, and requires every pixel to be covered in all four samples or in none. Co-Authored-By: Claude <noreply@anthropic.com> --- .../libSceAgcDriver/Graphics/src/State.cpp | 3 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 57 ++- .../execution/MultisampleCenteredLines.cpp | 330 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 4 +- 4 files changed, 381 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index d8a2554f0d..d83d48358d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1296,8 +1296,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.sampleLocationGrid = false; } if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { - const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; - Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + Require(RasterPrimitiveOf(result) != ShaderRecompiler::RasterPrimitive::Point && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)")); if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export")); result.customSampleLocations = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 12b11af2df..4f7a6b5593 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -765,10 +765,12 @@ void msaaEnableClearOneSampleTargetTests() { expectFailure([&] { DecodeState(queue); }, refused); queue = drawC(); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines"); - queue = drawC(); - queue.userConfig[0x242] = 2; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + for (const auto primitive : {2u, 3u}) { + queue = drawC(); + queue.userConfig[0x242] = primitive; + requireOneSample(queue, "a line list or strip into PPSA18216's draw C target"); + } const auto twoTargets = [&](std::uint32_t first, std::uint32_t second) { auto pair = drawC(); for (const auto offset : {0x318u, 0x31bu, 0x31cu}) pair.context[offset + 0xfu] = pair.context.at(offset); @@ -853,9 +855,11 @@ void msaaEnableClearOneSampleTargetTests() { queue = depthTarget(color); queue.context[0x2dc] = 0x0000aa01u; expectFailure([&] { DecodeState(queue); }, depthRefused); - queue = depthTarget(color); - queue.userConfig[0x242] = 2; - expectFailure([&] { DecodeState(queue); }, depthRefused); + for (const auto primitive : {2u, 3u}) { + queue = depthTarget(color); + queue.userConfig[0x242] = primitive; + expectFailure([&] { DecodeState(queue); }, depthRefused); + } for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { queue = depthTarget(color); queue.context[0x201] = eqaa; @@ -3282,6 +3286,41 @@ void msaaEnableClearDepthTests() { state = DecodeState(queue); Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target"); requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE"); + for (const auto primitive : {2u, 3u}) { + queue = drawB(); + queue.userConfig[0x242] = primitive; + state = DecodeState(queue); + Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP) && state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's line list or strip into its 4x depth and color pair lost its topology or color target"); + requireCentered(state, "a line list or strip into PPSA18216's 4x depth and color pair without MSAA_ENABLE"); + Require(!state.sampleLocationGrid && !state.perSampleShading, "a line list or strip without MSAA_ENABLE kept a sample location grid or sample shading"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a line list or strip into PPSA18216's 4x depth and color pair"); + } + queue = drawB(); + queue.userConfig[0x242] = 1; + queue.context[0x2d5] = 0x2030; + queue.userConfig[0x25b] = 0x6633; + queue.context[0x1ff] = 255; + queue.context[0x2ce] = 5; + queue.context[0x29b] = 2; + queue.context[0x2ab] = 4; + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; + state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.stages.mesh && state.stages.mesh->outputPrimitiveSize == 3u && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle && state.colors.size() == 1 && state.colors[0].samples == 4, "a point list into a geometry program that outputs triangle strips into PPSA18216's 4x depth and color pair did not decode as triangles"); + requireCentered(state, "triangles a geometry program makes from a point list into PPSA18216's 4x depth and color pair without MSAA_ENABLE"); + for (const auto primitive : {4u, 2u, 1u}) { + queue = drawB(); + queue.userConfig[0x242] = primitive; + queue.context[0x2d5] = 0x2030; + queue.userConfig[0x25b] = 0x6633; + queue.context[0x1ff] = 255; + queue.context[0x2ce] = 5; + queue.context[0x29b] = 0; + queue.context[0x2ab] = 4; + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + } for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { queue = drawB(); queue.context[0x201] = eqaa; @@ -3293,7 +3332,7 @@ void msaaEnableClearDepthTests() { expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"); queue = drawB(); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); queue = drawB(); queue.context[0x010] = 0xa2900987; expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); @@ -3600,7 +3639,7 @@ void multisampleTests() { const auto aliased = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); queue = multisampled(); queue.context[0x292] = 2; queue.context[0x302] = 0x44cc; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp new file mode 100644 index 0000000000..a5e2dfc7f6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp @@ -0,0 +1,330 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif +#include <algorithm> +#include <array> +#include <bit> +#include <cmath> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <exception> +#include <iostream> +#include <memory> +#include <span> +#include <string> +#include <vector> + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t AaConfig4x = 0x0020c002u; +constexpr std::uint32_t MsaaEnableClear = 2; +constexpr std::uint32_t StandardLocations4x = 0x622ae6aeu; +constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Type2DMsaa = 14; +constexpr std::uint32_t TileRenderTarget = 0x1b; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr std::array<std::uint32_t, 2> Primitives{2u, 3u}; +constexpr std::array<float, 4> Drawn{0.25f, 0.5f, 0.125f, 1.0f}; +constexpr std::array<std::byte, 4> DrawnBytes{std::byte{64}, std::byte{128}, std::byte{32}, std::byte{255}}; + +alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::uint32_t, 11> PixelCode{ + 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::uint32_t, 11> Unorm8PixelCode{ + 0x7e0802ff, std::bit_cast<std::uint32_t>(64.0f / 255.0f), 0x7e0a02ff, std::bit_cast<std::uint32_t>(128.0f / 255.0f), 0x7e0c02ff, std::bit_cast<std::uint32_t>(32.0f / 255.0f), 0x7e0e02ff, std::bit_cast<std::uint32_t>(1.0f), 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::uint32_t, 26> LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::array<float, 4>, 4> Lines{{ + {-0.828125f, -0.4375f, 0.5f, 1.0f}, {0.90625f, 0.140625f, 0.5f, 1.0f}, + {-0.265625f, 0.90625f, 0.5f, 1.0f}, {0.34375f, -0.921875f, 0.5f, 1.0f}, +}}; + +alignas(256) std::array<std::uint32_t, Threads * Words> Buffer{}; +alignas(256) std::array<std::array<std::byte, 4096>, Primitives.size()> LinePixels{}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::QueueState LineQueue(const Block& block, std::uint32_t primitive) { + const auto half = std::bit_cast<std::uint32_t>(static_cast<float>(Side) / 2.0f); + const auto extent = (Side << 16u) | Side; + AgcDriver::QueueState queue; + queue.userConfig[0x242] = primitive; + queue.context = { + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, AaConfig4x}, {0x292, MsaaEnableClear}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, + {0x318, static_cast<std::uint32_t>(block.Address() >> 8u)}, {0x390, static_cast<std::uint32_t>(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, Float16x4Info}, {0x31d, MultisampledAttrib}, + {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000}, {0x91, extent}, {0x94, 0x80000000}, {0x95, extent}, + {0x10f, half}, {0x110, half}, {0x111, std::bit_cast<std::uint32_t>(-static_cast<float>(Side) / 2.0f)}, {0x112, half}, + {0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, {0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)} + }; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? StandardLocations4x : 0u; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +AgcDriver::Graphics::State DecodeLines(const Block& block, std::uint32_t primitive) { + const auto state = AgcDriver::Graphics::DecodeState(LineQueue(block, primitive)); + const auto& color = state.color; + Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP), "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + " did not decode as a line list or strip"); + Require(state.colors.size() == 1 && color.address == block.Address() && color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + Require(state.samples == Samples && !state.depth && !state.perSampleShading && !state.alphaToCoverage, "the lines did not decode as a 4x draw without depth, sample shading or alpha-to-coverage"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < Samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, "the decoded 4x line state does not put every sample at the pixel center"); + Require(state.viewport.x == 0.0f && state.viewport.y == static_cast<float>(Side) && state.viewport.width == static_cast<float>(Side) && state.viewport.height == -static_cast<float>(Side) && state.scissor.offset.x == 0 && state.scissor.offset.y == 0 && state.scissor.extent.width == Side && state.scissor.extent.height == Side, "the decoded viewport or scissor does not cover the 16x16 target"); + return state; +} + +AgcDriver::Graphics::State OneSample(AgcDriver::Graphics::State state, std::span<std::byte> pixels, const AgcDriver::Graphics::ColorTargetLayout& linear) { + state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {Side, Side}, VK_FORMAT_R8G8B8A8_UNORM, linear.Bytes(), 0xe4u}; + state.colors = {state.color}; + state.samples = 1; + state.customSampleLocations = false; + state.sampleLocations.fill(0); + state.sampleLocationGrid = false; + return state; +} + +void DrawLines(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::span<const std::uint32_t> pixelCode) { + const auto target = device.Target(); + std::vector<std::uint32_t> vertexUserData(4, 0u); + const auto address = reinterpret_cast<std::uintptr_t>(Lines.data()); + const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Lines.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector<std::uint32_t> pixelUserData(8, 0u); + const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data()); + const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Lines.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::vector<std::array<float, 4>> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { + const auto address = block.Address(); + const std::array<std::uint32_t, 8> texture{ + static_cast<std::uint32_t>(address >> 8u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (SampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), + 0u, + SampleShift << 4u, + 0u, + 0u, + }; + const auto bufferAddress = reinterpret_cast<std::uintptr_t>(Buffer.data()); + const std::array<std::uint32_t, 4> buffer{static_cast<std::uint32_t>(bufferAddress), static_cast<std::uint32_t>((bufferAddress >> 32u) & 0xffffu), static_cast<std::uint32_t>(Buffer.size() * 4u), 0x31016facu}; + std::vector<std::uint32_t> userData(16, 0u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(LoadCode.data()), LoadCode, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::vector<std::array<float, 4>> loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array<std::uint32_t, 6> words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast<float>(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +bool NearEndpoint(std::uint32_t x, std::uint32_t y) { + return std::any_of(Lines.begin(), Lines.end(), [&](const std::array<float, 4>& vertex) { + const double px = (static_cast<double>(vertex[0]) + 1.0) * 0.5 * Side; + const double py = Side - (static_cast<double>(vertex[1]) + 1.0) * 0.5 * Side; + return std::fabs(x + 0.5 - px) < 1.5 && std::fabs(y + 0.5 - py) < 1.5; + }); +} + +void CompareLines(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t primitive, std::span<std::byte> pixels) { + const auto decoded = DecodeLines(block, primitive); + DrawLines(device, decoded, PixelCode); + const auto loaded = LoadSamples(device, block); + const AgcDriver::Graphics::ColorTargetLayout linear(Side, Side, AgcDriver::Graphics::ColorTileMode::Linear, 4u); + Require(linear.Bytes() <= pixels.size(), "the one-sample line target does not fit its memory"); + std::fill(pixels.begin(), pixels.end(), std::byte{0}); + DrawLines(device, OneSample(decoded, pixels, linear), Unorm8PixelCode); + std::uint32_t covered = 0; + std::uint32_t single = 0; + std::uint32_t compared = 0; + std::uint32_t differing = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + char where[80]; + std::snprintf(where, sizeof(where), "VGT_PRIMITIVE_TYPE %u: pixel (%u, %u)", primitive, x, y); + std::uint32_t drawn = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + Require(texel == Drawn || texel == std::array<float, 4>{}, std::string(where) + " of the 4x target holds neither the line color nor the cleared value"); + drawn += texel == Drawn ? 1u : 0u; + } + Require(drawn == 0u || drawn == Samples, std::string(where) + " has " + std::to_string(drawn) + " of its 4 samples covered by a line with every sample at the pixel center"); + const auto bytes = pixels.begin() + static_cast<std::ptrdiff_t>(linear.Offset(x, y)); + const bool oneSample = std::equal(DrawnBytes.begin(), DrawnBytes.end(), bytes); + Require(oneSample || std::all_of(bytes, bytes + 4, [](std::byte value) { return value == std::byte{0}; }), std::string(where) + " of the one-sample target holds neither the line color nor zero"); + covered += drawn == Samples ? 1u : 0u; + single += oneSample ? 1u : 0u; + if (NearEndpoint(x, y)) continue; + ++compared; + differing += (drawn == Samples) != oneSample ? 1u : 0u; + Require(!oneSample || drawn == Samples, std::string(where) + " is drawn by the one-sample line but not covered in the 4x target"); + } + } + Require(covered >= 20u && covered <= 2u * single && compared >= 200u, "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + ": the 4x lines covered " + std::to_string(covered) + " pixels and the one-sample lines " + std::to_string(single) + ", with " + std::to_string(compared) + " pixels compared"); + std::printf("VGT_PRIMITIVE_TYPE %u: 4x lines with every sample at the pixel center covered %u pixels in full and none in part; the one-sample lines drew %u; %u of the %u pixels away from the endpoints differ\n", primitive, covered, single, differing, compared); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { + std::puts("skipped, the device has no shaderStorageImageMultisample to load the samples of a 4x target"); + return VulkanTestSkipped; + } + if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).sampleLocationSampleCounts & Samples) == 0u) { + std::puts("skipped, the device has no VK_EXT_sample_locations at 4 samples to put every sample at the pixel center"); + return VulkanTestSkipped; + } + Block listTarget(4u * BlockBytes); + Block stripTarget(4u * BlockBytes); + struct ReleaseDevice { + std::unique_ptr<AgcDriver::VulkanDevice>& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + const std::array<const Block*, Primitives.size()> targets{&listTarget, &stripTarget}; + for (std::size_t index = 0; index < Primitives.size(); ++index) CompareLines(*device, *targets[index], Primitives[index], LinePixels[index]); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c4ea080793..fa40056151 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -31,9 +31,9 @@ - [Point-list geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` (point list) runs as a mesh shader with `OutputPoints` and scalar `PrimitivePointIndicesEXT`. Each primitive export emits one point whose vertex index is extracted from bits [8:0] of the packed word. A subgroup gets up to `maxVertices` points, with provoking vertex 0 and point raster primitive topology. Rectangle lists, per-stream types and point or line strips from a vertex program on the mesh path still throw. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. -- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. +- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw -- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - not measured on hardware: `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear puts every color, depth and stencil sample at the pixel center, as in GL without multisampling ([ARB_multisample](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089/extensions/ARB/ARB_multisample.txt#L185), [RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11)). Lines cover whole pixels by the host's line rule, not the console's aliased one ([D3D11.3](https://github.com/microsoft/DirectX-Specs/blob/f37b0d6033a1/d3d/archive/D3D11_3_FunctionalSpec.htm#L13705)). Depth sample tests and occlusion counts are unknown. Rasterized points, alpha-to-coverage, and Z interpolation, overrasterization or a sample-mask export with depth throw. PPSA06888 draws a 2x target, PPSA18216 a 4x depth and color pair and 4x lines. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From 0225ae65a77138267968fcf823966d637eb5a2d9 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Thu, 22 Oct 2026 05:03:59 +0100 Subject: [PATCH 0925/1360] test(libSceVideoOut): find the pre-vblank event among every queued event The controls check read exactly two events after subscribing to pre-vblank and waiting for one vblank, and expected one of them to be PRE_VBLANK_START. The VideoOut filter queues every vblank of the earlier VBLANK registration, and the queue returns one registration's events before the next, so when a second vblank lands between the earlier read and the wait (a real display's cadence), both events read are VBLANK and the pre-vblank event is the third. The check now reads up to eight events and requires a VBLANK for the vblank registration and a PRE_VBLANK_START with its own user data among them. --- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 59241e643b..d9ae149b3b 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -479,10 +479,12 @@ void testControls() { check(owner->GetTriggeredEvents(&event, 1) == 1 && event.udata == &settings && sceVideoOutGetEventId(&event) == VIDEO_OUT_EVENT_VBLANK, "vblank event or updated user data missing"); check(sceVideoOutAddPreVblankStartEvent(queue, handle, &mode) == 0, "pre-vblank subscription failed"); check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed"); - KernelEvent events[2]{}; - check(owner->GetTriggeredEvents(events, 2) == 2, "pre-vblank or vblank event missing"); - const auto& preVblank = sceVideoOutGetEventId(&events[0]) == VIDEO_OUT_EVENT_PRE_VBLANK_START ? events[0] : events[1]; - check(sceVideoOutGetEventId(&preVblank) == VIDEO_OUT_EVENT_PRE_VBLANK_START && preVblank.udata == &mode, "pre-vblank event missing"); + std::array<KernelEvent, 8> events{}; + const int collected = owner->GetTriggeredEvents(events.data(), static_cast<int>(events.size())); + check(collected >= 2, "pre-vblank or vblank event missing"); + const auto end = events.begin() + collected; + check(std::any_of(events.begin(), end, [&](KernelEvent& item) { return sceVideoOutGetEventId(&item) == VIDEO_OUT_EVENT_VBLANK && item.udata == &settings; }), "vblank event missing"); + check(std::any_of(events.begin(), end, [&](KernelEvent& item) { return sceVideoOutGetEventId(&item) == VIDEO_OUT_EVENT_PRE_VBLANK_START && item.udata == &mode; }), "pre-vblank event missing"); check(sceVideoOutDeletePreVblankStartEvent(queue, handle) == 0, "pre-vblank unsubscription failed"); std::vector<std::byte> allocation(65536 + 65535); const auto storage = alignedBuffer(allocation); From f4f60cf27765bb874bf67abaec027c67b4e07d0d Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Thu, 22 Oct 2026 05:05:28 +0100 Subject: [PATCH 0926/1360] test(libSceVideoOut): run the VideoOut flip tests with SDL's windows driver on Windows ANYPS5_VIDEO_OUT_TEST_DRIVER defaulted to SDL's offscreen driver on every host. That driver has no Vulkan on Windows, so all thirteen flip tests that open a display skipped there. Windows now defaults to the windows driver. A machine without a Vulkan driver makes DisplayWindow report that Vulkan lacks VK_KHR_surface, so the flip tests also skip on a DisplayWindow error that names Vulkan, as they do on offscreen's message. --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 7 ++++++- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 2b3dd27800..b60db0f129 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -84,7 +84,12 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() - set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING + if(WIN32) + set(videoOutTestDriver "windows") + else() + set(videoOutTestDriver "offscreen") + endif() + set(ANYPS5_VIDEO_OUT_TEST_DRIVER "${videoOutTestDriver}" CACHE STRING "SDL video driver for VideoOut flip tests") foreach(flipMode IN ITEMS labels labels_main) diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index d9ae149b3b..365eddf412 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -1054,7 +1054,9 @@ int main(int argc, char** argv) { std::fprintf(stderr, "%s\n", error.what()); try { LibcRunShutdown_nid_postfix(); } catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } - if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { + const std::string what = error.what(); + const bool noVulkanWindow = what.find("Vulkan support") != std::string::npos || (what.starts_with("DisplayWindow: ") && what.find("Vulkan") != std::string::npos); + if (noVulkanWindow && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { std::printf("skipped, no display or Vulkan device: %s\n", error.what()); return 77; } From 581905d997628546342e11246b6d0cd0b5a62861 Mon Sep 17 00:00:00 2001 From: erickerxhalli <313650569+erickerxhalli@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:20:28 +0000 Subject: [PATCH 0927/1360] docs(libkernel_sys): record which SDK field sceKernelGetCompiledSdkVersion reads The PS5 payload SDK declares the process parameters with a 32-bit PS4 SDK version at offset 0x10 and a 32-bit PS5 SDK version at 0x14, not one 64-bit field. The debt entry now states that the export returns the field at 0x10, as libSceHmd2 and shadPS4 read it, and that which field the console returns is unmeasured. The test puts different values at 0x10 and 0x14 instead of one 64-bit value with its upper half set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYjMwvLdPR8iX8EP8tQYWa --- core/libs/tests/GuestKernelSysSdkVersion.cpp | 8 +++++--- docs/dev/debt/libkernel_sys.md | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/GuestKernelSysSdkVersion.cpp b/core/libs/tests/GuestKernelSysSdkVersion.cpp index 2acba5d6d9..5579fe7e5e 100644 --- a/core/libs/tests/GuestKernelSysSdkVersion.cpp +++ b/core/libs/tests/GuestKernelSysSdkVersion.cpp @@ -26,16 +26,18 @@ void Require(bool condition, const char* message) { int main() { const std::uint64_t size = process.size(); const std::uint32_t magic = 0x4942524f; - const std::uint64_t sdkVersion = 0xFFFFFFFF07008001ull; + const std::uint32_t ps4SdkVersion = 0x07008001; + const std::uint32_t ps5SdkVersion = 0x0a008001; std::memcpy(process.data(), &size, sizeof(size)); std::memcpy(process.data() + 8, &magic, sizeof(magic)); - std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + std::memcpy(process.data() + 0x10, &ps4SdkVersion, sizeof(ps4SdkVersion)); + std::memcpy(process.data() + 0x14, &ps5SdkVersion, sizeof(ps5SdkVersion)); Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); Require(sceKernelGetCompiledSdkVersion(nullptr) == static_cast<int>(0x80020016), "null output is not EINVAL"); std::array<std::int32_t, 2> output{-1, 0x5a5a5a5a}; Require(sceKernelGetCompiledSdkVersion(output.data()) == 0, "query failed"); - Require(output[0] == 0x07008001, "version is not the low 32 bits of the process parameters' SDK version"); + Require(output[0] == 0x07008001, "version is not the 32-bit field at offset 0x10 of the process parameters"); Require(output[1] == 0x5a5a5a5a, "the query wrote past the version"); std::puts("KERNEL SYS SDK VERSION tests passed"); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 3767f130f3..9da76b47f8 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -5,7 +5,7 @@ ### Unknown function info -- [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a PS5: its `int (int32_t*)` signature and behaviour follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62) (PS4). A null output returns `EINVAL` (`0x80020016`); otherwise it writes the low 32 bits of the 64-bit SDK version at offset `0x10` of the title's process parameters, the field shadPS4 [reads from the eboot](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138), and returns 0. Closes when: measured on a PS5 +- [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - which SDK version the console returns is unmeasured. The [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/crt/kernel.c#L257-L275) declares the process parameters with a 32-bit PS4 SDK version at offset `0x10` and a 32-bit PS5 SDK version at `0x14`; this export returns the field at `0x10`, the one [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reads and the one [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138) (PS4, a single 64-bit `sdk_version` there) reads from the eboot. The `int (int32_t*)` signature and `EINVAL` (`0x80020016`) for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62). Closes when: measured on a PS5 ### Functional From a542a13f0017eeb68c352fc90a973d1bf52706d8 Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:37:27 +0500 Subject: [PATCH 0928/1360] feat(libSceVorbisDec): decode Ogg Vorbis with libvorbisfile 1.3.7 Add libogg 1.3.6 and libvorbis 1.3.7 as submodules, built from source as hidden static libraries, and implement the 31 libvorbisfile exports on top of them. Decoder state lives in a host table keyed by the guest OggVorbis_File, guest callbacks are trampolined, and vorbis_info is returned in its LP64 layout. --- .gitmodules | 6 + 3rdparty/ogg | 1 + 3rdparty/vorbis | 1 + core/libs/CMakeLists.txt | 4 +- core/libs/prx/libSceVorbisDec/CMakeLists.txt | 77 ++ core/libs/prx/libSceVorbisDec/Export.cpp | 339 ++++++--- .../include/GuestVorbisFile.hpp | 46 ++ core/libs/tests/GuestVorbisDec.cpp | 696 ++++++++++++++++-- docs/dev/BUILD.md | 2 +- docs/dev/debt/libSceVorbisDec.md | 38 +- tools/package_release.py | 3 + tools/tests/test_package_release.py | 15 + 12 files changed, 1026 insertions(+), 202 deletions(-) create mode 160000 3rdparty/ogg create mode 160000 3rdparty/vorbis create mode 100644 core/libs/prx/libSceVorbisDec/CMakeLists.txt create mode 100644 core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp diff --git a/.gitmodules b/.gitmodules index 64a98a2f94..215534a440 100644 --- a/.gitmodules +++ b/.gitmodules @@ -52,3 +52,9 @@ [submodule "3rdparty/vgmstream"] path = 3rdparty/vgmstream url = https://github.com/vgmstream/vgmstream.git +[submodule "3rdparty/ogg"] + path = 3rdparty/ogg + url = https://github.com/xiph/ogg.git +[submodule "3rdparty/vorbis"] + path = 3rdparty/vorbis + url = https://github.com/xiph/vorbis.git diff --git a/3rdparty/ogg b/3rdparty/ogg new file mode 160000 index 0000000000..be05b13e98 --- /dev/null +++ b/3rdparty/ogg @@ -0,0 +1 @@ +Subproject commit be05b13e98b048f0b5a0f5fa8ce514d56db5f822 diff --git a/3rdparty/vorbis b/3rdparty/vorbis new file mode 160000 index 0000000000..0657aee69d --- /dev/null +++ b/3rdparty/vorbis @@ -0,0 +1 @@ +Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d2a614da5e..e2422f9e58 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -697,9 +697,11 @@ if(BUILD_TESTING) add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp) target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec libc) + target_compile_definitions(guest_vorbis_dec_tests PRIVATE OV_EXCLUDE_STATIC_CALLBACKS) + target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec vorbisenc vorbisfile vorbis ogg libc) configure_windows_unwind(guest_vorbis_dec_tests) add_test(NAME guest_vorbis_dec COMMAND guest_vorbis_dec_tests) + set_tests_properties(guest_vorbis_dec PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVorbisDec>") diff --git a/core/libs/prx/libSceVorbisDec/CMakeLists.txt b/core/libs/prx/libSceVorbisDec/CMakeLists.txt new file mode 100644 index 0000000000..0da3f0a43e --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/CMakeLists.txt @@ -0,0 +1,77 @@ +enable_language(C) + +set(OGG_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ogg) +set(VORBIS_DIR ${CMAKE_SOURCE_DIR}/3rdparty/vorbis) +set(OGG_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/ogg/include) + +include(CheckIncludeFiles) +check_include_files(inttypes.h INCLUDE_INTTYPES_H) +check_include_files(stdint.h INCLUDE_STDINT_H) +check_include_files(sys/types.h INCLUDE_SYS_TYPES_H) +set(SIZE16 int16_t) +set(USIZE16 uint16_t) +set(SIZE32 int32_t) +set(USIZE32 uint32_t) +set(SIZE64 int64_t) +set(USIZE64 uint64_t) +configure_file(${OGG_DIR}/include/ogg/config_types.h.in ${OGG_GENERATED_DIR}/ogg/config_types.h @ONLY) + +add_library(ogg STATIC EXCLUDE_FROM_ALL + ${OGG_DIR}/src/bitwise.c + ${OGG_DIR}/src/framing.c +) +target_include_directories(ogg PUBLIC ${OGG_DIR}/include ${OGG_GENERATED_DIR}) + +add_library(vorbis STATIC EXCLUDE_FROM_ALL + ${VORBIS_DIR}/lib/analysis.c + ${VORBIS_DIR}/lib/bitrate.c + ${VORBIS_DIR}/lib/block.c + ${VORBIS_DIR}/lib/codebook.c + ${VORBIS_DIR}/lib/envelope.c + ${VORBIS_DIR}/lib/floor0.c + ${VORBIS_DIR}/lib/floor1.c + ${VORBIS_DIR}/lib/info.c + ${VORBIS_DIR}/lib/lookup.c + ${VORBIS_DIR}/lib/lpc.c + ${VORBIS_DIR}/lib/lsp.c + ${VORBIS_DIR}/lib/mapping0.c + ${VORBIS_DIR}/lib/mdct.c + ${VORBIS_DIR}/lib/psy.c + ${VORBIS_DIR}/lib/registry.c + ${VORBIS_DIR}/lib/res0.c + ${VORBIS_DIR}/lib/sharedbook.c + ${VORBIS_DIR}/lib/smallft.c + ${VORBIS_DIR}/lib/synthesis.c + ${VORBIS_DIR}/lib/window.c +) +target_include_directories(vorbis PUBLIC ${VORBIS_DIR}/include PRIVATE ${VORBIS_DIR}/lib) +target_link_libraries(vorbis PUBLIC ogg) +if(NOT WIN32) + target_link_libraries(vorbis PRIVATE m) +endif() + +add_library(vorbisfile STATIC EXCLUDE_FROM_ALL ${VORBIS_DIR}/lib/vorbisfile.c) +target_include_directories(vorbisfile PRIVATE ${VORBIS_DIR}/lib) +target_link_libraries(vorbisfile PUBLIC vorbis) +set_target_properties(ogg vorbis vorbisfile PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +if(MINGW) + foreach(library IN ITEMS ogg vorbis vorbisfile) + target_compile_options(${library} PRIVATE -fexceptions) + endforeach() +endif() + +add_library(libSceVorbisDec SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVorbisDec PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_definitions(libSceVorbisDec PRIVATE OV_EXCLUDE_STATIC_CALLBACKS) +target_link_libraries(libSceVorbisDec PRIVATE vorbisfile vorbis ogg) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceVorbisDec PRIVATE LINKER:--exclude-libs,ALL) +endif() +configure_windows_unwind(libSceVorbisDec) + +if(BUILD_TESTING) + add_library(vorbisenc STATIC EXCLUDE_FROM_ALL ${VORBIS_DIR}/lib/vorbisenc.c) + target_include_directories(vorbisenc PRIVATE ${VORBIS_DIR}/lib) + target_link_libraries(vorbisenc PUBLIC vorbis) + set_target_properties(vorbisenc PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +endif() diff --git a/core/libs/prx/libSceVorbisDec/Export.cpp b/core/libs/prx/libSceVorbisDec/Export.cpp index 44ae4fcc5b..b6b60cadf5 100644 --- a/core/libs/prx/libSceVorbisDec/Export.cpp +++ b/core/libs/prx/libSceVorbisDec/Export.cpp @@ -1,33 +1,202 @@ +#include "prx/libSceVorbisDec/include/GuestVorbisFile.hpp" +#include "prx/libc/include/General.hpp" + +#include <vorbis/vorbisfile.h> + #include <cstddef> #include <cstdint> -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include <cstdio> +#include <limits> +#include <memory> +#include <mutex> +#include <stdexcept> +#include <string> +#include <unordered_map> +#include <vector> + +using namespace VorbisDec; + +static_assert(sizeof(vorbis_comment) == sizeof(GuestComment)); +static_assert(offsetof(vorbis_comment, user_comments) == offsetof(GuestComment, user_comments)); +static_assert(offsetof(vorbis_comment, comment_lengths) == offsetof(GuestComment, comment_lengths)); +static_assert(offsetof(vorbis_comment, comments) == offsetof(GuestComment, comments)); +static_assert(offsetof(vorbis_comment, vendor) == offsetof(GuestComment, vendor)); +static_assert(SEEK_SET == 0 && SEEK_CUR == 1 && SEEK_END == 2); + +namespace { + +struct Handle { + OggVorbis_File file{}; + void* datasource = nullptr; + GuestCallbacks callbacks{}; + std::vector<GuestInfo> infos; + bool tellOutOfRange = false; + + ~Handle() { + file.datasource = nullptr; + ov_clear(&file); + } +}; + +struct Table { + std::mutex mutex; + std::unordered_map<const void*, std::unique_ptr<Handle>> handles; +}; + +Table& Handles() { + static auto* table = new Table; + return *table; +} + +Handle* Find(const void* vf) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + const auto found = table.handles.find(vf); + return found == table.handles.end() ? nullptr : found->second.get(); +} + +std::unique_ptr<Handle> Take(const void* vf) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + const auto found = table.handles.find(vf); + if (found == table.handles.end()) return nullptr; + auto handle = std::move(found->second); + table.handles.erase(found); + return handle; +} + +void Store(const void* vf, std::unique_ptr<Handle> handle) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + table.handles[vf] = std::move(handle); +} + +void SyncInfos(Handle& handle) { + for (std::size_t link = 0; link < handle.infos.size(); ++link) { + const vorbis_info& info = handle.file.vi[link]; + handle.infos[link] = {info.version, info.channels, info.rate, info.bitrate_upper, info.bitrate_nominal, info.bitrate_lower, info.bitrate_window, info.codec_setup}; + } +} + +void RequireFile(const void* vf, const char* function) { + if (!vf) throw std::invalid_argument(std::string(function) + ": a null OggVorbis_File is not implemented"); +} + +template <typename TCall> +auto WithHandle(const void* vf, const char* function, TCall call) { + RequireFile(vf, function); + if (Handle* handle = Find(vf)) { + const auto result = call(*handle); + SyncInfos(*handle); + return result; + } + Handle closed; + return call(closed); +} + +std::size_t ReadSource(void* buffer, std::size_t size, std::size_t count, void* source) { + const auto* handle = static_cast<const Handle*>(source); + return handle->callbacks.read_func(buffer, size, count, handle->datasource); +} + +int SeekSource(void* source, ogg_int64_t offset, int whence) { + const auto* handle = static_cast<const Handle*>(source); + return handle->callbacks.seek_func(handle->datasource, offset, whence); +} + +int CloseSource(void* source) { + const auto* handle = static_cast<const Handle*>(source); + return handle->callbacks.close_func(handle->datasource); +} + +long TellSource(void* source) { + auto* handle = static_cast<Handle*>(source); + const std::int64_t position = handle->callbacks.tell_func(handle->datasource); + if (position < std::numeric_limits<long>::min() || position > std::numeric_limits<long>::max()) { + handle->tellOutOfRange = true; + return -1; + } + return static_cast<long>(position); +} + +struct FilterContext { + GuestFilter filter; + void* param; +}; + +void RunFilter(float** pcm, long channels, long samples, void* context) { + const auto* filter = static_cast<const FilterContext*>(context); + filter->filter(pcm, channels, samples, filter->param); +} + +} extern "C" { -int APS5_VABI sceVorbisDecBitrate(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVorbisDecOpenCallbacks(void* datasource, void* vf, const char* initial, std::int64_t ibytes, GuestCallbacks callbacks) { + RequireFile(vf, __func__); + if (initial && (ibytes < std::numeric_limits<long>::min() || ibytes > std::numeric_limits<long>::max())) { + throw std::runtime_error(std::string(__func__) + ": an initial byte count outside the host long range is not implemented"); + } + Take(vf).reset(); + auto handle = std::make_unique<Handle>(); + handle->datasource = datasource; + handle->callbacks = callbacks; + const ov_callbacks host{ + callbacks.read_func ? ReadSource : nullptr, + callbacks.seek_func ? SeekSource : nullptr, + callbacks.close_func ? CloseSource : nullptr, + callbacks.tell_func ? TellSource : nullptr, + }; + const int result = ov_open_callbacks(datasource ? handle.get() : nullptr, &handle->file, initial, initial ? static_cast<long>(ibytes) : 0, host); + if (handle->tellOutOfRange) { + throw std::runtime_error(std::string(__func__) + ": a stream length outside the host long range is not implemented"); + } + if (result != 0) return result; + handle->infos.resize(static_cast<std::size_t>(handle->file.links)); + SyncInfos(*handle); + Store(vf, std::move(handle)); return 0; } -int APS5_VABI sceVorbisDecBitrateInstant(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecClear(void* vf) { + const std::unique_ptr<Handle> handle = Take(vf); + if (!handle) { + Handle closed; + return ov_clear(&closed.file); + } + void* const source = handle->file.datasource; + handle->file.datasource = nullptr; + const int result = ov_clear(&handle->file); + if (source && handle->callbacks.close_func) handle->callbacks.close_func(handle->datasource); + return result; } -int APS5_VABI sceVorbisDecClear(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecBitrate(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_bitrate(&handle.file, link); }); } -int APS5_VABI sceVorbisDecComment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecBitrateInstant(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_bitrate_instant(&handle.file); }); } -int APS5_VABI sceVorbisDecCrossLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +GuestComment* APS5_VABI sceVorbisDecComment(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) { return reinterpret_cast<GuestComment*>(ov_comment(&handle.file, link)); }); +} + +int APS5_VABI sceVorbisDecCrossLap(void* first, void* second) { + if (first != second) RequireFile(first, __func__); + Handle* from = Find(first); + if (first != second && from) RequireFile(second, __func__); + Handle* to = second == first ? from : Find(second); + Handle closedFrom; + Handle closedTo; + OggVorbis_File* fromFile = from ? &from->file : &closedFrom.file; + OggVorbis_File* toFile = second == first ? fromFile : to ? &to->file : &closedTo.file; + const int result = ov_crosslap(fromFile, toFile); + if (from) SyncInfos(*from); + if (to) SyncInfos(*to); + return result; } int APS5_VABI sceVorbisDecGetDecodePriority(void) { @@ -40,99 +209,84 @@ int APS5_VABI sceVorbisDecGetLastDecodeError(void) { return 0; } -int APS5_VABI sceVorbisDecHalfrate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecHalfrate(void* vf, int flag) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_halfrate(&handle.file, flag); }); } -int APS5_VABI sceVorbisDecHalfrateP(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecHalfrateP(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) { return ov_halfrate_p(&handle.file); }); } -int APS5_VABI sceVorbisDecInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +GuestInfo* APS5_VABI sceVorbisDecInfo(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> GuestInfo* { + const vorbis_info* info = ov_info(&handle.file, link); + return info ? &handle.infos.at(static_cast<std::size_t>(info - handle.file.vi)) : nullptr; + }); } -int APS5_VABI sceVorbisDecOpenCallbacks(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeek(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeek(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeekLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeekLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeekPage(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_page(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeekPage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeekPageLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_page_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeekPageLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecPcmTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_pcm_tell(&handle.file); }); } -int APS5_VABI sceVorbisDecPcmTell(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecPcmTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_pcm_total(&handle.file, link); }); } -int APS5_VABI sceVorbisDecPcmTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecRawSeek(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_raw_seek(&handle.file, position); }); } -int APS5_VABI sceVorbisDecRawSeek(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecRawSeekLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_raw_seek_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecRawSeekLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecRawTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_raw_tell(&handle.file); }); } -int APS5_VABI sceVorbisDecRawTell(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecRawTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_raw_total(&handle.file, link); }); } -int APS5_VABI sceVorbisDecRawTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecRead(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { + return ov_read(&handle.file, buffer, length, bigEndian, word, isSigned, bitstream); + }); } -int APS5_VABI sceVorbisDecRead(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecReadFilter(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream, GuestFilter filter, void* filterParam) { + FilterContext context{filter, filterParam}; + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { + return ov_read_filter(&handle.file, buffer, length, bigEndian, word, isSigned, bitstream, filter ? RunFilter : nullptr, &context); + }); } -int APS5_VABI sceVorbisDecReadFilter(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecReadFloat(void* vf, float*** pcmChannels, int samples, int* bitstream) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_read_float(&handle.file, pcmChannels, samples, bitstream); }); } -int APS5_VABI sceVorbisDecReadFloat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecSeekable(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_seekable(&handle.file); }); } -int APS5_VABI sceVorbisDecSeekable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVorbisDecSerialNumber(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecSerialNumber(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_serialnumber(&handle.file, link); }); } int APS5_VABI sceVorbisDecSetDecodePriority(void) { @@ -140,39 +294,32 @@ int APS5_VABI sceVorbisDecSetDecodePriority(void) { return 0; } -int APS5_VABI sceVorbisDecStreams(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecStreams(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_streams(&handle.file); }); } -int APS5_VABI sceVorbisDecTimeSeek(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeek(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeSeekLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeekLap(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeSeekPage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeekPage(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_page(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeSeekPageLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeekPageLap(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_page_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeTell(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +double APS5_VABI sceVorbisDecTimeTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) { return ov_time_tell(&handle.file); }); } -int APS5_VABI sceVorbisDecTimeTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +double APS5_VABI sceVorbisDecTimeTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_total(&handle.file, link); }); } } diff --git a/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp b/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp new file mode 100644 index 0000000000..9da5f3e1d5 --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp @@ -0,0 +1,46 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVORBISDEC_INCLUDE_GUESTVORBISFILE_HPP +#define CORE_LIBS_PRX_LIBSCEVORBISDEC_INCLUDE_GUESTVORBISFILE_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include <cstddef> +#include <cstdint> + +namespace VorbisDec { + +struct GuestCallbacks { + std::size_t (APS5_VABI *read_func)(void*, std::size_t, std::size_t, void*); + int (APS5_VABI *seek_func)(void*, std::int64_t, int); + int (APS5_VABI *close_func)(void*); + std::int64_t (APS5_VABI *tell_func)(void*); +}; + +struct GuestInfo { + std::int32_t version; + std::int32_t channels; + std::int64_t rate; + std::int64_t bitrate_upper; + std::int64_t bitrate_nominal; + std::int64_t bitrate_lower; + std::int64_t bitrate_window; + void* codec_setup; +}; + +struct GuestComment { + char** user_comments; + std::int32_t* comment_lengths; + std::int32_t comments; + char* vendor; +}; + +using GuestFilter = void (APS5_VABI *)(float**, std::int64_t, std::int64_t, void*); + +static_assert(sizeof(GuestCallbacks) == 32); +static_assert(offsetof(GuestInfo, rate) == 8 && offsetof(GuestInfo, bitrate_window) == 40 && offsetof(GuestInfo, codec_setup) == 48); +static_assert(sizeof(GuestInfo) == 56); +static_assert(offsetof(GuestComment, comment_lengths) == 8 && offsetof(GuestComment, comments) == 16 && offsetof(GuestComment, vendor) == 24); +static_assert(sizeof(GuestComment) == 32); + +} + +#endif diff --git a/core/libs/tests/GuestVorbisDec.cpp b/core/libs/tests/GuestVorbisDec.cpp index 80bce8c123..d34eb5d9ad 100644 --- a/core/libs/tests/GuestVorbisDec.cpp +++ b/core/libs/tests/GuestVorbisDec.cpp @@ -1,99 +1,649 @@ -#include "SceTypes.hpp" +#include "prx/libSceVorbisDec/include/GuestVorbisFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include <vorbis/vorbisenc.h> +#include <vorbis/vorbisfile.h> + +#include <algorithm> +#include <array> +#include <cmath> +#include <cstddef> #include <cstdint> #include <cstdio> #include <cstdlib> +#include <cstring> #include <stdexcept> +#include <vector> + +using namespace VorbisDec; extern "C" { -int APS5_VABI sceVorbisDecOpenCallbacks(void); -int APS5_VABI sceVorbisDecClear(void); -int APS5_VABI sceVorbisDecBitrate(void); -int APS5_VABI sceVorbisDecBitrateInstant(void); -int APS5_VABI sceVorbisDecStreams(void); -int APS5_VABI sceVorbisDecSeekable(void); -int APS5_VABI sceVorbisDecSerialNumber(void); -int APS5_VABI sceVorbisDecRawTotal(void); -int APS5_VABI sceVorbisDecPcmTotal(void); -int APS5_VABI sceVorbisDecTimeTotal(void); -int APS5_VABI sceVorbisDecRawSeek(void); -int APS5_VABI sceVorbisDecPcmSeek(void); -int APS5_VABI sceVorbisDecPcmSeekPage(void); -int APS5_VABI sceVorbisDecTimeSeek(void); -int APS5_VABI sceVorbisDecTimeSeekPage(void); -int APS5_VABI sceVorbisDecRawSeekLap(void); -int APS5_VABI sceVorbisDecPcmSeekLap(void); -int APS5_VABI sceVorbisDecPcmSeekPageLap(void); -int APS5_VABI sceVorbisDecTimeSeekLap(void); -int APS5_VABI sceVorbisDecTimeSeekPageLap(void); -int APS5_VABI sceVorbisDecRawTell(void); -int APS5_VABI sceVorbisDecPcmTell(void); -int APS5_VABI sceVorbisDecTimeTell(void); -int APS5_VABI sceVorbisDecInfo(void); -int APS5_VABI sceVorbisDecComment(void); -int APS5_VABI sceVorbisDecRead(void); -int APS5_VABI sceVorbisDecReadFloat(void); -int APS5_VABI sceVorbisDecReadFilter(void); -int APS5_VABI sceVorbisDecCrossLap(void); -int APS5_VABI sceVorbisDecHalfrate(void); -int APS5_VABI sceVorbisDecHalfrateP(void); +int APS5_VABI sceVorbisDecOpenCallbacks(void* datasource, void* vf, const char* initial, std::int64_t ibytes, GuestCallbacks callbacks); +int APS5_VABI sceVorbisDecClear(void* vf); +std::int64_t APS5_VABI sceVorbisDecBitrate(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecBitrateInstant(void* vf); +std::int64_t APS5_VABI sceVorbisDecStreams(void* vf); +std::int64_t APS5_VABI sceVorbisDecSeekable(void* vf); +std::int64_t APS5_VABI sceVorbisDecSerialNumber(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecRawTotal(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecPcmTotal(void* vf, int link); +double APS5_VABI sceVorbisDecTimeTotal(void* vf, int link); +int APS5_VABI sceVorbisDecRawSeek(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeek(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekPage(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecTimeSeek(void* vf, double position); +int APS5_VABI sceVorbisDecTimeSeekPage(void* vf, double position); +int APS5_VABI sceVorbisDecRawSeekLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekPageLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecTimeSeekLap(void* vf, double position); +int APS5_VABI sceVorbisDecTimeSeekPageLap(void* vf, double position); +std::int64_t APS5_VABI sceVorbisDecRawTell(void* vf); +std::int64_t APS5_VABI sceVorbisDecPcmTell(void* vf); +double APS5_VABI sceVorbisDecTimeTell(void* vf); +GuestInfo* APS5_VABI sceVorbisDecInfo(void* vf, int link); +GuestComment* APS5_VABI sceVorbisDecComment(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecRead(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream); +std::int64_t APS5_VABI sceVorbisDecReadFloat(void* vf, float*** pcmChannels, int samples, int* bitstream); +std::int64_t APS5_VABI sceVorbisDecReadFilter(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream, GuestFilter filter, void* filterParam); +int APS5_VABI sceVorbisDecCrossLap(void* first, void* second); +int APS5_VABI sceVorbisDecHalfrate(void* vf, int flag); +int APS5_VABI sceVorbisDecHalfrateP(void* vf); int APS5_VABI sceVorbisDecGetDecodePriority(void); int APS5_VABI sceVorbisDecSetDecodePriority(void); int APS5_VABI sceVorbisDecGetLastDecodeError(void); } -template <typename TCall> -static bool Throws(TCall call) { +namespace { + +constexpr int FirstSerial = 0x1234abcd; +constexpr int SecondSerial = -0x61c88647; +constexpr int FirstChannels = 3; +constexpr int SecondChannels = 2; +constexpr long FirstRate = 44100; +constexpr long SecondRate = 22050; +constexpr long FirstSamples = 44100; +constexpr long SecondSamples = 11025; +constexpr std::int64_t DecodedBytes = (FirstSamples * FirstChannels + SecondSamples * SecondChannels) * 2; +constexpr unsigned char Pattern = 0xA5; + +void Check(bool value, int line) { + if (value) return; + std::fprintf(stderr, "VorbisDec check failed at line %d\n", line); + std::abort(); +} +#define Require(value) Check((value), __LINE__) + +template <typename TException, typename TCall> +bool Throws(TCall call) { try { call(); - } catch (const std::runtime_error&) { + } catch (const TException&) { return true; } return false; } -static void Require(bool value, const char* what) { - if (value) return; - std::printf("VorbisDec test failed: %s\n", what); - std::abort(); +struct Source { + const std::vector<unsigned char>* bytes = nullptr; + std::size_t position = 0; + int closes = 0; + void* probe = nullptr; + std::int64_t probed = -1; + bool throwOnRead = false; + bool throwOnClose = false; +}; + +std::size_t ReadBytes(Source& source, void* buffer, std::size_t size, std::size_t count) { + if (source.throwOnRead) throw std::runtime_error("read callback"); + if (source.probe) source.probed = sceVorbisDecStreams(source.probe); + if (size == 0) return 0; + const std::size_t items = std::min(count, (source.bytes->size() - source.position) / size); + if (items != 0) std::memcpy(buffer, source.bytes->data() + source.position, items * size); + source.position += items * size; + return items; +} + +int SeekBytes(Source& source, std::int64_t offset, int whence) { + const auto size = static_cast<std::int64_t>(source.bytes->size()); + const std::int64_t base = whence == SEEK_SET ? 0 : whence == SEEK_CUR ? static_cast<std::int64_t>(source.position) : size; + if (offset < -base || offset > size - base) return -1; + source.position = static_cast<std::size_t>(base + offset); + return 0; +} + +std::size_t APS5_VABI GuestRead(void* buffer, std::size_t size, std::size_t count, void* source) { + return ReadBytes(*static_cast<Source*>(source), buffer, size, count); +} + +int APS5_VABI GuestSeek(void* source, std::int64_t offset, int whence) { + return SeekBytes(*static_cast<Source*>(source), offset, whence); +} + +int APS5_VABI GuestClose(void* source) { + auto& closing = *static_cast<Source*>(source); + ++closing.closes; + if (closing.throwOnClose) throw std::runtime_error("close callback"); + return 0; +} + +std::int64_t APS5_VABI GuestTell(void* source) { + return static_cast<std::int64_t>(static_cast<Source*>(source)->position); +} + +std::size_t HostRead(void* buffer, std::size_t size, std::size_t count, void* source) { + return ReadBytes(*static_cast<Source*>(source), buffer, size, count); +} + +int HostSeek(void* source, ogg_int64_t offset, int whence) { + return SeekBytes(*static_cast<Source*>(source), offset, whence); +} + +int HostClose(void* source) { + ++static_cast<Source*>(source)->closes; + return 0; +} + +long HostTell(void* source) { + return static_cast<long>(static_cast<Source*>(source)->position); +} + +constexpr GuestCallbacks SeekableCallbacks{GuestRead, GuestSeek, GuestClose, GuestTell}; +constexpr GuestCallbacks StreamingCallbacks{GuestRead, nullptr, GuestClose, nullptr}; +const ov_callbacks HostSeekableCallbacks{HostRead, HostSeek, HostClose, HostTell}; +const ov_callbacks HostStreamingCallbacks{HostRead, nullptr, HostClose, nullptr}; + +struct GuestFile { + alignas(16) std::array<unsigned char, 1024> bytes; + + GuestFile() { bytes.fill(Pattern); } + + void* Get() { return bytes.data(); } + + bool Untouched() const { + return std::all_of(bytes.begin(), bytes.end(), [](unsigned char value) { return value == Pattern; }); + } +}; + +struct FilterLog { + int calls = 0; + std::int64_t firstChannels = 0; + std::int64_t lastChannels = 0; + std::int64_t samples = 0; +}; + +void Halve(float** pcm, std::int64_t channels, std::int64_t samples) { + for (std::int64_t channel = 0; channel < channels; ++channel) + for (std::int64_t index = 0; index < samples; ++index) pcm[channel][index] *= 0.5f; +} + +void APS5_VABI GuestHalve(float** pcm, std::int64_t channels, std::int64_t samples, void* param) { + auto* log = static_cast<FilterLog*>(param); + if (log->calls++ == 0) log->firstChannels = channels; + log->lastChannels = channels; + log->samples += samples; + Halve(pcm, channels, samples); +} + +void HostHalve(float** pcm, long channels, long samples, void*) { + Halve(pcm, channels, samples); +} + +void AppendPage(std::vector<unsigned char>& out, const ogg_page& page) { + out.insert(out.end(), page.header, page.header + page.header_len); + out.insert(out.end(), page.body, page.body + page.body_len); +} + +void EncodeLink(std::vector<unsigned char>& out, int serial, int channels, long rate, long samples, const char* title) { + vorbis_info info; + vorbis_info_init(&info); + Require(vorbis_encode_init_vbr(&info, channels, rate, 0.1f) == 0); + vorbis_comment comment; + vorbis_comment_init(&comment); + vorbis_comment_add_tag(&comment, "TITLE", title); + vorbis_comment_add_tag(&comment, "ARTIST", "AnyPS5"); + vorbis_dsp_state dsp; + Require(vorbis_analysis_init(&dsp, &info) == 0); + vorbis_block block; + vorbis_block_init(&dsp, &block); + ogg_stream_state stream; + ogg_stream_init(&stream, serial); + ogg_packet identification; + ogg_packet comments; + ogg_packet codebooks; + vorbis_analysis_headerout(&dsp, &comment, &identification, &comments, &codebooks); + ogg_stream_packetin(&stream, &identification); + ogg_stream_packetin(&stream, &comments); + ogg_stream_packetin(&stream, &codebooks); + ogg_page page; + while (ogg_stream_flush(&stream, &page) != 0) AppendPage(out, page); + for (long done = 0;;) { + const long count = std::min(1024L, samples - done); + if (count > 0) { + float** buffer = vorbis_analysis_buffer(&dsp, static_cast<int>(count)); + for (int channel = 0; channel < channels; ++channel) { + const double frequency = 220.0 * (channel + 1) + 37.0 * serial / 1e9; + for (long index = 0; index < count; ++index) { + buffer[channel][index] = static_cast<float>(0.3 * std::sin(6.283185307179586 * frequency * static_cast<double>(done + index) / static_cast<double>(rate))); + } + } + } + vorbis_analysis_wrote(&dsp, static_cast<int>(count)); + done += count; + while (vorbis_analysis_blockout(&dsp, &block) == 1) { + vorbis_analysis(&block, nullptr); + vorbis_bitrate_addblock(&block); + ogg_packet packet; + while (vorbis_bitrate_flushpacket(&dsp, &packet) == 1) { + ogg_stream_packetin(&stream, &packet); + while (ogg_stream_pageout_fill(&stream, &page, 1024) != 0) AppendPage(out, page); + } + } + if (count == 0) break; + } + while (ogg_stream_flush(&stream, &page) != 0) AppendPage(out, page); + ogg_stream_clear(&stream); + vorbis_block_clear(&block); + vorbis_dsp_clear(&dsp); + vorbis_comment_clear(&comment); + vorbis_info_clear(&info); +} + +std::int64_t RequireSameRead(void* vf, OggVorbis_File& reference, int bigEndian = 0, int word = 2, int isSigned = 1) { + std::array<char, 4096> guest{}; + std::array<char, 4096> host{}; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecRead(vf, guest.data(), static_cast<int>(guest.size()), bigEndian, word, isSigned, &guestStream); + const long expected = ov_read(&reference, host.data(), static_cast<int>(host.size()), bigEndian, word, isSigned, &hostStream); + Require(got == expected); + Require(got <= 0 || (guestStream == hostStream && std::memcmp(guest.data(), host.data(), static_cast<std::size_t>(got)) == 0)); + return got; +} + +std::int64_t DecodeTogether(void* vf, OggVorbis_File& reference) { + std::int64_t total = 0; + for (;;) { + const std::int64_t got = RequireSameRead(vf, reference); + Require(got >= 0 || got == OV_HOLE); + if (got == 0) return total; + if (got > 0) total += got; + } +} + +void RequireSameTells(void* vf, OggVorbis_File& reference) { + Require(sceVorbisDecPcmTell(vf) == ov_pcm_tell(&reference)); + Require(sceVorbisDecRawTell(vf) == ov_raw_tell(&reference)); + Require(sceVorbisDecTimeTell(vf) == ov_time_tell(&reference)); +} + +void TestSeekable(const std::vector<unsigned char>& stream) { + GuestFile guest; + GuestFile probe; + void* vf = guest.Get(); + Source source{&stream}; + source.probe = probe.Get(); + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Require(source.probed == 0 && source.closes == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(sceVorbisDecStreams(vf) == 2 && sceVorbisDecSeekable(vf) == 1); + Require(sceVorbisDecSerialNumber(vf, 0) == FirstSerial && sceVorbisDecSerialNumber(vf, 1) == SecondSerial); + Require(sceVorbisDecSerialNumber(vf, -1) == FirstSerial && sceVorbisDecSerialNumber(vf, 7) == SecondSerial); + Require(sceVorbisDecPcmTotal(vf, 0) == FirstSamples && sceVorbisDecPcmTotal(vf, 1) == SecondSamples); + Require(sceVorbisDecPcmTotal(vf, -1) == FirstSamples + SecondSamples && sceVorbisDecPcmTotal(vf, 2) == OV_EINVAL); + Require(sceVorbisDecRawTotal(vf, -1) == ov_raw_total(&reference, -1) && sceVorbisDecRawTotal(vf, -1) > 0); + Require(sceVorbisDecRawTotal(vf, 0) == ov_raw_total(&reference, 0) && sceVorbisDecRawTotal(vf, 1) == ov_raw_total(&reference, 1)); + Require(sceVorbisDecTimeTotal(vf, 0) == 1.0 && sceVorbisDecTimeTotal(vf, 1) == 0.5 && sceVorbisDecTimeTotal(vf, -1) == 1.5); + Require(sceVorbisDecBitrate(vf, -1) == ov_bitrate(&reference, -1) && sceVorbisDecBitrate(vf, 1) == ov_bitrate(&reference, 1)); + Require(sceVorbisDecBitrate(vf, -1) > 0 && sceVorbisDecBitrate(vf, 2) == OV_EINVAL); + Require(sceVorbisDecBitrateInstant(vf) == OV_FALSE); + + GuestInfo* first = sceVorbisDecInfo(vf, 0); + GuestInfo* second = sceVorbisDecInfo(vf, 1); + Require(first && second && first != second && sceVorbisDecInfo(vf, 0) == first); + Require(sceVorbisDecInfo(vf, -1) == first && sceVorbisDecInfo(vf, 2) == nullptr); + Require(first->version == 0 && first->channels == FirstChannels && first->rate == FirstRate); + Require(second->version == 0 && second->channels == SecondChannels && second->rate == SecondRate); + for (int link = 0; link < 2; ++link) { + const GuestInfo* info = sceVorbisDecInfo(vf, link); + const vorbis_info* host = ov_info(&reference, link); + Require(info->bitrate_upper == host->bitrate_upper && info->bitrate_nominal == host->bitrate_nominal); + Require(info->bitrate_lower == host->bitrate_lower && info->bitrate_window == host->bitrate_window && info->codec_setup != nullptr); + } + const GuestComment* comment = sceVorbisDecComment(vf, 1); + Require(comment && comment->comments == 2 && comment->comment_lengths[0] == 12 && comment->comment_lengths[1] == 13); + Require(std::strcmp(comment->user_comments[0], "TITLE=second") == 0 && std::strcmp(comment->user_comments[1], "ARTIST=AnyPS5") == 0); + Require(std::strcmp(comment->vendor, ov_comment(&reference, 1)->vendor) == 0); + Require(sceVorbisDecComment(vf, -1) == sceVorbisDecComment(vf, 0) && sceVorbisDecComment(vf, 2) == nullptr); + Require(std::strcmp(sceVorbisDecComment(vf, 0)->user_comments[0], "TITLE=first") == 0); + + const std::int64_t firstRead = RequireSameRead(vf, reference); + Require(firstRead > 0 && sceVorbisDecBitrateInstant(vf) == ov_bitrate_instant(&reference)); + Require(sceVorbisDecInfo(vf, -1) == first); + Require(firstRead + DecodeTogether(vf, reference) == DecodedBytes); + Require(sceVorbisDecPcmTell(vf) == FirstSamples + SecondSamples && sceVorbisDecTimeTell(vf) == 1.5); + RequireSameTells(vf, reference); + Require(sceVorbisDecInfo(vf, -1) == second && sceVorbisDecSerialNumber(vf, -1) == SecondSerial); + + Require(sceVorbisDecPcmSeek(vf, 0) == 0 && ov_pcm_seek(&reference, 0) == 0); + Require(DecodeTogether(vf, reference) == DecodedBytes); + + Require(sceVorbisDecPcmSeek(vf, 1000) == 0 && ov_pcm_seek(&reference, 1000) == 0); + Require(sceVorbisDecPcmTell(vf) == 1000); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference, 1, 2, 0) > 0 && RequireSameRead(vf, reference, 0, 1, 0) > 0); + Require(sceVorbisDecPcmSeekPage(vf, FirstSamples + 3000) == ov_pcm_seek_page(&reference, FirstSamples + 3000)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == second); + Require(sceVorbisDecTimeSeek(vf, 0.25) == 0 && ov_time_seek(&reference, 0.25) == 0); + Require(sceVorbisDecPcmTell(vf) == FirstRate / 4); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == first); + Require(sceVorbisDecTimeSeekPage(vf, 1.2) == ov_time_seek_page(&reference, 1.2)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + const auto middle = static_cast<std::int64_t>(stream.size() / 2); + Require(sceVorbisDecRawSeek(vf, middle) == 0 && ov_raw_seek(&reference, middle) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + + Require(sceVorbisDecPcmSeekLap(vf, 5000) == 0 && ov_pcm_seek_lap(&reference, 5000) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecPcmSeekPageLap(vf, 30000) == ov_pcm_seek_page_lap(&reference, 30000)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecRawSeekLap(vf, middle / 2) == ov_raw_seek_lap(&reference, middle / 2)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecTimeSeekLap(vf, 1.1) == 0 && ov_time_seek_lap(&reference, 1.1) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == second); + Require(sceVorbisDecTimeSeekPageLap(vf, 0.75) == ov_time_seek_page_lap(&reference, 0.75)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == first); + + Require(sceVorbisDecPcmSeek(vf, -1) == OV_EINVAL && sceVorbisDecPcmSeek(vf, FirstSamples + SecondSamples + 1) == OV_EINVAL); + Require(sceVorbisDecTimeSeek(vf, -0.5) == OV_EINVAL && sceVorbisDecTimeSeek(vf, 2.0) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, -1) == OV_EINVAL && sceVorbisDecRawSeek(vf, middle * 3) == OV_EINVAL); + + Require(source.closes == 0 && sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(sceVorbisDecStreams(vf) == 0 && sceVorbisDecInfo(vf, 0) == nullptr && sceVorbisDecPcmTell(vf) == OV_EINVAL); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(ov_clear(&reference) == 0 && referenceSource.closes == 1); + Require(source.probed == 0 && guest.Untouched() && probe.Untouched()); +} + +void TestStreaming(const std::vector<unsigned char>& stream) { + GuestFile guest; + void* vf = guest.Get(); + const auto* initial = reinterpret_cast<const char*>(stream.data()); + Source source{&stream, 64}; + Require(sceVorbisDecOpenCallbacks(&source, vf, initial, 64, StreamingCallbacks) == 0); + Source referenceSource{&stream, 64}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, initial, 64, HostStreamingCallbacks) == 0); + + Require(sceVorbisDecSeekable(vf) == 0 && sceVorbisDecStreams(vf) == 1); + Require(sceVorbisDecPcmTotal(vf, -1) == OV_EINVAL && sceVorbisDecRawTotal(vf, 0) == OV_EINVAL && sceVorbisDecTimeTotal(vf, -1) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, 0) == OV_ENOSEEK && sceVorbisDecPcmSeek(vf, 0) == OV_ENOSEEK && sceVorbisDecTimeSeekPage(vf, 0.0) == OV_ENOSEEK); + Require(sceVorbisDecBitrate(vf, 0) == ov_bitrate(&reference, 0) && sceVorbisDecBitrate(vf, 1) == ov_bitrate(&reference, 1)); + Require(sceVorbisDecSerialNumber(vf, 3) == FirstSerial); + GuestInfo* info = sceVorbisDecInfo(vf, 0); + Require(info && info == sceVorbisDecInfo(vf, 5) && info == sceVorbisDecInfo(vf, -1)); + Require(info->channels == FirstChannels && info->rate == FirstRate); + const GuestComment* comment = sceVorbisDecComment(vf, 4); + Require(comment && std::strcmp(comment->user_comments[0], "TITLE=first") == 0); + + Require(DecodeTogether(vf, reference) == DecodedBytes); + Require(info->channels == SecondChannels && info->rate == SecondRate && sceVorbisDecInfo(vf, 0) == info); + Require(std::strcmp(comment->user_comments[0], "TITLE=second") == 0); + Require(sceVorbisDecSerialNumber(vf, 0) == SecondSerial); + RequireSameTells(vf, reference); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(ov_clear(&reference) == 0 && referenceSource.closes == 1); + Require(guest.Untouched()); +} + +void TestFloatAndFilter(const std::vector<unsigned char>& stream) { + GuestFile guest; + void* vf = guest.Get(); + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + for (int round = 0; round < 6; ++round) { + float** pcm = nullptr; + float** hostPcm = nullptr; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecReadFloat(vf, &pcm, 300, &guestStream); + const long expected = ov_read_float(&reference, &hostPcm, 300, &hostStream); + Require(got == expected && got > 0 && got <= 300 && guestStream == hostStream && guestStream == 0); + for (int channel = 0; channel < FirstChannels; ++channel) { + Require(std::memcmp(pcm[channel], hostPcm[channel], static_cast<std::size_t>(got) * sizeof(float)) == 0); + } + } + Require(sceVorbisDecReadFloat(vf, nullptr, 50, nullptr) == ov_read_float(&reference, nullptr, 50, nullptr)); + RequireSameTells(vf, reference); + + FilterLog log; + std::int64_t total = 0; + for (;;) { + std::array<char, 4096> guestBytes{}; + std::array<char, 4096> hostBytes{}; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecReadFilter(vf, guestBytes.data(), static_cast<int>(guestBytes.size()), 0, 2, 1, &guestStream, GuestHalve, &log); + const long expected = ov_read_filter(&reference, hostBytes.data(), static_cast<int>(hostBytes.size()), 0, 2, 1, &hostStream, HostHalve, nullptr); + Require(got == expected && got >= 0); + if (got == 0) break; + Require(guestStream == hostStream && std::memcmp(guestBytes.data(), hostBytes.data(), static_cast<std::size_t>(got)) == 0); + total += got; + } + Require(total > 0 && log.calls > 0 && log.samples > 0 && log.firstChannels == FirstChannels && log.lastChannels == SecondChannels); + + Require(sceVorbisDecPcmSeek(vf, 0) == 0 && ov_pcm_seek(&reference, 0) == 0); + std::array<char, 4096> filtered{}; + std::array<char, 4096> plain{}; + int bitstream = -1; + const int calls = log.calls; + const std::int64_t got = sceVorbisDecReadFilter(vf, filtered.data(), static_cast<int>(filtered.size()), 0, 2, 1, &bitstream, nullptr, &log); + Require(got > 0 && got == ov_read(&reference, plain.data(), static_cast<int>(plain.size()), 0, 2, 1, &bitstream)); + Require(std::memcmp(filtered.data(), plain.data(), static_cast<std::size_t>(got)) == 0 && log.calls == calls); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestHalfrate(const std::vector<unsigned char>& stream) { + GuestFile guest; + void* vf = guest.Get(); + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(sceVorbisDecHalfrateP(vf) == 0); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecHalfrate(vf, 1) == 0 && ov_halfrate(&reference, 1) == 0); + Require(sceVorbisDecHalfrateP(vf) == 1 && ov_halfrate_p(&reference) == 1); + RequireSameTells(vf, reference); + const std::int64_t half = DecodeTogether(vf, reference); + Require(half > 0 && half < DecodedBytes / 2); + RequireSameTells(vf, reference); + Require(sceVorbisDecPcmTell(vf) > FirstSamples); + Require(sceVorbisDecHalfrate(vf, 0) == 0 && sceVorbisDecHalfrateP(vf) == 0); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestCrossLap(const std::vector<unsigned char>& stream) { + GuestFile first; + GuestFile second; + GuestFile closed; + GuestFile otherClosed; + Source firstSource{&stream}; + Source secondSource{&stream}; + Require(sceVorbisDecOpenCallbacks(&secondSource, second.Get(), nullptr, 0, SeekableCallbacks) == 0); + firstSource.probe = second.Get(); + Require(sceVorbisDecOpenCallbacks(&firstSource, first.Get(), nullptr, 0, SeekableCallbacks) == 0); + Require(firstSource.probed == 2); + Source firstReferenceSource{&stream}; + Source secondReferenceSource{&stream}; + OggVorbis_File firstReference{}; + OggVorbis_File secondReference{}; + Require(ov_open_callbacks(&firstReferenceSource, &firstReference, nullptr, 0, HostSeekableCallbacks) == 0); + Require(ov_open_callbacks(&secondReferenceSource, &secondReference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(RequireSameRead(first.Get(), firstReference) > 0 && RequireSameRead(first.Get(), firstReference) > 0); + Require(sceVorbisDecPcmSeek(second.Get(), FirstSamples + 2000) == 0 && ov_pcm_seek(&secondReference, FirstSamples + 2000) == 0); + Require(sceVorbisDecCrossLap(first.Get(), second.Get()) == 0 && ov_crosslap(&firstReference, &secondReference) == 0); + for (int round = 0; round < 3; ++round) { + Require(RequireSameRead(second.Get(), secondReference) > 0); + Require(RequireSameRead(first.Get(), firstReference) > 0); + } + RequireSameTells(first.Get(), firstReference); + RequireSameTells(second.Get(), secondReference); + Require(sceVorbisDecCrossLap(first.Get(), first.Get()) == 0); + Require(sceVorbisDecCrossLap(first.Get(), closed.Get()) == OV_EINVAL && sceVorbisDecCrossLap(closed.Get(), second.Get()) == OV_EINVAL); + Require(sceVorbisDecCrossLap(closed.Get(), otherClosed.Get()) == OV_EINVAL && sceVorbisDecCrossLap(closed.Get(), closed.Get()) == 0); + Require(sceVorbisDecCrossLap(nullptr, nullptr) == 0); + OggVorbis_File zeroed{}; + Require(sceVorbisDecCrossLap(closed.Get(), nullptr) == OV_EINVAL && ov_crosslap(&zeroed, nullptr) == OV_EINVAL); + Require(Throws<std::invalid_argument>([&] { sceVorbisDecCrossLap(first.Get(), nullptr); })); + Require(Throws<std::invalid_argument>([&] { sceVorbisDecCrossLap(nullptr, closed.Get()); })); + Require(sceVorbisDecClear(first.Get()) == 0 && sceVorbisDecClear(second.Get()) == 0); + Require(firstSource.closes == 1 && secondSource.closes == 1); + Require(ov_clear(&firstReference) == 0 && ov_clear(&secondReference) == 0); + Require(first.Untouched() && second.Untouched() && closed.Untouched() && otherClosed.Untouched()); +} + +void TestNeverOpened() { + GuestFile guest; + void* vf = guest.Get(); + Require(sceVorbisDecStreams(vf) == 0 && sceVorbisDecSeekable(vf) == 0); + Require(sceVorbisDecSerialNumber(vf, -1) == 0 && sceVorbisDecSerialNumber(vf, 0) == 0 && sceVorbisDecSerialNumber(vf, 3) == 0); + Require(sceVorbisDecBitrate(vf, -1) == OV_EINVAL && sceVorbisDecBitrate(vf, 0) == OV_EINVAL && sceVorbisDecBitrateInstant(vf) == OV_EINVAL); + Require(sceVorbisDecRawTotal(vf, -1) == OV_EINVAL && sceVorbisDecPcmTotal(vf, 0) == OV_EINVAL && sceVorbisDecTimeTotal(vf, -1) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeek(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekPage(vf, 0) == OV_EINVAL); + Require(sceVorbisDecTimeSeek(vf, 0.0) == OV_EINVAL && sceVorbisDecTimeSeekPage(vf, 0.0) == OV_EINVAL); + Require(sceVorbisDecRawSeekLap(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekLap(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekPageLap(vf, 0) == OV_EINVAL); + Require(sceVorbisDecTimeSeekLap(vf, 0.0) == OV_EINVAL && sceVorbisDecTimeSeekPageLap(vf, 0.0) == OV_EINVAL); + Require(sceVorbisDecRawTell(vf) == OV_EINVAL && sceVorbisDecPcmTell(vf) == OV_EINVAL && sceVorbisDecTimeTell(vf) == OV_EINVAL); + Require(sceVorbisDecInfo(vf, -1) == nullptr && sceVorbisDecInfo(vf, 0) == nullptr); + Require(sceVorbisDecComment(vf, -1) == nullptr && sceVorbisDecComment(vf, 0) == nullptr); + std::array<char, 64> buffer{}; + int bitstream = 7; + Require(sceVorbisDecRead(vf, buffer.data(), static_cast<int>(buffer.size()), 0, 2, 1, &bitstream) == OV_EINVAL && bitstream == 7); + float** pcm = nullptr; + Require(sceVorbisDecReadFloat(vf, &pcm, 64, &bitstream) == OV_EINVAL && pcm == nullptr && bitstream == 7); + FilterLog log; + Require(sceVorbisDecReadFilter(vf, buffer.data(), static_cast<int>(buffer.size()), 0, 2, 1, &bitstream, GuestHalve, &log) == OV_EINVAL); + Require(log.calls == 0 && bitstream == 7); + Require(sceVorbisDecHalfrate(vf, 1) == OV_EINVAL && sceVorbisDecHalfrateP(vf) == OV_EINVAL); + Require(sceVorbisDecClear(vf) == 0 && sceVorbisDecClear(nullptr) == 0); + Require(guest.Untouched()); + + Source source; + Require(Throws<std::invalid_argument>([&] { sceVorbisDecOpenCallbacks(&source, nullptr, nullptr, 0, SeekableCallbacks); })); + Require(Throws<std::invalid_argument>([] { sceVorbisDecStreams(nullptr); })); + Require(Throws<std::invalid_argument>([] { sceVorbisDecInfo(nullptr, 0); })); + Require(Throws<std::invalid_argument>([] { sceVorbisDecPcmSeek(nullptr, 0); })); +} + +void TestFailedOpens(const std::vector<unsigned char>& stream) { + GuestFile guest; + void* vf = guest.Get(); + std::vector<unsigned char> noise(8192); + std::uint32_t state = 0x2545f491; + for (auto& value : noise) { + state = state * 1664525u + 1013904223u; + value = static_cast<unsigned char>(state >> 24); + } + Source noiseSource{&noise}; + Require(sceVorbisDecOpenCallbacks(&noiseSource, vf, nullptr, 0, SeekableCallbacks) == OV_ENOTVORBIS); + Require(noiseSource.closes == 0 && sceVorbisDecStreams(vf) == 0 && sceVorbisDecInfo(vf, 0) == nullptr); + Require(sceVorbisDecClear(vf) == 0 && noiseSource.closes == 0); + + Source untold{&stream}; + constexpr GuestCallbacks NoTell{GuestRead, GuestSeek, GuestClose, nullptr}; + Require(sceVorbisDecOpenCallbacks(&untold, vf, nullptr, 0, NoTell) == OV_EINVAL); + Require(untold.closes == 0 && sceVorbisDecStreams(vf) == 0 && sceVorbisDecClear(vf) == 0 && untold.closes == 0); + + constexpr GuestCallbacks NoRead{nullptr, GuestSeek, GuestClose, GuestTell}; + Source unread{&stream}; + OggVorbis_File reference{}; + const ov_callbacks hostNoRead{nullptr, HostSeek, HostClose, HostTell}; + Require(sceVorbisDecOpenCallbacks(&unread, vf, nullptr, 0, NoRead) == ov_open_callbacks(&unread, &reference, nullptr, 0, hostNoRead)); + Require(unread.closes == 0 && sceVorbisDecStreams(vf) == 0); + + Require(sceVorbisDecOpenCallbacks(nullptr, vf, nullptr, 0, SeekableCallbacks) == OV_ENOTVORBIS); + + Source previous{&stream}; + Source next{&stream}; + Require(sceVorbisDecOpenCallbacks(&previous, vf, nullptr, 0, SeekableCallbacks) == 0); + Require(sceVorbisDecOpenCallbacks(&next, vf, nullptr, 0, StreamingCallbacks) == 0); + Require(previous.closes == 0 && sceVorbisDecSeekable(vf) == 0 && sceVorbisDecStreams(vf) == 1); + Require(sceVorbisDecClear(vf) == 0 && previous.closes == 0 && next.closes == 1); + + Source unsized{&stream}; + Require(sceVorbisDecOpenCallbacks(&unsized, vf, nullptr, std::int64_t{1} << 32, SeekableCallbacks) == 0); + Require(sceVorbisDecStreams(vf) == 2 && sceVorbisDecClear(vf) == 0 && unsized.closes == 1); + + const auto* initial = reinterpret_cast<const char*>(stream.data()); + const auto size = static_cast<std::int64_t>(stream.size()); + Require(sceVorbisDecOpenCallbacks(nullptr, vf, initial, size, SeekableCallbacks) == 0); + Require(ov_open_callbacks(nullptr, &reference, initial, static_cast<long>(size), HostSeekableCallbacks) == 0); + Require(sceVorbisDecSeekable(vf) == 0 && DecodeTogether(vf, reference) == DecodedBytes); + Require(sceVorbisDecClear(vf) == 0 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestThrowingCallbacks(const std::vector<unsigned char>& stream) { + GuestFile guest; + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, guest.Get(), nullptr, 0, SeekableCallbacks) == 0); + std::array<char, 4096> buffer{}; + int bitstream = -1; + source.throwOnRead = true; + bool threw = false; + for (int call = 0; call < 1024 && !threw; ++call) { + threw = Throws<std::runtime_error>([&] { sceVorbisDecRead(guest.Get(), buffer.data(), static_cast<int>(buffer.size()), 0, 2, 1, &bitstream); }); + } + Require(threw); + source.throwOnRead = false; + source.throwOnClose = true; + Require(Throws<std::runtime_error>([&] { sceVorbisDecClear(guest.Get()); })); + Require(source.closes == 1 && sceVorbisDecStreams(guest.Get()) == 0); + Require(sceVorbisDecClear(guest.Get()) == 0 && source.closes == 1); + Require(guest.Untouched()); +} + } int main() { - Require(Throws([] { sceVorbisDecOpenCallbacks(); }), "sceVorbisDecOpenCallbacks must throw"); - Require(Throws([] { sceVorbisDecClear(); }), "sceVorbisDecClear must throw"); - Require(Throws([] { sceVorbisDecBitrate(); }), "sceVorbisDecBitrate must throw"); - Require(Throws([] { sceVorbisDecBitrateInstant(); }), "sceVorbisDecBitrateInstant must throw"); - Require(Throws([] { sceVorbisDecStreams(); }), "sceVorbisDecStreams must throw"); - Require(Throws([] { sceVorbisDecSeekable(); }), "sceVorbisDecSeekable must throw"); - Require(Throws([] { sceVorbisDecSerialNumber(); }), "sceVorbisDecSerialNumber must throw"); - Require(Throws([] { sceVorbisDecRawTotal(); }), "sceVorbisDecRawTotal must throw"); - Require(Throws([] { sceVorbisDecPcmTotal(); }), "sceVorbisDecPcmTotal must throw"); - Require(Throws([] { sceVorbisDecTimeTotal(); }), "sceVorbisDecTimeTotal must throw"); - Require(Throws([] { sceVorbisDecRawSeek(); }), "sceVorbisDecRawSeek must throw"); - Require(Throws([] { sceVorbisDecPcmSeek(); }), "sceVorbisDecPcmSeek must throw"); - Require(Throws([] { sceVorbisDecPcmSeekPage(); }), "sceVorbisDecPcmSeekPage must throw"); - Require(Throws([] { sceVorbisDecTimeSeek(); }), "sceVorbisDecTimeSeek must throw"); - Require(Throws([] { sceVorbisDecTimeSeekPage(); }), "sceVorbisDecTimeSeekPage must throw"); - Require(Throws([] { sceVorbisDecRawSeekLap(); }), "sceVorbisDecRawSeekLap must throw"); - Require(Throws([] { sceVorbisDecPcmSeekLap(); }), "sceVorbisDecPcmSeekLap must throw"); - Require(Throws([] { sceVorbisDecPcmSeekPageLap(); }), "sceVorbisDecPcmSeekPageLap must throw"); - Require(Throws([] { sceVorbisDecTimeSeekLap(); }), "sceVorbisDecTimeSeekLap must throw"); - Require(Throws([] { sceVorbisDecTimeSeekPageLap(); }), "sceVorbisDecTimeSeekPageLap must throw"); - Require(Throws([] { sceVorbisDecRawTell(); }), "sceVorbisDecRawTell must throw"); - Require(Throws([] { sceVorbisDecPcmTell(); }), "sceVorbisDecPcmTell must throw"); - Require(Throws([] { sceVorbisDecTimeTell(); }), "sceVorbisDecTimeTell must throw"); - Require(Throws([] { sceVorbisDecInfo(); }), "sceVorbisDecInfo must throw"); - Require(Throws([] { sceVorbisDecComment(); }), "sceVorbisDecComment must throw"); - Require(Throws([] { sceVorbisDecRead(); }), "sceVorbisDecRead must throw"); - Require(Throws([] { sceVorbisDecReadFloat(); }), "sceVorbisDecReadFloat must throw"); - Require(Throws([] { sceVorbisDecReadFilter(); }), "sceVorbisDecReadFilter must throw"); - Require(Throws([] { sceVorbisDecCrossLap(); }), "sceVorbisDecCrossLap must throw"); - Require(Throws([] { sceVorbisDecHalfrate(); }), "sceVorbisDecHalfrate must throw"); - Require(Throws([] { sceVorbisDecHalfrateP(); }), "sceVorbisDecHalfrateP must throw"); - Require(Throws([] { sceVorbisDecGetDecodePriority(); }), "sceVorbisDecGetDecodePriority must throw"); - Require(Throws([] { sceVorbisDecSetDecodePriority(); }), "sceVorbisDecSetDecodePriority must throw"); - Require(Throws([] { sceVorbisDecGetLastDecodeError(); }), "sceVorbisDecGetLastDecodeError must throw"); + std::vector<unsigned char> stream; + EncodeLink(stream, FirstSerial, FirstChannels, FirstRate, FirstSamples, "first"); + EncodeLink(stream, SecondSerial, SecondChannels, SecondRate, SecondSamples, "second"); + + TestSeekable(stream); + TestStreaming(stream); + TestFloatAndFilter(stream); + TestHalfrate(stream); + TestCrossLap(stream); + TestNeverOpened(); + TestFailedOpens(stream); + TestThrowingCallbacks(stream); + + Require(Throws<std::runtime_error>([] { sceVorbisDecGetDecodePriority(); })); + Require(Throws<std::runtime_error>([] { sceVorbisDecSetDecodePriority(); })); + Require(Throws<std::runtime_error>([] { sceVorbisDecGetLastDecodeError(); })); std::puts("All VorbisDec guest tests passed"); return 0; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 1b1a1287ac..7778788a16 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -125,7 +125,7 @@ Build configuration parameters: SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridden with `-D`. -With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator` and `LibAtrac9` always stay bundled and still need `git submodule update --init`. +With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator`, `LibAtrac9`, `ogg` and `vorbis` always stay bundled and still need `git submodule update --init`. ## Quick syntax checks diff --git a/docs/dev/debt/libSceVorbisDec.md b/docs/dev/debt/libSceVorbisDec.md index f0c2ddc877..45ad9e2d0e 100644 --- a/docs/dev/debt/libSceVorbisDec.md +++ b/docs/dev/debt/libSceVorbisDec.md @@ -2,37 +2,13 @@ ### Unknown function info -- [sceVorbisDecBitrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecBitrateInstant](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecClear](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecComment](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecCrossLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database +- [sceVorbisDecOpenCallbacks, sceVorbisDecRead and the other libvorbisfile exports](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - the 31 exports named after libvorbisfile functions are taken to be the libvorbisfile API with a `sceVorbisDec` prefix (`Read` is `ov_read`, `CrossLap` is `ov_crosslap`, `HalfrateP` is `ov_halfrate_p`). `ReadFilter`, `ReadFloat` and `Halfrate` exist in libvorbis (`ov_read_filter` since 1.2.2, with the same `vorbisfile.h` signatures through 1.3.7 apart from `const`) but not in Tremor, so times are `double` and `ov_read` takes `bigendianp`, `word` and `sgned`. Guest `long` arguments and results are 64-bit. They run the bundled libvorbis 1.3.7 and libogg 1.3.6; the console's libvorbis version, any Sony changes to its behaviour or error codes, and the signatures themselves are unverified on a PS5. - [sceVorbisDecGetDecodePriority](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database - [sceVorbisDecGetLastDecodeError](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecHalfrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecHalfrateP](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecInfo](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecOpenCallbacks](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeek](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeekLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeekPage](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeekPageLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmTell](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmTotal](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawSeek](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawSeekLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawTell](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawTotal](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRead](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecReadFilter](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecReadFloat](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecSeekable](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecSerialNumber](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database - [sceVorbisDecSetDecodePriority](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecStreams](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeek](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeekLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeekPage](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeekPageLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeTell](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeTotal](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database + +### Functional + +- [libSceVorbisDec](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - the guest `OggVorbis_File` is never written: the decoder state lives on the host, keyed by the struct's address, so fields a title reads directly keep what it stored, and a struct never opened, cleared or failed to open behaves as libvorbisfile's zeroed struct. `sceVorbisDecInfo` returns a copy of each link's `vorbis_info` with 64-bit `long` fields, refreshed after every call on the struct and valid until `sceVorbisDecClear`. Reopening a struct that is still open frees its old state without calling its close callback; libvorbisfile leaks it. +- [sceVorbisDecClear and sceVorbisDecCrossLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - a null `vf` throws, as libvorbisfile dereferences it, except that `sceVorbisDecClear` and `sceVorbisDecCrossLap` with the same pointer twice return 0, and `sceVorbisDecCrossLap` with a null second pointer after a first that is not open returns `OV_EINVAL`, as libvorbisfile does. `sceVorbisDecClear` frees the state before it calls the close callback, as `ov_clear` does, so when that callback throws the struct is already cleared, where libvorbisfile leaves it freed but not zeroed. +- [sceVorbisDecOpenCallbacks and sceVorbisDecBitrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - on Windows the host `long` is 32-bit: a tell result outside it, or an initial byte count outside it with a non-null `initial`, throws; a sample rate of 2^31 Hz or more is rejected as a bad header; and `sceVorbisDecBitrate` rounds halves up instead of to even. diff --git a/tools/package_release.py b/tools/package_release.py index 88558908a6..622e5bb056 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -52,6 +52,9 @@ def package(platform, build, output, version): licenses = [] if "libSceAjm.native.prx" in expected: licenses.append((ROOT / "3rdparty/vgmstream/COPYING", "libs/licenses/vgmstream.txt")) + if "libSceVorbisDec.prx" in expected: + licenses.append((ROOT / "3rdparty/ogg/COPYING", "libs/licenses/ogg.txt")) + licenses.append((ROOT / "3rdparty/vorbis/COPYING", "libs/licenses/vorbis.txt")) if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 4abcb1f37b..d5d1866ccb 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -61,6 +61,21 @@ class ReleasePackageTests(unittest.TestCase): with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: self.assertEqual(archive.extractfile("libs/licenses/vgmstream.txt").read(), b"decoder license") + def test_vorbis_decoder_licenses_in_archives(self): + (self.source / "core/libs/prx/libSceVorbisDec").mkdir() + (self.build / "core/libs/libs/libSceVorbisDec.prx").write_bytes(b"vorbis") + for name in ("ogg", "vorbis"): + license_file = self.source / f"3rdparty/{name}/COPYING" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(f"{name} license".encode()) + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/licenses/ogg.txt"), b"ogg license") + self.assertEqual(archive.read("libs/licenses/vorbis.txt"), b"vorbis license") + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/licenses/ogg.txt").read(), b"ogg license") + self.assertEqual(archive.extractfile("libs/licenses/vorbis.txt").read(), b"vorbis license") + def test_windows_runtime_follows_the_configured_compiler_directory(self): runtime = self.root / "custom toolchain" / "bin" runtime.mkdir(parents=True) From 5ac366931231e3ad8632f1ba33ac883cb68d8a33 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:18:56 +0300 Subject: [PATCH 0929/1360] feat(libSceNpCommon): compare, order and validate online IDs and NpIds --- core/libs/prx/libSceNpCommon/Export.cpp | 108 ++++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 20 ---- docs/dev/debt/libSceNpCommon.md | 6 +- 3 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceNpCommon/Export.cpp diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp new file mode 100644 index 0000000000..97a06c816c --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -0,0 +1,108 @@ +#include <cctype> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550003); +constexpr int NP_ERROR_INVALID_PLATFORM_TYPE = static_cast<int>(0x80550004); +constexpr int NP_UTIL_ERROR_NOT_MATCH = static_cast<int>(0x80550609); +constexpr int NP_LW_COND_ERROR_TIMEDOUT = static_cast<int>(0x8055800B); +constexpr int NP_LW_MUTEX_ERROR_BUSY = static_cast<int>(0x8055800F); +constexpr int KERNEL_ERROR_EBUSY = static_cast<int>(0x80020010); +constexpr int KERNEL_ERROR_ETIMEDOUT = static_cast<int>(0x8002003C); +constexpr std::size_t ONLINE_ID_MAX_LENGTH = 16; +constexpr int MUTEX_TYPE_RECURSIVE = 2; +constexpr KernelClockid CLOCK_MONOTONIC_ID = 4; + +struct NpIdFull { + NpOnlineId handle; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; +static_assert(sizeof(NpIdFull) == 36); + +struct SystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; +static_assert(sizeof(SystemSwVersion) == 0x28); + +int compareBytes(const std::uint8_t* lhs, const std::uint8_t* rhs, std::size_t count) { + for (std::size_t i = 0; i < count; ++i) { + if (lhs[i] != rhs[i]) return lhs[i] < rhs[i] ? -1 : 1; + } + return 0; +} + +} + +extern "C" int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +extern "C" int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +extern "C" int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +extern "C" int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +extern "C" int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +extern "C" int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +extern "C" int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +extern "C" int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +extern "C" int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +extern "C" int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); +extern "C" int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SystemSwVersion* version); + +extern "C" { + +int APS5_VABI sceNpCmpOnlineId(const NpOnlineId* onlineId1, const NpOnlineId* onlineId2) { + if (!onlineId1 || !onlineId2) return NP_ERROR_INVALID_ARGUMENT; + if (std::strncmp(onlineId1->data, onlineId2->data, ONLINE_ID_MAX_LENGTH) != 0) return NP_UTIL_ERROR_NOT_MATCH; + return 0; +} + +int APS5_VABI sceNpCmpNpIdInOrder(const NpIdFull* npId1, const NpIdFull* npId2, std::int32_t* order) { + if (!npId1 || !npId2 || !order) return NP_ERROR_INVALID_ARGUMENT; + int compare = std::strncmp(npId1->handle.data, npId2->handle.data, ONLINE_ID_MAX_LENGTH); + if (compare == 0) compare = compareBytes(npId1->opt, npId2->opt, sizeof(npId1->opt)); + if (compare == 0) compare = compareBytes(npId1->reserved, npId2->reserved, sizeof(npId1->reserved)); + *order = compare < 0 ? -1 : compare > 0 ? 1 : 0; + return 0; +} + +int APS5_VABI sceNpGetPlatformType(const NpIdFull* npId) { + constexpr std::uint32_t TAG_PS3 = 0x00337370; + constexpr std::uint32_t TAG_VITA = 0x32707370; + constexpr std::uint32_t TAG_PS4 = 0x00347370; + if (!npId) return NP_ERROR_INVALID_ARGUMENT; + if (npId->opt[4] == 0) return 0; + std::uint32_t tag = 0; + std::memcpy(&tag, npId->opt + 4, sizeof(tag)); + switch (tag) { + case TAG_PS3: return 1; + case TAG_VITA: return 2; + case TAG_PS4: return 3; + default: return NP_ERROR_INVALID_PLATFORM_TYPE; + } +} + +int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId) { + if (!onlineId || onlineId->data[ONLINE_ID_MAX_LENGTH] != 0) return 0; + const std::size_t length = strnlen(onlineId->data, ONLINE_ID_MAX_LENGTH + 1); + if (length < 3 || length > ONLINE_ID_MAX_LENGTH) return 0; + if (!std::isalnum(static_cast<unsigned char>(onlineId->data[0]))) return 0; + for (std::size_t i = 1; i < length; ++i) { + const unsigned char character = static_cast<unsigned char>(onlineId->data[i]); + if (!std::isalnum(character) && character != '_' && character != '-') return 0; + } + return 1; +} + +} diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index d2daed1a92..2a82f453d4 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -120,16 +120,6 @@ int APS5_VABI sceNpCmpNpId(void) { return 0; } -int APS5_VABI sceNpCmpNpIdInOrder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCmpOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondDestroy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -215,11 +205,6 @@ int APS5_VABI sceNpGetNavSdkVersion(void) { return 0; } -int APS5_VABI sceNpGetPlatformType(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetProcessId(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -315,11 +300,6 @@ int APS5_VABI sceNpIntIsOnlineIdString(void) { return 0; } -int APS5_VABI sceNpIntIsValidOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 2a8900021d..521ae4d85e 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. - [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -26,8 +26,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCmpNpIdInOrder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TcwEFnakiSc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCmpOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dj+O5aD2a0Q`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -45,7 +43,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sXVQUIGmk2U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -65,7 +62,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntIsValidOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hkeX9iuCwlI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From d4e046e0c94464f5dc264f451e0c6d11afa949ab Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:18:56 +0300 Subject: [PATCH 0930/1360] feat(libSceNpCommon): wrap the libkernel mutex calls --- core/libs/prx/libSceNpCommon/Export.cpp | 47 +++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 50 ------------------- docs/dev/debt/libSceNpCommon.md | 10 ---- 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index 97a06c816c..5eadd6d1a6 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -105,4 +105,51 @@ int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId) { return 1; } +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags) { + PthreadMutexattr attr = nullptr; + int result = scePthreadMutexattrInit(&attr); + if (result != 0) return result; + if (flags & 1) result = scePthreadMutexattrSettype(&attr, MUTEX_TYPE_RECURSIVE); + if (result == 0) result = scePthreadMutexInit(mutex, &attr, name); + scePthreadMutexattrDestroy(&attr); + return result; +} + +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex) { + return scePthreadMutexDestroy(mutex); +} + +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex) { + return scePthreadMutexLock(mutex); +} + +int APS5_VABI sceNpMutexTryLock(PthreadMutex* mutex) { + const int result = scePthreadMutexTrylock(mutex); + return result == KERNEL_ERROR_EBUSY ? NP_LW_MUTEX_ERROR_BUSY : result; +} + +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex) { + return scePthreadMutexUnlock(mutex); +} + +int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags) { + return sceNpMutexInit(mutex, name, flags); +} + +int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex) { + return sceNpMutexDestroy(mutex); +} + +int APS5_VABI sceNpLwMutexLock(PthreadMutex* mutex) { + return sceNpMutexLock(mutex); +} + +int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex) { + return sceNpMutexTryLock(mutex); +} + +int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex) { + return sceNpMutexUnlock(mutex); +} + } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 2a82f453d4..7b95f33614 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -385,31 +385,6 @@ int APS5_VABI sceNpLwCondWait(void) { return 0; } -int APS5_VABI sceNpLwMutexDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexTryLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexUnlock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpMemoryHeapDestroy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -430,31 +405,6 @@ int APS5_VABI sceNpMemoryHeapInit(void) { return 0; } -int APS5_VABI sceNpMutexDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexTryLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexUnlock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpOpenEventFlag(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 521ae4d85e..7905ee9572 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -79,20 +79,10 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4zxevggtYrQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1CiXI-MyEKs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `18j+qk6dRwk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hp0kVgu5Fxw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CQG2oyx1-nM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lQ11BpMM4LU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uEwag-0YZPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `r9Bet+s6fKc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DuslmoqQ+nk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `oZyb9ktuCpA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From 6aa26e212be4e44452bfa8631a9c211da3993bcb Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:18:57 +0300 Subject: [PATCH 0931/1360] feat(libSceNpCommon): wrap the condition, join, clock and version calls --- core/libs/prx/libSceNpCommon/Export.cpp | 42 +++++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 35 ---------------- docs/dev/debt/libSceNpCommon.md | 7 ---- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index 5eadd6d1a6..f10b45437e 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -152,4 +152,46 @@ int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex) { return sceNpMutexUnlock(mutex); } +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t) { + PthreadCondattr attr = nullptr; + int result = scePthreadCondattrInit(&attr); + if (result != 0) return result; + result = scePthreadCondInit(cond, &attr, name); + scePthreadCondattrDestroy(&attr); + return result; +} + +int APS5_VABI sceNpCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); +} + +int APS5_VABI sceNpCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); +} + +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec) { + if (usec == 0) return scePthreadCondWait(cond, mutex); + const int result = scePthreadCondTimedwait(cond, mutex, usec); + return result == KERNEL_ERROR_ETIMEDOUT ? NP_LW_COND_ERROR_TIMEDOUT : result; +} + +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval) { + return scePthreadJoin(thread, retval); +} + +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec) { + KernelTimespec now{}; + const int result = sceKernelClockGettime(CLOCK_MONOTONIC_ID, &now); + if (result == 0) *usec = now.tv_sec * 1000000 + now.tv_nsec / 1000; + return result; +} + +void APS5_VABI sceNpGetSdkVersion(char* version) { + SystemSwVersion swVersion{}; + swVersion.size = sizeof(swVersion); + sceKernelGetSystemSwVersion_nid_postfix(&swVersion); + const std::uint32_t hex = swVersion.version; + std::snprintf(version, 8, "%u.%02u", ((hex >> 24) & 0xF) + (hex >> 28) * 10, ((hex >> 16) & 0xF) + ((hex >> 20) & 0xF) * 10); +} + } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 7b95f33614..9d6ef0f8b0 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -120,21 +120,6 @@ int APS5_VABI sceNpCmpNpId(void) { return 0; } -int APS5_VABI sceNpCondDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondSignal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondSignalAll(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -145,11 +130,6 @@ int APS5_VABI sceNpCondSignalTo(void) { return 0; } -int APS5_VABI sceNpCondTimedwait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondWait(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -215,21 +195,11 @@ int APS5_VABI sceNpGetRandom(void) { return 0; } -int APS5_VABI sceNpGetSdkVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetSdkVersionUInt(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpGetSystemClockUsec(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGlobalHeapGetAllocator(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -320,11 +290,6 @@ int APS5_VABI sceNpIpc2ClientTerm(void) { return 0; } -int APS5_VABI sceNpJoinThread(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpJsonParse(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 7905ee9572..0429e246b8 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -26,12 +26,8 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondTimedwait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ss2xO9IJxKQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -45,9 +41,7 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSystemClockUsec](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PVVsRmMkO1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -66,7 +60,6 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpJoinThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EjMsfO3GCIA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From 8753303f4f376cfb13fbbe0d39ed6c019a8d8ecc Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:18:57 +0300 Subject: [PATCH 0932/1360] test(libSceNpCommon): cover the ID utilities, sync wrappers and clock --- core/libs/CMakeLists.txt | 13 +++ core/libs/tests/GuestNpCommon.cpp | 172 ++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 core/libs/tests/GuestNpCommon.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d2a614da5e..5911bcb63f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -392,6 +392,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpParty>") endif() + add_test_executable(guest_np_common_tests tests/GuestNpCommon.cpp) + target_include_directories(guest_np_common_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_common_tests PRIVATE libSceNpCommon libkernel libc) + configure_windows_unwind(guest_np_common_tests) + foreach(testCase IN ITEMS ids sync clock) + add_test(NAME guest_np_common_${testCase} COMMAND guest_np_common_tests ${testCase}) + set_tests_properties(guest_np_common_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_common_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCommon>") + endif() + endforeach() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc) diff --git a/core/libs/tests/GuestNpCommon.cpp b/core/libs/tests/GuestNpCommon.cpp new file mode 100644 index 0000000000..d605767148 --- /dev/null +++ b/core/libs/tests/GuestNpCommon.cpp @@ -0,0 +1,172 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include <cstdint> +#include <cstdio> +#include <cstring> + +struct NpIdFull { + NpOnlineId handle; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; + +extern "C" { +int APS5_VABI sceNpCmpOnlineId(const NpOnlineId* onlineId1, const NpOnlineId* onlineId2); +int APS5_VABI sceNpCmpNpIdInOrder(const NpIdFull* npId1, const NpIdFull* npId2, std::int32_t* order); +int APS5_VABI sceNpGetPlatformType(const NpIdFull* npId); +int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId); +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexTryLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t flags); +int APS5_VABI sceNpCondDestroy(PthreadCond* cond); +int APS5_VABI sceNpCondSignal(PthreadCond* cond); +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec); +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); +void APS5_VABI sceNpGetSdkVersion(char* version); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +} + +static constexpr int INVALID_ARGUMENT = static_cast<int>(0x80550003); +static constexpr int INVALID_PLATFORM_TYPE = static_cast<int>(0x80550004); +static constexpr int NOT_MATCH = static_cast<int>(0x80550609); +static constexpr int COND_TIMEDOUT = static_cast<int>(0x8055800B); +static constexpr int MUTEX_BUSY = static_cast<int>(0x8055800F); + +static bool expect(bool condition, const char* message) { + if (!condition) std::fprintf(stderr, "%s\n", message); + return condition; +} + +static NpOnlineId onlineId(const char* text) { + NpOnlineId id{}; + std::strncpy(id.data, text, sizeof(id.data)); + return id; +} + +static NpIdFull npIdWithTag(const char* handle, std::uint32_t tag) { + NpIdFull id{}; + id.handle = onlineId(handle); + std::memcpy(id.opt + 4, &tag, sizeof(tag)); + return id; +} + +static int testIds() { + const NpOnlineId kestrel = onlineId("kestrel_77"); + const NpOnlineId kestrelCopy = onlineId("kestrel_77"); + const NpOnlineId heron = onlineId("heron-4"); + bool ok = expect(sceNpCmpOnlineId(nullptr, &kestrel) == INVALID_ARGUMENT, "a null online ID was compared"); + ok &= expect(sceNpCmpOnlineId(&kestrel, &kestrelCopy) == 0, "equal online IDs did not match"); + ok &= expect(sceNpCmpOnlineId(&kestrel, &heron) == NOT_MATCH, "different online IDs matched"); + + NpIdFull low = npIdWithTag("kestrel_77", 0x00347370); + NpIdFull high = low; + high.reserved[7] = 1; + std::int32_t order = 5; + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, nullptr) == INVALID_ARGUMENT, "a null order was accepted"); + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, &order) == 0 && order == -1, "reserved bytes did not order the NpIds"); + ok &= expect(sceNpCmpNpIdInOrder(&high, &low, &order) == 0 && order == 1, "the reverse order is not 1"); + ok &= expect(sceNpCmpNpIdInOrder(&low, &low, &order) == 0 && order == 0, "an NpId is not equal to itself"); + high = low; + high.opt[0] = 9; + high.reserved[0] = 0; + low.reserved[0] = 200; + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, &order) == 0 && order == -1, "opt did not take precedence over reserved"); + const NpIdFull heronId = npIdWithTag("heron-4", 0); + ok &= expect(sceNpCmpNpIdInOrder(&heronId, &low, &order) == 0 && order == -1, "the handle did not take precedence"); + + const NpIdFull none = npIdWithTag("heron-4", 0); + const NpIdFull ps3 = npIdWithTag("heron-4", 0x00337370); + const NpIdFull vita = npIdWithTag("heron-4", 0x32707370); + const NpIdFull ps4 = npIdWithTag("heron-4", 0x00347370); + const NpIdFull other = npIdWithTag("heron-4", 0x00357370); + ok &= expect(sceNpGetPlatformType(nullptr) == INVALID_ARGUMENT, "a null NpId has a platform"); + ok &= expect(sceNpGetPlatformType(&none) == 0, "an untagged NpId is not platform 0"); + ok &= expect(sceNpGetPlatformType(&ps3) == 1, "the ps3 tag is not platform 1"); + ok &= expect(sceNpGetPlatformType(&vita) == 2, "the psp2 tag is not platform 2"); + ok &= expect(sceNpGetPlatformType(&ps4) == 3, "the ps4 tag is not platform 3"); + ok &= expect(sceNpGetPlatformType(&other) == INVALID_PLATFORM_TYPE, "an unknown tag was accepted"); + + NpOnlineId unterminated = onlineId("abcdefghijklmnop"); + unterminated.data[16] = 'q'; + ok &= expect(sceNpIntIsValidOnlineId(nullptr) == 0, "a null online ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&kestrel) == 1, "kestrel_77 is not valid"); + ok &= expect(sceNpIntIsValidOnlineId(&heron) == 1, "heron-4 is not valid"); + const NpOnlineId sixteen = onlineId("abcdefghijklmnop"); + const NpOnlineId shortId = onlineId("ab"); + const NpOnlineId leadingDash = onlineId("-heron"); + const NpOnlineId dotted = onlineId("her.on"); + ok &= expect(sceNpIntIsValidOnlineId(&sixteen) == 1, "a 16 character ID is not valid"); + ok &= expect(sceNpIntIsValidOnlineId(&unterminated) == 0, "an unterminated ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&shortId) == 0, "a 2 character ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&leadingDash) == 0, "an ID starting with '-' is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&dotted) == 0, "an ID with '.' is valid"); + return ok ? 0 : 1; +} + +static void* APS5_VABI returnTicket(void* arg) { + return reinterpret_cast<void*>(reinterpret_cast<std::uintptr_t>(arg) + 1); +} + +static int testSync() { + PthreadMutex plain = nullptr; + PthreadMutex recursive = nullptr; + bool ok = expect(sceNpMutexInit(&plain, "np-plain", 0) == 0 && plain, "a plain mutex was not created"); + ok &= expect(sceNpLwMutexInit(&recursive, "np-recursive", 1) == 0 && recursive, "a recursive mutex was not created"); + if (!ok) return 1; + ok &= expect(sceNpMutexLock(&plain) == 0, "the plain mutex did not lock"); + ok &= expect(sceNpMutexTryLock(&plain) == MUTEX_BUSY, "a held plain mutex did not report BUSY"); + ok &= expect(sceNpMutexUnlock(&plain) == 0, "the plain mutex did not unlock"); + ok &= expect(sceNpLwMutexTryLock(&recursive) == 0 && sceNpLwMutexTryLock(&recursive) == 0, "flag 1 did not make the mutex recursive"); + ok &= expect(sceNpLwMutexUnlock(&recursive) == 0 && sceNpLwMutexUnlock(&recursive) == 0, "the recursive mutex did not unlock twice"); + ok &= expect(sceNpLwMutexDestroy(&recursive) == 0, "the recursive mutex was not destroyed"); + + PthreadCond cond = nullptr; + ok &= expect(sceNpCondInit(&cond, "np-cond", 0) == 0 && cond, "a condition was not created"); + ok &= expect(sceNpMutexLock(&plain) == 0, "the plain mutex did not lock for the wait"); + ok &= expect(sceNpCondTimedwait(&cond, &plain, 2000) == COND_TIMEDOUT, "a timed wait did not report TIMEDOUT"); + ok &= expect(sceNpMutexUnlock(&plain) == 0, "the mutex was not held after the wait"); + ok &= expect(sceNpCondSignal(&cond) == 0, "signalling without waiters failed"); + ok &= expect(sceNpCondDestroy(&cond) == 0, "the condition was not destroyed"); + ok &= expect(sceNpMutexDestroy(&plain) == 0, "the plain mutex was not destroyed"); + + Pthread thread = nullptr; + void* retval = nullptr; + ok &= expect(scePthreadCreate(&thread, nullptr, returnTicket, reinterpret_cast<void*>(0x4140), "np-join") == 0, "the thread was not created"); + ok &= expect(sceNpJoinThread(thread, &retval) == 0 && retval == reinterpret_cast<void*>(0x4141), "the join did not return the thread result"); + return ok ? 0 : 1; +} + +static int testClock() { + KernelTimespec before{}; + KernelTimespec after{}; + std::int64_t usec = -1; + sceKernelClockGettime(4, &before); + bool ok = expect(sceNpGetSystemClockUsec(&usec) == 0, "the clock was not read"); + sceKernelClockGettime(4, &after); + ok &= expect(usec >= before.tv_sec * 1000000 + before.tv_nsec / 1000 && usec <= after.tv_sec * 1000000 + after.tv_nsec / 1000, "the clock is not the monotonic kernel clock in microseconds"); + char version[12]; + std::memset(version, 'x', sizeof(version)); + sceNpGetSdkVersion(version); + ok &= expect(std::strcmp(version, "1.00") == 0, "the version is not 1.00"); + ok &= expect(version[8] == 'x', "the version was written past 8 bytes"); + return ok ? 0 : 1; +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + int status = 2; + if (std::strcmp(argv[1], "ids") == 0) status = testIds(); + else if (std::strcmp(argv[1], "sync") == 0) status = testSync(); + else if (std::strcmp(argv[1], "clock") == 0) status = testClock(); + if (status == 0) std::printf("NpCommon %s tests passed\n", argv[1]); + return status; +} From 4aba717002f0b19dc81c653dc72da77f5a6f5f2d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:19:04 +0300 Subject: [PATCH 0933/1360] docs(debt): note where the NpCommon utilities and wrappers come from --- docs/dev/debt/libSceNpCommon.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 0429e246b8..e6e51903bb 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. - [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -133,3 +133,9 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. +- [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. +- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. From 15794c836f302d7676dac759caba17b5c647c9a3 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 14:57:16 +0300 Subject: [PATCH 0934/1360] feat(libSceVoice): initialize through sceVoiceInitHQ --- core/libs/prx/libSceVoice/Export.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 707d112ea4..cbbf0a5edf 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -411,9 +411,8 @@ int APS5_VABI sceVoiceDisableChat(void) { Fail(__func__, "audio chat backend is not implemented"); } -int APS5_VABI sceVoiceInitHQ(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceInitHQ(VoiceInitParam* param, int32_t version) { + return sceVoiceInit(param, version); } int APS5_VABI sceVoiceGetMemorySize(void) { From 882d2fb12eb95a20bde93d2705b15906d27590e2 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 14:57:16 +0300 Subject: [PATCH 0935/1360] feat(libSceVoice): accept voice activity detection versions 0 and 1 --- core/libs/prx/libSceVoice/Export.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index cbbf0a5edf..5d71a277f1 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -411,7 +411,7 @@ int APS5_VABI sceVoiceDisableChat(void) { Fail(__func__, "audio chat backend is not implemented"); } -int APS5_VABI sceVoiceInitHQ(VoiceInitParam* param, int32_t version) { +int APS5_VABI sceVoiceInitHQ(const VoiceInitParam* param, std::int32_t version) { return sceVoiceInit(param, version); } @@ -440,9 +440,8 @@ int APS5_VABI sceVoiceVADPVTAdjustment(void) { return 0; } -int APS5_VABI sceVoiceVADSetVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceVADSetVersion(std::int32_t version) { + return version == 0 || version == 1 ? 0 : InvalidArgument; } int APS5_VABI sceVoiceSetMuteFlagAll(void) { From 2d69f763abde4591b0f87fdea3bdc6d6448f032d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 14:57:16 +0300 Subject: [PATCH 0936/1360] test(libSceVoice): cover sceVoiceInitHQ and the VAD versions --- core/libs/tests/GuestVoice.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index cb12940acb..f2223a22ad 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -34,6 +34,8 @@ int APS5_VABI sceVoiceResumePortAll(void); int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); int APS5_VABI sceVoiceEnableChat(void); int APS5_VABI sceVoiceDisableChat(void); +int APS5_VABI sceVoiceInitHQ(const VoiceInitParam*, std::int32_t); +int APS5_VABI sceVoiceVADSetVersion(std::int32_t); } namespace { @@ -406,4 +408,17 @@ int main() { } Require(sceVoiceEnd_nid_postfix() == 0); + Require(sceVoiceVADSetVersion(0) == 0 && sceVoiceVADSetVersion(1) == 0); + Require(sceVoiceVADSetVersion(2) == invalidArgument && sceVoiceVADSetVersion(-1) == invalidArgument); + init.app_type = 0x10000000; + start.mem_size = 0x40000; + Require(sceVoiceInitHQ(nullptr, 100) == invalidArgument && sceVoiceInitHQ(&init, 0) == invalidArgument); + Require(sceVoiceInitHQ(&init, 100) == 0); + Require(sceVoiceInitHQ(&init, 100) == AlreadyInitialized); + Require(sceVoiceInit(&init, 100) == AlreadyInitialized); + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceEnd_nid_postfix() == 0); + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceInitHQ(&init, 100) == AlreadyInitialized); + Require(sceVoiceEnd_nid_postfix() == 0); } From a596bc3166129ab7ba84d8ad4f85602d8df9e944 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 14:57:16 +0300 Subject: [PATCH 0937/1360] docs(debt): note where sceVoiceInitHQ and sceVoiceVADSetVersion come from --- docs/dev/debt/libSceVoice.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 9e8d8fc46e..647208feaa 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -8,14 +8,13 @@ ### Unknown function info - [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 +- [sceVoiceInitHQ, sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the signatures and rules follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/voice/voice.cpp). `sceVoiceInitHQ` runs `sceVoiceInit`, as shadPS4's two share one path. `sceVoiceVADSetVersion` accepts versions 0 and 1 and returns `0x804E0805` for any other; nothing uses the version, as no voice activity detection runs - [sceVoicePausePortAll, sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: both return 0x804E0801 while the library is not initialized and 0 otherwise, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/voice/voice.cpp#L356-L437); prosper reads the same code for `sceVoiceStart`, and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L462-L543) make the same check. Like `sceVoicePausePort` they change nothing that is reported: [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L540-L557) documents that pausing every port keeps the buffered data and the reported state, and nothing here processes port data -- [sceVoiceInitHQ](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature From a946073ce124243323b4ffa08b9bd82fe0ddde4b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 15:02:07 +0300 Subject: [PATCH 0938/1360] feat(libSceSystemService): report no background music in sceSystemServiceIsBgmPlaying --- core/libs/prx/libSceSystemService/Export.cpp | 4 ++++ core/libs/prx/libSceSystemService/PendingExports.cpp | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 65eae0e1e4..4b9945c748 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -852,4 +852,8 @@ int APS5_VABI sceCoredumpSequenceHandlerCancelCoredump(void) { return 0; } +int APS5_VABI sceSystemServiceIsBgmPlaying(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 1c37a7a66c..4e67c9ae98 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -2117,11 +2117,6 @@ bool APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { return 0; } -int APS5_VABI sceSystemServiceIsBgmPlaying() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted(std::uint32_t userId, std::uint8_t* adjusted) { NotImplemented_nid_no_patch(__func__); return 0; From 13d3ef8080bad039783e016e07b22aa12763978b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 15:02:07 +0300 Subject: [PATCH 0939/1360] feat(libSceSystemService): report no background music in sceShellCoreUtilIsBgmPlaying --- core/libs/prx/libSceSystemService/Export.cpp | 4 ++++ core/libs/prx/libSceSystemService/PendingExports.cpp | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 4b9945c748..c2d503d1dc 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -856,4 +856,8 @@ int APS5_VABI sceSystemServiceIsBgmPlaying(void) { return 0; } +int APS5_VABI sceShellCoreUtilIsBgmPlaying(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 4e67c9ae98..9fbb45de38 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -1241,11 +1241,6 @@ bool APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { return 0; } -int APS5_VABI sceShellCoreUtilIsBgmPlaying() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - bool APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { NotImplemented_nid_no_patch(__func__); return 0; From 3602165e9697e0bb15116c2167993dc1ca759e1e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 15:02:07 +0300 Subject: [PATCH 0940/1360] test(libSceSystemService): expect no background music --- core/libs/tests/GuestSystemService.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 36e6e11853..9ad6c43d4c 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -41,6 +41,8 @@ bool LaunchAppThrows(const char* titleId, const char* const* arguments, const vo } extern "C" int APS5_VABI sceSystemServicePowerTick(void); +extern "C" int APS5_VABI sceSystemServiceIsBgmPlaying(void); +extern "C" int APS5_VABI sceShellCoreUtilIsBgmPlaying(void); extern "C" int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info); extern "C" int APS5_VABI sceSystemServiceDisableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); @@ -89,6 +91,7 @@ int main() { Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceIsBgmPlaying() == 0 && sceShellCoreUtilIsBgmPlaying() == 0); Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceDisableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceDisableMediaPlay() == SYSTEM_SERVICE_OK); From f0d0bcafb593dd743c645fd0eb6dc76ed94b5be5 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 15:02:07 +0300 Subject: [PATCH 0941/1360] docs(debt): note why no background music is reported --- docs/dev/debt/libSceSystemService.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index a5b01ed7ab..35d98361d3 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -152,3 +152,4 @@ - [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 100–111, 200–213 and 300 throw because their settings are not modeled. Language results omit the console SDK-version compatibility remapping, and parameters 7, 400 and 1000 do not reproduce platform-dependent availability. - [Tournament launch parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): occurrence parameters occupy 32 bytes and contain two string pointers and a user ID. The final string semantics are unresolved. Event-details and team-profile launches retain an unused first argument slot of unknown declared type. The daemon blocking-event receive argument is passed unchanged as 64 bits to IPC; its meaning remains unresolved. Protected bodies in [PendingExports.cpp](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) remain unchanged. +- [sceSystemServiceIsBgmPlaying, sceShellCoreUtilIsBgmPlaying](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - not measured on a PS5: both return 0. In [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/system/systemservice.cpp) they report whether its libSceMusicPlayerService is playing; there is no libSceMusicPlayerService here, so no background music can be playing From 008a78aa2c2afc293c1bff12ce1b5aa09472d020 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:39:51 +0300 Subject: [PATCH 0942/1360] feat(libSceHttp): check the arguments of sceHttpSetProxy --- core/libs/prx/libSceHttp/Export.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 1cde24b012..6599dbf379 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -822,9 +822,10 @@ int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int http_ctx_id, int en return SetHttpContextAcceptEncodingGzip(http_ctx_id, enable != 0) ? 0 : ERROR_INVALID_ID; } -int APS5_VABI sceHttpSetProxy(int id, int mode, int authentication, const char* hostname, std::uint16_t port) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpSetProxy(int id, int, int, const char* hostname, std::uint16_t) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!hostname) return ERROR_INVALID_VALUE; + return FindHttpOptions(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetSocketCreationCallback(std::int32_t id, HttpSocketCreationCallback callback, void* userArg) { From af5a58ad294a1b26a01add923e05c64178ceafef Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:39:51 +0300 Subject: [PATCH 0943/1360] test(libSceHttp): check the sceHttpSetProxy refusals --- core/libs/tests/GuestHttpOptions.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp index bfa0a25748..461c44658f 100644 --- a/core/libs/tests/GuestHttpOptions.cpp +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -32,6 +32,7 @@ int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(int, int); int APS5_VABI sceHttpsEnableOptionPrivate(int, std::uint32_t); int APS5_VABI sceHttpsDisableOptionPrivate(int, std::uint32_t); int APS5_VABI sceHttpAbortRequestForce(int); +int APS5_VABI sceHttpSetProxy(int, int, int, const char*, std::uint16_t); } namespace { @@ -149,6 +150,7 @@ void CheckTermEndsOptions() { Require(sceHttpGetAcceptEncodingGZIPEnabled(pattern, &value) == BeforeInit && sceHttpSetAcceptEncodingGZIPEnabled(pattern, 1) == BeforeInit, "the gzip option outlived the last sceHttpTerm"); Require(sceHttpGetEpoll(connection, &bound, nullptr) == BeforeInit && sceHttpUnsetEpoll(connection) == BeforeInit, "the epoll binding outlived the last sceHttpTerm"); Require(sceHttpCacheRedirectedConnectionEnabled(connection, 1) == BeforeInit, "sceHttpCacheRedirectedConnectionEnabled works after the last sceHttpTerm"); + Require(sceHttpSetProxy(connection, 1, 0, "proxy.example", 3128) == BeforeInit, "sceHttpSetProxy works after the last sceHttpTerm"); Require(sceHttpsEnableOptionPrivate(connection, 0x4000u) == BeforeInit && sceHttpsDisableOptionPrivate(connection, 0x4000u) == BeforeInit, "the private SSL options work after the last sceHttpTerm"); Require(sceHttpGetNonblock(connection, nullptr) == BeforeInit, "a null output is checked before initialisation"); Require(value == -1 && bound == nullptr, "a refused getter wrote its output"); @@ -170,6 +172,12 @@ void CheckUnobservedOptions(int context) { Require(option(UnknownId, 0x0200u) == InvalidValue, "the SSL flags are not checked before the id"); Require(option(UnknownId, 0x01u) == InvalidId, "a private SSL option took an unknown id"); } + for (const int id : {pattern, connection, request}) { + Require(sceHttpSetProxy(id, 1, 0, "proxy.example", 3128) == 0, "sceHttpSetProxy refused a live object"); + } + Require(sceHttpSetProxy(connection, 0, 0, nullptr, 0) == InvalidValue, "sceHttpSetProxy took a null host"); + Require(sceHttpSetProxy(UnknownId, 0, 0, nullptr, 0) == InvalidValue, "the proxy host is not checked before the id"); + Require(sceHttpSetProxy(UnknownId, 1, 1, "proxy.example", 8080) == InvalidId, "sceHttpSetProxy took an unknown id"); Require(sceHttpAbortRequestForce(request) == 0, "sceHttpAbortRequestForce failed on a live request"); Require(sceHttpDeleteRequest(request) == 0 && sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); } From abaaa66a61c646b054e0f3bfdc070026c7a1862d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:39:51 +0300 Subject: [PATCH 0944/1360] docs(debt): record sceHttpSetProxy as a silent stub --- docs/dev/debt/libSceHttp.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index fce148fc5f..6cc44f2301 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -4,6 +4,7 @@ - [libSceHttp](../../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title - [HTTP options without a network](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - `sceHttpCacheRedirectedConnectionEnabled`, `sceHttpsEnableOptionPrivate` and `sceHttpsDisableOptionPrivate` return 0 for a live template, connection or request and keep nothing, since no request reaches the network; the private options refuse flags outside `0x2dff` with `ERROR_INVALID_VALUE` before they look at the id, and `sceHttpAbortRequestForce` does what `sceHttpAbortRequest` does. Signatures, the mask and the order of the checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4), not measured on a PS5 +- [sceHttpSetProxy](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - returns 0 for a live template, connection or request and keeps nothing, since no request reaches the network. The signature `(id, HTTP proxy conf, WLAN proxy conf, host, port)` and the order of the checks (`ERROR_BEFORE_INIT`, then `ERROR_INVALID_VALUE` for a null host, then `ERROR_INVALID_ID`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4), which stores the proxy; not measured on a PS5 ### Unknown function info From 548aecf0303f78833778f1fece9c2e410cdd139a Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:36:57 +0000 Subject: [PATCH 0945/1360] test(net): make resolver failures deterministic --- core/libs/prx/libSceNet/Export.cpp | 14 ++++++ .../prx/libSceNet/src/ResolverTestHooks.hpp | 10 ++++ core/libs/tests/GuestSceNet.cpp | 49 ++++++++++++++++--- 3 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceNet/src/ResolverTestHooks.hpp diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 4861eac26d..fd70b3716c 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -43,6 +43,7 @@ #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "src/ResolverTestHooks.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -478,6 +479,7 @@ int g_next_epoll = 0x4000; int g_next_pool = 1; int g_next_resolver = 0x100; bool g_net_inited = false; +std::atomic<GuestSceNetTest::ResolverLookup> g_resolverLookupForTests{nullptr}; int* errno_slot() { static thread_local int e = 0; @@ -788,6 +790,10 @@ bool parse_ipv4_parts(const char* text, std::uint32_t& address) { } // namespace +void GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(ResolverLookup lookup) noexcept { + g_resolverLookupForTests.store(lookup); +} + extern "C" { extern const std::uint32_t sce_net_in6addr_any[4] = {}; @@ -2052,6 +2058,14 @@ int lookup_host(int rid, const char* hostname, const char* func, int family, std if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_ntoa, generation)) return error; + if (const auto lookup = g_resolverLookupForTests.load()) { + if (!lookup(hostname, family, addresses) || addresses.empty()) { + set_errno(NET_ENOENT); + log_soft(func, "host DNS lookup failed"); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } + return end_lookup(rid, generation, 0); + } addrinfo hints{}; hints.ai_family = family; addrinfo* results = nullptr; diff --git a/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp b/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp new file mode 100644 index 0000000000..cde7e83a8a --- /dev/null +++ b/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <array> +#include <cstdint> +#include <vector> + +namespace GuestSceNetTest { +using ResolverLookup = bool (*)(const char*, int, std::vector<std::array<std::uint8_t, 16>>&); +void SetResolverLookupForTests_nid_no_patch(ResolverLookup lookup) noexcept; +} diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 4eb69f493e..599f79d3ec 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -13,6 +13,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libSceNet/src/ResolverTestHooks.hpp" #include "SceTypes.hpp" #include <algorithm> #include <array> @@ -146,6 +147,20 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static bool FailResolverLookup(const char*, int, std::vector<std::array<std::uint8_t, 16>>&) { + return false; +} + +class ScopedResolverLookupOverride { +public: + explicit ScopedResolverLookupOverride(GuestSceNetTest::ResolverLookup lookup) { + GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(lookup); + } + ~ScopedResolverLookupOverride() { + GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(nullptr); + } +}; + static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} static bool Failed(std::int64_t result, int error) { @@ -1779,8 +1794,13 @@ int main() { int resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); std::array<std::uint8_t, 4> ipv4{}; - Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == - static_cast<int>(0x804101E1)); + ipv4.fill(0xA5); + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == + static_cast<int>(0x804101E1)); + } + Require(std::all_of(ipv4.begin(), ipv4.end(), [](std::uint8_t byte) { return byte == 0xA5; })); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast<int>(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, nullptr, ipv4.data(), 5000000, 1, 0) == static_cast<int>(0x80410116)); @@ -1794,7 +1814,12 @@ int main() { Require(std::all_of(resolved6.begin(), resolved6.end() - 1, [](std::uint8_t byte) { return byte == 0; }) && resolved6[15] == 1); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast<int>(0x80410116)); - Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast<int>(0x804101E1)); + const auto resolved6BeforeFailure = resolved6; + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast<int>(0x804101E1)); + } + Require(resolved6 == resolved6BeforeFailure); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast<int>(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); @@ -1811,6 +1836,7 @@ int main() { Require(std::all_of(records.begin() + 32 * record_count, records.begin() + 320, [](std::uint8_t byte) { return byte == 0; })); Require(std::all_of(records.begin() + 328, records.begin() + 384, [](std::uint8_t byte) { return byte == 0; })); Require(std::all_of(records.begin() + 384, records.end(), [](std::uint8_t byte) { return byte == 0xA5; })); + const auto resolvedRecords = records; for (int first = 0; first < record_count; ++first) { for (int second = first + 1; second < record_count; ++second) { Require(std::memcmp(records.data() + 32 * first, records.data() + 32 * second, 4) != 0); @@ -1821,8 +1847,12 @@ int main() { static_cast<int>(0x80410116) && *sceNetErrnoLoc() == 22 && records == resolved); Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", nullptr, 5000000, 1, 0) == static_cast<int>(0x80410116) && *sceNetErrnoLoc() == 22); - Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == - static_cast<int>(0x804101E1) && records == resolved); + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == + static_cast<int>(0x804101E1)); + } + Require(records == resolvedRecords); std::array<std::uint8_t, 512> plain{}; plain.fill(0xA5); @@ -1833,8 +1863,13 @@ int main() { static_cast<int>(0x80410116) && *sceNetErrnoLoc() == 22 && plain == resolved); Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", nullptr, 5000000, 1, 0) == static_cast<int>(0x80410116) && *sceNetErrnoLoc() == 22); - Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == - static_cast<int>(0x804101E1) && plain == resolved); + const auto resolvedPlain = plain; + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == + static_cast<int>(0x804101E1)); + } + Require(plain == resolvedPlain); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast<int>(0x804101E1)); for (const int flags : {0x1, 0x10000}) { bool threw = false; From 74f2fcb61201be0aa86102d67fc9c759670ec52e Mon Sep 17 00:00:00 2001 From: Werner <hamre.dev@gmail.com> Date: Thu, 22 Oct 2026 07:37:59 +0200 Subject: [PATCH 0946/1360] fix(agc): refuse a depth view mip on a surface without mips AI-assisted: yes --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index be7fd512c6..6ff33d99e1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -349,6 +349,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx } const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips"); if (maxMip != 0) { char message[320]; std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 48f92cabe8..8cc3a17f70 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1713,6 +1713,8 @@ void DisabledColorTests() { queue.context[0x002] = 3; const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue); Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset"); + queue.context[0x002] = 1u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "on a surface without mips"); queue.context[0x002] = 0; { using AgcDriver::Graphics::ComputeElementMipLayout; From 8a217a122409828e32f58df35f6200187dc046dd Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Thu, 22 Oct 2026 02:52:40 -0300 Subject: [PATCH 0947/1360] docs(libkernel): record the unmeasured sceKernelCreateEventFlag error order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6cdba1cc80..4fb32ed53b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -60,6 +60,7 @@ - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. +- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) From 0b3581b3870a07704563840d5e9f709fb5ecbbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Thu, 22 Oct 2026 08:00:59 +0200 Subject: [PATCH 0948/1360] fix(main): the blocker of agc_driver_pipelined_indirect_draw outlives its test #9433 merged red: main's push build 39725502001 failed agc_driver_pipelined_indirect_draw with "the queue worker and the draw pipeline deadlocked" (the failure predates #8807, whose diff only touches the frame dump writer; the test's timing was never robust). Cause: the Blocker queued on the draw pipeline captured `this`, a stack object. The first test's Finish() can return before the blocker lambda has run, so it ran during the second test through a dangling pointer, read a `release` that was never set, and held the committer forever. Reproduced under CPU starvation (12 processes on 2 cores, lavapipe, no udmabuf): ~17% of runs failed, 0 of 72 hang after the fix. The blocker state is now shared and the tests wait for the blocker to finish; the fixed 500 ms sleeps wait for the pipeline items and the drain instead. Co-Authored-By: Claude <noreply@anthropic.com> --- .../tests/execution/PipelinedIndirectDraw.cpp | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp index 437a8ecaa2..93abe602a5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp @@ -17,6 +17,7 @@ #include <future> #include <initializer_list> #include <iostream> +#include <memory> #include <mutex> #include <string> #include <thread> @@ -138,20 +139,34 @@ void Flush(const std::array<std::byte, 1024>& memory) { } struct Blocker { - std::atomic<bool> release{false}; + struct State { + std::atomic<bool> release{false}; + std::atomic<bool> done{false}; + }; + std::shared_ptr<State> state = std::make_shared<State>(); + void Release() { state->release = true; } void Enqueue() { - DrawPipeline::Queue0().Enqueue([this] { - while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); - std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + DrawPipeline::Queue0().Enqueue([state = state] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; }, {}); } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } }; -void SubmitBehind(Blocker& blocker, std::vector<std::uint32_t>& commands) { +void SubmitBehind(Blocker& blocker, std::vector<std::uint32_t>& commands, std::size_t queuedBefore) { std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}}; AgcDriver::Submit(&packet, 0); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < queuedBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(60)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); blocker.Enqueue(); } @@ -190,19 +205,20 @@ void IndirectDrawsQueueBehindTheCommitter() { AppendTarget(commands, SecondMemory, Clipped); AppendIndirect(commands); Blocker blocker; - SubmitBehind(blocker, commands); + SubmitBehind(blocker, commands, 3); const auto start = std::chrono::steady_clock::now(); while (DrawPipeline::Queue0().Outstanding() < 3) { if (std::chrono::steady_clock::now() - start > std::chrono::seconds(10)) { const auto outstanding = DrawPipeline::Queue0().Outstanding(); - blocker.release = true; + blocker.Release(); Finish(); Require(false, "the indirect draws drained the pipeline instead of queuing behind it (" + std::to_string(outstanding) + " items outstanding, expected the blocker and two draws)"); } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - blocker.release = true; + blocker.Release(); Finish(); + blocker.Join(); Flush(FirstMemory); Flush(SecondMemory); SameAsDirect(FirstMemory, "the first queued indirect draw"); @@ -219,12 +235,13 @@ void IndirectDrawWaitsForItsQueuedRecordWrite() { AppendTarget(commands, WrittenMemory, Clipped); AppendIndirect(commands); Blocker blocker; - SubmitBehind(blocker, commands); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x100); + SubmitBehind(blocker, commands, 1); const auto start = std::chrono::steady_clock::now(); - while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - blocker.release = true; + while (DrawPipeline::DrainRequestsByOpcode(0x100) == drainsBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + blocker.Release(); Finish(); + blocker.Join(); Flush(WrittenMemory); Require(LateRecords[0] == 3, "the queued label did not store the record's vertex count"); SameAsDirect(WrittenMemory, "the indirect draw whose record a queued label wrote"); From 28d9f26bdd4e01f5b5be8b752121645d50001ef7 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 20 Oct 2026 16:09:22 -0400 Subject: [PATCH 0949/1360] fix(libSceVideodec2): flush without draining when no picture is pending sceVideodec2Flush drained libavcodec with an empty packet whenever the decoder was not already flushing, and the next sceVideodec2Decode then restarted the decoder with avcodec_flush_buffers, dropping its reference pictures. A caller that flushes after every access unit, as Sony's PSP emulator does for its H.264 movies, lost every P picture after the first: libavcodec withholds pictures after a restart until it reaches a keyframe, so Daxter (PPSA09695) got 8 pictures from 459 access units and showed a black screen ("Failed to obtain a video frame") instead of its intro movie. Flush now drains only when a decoded access unit has not produced its picture yet. With nothing pending there is nothing to return, and the decoder keeps its references for the units that follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceVideodec2/Export.cpp | 2 +- core/libs/tests/GuestVideodec2.cpp | 87 ++++++++++++++++++++++++ docs/dev/debt/libSceVideodec2.md | 2 +- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 99b68e36c1..2d51f7cb70 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -676,7 +676,7 @@ int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, FrameBuffer* f if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); - if (!decoder.flushing) { + if (!decoder.flushing && decoder.ready.empty() && !decoder.inputs.empty()) { Send(decoder, nullptr); decoder.flushing = true; } diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 986837aa66..58b3f5240e 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -191,6 +191,50 @@ constexpr std::array<std::uint64_t, 6> HevcHashes{ 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, }; +constexpr std::array<std::uint8_t, 658> AvcIpppStream{ + 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x0a, 0xda, 0x11, + 0xec, 0x04, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x0f, 0x03, 0xc4, 0x89, 0xa8, 0x00, + 0x00, 0x00, 0x01, 0x68, 0xce, 0x0f, 0x2c, 0x80, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x09, 0x7f, + 0xe4, 0x5e, 0xd2, 0xf1, 0x40, 0x00, 0x40, 0x07, 0xc5, 0x00, 0x02, 0x50, 0x06, 0x46, 0x28, 0xcd, + 0x0f, 0x80, 0x38, 0x07, 0xff, 0xc0, 0x07, 0xd8, 0x76, 0x33, 0x23, 0x81, 0xce, 0x7d, 0x73, 0x08, + 0x2f, 0xc0, 0xff, 0x1f, 0xc1, 0x61, 0x01, 0x2f, 0xc1, 0x00, 0x01, 0xf0, 0x19, 0x8d, 0x80, 0x38, + 0xd1, 0x0d, 0xcc, 0x7a, 0x99, 0xdf, 0xff, 0xda, 0x00, 0x3d, 0x82, 0x8e, 0xf2, 0x8f, 0x22, 0x18, + 0x94, 0x07, 0x24, 0x94, 0x45, 0x22, 0x44, 0x5c, 0xb8, 0x7f, 0xe1, 0xa2, 0x99, 0xa3, 0xef, 0xe2, + 0x80, 0x01, 0x9f, 0x03, 0x81, 0x0e, 0x01, 0x80, 0x70, 0x00, 0x10, 0x05, 0x00, 0x04, 0x00, 0x02, + 0x00, 0x6e, 0xbf, 0x6f, 0x66, 0xdb, 0x0c, 0x3f, 0x7c, 0x5b, 0xcc, 0xf6, 0x6c, 0xcc, 0x30, 0xe1, + 0x8c, 0x1c, 0x00, 0x04, 0x04, 0x80, 0x00, 0x42, 0x73, 0xfd, 0x1f, 0xfe, 0x20, 0x00, 0x13, 0x00, + 0x01, 0x08, 0xf8, 0x80, 0x00, 0x20, 0x2c, 0x1f, 0x04, 0xbb, 0xf3, 0xc0, 0x23, 0xdd, 0x63, 0x6f, + 0xf9, 0xe1, 0x04, 0x82, 0x53, 0x90, 0x01, 0x89, 0xe5, 0x89, 0x44, 0x5a, 0xf8, 0x39, 0xe0, 0x03, + 0x20, 0x10, 0x50, 0x00, 0x25, 0xf9, 0x8d, 0xe0, 0x04, 0xf3, 0x62, 0xfd, 0x35, 0x7d, 0x77, 0x7e, + 0x0b, 0xe1, 0x4d, 0xfe, 0x85, 0x2b, 0xef, 0xe8, 0xf0, 0x80, 0xa2, 0x80, 0x03, 0x04, 0x00, 0x06, + 0xc0, 0x72, 0x80, 0x02, 0x8c, 0x14, 0xe0, 0x9d, 0xcd, 0xe8, 0x3f, 0xe9, 0xb5, 0x9b, 0x06, 0x4d, + 0x5e, 0x80, 0xa2, 0x1f, 0x06, 0xda, 0xdf, 0x36, 0xfd, 0xa1, 0x0f, 0xfe, 0x08, 0xb0, 0x26, 0x48, + 0xaf, 0x17, 0xe9, 0xc5, 0x9f, 0xbf, 0xba, 0x2e, 0x69, 0xa9, 0xe6, 0x7c, 0xbf, 0x4f, 0xfe, 0xc1, + 0x64, 0x0a, 0xaa, 0x69, 0xda, 0x43, 0xff, 0xf9, 0xfd, 0xfc, 0xff, 0x77, 0xe9, 0x1e, 0x20, 0x18, + 0x30, 0x1f, 0x80, 0x12, 0xe4, 0x51, 0x4d, 0x9b, 0xff, 0x9b, 0x83, 0xa0, 0x01, 0x72, 0xd1, 0x7f, + 0x83, 0xa2, 0xe9, 0x82, 0x73, 0x13, 0xd3, 0x8d, 0x59, 0x95, 0x1f, 0xa6, 0x70, 0x4a, 0xeb, 0xb5, + 0xe0, 0x42, 0x10, 0xc0, 0x02, 0x00, 0x1c, 0x20, 0x00, 0x28, 0x14, 0x68, 0xe0, 0x1a, 0x82, 0xd4, + 0x87, 0x10, 0x00, 0x46, 0x54, 0x41, 0xc1, 0x81, 0x9a, 0x0c, 0x5d, 0xd0, 0x03, 0xd8, 0x0f, 0x52, + 0x07, 0x2c, 0x00, 0x7a, 0x92, 0x12, 0x02, 0xdc, 0x01, 0x67, 0x3a, 0xc1, 0xa4, 0x5a, 0x83, 0x95, + 0xc0, 0x30, 0x02, 0xf8, 0x49, 0xe2, 0x1e, 0x2b, 0xd5, 0x4a, 0x29, 0xa9, 0x6d, 0xf2, 0xc0, 0x7b, + 0x27, 0x2c, 0xb3, 0xde, 0x18, 0x00, 0x11, 0x71, 0xe0, 0x70, 0x02, 0x0c, 0x34, 0x07, 0x06, 0x01, + 0xa2, 0x90, 0x71, 0x40, 0x05, 0x12, 0x98, 0x01, 0x01, 0x63, 0xe2, 0xa8, 0xc0, 0xb8, 0xe0, 0xe0, + 0xf0, 0x06, 0x8a, 0x41, 0xd0, 0x01, 0x59, 0x12, 0x08, 0x30, 0x7d, 0x39, 0x02, 0x1d, 0x5e, 0x01, + 0x80, 0x26, 0x39, 0x0f, 0xc1, 0xef, 0x83, 0xc3, 0xf8, 0x3c, 0x7c, 0x4f, 0x86, 0xdd, 0x50, 0x6d, + 0x69, 0xd0, 0xef, 0xe9, 0x70, 0x35, 0xc4, 0xb3, 0x84, 0x40, 0x00, 0x40, 0x48, 0x00, 0x54, 0x84, + 0x00, 0x0d, 0x51, 0x81, 0xc3, 0x80, 0x3d, 0xd2, 0x1c, 0x20, 0x1c, 0xca, 0xa0, 0x10, 0x1a, 0x46, + 0x38, 0x04, 0x06, 0xd0, 0x75, 0x00, 0x2f, 0x48, 0x1e, 0x80, 0x3f, 0x49, 0x14, 0xc0, 0x3f, 0xc4, + 0x52, 0x01, 0x7e, 0x78, 0x36, 0x19, 0x08, 0x43, 0xf6, 0xc5, 0xc0, 0x01, 0xf8, 0x9f, 0x6e, 0x65, + 0xa4, 0xff, 0xff, 0x86, 0x0e, 0xf8, 0x00, 0x8b, 0xfb, 0xe4, 0xf4, 0x00, 0x57, 0x5c, 0x07, 0x4b, + 0x80, 0x66, 0xe3, 0xd2, 0xcd, 0xa7, 0x03, 0xa0, 0x41, 0x81, 0x20, 0x00, 0x20, 0x24, 0x80, 0x73, + 0x80, 0x0e, 0x52, 0x0e, 0xe5, 0xc0, 0x27, 0xd9, 0x08, 0x9c, 0x6c, 0xa6, 0x58, 0x86, 0xc5, 0xe9, + 0x0e, 0x40, 0x0c, 0xc8, 0x17, 0x5c, 0x50, 0x1a, 0xc1, 0x74, 0x47, 0x9f, 0xf0, 0x81, 0x0d, 0xb0, + 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x20, 0x25, 0xde, 0x49, 0xba, + 0xbd, 0x70, 0x47, 0xbd, 0x5a, 0xa0, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, + 0x9a, 0x40, 0x2d, 0xc4, 0xf0, 0x47, 0x55, 0xf7, 0xfa, 0xdf, 0xe0, 0x97, 0xbb, 0xee, 0xad, 0xf0, + 0x43, 0xd5, 0x7b, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x60, 0x35, + 0xc6, 0xc0}; + constexpr std::array<std::uint8_t, 234> HevcMain10Stream{ 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, @@ -723,6 +767,48 @@ void testEmptyAccessUnit() { check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } +void testFlushAfterEveryUnit() { + std::vector<std::vector<std::uint8_t>> units; + for (std::size_t index = 0; index + 4 < AvcIpppStream.size(); ++index) { + if (AvcIpppStream[index] == 0 && AvcIpppStream[index + 1] == 0 && AvcIpppStream[index + 2] == 0 && AvcIpppStream[index + 3] == 1 && (AvcIpppStream[index + 4] & 0x1fu) == 9) units.emplace_back(); + check(!units.empty(), "the IPPP stream does not start with an access unit delimiter"); + units.back().push_back(AvcIpppStream[index]); + } + for (std::size_t index = AvcIpppStream.size() - 4; index < AvcIpppStream.size(); ++index) units.back().push_back(AvcIpppStream[index]); + check(units.size() == 4, "unexpected IPPP access unit count"); + std::vector<std::uint8_t> buffer(frameBufferBytes()); + const auto decode = [&](bool flushEachUnit) { + const auto handle = createDecoder(1, 66); + std::vector<std::uint64_t> hashes; + const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { + if (!output.isValid) return false; + check(frame.isAccepted && output.frameWidth == Width && output.frameHeight == CodedHeight, "unexpected IPPP picture"); + hashes.push_back(hashNv12(buffer.data(), output.framePitch, output.frameHeight)); + return true; + }; + const auto flush = [&] { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + return take(output, frame); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 3000 + unit, unit, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "IPPP decode failed"); + take(output, frame); + if (flushEachUnit) flush(); + } + while (flush()) {} + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); + return hashes; + }; + const auto reference = decode(false); + check(reference.size() == units.size(), "an IPPP picture is missing without flushes"); + check(decode(true) == reference, "a flush after each access unit lost or changed the pictures that follow it"); +} + void testDecodeAfterFlush() { std::vector<std::uint8_t> buffer(frameBufferBytes()); const auto handle = createDecoder(1, 100); @@ -891,6 +977,7 @@ int main() { testTenBit(); testDecodeAfterFlush(); testEmptyAccessUnit(); + testFlushAfterEveryUnit(); testPictureOwnership(); testConcurrentOwnership(); std::puts("Videodec2 tests passed"); diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 411c4b2885..a86293f197 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -13,6 +13,6 @@ - [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. -- [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. +- [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. A flush with no picture pending (each decoded unit has returned its picture) leaves the decoder and its reference pictures as they are, so a stream that continues without a new IDR picture keeps decoding: Daxter (PPSA09695), also in the PSP emulator, flushes after every access unit of its H.264 movies, and restarting the decoder there lost every P picture until the next keyframe. What the console returns for pending pictures, and for a stream that continues across a flush that had pictures pending, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping - [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. From 3f698c10161edc8389e56bc6480736355c3eb18c Mon Sep 17 00:00:00 2001 From: memob <memobayoglu2007@gmail.com> Date: Wed, 21 Oct 2026 15:46:05 -0400 Subject: [PATCH 0950/1360] perf(libSceNetCtl): reuse host interface queries until Windows reports a change PPSA34501 calls sceNetCtlGetInfo often enough that GetAdaptersAddresses and its RPC took about a fifth of a game thread at the Kino der Toten spawn. The answers are now kept per address family and for the IPv4 interface, and dropped when an address, interface or route change notification arrives. --- core/libs/prx/libSceNetCtl/Export.cpp | 91 ++++++++++++++++++++++++++- docs/dev/debt/libSceNetCtl.md | 1 + 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 519bcf4ce1..54c9d941fe 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -131,7 +131,7 @@ static bool HasIpv4Callback() { return false; } -static bool HostHasAddress(int family) { +static bool QueryHostHasAddress(int family) { #ifdef _WIN32 static const int socket_status = [] { WSADATA data{}; @@ -188,6 +188,74 @@ struct HostIpv4Interface { std::vector<in_addr> dns; }; +#ifdef _WIN32 +namespace { + +struct HostAddressCache { + std::mutex lock; + bool registered = false; + bool tried = false; + std::atomic<std::uint64_t> generation{1}; + std::uint64_t seen[3]{}; + bool value[3]{}; + std::uint64_t interfaceSeen = 0; + bool interfaceFound = false; + HostIpv4Interface interfaceValue; +}; + +HostAddressCache& AddressCache() { + static HostAddressCache cache; + return cache; +} + +void CALLBACK HostAddressChanged(PVOID, PMIB_UNICASTIPADDRESS_ROW, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void CALLBACK HostInterfaceChanged(PVOID, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void CALLBACK HostRouteChanged(PVOID, PMIB_IPFORWARD_ROW2, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void WatchHostAddresses(HostAddressCache& cache) { + if (cache.tried) return; + cache.tried = true; + HANDLE addresses = nullptr; + HANDLE interfaces = nullptr; + HANDLE routes = nullptr; + cache.registered = NotifyUnicastIpAddressChange(AF_UNSPEC, HostAddressChanged, nullptr, FALSE, &addresses) == NO_ERROR && + NotifyIpInterfaceChange(AF_UNSPEC, HostInterfaceChanged, nullptr, FALSE, &interfaces) == NO_ERROR && + NotifyRouteChange2(AF_UNSPEC, HostRouteChanged, nullptr, FALSE, &routes) == NO_ERROR; + if (!cache.registered) { + if (addresses != nullptr) CancelMibChangeNotify2(addresses); + if (interfaces != nullptr) CancelMibChangeNotify2(interfaces); + if (routes != nullptr) CancelMibChangeNotify2(routes); + } +} + +} +#endif + +static bool HostHasAddress(int family) { +#ifdef _WIN32 + auto& cache = AddressCache(); + const int slot = family == AF_INET ? 0 : family == AF_INET6 ? 1 : 2; + std::lock_guard guard(cache.lock); + WatchHostAddresses(cache); + const auto generation = cache.generation.load(std::memory_order_acquire); + if (cache.registered && cache.seen[slot] == generation) return cache.value[slot]; + const bool value = QueryHostHasAddress(family); + cache.value[slot] = value; + cache.seen[slot] = generation; + return value; +#else + return QueryHostHasAddress(family); +#endif +} + static bool UsableIpv4(in_addr address) { const auto host = ntohl(address.s_addr); return host != 0 && (host >> 24) != 127; @@ -310,7 +378,7 @@ static std::vector<in_addr> HostDnsServers() { } #endif -static bool FindHostIpv4Interface(HostIpv4Interface& found) { +static bool QueryHostIpv4Interface(HostIpv4Interface& found) { #ifdef _WIN32 ULONG size = 0; const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; @@ -371,6 +439,25 @@ static bool FindHostIpv4Interface(HostIpv4Interface& found) { #endif } +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + auto& cache = AddressCache(); + std::lock_guard guard(cache.lock); + WatchHostAddresses(cache); + const auto generation = cache.generation.load(std::memory_order_acquire); + if (!cache.registered || cache.interfaceSeen != generation) { + HostIpv4Interface value; + cache.interfaceFound = QueryHostIpv4Interface(value); + cache.interfaceValue = std::move(value); + cache.interfaceSeen = generation; + } + if (cache.interfaceFound) found = cache.interfaceValue; + return cache.interfaceFound; +#else + return QueryHostIpv4Interface(found); +#endif +} + extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity) { HostIpv4Interface host; if (!FindHostIpv4Interface(host)) return 0; diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 96b0e7ed58..aaa9cf2f9d 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -19,3 +19,4 @@ - [libSceNetCtl initialization](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the library starts initialized, as titles find it: it is the internal sysmodule `0x80000009`, and PPSA11386, PPSA20049, PPSA02530, PPSA14632 and PPSA15878 register and check NetCtl callbacks without importing `sceNetCtlInit`. PPSA11386 executes `int 0x42` at start-up unless `sceNetCtlRegisterCallback` returns 0. So the supplied module's `SCE_NET_CTL_ERROR_NOT_INITIALIZED` answers are unreachable, since Term is a no-op. Whether the console's process start or the module's start sets that state is not measured. - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB +- [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. From a22f22085713eb58ef412bf5ffe1bc5faba75189 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:52:17 -0300 Subject: [PATCH 0951/1360] fix(tools): check environment references from a subdirectory --- tools/check_conventions.py | 6 ++-- .../test_check_conventions_env_reference.py | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index bc985e1480..a2be791e14 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -340,7 +340,7 @@ class Check: doc = "docs/dev/ENVIRONMENT.md" added, removed, dropped = {}, set(), set() path, number, in_hunk = "", 0, False - for line in git("diff", "-U0", self.base, self.head, "--", "core", doc).splitlines(): + for line in git("diff", "-U0", self.base, self.head, "--", ":(top)core", f":(top){doc}").splitlines(): if line.startswith("diff --git"): path, in_hunk = "", False elif line.startswith("@@"): @@ -358,14 +358,14 @@ class Check: removed.update(match.group("name") for _, match in env_reference.line_reads(line[1:])) elif line.startswith("+++ b/"): path = line[6:] - result = subprocess.run(["git", "grep", "-nE", env_reference.GREP, self.head, "--", "core"], + result = subprocess.run(["git", "grep", "-nE", "--full-name", env_reference.GREP, self.head, "--", ":(top)core"], capture_output=True, encoding="utf-8", errors="replace") reads = set() for line in result.stdout.splitlines(): reads.update(match.group("name") for _, match in env_reference.line_reads(line)) pattern = env_reference.documented_pattern(env_reference.PREFIXES) documented = set(pattern.findall(self.show(doc))) if doc in self.tree else set() - if git("ls-tree", "--name-only", self.target, "--", doc).strip() == doc: + if git("ls-tree", "--full-tree", "--name-only", self.target, "--", doc).strip() == doc: documented |= set(pattern.findall(git("show", f"{self.target}:{doc}"))) - dropped for name, (source, line) in sorted(added.items()): if name not in documented: diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py index b217e80247..270218d686 100644 --- a/tools/tests/test_check_conventions_env_reference.py +++ b/tools/tests/test_check_conventions_env_reference.py @@ -46,9 +46,9 @@ def repository(root, base, head, target=None): git("commit", "-qm", "feat: read a new name") -def findings_of(root, base, head, ref="base"): +def findings_of(root, base, head, ref="base", directory="."): previous = os.getcwd() - os.chdir(root) + os.chdir(root / directory) try: check = Check(ref, "HEAD") check.env_reference() @@ -58,12 +58,32 @@ def findings_of(root, base, head, ref="base"): class EnvReferenceDiffTests(unittest.TestCase): - def run_case(self, base, head, target=None): - directory = tempfile.TemporaryDirectory() - self.addCleanup(directory.cleanup) - root = Path(directory.name) + def run_case(self, base, head, target=None, directory="."): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) repository(root, base, head, target) - return findings_of(root, base, head, "base" if target is None else "target") + return findings_of(root, base, head, "base" if target is None else "target", directory) + + def test_a_check_from_a_subdirectory_reports_the_same_findings(self): + other = "core/libs/prx/libSceExample/Other.cpp" + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + other: 'static bool b = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + target = {DOC: DOC_TEXT + "| `APS5_TARGET_ROW` | presence |\n"} + head = { + DOC: "| `APS5_DOCUMENTED` | presence |\n", + SAMPLE: base[SAMPLE] + 'static bool c = std::getenv("APS5_NEW_READ") != nullptr;\n' + 'static bool d = std::getenv("APS5_TARGET_ROW") != nullptr;\n', + other: None, + } + expected = [("env-reference", SAMPLE, 2, f"APS5_NEW_READ is read here but has no row in {DOC}")] + self.assertEqual(self.run_case(base, head, target), expected) + for directory in ("core", "core/libs/prx/libSceExample", "docs/dev"): + with self.subTest(directory=directory): + self.assertEqual(self.run_case(base, head, target, directory), expected) def test_only_a_name_the_pull_request_adds_is_reported_at_its_line(self): base = { From 2e2445d622902a9c729d715f08bee681435ec956 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:37:30 -0300 Subject: [PATCH 0952/1360] fix(tools): read pr_overlap paths from the top of the repository --- tools/pr_overlap.py | 3 ++ tools/tests/test_pr_overlap.py | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 6699df9ebf..3c75db06ec 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -264,7 +264,10 @@ if __name__ == "__main__": parser.error("pass exactly one of --out or --branch") if args.jobs < 1: parser.error("--jobs must be at least 1") + if args.out: + args.out = args.out.resolve() try: + os.chdir(git("rev-parse", "--show-toplevel").strip()) REPO = args.repo or upstream() prs = open_prs(args.base) fetch(args.base, prs) diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 520620c8a3..4017ab3718 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -312,6 +312,62 @@ class UnicodePathTests(unittest.TestCase): os.chdir(previous) +class SubdirectoryTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "fake gh is a shell script") + def test_branch_report_from_a_subdirectory_matches_the_root(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() / "repo" + fake = Path(directory) / "bin" + fake.mkdir() + + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=root, + capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() + + filename = "core/libs/prx/libSceExample/Export.cpp" + source = root / filename + source.parent.mkdir(parents=True) + source.write_text("int value = 0;\n", encoding="utf-8") + (root / "tools").mkdir() + (root / "tools" / "tool.py").write_text("print()\n", encoding="utf-8") + git("init", "-q", "-b", "main") + git("add", ".") + git("commit", "-qm", "test: create base") + heads = {} + for branch, value in (("theirs", 1), ("mine", 2)): + git("checkout", "-qb", branch, "main") + source.write_text(f"int value = {value};\nint APS5_VABI sceExample() {{ return {value}; }}\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "fix: change value") + heads[branch] = git("rev-parse", "HEAD") + pulls = [{"number": 7, "user": {"login": "someone"}, "head": {"sha": heads["theirs"]}, "body": "Depends on: none"}] + (fake / "pulls.json").write_text(json.dumps(pulls), encoding="utf-8") + (fake / "gh").write_text(f"#!/bin/sh\ncat '{fake / 'pulls.json'}'\n", encoding="utf-8") + (fake / "gh").chmod(0o755) + environment = {**os.environ, "PATH": f"{fake}{os.pathsep}{os.environ['PATH']}", "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": f"url.{root.as_uri()}.insteadOf", "GIT_CONFIG_VALUE_0": "https://github.com/owner/project.git"} + environment.pop("GITHUB_REPOSITORY", None) + script = Path(pr_overlap.__file__).resolve() + outputs = {} + for subdirectory in (".", "tools", "core/libs/prx"): + result = subprocess.run([sys.executable, str(script), "--repo", "owner/project", "--branch", "mine"], + cwd=root / subdirectory, env=environment, capture_output=True, text=True, + encoding="utf-8", timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + outputs[subdirectory] = result.stdout + self.assertIn(f"`{filename}` [L1](", outputs["."]) + self.assertIn("| `sceExample` |", outputs["."]) + self.assertEqual(outputs["tools"], outputs["."]) + self.assertEqual(outputs["core/libs/prx"], outputs["."]) + result = subprocess.run([sys.executable, str(script), "--repo", "owner/project", "--out", "reports"], + cwd=root / "tools", env=environment, capture_output=True, text=True, + encoding="utf-8", timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((root / "tools" / "reports" / "7.md").is_file()) + + class CommentStateTests(unittest.TestCase): def setUp(self): if os.name == "nt" or not all(shutil.which(tool) for tool in ("bash", "jq", "sed", "sha256sum")): From 81ae747acdb9c0baa70f4bd3c5f2bc2edf58d3ae Mon Sep 17 00:00:00 2001 From: krishalivaibhav <krishalivaibhav@gmail.com> Date: Thu, 22 Oct 2026 12:38:04 +0530 Subject: [PATCH 0953/1360] feat(libScePad): answer the tracker DualShock 4, Blaster and Move queries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libScePad/Export.cpp | 12 ++++++------ core/libs/tests/GuestPad.cpp | 12 ++++++++++++ docs/dev/debt/libScePad.md | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 3a6f40f658..7be40a0d7a 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -923,18 +923,18 @@ bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { } bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + return false; } bool APS5_VABI scePadIsBlasterConnected(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + return false; } bool APS5_VABI scePadIsMoveConnected(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + return false; } int APS5_VABI scePadGetBluetoothAddress(std::int32_t handle, std::uint64_t* address) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 335c38cea7..bfef37f465 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -30,6 +30,9 @@ int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); +bool APS5_VABI scePadIsDS4Connected(std::int32_t); +bool APS5_VABI scePadIsBlasterConnected(std::int32_t); +bool APS5_VABI scePadIsMoveConnected(std::int32_t); int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); @@ -305,6 +308,14 @@ static void CheckRemoteController(int handle) { Require(!remote); } +static void CheckTrackerDevices(int handle) { + for (const int candidate : {handle, handle + 1}) { + Require(!scePadIsDS4Connected(candidate)); + Require(!scePadIsBlasterConnected(candidate)); + Require(!scePadIsMoveConnected(candidate)); + } +} + static void CheckAccessibilityControllerState(int handle) { std::uint32_t state = 7; Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); @@ -448,6 +459,7 @@ int main() { CheckLatchedPressLifetime(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); + CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); CheckInjectedState(commandPath); PadLightBarParam lightBar{255, 128, 64}; diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index d533ccb116..216ac24734 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -17,6 +17,7 @@ - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false +- [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown From 8f6f1eac86e08d83e9bcfbc0ae1d4db4d69e15c8 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:11:01 +0200 Subject: [PATCH 0954/1360] fix(libSceNgs2): throw for a custom Gain module with an option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceNgs2.native/src/Custom.cpp | 1 + core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp | 2 ++ docs/dev/debt/libSceNgs2.native.md | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index ac34cef5c3..aaad9fdb18 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -71,6 +71,7 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, 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"); for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto moduleId = option.module[m].module_id; + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && option.module[m].option != nullptr) throw std::runtime_error("NGS2: an option for the custom Gain module is not implemented"); if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue; if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented"); const char* name = BuiltInModuleName(moduleId); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 648c1de62f..5b6acd7ad2 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -436,6 +436,8 @@ static void TestCustomRackChecks() { Require(!CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + const ModuleOptionWords gainOption{sizeof(Ngs2CustomModuleOption), {}}; + Require(CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, &gainOption, 0))); const ModuleOptionWords reverb{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {1}}; const ModuleOptionWords reverbOff{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {0}}; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index a6f68fb8a1..753ea906f8 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -19,9 +19,9 @@ - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules and Gain on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input - [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed, nor Gain (0x15) on a custom sampler rack: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) -- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. The extra buffer is ignored, as fma-rc reads it +- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it - [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. From 9587c4467bdc6628567903a540d6a5e117880ad3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:54:41 +0000 Subject: [PATCH 0955/1360] test(agc): share the pipelined fill blocker with its lambda and end each case on an empty pipeline agc_driver_pipelined_fill failed under load with "a satisfied WAIT_REG_MEM skipped an overlapping queued write" (8 of 72 runs, 12 copies pinned to two cores, on main d6eafa54d0). When SatisfiedWaitKeepsUnrelatedQueuedWork returned, the draw pipeline still counted one item: the queue had gone idle before the committer popped its last item, which cleared within half a millisecond. SatisfiedWaitDrainsOverlappingQueuedWork then saw two outstanding items as soon as it queued its blocker, and its wait ended before the WAIT_REG_MEM asked for a drain. Finish() now waits until the draw pipeline is empty, so each case starts on an empty one. The blocker also takes #9647's shape: its state is shared with the queued lambda, and each case waits for the blocker to finish, so no lambda can read a blocker that went out of scope. That shape alone still failed 5 of 72 loaded runs; with the empty-pipeline wait, 144 loaded and 20 unloaded runs passed. The 500 ms sleep in SubmitBehind stays. It lets the queue worker pass the scratch fill's DISPATCH on an empty pipeline and block on the GPU lock the test holds. Without it the worker drains behind the unreleased blocker, and all 72 loaded runs failed with "the queue worker never reached the fill". No counter shows that state to wait on. --- .../tests/execution/PipelinedFill.cpp | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp index 1c13bc169d..c501698a57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -11,6 +11,7 @@ #include <cstdlib> #include <future> #include <iostream> +#include <memory> #include <mutex> #include <string> #include <thread> @@ -75,16 +76,29 @@ void AppendLabel(std::vector<std::uint32_t>& words, std::uint32_t* target, std:: } struct Blocker { - std::atomic<bool> release{false}; + struct State { + std::atomic<bool> release{false}; + std::atomic<bool> done{false}; + }; + std::shared_ptr<State> state = std::make_shared<State>(); std::uint32_t* target = nullptr; - ~Blocker() { release = true; } + ~Blocker() { Release(); } + void Release() { state->release = true; } void Enqueue(std::vector<DrawPipeline::Range> writes = {}) { - DrawPipeline::Queue0().Enqueue([this] { - while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + DrawPipeline::Queue0().Enqueue([state = state, target = target] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); if (target != nullptr) std::atomic_ref<std::uint32_t>(*target).store(1); - std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; }, std::move(writes)); } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } }; void SubmitBehind(Blocker& blocker, std::vector<std::uint32_t>& words) { @@ -107,7 +121,7 @@ void ReleaseWhen(Blocker& blocker, std::size_t reached, std::size_t passed) { Require(now - start < std::chrono::seconds(20), "the queue worker never reached the fill"); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - blocker.release = true; + blocker.Release(); } void Finish() { @@ -118,6 +132,11 @@ void Finish() { std::_Exit(1); } idle.get(); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() != 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the draw pipeline kept an item after the queue went idle"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output)); AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Scratch.data()), sizeof(Scratch)); } @@ -135,6 +154,7 @@ void InputsAfterQueuedLabels() { SubmitBehind(blocker, words); ReleaseWhen(blocker, 4, 5); Finish(); + blocker.Join(); for (std::size_t i = 0; i < Output.size(); ++i) { const auto expected = i < 128 ? 0xababababu : 0xcafebabeu; Require(Output[i] == expected, "a fill read its control or pattern before the queued labels that write them, at " + std::to_string(i)); @@ -156,6 +176,7 @@ void FillIntoFullPipeline() { SubmitBehind(blocker, words); ReleaseWhen(blocker, depth, depth + 1); Finish(); + blocker.Join(); for (std::size_t i = 0; i + 2 < depth; ++i) Require(Labels[i] == i + 1, "a queued label was not stored"); for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i)); } @@ -180,8 +201,9 @@ void SatisfiedWaitKeepsUnrelatedQueuedWork() { while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2; - blocker.release = true; + blocker.Release(); Finish(); + blocker.Join(); Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work"); Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute"); } @@ -211,11 +233,12 @@ void SatisfiedWaitDrainsOverlappingQueuedWork() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore; const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2; - blocker.release = true; + blocker.Release(); Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready, "WAIT_REG_MEM submission did not finish after the overlapping write"); submitted.get(); Finish(); + blocker.Join(); Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write"); Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute"); } From ac1dfaf7ef4a28120b497ce2d1fb10b7c1b5e574 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Wed, 21 Oct 2026 23:30:43 +0100 Subject: [PATCH 0956/1360] feat(libSceJemspace): declare eleven pending exports --- core/libs/prx/libSceJemspace/Export.cpp | 46 +++++++++++++++++++++++-- docs/dev/debt/libSceJemspace.md | 6 ++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 docs/dev/debt/libSceJemspace.md diff --git a/core/libs/prx/libSceJemspace/Export.cpp b/core/libs/prx/libSceJemspace/Export.cpp index d81f4a5e32..9ba406081e 100644 --- a/core/libs/prx/libSceJemspace/Export.cpp +++ b/core/libs/prx/libSceJemspace/Export.cpp @@ -1,10 +1,50 @@ -#include <cstddef> -#include <cstdint> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceJemspaceCalloc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceFree() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceGetStats() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceMalloc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceMallocUsableSize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceMemalign() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspacePosixMemalign() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceRealloc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceReallocalign() { + NotImplemented_nid_no_patch(__func__); +} } diff --git a/docs/dev/debt/libSceJemspace.md b/docs/dev/debt/libSceJemspace.md new file mode 100644 index 0000000000..a4169cf3ca --- /dev/null +++ b/docs/dev/debt/libSceJemspace.md @@ -0,0 +1,6 @@ +# libSceJemspace technical debt + +### Unknown function info + +- The eleven exports in [Export.cpp](../../../core/libs/prx/libSceJemspace/Export.cpp) resolve imports only and throw through `NotImplemented_nid_no_patch`. Each declared name hashes to a defined function NID in the `libSceJemspace` ELF export table. The module firmware version is unverified. The no-parameter declarations do not establish argument layouts or return types. No allocator behavior, memory ownership, allocation failure handling, statistics layout, or console compatibility is implemented or verified. +- Two further defined function exports in [libSceJemspace](../../../core/libs/prx/libSceJemspace/Export.cpp), `s686MsLMwns` and `4aop4GKLZaA`, remain undeclared because their names and signatures are unknown. Their callers still fail import resolution. From 5177cdb9acc03e52aac469630878df15f4717a60 Mon Sep 17 00:00:00 2001 From: Simone <simone@MacBook-Air-di-Simone.local> Date: Thu, 22 Oct 2026 10:00:23 +0200 Subject: [PATCH 0957/1360] feat(libSceVoice): implement mute-all, bitrate and port update Implement sceVoiceSetMuteFlagAll, sceVoiceSetBitRate and sceVoiceUpdatePort on main's port model. They return the not-initialized, port-range and missing-port codes of the other port calls, validate bitrates against the table sceVoiceCreatePort uses and throw where the console's code is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceVoice/Export.cpp | 77 +++++++++++++++++++++------- core/libs/tests/GuestVoice.cpp | 56 ++++++++++++++++++++ docs/dev/debt/libSceVoice.md | 4 +- 3 files changed, 115 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 707d112ea4..19de3a1ec6 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -42,6 +42,7 @@ struct Voice { bool initialized = false; bool started = false; std::int32_t appType = 0; + bool mutedAll = false; std::deque<std::uint32_t> freePorts; std::map<std::uint32_t, Port> ports; std::set<std::pair<std::uint32_t, std::uint32_t>> connections; @@ -65,6 +66,21 @@ Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInPcm || type == PortInVoice; } +bool VoiceFrame(std::int32_t bitrate, std::uint16_t& frameSize, std::uint16_t& frameDuration) { + switch (bitrate) { + case 3850: frameSize = 20; frameDuration = 40; return true; + case 4650: frameSize = 24; frameDuration = 40; return true; + case 5700: frameSize = 15; frameDuration = 20; return true; + case 7300: frameSize = 19; frameDuration = 20; return true; + case 11800: frameSize = 15; frameDuration = 10; return true; + case 12200: frameSize = 16; frameDuration = 10; return true; + case 14400: frameSize = 27; frameDuration = 15; return true; + case 16000: frameSize = 20; frameDuration = 10; return true; + case 22533: frameSize = 43; frameDuration = 15; return true; + default: return false; + } +} + } extern "C" { @@ -121,6 +137,7 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { voice.connections.clear(); voice.started = false; voice.appType = 0; + voice.mutedAll = false; voice.initialized = false; return 0; } @@ -138,18 +155,7 @@ int APS5_VABI sceVoiceCreatePort(std::uint32_t* portId, const VoicePortParam* pa if (type == PortInPcm || type == PortOutPcm) { if (param->pcmaudio.sample_rate != 16000 || param->pcmaudio.data_type != 0) return InvalidArgument; } else if (type == PortInVoice || type == PortOutVoice) { - switch (param->voice.bitrate) { - case 3850: frameSize = 20; frameDuration = 40; break; - case 4650: frameSize = 24; frameDuration = 40; break; - case 5700: frameSize = 15; frameDuration = 20; break; - case 7300: frameSize = 19; frameDuration = 20; break; - case 11800: frameSize = 15; frameDuration = 10; break; - case 12200: frameSize = 16; frameDuration = 10; break; - case 14400: frameSize = 27; frameDuration = 15; break; - case 16000: frameSize = 20; frameDuration = 10; break; - case 22533: frameSize = 43; frameDuration = 15; break; - default: return InvalidArgument; - } + if (!VoiceFrame(param->voice.bitrate, frameSize, frameDuration)) return InvalidArgument; } constexpr auto resourceFull = static_cast<std::int32_t>(0x804e0808u); if (voice.freePorts.empty()) return resourceFull; @@ -338,7 +344,7 @@ int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t portId, bool* muted) { if (!muted) return InvalidArgument; const auto found = voice.ports.find(portId); if (found == voice.ports.end()) return PortNotFound; - *muted = found->second.muted; + *muted = found->second.muted || voice.mutedAll; return 0; } @@ -446,18 +452,51 @@ int APS5_VABI sceVoiceVADSetVersion(void) { return 0; } -int APS5_VABI sceVoiceSetMuteFlagAll(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceSetMuteFlagAll(bool muted) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + voice.mutedAll = muted; return 0; } -int APS5_VABI sceVoiceSetBitRate(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceSetBitRate(std::uint32_t portId, std::uint32_t bitrate) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + auto& port = found->second; + if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + std::uint16_t frameSize = 0; + std::uint16_t frameDuration = 0; + if (bitrate > INT32_MAX || !VoiceFrame(static_cast<std::int32_t>(bitrate), frameSize, frameDuration)) Fail(__func__, "unsupported bitrate " + std::to_string(bitrate)); + port.bitrate = bitrate; + port.frameSize = frameSize; return 0; } -int APS5_VABI sceVoiceUpdatePort(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceUpdatePort(std::uint32_t portId, const VoicePortParam* param) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!param) Fail(__func__, "null port parameters"); + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + auto& port = found->second; + if (param->port_type != port.type) Fail(__func__, "port type " + std::to_string(param->port_type) + " differs from the port's " + std::to_string(port.type)); + std::uint16_t frameSize = port.frameSize; + std::uint16_t frameDuration = 0; + const bool voicePort = port.type == PortInVoice || port.type == PortOutVoice; + if (voicePort && !VoiceFrame(param->voice.bitrate, frameSize, frameDuration)) Fail(__func__, "unsupported bitrate " + std::to_string(param->voice.bitrate)); + port.muted = param->mute != 0; + port.volume = std::isfinite(param->volume) ? param->volume : 0.0f; + if (voicePort) { + port.bitrate = static_cast<std::uint32_t>(param->voice.bitrate); + port.frameSize = frameSize; + } return 0; } diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index cb12940acb..7e1a70ffcf 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -34,6 +34,9 @@ int APS5_VABI sceVoiceResumePortAll(void); int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); int APS5_VABI sceVoiceEnableChat(void); int APS5_VABI sceVoiceDisableChat(void); +int APS5_VABI sceVoiceSetMuteFlagAll(bool); +int APS5_VABI sceVoiceSetBitRate(std::uint32_t, std::uint32_t); +int APS5_VABI sceVoiceUpdatePort(std::uint32_t, const VoicePortParam*); } namespace { @@ -70,6 +73,9 @@ void TestNotInitialized() { Require(std::memcmp(&info, &sentinel, sizeof(info)) == 0); Require(sceVoicePausePortAll() == NotInitialized); Require(sceVoiceResumePortAll() == NotInitialized); + Require(sceVoiceSetMuteFlagAll(true) == NotInitialized); + Require(sceVoiceSetBitRate(0, 16000) == NotInitialized); + Require(sceVoiceUpdatePort(0, nullptr) == NotInitialized); } std::uint32_t CreatePort(std::int32_t type, float volume) { @@ -220,6 +226,56 @@ int main() { Require(sceVoiceReadFromOPort(output, data, &size) == 0 && size == 0); Require(sceVoiceDisconnectIPortFromOPort(input, output) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); + Require(sceVoiceSetMuteFlagAll(true) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); + Require(sceVoiceSetMuteFlagAll(false) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && muted); + + std::uint32_t bitrate = 0; + Require(sceVoiceSetBitRate(input, 7300) == 0); + Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 7300); + VoicePortInfo rateInfo{}; + Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 19); + Require(sceVoiceSetBitRate(output + 1, 7300) == portNotFound); + Require(sceVoiceSetBitRate(128, 7300) == invalidPort); + RequireThrows([&] { sceVoiceSetBitRate(output, 7300); }); + RequireThrows([&] { sceVoiceSetBitRate(input, 7301); }); + RequireThrows([&] { sceVoiceSetBitRate(input, 0x80000000u); }); + Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 7300); + + VoicePortParam update{}; + update.port_type = 2; + update.mute = 1; + update.volume = 0.75f; + update.voice.bitrate = 14400; + Require(sceVoiceUpdatePort(input, &update) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); + Require(sceVoiceGetVolume(input, &volume) == 0 && volume == 0.75f); + Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 14400); + Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 27); + update.voice.bitrate = 1; + RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); + Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 14400); + update.voice.bitrate = 14400; + update.port_type = 3; + RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); + RequireThrows([&] { sceVoiceUpdatePort(input, nullptr); }); + Require(sceVoiceUpdatePort(output + 1, &update) == portNotFound); + Require(sceVoiceUpdatePort(128, &update) == invalidPort); + VoicePortParam pcm{}; + pcm.port_type = 3; + pcm.volume = std::numeric_limits<float>::quiet_NaN(); + Require(sceVoiceUpdatePort(output, &pcm) == 0); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && !muted); + Require(sceVoiceGetVolume(output, &volume) == 0 && volume == 0.0f); + pcm.volume = 0.25f; + pcm.mute = 1; + Require(sceVoiceUpdatePort(output, &pcm) == 0); + + Require(sceVoiceSetMuteFlagAll(true) == 0); + Require(sceVoiceDeletePort(input) == 0); Require(sceVoiceResetPort(input) == portNotFound); Require(sceVoicePausePort(input) == portNotFound); diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 9e8d8fc46e..406b88e228 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -16,9 +16,7 @@ - [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll, sceVoiceSetBitRate, sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), behaviour the Vita's [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L250-L557). They return the not-initialized, port-range and missing-port codes the other port calls return. The mute-all flag makes `sceVoiceGetMuteFlag` report every port muted without changing port flags. A non-voice port, a bitrate `sceVoiceCreatePort` rejects, a null param or a different port type throw, since the console's codes for them are unknown - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. - [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the 48-byte mode/priority/affinity layout and initialization, null, started-state and mode validation are recovered. Mode 1 throws because worker scheduling is not implemented; mode 0 is the native no-op. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. From 106e73669d98c3b50870bd321faf747081e4ef68 Mon Sep 17 00:00:00 2001 From: Simone <simone@MacBook-Air-di-Simone.local> Date: Wed, 21 Oct 2026 10:40:25 +0200 Subject: [PATCH 0958/1360] feat(libkernel_sys): implement the scePthreadBarrier, cpuclockid, concurrency and msync forms Forward scePthreadBarrier*, scePthreadBarrierattr*, scePthreadGetcpuclockid, scePthreadGetconcurrency and sceKernelMsync to libkernel's POSIX implementations and return POSIX errors as SCE kernel errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 8 +- .../prx/libkernel_sys/Export.Discovery.cpp | 74 ++++++++----- core/libs/tests/GuestKernelSysBarrier.cpp | 104 ++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 4 + 4 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysBarrier.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..16cc1b019b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -315,12 +315,18 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_sys_thread_defaults_tests) add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests) set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20) + add_test_executable(guest_kernel_sys_barrier_tests tests/GuestKernelSysBarrier.cpp) + target_include_directories(guest_kernel_sys_barrier_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_barrier_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_barrier_tests) + add_test(NAME guest_kernel_sys_barrier COMMAND guest_kernel_sys_barrier_tests) + set_tests_properties(guest_kernel_sys_barrier PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>) endif() if(WIN32) - set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_barrier PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") endif() if(APPLE) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 702eff446b..c07286436a 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1,5 +1,29 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include <cstddef> + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI pthread_barrier_init_nid_postfix(void** barrier, void* const* attr, unsigned count); +int APS5_VABI pthread_barrier_wait_nid_postfix(void** barrier); +int APS5_VABI pthread_barrier_destroy_nid_postfix(void** barrier); +int APS5_VABI pthread_barrierattr_init_nid_postfix(void** attr); +int APS5_VABI pthread_barrierattr_destroy_nid_postfix(void** attr); +int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(void* const* attr, int* pshared); +int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(void** attr, int pshared); +int APS5_VABI pthread_getcpuclockid_nid_postfix(void* thread, int* clockId); +int APS5_VABI pthread_getconcurrency_nid_postfix(); +int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags); +} + +namespace { + +int SceResult(int error) { + return error == 0 ? 0 : SceKernelError(error); +} + +} extern "C" { @@ -1733,9 +1757,8 @@ int APS5_VABI sceKernelMlockall(void) { return 0; } -int APS5_VABI sceKernelMsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelMsync(void* address, std::size_t length, int flags) { + return msync_nid_postfix(address, length, flags) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); } int APS5_VABI sceKernelNormalizePath(void) { @@ -2303,39 +2326,34 @@ int APS5_VABI scePthreadAttrSetscope(void) { return 0; } -int APS5_VABI scePthreadBarrierDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierDestroy(void** barrier) { + return SceResult(pthread_barrier_destroy_nid_postfix(barrier)); } -int APS5_VABI scePthreadBarrierInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierInit(void** barrier, void* const* attr, unsigned count, const char* name) { + (void)name; + return SceResult(pthread_barrier_init_nid_postfix(barrier, attr, count)); } -int APS5_VABI scePthreadBarrierWait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierWait(void** barrier) { + const int result = pthread_barrier_wait_nid_postfix(barrier); + return result > 0 ? SceKernelError(result) : result; } -int APS5_VABI scePthreadBarrierattrDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrDestroy(void** attr) { + return SceResult(pthread_barrierattr_destroy_nid_postfix(attr)); } -int APS5_VABI scePthreadBarrierattrGetpshared(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrGetpshared(void* const* attr, int* pshared) { + return SceResult(pthread_barrierattr_getpshared_nid_postfix(attr, pshared)); } -int APS5_VABI scePthreadBarrierattrInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrInit(void** attr) { + return SceResult(pthread_barrierattr_init_nid_postfix(attr)); } -int APS5_VABI scePthreadBarrierattrSetpshared(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadBarrierattrSetpshared(void** attr, int pshared) { + return SceResult(pthread_barrierattr_setpshared_nid_postfix(attr, pshared)); } int APS5_VABI scePthreadCondattrGetclock(void) { @@ -2344,13 +2362,11 @@ int APS5_VABI scePthreadCondattrGetclock(void) { } int APS5_VABI scePthreadGetconcurrency(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return pthread_getconcurrency_nid_postfix(); } -int APS5_VABI scePthreadGetcpuclockid(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadGetcpuclockid(void* thread, int* clockId) { + return SceResult(pthread_getcpuclockid_nid_postfix(thread, clockId)); } int APS5_VABI scePthreadMain(void) { diff --git a/core/libs/tests/GuestKernelSysBarrier.cpp b/core/libs/tests/GuestKernelSysBarrier.cpp new file mode 100644 index 0000000000..6969511d3a --- /dev/null +++ b/core/libs/tests/GuestKernelSysBarrier.cpp @@ -0,0 +1,104 @@ +#include "SceTypes.hpp" +#include <array> +#include <atomic> +#include <cstddef> +#include <cstdint> +#include <cstdlib> + +struct GuestBarrier; +struct GuestBarrierattr; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadBarrierattrInit(GuestBarrierattr** attr); +int APS5_VABI scePthreadBarrierattrDestroy(GuestBarrierattr** attr); +int APS5_VABI scePthreadBarrierattrGetpshared(GuestBarrierattr* const* attr, int* pshared); +int APS5_VABI scePthreadBarrierattrSetpshared(GuestBarrierattr** attr, int pshared); +int APS5_VABI scePthreadBarrierInit(GuestBarrier** barrier, GuestBarrierattr* const* attr, unsigned count, const char* name); +int APS5_VABI scePthreadBarrierWait(GuestBarrier** barrier); +int APS5_VABI scePthreadBarrierDestroy(GuestBarrier** barrier); +int APS5_VABI scePthreadGetcpuclockid(Pthread thread, int* clockId); +int APS5_VABI pthread_getcpuclockid_nid_postfix(Pthread thread, int* clockId); +int APS5_VABI scePthreadGetconcurrency(); +int APS5_VABI pthread_setconcurrency_nid_postfix(int level); +int APS5_VABI sceKernelMsync(void* address, std::size_t length, int flags); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast<int>(0x8002000E); +static constexpr int SCE_KERNEL_ERROR_ENOMEM = static_cast<int>(0x8002000C); +static constexpr int PROCESS_PRIVATE = 0; +static constexpr int PROCESS_SHARED = 1; +static constexpr int BARRIER_SERIAL_THREAD = -1; +static constexpr int MS_ASYNC = 0x1; +static constexpr int MS_INVALIDATE = 0x2; +static constexpr unsigned WAITERS = 4; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Context { + GuestBarrier* barrier = nullptr; + std::atomic<int> serial{0}; + std::atomic<int> released{0}; +}; + +static void* APS5_VABI Waiter(void* arg) { + auto& context = *static_cast<Context*>(arg); + const int result = scePthreadBarrierWait(&context.barrier); + if (result == BARRIER_SERIAL_THREAD) ++context.serial; + else if (result == 0) ++context.released; + else std::abort(); + return nullptr; +} + +int main() { + GuestBarrierattr* attr = nullptr; + Require(scePthreadBarrierattrInit(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrInit(&attr) == 0 && attr != nullptr); + int pshared = -1; + Require(scePthreadBarrierattrGetpshared(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Require(scePthreadBarrierattrSetpshared(&attr, PROCESS_SHARED) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrSetpshared(&attr, PROCESS_PRIVATE) == 0); + + Context context; + Require(scePthreadBarrierInit(&context.barrier, &attr, 0, "zero") == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierInit(&context.barrier, &attr, 0x80000000u, "over") == SCE_KERNEL_ERROR_EINVAL && context.barrier == nullptr); + Require(scePthreadBarrierInit(nullptr, &attr, WAITERS, "null") == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierInit(&context.barrier, &attr, WAITERS, "barrier") == 0 && context.barrier != nullptr); + Require(scePthreadBarrierattrDestroy(&attr) == 0 && attr == nullptr); + Require(scePthreadBarrierattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + + for (int round = 0; round < 2; ++round) { + context.serial = 0; + context.released = 0; + std::array<Pthread, WAITERS> threads{}; + for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, Waiter, &context, nullptr) == 0); + for (auto& thread : threads) Require(scePthreadJoin(thread, nullptr) == 0); + Require(context.serial == 1 && context.released == static_cast<int>(WAITERS) - 1); + } + Require(scePthreadBarrierDestroy(&context.barrier) == 0 && context.barrier == nullptr); + Require(scePthreadBarrierDestroy(&context.barrier) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierWait(nullptr) == SCE_KERNEL_ERROR_EINVAL); + + GuestBarrier* single = nullptr; + Require(scePthreadBarrierInit(&single, nullptr, 1, nullptr) == 0); + Require(scePthreadBarrierWait(&single) == BARRIER_SERIAL_THREAD); + Require(scePthreadBarrierDestroy(&single) == 0); + + int clock = 0; + int posixClock = 0; + Require(scePthreadGetcpuclockid(scePthreadSelf(), &clock) == 0); + Require(pthread_getcpuclockid_nid_postfix(scePthreadSelf(), &posixClock) == 0 && clock == posixClock); + Require(scePthreadGetcpuclockid(scePthreadSelf(), nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(scePthreadGetcpuclockid(nullptr, &clock) == SCE_KERNEL_ERROR_EINVAL); + + Require(pthread_setconcurrency_nid_postfix(3) == 0 && scePthreadGetconcurrency() == 3); + Require(pthread_setconcurrency_nid_postfix(0) == 0 && scePthreadGetconcurrency() == 0); + + auto* unmapped = reinterpret_cast<void*>(static_cast<std::uintptr_t>(0x4000)); + Require(sceKernelMsync(unmapped, 0x4000, 0) == SCE_KERNEL_ERROR_ENOMEM); + Require(sceKernelMsync(unmapped, 0x4000, MS_ASYNC | MS_INVALIDATE) == SCE_KERNEL_ERROR_EINVAL); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 0d634c1296..4c333c4b78 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -19,3 +19,7 @@ - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 + +### Unknown function info + +- [scePthreadBarrier*, scePthreadBarrierattr*, scePthreadGetcpuclockid, scePthreadGetconcurrency and sceKernelMsync](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_barrier*`, `pthread_getcpuclockid`, `pthread_getconcurrency` and `msync` and turn a POSIX error into `0x80020000 | errno`; that the console's `sce` forms do the same is not measured. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); `scePthreadBarrierInit` ignores its name, and `scePthreadBarrierWait` returns -1 to the serial thread, both as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/barrier.cpp) (PS4). `scePthreadGetconcurrency` returns the level unchanged From bf0806ecd6066ffec064e9303072df1407c3c006 Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:35:44 +0200 Subject: [PATCH 0959/1360] fix(agc): retire an unwritten native multisample depth surface on guest access A guest read over the depth or HTILE range of a native multisample depth surface threw even when the host had not written the surface since its import. Its samples are then still the clear value the import took from the guest HTILE, so the guest bytes already describe it. Retire such a surface and its guards instead, and keep the refusal for a surface the host wrote: draws with depth or stencil writes, depth/stencil copies and storage write-back. Onimusha: Way of the Sword (PPSA27836) imports a 4-sample D32 target, draws into it once with depth tests only and no color target, then reads the memory back as a structured buffer in a compute dispatch. Unblocks PPSA27836. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 40 +++++++++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 10 ++- .../execution/NativeMultisampleDepth.cpp | 79 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 124 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f507ddd3a1..538d19aec6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -29,6 +29,7 @@ struct DepthPlaneStoreStatistics { std::uint64_t bytes = 0; }; DepthPlaneStoreStatistics DepthPlaneStoreCounts(); +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSurfacesSerial(); std::uint64_t DepthSurfacesReleased(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 9090b5952b..7b2d9ace3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -58,6 +58,7 @@ struct DepthGuard { std::size_t bytes; std::shared_ptr<std::atomic<bool>> active; std::vector<GuestMemory::UnchangedQuery> live; + std::shared_ptr<std::atomic<bool>> hostWritten; }; std::shared_mutex& depthGuardMutex() { @@ -491,6 +492,7 @@ public: void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { ForgetClear(aspect); + NoteHostWrite(); auto* recorder = RecorderFor(context); std::unique_ptr<CommandBatch> batch; if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context); @@ -540,7 +542,12 @@ public: return std::any_of(textures.begin(), textures.end(), [](const auto& entry) { return entry.second.use_count() > 1; }); } + void NoteHostWrite() { + hostWritten->store(true, std::memory_order_release); + } + std::uint64_t written = ++depthWrites; + std::shared_ptr<std::atomic<bool>> hostWritten = std::make_shared<std::atomic<bool>>(false); bool retired = false; bool Overlaps(std::uint64_t address, std::uint64_t bytes) const { @@ -690,6 +697,7 @@ public: if (plane.storage.get() == &candidate) { if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; + NoteHostWrite(); return true; } if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue; @@ -707,6 +715,7 @@ public: const auto source = std::const_pointer_cast<StorageTexture>(candidate.shared_from_this()); copyPlane(plane, source, stencil, false); written = ++depthWrites; + NoteHostWrite(); return true; } return false; @@ -1826,8 +1835,28 @@ std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32 void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes) { if (bytes == 0u || !anyDepthGuard().load(std::memory_order_acquire)) return; Require(address <= std::numeric_limits<std::uint64_t>::max() - bytes, "native depth read range overflows"); - std::shared_lock lock(depthGuardMutex()); - for (const auto& range : depthGuards()) Require(address >= range.address + range.bytes || range.address >= address + bytes, "sample-preserving guest publication of native multisample depth is not implemented"); + const auto overlaps = [&](const DepthGuard& range) { return address < range.address + range.bytes && range.address < address + bytes; }; + { + std::shared_lock lock(depthGuardMutex()); + if (std::ranges::none_of(depthGuards(), overlaps)) return; + } + std::unique_lock lock(depthGuardMutex()); + for (const auto& range : depthGuards()) { + if (overlaps(range)) Require(!range.hostWritten->load(std::memory_order_acquire), "sample-preserving guest publication of native multisample depth is not implemented"); + } + for (const auto& range : depthGuards()) { + if (overlaps(range)) range.active->store(false, std::memory_order_release); + } + std::erase_if(depthGuards(), [](const auto& range) { return !range.active->load(std::memory_order_acquire); }); + anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release); +} + +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target) { + if (target.samples == 1u || !anyDepthGuard().load(std::memory_order_acquire)) return; + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && sameSurface(surface->target, target)) surface->NoteHostWrite(); + } } bool HasResidentDepthGuards_nid_no_patch() { @@ -1936,7 +1965,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, auto& fresh = *surfaces().back(); if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) { std::unique_lock guardLock(depthGuardMutex()); - for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}}); + for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}, fresh.hostWritten}); anyDepthGuard().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); fresh.samplingActive = true; @@ -2454,7 +2483,10 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue; const auto slices = layeredSlices(context, *entry); entry->Copy(slices, false); - for (const auto& slice : slices) slice.surface->written = ++depthWrites; + for (const auto& slice : slices) { + slice.surface->written = ++depthWrites; + slice.surface->NoteHostWrite(); + } return true; } for (const auto& surface : surfaces()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 90c38e18c1..86262020fb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -81,6 +81,10 @@ GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { namespace { +bool writesDepthTarget(const State& state) { + return state.depthWrite || (state.stencilTest && (state.stencilFront.writeMask != 0u || state.stencilBack.writeMask != 0u)); +} + std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) { if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes); return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes); @@ -2317,7 +2321,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique<RenderTarget>(context, color, state.blends.at(color.exportIndex).blendEnable != 0); targetViews.push_back(binding.target->View()); } - if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); + if (state.depth) { + targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); + if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); + } timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -2899,6 +2906,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (recipe.hasDepth) { timer.phase(PhaseSetup); const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); + if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); timer.phase(PhaseReadTarget); if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index cda5b9280a..0ce3c64bca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -548,7 +548,8 @@ void CheckDrawFeedback() { Require(device != nullptr, "native depth draw feedback requires Vulkan"); constexpr VkExtent2D extent{5u, 3u}; constexpr std::uint32_t samples = 4u; - TargetMemory memory(static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples))), metadata(65536u); + const auto depthBytes = static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(depthBytes), metadata(65536u); std::memset(metadata.Data(), 0, 65536u); DepthTarget depth{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples}; depth.depthSwizzle = 24u; @@ -581,6 +582,9 @@ void CheckDrawFeedback() { const std::array<CompiledShader, 2> initial{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &producer, pixelOffset}}}; device->Draw(state, draw, initial); device->WaitIdle(); + std::array<std::byte, 1> probe{}; + TargetRefuses([&] { AgcDriver::GuestMemory::Read(depth.address, probe); }, "sample-preserving guest publication"); + Require(DepthSurfaceAt(depth.address), "a refused guest read retired a native depth surface a draw wrote"); reader.spirv = DepthFeedbackFragmentShader(binding, true); reader.variantId = 3u; const auto words = DepthSampleWords(depth); @@ -597,7 +601,22 @@ void CheckDrawFeedback() { const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &reader, pixelOffset}}}; TargetRefuses([&] { device->Draw(state, draw, shaders); }, "sampling a native multisampled depth target the draw attaches"); device->WaitIdle(); - std::puts("Native depth feedback refused through the draw resource path"); + TargetMemory testedMemory(depthBytes), testedMetadata(65536u); + std::memset(testedMetadata.Data(), 0, 65536u); + const auto guestByte = [](std::size_t offset) { return static_cast<std::byte>(offset * 7u + 3u); }; + for (std::size_t offset = 0u; offset < depthBytes; ++offset) testedMemory.Data()[offset] = guestByte(offset); + auto tested = depth; + tested.address = testedMemory.Address(); + tested.htileAddress = testedMetadata.Address(); + state.depth = tested; + state.depthWrite = false; + device->Draw(state, draw, initial); + device->WaitIdle(); + Require(DepthSurfaceAt(tested.address), "a depth-test-only draw left no resident native depth surface"); + AgcDriver::GuestMemory::Read(tested.address + depthBytes - 1u, probe); + Require(probe[0] == guestByte(depthBytes - 1u), "a guest read after a depth-test-only draw did not return its guest bytes"); + Require(!DepthSurfaceAt(tested.address), "a guest read left a native depth surface only depth-tested resident"); + std::puts("Native depth feedback refused through the draw resource path; a written surface refused and a depth-tested one retired on guest read"); } enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap }; @@ -679,6 +698,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, } const auto view = DepthSurfaceView(context, target); Require(DepthSurfaceView(context, target) == view, "native depth attachment did not reuse its image"); + NoteDepthSurfaceWrite(context, target); std::array<std::byte, 1> beforeSampling{}; TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address + bytes - 1u, beforeSampling); }, "sample-preserving guest publication"); TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); }, "sample-preserving guest publication"); @@ -843,6 +863,59 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast<std::uint32_t>(format), fragments, one, iterate256); } +void CheckUnwrittenGuestAccess(Context context, VkFormat format, std::uint32_t fragments) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + const auto metadataBytes = static_cast<std::size_t>(HtileMetadataBytes(extent)); + TargetMemory memory(bytes), metadata(65536u), unrelated(65536u); + const auto guestByte = [](std::size_t offset) { return static_cast<std::byte>(offset * 7u + 3u); }; + for (std::size_t offset = 0u; offset < bytes; ++offset) memory.Data()[offset] = guestByte(offset); + std::memset(metadata.Data(), 0, metadataBytes); + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = target.htileDepthOnly = true; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + const auto resident = [&] { return HasResidentDepthGuards_nid_no_patch() && DepthSurfaceAt(target.address); }; + auto view = DepthSurfaceView(context, target); + Require(view != VK_NULL_HANDLE && resident(), "unwritten native depth fixture has no resident owner"); + auto otherDevice = context; + otherDevice.device = VK_NULL_HANDLE; + NoteDepthSurfaceWrite(otherDevice, target); + auto unattached = target; + unattached.address += 65536u; + NoteDepthSurfaceWrite(context, unattached); + std::array<std::byte, 1> probe{}; + AgcDriver::GuestMemory::Read(unrelated.Address(), probe); + Require(resident(), "an unrelated guest read retired a native depth surface"); + for (const bool readMetadata : {false, true}) { + if (readMetadata) RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); + else { + AgcDriver::GuestMemory::Read(target.address + bytes - 1u, probe); + Require(probe[0] == guestByte(bytes - 1u), "a guest read over an unwritten native depth surface did not return its guest bytes"); + } + Require(!resident(), "a guest access left an unwritten native depth surface resident"); + RequireNoResidentDepthRead_nid_no_patch(target.address, bytes); + RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, metadataBytes); + recorder.Sync(); + const auto rebound = DepthSurfaceView(context, target); + Require(rebound != VK_NULL_HANDLE && rebound != view && resident(), "rebinding after a guest access did not import a new native depth surface"); + view = rebound; + } + NoteDepthSurfaceWrite(context, target); + TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address, probe); }, "sample-preserving guest publication"); + TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress, 1u, "a DMA copy"); }, "sample-preserving guest publication"); + Require(resident(), "a refused guest access retired a written native depth surface"); + for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == guestByte(offset), "retiring an unwritten native depth surface changed its guest bytes"); + Require(std::all_of(metadata.Data(), metadata.Data() + metadataBytes, [](std::byte value) { return value == std::byte{0}; }), "retiring an unwritten native depth surface changed its HTILE"); + std::printf("Native depth format=%u samples=%u: guest access retired the unwritten surface and refused the written one\n", static_cast<std::uint32_t>(format), fragments); +} + void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); @@ -1031,6 +1104,7 @@ void CheckRetirement(Context context, VkFormat format, std::uint32_t samples, st const auto words = DepthSampleWords(target); const auto resource = DecodeTextureResource(words); const auto firstView = DepthSurfaceView(context, target); + NoteDepthSurfaceWrite(context, target); auto firstTexture = DepthSurfaceTexture(context, words, resource, VkComponentMapping{}); Require(firstTexture != nullptr, "retirement fixture has no native sampled view"); ColorTarget color{colorMemory.Address(), extent, VK_FORMAT_R8G8B8A8_UNORM, extent.width * extent.height * 4u, 0u}; @@ -1179,6 +1253,7 @@ int main(int argc, char** argv) { for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, iterate256); ++checked; } } CheckHtileRouting(context, format, samples); + CheckUnwrittenGuestAccess(context, format, samples); } } CheckEligibility(context); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index baec5ded56..74ea0b3df2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -23,7 +23,7 @@ - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. - [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. -- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). +- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. A guest read over the depth or HTILE range of a surface the host has not written since import (draws with depth or stencil writes, depth/stencil copies, storage write-back; not clears after a proven HTILE clear) retires it instead of throwing; that a draw without those writes leaves the console's Z planes and HTILE unchanged is inferred, not measured. Guest publication of a written surface, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [sceAgcDriver retail constant results](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the debug, capture, GPU printf, register access, GPU reset, suspend point and user data exports taken from the retail `libSceAgcDriver.sprx`, and `sceAgcDriverRegisterOwnerForSystem`, `sceAgcDriverInitResourceRegistrationInterface` and `sceAgcDriverSetResourceRegistrationInfo` in [Registration.cpp](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp), are declared without parameters: the console bodies return a constant without reading any argument, so their signatures are unknown. `sceAgcDriverAllocateToolMemoryForGpuReset` and `sceAgcDriverGetAllocatedToolMemoryForGpuReset` read only the output pointer (fourth and second argument, a 12-byte range the console zeroes); the other arguments are declared as `uint64_t`. - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice From e7ea986aa7ebf7c7982dbcf07a95ff03e365b964 Mon Sep 17 00:00:00 2001 From: Simone <simone@MacBook-Air-di-Simone.local> Date: Thu, 22 Oct 2026 10:03:09 +0200 Subject: [PATCH 0960/1360] test(json2): build a decimal-comma locale for the numeric locale test Linux runners have no decimal-comma locale, so guest_json_numeric_locale skipped there. Build xx_COMMA from a charmap and an LC_NUMERIC source shipped with the test as a CTest fixture, point the test at it with LOCPATH and try it first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceJson2/CMakeLists.txt | 9 ++ core/libs/tests/GuestJson.cpp | 2 +- core/libs/tests/numeric-locale/comma.charmap | 135 +++++++++++++++++++ core/libs/tests/numeric-locale/comma.locale | 5 + 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/numeric-locale/comma.charmap create mode 100644 core/libs/tests/numeric-locale/comma.locale diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index edfcf10e10..dbc704366f 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -10,6 +10,15 @@ if(BUILD_TESTING) add_test(NAME guest_json COMMAND guest_json_tests) add_test(NAME guest_json_numeric_locale COMMAND guest_json_tests --numeric-locale) set_tests_properties(guest_json_numeric_locale PROPERTIES SKIP_RETURN_CODE 77) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(numeric_locale_dir ${CMAKE_CURRENT_BINARY_DIR}/numeric-locale) + set(numeric_locale_data ${LIBS_INCLUDE_DIR}/tests/numeric-locale) + add_test(NAME guest_json_numeric_locale_setup COMMAND sh -c + "rm -rf '${numeric_locale_dir}' && mkdir -p '${numeric_locale_dir}' && I18NPATH='${numeric_locale_dir}' localedef -c -f '${numeric_locale_data}/comma.charmap' -i '${numeric_locale_data}/comma.locale' '${numeric_locale_dir}/xx_COMMA'; test -f '${numeric_locale_dir}/xx_COMMA/LC_NUMERIC'") + set_tests_properties(guest_json_numeric_locale_setup PROPERTIES FIXTURES_SETUP json_numeric_locale) + set_tests_properties(guest_json_numeric_locale PROPERTIES FIXTURES_REQUIRED json_numeric_locale + ENVIRONMENT "LOCPATH=${numeric_locale_dir}") + endif() if(WIN32) set_tests_properties(guest_json guest_json_numeric_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceJson2>") diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 05c455b6c3..506e7da1a2 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -1380,7 +1380,7 @@ static void ValueOperatorBool() { static int NumericLocale() { const std::string previous = std::setlocale(LC_NUMERIC, nullptr); const char* selected = nullptr; - for (const char* name : {"de_DE.UTF-8", "de_DE.utf8", "de-DE", "German_Germany.1252", "fr_FR.UTF-8"}) { + for (const char* name : {"xx_COMMA", "de_DE.UTF-8", "de_DE.utf8", "de-DE", "German_Germany.1252", "fr_FR.UTF-8"}) { if (std::setlocale(LC_NUMERIC, name) && std::strcmp(std::localeconv()->decimal_point, ",") == 0) { selected = name; break; diff --git a/core/libs/tests/numeric-locale/comma.charmap b/core/libs/tests/numeric-locale/comma.charmap new file mode 100644 index 0000000000..cbfea3c784 --- /dev/null +++ b/core/libs/tests/numeric-locale/comma.charmap @@ -0,0 +1,135 @@ +<code_set_name> ANYPS5_COMMA +<comment_char> % +<escape_char> / +<mb_cur_max> 1 +<mb_cur_min> 1 +CHARMAP +<U0000> /x00 +<U0001> /x01 +<U0002> /x02 +<U0003> /x03 +<U0004> /x04 +<U0005> /x05 +<U0006> /x06 +<U0007> /x07 +<U0008> /x08 +<U0009> /x09 +<U000A> /x0a +<U000B> /x0b +<U000C> /x0c +<U000D> /x0d +<U000E> /x0e +<U000F> /x0f +<U0010> /x10 +<U0011> /x11 +<U0012> /x12 +<U0013> /x13 +<U0014> /x14 +<U0015> /x15 +<U0016> /x16 +<U0017> /x17 +<U0018> /x18 +<U0019> /x19 +<U001A> /x1a +<U001B> /x1b +<U001C> /x1c +<U001D> /x1d +<U001E> /x1e +<U001F> /x1f +<U0020> /x20 +<U0021> /x21 +<U0022> /x22 +<U0023> /x23 +<U0024> /x24 +<U0025> /x25 +<U0026> /x26 +<U0027> /x27 +<U0028> /x28 +<U0029> /x29 +<U002A> /x2a +<U002B> /x2b +<U002C> /x2c +<U002D> /x2d +<U002E> /x2e +<U002F> /x2f +<U0030> /x30 +<U0031> /x31 +<U0032> /x32 +<U0033> /x33 +<U0034> /x34 +<U0035> /x35 +<U0036> /x36 +<U0037> /x37 +<U0038> /x38 +<U0039> /x39 +<U003A> /x3a +<U003B> /x3b +<U003C> /x3c +<U003D> /x3d +<U003E> /x3e +<U003F> /x3f +<U0040> /x40 +<U0041> /x41 +<U0042> /x42 +<U0043> /x43 +<U0044> /x44 +<U0045> /x45 +<U0046> /x46 +<U0047> /x47 +<U0048> /x48 +<U0049> /x49 +<U004A> /x4a +<U004B> /x4b +<U004C> /x4c +<U004D> /x4d +<U004E> /x4e +<U004F> /x4f +<U0050> /x50 +<U0051> /x51 +<U0052> /x52 +<U0053> /x53 +<U0054> /x54 +<U0055> /x55 +<U0056> /x56 +<U0057> /x57 +<U0058> /x58 +<U0059> /x59 +<U005A> /x5a +<U005B> /x5b +<U005C> /x5c +<U005D> /x5d +<U005E> /x5e +<U005F> /x5f +<U0060> /x60 +<U0061> /x61 +<U0062> /x62 +<U0063> /x63 +<U0064> /x64 +<U0065> /x65 +<U0066> /x66 +<U0067> /x67 +<U0068> /x68 +<U0069> /x69 +<U006A> /x6a +<U006B> /x6b +<U006C> /x6c +<U006D> /x6d +<U006E> /x6e +<U006F> /x6f +<U0070> /x70 +<U0071> /x71 +<U0072> /x72 +<U0073> /x73 +<U0074> /x74 +<U0075> /x75 +<U0076> /x76 +<U0077> /x77 +<U0078> /x78 +<U0079> /x79 +<U007A> /x7a +<U007B> /x7b +<U007C> /x7c +<U007D> /x7d +<U007E> /x7e +<U007F> /x7f +END CHARMAP diff --git a/core/libs/tests/numeric-locale/comma.locale b/core/libs/tests/numeric-locale/comma.locale new file mode 100644 index 0000000000..3207685c62 --- /dev/null +++ b/core/libs/tests/numeric-locale/comma.locale @@ -0,0 +1,5 @@ +LC_NUMERIC +decimal_point "<U002C>" +thousands_sep "" +grouping -1 +END LC_NUMERIC From b9e5dc160f148ddd5724b6c055e5c129c152ba2f Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:17:00 +0500 Subject: [PATCH 0961/1360] feat(libScePosixForWebKit): forward the --wrap exports with implemented targets __wrap_access, __wrap_getcwd and __wrap_getenv forward to libc; __wrap_clock_gettime, __wrap_lstat and __wrap_mmap forward to libkernel. --- core/libs/prx/libScePosixForWebKit/Export.cpp | 40 ++++++------ core/libs/tests/GuestPosixFilesystem.cpp | 61 +++++++++++++++++++ docs/dev/debt/libScePosixForWebKit.md | 4 +- 3 files changed, 86 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index ca47a2ecf7..c02c94fd7a 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,3 +1,4 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostEntropy.hpp" #include "prx/libc/include/General.hpp" @@ -7,6 +8,15 @@ #include <cstring> #include <stdexcept> +extern "C" { +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI clock_gettime_nid_postfix(int, KernelTimespec*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +char* APS5_VABI getenv_nid_postfix(const char*); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +} + namespace { constexpr int FnmNomatch = 1; @@ -181,14 +191,12 @@ std::uint32_t APS5_VABI arc4random_nid_postfix() { return value; } -int APS5_VABI __wrap_access() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI __wrap_access(const char* path, int mode) { + return access_nid_postfix(path, mode); } -int APS5_VABI __wrap_clock_gettime() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI __wrap_clock_gettime(int clockId, KernelTimespec* tp) { + return clock_gettime_nid_postfix(clockId, tp); } int APS5_VABI __wrap_fchown() { @@ -196,14 +204,12 @@ int APS5_VABI __wrap_fchown() { return 0; } -int APS5_VABI __wrap_getcwd() { - NotImplemented_nid_no_patch(__func__); - return 0; +char* APS5_VABI __wrap_getcwd(char* buffer, std::size_t size) { + return getcwd_nid_postfix(buffer, size); } -int APS5_VABI __wrap_getenv() { - NotImplemented_nid_no_patch(__func__); - return 0; +char* APS5_VABI __wrap_getenv(const char* name) { + return getenv_nid_postfix(name); } int APS5_VABI __wrap_link() { @@ -211,14 +217,12 @@ int APS5_VABI __wrap_link() { return 0; } -int APS5_VABI __wrap_lstat() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI __wrap_lstat(const char* path, FileStat* sb) { + return lstat_nid_postfix(path, sb); } -int APS5_VABI __wrap_mmap() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI __wrap_mmap(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) { + return mmap_nid_postfix(address, length, protection, flags, descriptor, offset); } int APS5_VABI __wrap_readlink() { diff --git a/core/libs/tests/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp index 0e3326d3d0..fbe8e7566e 100644 --- a/core/libs/tests/GuestPosixFilesystem.cpp +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" #include <array> #include <cstdio> #include <cstdlib> @@ -27,6 +28,20 @@ int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI __wrap_access(const char*, int); +int APS5_VABI clock_gettime_nid_postfix(int, KernelTimespec*); +int APS5_VABI __wrap_clock_gettime(int, KernelTimespec*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +char* APS5_VABI __wrap_getcwd(char*, std::size_t); +char* APS5_VABI getenv_nid_postfix(const char*); +char* APS5_VABI __wrap_getenv(const char*); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); +int APS5_VABI __wrap_lstat(const char*, FileStat*); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +void* APS5_VABI __wrap_mmap(void*, std::size_t, int, int, int, std::int64_t); +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -134,6 +149,52 @@ int main() { Require(isatty_nid_postfix(slave) == 1 && *__error_nid_postfix() == 123); Require(::close(slave) == 0 && ::close(terminal) == 0); #endif + Require(setenv_nid_postfix("WEBKIT_WRAP_SET", "set", 1) == 0); + char* const variable = getenv_nid_postfix("WEBKIT_WRAP_SET"); + Require(variable != nullptr && std::strcmp(variable, "set") == 0 && __wrap_getenv("WEBKIT_WRAP_SET") == variable); + Require(getenv_nid_postfix("WEBKIT_WRAP_UNSET") == nullptr && __wrap_getenv("WEBKIT_WRAP_UNSET") == nullptr); + Require(access_nid_postfix(pattern.c_str(), 0) == 0 && __wrap_access(pattern.c_str(), 0) == 0); + Require(access_nid_postfix("/anyps5-posix-test/absent", 0) == -1); + const int accessError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(__wrap_access("/anyps5-posix-test/absent", 0) == -1 && *__error_nid_postfix() == accessError && accessError == 2); + std::array<char, 1024> expectedDirectory{}; + std::array<char, 1024> wrappedDirectory{}; + Require(getcwd_nid_postfix(expectedDirectory.data(), expectedDirectory.size()) == expectedDirectory.data()); + Require(__wrap_getcwd(wrappedDirectory.data(), wrappedDirectory.size()) == wrappedDirectory.data()); + Require(std::strcmp(expectedDirectory.data(), wrappedDirectory.data()) == 0); + Require(getcwd_nid_postfix(wrappedDirectory.data(), 1) == nullptr); + const int directoryError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(__wrap_getcwd(wrappedDirectory.data(), 1) == nullptr && *__error_nid_postfix() == directoryError && directoryError == 34); + FileStat expectedStatus{}; + FileStat wrappedStatus{}; + Require(lstat_nid_postfix(pattern.c_str(), &expectedStatus) == 0 && __wrap_lstat(pattern.c_str(), &wrappedStatus) == 0); + Require((wrappedStatus.st_mode & 0170000) == 0100000 && wrappedStatus.st_size == static_cast<std::int64_t>(sizeof(payload))); + Require(std::memcmp(&expectedStatus, &wrappedStatus, sizeof(FileStat)) == 0); + const auto notAfter = [](const KernelTimespec& earlier, const KernelTimespec& later) { + return earlier.tv_sec < later.tv_sec || (earlier.tv_sec == later.tv_sec && earlier.tv_nsec <= later.tv_nsec); + }; + KernelTimespec before{}; + KernelTimespec during{}; + KernelTimespec after{}; + Require(clock_gettime_nid_postfix(4, &before) == 0 && __wrap_clock_gettime(4, &during) == 0 && clock_gettime_nid_postfix(4, &after) == 0); + Require(notAfter(before, during) && notAfter(during, after)); + Require(clock_gettime_nid_postfix(4, nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(__wrap_clock_gettime(4, nullptr) == -1 && *__error_nid_postfix() == 14); + constexpr std::size_t page = 0x4000; + void* const failed = reinterpret_cast<void*>(static_cast<std::uintptr_t>(-1)); + void* const mapped = __wrap_mmap(nullptr, page, 3, 0x1002, -1, 0); + Require(mapped != failed); + auto* const bytes = static_cast<volatile unsigned char*>(mapped); + bytes[0] = 0x5a; + bytes[page - 1] = 0xa5; + Require(bytes[0] == 0x5a && bytes[page - 1] == 0xa5); + Require(munmap_nid_postfix(mapped, page) == 0); + Require(mmap_nid_postfix(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(__wrap_mmap(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); RemovePathAlias_nid_no_patch("/anyps5-posix-test"); Require(std::filesystem::remove_all(root) > 0); } diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index 681a24a3d5..39e20c0542 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -3,7 +3,8 @@ ### Unknown function info - [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver -- [__wrap_access, __wrap_clock_gettime, __wrap_fchown, __wrap_getcwd, __wrap_getenv, __wrap_link, __wrap_lstat, __wrap_mmap, __wrap_readlink, __wrap_statvfs, __wrap_symlink, clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. +- [clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. +- [__wrap_fchown, __wrap_link, __wrap_readlink, __wrap_statvfs, __wrap_symlink](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - parameterless throwing stubs. Under the `--wrap` convention assumed for the forwarding wrappers (see Functional) each would take the POSIX signature of the function it wraps, but the targets are in libkernel_sys (`fchown`, `link`, `readlink`, `symlink`) and libSceLibcInternal (`statvfs`), which no prx links; libkernel_sys's `fchown`, `readlink` and `symlink` also still throw for some arguments ### Functional @@ -11,3 +12,4 @@ - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database +- [__wrap_access, __wrap_clock_gettime, __wrap_getcwd, __wrap_getenv, __wrap_lstat and __wrap_mmap](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) assume, from the export names alone, the GNU ld [`--wrap`](https://sourceware.org/binutils/docs/ld/Options.html) convention (a reference to `<name>` resolves to `__wrap_<name>`): each takes its target's POSIX signature and forwards to libc's (`access`, `getcwd`, `getenv`) or libkernel's (`clock_gettime`, `lstat`, `mmap`) implementation, which sets `errno`. No PS5 module or WebKit call site was read to confirm that WebKit imports `__wrap_<name>` in place of `<name>` or that the console wrappers only forward. Any sandbox or path handling the console module adds is not modelled, and how the wrappers relate to `getenv_np`, `setenv_np`, `mmap_np` and `clock_gettime_np` (such as a separate environment that `setenv_np` fills) is unknown From 785e7156516d9fd5d5ef8131be39f094c2e49823 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Thu, 22 Oct 2026 09:09:06 +0100 Subject: [PATCH 0962/1360] feat(libSceJemspace): declare purge and statistics printing exports --- core/libs/prx/libSceJemspace/Export.cpp | 8 ++++++++ docs/dev/debt/libSceJemspace.md | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceJemspace/Export.cpp b/core/libs/prx/libSceJemspace/Export.cpp index 9ba406081e..5dc318d1a1 100644 --- a/core/libs/prx/libSceJemspace/Export.cpp +++ b/core/libs/prx/libSceJemspace/Export.cpp @@ -39,6 +39,14 @@ int APS5_VABI sceJemspacePosixMemalign() { NotImplemented_nid_no_patch(__func__); } +int APS5_VABI sceJemspacePrintStats() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspacePurge() { + NotImplemented_nid_no_patch(__func__); +} + int APS5_VABI sceJemspaceRealloc() { NotImplemented_nid_no_patch(__func__); } diff --git a/docs/dev/debt/libSceJemspace.md b/docs/dev/debt/libSceJemspace.md index a4169cf3ca..c0baf800a1 100644 --- a/docs/dev/debt/libSceJemspace.md +++ b/docs/dev/debt/libSceJemspace.md @@ -2,5 +2,4 @@ ### Unknown function info -- The eleven exports in [Export.cpp](../../../core/libs/prx/libSceJemspace/Export.cpp) resolve imports only and throw through `NotImplemented_nid_no_patch`. Each declared name hashes to a defined function NID in the `libSceJemspace` ELF export table. The module firmware version is unverified. The no-parameter declarations do not establish argument layouts or return types. No allocator behavior, memory ownership, allocation failure handling, statistics layout, or console compatibility is implemented or verified. -- Two further defined function exports in [libSceJemspace](../../../core/libs/prx/libSceJemspace/Export.cpp), `s686MsLMwns` and `4aop4GKLZaA`, remain undeclared because their names and signatures are unknown. Their callers still fail import resolution. +- The thirteen exports in [Export.cpp](../../../core/libs/prx/libSceJemspace/Export.cpp) resolve imports only and throw through `NotImplemented_nid_no_patch`. Each declared name hashes to a defined function NID in the `libSceJemspace` ELF export table. The module firmware version is unverified. The no-parameter declarations do not establish argument layouts or return types. No allocator behavior, memory ownership, allocation failure handling, statistics layout, or console compatibility is implemented or verified. From f83abf796a664a3dda33652c9db6d795712d4d1b Mon Sep 17 00:00:00 2001 From: seven <pantsaarthak@gmail.com> Date: Tue, 20 Oct 2026 14:26:18 +0530 Subject: [PATCH 0963/1360] perf(agc): stage written buffer elements up to 8 MiB on discrete GPUs --- .../Execution/src/VulkanDevice.cpp | 1 + .../Graphics/include/Context.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 18 +++++++++------ .../prx/libSceAgcDriver/tests/Recorder.cpp | 22 ++++++++++--------- docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 1 + 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 89ad6e3157..05e8b9d85e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3815,6 +3815,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.graphicsPipelineLibrary = state->graphicsPipelineLibrary; context.pipelineLinkTimeOptimization = state->pipelineLinkTimeOptimization; context.drawInputsInPlace = state->drawInputsInPlace; + context.discreteGpu = state->properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; context.provokingVertexModePerPipeline = state->provokingVertexModePerPipeline; context.drawIndirectCount = state->drawIndirectCount; context.occlusionQueryPrecise = state->occlusionQueryPrecise; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 1ac783d39e..c96cdbe890 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -180,6 +180,7 @@ struct Context { // Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment. VkDeviceSize hostImportAlignment = 0; bool drawInputsInPlace = true; + bool discreteGpu = false; bool dmaBufImport = false; bool sparseBinding = false; std::uint32_t queueFamily = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 074681e607..cb59eb6dec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4116,10 +4116,13 @@ VkBufferUsageFlags gpuCopyUsage(bool addressable) { // since store extents are not known), for the batch's latency; the staging window keeps the // exposure to the elements named on the [buffers] staging line and APS5_TRACE_STAGING. // APS5_NO_WRITTEN_SHADOW=1 and APS5_NO_ATOMIC_STAGING=1 bind those elements in place as before; -// APS5_WRITTEN_SHADOW_MIN_KIB / APS5_WRITTEN_SHADOW_MAX_KIB (16 / 2048) bound the written window -// (a kernel streaming once through a large buffer, the engine's memcpy kernel over 4-8 MiB video -// frames, would only gain the two copies), APS5_ATOMIC_STAGE_MAX_KIB (1024) the atomic one. +// APS5_WRITTEN_SHADOW_MIN_KIB / APS5_WRITTEN_SHADOW_MAX_KIB (16 / 2048, 8192 on a discrete GPU) bound +// the written window: on an integrated GPU a kernel streaming once through a large buffer (the +// engine's memcpy kernel over 4-8 MiB video frames) would only gain the two copies, while on a +// discrete GPU its stores into the host import cross the bus and cost more than the copies, so the +// window takes those elements there. APS5_ATOMIC_STAGE_MAX_KIB (1024) bounds the atomic one. constexpr std::uint64_t SweptShadowMax = std::uint64_t{16} << 20u; +constexpr std::uint64_t DiscreteWrittenShadowMax = std::uint64_t{8} << 20u; bool writtenShadowEnabled() { static const bool disabled = std::getenv("APS5_NO_WRITTEN_SHADOW") != nullptr; @@ -4141,9 +4144,10 @@ std::uint64_t writtenShadowMin() { return bytes; } -std::uint64_t writtenShadowMax() { - static const std::uint64_t bytes = kibSetting("APS5_WRITTEN_SHADOW_MAX_KIB", 2048); - return bytes; +std::uint64_t writtenShadowMax(bool discreteGpu) { + static const std::uint64_t setting = kibSetting("APS5_WRITTEN_SHADOW_MAX_KIB", 0); + if (setting != 0) return setting; + return discreteGpu ? DiscreteWrittenShadowMax : std::uint64_t{2048} << 10u; } std::uint64_t atomicStageMax() { @@ -4261,7 +4265,7 @@ bool GuestBufferMemory::stagingEligible(const Region& region, bool addressable) const auto bytes = region.end - region.begin; if (!WritesOverlap(region.begin, static_cast<std::size_t>(bytes))) return false; if (region.atomic && atomicStagingEnabled() && bytes <= atomicStageMax()) return true; - return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(), SweptShadowMax) : writtenShadowMax()); + return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(context.discreteGpu), SweptShadowMax) : writtenShadowMax(context.discreteGpu)); } void GuestBufferMemory::UploadPrepare(bool addressable) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 97f02e7f2f..cab1151ca7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -384,6 +384,7 @@ public: VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; function<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(context.physical, &properties); context.hostImportAlignment = hostProperties.minImportedHostPointerAlignment; + context.discreteGpu = properties.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; } if (driverProperties.driverID != VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) && hasExtension(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME)) { extensionsEnabled.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME); @@ -3132,7 +3133,7 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { std::cout << "host imports unavailable: read and written staging not tested\n"; return false; } - constexpr std::size_t bytes = 8u << 20u; + constexpr std::size_t bytes = 16u << 20u; #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else @@ -3162,11 +3163,12 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { return direct; }; Require(!boundInPlace(address + 4096, 1u << 20u, false), "a written 1 MiB element was not staged"); - Require(boundInPlace(address + 4096, 4u << 20u, false), "a written-only 4 MiB element was staged past the written window"); + Require(boundInPlace(address + 4096, 4u << 20u, false) != context.discreteGpu, context.discreteGpu ? "a written-only 4 MiB element was bound in place on a discrete GPU" : "a written-only 4 MiB element was staged past the written window"); + Require(boundInPlace(address + 4096, 12u << 20u, false), "a written-only 12 MiB element was staged past the discrete written window"); Require(!boundInPlace(address + 4096, 4u << 20u, true), "a read and written 4 MiB element was bound in place"); const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read, std::uint32_t stride, std::uint32_t extent) { const auto element = address + 4096; - constexpr std::uint32_t elementBytes = 4u << 20u; + constexpr std::uint32_t elementBytes = 12u << 20u; const std::uint32_t records = stride != 0 ? elementBytes / stride : elementBytes; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; @@ -3188,13 +3190,13 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { recorder.Sync(); return direct; }; - Require(!buildInPlace((4u << 20u) / 16u, true, 16, 0), "a dispatch sweeping a read and written 4 MiB element bound it in place"); - Require(buildInPlace(1024, true, 16, 0), "a dispatch touching 16 KiB of a read and written 4 MiB element staged all of it"); - Require(buildInPlace(0, true, 16, 0), "a dispatch of unknown size staged a read and written 4 MiB element"); - Require(buildInPlace((4u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 4 MiB element staged it"); - Require(!buildInPlace((4u << 20u) / 32u, true, 0, 32), "a dispatch sweeping a read and written 4 MiB raw element bound it in place"); - Require(buildInPlace(1024, true, 0, 32), "a dispatch touching 32 KiB of a read and written 4 MiB raw element staged all of it"); - Require(buildInPlace((4u << 20u) / 32u, true, 0, 0), "a raw element without an access extent was staged"); + Require(!buildInPlace((12u << 20u) / 16u, true, 16, 0), "a dispatch sweeping a read and written 12 MiB element bound it in place"); + Require(buildInPlace(1024, true, 16, 0), "a dispatch touching 16 KiB of a read and written 12 MiB element staged all of it"); + Require(buildInPlace(0, true, 16, 0), "a dispatch of unknown size staged a read and written 12 MiB element"); + Require(buildInPlace((12u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 12 MiB element staged it"); + Require(!buildInPlace((12u << 20u) / 32u, true, 0, 32), "a dispatch sweeping a read and written 12 MiB raw element bound it in place"); + Require(buildInPlace(1024, true, 0, 32), "a dispatch touching 32 KiB of a read and written 12 MiB raw element staged all of it"); + Require(buildInPlace((12u << 20u) / 32u, true, 0, 0), "a raw element without an access extent was staged"); Require(std::all_of(static_cast<const std::byte*>(block), static_cast<const std::byte*>(block) + bytes, [](std::byte value) { return value == std::byte{0x11}; }), "a staged copy-back changed the guest bytes"); { GuestAllocations::Mutation mutation; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index dd75c7f25f..1cc65c5a83 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -449,7 +449,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_WRITE_EVIDENCE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 16 | largest written range in KiB that the evidence may serve | | `APS5_WRITE_EVIDENCE_SAMPLE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 16 | reads one in this many pending writes to sample them | | `APS5_WRITE_WATCH_IMPORTS` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | how host imports are write-tracked: `probe`, `watch` or `unwatch` | -| `APS5_WRITTEN_SHADOW_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 2048 | upper bound in KiB of the written staging window | +| `APS5_WRITTEN_SHADOW_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 2048 (8192 on a discrete GPU) | upper bound in KiB of the written staging window; a set value is used as given on both device types | | `APS5_WRITTEN_SHADOW_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 16 | lower bound in KiB of the written staging window | ### libSceAjm.native diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index baec5ded56..27eb5e2777 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -62,6 +62,7 @@ ### Functional +- [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. From f9e1d2fc24d9623d40ad052ce0a6dd626c9b6382 Mon Sep 17 00:00:00 2001 From: seven <pantsaarthak@gmail.com> Date: Mon, 19 Oct 2026 20:39:00 +0530 Subject: [PATCH 0964/1360] perf(agc): replace the indexed 16-byte copy kernel with a buffer copy Recognise the compute kernel that copies count 16-byte elements from a source V# to a destination V# at offsets read from a one-record constant buffer, and run it through the copy HLE like the other two copy kernels. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- .../include/Driver/Memory/BufferCopy.hpp | 2 + .../src/Driver/Dispatch/Dispatch.cpp | 4 +- .../src/Driver/Memory/BufferCopy.cpp | 51 ++++- .../tests/execution/IndexedCopy.cpp | 187 ++++++++++++++++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp index 0c72dfd1f1..fa3e284410 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp @@ -28,6 +28,8 @@ struct StreamCopy { bool MatchesStreamCopyKernel(std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); std::optional<StreamCopy> StreamCopyRange(const std::vector<std::uint32_t>& userData, std::uint32_t groups); +bool MatchesIndexedCopyKernel(std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional<StreamCopy> IndexedCopyRange(const std::vector<std::uint32_t>& userData, std::uint32_t groups, std::uint32_t count, std::uint32_t sourceOffset, std::uint32_t destinationOffset); } 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 ccdc67b4aa..54b857a80d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -117,12 +117,12 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet, if (indirectArguments == 0 && (packet[4] & 0x2u) != 0) { partialGroups = Pm4::PartialGroupThreads({packet[1], packet[2], packet[3]}, {readRegister(queue.shader, 0x207), readRegister(queue.shader, 0x208), readRegister(queue.shader, 0x209)}); } - const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); + const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesIndexedCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); const bool partialLastGroup = partialGroups != std::array<std::uint32_t, 3>{}; if (resolving) { if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return; if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; - if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; + if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesIndexedCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; } else { if (indirectArguments == 0 && Graphics::TakeMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) { pendingDispatchPhases().outcome = DispatchOutcome::FillHle; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index 6a76867635..80ce303473 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -52,6 +52,28 @@ std::optional<StreamCopy> StreamCopyRange(const std::vector<std::uint32_t>& user return copy; } +bool MatchesIndexedCopyKernel(std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + if (!enabled || userData.size() != 12 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; + static constexpr std::array<std::uint32_t, 19> kernel{0xbfa00001u, 0xd7460000u, 0x04010c0cu, 0xf4201a84u, 0xfa000000u, 0xbf8cc07fu, 0x7da8006au, 0xbf88000au, 0xf4241a84u, 0xfa000004u, 0xbf8cc07fu, 0x4a08006bu, 0x4a00006au, 0xe0382000u, 0x80000000u, 0xbf8c3f70u, 0xe0782000u, 0x80010004u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return false; + const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; + const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto type = userData[3] >> 30u; + return stride(1) == 16 && stride(5) == 16 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[3] == userData[7] && type == 0 && !addTid && outOfBounds == 0 && userData[10] >= 1; +} + +std::optional<StreamCopy> IndexedCopyRange(const std::vector<std::uint32_t>& userData, std::uint32_t groups, std::uint32_t count, std::uint32_t sourceOffset, std::uint32_t destinationOffset) { + const auto base = [&](std::size_t word) { return userData[word] | (static_cast<std::uint64_t>(userData[word + 1] & 0xffffu) << 32u); }; + const auto elements = std::min<std::uint64_t>(std::uint64_t{groups} * 64u, count); + if (elements == 0 || std::uint64_t{sourceOffset} + elements > userData[2] || std::uint64_t{destinationOffset} + elements > userData[6]) return std::nullopt; + const StreamCopy copy{base(0) + std::uint64_t{sourceOffset} * 16u, base(4) + std::uint64_t{destinationOffset} * 16u, static_cast<std::size_t>(elements) * 16u}; + if (copy.bytes > (16u << 20u) || copy.source % 16 != 0 || copy.destination % 16 != 0) return std::nullopt; + return copy; +} + void Driver::countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (path < 0 || path >= CopyPaths) return; @@ -119,7 +141,9 @@ bool Driver::knownValueVerify() { } bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr<VulkanDevice>& localDevice, std::uint64_t programAddress) { - const bool stream = !matchesCopyKernel(code, userData, compute); + const bool hashed = matchesCopyKernel(code, userData, compute); + const bool indexed = !hashed && MatchesIndexedCopyKernel(code, userData, compute); + const bool stream = !hashed && !indexed; if (stream && !MatchesStreamCopyKernel(code, userData, compute)) return false; const auto started = std::chrono::steady_clock::now(); static const std::size_t cpuMax = [] { @@ -132,8 +156,8 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons return text ? static_cast<std::size_t>(std::strtoull(text, nullptr, 10)) : std::numeric_limits<std::size_t>::max(); }(); const auto base = [&](std::size_t word) { return userData[word] | (static_cast<std::uint64_t>(userData[word + 1] & 0xffffu) << 32u); }; - const auto source = base(0); - const auto destination = base(4); + auto source = base(0); + auto destination = base(4); const auto record = stream ? std::uint64_t{0} : base(8); std::array<std::uint32_t, 3> groups{packet[1], packet[2], packet[3]}; if ((packet[4] & 0x20u) != 0) { @@ -168,6 +192,27 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons return false; } bytes = range->bytes; + } else if (indexed) { + const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast<const void*>(record), 12); + + bool published = false; + if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 12) || Graphics::StorageTexture::FlushPending(record, 12, nullptr, "indexed copy record", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 12)) { + if (traceCopy()) traceCopyPending(recordAccessible ? "copy record" : "inaccessible copy record", record, 12, source, destination, 0, localDevice); + countCopy(CopyRecordPending, 0, started, locked); + return false; + } + std::uint32_t count = 0, sourceOffset = 0, destinationOffset = 0; + std::memcpy(&count, reinterpret_cast<const void*>(record), 4); + std::memcpy(&sourceOffset, reinterpret_cast<const void*>(record + 4), 4); + std::memcpy(&destinationOffset, reinterpret_cast<const void*>(record + 8), 4); + const auto range = IndexedCopyRange(userData, groups[0], count, sourceOffset, destinationOffset); + if (!range || (record < range->destination + range->bytes && range->destination < record + 12)) { + countCopy(CopyShape, range ? range->bytes : 0, started, locked); + return false; + } + source = range->source; + destination = range->destination; + bytes = range->bytes; } else { const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast<const void*>(record), 8); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopy.cpp new file mode 100644 index 0000000000..43966bcf5b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopy.cpp @@ -0,0 +1,187 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdio> +#include <cstdlib> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::IndexedCopyRange; +using AgcDriver::DriverDetail::MatchesIndexedCopyKernel; + +alignas(256) constexpr std::array<std::uint32_t, 19> Code{0xbfa00001u, 0xd7460000u, 0x04010c0cu, 0xf4201a84u, 0xfa000000u, 0xbf8cc07fu, 0x7da8006au, 0xbf88000au, 0xf4241a84u, 0xfa000004u, 0xbf8cc07fu, 0x4a08006bu, 0x4a00006au, 0xe0382000u, 0x80000000u, 0xbf8c3f70u, 0xe0782000u, 0x80010004u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format = 0x00005204u; +constexpr std::uint32_t RecordFormat = 0x0004dfacu; +constexpr std::uint32_t LargeGroups = 16385; +constexpr std::size_t Words = (std::size_t{LargeGroups} * 64u + 64u) * 4u; +alignas(256) std::array<std::uint32_t, Words> Source{}; +alignas(256) std::array<std::uint32_t, Words> Destination{}; +alignas(256) std::array<std::uint32_t, 4> Record{}; + +std::vector<std::uint32_t> UserData(const std::uint32_t* source, std::uint32_t sourceRecords, const std::uint32_t* destination, std::uint32_t destinationRecords, const std::uint32_t* record = Record.data(), std::uint32_t sourceStride = 16, std::uint32_t destinationStride = 16, std::uint32_t recordStride = 16) { + const auto from = reinterpret_cast<std::uintptr_t>(source); + const auto to = reinterpret_cast<std::uintptr_t>(destination); + const auto at = reinterpret_cast<std::uintptr_t>(record); + return {static_cast<std::uint32_t>(from), static_cast<std::uint32_t>((from >> 32u) & 0xffffu) | (sourceStride << 16u), sourceRecords, Format, + static_cast<std::uint32_t>(to), static_cast<std::uint32_t>((to >> 32u) & 0xffffu) | (destinationStride << 16u), destinationRecords, Format, + static_cast<std::uint32_t>(at), static_cast<std::uint32_t>((at >> 32u) & 0xffffu) | (recordStride << 16u), 1u, RecordFormat}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const auto data = UserData(Source.data(), 512, Destination.data(), 512); + Require(MatchesIndexedCopyKernel(Code, data, compute), "the indexed copy kernel was not recognized"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchesIndexedCopyKernel(altered, data, compute), "a different shader matched the indexed copy kernel (word " + std::to_string(word) + ")"); + } + Require(!MatchesIndexedCopyKernel(std::span(Code).first(18), data, compute), "a truncated shader matched"); + Require(!MatchesIndexedCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, Record.data(), 4), compute), "a source stride other than 16 matched"); + Require(!MatchesIndexedCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, Record.data(), 16, 4), compute), "a destination stride other than 16 matched"); + Require(!MatchesIndexedCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, Record.data(), 16, 16, 4), compute), "a record stride other than 16 matched"); + auto swizzled = data; + swizzled[5] |= 0x80000000u; + Require(!MatchesIndexedCopyKernel(Code, swizzled, compute), "a swizzled destination matched"); + auto formats = data; + formats[7] ^= 0x1000u; + Require(!MatchesIndexedCopyKernel(Code, formats, compute), "descriptors of different formats matched"); + const auto both = [&](std::uint32_t word3) { + auto changed = data; + changed[3] = changed[7] = word3; + return MatchesIndexedCopyKernel(Code, changed, compute); + }; + Require(!both(Format | 0x00800000u), "an add-tid descriptor matched"); + Require(!both(Format | 0x30000000u), "a raw out-of-bounds mode matched"); + Require(!both(Format | 0x40000000u), "a descriptor of another type matched"); + auto noRecords = data; + noRecords[10] = 0; + Require(!MatchesIndexedCopyKernel(Code, noRecords, compute), "a record descriptor without records matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchesIndexedCopyKernel(Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto wave32 = compute; + wave32.numThreads[0] = 32; + Require(!MatchesIndexedCopyKernel(Code, data, wave32), "a group of 32 threads matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchesIndexedCopyKernel(Code, data, noGroup), "a dispatch without the group id matched"); + + const auto source = reinterpret_cast<std::uintptr_t>(Source.data()); + const auto destination = reinterpret_cast<std::uintptr_t>(Destination.data()); + const auto copy = IndexedCopyRange(data, 4, 256, 0, 0); + Require(copy && copy->source == source && copy->destination == destination && copy->bytes == 4096, "4 groups were not taken as a copy of 256 elements"); + const auto offset = IndexedCopyRange(data, 4, 200, 16, 300); + Require(offset && offset->source == source + 256 && offset->destination == destination + 4800 && offset->bytes == 3200, "the offsets were not applied in elements"); + const auto launched = IndexedCopyRange(data, 4, 300, 0, 0); + Require(launched && launched->bytes == 4096, "the copy exceeded the launched threads"); + const auto fewer = IndexedCopyRange(data, 4, 100, 0, 0); + Require(fewer && fewer->bytes == 1600, "a count below the launch was not taken as the copy"); + Require(!IndexedCopyRange(data, 4, 256, 300, 0), "a source window past the records was taken"); + Require(!IndexedCopyRange(data, 4, 256, 0, 300), "a destination window past the records was taken"); + Require(!IndexedCopyRange(data, 4, 0, 0, 0), "a copy of 0 elements was taken"); + Require(!IndexedCopyRange(data, 0, 256, 0, 0), "a copy of 0 groups was taken"); + Require(IndexedCopyRange(UserData(Source.data(), 16384u * 64u, Destination.data(), 16384u * 64u), 16384, 16384u * 64u, 0, 0).has_value(), "a copy of 16 MiB was refused"); + Require(!IndexedCopyRange(UserData(Source.data(), LargeGroups * 64u, Destination.data(), LargeGroups * 64u), LargeGroups, LargeGroups * 64u, 0, 0), "a copy above 16 MiB was taken"); + Require(!IndexedCopyRange(UserData(Source.data() + 1, 512, Destination.data(), 512), 4, 256, 0, 0), "a source not aligned to 16 bytes was taken"); + Require(!IndexedCopyRange(UserData(Source.data(), 512, Destination.data() + 1, 512), 4, 256, 0, 0), "a destination not aligned to 16 bytes was taken"); +} + +std::vector<std::uint32_t> Commands(const std::vector<std::uint32_t>& data, std::uint32_t groups) { + std::vector<std::uint32_t> words; + const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) { + words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array<std::uint32_t, 3> shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast<std::uintptr_t>(Code.data()); + const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)}; + registers(0x20c, program); + const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +void ExecuteTests() { + using AgcDriver::DriverDetail::copyPathCounts; + using AgcDriver::DriverDetail::CopyShape; + struct Case { + const char* name; + std::uint32_t groups, count, sourceOffset, destinationOffset, sourceRecords, destinationRecords; + bool hle; + }; + constexpr std::array<Case, 8> cases{{ + {"4 groups", 4, 256, 0, 0, 512, 512, true}, + {"16 groups", 16, 1024, 0, 0, 2048, 2048, true}, + {"offsets", 4, 200, 16, 300, 512, 512, true}, + {"count below the launch", 4, 100, 0, 0, 512, 512, true}, + {"count above the launch", 4, 300, 0, 0, 512, 512, true}, + {"source window past the records", 4, 256, 300, 0, 512, 512, false}, + {"destination window past the records", 4, 256, 0, 300, 512, 512, false}, + {"above 16 MiB", LargeGroups, LargeGroups * 64u, 0, 0, LargeGroups * 64u, LargeGroups * 64u, false}, + }}; + const bool hleEnabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + for (const auto& item : cases) { + const bool hle = item.hle && hleEnabled; + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast<std::uint32_t>(i) * 2654435761u + 1u; + Destination.fill(Untouched); + Record = {item.count, item.sourceOffset, item.destinationOffset, 0u}; + const auto taken = Taken(); + const auto refused = copyPathCounts[CopyShape].load(); + const auto words = Commands(UserData(Source.data(), item.sourceRecords, Destination.data(), item.destinationRecords), item.groups); + Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Destination.data()), sizeof(Destination)); + const std::string where = std::string(" (") + item.name + ")"; + Require(Taken() - taken == (hle ? 1u : 0u), (hle ? "the copy did not take the HLE path" : "the copy took the HLE path") + where); + Require(copyPathCounts[CopyShape].load() - refused == (hle || !hleEnabled ? 0u : 1u), (hle ? "the copy was refused for its shape" : "the copy was not refused for its shape") + where); + const auto elements = std::min(item.groups * 64u, item.count); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const auto element = i / 4u; + const bool written = element >= item.destinationOffset && element < std::uint64_t{item.destinationOffset} + elements && element < item.destinationRecords; + const auto sourceElement = element - item.destinationOffset + item.sourceOffset; + const auto expected = !written ? Untouched : sourceElement < item.sourceRecords ? Source[sourceElement * 4u + i % 4u] : 0u; + Require(Destination[i] == expected, "indexed copy mismatch at word " + std::to_string(i) + where + ": got " + std::to_string(Destination[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_COPY_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("indexed copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 17bc1bc3dc15b0407efc886cebbbb34732cbdba3 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Mon, 19 Oct 2026 19:22:39 +0200 Subject: [PATCH 0965/1360] fix(agc): wait for leased writers before indexed-copy parameters (cherry picked from commit 1b2d6c53a1adc6a6c4dcb77cd04eaa83301ec6be) --- .../src/Driver/Memory/BufferCopy.cpp | 2 + .../execution/IndexedCopyGpuParameters.cpp | 150 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopyGpuParameters.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index 80ce303473..750f2d6649 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include <algorithm> #include <cstdlib> #include <cstring> @@ -193,6 +194,7 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons } bytes = range->bytes; } else if (indexed) { + Graphics::FinishLeaseWork(); const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast<const void*>(record), 12); bool published = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopyGpuParameters.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopyGpuParameters.cpp new file mode 100644 index 0000000000..b2878fc79a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndexedCopyGpuParameters.cpp @@ -0,0 +1,150 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdio> +#include <cstdlib> +#include <cstdint> +#include <iostream> +#include <span> +#include <string> +#include <vector> +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array<std::uint32_t, 19> Code{ + 0xbfa00001u, 0xd7460000u, 0x04010c0cu, 0xf4201a84u, 0xfa000000u, + 0xbf8cc07fu, 0x7da8006au, 0xbf88000au, 0xf4241a84u, 0xfa000004u, + 0xbf8cc07fu, 0x4a08006bu, 0x4a00006au, 0xe0382000u, 0x80000000u, + 0xbf8c3f70u, 0xe0782000u, 0x80010004u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::size_t Prefix = 16; +constexpr std::size_t Records = 256; +alignas(256) std::array<std::uint32_t, Records * 4 + Prefix * 2> Source{}, Destination{}; +alignas(16) std::array<std::uint32_t, 4> Parameters{}; +alignas(256) constexpr std::array<std::uint32_t, 7> AddressWriterCode{0x7e000202u, 0x7e020203u, 0x7e040204u, 0x7e060280u, 0xdc7c8000u, 0x00000003u, 0xbf810000u}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + data = static_cast<std::uint32_t*>(VirtualAlloc(nullptr, Bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast<std::uint32_t*>(std::aligned_alloc(Bytes, Bytes)); +#endif + Require(data != nullptr, "cannot allocate parameters"); + std::fill_n(data, Bytes / 4, 0); + GuestAllocations::Mutation().Add(data, Bytes, true, true, true); + } + ~GuestBlock() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint32_t* Data() const { return data; } + +private: + static constexpr std::size_t Bytes = 65536; + std::uint32_t* data = nullptr; +}; + +std::vector<std::uint32_t> UserData(std::uint32_t sourceRecords, std::uint32_t destinationRecords, const void* parameters = Parameters.data()) { + std::vector<std::uint32_t> words; + const auto descriptor = [&](const void* pointer, std::uint32_t records, std::uint32_t format) { + const auto address = reinterpret_cast<std::uintptr_t>(pointer); + words.insert(words.end(), {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), records, format}); + }; + descriptor(Source.data() + Prefix, sourceRecords, 0x00005204u); + descriptor(Destination.data() + Prefix, destinationRecords, 0x00005204u); + descriptor(parameters, 1, 0x0004dfacu); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +std::vector<std::uint32_t> Commands(const std::vector<std::uint32_t>& data, std::uint32_t groups, const void* code = Code.data(), std::uint32_t threads = 64, bool groupId = true) { + std::vector<std::uint32_t> words; + const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) { + words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array<std::uint32_t, 3> shape{threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast<std::uintptr_t>(code); + const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)}; + registers(0x20c, program); + const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (static_cast<std::uint32_t>(groupId) << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +void GpuParameters() { + GuestBlock parameters; + parameters.Data()[0] = 1; + Destination.fill(Untouched); + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast<std::uint32_t>(i) * 2654435761u + 1u; + const auto address = reinterpret_cast<std::uintptr_t>(parameters.Data()); + auto words = Commands({static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 23, 17, 29}, 1, AddressWriterCode.data(), 1, false); + const auto copy = Commands(UserData(Records, Records, parameters.Data()), 1); + words.insert(words.end(), copy.begin(), copy.end()); + const auto taken = Taken(); + const auto deferred = AgcDriver::Graphics::LeaseCounters().deferred; + Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Destination.data()), sizeof(Destination)); + const auto deferredAfter = AgcDriver::Graphics::LeaseCounters().deferred; + const auto takenAfter = Taken(); + AgcDriverShutdown_nid_postfix(); + Require(deferredAfter > deferred, "parameter producer was not deferred"); + Require(parameters.Data()[0] == 23 && parameters.Data()[1] == 17 && parameters.Data()[2] == 29, "GPU parameter store differs"); + Require(takenAfter - taken == (std::getenv("APS5_NO_COPY_HLE") == nullptr ? 1u : 0u), "GPU parameter copy did not take the expected path"); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const bool copied = i >= Prefix + 29 * 4 && i < Prefix + (29 + 23) * 4; + const auto expected = copied ? Source[i - (29 - 17) * 4] : Untouched; + Require(Destination[i] == expected, "GPU parameter copy differs at word " + std::to_string(i)); + } + std::puts("PASS: copy consumes parameters written by a preceding address-based GPU store"); +} + + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GpuParameters(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 93170e60493cba6dd27d247de64567d47f5bc05e Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:22:25 +0100 Subject: [PATCH 0966/1360] fix(libSceCommonDialog): return ALREADY_SYSTEM_INITIALIZED from a second sceCommonDialogInitialize A second sceCommonDialogInitialize returned 0x80B80004 (ALREADY_INITIALIZED), the code each dialog's own Initialize returns. The common-dialog system initializer returns 0x80B80002 (ALREADY_SYSTEM_INITIALIZED), as shadPS4 and the SDK error names have it, so callers that accept only SCE_OK or ALREADY_SYSTEM_INITIALIZED treated a second call as a failure. The header keeps ALREADY_INITIALIZED for the dialog libraries that return it. The test pins the literal instead of the header constant, and the debt entry records that a repeated call's value is not measured on a console. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceCommonDialog/CommonDialog.hpp | 1 + core/libs/prx/libSceCommonDialog/Export.cpp | 2 +- core/libs/tests/GuestCommonDialog.cpp | 5 ++++- docs/dev/debt/libSceCommonDialog.md | 4 ++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index f1949f6eeb..260eb371bc 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -248,6 +248,7 @@ inline bool ValidateCommonDialogBaseParam(const CommonDialogBaseParam& param) { } constexpr int COMMON_DIALOG_OK = 0; +constexpr int COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED = static_cast<int>(0x80B80002); constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004); #endif diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index bb18755ae2..47f9a84bb1 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -12,7 +12,7 @@ extern "C" { int APS5_VABI sceCommonDialogInitialize(void) { bool expected = false; if (!g_initialized.compare_exchange_strong(expected, true)) { - return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED; } return COMMON_DIALOG_OK; } diff --git a/core/libs/tests/GuestCommonDialog.cpp b/core/libs/tests/GuestCommonDialog.cpp index 1638caf9ec..e5b9cd993b 100644 --- a/core/libs/tests/GuestCommonDialog.cpp +++ b/core/libs/tests/GuestCommonDialog.cpp @@ -13,12 +13,15 @@ void Require(bool value) { if (!value) std::abort(); } +constexpr int kErrAlreadySystemInitialized = static_cast<int>(0x80B80002); + } int main() { Require(!sceCommonDialogIsUsed()); Require(sceCommonDialogInitialize() == COMMON_DIALOG_OK); Require(!sceCommonDialogIsUsed()); - Require(sceCommonDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); + Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); + Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); Require(!sceCommonDialogIsUsed()); } diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md index 0e7ac021bf..f46aaf0394 100644 --- a/docs/dev/debt/libSceCommonDialog.md +++ b/docs/dev/debt/libSceCommonDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceCommonDialog](../../../core/libs/prx/libSceCommonDialog/Export.cpp) + +### Functional + +- [sceCommonDialogInitialize](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - returning `ALREADY_SYSTEM_INITIALIZED` (`0x80B80002`) from a repeated call is not measured on a console by the project. A third-party PS5 log ([obSCEne](https://github.com/project-oops/obSCEne/blob/67b4c529eb3c271184117e26bb54330531ef2cf6/reports/hardware/report-20261014-004800.log#L260-L261), firmware 12.40) records two calls, `0x0` and then `0x80b80002`; a third call is not covered. The SDK error names, shadPS4 (a PS4 emulator) and SharpProspero give the same value; PPSA09695 compares the result with `0x80b80002` (call site 0x5F7BD0) and PPSA06327 stops on `0x80b80004`. Closes when: measured on a PS5, including a third call. From 893dba7850cd5d70899d83c1e9588968ac2e9442 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:24:28 -0300 Subject: [PATCH 0967/1360] fix(tools): read check_main_sync paths from the top of the repository --- tools/check_main_sync.py | 1 + tools/tests/test_check_main_sync.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index e1f41677d7..56ef66c6bf 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -52,6 +52,7 @@ def merged_pull_requests(repo, mergebase, base, paths): def report(repo, base, head): + repo = git(repo, "rev-parse", "--show-toplevel").stdout.strip() found = git(repo, "merge-base", base, head, check=False) if found.returncode: raise RuntimeError(f"{head} and {base} have no common ancestor") diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index a4590a60e5..15a931879b 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -111,6 +111,26 @@ class CheckMainSyncTests(unittest.TestCase): self.assertIn("1 file(s) changed on both sides", result.stdout) self.assertIn("#77", result.stdout) + def test_a_subdirectory_reports_the_same_conflicts_and_pull_requests(self): + (self.repo.path / "docs").mkdir() + self.repo.write("docs/notes.txt", "x\n") + self.repo.commit("add docs") + self.branch("feature") + self.repo.write("shared.txt", "one\nmine\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write("shared.txt", "one\ntheirs\nthree\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.merge_pull(77, "theirs") + from_root = check_main_sync.report(self.repo.path, "main", "feature") + self.assertEqual((from_root["conflicts"], from_root["pulls"].keys()), (["shared.txt"], {77})) + self.assertEqual(check_main_sync.report(self.repo.path / "docs", "main", "feature"), from_root) + result = run_tool(self.repo.path / "docs", "--head", "feature") + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(result.stdout, run_tool(self.repo.path, "--head", "feature").stdout) + def test_paths_with_spaces_and_unicode_are_reported_verbatim(self): name = "sp ace \u00e9.txt" self.repo.write(name, "one\ntwo\n") From a37f8c630aa0baa33acd1eadd6d33f4af2056626 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Thu, 22 Oct 2026 22:09:31 +1300 Subject: [PATCH 0968/1360] docs(libc): record the bundled libc.prx UTF-8 default for the multibyte debt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e37ab341c2..14e229524f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -107,7 +107,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (SDK 4.5x, 0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, sequences of up to six bytes, and no check for surrogates or values above 0x10FFFF when encoding. The state holds the value in its first eight bytes and the count of bytes still expected (0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at 0xA. As on the console, `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without setting `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (firmware 13.60, homebrew payload) the system library's default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; the locale of a title's own `libc.prx` was not measured +- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not go through the guest's `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate<char> and collate<wchar_t> _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits<wchar_t>::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. From e0c67af31f40c1117d779f6c5cb96aeda31453c8 Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:13:05 -0400 Subject: [PATCH 0969/1360] fix(agc): accept DB_EQAA INTERPOLATE_COMP_Z for a one-sample depth target without MSAA_ENABLE A one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is drawn as one sample at the pixel center, but DB_EQAA INTERPOLATE_COMP_Z threw on it. Gran Turismo 7 (PPSA01317) draws its 3840x2160 main pass this way after its EQAA pass (PA_SC_AA_CONFIG 0x0020c002, PA_SC_MODE_CNTL_0 0x2, DB_EQAA 0x00050000, a Z_32_FLOAT depth target with one sample) and stops on it. On this path every sample is taken to be at the pixel center, so Z interpolated to any sample position is the center's Z, which the one-sample draw already tests and writes. INTERPOLATE_COMP_Z is now accepted there. INTERPOLATE_SRC_Z and overrasterization still throw. Neither PAL nor radeonsi sets the bit and its meaning is not documented, so the debt entry records this as the same inference. --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 44bc19a7d6..a875a68d6b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1327,7 +1327,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (depthKeepsSampleZero && (read(cx, 0x292) & 1u) == 0) { const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage)); - Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f0c0000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization"); + Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f080000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization"); result.samples = 1; result.customSampleLocations = false; result.sampleLocations.fill(0); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9f97985dcf..37c0446ee4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -831,7 +831,7 @@ void msaaEnableClearOneSampleTargetTests() { queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "the depth-only draw did not skip its pixel program"); requireOneSampleDepth(queue, 0, "a one-sample depth target whose skipped pixel program has PS_ITER_SAMPLE and sample index and coverage inputs"); - constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization"; + constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization"; for (const bool color : {false, true}) { for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { queue = depthTarget(color); @@ -857,7 +857,12 @@ void msaaEnableClearOneSampleTargetTests() { queue = depthTarget(color); queue.userConfig[0x242] = 2; expectFailure([&] { DecodeState(queue); }, depthRefused); - for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + for (const auto eqaa : {0x00040000u, 0x00050000u}) { + queue = depthTarget(color); + queue.context[0x201] = eqaa; + requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE with DB_EQAA INTERPOLATE_COMP_Z"); + } + for (const auto eqaa : {0x00090002u, 0x000d0000u, 0x01010002u, 0x08010002u}) { queue = depthTarget(color); queue.context[0x201] = eqaa; expectFailure([&] { DecodeState(queue); }, depthRefused); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index baec5ded56..d1ae439ebf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -32,7 +32,7 @@ - [Point-list geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` (point list) runs as a mesh shader with `OutputPoints` and scalar `PrimitivePointIndicesEXT`. Each primitive export emits one point whose vertex index is extracted from bits [8:0] of the packed word. A subgroup gets up to `maxVertices` points, with provoking vertex 0 and point raster primitive topology. Rectangle lists, per-stream types and point or line strips from a vertex program on the mesh path still throw. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. -- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. +- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA.INTERPOLATE_COMP_Z` is accepted: with every sample at the pixel center, Z interpolated to any sample position is the center's Z, which the one-sample draw tests. That rests on the same inference; neither [PAL](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MsaaState.cpp#L117-L118) nor [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/761d5c8a54f93b3d43608f718910ed742983dd0b/src/gallium/drivers/radeonsi/si_state.c#L3314-L3316) sets the bit, and its meaning is not documented. `INTERPOLATE_SRC_Z` and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) still throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. Gran Turismo 7 (PPSA01317) draws its main pass with a one-sample depth target, `DB_EQAA` 0x00050000 and a 4x configuration. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw - [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw From 4b717912d7bab02b675e687896164573e19c7d18 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:53:20 +0300 Subject: [PATCH 0970/1360] feat(shader): decode PS4 (GCN) shaders by re-encoding them to RDNA Each GCN instruction is re-encoded to its RDNA form and decoded by the existing RDNA decoder, so the PS4 path shares the recompiler. Covers the legacy VS stage (program at the VS slot, user SGPRs from s0, vertex and instance index in v0-v3), fetch shaders linked as user-data calls, MIMG (image_sample and friends), PS4 buffer descriptors (GCN DATA_FORMAT / NUM_FORMAT, 44-bit base) and T# image descriptors (GCN tiling in word 7). The GCN switch is keyed into the shader disk cache. PS5 behaviour is unchanged: every PS4-only rule is gated on GcnGuestShaders(). --- .../libSceAgcDriver/tests/TextureFormat.cpp | 20 + core/shader/recompiler/CMakeLists.txt | 6 + .../ControlFlow/src/GraphBuilder.cpp | 15 +- core/shader/recompiler/Deserialize.cpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 27 +- .../RdnaDecoder/RdnaDescriptorFormat.hpp | 11 + .../RdnaDecoder/RdnaInstructionDecoder.hpp | 4 + .../RdnaDecoder/src/GcnInstructionDecoder.cpp | 347 ++++++++++++++++++ .../RdnaDecoder/src/GcnOpcodeTables.hpp | 187 ++++++++++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 71 ++++ .../src/RdnaInstructionDecoder.cpp | 5 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 3 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 31 +- .../Translation/InstructionTranslator.hpp | 1 + .../Translation/src/InstructionTranslator.cpp | 9 +- docs/dev/debt/shader.md | 3 + 18 files changed, 729 insertions(+), 14 deletions(-) create mode 100644 core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp create mode 100644 core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 31ff43033e..9feb514924 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -143,6 +143,25 @@ void repackedFormatTests() { Require(TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_A) == VK_COMPONENT_SWIZZLE_A && TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_ZERO) == VK_COMPONENT_SWIZZLE_ZERO, "format 43 keeps alpha and constant selectors"); } +void gcnBufferFormatTests() { + using ShaderRecompiler::IrBufferFormat; + const auto& formats = ShaderRecompiler::GcnBufferFormats(); + const auto gcn = [&](std::uint32_t data, std::uint32_t number) { return static_cast<IrBufferFormat>(formats[(data << 3u) | number]); }; + std::array<std::uint32_t, 78> seen{}; + for (const auto format : formats) ++seen[format]; + for (std::uint32_t format = 1; format < seen.size(); ++format) Require(seen[format] == 1u, "unified buffer format " + std::to_string(format) + " must come from exactly one GCN format pair"); + Require(gcn(0u, 4u) == IrBufferFormat::Invalid && gcn(15u, 7u) == IrBufferFormat::Invalid, "GCN DATA_FORMAT 0 and 15 have no buffer form"); + Require(gcn(1u, 7u) == IrBufferFormat::Invalid && gcn(4u, 0u) == IrBufferFormat::Invalid, "8-bit float and 32-bit UNORM have no buffer form"); + Require(gcn(1u, 0u) == IrBufferFormat::Format8UNorm && gcn(2u, 7u) == IrBufferFormat::Format16Float, "8 and 16-bit pairs must map"); + Require(gcn(4u, 4u) == IrBufferFormat::Format32UInt && gcn(4u, 7u) == IrBufferFormat::Format32Float, "32-bit pairs must map"); + Require(gcn(6u, 7u) == IrBufferFormat::Format11_11_10Float && gcn(7u, 7u) == IrBufferFormat::Format10_11_11Float, "GCN 10_11_11 and 11_11_10 must map by bit layout"); + Require(gcn(8u, 0u) == IrBufferFormat::Format2_10_10_10UNorm && gcn(9u, 0u) == IrBufferFormat::Format10_10_10_2UNorm, "GCN 10_10_10_2 and 2_10_10_10 must map by bit layout"); + Require(gcn(10u, 0u) == IrBufferFormat::Format8_8_8_8UNorm && gcn(14u, 7u) == IrBufferFormat::Format32_32_32_32Float, "four-channel pairs must map"); + const std::uint32_t word3 = 0x00000facu | (7u << 12u) | (14u << 15u) | (7u << 27u); + Require(ShaderRecompiler::GcnBufferWord3(16u << 16u, word3) == (0x00000facu | (77u << 12u) | (1u << 28u)), "a strided GCN V# must check its index range"); + Require(ShaderRecompiler::GcnBufferWord3(0u, word3) == (0x00000facu | (77u << 12u) | (3u << 28u)), "a GCN V# without a stride must check its byte range"); +} + void convertedDccClearTests() { alignas(64) std::array<std::uint8_t, 16> keys{}; GuestTextureResource resource{}; @@ -268,4 +287,5 @@ void RunTextureFormatTests() { convertedDccClearTests(); singleDccKeysTests(); packedFloatDccClearTests(); + gcnBufferFormatTests(); } diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index ad13332d35..652d255115 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -43,6 +43,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaOpcode.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaProgram.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaInstructionDecoder.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/GcnInstructionDecoder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaScalarOpDecoder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaVectorOpDecoder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -271,6 +272,11 @@ function(add_shader_recompiler target runtimeTarget) ReleaseUndrawnResourcePlan ResourceMaterializer::SpecializationNanoseconds RuntimeAbi::RequireVersion + GcnGuestShaders + SetGcnGuestShaders + GcnImageTilingIndex + GcnImagePitch + GcnImagePow2Pad RuntimeAbi::HeapCapacity SampledTextureNumericClass SetDebugProbeActive diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 5a2459f5af..eaccf98c5c 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -479,7 +479,7 @@ bool isFetchCall(const RdnaProgram& program, std::uint32_t index, const SwappcIn const std::uint32_t targetRegister = scalarIndex(instruction.source0); const bool userDataPair = swappc != nullptr && targetRegister != NoScalarRegister && targetRegister >= swappc->userDataBaseRegister && targetRegister + 1u < swappc->userDataBaseRegister + swappc->userDataCount && - std::none_of(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& other) { + std::none_of(program.instructions.begin(), GcnGuestShaders() ? program.instructions.begin() + index : program.instructions.end(), [&](const RdnaInstruction& other) { return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u); }); return swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram); @@ -715,9 +715,10 @@ struct LinkEscape { template <typename TError> LinkEscape requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::vector<SwappcCall>& calls, const std::vector<std::uint32_t>& unlinked, std::uint32_t knownWaveSize, const TError& escapeError) { + const bool gcn = GcnGuestShaders(); std::set<std::uint32_t> links; for (const auto& call : calls) { - if (!call.fetch) links.insert(call.linkRegister); + if (!call.fetch || gcn) links.insert(call.linkRegister); } const auto size = static_cast<std::uint32_t>(program.instructions.size()); const auto successors = [&](std::uint32_t index) { @@ -776,7 +777,7 @@ LinkEscape requireReturnAddressesStayInCalls(const RdnaProgram& program, const s if (holds[index] == 0u || writesLink(index) || instruction.family == RdnaInstructionFamily::SOPC || findReturnAt(calls, index) != nullptr) continue; const bool inside = std::ranges::any_of(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link && index >= call.targetIndex && index <= call.returnIndex; }); if (inside || !(((holds[index] & 1u) != 0u && readsScalarDword(instruction, link, waveSize)) || ((holds[index] & 2u) != 0u && readsScalarDword(instruction, link + 1u, waveSize)))) continue; - const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link; }); + const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return (!call.fetch || gcn) && call.linkRegister == link; }); return escapeError(*owner, instruction); } } @@ -897,7 +898,7 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } const bool outsideRegion = regionEnd != InvalidControlFlowId && (index < call.targetIndex || index > regionEnd); - if (outsideRegion) continue; + if (outsideRegion || (call.fetch && GcnGuestShaders())) continue; if (touchesLinkRegister(instruction, call.linkRegister)) { throw escapeError(call, instruction); } @@ -1292,8 +1293,8 @@ std::set<std::uint32_t> branchLabels(const RdnaProgram& program) { bool unchangedUntil(const RdnaProgram& program, std::uint32_t reg, std::uint32_t reader, std::uint32_t waveSize) { for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { - if (!writesScalar(program.instructions[index], reg, waveSize)) continue; - if (index <= reader) return false; + if (index == reader || !writesScalar(program.instructions[index], reg, waveSize)) continue; + if (index < reader) return false; for (std::uint32_t later = index; later < program.instructions.size(); ++later) { const auto& instruction = program.instructions[later]; if ((IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) && instruction.branchTarget <= program.instructions[reader].programCounter) return false; @@ -1402,7 +1403,7 @@ std::vector<UserDataCall> FindUserDataCalls(const RdnaProgram& program, const Sw const auto source = scalarIndex(instruction.source0); if (link == NoScalarRegister || source == NoScalarRegister || source % 2u != 0u) continue; std::uint32_t pair = NoScalarRegister; - if (source != link && userData(source) && userData(source + 1u) && unchanged(source, index) && unchanged(source + 1u, index)) { + if ((source != link || GcnGuestShaders()) && userData(source) && userData(source + 1u) && unchanged(source, index) && unchanged(source + 1u, index)) { pair = source; } else { std::uint32_t low = 0, high = 0; diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index 6f1b3a0ca9..5bc2aec696 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -165,6 +165,7 @@ int main(int argc, char** argv) { translateOptions.stage = stageKind; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; + translateOptions.gcnLegacyVertexInputs = GcnGuestShaders() && request.context.userDataBaseRegister == 0u; translateOptions.userDataCount = static_cast<std::uint32_t>(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast<std::uint64_t>(request.context.compute->ldsSizeDwords) * 4u + 4u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 16bbae5c22..58654e3f25 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -4,6 +4,7 @@ #include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "ThreadOwned.hpp" #include "ImageTableAbi.hpp" @@ -118,6 +119,15 @@ RdnaImageDimension descriptorDimension(const DescriptorValue& descriptor, RdnaIm } } +std::uint32_t imageCoordinates(RdnaImageDimension dimension) { + switch (dimension) { + case RdnaImageDimension::Dim1D: return 1u; + case RdnaImageDimension::Dim1DArray: case RdnaImageDimension::Dim2D: return 2u; + case RdnaImageDimension::Dim2DMsaaArray: return 4u; + default: return 3u; + } +} + bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { const auto type = rawImageType(descriptor); const auto format = rawImageFormat(descriptor); @@ -199,6 +209,9 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.dimension = RdnaImageDimension::Dim2DArray; } } + if (GcnGuestShaders() && imageCoordinates(decoded.dimension) != imageCoordinates(base.dimension)) { + throw std::runtime_error("GCN image instruction on a descriptor of type " + std::to_string(static_cast<std::uint32_t>(rawImageType(descriptor))) + " needs a different address count than its inferred dimension; not implemented"); + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { @@ -281,7 +294,9 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.conversionFormat = format; } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { - return rejectImage(PoisonReason::NumericClass, "sampled image descriptor uses an unsupported format"); + char words[96]; + std::snprintf(words, sizeof(words), " (T# %08x %08x %08x %08x %08x %08x %08x %08x)", descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2], descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5], descriptor.dwords[6], descriptor.dwords[7]); + return rejectImage(PoisonReason::NumericClass, std::string("sampled image descriptor uses an unsupported format") + words); } return result; } @@ -1285,6 +1300,12 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, throw std::runtime_error("materialization sources are missing buffer descriptors"); } snapshot.buffers.assign(values.begin(), values.begin() + plan.info.buffers.size()); + if (GcnGuestShaders()) { + for (auto& buffer : snapshot.buffers) { + buffer.dwords[1] &= GcnBufferWord1Mask; + buffer.dwords[3] = GcnBufferWord3(buffer.dwords[1], buffer.dwords[3]); + } + } cursor += plan.info.buffers.size(); snapshot.images.resize(plan.info.images.size()); @@ -1305,6 +1326,10 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (descriptor.dwordCount != 8u) { throw std::runtime_error("image descriptor has an invalid width"); } + if (GcnGuestShaders()) { + const auto words = GcnImageWords(std::span<const std::uint32_t, 8>(descriptor.dwords.data(), 8u)); + std::copy(words.begin(), words.end(), descriptor.dwords.begin()); + } const auto activity = plan.descriptorSources.size() + i; if (activity < activeSources.size() && activeSources[activity] == 0u) { descriptor = DescriptorValue{}; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index 5eb41904a0..ae3cbe0839 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -2,7 +2,9 @@ #define CORE_SHADER_RECOMPILIER_RDNADECODER_INCLUDE_RDNADECODER_RDNADESCRIPTORFORMAT_HPP #include "IntermediateRepresentation/IrMetadata.hpp" +#include <array> #include <cstdint> +#include <span> namespace ShaderRecompiler { @@ -24,6 +26,15 @@ 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]] const std::array<std::uint8_t, 128>& GcnBufferFormats(); +[[nodiscard]] std::uint32_t GcnBufferWord3(std::uint32_t word1, std::uint32_t word3); +inline constexpr std::uint32_t GcnBufferWord1Mask = 0xffff0fffu; +[[nodiscard]] std::uint32_t GcnImageFormat(std::uint32_t dataFormat, std::uint32_t numberFormat); +[[nodiscard]] std::array<std::uint32_t, 8> GcnImageWords(std::span<const std::uint32_t, 8> words); +[[nodiscard]] constexpr std::uint32_t GcnImageTilingIndex(std::uint32_t word7) { return word7 & 0x1fu; } +[[nodiscard]] constexpr bool GcnImagePow2Pad(std::uint32_t word7) { return ((word7 >> 5u) & 1u) != 0u; } +[[nodiscard]] constexpr std::uint32_t GcnImagePitch(std::uint32_t word7) { return ((word7 >> 6u) & 0x3fffu) + 1u; } + [[nodiscard]] constexpr std::uint32_t SrgbDecodeBit(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format8Srgb: return 1u; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp index 9456f38cd1..d52379a7c3 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp @@ -21,6 +21,10 @@ private: RdnaInstructionFamily GetRdnaInstructionFamily(std::uint32_t word); RdnaInstruction DecodeRdnaInstruction(std::uint32_t programCounter, std::span<const std::uint32_t> code, std::uint32_t wordIndex); +RdnaInstruction DecodeRdnaEncodedInstruction(std::uint32_t programCounter, std::span<const std::uint32_t> code, std::uint32_t wordIndex); +RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span<const std::uint32_t> code, std::uint32_t wordIndex); +void SetGcnGuestShaders(bool enabled); +bool GcnGuestShaders(); RdnaProgram DecodeRdnaFrontProgram(std::span<const std::uint32_t> front); void DecodeRdnaProgram(std::span<const std::uint32_t> code, RdnaProgram& program); RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCounter); diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp new file mode 100644 index 0000000000..b92bde53df --- /dev/null +++ b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp @@ -0,0 +1,347 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "GcnOpcodeTables.hpp" +#include <algorithm> +#include <array> +#include <atomic> +#include <cstdint> +#include <optional> +#include <span> +#include <stdexcept> +#include <string> + +namespace ShaderRecompiler { + +namespace { + +std::atomic<bool> g_gcnGuestShaders{false}; + +constexpr std::uint32_t LiteralSource = 0xffu; +constexpr std::uint32_t VccSdst = 106u; +constexpr std::uint32_t NullSoffset = 0x7du; +constexpr std::uint32_t LinkedCallSopk = 0x16u; +constexpr std::uint32_t CodeEndSopp = 0x1fu; + +template <std::size_t N> +std::optional<std::uint32_t> mapOpcode(const std::array<std::pair<std::uint16_t, std::uint16_t>, N>& table, std::uint32_t opcode) { + const auto found = std::lower_bound(table.begin(), table.end(), opcode, [](const auto& entry, std::uint32_t value) { return entry.first < value; }); + if (found == table.end() || found->first != opcode) return std::nullopt; + return found->second; +} + +[[noreturn]] void unsupported(const char* family, std::uint32_t opcode, std::uint32_t programCounter) { + throw UnsupportedInstructionError(std::string("GCN ") + family + " opcode " + std::to_string(opcode) + " at program counter " + std::to_string(programCounter) + " is not implemented"); +} + +std::uint32_t replaceBits(std::uint32_t word, std::uint32_t shift, std::uint32_t width, std::uint32_t value) { + const std::uint32_t mask = ((1u << width) - 1u) << shift; + return (word & ~mask) | ((value << shift) & mask); +} + +bool hasLiteral(std::uint32_t source) { + return source == LiteralSource; +} + +struct Encoded { + std::array<std::uint32_t, 4> words{}; + std::uint32_t rdnaWords = 0; + std::uint32_t gcnWords = 0; +}; + +std::uint32_t literalAt(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + if (index >= code.size()) throw std::out_of_range("GCN literal constant at program counter " + std::to_string(programCounter) + " is past the end of the code"); + return code[index]; +} + +Encoded encodeScalar(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + Encoded out; + out.gcnWords = 1; + std::uint32_t opcode = 0; + std::optional<std::uint32_t> mapped; + bool literal = false; + const auto prefix9 = word >> 23u; + if (prefix9 == 0x17du) { + opcode = (word >> 8u) & 0xffu; + mapped = mapOpcode(GcnTables::Sop1, opcode); + if (!mapped) unsupported("SOP1", opcode, programCounter); + out.words[0] = replaceBits(word, 8, 8, *mapped); + literal = hasLiteral(word & 0xffu); + } else if (prefix9 == 0x17eu) { + opcode = (word >> 16u) & 0x7fu; + mapped = mapOpcode(GcnTables::Sopc, opcode); + if (!mapped) unsupported("SOPC", opcode, programCounter); + out.words[0] = replaceBits(word, 16, 7, *mapped); + literal = hasLiteral(word & 0xffu) || hasLiteral((word >> 8u) & 0xffu); + } else if (prefix9 == 0x17fu) { + opcode = (word >> 16u) & 0x7fu; + mapped = opcode == CodeEndSopp ? std::optional<std::uint32_t>(CodeEndSopp) : mapOpcode(GcnTables::Sopp, opcode); + if (!mapped) unsupported("SOPP", opcode, programCounter); + out.words[0] = replaceBits(word, 16, 7, *mapped); + } else if ((word >> 28u) == 0xbu) { + opcode = (word >> 23u) & 0x1fu; + mapped = opcode == LinkedCallSopk ? std::optional<std::uint32_t>(LinkedCallSopk) : mapOpcode(GcnTables::Sopk, opcode); + if (!mapped) unsupported("SOPK", opcode, programCounter); + out.words[0] = replaceBits(word, 23, 5, *mapped); + } else { + opcode = (word >> 23u) & 0x7fu; + mapped = mapOpcode(GcnTables::Sop2, opcode); + if (!mapped) unsupported("SOP2", opcode, programCounter); + out.words[0] = replaceBits(word, 23, 7, *mapped); + literal = hasLiteral(word & 0xffu) || hasLiteral((word >> 8u) & 0xffu); + } + out.rdnaWords = 1; + if (literal) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; +} + +Encoded encodeSmrd(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 22u) & 0x1fu; + const auto mapped = mapOpcode(GcnTables::Smrd, opcode); + if (!mapped) unsupported("SMRD", opcode, programCounter); + const auto sdst = (word >> 15u) & 0x7fu; + const auto sbase = (word >> 9u) & 0x3fu; + const bool immediate = ((word >> 8u) & 1u) != 0; + const auto offset = word & 0xffu; + if (!immediate && offset == LiteralSource) unsupported("SMRD literal-offset", opcode, programCounter); + Encoded out; + out.gcnWords = 1; + out.rdnaWords = 2; + out.words[0] = (0x3du << 26u) | (*mapped << 18u) | (sdst << 6u) | sbase; + out.words[1] = immediate ? (NullSoffset << 25u) | (offset * 4u) : (offset << 25u); + return out; +} + +Encoded encodeVop3Words(std::uint32_t word0, std::uint32_t word1, std::uint32_t rdnaOpcode, bool carryOut) { + Encoded out; + out.gcnWords = 2; + out.rdnaWords = 2; + const auto vdst = word0 & 0xffu; + std::uint32_t encoded = (0x35u << 26u) | (rdnaOpcode << 16u) | vdst; + if (carryOut) { + encoded |= ((word0 >> 8u) & 0x7fu) << 8u; + } else { + encoded |= ((word0 >> 8u) & 0x7u) << 8u; + encoded |= ((word0 >> 11u) & 1u) << 15u; + } + out.words[0] = encoded; + out.words[1] = word1; + return out; +} + +std::optional<std::uint32_t> reversedShift(std::uint32_t gcnVop3Opcode) { + switch (gcnVop3Opcode) { + case 0x115u: return 0x116u; + case 0x117u: return 0x118u; + case 0x119u: return 0x11au; + case 0x161u: return 0x2ffu; + case 0x162u: return 0x300u; + case 0x163u: return 0x301u; + default: return std::nullopt; + } +} + +bool compareWritesExec(std::uint32_t gcnVop3Opcode) { + return gcnVop3Opcode < 0x100u && (gcnVop3Opcode & 0x10u) != 0; +} + +std::uint32_t swapSources(std::uint32_t word1) { + const auto src0 = word1 & 0x1ffu; + const auto src1 = (word1 >> 9u) & 0x1ffu; + const auto neg0 = (word1 >> 29u) & 1u; + const auto neg1 = (word1 >> 30u) & 1u; + word1 = (word1 & ~0x3ffffu & ~(3u << 29u)) | src1 | (src0 << 9u) | (neg1 << 29u) | (neg0 << 30u); + return word1; +} + +std::uint32_t swapAbsolute(std::uint32_t word0) { + const auto abs0 = (word0 >> 8u) & 1u; + const auto abs1 = (word0 >> 9u) & 1u; + return (word0 & ~(3u << 8u)) | (abs1 << 8u) | (abs0 << 9u); +} + +bool gcnVop3bCarryOut(std::uint32_t opcode) { + switch (opcode) { + case 0x125u: case 0x126u: case 0x127u: case 0x128u: case 0x129u: case 0x12au: case 0x16du: case 0x16eu: case 0x176u: case 0x177u: return true; + default: return false; + } +} + +Encoded encodeVop3(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN VOP3 instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 17u) & 0x1ffu; + if (compareWritesExec(opcode)) unsupported("VOP3 v_cmpx (writes both the scalar destination and EXEC)", opcode, programCounter); + if (const auto shift = reversedShift(opcode)) return encodeVop3Words(swapAbsolute(word0), swapSources(code[index + 1]), *shift, false); + const auto mapped = mapOpcode(GcnTables::Vop3, opcode); + if (!mapped) unsupported("VOP3", opcode, programCounter); + return encodeVop3Words(word0, code[index + 1], *mapped, gcnVop3bCarryOut(opcode)); +} + +Encoded promoteToVop3(std::uint32_t gcnVop3Opcode, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, bool carryOut, std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto shift = reversedShift(gcnVop3Opcode); + const auto mapped = shift ? shift : mapOpcode(GcnTables::Vop3, gcnVop3Opcode); + if (!mapped) unsupported("VOP3-promoted", gcnVop3Opcode, programCounter); + const std::uint32_t word0 = carryOut ? (vdst | (VccSdst << 8u)) : vdst; + const std::uint32_t word1 = shift ? (src1 | (src0 << 9u) | (src2 << 18u)) : (src0 | (src1 << 9u) | (src2 << 18u)); + auto out = encodeVop3Words(word0, word1, *mapped, carryOut); + out.gcnWords = 1; + if (hasLiteral(src0)) { + out.words[2] = literalAt(code, index + 1, programCounter); + out.rdnaWords = 3; + out.gcnWords = 2; + } + return out; +} + +Encoded encodeVop2(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 25u) & 0x3fu; + const auto src0 = word & 0x1ffu; + if (const auto mapped = mapOpcode(GcnTables::Vop2, opcode)) { + Encoded out; + out.words[0] = replaceBits(word, 25, 6, *mapped); + out.rdnaWords = out.gcnWords = 1; + const bool constantK = opcode == 0x20u || opcode == 0x21u; + if (hasLiteral(src0) || constantK) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; + } + const auto vdst = (word >> 17u) & 0xffu; + const auto vsrc1 = (word >> 9u) & 0xffu; + const bool laneSelect = opcode == 0x01u || opcode == 0x02u; + const bool carryOut = opcode == 0x25u || opcode == 0x26u || opcode == 0x27u; + return promoteToVop3(0x100u + opcode, vdst, src0, laneSelect ? vsrc1 : 0x100u + vsrc1, 0, carryOut, code, index, programCounter); +} + +Encoded encodeVop1(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 9u) & 0xffu; + const auto src0 = word & 0x1ffu; + if (const auto mapped = mapOpcode(GcnTables::Vop1, opcode)) { + Encoded out; + out.words[0] = replaceBits(word, 9, 8, *mapped); + out.rdnaWords = out.gcnWords = 1; + if (hasLiteral(src0)) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; + } + return promoteToVop3(0x180u + opcode, (word >> 17u) & 0xffu, src0, 0, 0, false, code, index, programCounter); +} + +Encoded encodeVopc(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 17u) & 0xffu; + if (compareWritesExec(opcode)) unsupported("VOPC v_cmpx (writes both VCC and EXEC)", opcode, programCounter); + const auto mapped = mapOpcode(GcnTables::Vopc, opcode); + if (!mapped) unsupported("VOPC", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(word, 17, 8, *mapped); + out.rdnaWords = out.gcnWords = 1; + if (hasLiteral(word & 0x1ffu)) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; +} + +Encoded encodeDs(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN DS instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0xffu; + const auto mapped = mapOpcode(GcnTables::Ds, opcode); + if (!mapped) unsupported("DS", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(word0, 18, 8, *mapped); + out.words[1] = code[index + 1]; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodeMubuf(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN MUBUF instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0x7fu; + const auto mapped = mapOpcode(GcnTables::Mubuf, opcode); + if (!mapped) unsupported("MUBUF", opcode, programCounter); + if (((word0 >> 15u) & 1u) != 0) unsupported("MUBUF addr64", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(replaceBits(word0, 18, 7, *mapped & 0x7fu), 25, 1, *mapped >> 7u); + out.words[1] = code[index + 1]; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodeMimg(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN MIMG instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0x7fu; + const auto mapped = mapOpcode(GcnTables::Mimg, opcode); + if (!mapped) unsupported("MIMG", opcode, programCounter); + const bool array = ((word0 >> 14u) & 1u) != 0; + Encoded out; + out.words[0] = replaceBits(replaceBits(replaceBits(word0, 18, 7, *mapped & 0x7fu), 14, 1, 0), 0, 8, (*mapped >> 7u) | ((array ? 5u : 1u) << 3u)); + out.words[1] = code[index + 1] & 0x03ffffffu; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodePassThrough(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t words, const char* family) { + if (index + words > code.size()) throw std::out_of_range(std::string("truncated GCN ") + family + " instruction"); + Encoded out; + for (std::uint32_t i = 0; i < words; ++i) out.words[i] = code[index + i]; + out.rdnaWords = out.gcnWords = words; + return out; +} + +Encoded encodeGcn(std::span<const std::uint32_t> code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + if ((word >> 31u) == 0) { + switch ((word >> 25u) & 0x3fu) { + case 0x3eu: return encodeVopc(code, index, programCounter); + case 0x3fu: return encodeVop1(code, index, programCounter); + default: return encodeVop2(code, index, programCounter); + } + } + if ((word >> 30u) == 2u) return encodeScalar(code, index, programCounter); + switch (word >> 26u) { + case 0x30u: case 0x31u: return encodeSmrd(code, index, programCounter); + case 0x32u: return encodePassThrough(code, index, 1, "VINTRP"); + case 0x34u: return encodeVop3(code, index, programCounter); + case 0x36u: return encodeDs(code, index, programCounter); + case 0x38u: return encodeMubuf(code, index, programCounter); + case 0x3eu: return encodePassThrough(code, index, 2, "EXP"); + case 0x37u: unsupported("FLAT", (word >> 18u) & 0x7fu, programCounter); + case 0x3au: unsupported("MTBUF", (word >> 16u) & 7u, programCounter); + case 0x3cu: return encodeMimg(code, index, programCounter); + default: break; + } + throw std::invalid_argument("unknown GCN instruction family at program counter " + std::to_string(programCounter) + " raw word " + std::to_string(word)); +} + +} + +void SetGcnGuestShaders(bool enabled) { + g_gcnGuestShaders.store(enabled, std::memory_order_release); +} + +bool GcnGuestShaders() { + return g_gcnGuestShaders.load(std::memory_order_acquire); +} + +RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span<const std::uint32_t> code, std::uint32_t wordIndex) { + if (wordIndex >= code.size()) throw std::out_of_range("word index is out of the code span bounds"); + const auto encoded = encodeGcn(code, wordIndex, programCounter); + auto instruction = DecodeRdnaEncodedInstruction(programCounter, std::span<const std::uint32_t>(encoded.words.data(), encoded.rdnaWords), 0); + instruction.wordCount = encoded.gcnWords; + return instruction; +} + +} diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp new file mode 100644 index 0000000000..cbe30ec329 --- /dev/null +++ b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp @@ -0,0 +1,187 @@ +#ifndef CORE_SHADER_RECOMPILIER_RDNADECODER_SRC_GCNOPCODETABLES_HPP +#define CORE_SHADER_RECOMPILIER_RDNADECODER_SRC_GCNOPCODETABLES_HPP + +#include <array> +#include <cstdint> +#include <utility> + +namespace ShaderRecompiler::GcnTables { + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 47> Sop1{{ + {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, + {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, + {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, + {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, + {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, + {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x34, 0x34}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 42> Sop2{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, + {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, + {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, + {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, + {0x2a, 0x2a}, {0x2c, 0x2c}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 16> Sopc{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 6> Sopk{{ + {0x0, 0x0}, {0x2, 0x2}, {0x10, 0x10}, {0x12, 0x12}, {0x13, 0x13}, {0x15, 0x15}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 21> Sopp{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, + {0x9, 0x9}, {0xa, 0xa}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, + {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 13> Smrd{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, + {0xb, 0xb}, {0xc, 0xc}, {0x1d, 0x21}, {0x1e, 0x24}, {0x1f, 0x21}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 58> Vop1{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, + {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, + {0x19, 0x19}, {0x1a, 0x1a}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, + {0x27, 0x27}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x31, 0x31}, {0x33, 0x33}, {0x34, 0x34}, + {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, + {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x42, 0x42}, {0x43, 0x43}, {0x44, 0x44}, + {0x54, 0x54}, {0x55, 0x55}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 37> Vop2{{ + {0x0, 0x1}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, {0x9, 0x9}, + {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, + {0x14, 0x14}, {0x16, 0x16}, {0x18, 0x18}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1f, 0x1f}, + {0x20, 0x20}, {0x21, 0x21}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2f, 0x2f}, {0x32, 0x32}, {0x33, 0x33}, + {0x34, 0x34}, {0x35, 0x35}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 216> Vopc{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, + {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, + {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, + {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, + {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, + {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, + {0x40, 0x0}, {0x41, 0x1}, {0x42, 0x2}, {0x43, 0x3}, {0x44, 0x4}, {0x45, 0x5}, {0x46, 0x6}, {0x47, 0x7}, + {0x48, 0x8}, {0x49, 0x9}, {0x4a, 0xa}, {0x4b, 0xb}, {0x4c, 0xc}, {0x4d, 0xd}, {0x4e, 0xe}, {0x4f, 0xf}, + {0x50, 0x10}, {0x51, 0x11}, {0x52, 0x12}, {0x53, 0x13}, {0x54, 0x14}, {0x55, 0x15}, {0x56, 0x16}, {0x57, 0x17}, + {0x58, 0x18}, {0x59, 0x19}, {0x5a, 0x1a}, {0x5b, 0x1b}, {0x5c, 0x1c}, {0x5d, 0x1d}, {0x5e, 0x1e}, {0x5f, 0x1f}, + {0x60, 0x20}, {0x61, 0x21}, {0x62, 0x22}, {0x63, 0x23}, {0x64, 0x24}, {0x65, 0x25}, {0x66, 0x26}, {0x67, 0x27}, + {0x68, 0x28}, {0x69, 0x29}, {0x6a, 0x2a}, {0x6b, 0x2b}, {0x6c, 0x2c}, {0x6d, 0x2d}, {0x6e, 0x2e}, {0x6f, 0x2f}, + {0x70, 0x30}, {0x71, 0x31}, {0x72, 0x32}, {0x73, 0x33}, {0x74, 0x34}, {0x75, 0x35}, {0x76, 0x36}, {0x77, 0x37}, + {0x78, 0x38}, {0x79, 0x39}, {0x7a, 0x3a}, {0x7b, 0x3b}, {0x7c, 0x3c}, {0x7d, 0x3d}, {0x7e, 0x3e}, {0x7f, 0x3f}, + {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, {0x87, 0x87}, + {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, {0x8c, 0x8c}, {0x8d, 0x8d}, {0x8e, 0x8e}, {0x90, 0x90}, + {0x91, 0x91}, {0x92, 0x92}, {0x93, 0x93}, {0x94, 0x94}, {0x95, 0x95}, {0x96, 0x96}, {0x97, 0x97}, {0x98, 0x98}, + {0xa0, 0xa0}, {0xa1, 0xa1}, {0xa2, 0xa2}, {0xa3, 0xa3}, {0xa4, 0xa4}, {0xa5, 0xa5}, {0xa6, 0xa6}, {0xa7, 0xa7}, + {0xa8, 0xa8}, {0xa9, 0xa9}, {0xaa, 0xaa}, {0xab, 0xab}, {0xac, 0xac}, {0xad, 0xad}, {0xae, 0xae}, {0xb0, 0xb0}, + {0xb1, 0xb1}, {0xb2, 0xb2}, {0xb3, 0xb3}, {0xb4, 0xb4}, {0xb5, 0xb5}, {0xb6, 0xb6}, {0xb7, 0xb7}, {0xb8, 0xb8}, + {0xc0, 0xc0}, {0xc1, 0xc1}, {0xc2, 0xc2}, {0xc3, 0xc3}, {0xc4, 0xc4}, {0xc5, 0xc5}, {0xc6, 0xc6}, {0xc7, 0xc7}, + {0xc8, 0xc8}, {0xc9, 0xc9}, {0xca, 0xca}, {0xcb, 0xcb}, {0xcc, 0xcc}, {0xcd, 0xcd}, {0xce, 0xce}, {0xcf, 0xcf}, + {0xd0, 0xd0}, {0xd1, 0xd1}, {0xd2, 0xd2}, {0xd3, 0xd3}, {0xd4, 0xd4}, {0xd5, 0xd5}, {0xd6, 0xd6}, {0xd7, 0xd7}, + {0xe0, 0xe0}, {0xe1, 0xe1}, {0xe2, 0xe2}, {0xe3, 0xe3}, {0xe4, 0xe4}, {0xe5, 0xe5}, {0xe6, 0xe6}, {0xe7, 0xe7}, + {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 324> Vop3{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0x10, 0x10}, + {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, + {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x20, 0x20}, {0x21, 0x21}, + {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, + {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, + {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, + {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, {0x87, 0x87}, {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, + {0x8c, 0x8c}, {0x8d, 0x8d}, {0x8e, 0x8e}, {0x90, 0x90}, {0x91, 0x91}, {0x92, 0x92}, {0x93, 0x93}, {0x94, 0x94}, + {0x95, 0x95}, {0x96, 0x96}, {0x97, 0x97}, {0x98, 0x98}, {0xa0, 0xa0}, {0xa1, 0xa1}, {0xa2, 0xa2}, {0xa3, 0xa3}, + {0xa4, 0xa4}, {0xa5, 0xa5}, {0xa6, 0xa6}, {0xa7, 0xa7}, {0xa8, 0xa8}, {0xa9, 0xa9}, {0xaa, 0xaa}, {0xab, 0xab}, + {0xac, 0xac}, {0xad, 0xad}, {0xae, 0xae}, {0xb0, 0xb0}, {0xb1, 0xb1}, {0xb2, 0xb2}, {0xb3, 0xb3}, {0xb4, 0xb4}, + {0xb5, 0xb5}, {0xb6, 0xb6}, {0xb7, 0xb7}, {0xb8, 0xb8}, {0xc0, 0xc0}, {0xc1, 0xc1}, {0xc2, 0xc2}, {0xc3, 0xc3}, + {0xc4, 0xc4}, {0xc5, 0xc5}, {0xc6, 0xc6}, {0xc7, 0xc7}, {0xc8, 0xc8}, {0xc9, 0xc9}, {0xca, 0xca}, {0xcb, 0xcb}, + {0xcc, 0xcc}, {0xcd, 0xcd}, {0xce, 0xce}, {0xcf, 0xcf}, {0xd0, 0xd0}, {0xd1, 0xd1}, {0xd2, 0xd2}, {0xd3, 0xd3}, + {0xd4, 0xd4}, {0xd5, 0xd5}, {0xd6, 0xd6}, {0xd7, 0xd7}, {0xe0, 0xe0}, {0xe1, 0xe1}, {0xe2, 0xe2}, {0xe3, 0xe3}, + {0xe4, 0xe4}, {0xe5, 0xe5}, {0xe6, 0xe6}, {0xe7, 0xe7}, {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, + {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, {0x100, 0x101}, {0x101, 0x360}, {0x102, 0x361}, {0x103, 0x103}, + {0x104, 0x104}, {0x105, 0x105}, {0x106, 0x106}, {0x107, 0x107}, {0x108, 0x108}, {0x109, 0x109}, {0x10a, 0x10a}, {0x10b, 0x10b}, + {0x10c, 0x10c}, {0x10f, 0x10f}, {0x110, 0x110}, {0x111, 0x111}, {0x112, 0x112}, {0x113, 0x113}, {0x114, 0x114}, {0x116, 0x116}, + {0x118, 0x118}, {0x11a, 0x11a}, {0x11b, 0x11b}, {0x11c, 0x11c}, {0x11d, 0x11d}, {0x11e, 0x363}, {0x11f, 0x11f}, {0x120, 0x120}, + {0x121, 0x121}, {0x122, 0x364}, {0x123, 0x365}, {0x124, 0x366}, {0x125, 0x30f}, {0x126, 0x310}, {0x127, 0x319}, {0x128, 0x128}, + {0x12b, 0x362}, {0x12d, 0x368}, {0x12e, 0x369}, {0x12f, 0x12f}, {0x130, 0x36a}, {0x131, 0x36b}, {0x132, 0x132}, {0x133, 0x133}, + {0x134, 0x134}, {0x135, 0x135}, {0x139, 0x139}, {0x13a, 0x13a}, {0x13b, 0x13b}, {0x140, 0x140}, {0x141, 0x141}, {0x142, 0x142}, + {0x143, 0x143}, {0x144, 0x144}, {0x145, 0x145}, {0x146, 0x146}, {0x147, 0x147}, {0x148, 0x148}, {0x149, 0x149}, {0x14a, 0x14a}, + {0x14b, 0x14b}, {0x14c, 0x14c}, {0x14d, 0x14d}, {0x14e, 0x14e}, {0x14f, 0x14f}, {0x150, 0x150}, {0x151, 0x151}, {0x152, 0x152}, + {0x153, 0x153}, {0x154, 0x154}, {0x155, 0x155}, {0x156, 0x156}, {0x157, 0x157}, {0x158, 0x158}, {0x159, 0x159}, {0x15a, 0x15a}, + {0x15b, 0x15b}, {0x15c, 0x15c}, {0x15d, 0x15d}, {0x15e, 0x15e}, {0x15f, 0x15f}, {0x160, 0x160}, {0x164, 0x164}, {0x165, 0x165}, + {0x166, 0x166}, {0x167, 0x167}, {0x168, 0x168}, {0x169, 0x169}, {0x16a, 0x16a}, {0x16b, 0x16b}, {0x16c, 0x16c}, {0x16d, 0x16d}, + {0x16e, 0x16e}, {0x16f, 0x16f}, {0x170, 0x170}, {0x171, 0x171}, {0x172, 0x172}, {0x173, 0x173}, {0x174, 0x174}, {0x175, 0x175}, + {0x176, 0x176}, {0x177, 0x177}, {0x180, 0x180}, {0x181, 0x181}, {0x182, 0x182}, {0x183, 0x183}, {0x184, 0x184}, {0x185, 0x185}, + {0x186, 0x186}, {0x187, 0x187}, {0x188, 0x188}, {0x18a, 0x18a}, {0x18b, 0x18b}, {0x18c, 0x18c}, {0x18d, 0x18d}, {0x18e, 0x18e}, + {0x18f, 0x18f}, {0x190, 0x190}, {0x191, 0x191}, {0x192, 0x192}, {0x193, 0x193}, {0x194, 0x194}, {0x195, 0x195}, {0x196, 0x196}, + {0x197, 0x197}, {0x198, 0x198}, {0x199, 0x199}, {0x19a, 0x19a}, {0x1a0, 0x1a0}, {0x1a1, 0x1a1}, {0x1a2, 0x1a2}, {0x1a3, 0x1a3}, + {0x1a4, 0x1a4}, {0x1a5, 0x1a5}, {0x1a7, 0x1a7}, {0x1aa, 0x1aa}, {0x1ab, 0x1ab}, {0x1ae, 0x1ae}, {0x1af, 0x1af}, {0x1b1, 0x1b1}, + {0x1b3, 0x1b3}, {0x1b4, 0x1b4}, {0x1b5, 0x1b5}, {0x1b6, 0x1b6}, {0x1b7, 0x1b7}, {0x1b8, 0x1b8}, {0x1b9, 0x1b9}, {0x1ba, 0x1ba}, + {0x1bb, 0x1bb}, {0x1bc, 0x1bc}, {0x1bd, 0x1bd}, {0x1be, 0x1be}, {0x1bf, 0x1bf}, {0x1c0, 0x1c0}, {0x1c1, 0x1c1}, {0x1c2, 0x1c2}, + {0x1c3, 0x1c3}, {0x1c4, 0x1c4}, {0x1d4, 0x1d4}, {0x1d5, 0x1d5}, {0x303, 0x303}, {0x307, 0x307}, {0x308, 0x308}, {0x30d, 0x30d}, + {0x30e, 0x30e}, {0x314, 0x314}, {0x346, 0x346}, {0x347, 0x347}, {0x351, 0x351}, {0x354, 0x354}, {0x357, 0x357}, {0x35e, 0x35e}, + {0x36d, 0x36d}, {0x36f, 0x36f}, {0x371, 0x371}, {0x372, 0x372}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 56> Mubuf{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x18, 0x18}, {0x1a, 0x1a}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x30, 0x30}, {0x31, 0x31}, + {0x32, 0x32}, {0x33, 0x33}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x50, 0x50}, {0x51, 0x51}, + {0x52, 0x52}, {0x53, 0x53}, {0x55, 0x55}, {0x56, 0x56}, {0x57, 0x57}, {0x58, 0x58}, {0x59, 0x59}, {0x5a, 0x5a}, + {0x5b, 0x5b}, {0x5c, 0x5c}, {0x5d, 0x5d}, {0x5e, 0x5e}, {0x5f, 0x5f}, {0x60, 0x60}, {0x70, 0x71}, {0x71, 0x71}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 124> Ds{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, + {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, + {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, + {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, + {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x42, 0x42}, + {0x43, 0x43}, {0x44, 0x44}, {0x45, 0x45}, {0x46, 0x46}, {0x47, 0x47}, {0x48, 0x48}, {0x49, 0x49}, {0x4a, 0x4a}, + {0x4b, 0x4b}, {0x4c, 0x4c}, {0x4d, 0x4d}, {0x4e, 0x4e}, {0x4f, 0x4f}, {0x50, 0x50}, {0x51, 0x51}, {0x52, 0x52}, + {0x53, 0x53}, {0x60, 0x60}, {0x61, 0x61}, {0x62, 0x62}, {0x63, 0x63}, {0x64, 0x64}, {0x65, 0x65}, {0x66, 0x66}, + {0x67, 0x67}, {0x68, 0x68}, {0x69, 0x69}, {0x6a, 0x6a}, {0x6b, 0x6b}, {0x6c, 0x6c}, {0x6d, 0x6d}, {0x6e, 0x6e}, + {0x6f, 0x6f}, {0x70, 0x70}, {0x71, 0x71}, {0x72, 0x72}, {0x73, 0x73}, {0x76, 0x76}, {0x77, 0x77}, {0x78, 0x78}, + {0x7e, 0x7e}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, + {0x87, 0x87}, {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, {0x8d, 0x8d}, {0x92, 0x92}, {0x93, 0x93}, + {0xde, 0xde}, {0xdf, 0xdf}, {0xfe, 0xfe}, {0xff, 0xff}, +}}; + +inline constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 92> Mimg{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x8, 0x8}, {0x9, 0x9}, + {0xa, 0xa}, {0xb, 0xb}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x14, 0x14}, + {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, + {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, + {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, + {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, {0x33, 0x33}, {0x34, 0x34}, + {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, + {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x44, 0x44}, {0x45, 0x45}, {0x46, 0x46}, + {0x47, 0x47}, {0x48, 0x48}, {0x49, 0x49}, {0x4c, 0x4c}, {0x4d, 0x4d}, {0x4e, 0x4e}, {0x4f, 0x4f}, {0x50, 0x50}, + {0x51, 0x51}, {0x54, 0x54}, {0x55, 0x55}, {0x56, 0x56}, {0x57, 0x57}, {0x58, 0x58}, {0x59, 0x59}, {0x5c, 0x5c}, + {0x5d, 0x5d}, {0x5e, 0x5e}, {0x5f, 0x5f}, {0x60, 0x60}, {0x68, 0x68}, {0x69, 0x69}, {0x6a, 0x6a}, {0x6b, 0x6b}, + {0x6c, 0x6c}, {0x6d, 0x6d}, {0x6e, 0x6e}, {0x6f, 0x6f}, +}}; + +} + +#endif diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index c3d1a68701..f28258ca1e 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -1,6 +1,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include <algorithm> #include <array> +#include <utility> namespace ShaderRecompiler { @@ -139,6 +140,76 @@ bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3) { return DepthBitsTextureWidth(word1, word3) != 0u; } +const std::array<std::uint8_t, 128>& GcnBufferFormats() { + static const auto table = [] { + std::array<std::uint8_t, 128> formats{}; + struct Family { std::uint32_t data; std::uint32_t first; bool normalized; bool floating; }; + constexpr std::array<Family, 10> normalizedFamilies{{ + {1u, 1u, true, false}, {2u, 7u, true, true}, {3u, 14u, true, false}, {5u, 23u, true, true}, {6u, 30u, true, true}, + {7u, 37u, true, true}, {8u, 44u, true, false}, {9u, 50u, true, false}, {10u, 56u, true, false}, {12u, 65u, true, true}, + }}; + for (const auto& family : normalizedFamilies) { + for (std::uint32_t number = 0; number < 6u; ++number) formats[(family.data << 3u) | number] = static_cast<std::uint8_t>(family.first + number); + if (family.floating) formats[(family.data << 3u) | 7u] = static_cast<std::uint8_t>(family.first + 6u); + } + constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 4> wideFamilies{{{4u, 20u}, {11u, 62u}, {13u, 72u}, {14u, 75u}}}; + for (const auto& [data, first] : wideFamilies) { + formats[(data << 3u) | 4u] = static_cast<std::uint8_t>(first); + formats[(data << 3u) | 5u] = static_cast<std::uint8_t>(first + 1u); + formats[(data << 3u) | 7u] = static_cast<std::uint8_t>(first + 2u); + } + return formats; + }(); + return table; +} + +std::uint32_t GcnImageFormat(std::uint32_t dataFormat, std::uint32_t numberFormat) { + constexpr std::uint32_t Unorm = 0u, Snorm = 1u, Float = 7u, Srgb = 9u; + if (dataFormat < 16u && numberFormat < 8u) return GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; + switch (dataFormat) { + case 1u: return numberFormat == Srgb ? 128u : 0u; + case 3u: return numberFormat == Srgb ? 129u : 0u; + case 10u: return numberFormat == Srgb ? 130u : 0u; + case 16u: return numberFormat == Unorm ? 133u : 0u; + case 17u: return numberFormat == Unorm ? 134u : 0u; + case 18u: return numberFormat == Unorm ? 135u : 0u; + case 19u: return numberFormat == Unorm ? 136u : 0u; + case 34u: return numberFormat == Float ? 132u : 0u; + case 35u: return numberFormat == Unorm ? 169u : numberFormat == Srgb ? 170u : 0u; + case 36u: return numberFormat == Unorm ? 171u : numberFormat == Srgb ? 172u : 0u; + case 37u: return numberFormat == Unorm ? 173u : numberFormat == Srgb ? 174u : 0u; + case 38u: return numberFormat == Unorm ? 175u : numberFormat == Snorm ? 176u : 0u; + case 39u: return numberFormat == Unorm ? 177u : numberFormat == Snorm ? 178u : 0u; + case 40u: return numberFormat == Unorm ? 179u : numberFormat == Snorm ? 180u : 0u; + case 41u: return numberFormat == Unorm ? 181u : numberFormat == Srgb ? 182u : 0u; + default: return 0u; + } +} + +std::array<std::uint32_t, 8> GcnImageWords(std::span<const std::uint32_t, 8> gcn) { + const auto format = GcnImageFormat((gcn[1] >> 20u) & 0x3fu, (gcn[1] >> 26u) & 0xfu); + const auto width = gcn[2] & 0x3fffu; + const auto height = (gcn[2] >> 14u) & 0x3fffu; + const auto type = gcn[3] >> 28u; + const auto lastLevel = (gcn[3] >> 16u) & 0xfu; + const bool layered = type == 11u || type == 12u || type == 13u || type == 15u; + std::array<std::uint32_t, 8> words{}; + words[0] = gcn[0]; + words[1] = (gcn[1] & 0x000fffffu) | (format << 20u) | ((width & 3u) << 30u); + words[2] = (width >> 2u) | (height << 14u); + words[3] = (gcn[3] & 0x000fffffu) | (type << 28u); + words[4] = (layered ? (gcn[5] >> 13u) & 0x1fffu : gcn[4] & 0x1fffu) | ((gcn[5] & 0x1fffu) << 16u); + words[5] = lastLevel << 4u; + words[7] = ((gcn[3] >> 20u) & 0x1fu) | (((gcn[3] >> 25u) & 1u) << 5u) | (((gcn[4] >> 13u) & 0x3fffu) << 6u); + return words; +} + +std::uint32_t GcnBufferWord3(std::uint32_t word1, std::uint32_t word3) { + const std::uint32_t format = GcnBufferFormats()[(word3 >> 12u) & 0x7fu]; + const std::uint32_t outOfBounds = ((word1 >> 16u) & 0x3fffu) != 0u ? 1u : 3u; + return (word3 & 0xc0e00fffu) | (format << 12u) | (outOfBounds << 28u); +} + IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format8UInt: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index abdb151c23..29cdbcc01b 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -90,6 +90,11 @@ RdnaInstructionFamily GetRdnaInstructionFamily(std::uint32_t word) { } RdnaInstruction DecodeRdnaInstruction(std::uint32_t programCounter, std::span<const std::uint32_t> code, std::uint32_t wordIndex) { + if (GcnGuestShaders()) return DecodeGcnInstruction(programCounter, code, wordIndex); + return DecodeRdnaEncodedInstruction(programCounter, code, wordIndex); +} + +RdnaInstruction DecodeRdnaEncodedInstruction(std::uint32_t programCounter, std::span<const std::uint32_t> code, std::uint32_t wordIndex) { if (wordIndex >= code.size()) { throw std::out_of_range("word index is out of the code span bounds"); } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..dae9b85256 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -231,6 +231,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.shaderHash = request.shader.codeAddress; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; + translateOptions.gcnLegacyVertexInputs = GcnGuestShaders() && request.context.userDataBaseRegister == 0u; translateOptions.userDataCount = static_cast<std::uint32_t>(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request); diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bcda9f0c40..912e93e844 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1,4 +1,5 @@ #include "ShaderDiskCache.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "CacheKey.hpp" #include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" @@ -1780,6 +1781,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); + writer.Value(static_cast<std::uint8_t>(GcnGuestShaders())); thread_local std::vector<std::uint64_t>* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); RecompileCacheKey::Build(request, memoryKey); @@ -1800,6 +1802,7 @@ void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSiz writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); + writer.Value(static_cast<std::uint8_t>(GcnGuestShaders())); writer.Value(PlanEntryKind); thread_local std::vector<std::uint64_t>* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index dc3a268ee3..92ea828d77 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -187,6 +187,7 @@ struct SpirvEmitterState { std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; std::uint32_t srgbTableVariable = 0; + std::uint32_t gcnBufferFormatVariable = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index aa7dc2bbe8..b71a38a1c1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -5,8 +5,11 @@ #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvMemory/SpirvInputOutput.hpp" #include "PipelineSpecialization.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include <algorithm> #include <array> +#include <span> #include <utility> #include <string> #include <vector> @@ -47,6 +50,23 @@ std::uint32_t nonzero(SpirvEmitterState& state, std::uint32_t value) { return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0u)); } +std::uint32_t gcnWord3(SpirvEmitterState& state, std::uint32_t word1, std::uint32_t word3) { + const auto u32 = TypeU32(state); + const auto& formats = GcnBufferFormats(); + std::array<std::uint32_t, 128> words{}; + for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, formats[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, static_cast<std::uint32_t>(words.size()))); + if (state.gcnBufferFormatVariable == 0) state.gcnBufferFormatVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, state.gcnBufferFormatVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span<const std::uint32_t>(words))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, state.gcnBufferFormatVariable, field(state, word3, 12u, 7u)); + const auto format = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, format, pointer); + const auto outOfBounds = Select(state, u32, nonzero(state, field(state, word1, 16u, 14u)), ConstantU32(state, 1u << 28u), ConstantU32(state, 3u << 28u)); + const auto kept = Binary(state, spv::OpBitwiseAnd, u32, word3, ConstantU32(state, 0xc0e00fffu)); + return Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, kept, Binary(state, spv::OpShiftLeftLogical, u32, format, ConstantU32(state, 12u))), outOfBounds); +} + void faultIf(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, BdaAbi::FaultReason reason) { auto& state = context.state; EmitIfCondition(state, condition, [&] { RecordBdaFault(state, address, ConstantU32(state, bytes), BdaInstructionPc(state, instruction), reason); }); @@ -60,14 +80,16 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto& memory = context.Memory(instruction); const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + if (runtime && GcnGuestShaders()) word1 = Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1, ConstantU32(state, GcnBufferWord1Mask)); const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted); - const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + if (runtimeFormat && GcnGuestShaders()) word3 = gcnWord3(state, word1, word3); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { const auto u64 = TypeScalarU64(state); - const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, GcnGuestShaders() ? Binary(state, spv::OpBitwiseAnd, u32, context.Arg(*handle, 1u), ConstantU32(state, GcnBufferWord1Mask)) : context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, context.Arg(*handle, 0u)), high); } auto invalid = nonzero(state, field(state, word3, 30u, 2u)); @@ -526,7 +548,8 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i } const auto* word = instruction.Argument(0)->Resolve()->Argument(3)->Resolve(); if (memory.gpuDescriptor && word->HasImmediate()) { - const auto format = static_cast<IrBufferFormat>((word->ImmediateU32() >> 12u) & 0x7fu); + const auto bits = (word->ImmediateU32() >> 12u) & 0x7fu; + const auto format = static_cast<IrBufferFormat>(GcnGuestShaders() ? GcnBufferFormats()[bits] : bits); if (format == IrBufferFormat::Invalid) context.Fail(instruction, "formatted buffer instruction has an invalid constant format"); return formatted(context, instruction, resource, GetFormatInfo(format), components, store, data); } diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index e54dfdec75..2c190b4239 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -36,6 +36,7 @@ struct TranslateOptions { bool interpolationFunctions = false; bool nativeFloat16 = false; bool float32FlushToZero = false; + bool gcnLegacyVertexInputs = false; std::optional<ShaderFloatMode> floatMode; ShaderStageInputInfo inputInfo; const EmbeddedFetchPlan* embeddedFetch = nullptr; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index cbd1075512..4a3849fbd6 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -446,8 +446,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO entryIr.SetScalarReg(static_cast<ScalarReg>(2), entryIr.Constant((lanes << 12u) | (lanes << 22u))); entryIr.SetScalarReg(static_cast<ScalarReg>(3), entryIr.Constant(lanes | (lanes << 8u))); } - setVector(static_cast<VectorReg>(5), drawIndex(StageInputKind::VertexIndex)); - setVector(static_cast<VectorReg>(8), drawIndex(StageInputKind::InstanceIndex)); + if (options.gcnLegacyVertexInputs) { + setVector(static_cast<VectorReg>(0), drawIndex(StageInputKind::VertexIndex)); + for (std::uint32_t reg = 1u; reg <= 3u; reg++) setVector(static_cast<VectorReg>(reg), drawIndex(StageInputKind::InstanceIndex)); + } else { + setVector(static_cast<VectorReg>(5), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast<VectorReg>(8), drawIndex(StageInputKind::InstanceIndex)); + } } } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 551838eb6d..d067e8518a 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,6 +22,9 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) +- [GCN instructions](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - PS4 (Sea Islands) instructions are re-encoded to their RDNA encodings with opcode tables built from shadPS4's `opcodes.h` and decoded by the RDNA decoder; an encoding without an RDNA counterpart throws. MIMG has no DIM field on GCN: the dimension is taken as 2D, or 2D array when DA is set, and the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects a T# whose type needs another address count. MIMG word 1 bits [31:26] are reserved on GCN (A16 and D16 on RDNA). +- [GCN buffer and image descriptors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) (shader, PS4) - a GCN V# word 3 (DATA_FORMAT [18:15], NUM_FORMAT [14:12], names read from the most significant field) is rewritten to the unified format of the PS5 layout with OOB_SELECT 1 for a strided buffer and 3 otherwise, following the GFX6-7 NUM_RECORDS rules in Mesa's `ac_descriptors`; ATC, HASH, HEAP and MTYPE are dropped. The V# base is 44 bits, as in shadPS4's `AmdGpu::Buffer`: Limbo leaves 0x7 in word 1 bits [15:12]. A GCN T# is rewritten to the PS5 layout with SW_MODE linear, keeping TILING_INDEX [4:0], POW2_PAD [5] and PITCH - 1 [19:6] in word 7. None of it is checked on a console. +- [PS4 legacy vertex shaders](../../../core/shader/recompiler/Recompiler.cpp) (shader, PS4) - a PS4 vertex program at the VS slot takes its user data from s0, its vertex index in v0 and its instance index in v1-v3, and a fetch shader it calls through the user pair (`s_swappc_b64 s[0:1], s[0:1]`) returns through the same pair. These rules apply only while GCN shaders are decoded. ### Functional From 11d145855e4a50e042df1e2234e2e218fb06b4ec Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:28:13 +0300 Subject: [PATCH 0971/1360] fix(shader): keep the literal word of a GCN s_setreg_imm32_b32 The SOPK branch of the GCN decoder never set the literal for s_setreg_imm32_b32, so the eight-byte instruction was re-encoded as one word and its decode threw "literal operand word is out of the code span bounds". The decoder now copies the literal for that opcode; the new agc_gcn_decode test decodes the reported stream. Co-authored-by: javierUDP <35921725+javierUDP@users.noreply.github.com> Co-authored-by: Savvanis Spyros <savvaniss@yahoo.gr> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++++++ .../prx/libSceAgcDriver/tests/GcnDecode.cpp | 45 +++++++++++++++++++ .../RdnaDecoder/src/GcnInstructionDecoder.cpp | 2 + 3 files changed, 60 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4a6440642d..5d300d3b4d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3365,6 +3365,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_gcn_decode_tests EXCLUDE_FROM_ALL tests/GcnDecode.cpp) +target_link_libraries(agc_gcn_decode_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_gcn_decode_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_gcn_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_gcn_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_gcn_decode COMMAND agc_gcn_decode_tests) + if(WIN32) + set_tests_properties(agc_gcn_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() + add_test_executable(agc_pixel_position_export_tests EXCLUDE_FROM_ALL tests/PixelPositionExport.cpp) target_link_libraries(agc_pixel_position_export_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_pixel_position_export_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp new file mode 100644 index 0000000000..855c0aecdb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp @@ -0,0 +1,45 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include <array> +#include <cstdint> +#include <cstdio> +#include <exception> +#include <span> + +using namespace ShaderRecompiler; + +namespace { + +int failures = 0; + +void Check(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + ++failures; +} + +RdnaInstruction Decode(std::span<const std::uint32_t> words) { + return DecodeGcnInstruction(0u, words, 0u); +} + +void SetregImmKeepsItsLiteral() { + const std::array<std::uint32_t, 3> setreg{0xba80f801u, 0x12345678u, 0xbf810000u}; + const auto instruction = Decode(setreg); + Check(instruction.op == RdnaOpcode::SSetregImm32B32, "s_setreg_imm32_b32 did not decode as its RDNA opcode"); + Check(instruction.wordCount == 2u, "a GCN s_setreg_imm32_b32 is not two words"); + Check(instruction.source0.value == 0x12345678u, "s_setreg_imm32_b32 lost its literal"); + const std::array<std::uint32_t, 2> movk{0xb0001234u, 0xbf810000u}; + Check(Decode(movk).wordCount == 1u, "s_movk_i32 took a literal word"); +} + +} + +int main() { + try { + SetregImmKeepsItsLiteral(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (failures == 0) std::puts("GCN decode tests passed"); + return failures == 0 ? 0 : 1; +} diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp index b92bde53df..e5ccedb328 100644 --- a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp @@ -20,6 +20,7 @@ constexpr std::uint32_t VccSdst = 106u; constexpr std::uint32_t NullSoffset = 0x7du; constexpr std::uint32_t LinkedCallSopk = 0x16u; constexpr std::uint32_t CodeEndSopp = 0x1fu; +constexpr std::uint32_t SetRegImmSopk = 0x15u; template <std::size_t N> std::optional<std::uint32_t> mapOpcode(const std::array<std::pair<std::uint16_t, std::uint16_t>, N>& table, std::uint32_t opcode) { @@ -82,6 +83,7 @@ Encoded encodeScalar(std::span<const std::uint32_t> code, std::uint32_t index, s mapped = opcode == LinkedCallSopk ? std::optional<std::uint32_t>(LinkedCallSopk) : mapOpcode(GcnTables::Sopk, opcode); if (!mapped) unsupported("SOPK", opcode, programCounter); out.words[0] = replaceBits(word, 23, 5, *mapped); + literal = opcode == SetRegImmSopk; } else { opcode = (word >> 23u) & 0x7fu; mapped = mapOpcode(GcnTables::Sop2, opcode); From 930b9640d797f006847c592d5a2b46b1d395cb2b Mon Sep 17 00:00:00 2001 From: SP4C3B4R-8 <SP4C3B4R-8@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:19:15 +0200 Subject: [PATCH 0972/1360] refactor(agc): split Graphics::Draw into a prepare and a record step --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 207 +++++++++++++----- 1 file changed, 148 insertions(+), 59 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 90c38e18c1..5ab8a81dc6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2148,8 +2148,66 @@ std::optional<std::string> KnownValidationFailure(const Context& context, std::s return std::nullopt; } -void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut) { +namespace { + +struct TargetBinding { + ColorTarget color; + bool gpuTiling = false; + std::shared_ptr<StorageTexture> resident; + bool proxied = false; + std::unique_ptr<Buffer> transfer; + std::unique_ptr<Buffer> tiled; + std::unique_ptr<DeviceBuffer> tiledDevice; + std::unique_ptr<DeviceBuffer> linearDevice; + std::vector<std::byte> original; + TileMipLayout mip{}; + std::unique_ptr<RenderTarget> target; + bool multisampled = false; + VkImage multisampleImage = VK_NULL_HANDLE; + std::shared_ptr<void> multisampleOwner; + std::unique_ptr<Buffer> dump; +}; + +struct PreparedDraw { + explicit PreparedDraw(bool profile) : timer(profile) {} + PreparedDraw(const PreparedDraw&) = delete; + PreparedDraw& operator=(const PreparedDraw&) = delete; + std::shared_ptr<const DrawRecipe>* recipeOut = nullptr; std::vector<std::uint32_t> stencilReferences; + std::optional<PerformanceTimer> timing; + DrawTimer timer; + DrawOutcome outcome; + const ShaderResources::BuildTiming* built = nullptr; + double waitedBefore = 0; + double ownWaitedMs = 0; + DrawInputs inputs; + const Pm4::DrawParameters::IndirectDraw* args = nullptr; + int dumpLimit = 0; + std::vector<TargetBinding> targets; + std::vector<VkImageView> targetViews; + Recorder* recorder = nullptr; + ResolvedResources resolved; + IndirectRecord indirect; + std::vector<Pm4::DrawArguments> records; + bool completion = false; + bool listed = false; + bool recorded = false; + bool lean = false; + bool multisampled = false; + std::optional<State> masked; + std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay; + std::shared_ptr<Pipeline> pipeline; + std::vector<std::shared_ptr<StorageTexture>> owners; + std::vector<std::shared_ptr<void>> framebufferOwners; + std::shared_ptr<Framebuffer> framebuffer; + std::vector<SampleOrderTarget> reordered; + SampleOrderPass reorderPass; + std::optional<DepthSampleLocations> depthLocations; +}; + +bool prepareDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, PreparedDraw& prepared) { + auto*& recipeOut = prepared.recipeOut; + auto& stencilReferences = prepared.stencilReferences; for (const auto& shader : shaders) { if (shader.program != nullptr && !shader.program->stencilReferences.empty()) { Require(shader.stage == ShaderRecompiler::ShaderStage::Fragment && stencilReferences.empty(), "stencil replay requires exactly one fragment stage"); @@ -2174,58 +2232,35 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } if (state.quadList) recipeOut = nullptr; if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); - PerformanceTimer timing("Graphics.Draw"); + auto& timing = prepared.timing.emplace("Graphics.Draw"); // APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals. static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - DrawTimer timer(profile); + auto& timer = prepared.timer; + timer = DrawTimer(profile); // APS5_TRACE_DRAWS (together with APS5_PROFILE_DRAW; alone it only enables Driver.cpp's own // [draw] lines) additionally prints one line per draw with its phases and, for synchronous // draws, their inputs: per-draw string building at ~1400 draws per 10 s is itself a cost the // totals must not carry. static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; - DrawOutcome outcome; - const ShaderResources::BuildTiming* built = nullptr; + auto& outcome = prepared.outcome; + auto& built = prepared.built; // The thread's GPU waits so far: the difference at the report, less the draw's own syncs // (`ownWaitedMs`, sampled around them), is what this draw waited for through the flush hook // (nested in the draw's hold, see DrawOutcome::hookWaitUs). - const auto waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; - double ownWaitedMs = 0; + const auto waitedBefore = prepared.waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; + auto& ownWaitedMs = prepared.ownWaitedMs; const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; - auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); - if (inputs.nothing) return; - const auto* args = draw.indirect ? &*draw.indirect : nullptr; + auto& inputs = prepared.inputs; + inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); + if (inputs.nothing) return false; + const auto* args = prepared.args = draw.indirect ? &*draw.indirect : nullptr; const auto indexBytes = inputs.indexBytes; timing.Mark("validate"); timing.Mark("index_upload"); - // One binding per color attachment. Tiled targets are detiled and retiled on the GPU in video memory; - // only changed guest blocks are stored back. - struct TargetBinding { - ColorTarget color; - bool gpuTiling = false; - // Resident target: the surface's cached storage image is attached directly and marked dirty - // afterwards, so nothing is copied in or out per draw. - std::shared_ptr<StorageTexture> resident; - bool proxied = false; - // Linear pixels converted on the CPU, for targets the GPU detiler does not handle. - std::unique_ptr<Buffer> transfer; - // Guest bytes in host memory, and their device-local copies for the detiler. - std::unique_ptr<Buffer> tiled; - std::unique_ptr<DeviceBuffer> tiledDevice; - std::unique_ptr<DeviceBuffer> linearDevice; - std::vector<std::byte> original; - TileMipLayout mip{}; - std::unique_ptr<RenderTarget> target; - bool multisampled = false; - VkImage multisampleImage = VK_NULL_HANDLE; - std::shared_ptr<void> multisampleOwner; - // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_<address>_<n>.raw. - std::unique_ptr<Buffer> dump; - }; - const int dumpLimit = DumpTargetLimit(); - static std::mutex dumpMutex; - static std::map<std::uint64_t, int> dumped; - std::vector<TargetBinding> targets(state.colors.size()); - std::vector<VkImageView> targetViews; + const int dumpLimit = prepared.dumpLimit = DumpTargetLimit(); + auto& targets = prepared.targets; + targets.resize(state.colors.size()); + auto& targetViews = prepared.targetViews; for (std::size_t index = 0; index < state.colors.size(); ++index) { auto& binding = targets[index]; binding.color = state.colors[index]; @@ -2320,9 +2355,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); - auto* recorder = Recorder::Active(); + auto* recorder = prepared.recorder = Recorder::Active(); bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); - auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); + auto& resolved = prepared.resolved; + resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; if (!stencilReferences.empty() && resources->ChecksFaults()) recordable = false; if (!stencilReferences.empty()) { @@ -2362,9 +2398,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // and records plain draws with the same per-dimension rules. The import pointer stays valid: // refreshImports runs only under GuestMemory::GpuMutex, held here, and a retired import's // buffer is kept while the recorder is busy. - IndirectRecord indirect; + auto& indirect = prepared.indirect; indirect.args = args; - std::vector<Pm4::DrawArguments> records; + auto& records = prepared.records; if (args != nullptr) { static const bool gpuIndirectDraws = std::getenv("APS5_NO_GPU_INDIRECT_DRAW") == nullptr; const auto decide = [&](std::uint64_t address, std::size_t bytes, const HostImport*& import) { @@ -2385,7 +2421,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto rangeBytes = args->RangeBytes(); if (rangeBytes == 0) { report(" indirect draw without records"); - return; + return false; } Require(rangeBytes <= std::numeric_limits<std::size_t>::max(), "indirect draw record range overflow"); if (!gpuIndirectDraws) indirect.path = IndirectDrawPath::Disabled; @@ -2406,7 +2442,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (std::none_of(records.begin(), records.end(), [](const Pm4::DrawArguments& record) { return record.count != 0 && record.instances != 0; })) { CountIndirectDraw(indirect.path, indirect.readMs); report(" indirect draw with empty records"); - return; + return false; } } } @@ -2431,14 +2467,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& static const bool syncCompletionDraws = std::getenv("APS5_SYNC_COMPLETION_DRAWS") != nullptr; static const bool recorderSyncDraws = std::getenv("APS5_NO_RECORDER_SYNC_DRAWS") == nullptr; static const bool recordCopiedDraws = std::getenv("APS5_NO_RECORD_COPIED_DRAWS") == nullptr; - const bool completion = resources->NeedsCompletion(); + const bool completion = prepared.completion = resources->NeedsCompletion(); const bool copiedWrites = completion && resources->HasCopiedWrites(); // A lease only forces the wait when deferred release is off. const bool lease = resources->HoldsLease() && SyncLeaseWork(); outcome.completion = completion; outcome.recorded = recordable; // Whether the copied write-back is listed in DrawCopiedWriters instead of waited for. - bool listed = false; + auto& listed = prepared.listed; if (!recordable) { outcome.reason = !recordDraws || dumpLimit != 0 ? SyncDisabled : recorder == nullptr ? SyncNoRecorder : SyncNotResident; } else if (completion && (syncCompletionDraws || copiedWrites || lease)) { @@ -2448,7 +2484,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& else if (recorderSyncDraws) outcome.waited = true; else outcome.recorded = false; } - const bool recorded = outcome.recorded; + const bool recorded = prepared.recorded = outcome.recorded; // A build this recorded draw can share with later identical ones goes into the cache (a cache // hit is reusable by construction, so `recorded` holds for it; one with completion work is // never reusable). @@ -2457,8 +2493,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // aid: APS5_DRAW_TRANSITIONS=1 keeps the per-draw upload and download barriers, the layout // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; - const bool lean = recorded && !drawTransitions; - const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); + const bool lean = prepared.lean = recorded && !drawTransitions; + const bool multisampled = prepared.multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); for (const auto& binding : targets) { if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); @@ -2470,7 +2506,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (state.depth) RequireNoNativeDepthFeedback_nid_no_patch(context, *state.depth, resources->SampledTextures()); APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline"); // The state is copied only when a mask must change. - auto masked = maskedState(state, inputs.fragmentOutputs); + auto& masked = prepared.masked; + masked = maskedState(state, inputs.fragmentOutputs); if (std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied; })) recipeOut = nullptr; for (std::size_t index = 0; index < targets.size(); ++index) { if (!targets[index].proxied) continue; @@ -2485,7 +2522,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto built = resources->SetLayouts(); Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's"); } - std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay; + auto& stencilReplay = prepared.stencilReplay; for (const auto reference : stencilReferences) { auto selected = pipelineState; selected.stencilReplayReference = reference; @@ -2495,20 +2532,22 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(!selectedPipeline->SplitsFaces() && !selectedPipeline->SplitsSecondChannel(), "stencil replay refuses pipeline face or channel splits"); stencilReplay.emplace_back(reference, std::move(selectedPipeline)); } - auto pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; + auto& pipeline = prepared.pipeline; + pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; // Resident targets' views are stable while their storage image lives, so the framebuffer is // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. - std::vector<std::shared_ptr<StorageTexture>> owners; + auto& owners = prepared.owners; owners.reserve(targets.size()); - std::vector<std::shared_ptr<void>> framebufferOwners; + auto& framebufferOwners = prepared.framebufferOwners; framebufferOwners.reserve(targets.size()); for (const auto& binding : targets) { owners.push_back(binding.resident); framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident); } - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); - std::vector<SampleOrderTarget> reordered; - SampleOrderPass reorderPass; + auto& framebuffer = prepared.framebuffer; + framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); + auto& reordered = prepared.reordered; + auto& reorderPass = prepared.reorderPass; if (pipeline->ReordersSamples()) { reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder()); for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) { @@ -2517,9 +2556,50 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order"); } } - const auto depthLocations = passDepthLocations(context, pipelineState, *pipeline); + prepared.depthLocations = passDepthLocations(context, pipelineState, *pipeline); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); + return true; +} + +void recordPreparedDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, PreparedDraw& prepared) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; + static std::mutex dumpMutex; + static std::map<std::uint64_t, int> dumped; + auto& timing = *prepared.timing; + auto& timer = prepared.timer; + auto& outcome = prepared.outcome; + const auto* built = prepared.built; + const auto waitedBefore = prepared.waitedBefore; + auto& ownWaitedMs = prepared.ownWaitedMs; + const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; + auto* recipeOut = prepared.recipeOut; + const auto& stencilReferences = prepared.stencilReferences; + auto& inputs = prepared.inputs; + const auto* args = prepared.args; + const int dumpLimit = prepared.dumpLimit; + auto& targets = prepared.targets; + const auto& targetViews = prepared.targetViews; + auto* recorder = prepared.recorder; + auto& resolved = prepared.resolved; + auto& resources = resolved.resources; + const auto& contentKey = resolved.contentKey; + const bool cacheable = resolved.cacheable; + auto& indirect = prepared.indirect; + const bool completion = prepared.completion; + const bool listed = prepared.listed; + const bool recorded = prepared.recorded; + const bool lean = prepared.lean; + const bool multisampled = prepared.multisampled; + const auto& masked = prepared.masked; + const auto& stencilReplay = prepared.stencilReplay; + const auto& pipeline = prepared.pipeline; + const auto& owners = prepared.owners; + const auto& framebuffer = prepared.framebuffer; + const auto& reordered = prepared.reordered; + const auto& reorderPass = prepared.reorderPass; + const auto& depthLocations = prepared.depthLocations; if (lean) { RecordedDraw record; record.recorder = recorder; @@ -2844,6 +2924,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Draw finished"); } +} + +void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + PreparedDraw prepared(profile); + prepared.recipeOut = recipeOut; + if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared); +} + DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, const DrawRecipe& recipe) { if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); static_cast<void>(snapshots); From a13d01bcc9416de62805c4213485687124afe14b Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:19:13 +0300 Subject: [PATCH 0973/1360] fix(shader): keep the PS5 user-data pointer check unchanged outside PS4 shaders unchangedUntil skipped a write by the reading instruction itself for every shader, so an RDNA s_load that overwrites its own address pair was taken as a user-data pointer load where main refused it. The skip now applies only to PS4 (GCN) shaders, where it is needed; PS5 shaders take main's path. --- core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index eaccf98c5c..afe341bb79 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1293,8 +1293,9 @@ std::set<std::uint32_t> branchLabels(const RdnaProgram& program) { bool unchangedUntil(const RdnaProgram& program, std::uint32_t reg, std::uint32_t reader, std::uint32_t waveSize) { for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { - if (index == reader || !writesScalar(program.instructions[index], reg, waveSize)) continue; - if (index < reader) return false; + const bool readerWritesAfterReading = index == reader && GcnGuestShaders(); + if (readerWritesAfterReading || !writesScalar(program.instructions[index], reg, waveSize)) continue; + if (index <= reader) return false; for (std::uint32_t later = index; later < program.instructions.size(); ++later) { const auto& instruction = program.instructions[later]; if ((IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) && instruction.branchTarget <= program.instructions[reader].programCounter) return false; From c27787a7db406ddff2872479fc1030b5dc6fff29 Mon Sep 17 00:00:00 2001 From: mugurc <mceker@gmail.com> Date: Mon, 12 Oct 2026 15:12:03 +0300 Subject: [PATCH 0974/1360] fix(libc): build the host-facing libc code on macOS feof_unlocked, ferror_unlocked, getc_unlocked and putc_unlocked are macros on macOS, so they are called without the global scope qualifier. The remaining differences are the ones Darwin's headers and libSystem have: - CLOCK_REALTIME_COARSE and st_mtim do not exist; the write mark uses CLOCK_REALTIME and st_mtimespec. - tm_zone is a non-const char*. - __cxa_thread_atexit returns void, so the wrapper reports success. - Darwin's stack protector reads __stack_chk_guard from libSystem; the guard of the guest libc has the postfixed name, which receives the NID of the original. - APS5_EXPORT spells the symbols with the leading underscore of Mach-O. - GuestArenaSetProtection and PosixProtection are built on macOS as on Linux. - The fclose failure test fills /dev/full, which exists on Linux only. exception_runtime relies on the Itanium unwinder of libc, which is not registered on macOS (the host libc++abi is used), so its test is only registered elsewhere. --- core/libs/prx/libc/CMakeLists.txt | 8 +++---- core/libs/prx/libc/include/FileStream.hpp | 4 ++-- .../prx/libc/include/general/ExportMacros.hpp | 6 ++++- core/libs/prx/libc/src/CxxAbiSupport.cpp | 22 +++++++++++-------- core/libs/prx/libc/src/General.cpp | 8 +++++++ core/libs/prx/libc/src/StandardStreams.cpp | 4 ++-- core/libs/prx/libc/src/Time.cpp | 4 ++++ .../prx/libc/src/specifics/gcc/StackGuard.cpp | 4 ++++ core/libs/tests/Formatting.cpp | 7 ++++++ core/libs/tests/GuestFiopen.cpp | 2 +- 10 files changed, 50 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d53c2d0461..68e970b46d 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -817,10 +817,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() if(UNIX) - add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) - target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(exception_runtime_tests PRIVATE libc) - add_test(NAME exception_runtime COMMAND exception_runtime_tests) add_test_executable(exception_personality_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionPersonality.cpp) target_include_directories(exception_personality_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(exception_personality_tests PRIVATE libc) @@ -835,6 +831,10 @@ if(BUILD_TESTING) add_test(NAME guest_path_lifetime COMMAND guest_path_lifetime_tests) set_tests_properties(guest_path_lifetime PROPERTIES TIMEOUT 20) if(NOT APPLE) + add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) + target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(exception_runtime_tests PRIVATE libc) + add_test(NAME exception_runtime COMMAND exception_runtime_tests) enable_language(C) add_test_executable(exception_c_cleanup_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.cpp ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c) set_source_files_properties(${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c PROPERTIES COMPILE_OPTIONS -fexceptions) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 7a81d29f07..1056a6d4c6 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -228,8 +228,8 @@ public: const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; #else - const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; - const bool error = ::ferror_unlocked(GetHandle()) != 0; + const bool endOfFile = feof_unlocked(GetHandle()) != 0; + const bool error = ferror_unlocked(GetHandle()) != 0; #endif _guest.flags = static_cast<std::int16_t>((_guest.flags & ~0x60) | (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); diff --git a/core/libs/prx/libc/include/general/ExportMacros.hpp b/core/libs/prx/libc/include/general/ExportMacros.hpp index 49ab7fbf8c..c175c74f2b 100644 --- a/core/libs/prx/libc/include/general/ExportMacros.hpp +++ b/core/libs/prx/libc/include/general/ExportMacros.hpp @@ -1,7 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP -#if defined(__GNUC__) || defined(__clang__) +#if defined(__APPLE__) + #define APS5_EXPORT(exportName, funcName) \ + __asm__(".globl \"_" exportName "_nid_no_patch_cut\"\n\t" \ + ".set \"_" exportName "_nid_no_patch_cut\", _" #funcName) +#elif defined(__GNUC__) || defined(__clang__) #define APS5_EXPORT(exportName, funcName) \ __asm__(".globl \"" exportName "_nid_no_patch_cut\"\n\t" \ ".set \"" exportName "_nid_no_patch_cut\", " #funcName) diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index 1cbe9c38cd..ccecea01a8 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -74,18 +74,22 @@ int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(void (APS5_VABI *func)(void*) auto* context = new (std::nothrow) ThreadAtexitContext{func, arg}; if (context == nullptr) return -1; - const int result = __cxxabiv1::__cxa_thread_atexit( - [](void* opaque) { - auto* context = static_cast<ThreadAtexitContext*>(opaque); - const auto destructor = context->destructor; - void* object = context->object; - delete context; - destructor(object); - }, - context, dso); + const auto run = [](void* opaque) { + auto* context = static_cast<ThreadAtexitContext*>(opaque); + const auto destructor = context->destructor; + void* object = context->object; + delete context; + destructor(object); + }; +#ifdef __APPLE__ + __cxxabiv1::__cxa_thread_atexit(run, context, dso); + return 0; +#else + const int result = __cxxabiv1::__cxa_thread_atexit(run, context, dso); if (result != 0) delete context; return result; +#endif } int APS5_VABI __cxa_thread_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dso) { diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 26c1935c5a..6bbe2aafc7 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -376,7 +376,11 @@ bool NotBefore(const WriteMark& time, const WriteMark& mark) { using WriteMark = timespec; WriteMark Now() { timespec now; +#ifdef __APPLE__ + clock_gettime(CLOCK_REALTIME, &now); +#else clock_gettime(CLOCK_REALTIME_COARSE, &now); +#endif return now; } bool NotBefore(const WriteMark& time, const WriteMark& mark) { @@ -415,7 +419,11 @@ void FlushPath(const std::filesystem::path& path) { #else std::optional<WriteMark> WriteTime(const std::filesystem::path& path) { struct stat data; +#ifdef __APPLE__ + if (::stat(path.c_str(), &data) == 0) return data.st_mtimespec; +#else if (::stat(path.c_str(), &data) == 0) return data.st_mtim; +#endif if (errno == ENOENT || errno == ENOTDIR) return std::nullopt; throw std::system_error(errno, std::generic_category(), "Reading the write time of " + path.string()); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 2cd656f01f..4d8c34a019 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -92,7 +92,7 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { #ifdef _WIN32 const int result = _fgetc_nolock(native); #else - const int result = ::getc_unlocked(native); + const int result = getc_unlocked(native); #endif stream->SyncUnlockedStatus(); stream->KeepDirectoryReadError(result == -1); @@ -115,7 +115,7 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { #ifdef _WIN32 const int result = _fputc_nolock(value, GetNativeStream(stream)); #else - const int result = ::putc_unlocked(value, GetNativeStream(stream)); + const int result = putc_unlocked(value, GetNativeStream(stream)); #endif stream->SyncUnlockedStatus(); return result; diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index c3499f8047..f732111f27 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -101,7 +101,11 @@ std::tm toHostTm(const GuestTm& guest) { host.tm_isdst = guest.tm_isdst; #ifndef _WIN32 host.tm_gmtoff = static_cast<long>(guest.tm_gmtoff); +#ifdef __APPLE__ + host.tm_zone = const_cast<char*>(guest.tm_zone); +#else host.tm_zone = guest.tm_zone; +#endif #endif return host; } diff --git a/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp b/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp index bae022e001..a18ff70dc9 100644 --- a/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp +++ b/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp @@ -1,4 +1,8 @@ #include <cstdint> #include <cstdlib> +#ifdef __APPLE__ +extern "C" std::uintptr_t __stack_chk_guard_nid_postfix = 0xDEADBEEFCAFEBABEull; +#else std::uintptr_t __stack_chk_guard = 0xDEADBEEFCAFEBABEull; +#endif diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index f22d7880dd..987a120d95 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -10,6 +10,13 @@ #include <stdexcept> #include <cstdint> #include <cstdio> +#include <cstdarg> + +#if defined(__clang__) && !defined(_WIN32) +#define __builtin_sysv_va_list va_list +#define __builtin_sysv_va_start va_start +#define __builtin_sysv_va_end va_end +#endif extern "C" { int APS5_VABI snprintf_nid_postfix(char*, size_t, const char*, ...); diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 276459ec07..044265f11c 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -152,7 +152,7 @@ int main() { Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); fclose_nid_postfix(stream); -#ifndef _WIN32 +#ifdef __linux__ auto* full = std::fopen("/dev/full", "wb"); Require(full != nullptr); auto* fullStream = new FileStream(full, true); From 030ef8f772c62d4122fbad1268367e02db8c6d31 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:22:46 -0700 Subject: [PATCH 0975/1360] fix(hmd): check the command queries' handle and 2D VR arguments in libSceHmd's order BunnyFl ran firmware 13.42's libSceHmd exports under an emulator. The distortion and wide-near correction queries look the handle up before checking initialization: a handle other than the one sceHmdOpen stored, which stays 0 without a headset, returns HANDLE_INVALID even before sceHmdInitialize. sceHmdGet2dVrCommand checks its arguments first: PARAMETER_NULL for a null descriptor or command buffer or a null pointer at descriptor offset 0, 8 or 0x20, and PARAMETER_INVALID for a non-zero field at 0x34 or 0x38. The queries and their sceHmdDistortion* aliases now answer in that order. What handle 0 gets after initialization isn't measured, so it throws NotImplemented_nid_no_patch instead of returning HANDLE_INVALID. --- core/libs/prx/libSceHmd/CMakeLists.txt | 1 + core/libs/prx/libSceHmd/Export.cpp | 50 +++++++++----- core/libs/tests/GuestHmd.cpp | 90 ++++++++++++++++++++++---- docs/dev/debt/libSceHmd.md | 2 +- 4 files changed, 112 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceHmd/CMakeLists.txt b/core/libs/prx/libSceHmd/CMakeLists.txt index d5a15e9ca8..8db3fc4a98 100644 --- a/core/libs/prx/libSceHmd/CMakeLists.txt +++ b/core/libs/prx/libSceHmd/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_hmd_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmd.cpp) target_include_directories(guest_hmd_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_hmd_tests PRIVATE libSceHmd) + configure_windows_unwind(guest_hmd_tests) add_test(NAME guest_hmd COMMAND guest_hmd_tests) if(WIN32) set_tests_properties(guest_hmd PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index bbce694977..e0e935f610 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -2,6 +2,10 @@ #include "prx/libc/include/General.hpp" #include <atomic> +#include <cstddef> +#include <cstring> +#include <initializer_list> +#include <string> struct HmdDistortionCommandParameter; struct HmdWideNearDistortionCommandParameter; @@ -64,6 +68,28 @@ std::int32_t Initialize(const Hmd::InitializeParam* param) { return initialized.exchange(true) ? Hmd::AlreadyInitialized : 0; } +std::int32_t CommandQueryHandle(std::int32_t handle, const char* query) { + if (handle != 0) return Hmd::InvalidHandle; + if (!initialized.load()) return Hmd::NotInitialized; + NotImplemented_nid_no_patch((std::string(query) + " with handle 0 after sceHmdInitialize").c_str()); +} + +template <typename T> +T ParameterWord(const Hmd2dVrCommandParameter* parameter, std::size_t offset) { + T value; + std::memcpy(&value, reinterpret_cast<const unsigned char*>(parameter) + offset, sizeof(value)); + return value; +} + +std::int32_t Vr2dCommandArguments(const Hmd2dVrCommandParameter* parameter, const HmdCommandBuffer* commandBuffer) { + if (!parameter || !commandBuffer) return Hmd::ParameterNull; + for (const std::size_t offset : {0x00u, 0x08u, 0x20u}) { + if (ParameterWord<std::uint64_t>(parameter, offset) == 0) return Hmd::ParameterNull; + } + if (ParameterWord<std::uint32_t>(parameter, 0x34) != 0 || ParameterWord<std::uint64_t>(parameter, 0x38) != 0) return Hmd::ParameterInvalid; + return 0; +} + } struct HmdEyeDistances { @@ -437,10 +463,9 @@ int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void* buffer, std::uint } int APS5_VABI sceHmdDistortionGetCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - (void)handle; (void)parameter; (void)commandBuffer; - return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; + return CommandQueryHandle(handle, __func__); } int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { @@ -571,10 +596,9 @@ int APS5_VABI sceHmdInternalGetHmuSerialNumber(std::uint8_t* serialNumber) { } int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - (void)handle; (void)parameter; (void)commandBuffer; - return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; + return CommandQueryHandle(handle, __func__); } int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(bool* available) { @@ -727,10 +751,8 @@ int APS5_VABI sceHmdInternalMmapGetModeId(std::uint32_t* modeId) { } int APS5_VABI sceHmdDistortionGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - (void)handle; - (void)parameter; - (void)commandBuffer; - return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; + if (const auto result = Vr2dCommandArguments(parameter, commandBuffer); result != 0) return result; + return CommandQueryHandle(handle, __func__); } int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { @@ -739,10 +761,9 @@ int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { } int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - (void)handle; (void)parameter; (void)commandBuffer; - return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; + return CommandQueryHandle(handle, __func__); } int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayerParameter* layers, std::uint32_t layerCount, const HmdReprojectionSubmissionParameter* submission, std::uint64_t flipArg, const void* reserved) { @@ -751,10 +772,9 @@ int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayer } int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - (void)handle; (void)parameter; (void)commandBuffer; - return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; + return CommandQueryHandle(handle, __func__); } int APS5_VABI sceHmdInternalSetDeviceConnection(std::uint64_t connection) { @@ -968,10 +988,8 @@ int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { } int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { - (void)handle; - (void)parameter; - (void)commandBuffer; - return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; + if (const auto result = Vr2dCommandArguments(parameter, commandBuffer); result != 0) return result; + return CommandQueryHandle(handle, __func__); } } diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index a7b8179b1c..0bccb53a85 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -1,9 +1,11 @@ #include "prx/libSceHmd/Hmd.hpp" +#include <cstddef> #include <cstdio> #include <cstdlib> #include <cstring> #include <initializer_list> +#include <stdexcept> namespace { @@ -40,36 +42,94 @@ void CheckHandleQueries(std::int32_t expected) { unsigned char right[16]; unsigned char fieldOfView[16]; unsigned char sensor[64]; - unsigned char descriptor[64] = {}; - unsigned char commands[16]; std::memset(left, 0x5a, sizeof(left)); std::memset(right, 0x5a, sizeof(right)); std::memset(fieldOfView, 0x5a, sizeof(fieldOfView)); std::memset(sensor, 0x5a, sizeof(sensor)); - std::memset(commands, 0x5a, sizeof(commands)); - const auto* distortion = reinterpret_cast<const HmdDistortionCommandParameter*>(descriptor); - const auto* wideNear = reinterpret_cast<const HmdWideNearDistortionCommandParameter*>(descriptor); - const auto* vr2d = reinterpret_cast<const Hmd2dVrCommandParameter*>(descriptor); - auto* commandBuffer = reinterpret_cast<HmdCommandBuffer*>(commands); for (const auto handle : {0, -1, 1, 0x0f000000}) { Require(sceHmdGetFieldOfView(handle, nullptr) == Hmd::ParameterNull, "null field of view accepted"); Require(sceHmdGetFieldOfView(handle, reinterpret_cast<HmdFieldOfView*>(fieldOfView)) == expected, "field of view without a headset"); Require(sceHmdGet2DEyeOffset(handle, reinterpret_cast<HmdEyeOffset*>(left), reinterpret_cast<HmdEyeOffset*>(right)) == expected, "eye offset without a headset"); Require(sceHmdGet2DEyeOffset(handle, nullptr, nullptr) == expected, "eye offset with null outputs"); Require(sceHmdGetInertialSensorData(handle, reinterpret_cast<HmdInertialSensorData*>(sensor), 1) == expected, "sensor data without a headset"); - Require(sceHmdGetDistortionCorrectionCommand(handle, distortion, commandBuffer) == expected, "distortion command without a headset"); - Require(sceHmdDistortionGetCorrectionCommand(handle, distortion, commandBuffer) == expected, "aliased distortion command without a headset"); - Require(sceHmdGetWideNearDistortionCorrectionCommand(handle, wideNear, commandBuffer) == expected, "wide-near distortion command without a headset"); - Require(sceHmdDistortionGetWideNearCorrectionCommand(handle, wideNear, commandBuffer) == expected, "aliased wide-near distortion command without a headset"); - Require(sceHmdGet2dVrCommand(handle, vr2d, commandBuffer) == expected, "2D VR command without a headset"); - Require(sceHmdDistortionGet2dVrCommand(handle, vr2d, commandBuffer) == expected, "aliased 2D VR command without a headset"); } - for (const auto* bytes : {left, right, fieldOfView, commands}) { + for (const auto* bytes : {left, right, fieldOfView}) { for (std::size_t index = 0; index < 16; ++index) Require(bytes[index] == 0x5a, "a refused query wrote its output"); } for (const auto byte : sensor) Require(byte == 0x5a, "a refused sensor query wrote its output"); } +template <typename Call> +bool Unimplemented(Call call) { + try { + static_cast<void>(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void CheckCommandQueries(bool afterInitialize) { + unsigned char zeroed[64] = {}; + unsigned char vr2dBytes[64] = {}; + unsigned char commands[16]; + std::memset(commands, 0x5a, sizeof(commands)); + for (const std::size_t offset : {0x00u, 0x08u, 0x20u}) std::memset(vr2dBytes + offset, 0x11, 8); + const auto* distortion = reinterpret_cast<const HmdDistortionCommandParameter*>(zeroed); + const auto* wideNear = reinterpret_cast<const HmdWideNearDistortionCommandParameter*>(zeroed); + const auto* zeroedVr2d = reinterpret_cast<const Hmd2dVrCommandParameter*>(zeroed); + const auto* vr2d = reinterpret_cast<const Hmd2dVrCommandParameter*>(vr2dBytes); + auto* commandBuffer = reinterpret_cast<HmdCommandBuffer*>(commands); + const auto vr2dWith = [&](std::size_t offset, unsigned char value, std::size_t bytes, auto check) { + unsigned char changed[64]; + std::memcpy(changed, vr2dBytes, sizeof(changed)); + std::memset(changed + offset, value, bytes); + check(reinterpret_cast<const Hmd2dVrCommandParameter*>(changed)); + }; + for (const auto handle : {-1, 1, 0x0f000000}) { + Require(sceHmdGetDistortionCorrectionCommand(handle, distortion, commandBuffer) == Hmd::InvalidHandle, "distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdDistortionGetCorrectionCommand(handle, distortion, commandBuffer) == Hmd::InvalidHandle, "aliased distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdGetWideNearDistortionCorrectionCommand(handle, wideNear, commandBuffer) == Hmd::InvalidHandle, "wide-near distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdDistortionGetWideNearCorrectionCommand(handle, wideNear, commandBuffer) == Hmd::InvalidHandle, "aliased wide-near distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdGet2dVrCommand(handle, vr2d, commandBuffer) == Hmd::InvalidHandle, "2D VR command accepted a handle sceHmdOpen never issued"); + Require(sceHmdDistortionGet2dVrCommand(handle, vr2d, commandBuffer) == Hmd::InvalidHandle, "aliased 2D VR command accepted a handle sceHmdOpen never issued"); + } + for (const auto handle : {0, -1, 1, 0x0f000000}) { + for (const auto query : {sceHmdGet2dVrCommand, sceHmdDistortionGet2dVrCommand}) { + Require(query(handle, zeroedVr2d, commandBuffer) == Hmd::ParameterNull, "2D VR command took a parameter with null pointers"); + Require(query(handle, nullptr, commandBuffer) == Hmd::ParameterNull, "2D VR command took a null parameter"); + Require(query(handle, vr2d, nullptr) == Hmd::ParameterNull, "2D VR command took a null command buffer"); + for (const std::size_t offset : {0x00u, 0x08u, 0x20u}) { + vr2dWith(offset, 0, 8, [&](const Hmd2dVrCommandParameter* changed) { + Require(query(handle, changed, commandBuffer) == Hmd::ParameterNull, "2D VR command took a null pointer in its parameter"); + }); + } + vr2dWith(0x34, 0x01, 4, [&](const Hmd2dVrCommandParameter* changed) { + Require(query(handle, changed, commandBuffer) == Hmd::ParameterInvalid, "2D VR command took a non-zero field at 0x34"); + }); + vr2dWith(0x38, 0x01, 8, [&](const Hmd2dVrCommandParameter* changed) { + Require(query(handle, changed, commandBuffer) == Hmd::ParameterInvalid, "2D VR command took a non-zero field at 0x38"); + }); + } + } + if (afterInitialize) { + Require(Unimplemented([&] { return sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer); }), "distortion command answered handle 0 after initialize"); + Require(Unimplemented([&] { return sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer); }), "aliased distortion command answered handle 0 after initialize"); + Require(Unimplemented([&] { return sceHmdGetWideNearDistortionCorrectionCommand(0, wideNear, commandBuffer); }), "wide-near distortion command answered handle 0 after initialize"); + Require(Unimplemented([&] { return sceHmdDistortionGetWideNearCorrectionCommand(0, wideNear, commandBuffer); }), "aliased wide-near distortion command answered handle 0 after initialize"); + Require(Unimplemented([&] { return sceHmdGet2dVrCommand(0, vr2d, commandBuffer); }), "2D VR command answered handle 0 after initialize"); + Require(Unimplemented([&] { return sceHmdDistortionGet2dVrCommand(0, vr2d, commandBuffer); }), "aliased 2D VR command answered handle 0 after initialize"); + } else { + Require(sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer) == Hmd::NotInitialized, "distortion command before initialize"); + Require(sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer) == Hmd::NotInitialized, "aliased distortion command before initialize"); + Require(sceHmdGetWideNearDistortionCorrectionCommand(0, wideNear, commandBuffer) == Hmd::NotInitialized, "wide-near distortion command before initialize"); + Require(sceHmdDistortionGetWideNearCorrectionCommand(0, wideNear, commandBuffer) == Hmd::NotInitialized, "aliased wide-near distortion command before initialize"); + Require(sceHmdGet2dVrCommand(0, vr2d, commandBuffer) == Hmd::NotInitialized, "2D VR command before initialize"); + Require(sceHmdDistortionGet2dVrCommand(0, vr2d, commandBuffer) == Hmd::NotInitialized, "aliased 2D VR command before initialize"); + } + for (const auto byte : commands) Require(byte == 0x5a, "a refused command query wrote its output"); +} + void CheckAssyError(std::int32_t expected) { unsigned char data[32]; std::memset(data, 0x5a, sizeof(data)); @@ -95,6 +155,7 @@ int main() { Require(sceHmdGetDeviceInformation(&info) == Hmd::NotInitialized, "query before initialize"); Require(std::memcmp(&info, &untouched, sizeof(info)) == 0, "failed query modified output"); CheckHandleQueries(Hmd::NotInitialized); + CheckCommandQueries(false); CheckAssyError(Hmd::NotInitialized); Require(sceHmdGetDeviceInformationByHandle(0, &info) == Hmd::NotInitialized, "handle query before initialize"); @@ -133,6 +194,7 @@ int main() { Require(std::memcmp(&info, &expected, sizeof(info)) == 0, "invalid handle modified output"); } CheckHandleQueries(Hmd::InvalidHandle); + CheckCommandQueries(true); CheckAssyError(Hmd::DeviceDisconnected); Require(sceHmdGetDeviceInformationByHandle(0, nullptr) == Hmd::InvalidHandle, "null query accepted invalid handle"); diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 7cd2c2872d..59c79396ff 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -21,6 +21,6 @@ [Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. -[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Without a headset `sceHmdOpen` never issues a handle, so the queries answer as the other handle queries do: `NOT_INITIALIZED` before initialization and `HANDLE_INVALID` after it, without reading the descriptor or writing the output. Like `sceHmdGet2DEyeOffset` they do not check their pointers; whether the console checks a null descriptor or output before the handle is not measured. +[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Without a headset `sceHmdOpen` never issues a handle, so the stored handle stays 0. As firmware 13.42's libSceHmd does when its exports are run under an emulator ([#9577](https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6336871370)), the distortion queries return `HANDLE_INVALID` for any other handle before checking initialization, and `NOT_INITIALIZED` for handle 0 before it. The 2D VR query first returns `PARAMETER_NULL` for a null descriptor or output or a null pointer at descriptor offset 0, 8 or 0x20, and `PARAMETER_INVALID` for a non-zero field at 0x34 (32-bit) or 0x38 (64-bit). None writes the output. What handle 0 gets after initialization is not measured, so it throws. The ELF export table aliases the [Distortion correction/2D command and work-memory queries](../../../core/libs/prx/libSceHmd/Export.cpp) to their corresponding HMD queries, confirming identical signatures. Distortion initialization consumes only a work-memory pointer. No shader code or calibration constants are copied; each aliased command query answers as its twin, and distortion initialization keeps its protected body. From 2f579e0eb7348bc77efb9d69921b53afe208adcb Mon Sep 17 00:00:00 2001 From: Simone <simone@MacBook-Air-di-Simone.local> Date: Thu, 22 Oct 2026 11:42:06 +0200 Subject: [PATCH 0976/1360] fix(json2): remove the conflict markers left by the main merge Keep both includes: <sstream> for the classic-locale fallback and <memory> from main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceJson2/Export.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 02debb9b94..51c4bb6730 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -10,11 +10,8 @@ #include <list> #include <locale> #include <mutex> -<<<<<<< HEAD -#include <sstream> -======= #include <memory> ->>>>>>> origin/main +#include <sstream> #include <stdexcept> #include <string> #include "SceTypes.hpp" From a07d5a50740efcb878d16bd81f27820bcd205dbc Mon Sep 17 00:00:00 2001 From: Simone <simone@MacBook-Air-di-Simone.local> Date: Thu, 22 Oct 2026 11:44:03 +0200 Subject: [PATCH 0977/1360] test(json2): follow main's parser in the numeric locale test main now keeps object keys sorted and stops at the end of the first value, so the nested document lists its keys in order and "1,5" parses as the integer 1 instead of failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestJson.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 506e7da1a2..7c59af4225 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -1427,12 +1427,13 @@ static int NumericLocale() { } Value root{}; _ZN3sce4Json5ValueC1Ev(&root); - constexpr char document[] = "{\"value\":1.5,\"list\":[2.5,-0.125]}"; + constexpr char document[] = "{\"list\":[2.5,-0.125],\"value\":1.5}"; check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, document, sizeof(document) - 1) == 0, "Nested numeric parsing", document); const auto serialized = Serialize(root); check(serialized == document, "Nested numeric serialization", serialized.c_str()); - check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1,5", 3) != 0, "Reject decimal comma", "1,5"); + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1,5", 3) == 0 && _ZNK3sce4Json5Value7getTypeEv(&root) == TypeUInteger && + *_ZNK3sce4Json5Value11getUIntegerEv(&root) == 1, "Stop at a decimal comma", "1,5"); bool overflow = false; try { _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1e309", 5); From bec128911cfc2b78db66a0f87d51ce1013885eed Mon Sep 17 00:00:00 2001 From: Simone <simone@MacBook-Air-di-Simone.local> Date: Thu, 22 Oct 2026 11:45:43 +0200 Subject: [PATCH 0978/1360] test(libSceVoice): rename the bitrate variable the main tests also declare Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestVoice.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 7e1a70ffcf..d8156f4e19 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -233,9 +233,9 @@ int main() { Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); Require(sceVoiceGetMuteFlag(output, &muted) == 0 && muted); - std::uint32_t bitrate = 0; + std::uint32_t portBitrate = 0; Require(sceVoiceSetBitRate(input, 7300) == 0); - Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 7300); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 7300); VoicePortInfo rateInfo{}; Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 19); Require(sceVoiceSetBitRate(output + 1, 7300) == portNotFound); @@ -243,7 +243,7 @@ int main() { RequireThrows([&] { sceVoiceSetBitRate(output, 7300); }); RequireThrows([&] { sceVoiceSetBitRate(input, 7301); }); RequireThrows([&] { sceVoiceSetBitRate(input, 0x80000000u); }); - Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 7300); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 7300); VoicePortParam update{}; update.port_type = 2; @@ -253,11 +253,11 @@ int main() { Require(sceVoiceUpdatePort(input, &update) == 0); Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); Require(sceVoiceGetVolume(input, &volume) == 0 && volume == 0.75f); - Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 14400); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 14400); Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 27); update.voice.bitrate = 1; RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); - Require(sceVoiceGetBitRate(input, &bitrate) == 0 && bitrate == 14400); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 14400); update.voice.bitrate = 14400; update.port_type = 3; RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); From 6e6c15962a2ee2db799aae74bbe38f6aeed5def4 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:45:48 +0300 Subject: [PATCH 0979/1360] fix(shader): fold constant composite tables localized to Function storage --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 154 +++++++++--------- .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 103 ++++++------ .../SpirvBackend/src/SpirvSpecialization.cpp | 32 ++-- 3 files changed, 151 insertions(+), 138 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 00796cd2b5..06de269deb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -299,82 +299,88 @@ void testIdsBeyondTheBoundAreRejected() { } void testPrivateCompositeTableLoadFolding() { - for (const auto accessOp : {spv::OpAccessChain, spv::OpInBoundsAccessChain}) { - const auto build = [&](bool dynamicIndex, bool storeThroughChain) { - SpirvModule module; - module.EmitCapability(spv::CapabilityShader); - const auto typeVoid = module.Type(spv::OpTypeVoid); - const auto typeBool = module.Type(spv::OpTypeBool); - const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); - const auto falseVal = module.Constant(spv::OpConstantFalse, typeBool); - const auto zero = module.Constant(spv::OpConstant, typeU32, 0u); - const auto one = module.Constant(spv::OpConstant, typeU32, 1u); - const auto three = module.Constant(spv::OpConstant, typeU32, 3u); - const auto sixteen = module.Constant(spv::OpConstant, typeU32, 16u); - const auto twenty = module.Constant(spv::OpConstant, typeU32, 20u); - const auto thirty = module.Constant(spv::OpConstant, typeU32, 30u); - const auto typeArray = module.Type(spv::OpTypeArray, typeU32, three); - const auto initial = module.Constant(spv::OpConstantComposite, typeArray, std::vector<std::uint32_t>{sixteen, twenty, thirty}); - const auto typeInputPtr = module.Type(spv::OpTypePointer, spv::StorageClassInput, typeU32); - const auto typeTablePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeArray); - const auto typePrivatePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeU32); - const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); - const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); - const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); - const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); - const auto mainId = module.AllocateId(); - const auto lane = module.AllocateId(); - const auto rawCond = module.AllocateId(); - const auto cond = module.AllocateId(); - const auto baseIndex = module.AllocateId(); - const auto index = module.AllocateId(); - const auto element = module.AllocateId(); - const auto loaded = module.AllocateId(); - const auto sum = module.AllocateId(); - module.AddFunction(spv::OpFunction, typeVoid, mainId, spv::FunctionControlMaskNone, typeFunction); - module.AddFunction(spv::OpLabel, module.AllocateId()); - module.AddFunction(spv::OpLoad, typeU32, lane, input); - if (storeThroughChain) { - const auto writePtr = module.AllocateId(); - module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); - module.AddFunction(spv::OpStore, writePtr, lane); + for (const auto storageClass : {spv::StorageClassPrivate, spv::StorageClassFunction}) { + for (const auto accessOp : {spv::OpAccessChain, spv::OpInBoundsAccessChain}) { + const auto build = [&](bool dynamicIndex, bool storeThroughChain) { + SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + const auto typeVoid = module.Type(spv::OpTypeVoid); + const auto typeBool = module.Type(spv::OpTypeBool); + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto falseVal = module.Constant(spv::OpConstantFalse, typeBool); + const auto zero = module.Constant(spv::OpConstant, typeU32, 0u); + const auto one = module.Constant(spv::OpConstant, typeU32, 1u); + const auto three = module.Constant(spv::OpConstant, typeU32, 3u); + const auto sixteen = module.Constant(spv::OpConstant, typeU32, 16u); + const auto twenty = module.Constant(spv::OpConstant, typeU32, 20u); + const auto thirty = module.Constant(spv::OpConstant, typeU32, 30u); + const auto typeArray = module.Type(spv::OpTypeArray, typeU32, three); + const auto initial = module.Constant(spv::OpConstantComposite, typeArray, std::vector<std::uint32_t>{sixteen, twenty, thirty}); + const auto typeInputPtr = module.Type(spv::OpTypePointer, spv::StorageClassInput, typeU32); + const auto typeTablePtr = module.Type(spv::OpTypePointer, storageClass, typeArray); + const auto typeElementPtr = module.Type(spv::OpTypePointer, storageClass, typeU32); + const auto typePrivatePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeU32); + const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); + const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); + const auto table = storageClass == spv::StorageClassPrivate + ? module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial) + : module.AllocateId(); + const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); + const auto mainId = module.AllocateId(); + const auto lane = module.AllocateId(); + const auto rawCond = module.AllocateId(); + const auto cond = module.AllocateId(); + const auto baseIndex = module.AllocateId(); + const auto index = module.AllocateId(); + const auto element = module.AllocateId(); + const auto loaded = module.AllocateId(); + const auto sum = module.AllocateId(); + module.AddFunction(spv::OpFunction, typeVoid, mainId, spv::FunctionControlMaskNone, typeFunction); + module.AddFunction(spv::OpLabel, module.AllocateId()); + if (storageClass == spv::StorageClassFunction) module.AddFunction(spv::OpVariable, typeTablePtr, table, spv::StorageClassFunction, initial); + module.AddFunction(spv::OpLoad, typeU32, lane, input); + if (storeThroughChain) { + const auto writePtr = module.AllocateId(); + module.AddFunction(accessOp, typeElementPtr, writePtr, table, zero); + module.AddFunction(spv::OpStore, writePtr, lane); + } + module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); + module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); + module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); + module.AddFunction(spv::OpSelect, typeU32, index, cond, baseIndex, baseIndex); + module.AddFunction(accessOp, typeElementPtr, element, table, dynamicIndex ? lane : index); + module.AddFunction(spv::OpLoad, typeU32, loaded, element); + module.AddFunction(spv::OpIAdd, typeU32, sum, lane, loaded); + module.AddFunction(spv::OpStore, output, sum); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + return std::pair{ShaderRecompiler::SpecializeSpirv(module.Finalize()), thirty}; + }; + const auto [folded, expectedConstant] = build(false, false); + const auto foldedCounts = opcodeCounts(folded); + check(!foldedCounts.contains(accessOp), "a constant-index composite table access chain was not removed"); + check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); + check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); + check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index composite table load was not folded"); + bool addedThirty = false; + for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { + if ((folded[cursor] >> 16u) == 0u) break; + if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; } - module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); - module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); - module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); - module.AddFunction(spv::OpSelect, typeU32, index, cond, baseIndex, baseIndex); - module.AddFunction(accessOp, typePrivatePtr, element, table, dynamicIndex ? lane : index); - module.AddFunction(spv::OpLoad, typeU32, loaded, element); - module.AddFunction(spv::OpIAdd, typeU32, sum, lane, loaded); - module.AddFunction(spv::OpStore, output, sum); - module.AddFunction(spv::OpReturn); - module.AddFunction(spv::OpFunctionEnd); - return std::pair{ShaderRecompiler::SpecializeSpirv(module.Finalize()), thirty}; - }; - const auto [folded, expectedConstant] = build(false, false); - const auto foldedCounts = opcodeCounts(folded); - check(!foldedCounts.contains(accessOp), "a constant-index private composite table access chain was not removed"); - check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); - check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); - check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); - bool addedThirty = false; - for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { - if ((folded[cursor] >> 16u) == 0u) break; - if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; + check(addedThirty, "a constant-index composite table load did not fold to its element constant"); + + const auto [dynamic, unusedDynamic] = build(true, false); + static_cast<void>(unusedDynamic); + const auto dynamicCounts = opcodeCounts(dynamic); + check(dynamicCounts.contains(accessOp) && dynamicCounts.at(accessOp) == 1u, "a dynamic-index composite table access chain was removed"); + check(dynamicCounts.contains(spv::OpLoad) && dynamicCounts.at(spv::OpLoad) == 2u, "a dynamic-index composite table load was folded"); + + const auto [mutated, unusedMutated] = build(false, true); + static_cast<void>(unusedMutated); + const auto mutatedCounts = opcodeCounts(mutated); + check(mutatedCounts.contains(accessOp) && mutatedCounts.at(accessOp) == 2u, "a mutated composite table access chain was removed"); + check(mutatedCounts.contains(spv::OpLoad) && mutatedCounts.at(spv::OpLoad) == 2u, "a mutated composite table load was folded despite a store through an access chain"); } - check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); - - const auto [dynamic, unusedDynamic] = build(true, false); - static_cast<void>(unusedDynamic); - const auto dynamicCounts = opcodeCounts(dynamic); - check(dynamicCounts.contains(accessOp) && dynamicCounts.at(accessOp) == 1u, "a dynamic-index private composite table access chain was removed"); - check(dynamicCounts.contains(spv::OpLoad) && dynamicCounts.at(spv::OpLoad) == 2u, "a dynamic-index private composite table load was folded"); - - const auto [mutated, unusedMutated] = build(false, true); - static_cast<void>(unusedMutated); - const auto mutatedCounts = opcodeCounts(mutated); - check(mutatedCounts.contains(accessOp) && mutatedCounts.at(accessOp) == 2u, "a mutated private composite table access chain was removed"); - check(mutatedCounts.contains(spv::OpLoad) && mutatedCounts.at(spv::OpLoad) == 2u, "a mutated private composite table load was folded despite a store through an access chain"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index 9534ce7716..b50010c578 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -396,6 +396,7 @@ std::optional<std::pair<std::uint32_t, std::uint32_t>> storedLaneFunction(const switch (opcode) { case spv::OpFunction: case spv::OpLabel: + case spv::OpVariable: case spv::OpReturn: case spv::OpFunctionEnd: break; @@ -936,7 +937,10 @@ OpStore %slot2 %second check(opcodeCount(acrossCall, spv::OpGroupNonUniformBallot) == 2, "a ballot was shared across a function call"); } -std::string privateTableModuleSource(const std::string& body) { +std::string privateTableModuleSource(const std::string& body, bool functionTable = false) { + const std::string storage = functionTable ? "Function" : "Private"; + const std::string globalTable = functionTable ? "" : "%table = OpVariable %tableArray Private %initial\n"; + const std::string localTable = functionTable ? "%table = OpVariable %tableArray Function %initial\n" : ""; return R"(OpCapability Shader OpMemoryModel Logical GLSL450 OpEntryPoint GLCompute %main "main" %index @@ -955,66 +959,69 @@ OpDecorate %index BuiltIn LocalInvocationIndex %array = OpTypeArray %uint %three %initial = OpConstantComposite %array %sixteen %twenty %thirty %input = OpTypePointer Input %uint -%privateArray = OpTypePointer Private %array +%tableArray = OpTypePointer )" + storage + R"( %array +%tableElement = OpTypePointer )" + storage + R"( %uint %private = OpTypePointer Private %uint %function = OpTypeFunction %void %index = OpVariable %input Input -%table = OpVariable %privateArray Private %initial -%output = OpVariable %private Private +)" + globalTable + R"(%output = OpVariable %private Private %main = OpFunction %void None %function %entry = OpLabel -%lane = OpLoad %uint %index +)" + localTable + R"(%lane = OpLoad %uint %index )" + body + R"(OpReturn OpFunctionEnd )"; } void testSpecializedPrivateCompositeTableLoad() { - for (const bool inBounds : {false, true}) { - const auto accessName = inBounds ? "OpInBoundsAccessChain" : "OpAccessChain"; - const auto accessOp = inBounds ? spv::OpInBoundsAccessChain : spv::OpAccessChain; - try { - const auto folded = assemble(privateTableModuleSource( - "%rawCond = OpUGreaterThan %bool %lane %zero\n" - "%cond = OpLogicalOr %bool %rawCond %false\n" - "%baseOffset = OpIAdd %uint %one %one\n" - "%offset = OpSelect %uint %cond %baseOffset %baseOffset\n" - "%element = " + std::string(accessName) + " %private %table %offset\n" - "%loaded = OpLoad %uint %element\n" - "%sum = OpIAdd %uint %lane %loaded\n" - "OpStore %output %sum\n")); - static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(folded, Vulkan11, Spirv13, false, false)); - const auto specializedFolded = ShaderRecompiler::SpecializeSpirv(folded); - static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedFolded, Vulkan11, Spirv13, false, false)); - check(opcodeCount(specializedFolded, accessOp) == 0u, std::string(accessName) + ": constant-index private table access chain was not removed"); - check(opcodeCount(specializedFolded, spv::OpLogicalOr) == 0u, std::string(accessName) + ": right-false OpLogicalOr was not simplified"); - check(opcodeCount(specializedFolded, spv::OpSelect) == 0u, std::string(accessName) + ": identical-arm OpSelect was not simplified"); - check(opcodeCount(specializedFolded, spv::OpLoad) == 1u, std::string(accessName) + ": constant-index private table load was not folded"); - check(storedLaneFunction(specializedFolded) == std::optional<std::pair<std::uint32_t, std::uint32_t>>({1u, 30u}), std::string(accessName) + ": constant-index private table load folded to the wrong value"); + for (const bool functionTable : {false, true}) { + for (const bool inBounds : {false, true}) { + const auto accessName = inBounds ? "OpInBoundsAccessChain" : "OpAccessChain"; + const auto accessOp = inBounds ? spv::OpInBoundsAccessChain : spv::OpAccessChain; + const std::string label = std::string(accessName) + (functionTable ? " (Function)" : " (Private)"); + try { + const auto folded = assemble(privateTableModuleSource( + "%rawCond = OpUGreaterThan %bool %lane %zero\n" + "%cond = OpLogicalOr %bool %rawCond %false\n" + "%baseOffset = OpIAdd %uint %one %one\n" + "%offset = OpSelect %uint %cond %baseOffset %baseOffset\n" + "%element = " + std::string(accessName) + " %tableElement %table %offset\n" + "%loaded = OpLoad %uint %element\n" + "%sum = OpIAdd %uint %lane %loaded\n" + "OpStore %output %sum\n", functionTable)); + static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(folded, Vulkan11, Spirv13, false, false)); + const auto specializedFolded = ShaderRecompiler::SpecializeSpirv(folded); + static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedFolded, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedFolded, accessOp) == 0u, label + ": constant-index table access chain was not removed"); + check(opcodeCount(specializedFolded, spv::OpLogicalOr) == 0u, label + ": right-false OpLogicalOr was not simplified"); + check(opcodeCount(specializedFolded, spv::OpSelect) == 0u, label + ": identical-arm OpSelect was not simplified"); + check(opcodeCount(specializedFolded, spv::OpLoad) == 1u, label + ": constant-index table load was not folded"); + check(storedLaneFunction(specializedFolded) == std::optional<std::pair<std::uint32_t, std::uint32_t>>({1u, 30u}), label + ": constant-index table load folded to the wrong value"); - const auto dynamic = assemble(privateTableModuleSource( - "%element = " + std::string(accessName) + " %private %table %lane\n" - "%loaded = OpLoad %uint %element\n" - "OpStore %output %loaded\n")); - static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(dynamic, Vulkan11, Spirv13, false, false)); - const auto specializedDynamic = ShaderRecompiler::SpecializeSpirv(dynamic); - static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedDynamic, Vulkan11, Spirv13, false, false)); - check(opcodeCount(specializedDynamic, accessOp) == 1u, std::string(accessName) + ": dynamic-index private table access chain was removed"); - check(opcodeCount(specializedDynamic, spv::OpLoad) == 2u, std::string(accessName) + ": dynamic-index private table load was folded"); + const auto dynamic = assemble(privateTableModuleSource( + "%element = " + std::string(accessName) + " %tableElement %table %lane\n" + "%loaded = OpLoad %uint %element\n" + "OpStore %output %loaded\n", functionTable)); + static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(dynamic, Vulkan11, Spirv13, false, false)); + const auto specializedDynamic = ShaderRecompiler::SpecializeSpirv(dynamic); + static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedDynamic, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedDynamic, accessOp) == 1u, label + ": dynamic-index table access chain was removed"); + check(opcodeCount(specializedDynamic, spv::OpLoad) == 2u, label + ": dynamic-index table load was folded"); - const auto mutated = assemble(privateTableModuleSource( - "%writeElement = " + std::string(accessName) + " %private %table %zero\n" - "OpStore %writeElement %lane\n" - "%readElement = " + std::string(accessName) + " %private %table %one\n" - "%loaded = OpLoad %uint %readElement\n" - "OpStore %output %loaded\n")); - static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(mutated, Vulkan11, Spirv13, false, false)); - const auto specializedMutated = ShaderRecompiler::SpecializeSpirv(mutated); - static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedMutated, Vulkan11, Spirv13, false, false)); - check(opcodeCount(specializedMutated, accessOp) == 2u, std::string(accessName) + ": mutated private table access chain was removed"); - check(opcodeCount(specializedMutated, spv::OpLoad) == 2u, std::string(accessName) + ": mutated private table load was folded despite a store through an access chain"); - } catch (const std::exception& error) { - check(false, std::string(accessName) + " private table specialization: " + error.what()); + const auto mutated = assemble(privateTableModuleSource( + "%writeElement = " + std::string(accessName) + " %tableElement %table %zero\n" + "OpStore %writeElement %lane\n" + "%readElement = " + std::string(accessName) + " %tableElement %table %one\n" + "%loaded = OpLoad %uint %readElement\n" + "OpStore %output %loaded\n", functionTable)); + static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(mutated, Vulkan11, Spirv13, false, false)); + const auto specializedMutated = ShaderRecompiler::SpecializeSpirv(mutated); + static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedMutated, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedMutated, accessOp) == 2u, label + ": mutated table access chain was removed"); + check(opcodeCount(specializedMutated, spv::OpLoad) == 2u, label + ": mutated table load was folded despite a store through an access chain"); + } catch (const std::exception& error) { + check(false, label + " table specialization: " + error.what()); + } } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index b20fa14351..a47da0f9dd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -85,21 +85,6 @@ Instruction Make(spv::Op op, std::initializer_list<std::uint32_t> operands) { return result; } -class IdSet { -public: - void Reset(std::size_t bound) { present.assign(bound, 0u); } - bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } - bool insert(std::uint32_t id) { - if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); - if (present[id] != 0u) return false; - present[id] = 1u; - return true; - } - -private: - std::vector<std::uint8_t> present; -}; - bool PureInstruction(const Instruction& instruction) { switch (Opcode(instruction)) { case spv::OpVectorExtractDynamic: @@ -568,6 +553,21 @@ private: std::size_t count = 0; }; +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector<std::uint8_t> present; +}; + class Specialization { public: explicit Specialization(std::span<const std::uint32_t> words) : arena(words.size()) { @@ -599,7 +599,7 @@ public: if (op == spv::OpConstantTrue || op == spv::OpConstantFalse) values.emplace(instruction.at(2), op == spv::OpConstantTrue ? 1u : 0u); if ((op == spv::OpConstant || op == spv::OpConstantTrue || op == spv::OpConstantFalse) && values.contains(instruction.at(2))) canonical.emplace(std::pair{instruction.at(1), values.at(instruction[2])}, instruction[2]); if (op == spv::OpConstantComposite && instruction.size() >= 3u) composites.emplace(instruction.at(2), std::vector<std::uint32_t>(instruction.begin() + 3, instruction.end())); - if (op == spv::OpVariable && instruction.size() == 5u && instruction[3] == spv::StorageClassPrivate && composites.contains(instruction[4])) privateTables.emplace(instruction.at(2), instruction[4]); + if (op == spv::OpVariable && instruction.size() == 5u && (instruction[3] == spv::StorageClassPrivate || instruction[3] == spv::StorageClassFunction) && composites.contains(instruction[4])) privateTables.emplace(instruction.at(2), instruction[4]); cursor += count; } IdTable<std::uint32_t> tablePointers; From 65ba3a665fc60c1d932b8938746c917ad57fb036 Mon Sep 17 00:00:00 2001 From: mugurc <mceker@gmail.com> Date: Tue, 20 Oct 2026 11:42:36 +0300 Subject: [PATCH 0980/1360] fix(tests): compare the floating-point exception masks with the ones the host keeps Under Rosetta 2 the six MXCSR exception masks always read back set: a write that unmasks an SSE exception is not kept, while the x87 masks and the other MXCSR bits are. guest_floating_point unmasked the overflow exception on both units and then expected MXCSR to equal the value it wrote, so it failed on macOS although fesetexceptflag and feclearexcept left the masks alone. The test now compares with the environment read back after the write, which on other hosts is the value written. The debt entry for the floating-point functions notes the Rosetta behaviour. --- core/libs/tests/GuestFloatingPoint.cpp | 1 + docs/dev/debt/libc.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 3ea9884bb0..41275eaad7 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -142,6 +142,7 @@ void CheckFlags() { traps.x87[0] &= ~0x08u; traps.sse &= ~(0x08u << 7); WriteEnvironment(traps); + traps = ReadEnvironment(); const std::uint16_t none = 0; Require(fesetexceptflag_nid_postfix(&none, 0x01) == 0, "restore with an unmasked exception"); Require(feclearexcept_nid_postfix(0x02) == 0, "clear with an unmasked exception"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5014c4dd6f..12eb890d98 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -15,7 +15,7 @@ - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. -- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. +- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error From 100148f7624af4dd712837c901fd4813c43e2800 Mon Sep 17 00:00:00 2001 From: mugurc <mceker@gmail.com> Date: Thu, 22 Oct 2026 13:00:10 +0300 Subject: [PATCH 0981/1360] fix(tests): expect the SSE traps the host keeps in the floating-point trap checks The trap checks unmask exceptions with feenableexcept and its relatives and expect MXCSR to hold the new masks, and the enable and disable results to report the traps enabled on both units. Under Rosetta 2 MXCSR keeps its six exception masks set, so no SSE trap is ever enabled and every result reports none. The test now asks the host once whether it keeps an unmasked MXCSR exception and, where it does not, expects all six SSE masks set and no trap on both units; on other hosts the expectations are the same as before. A debt entry records that the trap functions change only the x87 masks under Rosetta. --- core/libs/tests/GuestFloatingPoint.cpp | 29 ++++++++++++++++++-------- docs/dev/debt/libc.md | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 41275eaad7..31e3374567 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -188,25 +188,36 @@ unsigned SseMasks() { return (ReadEnvironment().sse >> 7) & 0x3fu; } +bool HostKeepsSseTraps() { + const unsigned saved = _mm_getcsr(); + _mm_setcsr(saved & ~(0x01u << 7)); + const bool kept = (_mm_getcsr() & (0x01u << 7)) == 0; + _mm_setcsr(saved); + return kept; +} + void CheckTraps() { const auto seeded = SeedFlags(0, 0); + const bool sseKept = HostKeepsSseTraps(); + const auto sse = [&](unsigned masks) { return sseKept ? masks : 0x3fu; }; + const auto both = [&](unsigned traps) { return static_cast<int>(0xffffffc0u | (sseKept ? traps : 0u)); }; Require(feenableexcept_nid_postfix(0x49) == static_cast<int>(0xffffffc0u), "enable reports no previous trap as the console does"); Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); - Require(SseMasks() == 0x36, "enable unmasks the SSE exceptions"); - Require(fedisableexcept_nid_postfix(0x41) == static_cast<int>(0xffffffc9u), "disable returns the traps enabled on both units"); + Require(SseMasks() == sse(0x36), "enable unmasks the SSE exceptions"); + Require(fedisableexcept_nid_postfix(0x41) == both(0x09), "disable returns the traps enabled on both units"); Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); - Require(SseMasks() == 0x37, "disable masks the SSE exception"); - Require(__feenableexcept_nid_postfix(0x02) == static_cast<int>(0xffffffc8u) && X87Masks() == 0x35, "the internal enable differs"); - Require(__fedisableexcept_nid_postfix(0x3f) == static_cast<int>(0xffffffcau) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(SseMasks() == sse(0x37), "disable masks the SSE exception"); + Require(__feenableexcept_nid_postfix(0x02) == both(0x08) && X87Masks() == 0x35, "the internal enable differs"); + Require(__fedisableexcept_nid_postfix(0x3f) == both(0x0a) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); - Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == 0x3b, "a trap set did not use the low 16 bits"); - Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "a trap set masked enabled traps"); - Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "an empty trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == sse(0x3b), "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == sse(0x3a), "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == sse(0x3a), "an empty trap set masked enabled traps"); auto mixed = ReadEnvironment(); mixed.x87[0] = (mixed.x87[0] & ~0x3fu) | 0x36; mixed.sse = (mixed.sse & ~(0x3fu << 7)) | (0x3au << 7); WriteEnvironment(mixed); - Require(feenableexcept_nid_postfix(0) == static_cast<int>(0xffffffc1u), "enable reports only the traps enabled on both units"); + Require(feenableexcept_nid_postfix(0) == both(0x01), "enable reports only the traps enabled on both units"); Require(fegettrapenable_nid_postfix() == 0x09, "the trap query reads only the x87 control word"); WriteEnvironment(seeded); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 12eb890d98..8702e50803 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -16,6 +16,7 @@ - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. +- [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error From 2d7a8dab59037b7e12aace0eebef279ba6fa7284 Mon Sep 17 00:00:00 2001 From: ebosido444-ai <ebosido444-ai@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:03:32 +0300 Subject: [PATCH 0982/1360] test(agc): cover a layered depth view's layer that only overlaps the depth slice The layer-pitch assertion was guarded by layerPitch + sliceBytes > BlockBytes, which holds for this fixture, so it never ran. It now asserts the content of the layer the depth surface does not start at, and a new case fills a 64 KiB swizzled depth plane without storing it back, takes a four-layer view over the same memory, and checks that the pending depth result is in guest memory before the view is uploaded and that the overlapping layer holds the guest bytes that were there. --- .../tests/execution/DepthStorage.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index d5a4fac983..d7edece100 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -257,7 +257,7 @@ void Run(Context context, std::uint8_t* block) { Require(layered != nullptr && layered->ImageLayers() == 2, "a two-layer 2D array view over the depth plane's guest layer pitch was not taken as a storage image"); submit(recorder); readBack(*layered); - Require(layerPitch + sliceBytes > BlockBytes || CountFloats(block + layerPitch, 0.5f) == Side * Side, "the two-layer 2D array view did not place its layers at the guest layer pitch"); + Require(CountFloats(block + layerPitch, 0.f) == Side * Side, "the two-layer 2D array view did not take the layer that only overlaps the depth slice from guest memory"); Require(CountFloats(block, 0.5f) == Side * Side, "the two-layer 2D array view did not receive the layer the depth surface holds"); layeredDepth.baseArray = 1; Require(DepthSurfaceStorage(context, {}, layeredDepth, 0) != nullptr, "a 2D array view from layer 1 of the depth plane was not taken as a storage image"); @@ -271,6 +271,24 @@ void Run(Context context, std::uint8_t* block) { submit(recorder); readBack(*reread); Require(CountFloats(block, 0.125f) == Side * Side, "a storage image written over the depth plane did not reach the depth surface"); + const float overlapping = 0.375f; + for (std::size_t i = 0; i < Side * Side; ++i) std::memcpy(block + layerPitch + i * sizeof(overlapping), &overlapping, sizeof(overlapping)); + auto swizzled = PlaneResource(address, Format32Float); + swizzled.tileMode = TextureTileMode::kStandard64KB; + const auto tiled = DepthSurfaceStorage(context, {}, swizzled, 0); + Require(tiled != nullptr, "a 64 KiB depth plane has no storage image"); + const std::array<std::uint32_t, 4> pendingFill{std::bit_cast<std::uint32_t>(0.75f), std::bit_cast<std::uint32_t>(0.75f), std::bit_cast<std::uint32_t>(0.75f), std::bit_cast<std::uint32_t>(0.75f)}; + Require(tiled->FillClear(pendingFill, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the 64 KiB depth plane's storage image could not be filled"); + submit(recorder); + auto spanning = layeredDepth; + spanning.baseArray = 0; + spanning.depthOrLastArray = 3; + const auto spanningView = DepthSurfaceStorage(context, {}, spanning, 0); + Require(spanningView != nullptr && spanningView->ImageLayers() == 4, "a four-layer 2D array view over a 64 KiB depth plane was not taken as a storage image"); + Require(CountFloats(block, 0.75f) == Side * Side, "the pending depth result was not stored into guest memory before the layered view was created"); + submit(recorder); + readBack(*spanningView); + Require(CountFloats(block + layerPitch, overlapping) == Side * Side, "the layer that only overlaps the depth surface did not hold the guest bytes that were there"); Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); } From ed8a900caf92bbc7f5de356d1cdd727361b184d8 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Thu, 22 Oct 2026 04:23:33 -0500 Subject: [PATCH 0983/1360] fix(voice): accept valid worker priorities and affinities in sceVoiceSetThreadsParams Mode 1 threw for every input since fe4016f61, which stopped Grand Theft Auto V (PPSA04264) right after sceVoiceInit. It now returns 0 for priorities 256-767 with non-zero affinity masks within 0x1fff, the range sceVideoOutOpen accepts for its service thread; no voice worker threads run, so the values reach no host scheduling, and other values still throw. --- core/libs/prx/libSceVoice/Export.cpp | 12 +++++++++++- core/libs/tests/GuestVoice.cpp | 20 ++++++++++++++++++++ docs/dev/debt/libSceVoice.md | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 707d112ea4..db760070b5 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -26,6 +26,9 @@ constexpr auto NotInitialized = static_cast<std::int32_t>(0x804e0801u); constexpr auto InvalidPort = static_cast<std::int32_t>(0x804e0804u); constexpr auto InvalidArgument = static_cast<std::int32_t>(0x804e0805u); constexpr auto PortNotFound = static_cast<std::int32_t>(0x804e0807u); +constexpr std::int32_t ThreadPriorityHighest = 256; +constexpr std::int32_t ThreadPriorityLowest = 767; +constexpr std::uint64_t ThreadAffinityAll = 0x1fff; struct Port { std::int32_t type; @@ -56,6 +59,10 @@ Voice& State() { throw std::logic_error(std::string(function) + ": " + why); } +bool IsThreadSetting(std::int32_t priority, std::uint64_t affinity) { + return priority >= ThreadPriorityHighest && priority <= ThreadPriorityLowest && affinity != 0 && (affinity & ~ThreadAffinityAll) == 0; +} + Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { if (!voice.initialized) Fail(function, "library not initialized"); const auto found = voice.ports.find(id); @@ -90,7 +97,10 @@ int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams* params) { if (voice.started) return static_cast<std::int32_t>(0x804e080bu); if (params->mode == 0) return 0; if (params->mode != 1) return InvalidArgument; - Fail(__func__, "worker priority and affinity are not implemented"); + if (!IsThreadSetting(params->firstPriority, params->firstAffinity) || !IsThreadSetting(params->secondPriority, params->secondAffinity) || !IsThreadSetting(params->thirdPriority, params->thirdAffinity)) { + Fail(__func__, "the result for a priority outside 256-767 or an affinity mask outside 0x1fff is unknown"); + } + return 0; } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index cb12940acb..de0e76296c 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -139,6 +139,24 @@ int main() { Require(sceVoiceSetThreadsParams(&threads) == invalidArgument); threads.mode = 1; RequireThrows([&] { sceVoiceSetThreadsParams(&threads); }); + VoiceThreadsParams scheduled{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + Require(sceVoiceSetThreadsParams(&scheduled) == 0); + scheduled = {1, 256, 0x1fff, 767, 0, 1, 512, 0, 0x1000}; + Require(sceVoiceSetThreadsParams(&scheduled) == 0); + for (const auto priority : {255, 768, 0, -1}) { + for (int slot = 0; slot < 3; ++slot) { + VoiceThreadsParams outside{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + (slot == 0 ? outside.firstPriority : slot == 1 ? outside.secondPriority : outside.thirdPriority) = priority; + RequireThrows([&] { sceVoiceSetThreadsParams(&outside); }); + } + } + for (const std::uint64_t affinity : {std::uint64_t{0}, std::uint64_t{0x2000}, std::uint64_t{0x3fff}, UINT64_MAX}) { + for (int slot = 0; slot < 3; ++slot) { + VoiceThreadsParams outside{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + (slot == 0 ? outside.firstAffinity : slot == 1 ? outside.secondAffinity : outside.thirdAffinity) = affinity; + RequireThrows([&] { sceVoiceSetThreadsParams(&outside); }); + } + } threads.mode = 0; Require(sceVoiceStart(nullptr) == invalidArgument); Require(sceVoiceStart(&start) == invalidArgument); @@ -155,6 +173,8 @@ int main() { start.mem_size = 0x40000; Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); Require(sceVoiceSetThreadsParams(&threads) == static_cast<std::int32_t>(0x804e080bu)); + scheduled = {1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + Require(sceVoiceSetThreadsParams(&scheduled) == static_cast<std::int32_t>(0x804e080bu)); Require(sceVoiceGetResourceInfo(nullptr) == invalidArgument); struct ResourceOutput { VoiceResourceInfo info; diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 9e8d8fc46e..1d4cac3b5c 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -21,6 +21,6 @@ - [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. -- [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the 48-byte mode/priority/affinity layout and initialization, null, started-state and mode validation are recovered. Mode 1 throws because worker scheduling is not implemented; mode 0 is the native no-op. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. +- [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the priorities and affinity masks that mode 1 sets reach no host scheduling, because no voice worker threads run (see sceVoiceStart below). Mode 1 returns 0 for priorities 256-767 with non-zero masks within 0x1fff, the values a PS5 thread takes; the native result for other values is unverified, so they throw. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. - [sceVoiceStart, sceVoiceStop and sceVoiceEnd](../../../core/libs/prx/libSceVoice/Export.cpp) validate initialization, container presence and native mode-dependent memory sizes, but do not partition guest working memory or run the audio workers. Start/stop currently change software state only; backend failures and active-stream shutdown are not reproduced. From fa5fc8d5ac117a30244a98a8020391cb00973f9a Mon Sep 17 00:00:00 2001 From: verusferro <99999109+verusferro@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:22:59 +0300 Subject: [PATCH 0984/1360] test(libkernel_sys): compare fstat with _fstat fstat must fill the same FileStat as _fstat for an open descriptor, return EFAULT for a null buffer and EBADF for a closed or invalid descriptor, as the fstatfs check beside it does for _fstatfs. --- core/libs/tests/GuestKernelSys.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a60abca020..4df6c4c056 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -29,6 +29,8 @@ extern "C" { int APS5_VABI fstatfs_nid_postfix(int, void*); int APS5_VABI _fstatfs_nid_postfix(int, void*); +int APS5_VABI fstat_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); @@ -281,8 +283,15 @@ int main() { Require(std::memcmp(viaPublic, viaSyscall, 4) == 0 && std::memcmp(viaPublic + 16, viaSyscall + 16, 8) == 0); Require(std::memcmp(viaPublic + 280, viaSyscall + 280, 16) == 0); Require(fstatfs_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); + FileStat viaFstat{}; + FileStat viaUnderscore{}; + Require(fstat_nid_postfix(descriptor, &viaFstat) == 0 && _fstat_nid_postfix(descriptor, &viaUnderscore) == 0); + Require(std::memcmp(&viaFstat, &viaUnderscore, sizeof(FileStat)) == 0 && viaFstat.st_size == 1); + Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); + Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); + Require(fstat_nid_postfix(-1, &viaFstat) == -1 && Error() == 9); char buffer[64]; for (const auto& component : {std::string("missing"), std::string("file.txt")}) { const auto prefix = root.generic_string() + "/" + component + "/../"; From 1f37078c044124fe81a6ac13f24c4ef586d3a98b Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:11:11 +0000 Subject: [PATCH 0985/1360] feat(libSceKeyboard): implement connection query --- core/libs/prx/libSceKeyboard/Export.cpp | 9 ++++----- core/libs/prx/libSceKeyboard/include/KeyboardState.hpp | 1 + core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp | 9 +++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index c7be6fe290..4c2505bd13 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -27,6 +27,10 @@ int APS5_VABI sceKeyboardReadState(int32_t handle, KeyboardData* data) { return Keyboard::ReadState(handle, data); } +int APS5_VABI sceKeyboardGetConnection(std::int32_t handle, bool* connected) { + return Keyboard::GetConnection(handle, connected); +} + int APS5_VABI sceKeyboardGetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint8_t led, std::uint8_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { return Keyboard::GetKey2Char(handle, arrange, led, modifierKey, keyCode, charData); } @@ -61,11 +65,6 @@ int APS5_VABI sceKeyboardDisconnectPort(std::int32_t first, std::int32_t second) return 0; } -int APS5_VABI sceKeyboardGetConnection(std::int32_t handle, bool* connected) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKeyboardGetDeviceInfo(std::int32_t deviceId, KeyboardDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp index 11c32a5447..afd6430247 100644 --- a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp +++ b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp @@ -18,6 +18,7 @@ int Open(int userId, std::int32_t type, std::int32_t index); int Close(std::int32_t handle); int Read(std::int32_t handle, KeyboardData* data, std::int32_t num); int ReadState(std::int32_t handle, KeyboardData* data); +int GetConnection(std::int32_t handle, bool* connected); int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData); void Publish(const KeyboardInputEvent& event); } diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index bd9cf7d8f8..6c5d4022fc 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -154,6 +154,15 @@ int ReadState(std::int32_t handle, KeyboardData* data) { return KEYBOARD_OK; } +int GetConnection(std::int32_t handle, bool* isConnected) { + std::lock_guard lock(keyboardMutex); + if (!initialized) return KEYBOARD_ERROR_NOT_INITIALIZED; + if (handle != KEYBOARD_HANDLE || !opened) return KEYBOARD_ERROR_INVALID_HANDLE; + if (isConnected == nullptr) return KEYBOARD_ERROR_INVALID_ARG; + *isConnected = connected; + return KEYBOARD_OK; +} + int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { { std::lock_guard lock(keyboardMutex); From 21688ceb55aff7b1dd226aa98a361a0885959efe Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:26:37 +0000 Subject: [PATCH 0986/1360] feat(libSceKeyboard): disconnect all modeled devices --- core/libs/prx/libSceKeyboard/Export.cpp | 3 +-- core/libs/prx/libSceKeyboard/include/KeyboardState.hpp | 1 + core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index 4c2505bd13..9cbc7cd436 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -51,8 +51,7 @@ int APS5_VABI sceKeyboardDeviceOpen(std::uint64_t deviceId, std::int32_t index, } int APS5_VABI sceKeyboardDisconnectAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Keyboard::DisconnectAll(); } int APS5_VABI sceKeyboardDisconnectDevice(std::int32_t deviceId) { diff --git a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp index afd6430247..3552af3153 100644 --- a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp +++ b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp @@ -19,6 +19,7 @@ int Close(std::int32_t handle); int Read(std::int32_t handle, KeyboardData* data, std::int32_t num); int ReadState(std::int32_t handle, KeyboardData* data); int GetConnection(std::int32_t handle, bool* connected); +int DisconnectAll(); int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData); void Publish(const KeyboardInputEvent& event); } diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 6c5d4022fc..954040b147 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -163,6 +163,14 @@ int GetConnection(std::int32_t handle, bool* isConnected) { return KEYBOARD_OK; } +int DisconnectAll() { + KeyboardInputEvent event{}; + event.connectionChange = true; + event.connected = false; + Publish(event); + return KEYBOARD_OK; +} + int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { { std::lock_guard lock(keyboardMutex); From 6750982565debeae306463d53747e6d71cf73798 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:11:11 +0000 Subject: [PATCH 0987/1360] test(libSceKeyboard): cover connection query state --- core/libs/prx/libSceKeyboard/tests/Keyboard.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 7c97932bff..eda8b178ce 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceKeyboardOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceKeyboardClose(std::int32_t); int APS5_VABI sceKeyboardRead(std::int32_t, KeyboardData*, std::int32_t); int APS5_VABI sceKeyboardReadState(std::int32_t, KeyboardData*); +int APS5_VABI sceKeyboardGetConnection(std::int32_t, bool*); int APS5_VABI sceKeyboardGetKey2Char(std::int32_t, std::int32_t, std::uint8_t, std::uint8_t, std::uint16_t, KeyboardCharData*); int APS5_VABI sceKeyboardConnectPort(void); int APS5_VABI sceKeyboardDisconnectAll(void); @@ -44,14 +45,20 @@ static std::uint16_t Jis(std::uint32_t modifierKey, std::uint16_t keyCode, std:: int main() { KeyboardData data[16]{}; KeyboardCharData charData{}; + bool connected = false; Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_NOT_INITIALIZED); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_NOT_INITIALIZED); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardOpen(1, 0, 1, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardOpen(1, 1, 0, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_HANDLE); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_ALREADY_OPENED); + Require(sceKeyboardGetConnection(42, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && connected); Require(sceKeyboardRead(42, data, 1) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, nullptr, 1) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 17) == KEYBOARD_ERROR_INVALID_ARG); @@ -96,9 +103,11 @@ int main() { window.window.event = SDL_WINDOWEVENT_CLOSE; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && !connected); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && connected); for (std::uint16_t key = 0x04; key < 0x04 + 20; ++key) { KeyboardInputEvent press{}; @@ -143,6 +152,7 @@ int main() { Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, 2, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); From 583533413ca725d5abcec4a230a37eb9d175c561 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:26:37 +0000 Subject: [PATCH 0988/1360] test(libSceKeyboard): cover disconnect all lifecycle --- .../prx/libSceKeyboard/tests/Keyboard.cpp | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index eda8b178ce..0307b994df 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -158,6 +158,25 @@ int main() { Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 1 && data[0].key_code[0] == 0); + Require(data[0].connected); + KeyboardInputEvent pressed{}; + pressed.keyCode = 0x04; + pressed.pressed = true; + KeyboardPublishInput_nid_postfix(pressed); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].key_code[0] == 0x04); + Require(sceKeyboardDisconnectAll() == KEYBOARD_OK); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); + Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_OK && !data[0].connected && data[0].key_code[0] == 0); + Require(sceKeyboardDisconnectAll() == KEYBOARD_OK); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); + pressed.keyCode = 0x05; + KeyboardPublishInput_nid_postfix(pressed); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); + window.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + input.HandleEvent(window, 7); + window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; + input.HandleEvent(window, 7); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); bool threwConnect = false; @@ -167,12 +186,4 @@ int main() { threwConnect = true; } Require(threwConnect); - - bool threwDisconnect = false; - try { - sceKeyboardDisconnectAll(); - } catch (const std::runtime_error&) { - threwDisconnect = true; - } - Require(threwDisconnect); } From 107a17bd7b053f12ea74991992cc5ef91bdc6a34 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:27:55 +0300 Subject: [PATCH 0989/1360] feat(libSceVrTracker): report the tracker memory requirements --- core/libs/prx/libSceVrTracker/Export.cpp | 67 +++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index 14776d3483..fd23d253a0 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -74,6 +74,60 @@ static_assert(offsetof(VrTrackerGetResultParam, statusMode) == 24); static_assert(offsetof(VrTrackerGetResultParam, outputValue) == 36); static_assert(offsetof(VrTrackerGetResultParam, reserved1) == 44); +namespace { + +constexpr int NotInit = static_cast<int>(0x81260801u); +constexpr int AlreadyInitialized = static_cast<int>(0x81260802u); +constexpr int DeviceNotRegistered = static_cast<int>(0x81260803u); +constexpr int DeviceAlreadyRegistered = static_cast<int>(0x81260804u); +constexpr int ArgumentInvalid = static_cast<int>(0x81260806u); +constexpr int NotExecuteGpuSubmit = static_cast<int>(0x81260815u); + +constexpr std::uint32_t MemoryAlignment = 0x10000; +constexpr std::uint32_t BaseOnionSize = 0x400000; +constexpr std::uint32_t GarlicSize = 0x3000000; +constexpr std::uint32_t WorkSize = 0x1000000; +constexpr std::uint32_t CalibrationManual = 0; +constexpr std::uint32_t CalibrationAuto = 1; + +struct CalibrationSettings { + std::uint32_t hmd_position; + std::uint32_t pad_position; + std::uint32_t move_position; + std::uint32_t gun_position; + std::uint32_t reserved[4]; +}; + +struct QueryMemoryParam { + std::uint32_t size; + std::uint32_t profile; + std::uint32_t reserved[6]; + CalibrationSettings calibration_settings; +}; +static_assert(sizeof(QueryMemoryParam) == 0x40); + +struct QueryMemoryResult { + std::uint32_t size; + std::uint32_t direct_memory_onion_size; + std::uint32_t direct_memory_onion_alignment; + std::uint32_t direct_memory_garlic_size; + std::uint32_t direct_memory_garlic_alignment; + std::uint32_t work_memory_size; + std::uint32_t work_memory_alignment; + std::uint32_t reserved[9]; +}; + +bool ValidCalibration(const CalibrationSettings& settings) { + return settings.pad_position <= CalibrationAuto && settings.hmd_position <= CalibrationManual && + settings.move_position <= CalibrationAuto && settings.gun_position <= CalibrationAuto; +} + +std::uint32_t OnionSize(const CalibrationSettings& settings) { + return settings.move_position == CalibrationAuto ? BaseOnionSize * 2 : BaseOnionSize; +} + +} // namespace + extern "C" { int APS5_VABI sceVrTrackerCpuProcess(const VrTrackerCpuProcessParam* param) { @@ -125,7 +179,18 @@ int APS5_VABI sceVrTrackerNotifyEndOfCpuProcess(const void* param) { return 0; } int APS5_VABI sceVrTrackerQueryMemory(const void* param, void* result) { - NotImplemented_nid_no_patch(__func__); + const auto* query = static_cast<const QueryMemoryParam*>(param); + auto* memory = static_cast<QueryMemoryResult*>(result); + if (!query || !memory || query->size != sizeof(QueryMemoryParam) || (query->profile != 0 && query->profile != 100) || + !ValidCalibration(query->calibration_settings)) { + return ArgumentInvalid; + } + memory->direct_memory_onion_size = OnionSize(query->calibration_settings); + memory->direct_memory_onion_alignment = MemoryAlignment; + memory->direct_memory_garlic_size = GarlicSize; + memory->direct_memory_garlic_alignment = MemoryAlignment; + memory->work_memory_size = WorkSize; + memory->work_memory_alignment = MemoryAlignment; return 0; } int APS5_VABI sceVrTrackerRecalibrate(const void* param) { From 733ca2c661547b2afe756f3b55fde55d0dee13d1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:27:55 +0300 Subject: [PATCH 0990/1360] feat(libSceVrTracker): initialize and terminate the tracker --- core/libs/prx/libSceVrTracker/Export.cpp | 51 ++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index fd23d253a0..79928a7e37 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -1,3 +1,4 @@ +#include <atomic> #include <cstdint> #include <cstddef> #include "SceTypes.hpp" @@ -126,6 +127,36 @@ std::uint32_t OnionSize(const CalibrationSettings& settings) { return settings.move_position == CalibrationAuto ? BaseOnionSize * 2 : BaseOnionSize; } +constexpr std::int32_t ComputePipes = 7; +constexpr std::int32_t QueuesPerPipe = 8; + +struct InitParam { + std::uint32_t size; + std::uint32_t profile; + std::uint32_t execution_mode; + std::int32_t hmd_thread_priority; + std::int32_t pad_thread_priority; + std::int32_t move_thread_priority; + std::int32_t gun_thread_priority; + std::int32_t reserved; + std::uint64_t cpu_mask; + CalibrationSettings calibration_settings; + void* direct_memory_onion; + std::uint32_t direct_memory_onion_size; + std::uint32_t direct_memory_onion_alignment; + void* direct_memory_garlic; + std::uint32_t direct_memory_garlic_size; + std::uint32_t direct_memory_garlic_alignment; + void* work_memory; + std::uint32_t work_memory_size; + std::uint32_t work_memory_alignment; + std::int32_t gpu_pipe_id; + std::int32_t gpu_queue_id; +}; +static_assert(sizeof(InitParam) == 0x80); + +std::atomic<bool> g_initialized{false}; + } // namespace extern "C" { @@ -151,8 +182,20 @@ int APS5_VABI sceVrTrackerGpuWait(const VrTrackerGpuWaitParam* param) { return 0; } int APS5_VABI sceVrTrackerInit(const VrTrackerInitParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (g_initialized.load()) return AlreadyInitialized; + if (param && param->size == sizeof(VrTrackerInitParam)) NotImplemented_nid_no_patch(__func__); + const auto* init = reinterpret_cast<const InitParam*>(param); + if (!init || init->size != sizeof(InitParam) || !init->direct_memory_garlic || + init->direct_memory_garlic_alignment != MemoryAlignment || init->direct_memory_garlic_size != GarlicSize || + !init->direct_memory_onion || init->direct_memory_onion_alignment != MemoryAlignment || + init->direct_memory_onion_size != OnionSize(init->calibration_settings) || !init->work_memory || + init->work_memory_alignment != MemoryAlignment || init->work_memory_size != WorkSize || + init->gpu_pipe_id >= ComputePipes || init->gpu_queue_id >= QueuesPerPipe || + !ValidCalibration(init->calibration_settings)) { + return ArgumentInvalid; + } + bool expected = false; + return g_initialized.compare_exchange_strong(expected, true) ? 0 : AlreadyInitialized; } int APS5_VABI sceVrTrackerCpuPopMarker() { NotImplemented_nid_no_patch(__func__); @@ -250,8 +293,8 @@ int APS5_VABI sceVrTrackerStopLiveCapture() { return 0; } int APS5_VABI sceVrTrackerTerm() { - NotImplemented_nid_no_patch(__func__); - return 0; + bool expected = true; + return g_initialized.compare_exchange_strong(expected, false) ? 0 : NotInit; } int APS5_VABI sceVrTrackerUnregisterDevice(int handle) { NotImplemented_nid_no_patch(__func__); From 764c4c8ddf28b327b423479aa7da17045e8a9907 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:27:55 +0300 Subject: [PATCH 0991/1360] feat(libSceVrTracker): register and unregister tracked devices --- core/libs/prx/libSceVrTracker/Export.cpp | 36 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index 79928a7e37..4f51c06099 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -1,4 +1,5 @@ #include <atomic> +#include <mutex> #include <cstdint> #include <cstddef> #include "SceTypes.hpp" @@ -157,10 +158,18 @@ static_assert(sizeof(InitParam) == 0x80); std::atomic<bool> g_initialized{false}; +constexpr std::uint32_t DeviceHmd = 0; +constexpr std::uint32_t DeviceGun = 3; + +std::mutex g_devicesMutex; +std::int32_t g_deviceHandles[DeviceGun + 1] = {-1, -1, -1, -1}; + } // namespace extern "C" { +int APS5_VABI sceVrTrackerRegisterDeviceInternal(int device_type, int handle, int unk0, int); + int APS5_VABI sceVrTrackerCpuProcess(const VrTrackerCpuProcessParam* param) { NotImplemented_nid_no_patch(__func__); return 0; @@ -241,15 +250,18 @@ int APS5_VABI sceVrTrackerRecalibrate(const void* param) { return 0; } int APS5_VABI sceVrTrackerRegisterDevice(int device_type, int handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceVrTrackerRegisterDeviceInternal(device_type, handle, -1, 0); } int APS5_VABI sceVrTrackerRegisterDevice2(int device_type, int handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceVrTrackerRegisterDeviceInternal(device_type, handle, -1, 1); } -int APS5_VABI sceVrTrackerRegisterDeviceInternal(int device_type, int handle, int unk0, int unk1) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVrTrackerRegisterDeviceInternal(int device_type, int handle, int unk0, int) { + if (!g_initialized.load()) return NotInit; + const auto type = static_cast<std::uint32_t>(device_type); + if (type > DeviceGun || unk0 > 4) return ArgumentInvalid; + std::lock_guard lock(g_devicesMutex); + if (g_deviceHandles[type] != -1) return DeviceAlreadyRegistered; + g_deviceHandles[type] = handle; return 0; } int APS5_VABI sceVrTrackerResetAll() { @@ -297,8 +309,16 @@ int APS5_VABI sceVrTrackerTerm() { return g_initialized.compare_exchange_strong(expected, false) ? 0 : NotInit; } int APS5_VABI sceVrTrackerUnregisterDevice(int handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!g_initialized.load()) return NotInit; + if (handle < 0) return ArgumentInvalid; + std::lock_guard lock(g_devicesMutex); + for (auto& registered : g_deviceHandles) { + if (registered == handle) { + registered = -1; + return 0; + } + } + return ArgumentInvalid; } int APS5_VABI sceVrTrackerUpdateMotionSensorData(const void* param) { NotImplemented_nid_no_patch(__func__); From b182301586a3350253bbd11bd638c394351afdd4 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:27:55 +0300 Subject: [PATCH 0992/1360] feat(libSceVrTracker): check recalibration and tracking duration requests --- core/libs/prx/libSceVrTracker/Export.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index 4f51c06099..14dcec744c 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -164,6 +164,13 @@ constexpr std::uint32_t DeviceGun = 3; std::mutex g_devicesMutex; std::int32_t g_deviceHandles[DeviceGun + 1] = {-1, -1, -1, -1}; +struct RecalibrateParam { + std::uint32_t size; + std::uint32_t device_type; + std::uint32_t calibration_type; + std::uint32_t reserved[5]; +}; + } // namespace extern "C" { @@ -246,8 +253,12 @@ int APS5_VABI sceVrTrackerQueryMemory(const void* param, void* result) { return 0; } int APS5_VABI sceVrTrackerRecalibrate(const void* param) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!g_initialized.load()) return NotInit; + const auto* recalibrate = static_cast<const RecalibrateParam*>(param); + if (!recalibrate || recalibrate->size != sizeof(RecalibrateParam) || recalibrate->device_type > DeviceGun) return ArgumentInvalid; + if (recalibrate->device_type == DeviceHmd) return DeviceNotRegistered; + std::lock_guard lock(g_devicesMutex); + return g_deviceHandles[recalibrate->device_type] == -1 ? DeviceNotRegistered : 0; } int APS5_VABI sceVrTrackerRegisterDevice(int device_type, int handle) { return sceVrTrackerRegisterDeviceInternal(device_type, handle, -1, 0); @@ -280,9 +291,9 @@ int APS5_VABI sceVrTrackerSetDeviceRejection() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTrackerSetDurationUntilStatusNotTracking(int device_type, std::uint32_t duration_camera_frames) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVrTrackerSetDurationUntilStatusNotTracking(int device_type, std::uint32_t) { + if (!g_initialized.load()) return NotInit; + return static_cast<std::uint32_t>(device_type) > DeviceGun ? ArgumentInvalid : 0; } int APS5_VABI sceVrTrackerSetExtendedMode() { NotImplemented_nid_no_patch(__func__); From a9e23bcc0b0feff7769f4f7ab8af37c5d4f07bdc Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:27:55 +0300 Subject: [PATCH 0993/1360] feat(libSceVrTracker): refuse a GPU wait with no GPU submission --- core/libs/prx/libSceVrTracker/Export.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp index 14dcec744c..cc4a5d1778 100644 --- a/core/libs/prx/libSceVrTracker/Export.cpp +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -230,8 +230,7 @@ int APS5_VABI sceVrTrackerGetPlayAreaWarningInfo(void* info) { return 0; } int APS5_VABI sceVrTrackerGpuWaitAndCpuProcess() { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_initialized.load() ? NotExecuteGpuSubmit : NotInit; } int APS5_VABI sceVrTrackerNotifyEndOfCpuProcess(const void* param) { NotImplemented_nid_no_patch(__func__); From 9fb84e2b550fba4eaf7bb1da0ac45de59c23e35d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:28:43 +0300 Subject: [PATCH 0994/1360] test(libSceVrTracker): check memory queries, the tracker lifecycle and device registration --- core/libs/CMakeLists.txt | 10 + core/libs/tests/GuestVrTrackerDevices.cpp | 219 ++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 core/libs/tests/GuestVrTrackerDevices.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..2667c3eddd 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -388,6 +388,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>") endif() + add_test_executable(guest_vr_tracker_devices_tests tests/GuestVrTrackerDevices.cpp) + target_include_directories(guest_vr_tracker_devices_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_vr_tracker_devices_tests PRIVATE libSceVrTracker libc) + configure_windows_unwind(guest_vr_tracker_devices_tests) + add_test(NAME guest_vr_tracker_devices COMMAND guest_vr_tracker_devices_tests) + if(WIN32) + set_tests_properties(guest_vr_tracker_devices PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVrTracker>") + endif() + add_test_executable(guest_move_tests tests/GuestMove.cpp) target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_move_tests PRIVATE libSceMove libc) diff --git a/core/libs/tests/GuestVrTrackerDevices.cpp b/core/libs/tests/GuestVrTrackerDevices.cpp new file mode 100644 index 0000000000..7377eaf528 --- /dev/null +++ b/core/libs/tests/GuestVrTrackerDevices.cpp @@ -0,0 +1,219 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdlib> + +extern "C" { +int APS5_VABI sceVrTrackerQueryMemory(const void*, void*); +int APS5_VABI sceVrTrackerInit(const void*); +int APS5_VABI sceVrTrackerTerm(); +int APS5_VABI sceVrTrackerRegisterDevice(int, int); +int APS5_VABI sceVrTrackerRegisterDevice2(int, int); +int APS5_VABI sceVrTrackerRegisterDeviceInternal(int, int, int, int); +int APS5_VABI sceVrTrackerUnregisterDevice(int); +int APS5_VABI sceVrTrackerRecalibrate(const void*); +int APS5_VABI sceVrTrackerSetDurationUntilStatusNotTracking(int, std::uint32_t); +int APS5_VABI sceVrTrackerGpuWaitAndCpuProcess(); +} + +namespace { + +constexpr int NotInit = static_cast<int>(0x81260801u); +constexpr int AlreadyInitialized = static_cast<int>(0x81260802u); +constexpr int DeviceNotRegistered = static_cast<int>(0x81260803u); +constexpr int DeviceAlreadyRegistered = static_cast<int>(0x81260804u); +constexpr int ArgumentInvalid = static_cast<int>(0x81260806u); +constexpr int NotExecuteGpuSubmit = static_cast<int>(0x81260815u); +constexpr std::uint32_t Untouched = 0xa5a5a5a5u; +constexpr int Hmd = 0; +constexpr int Pad = 1; +constexpr int Move = 2; +constexpr int Gun = 3; + +struct Calibration { + std::uint32_t hmd, pad, move, gun; + std::uint32_t reserved[4]; +}; + +struct QueryParam { + std::uint32_t size = sizeof(QueryParam); + std::uint32_t profile = 0; + std::uint32_t reserved[6]{}; + Calibration calibration{}; +}; + +struct QueryResult { + std::uint32_t words[16]; +}; + +struct InitParam { + std::uint32_t size = sizeof(InitParam); + std::uint32_t profile = 0; + std::uint32_t executionMode = 0; + std::int32_t priorities[4]{}; + std::int32_t reserved = 0; + std::uint64_t cpuMask = 0; + Calibration calibration{}; + void* onion = nullptr; + std::uint32_t onionSize = 0x400000; + std::uint32_t onionAlignment = 0x10000; + void* garlic = nullptr; + std::uint32_t garlicSize = 0x3000000; + std::uint32_t garlicAlignment = 0x10000; + void* work = nullptr; + std::uint32_t workSize = 0x1000000; + std::uint32_t workAlignment = 0x10000; + std::int32_t pipe = 0; + std::int32_t queue = 0; +}; + +struct RecalibrateParam { + std::uint32_t size = sizeof(RecalibrateParam); + std::uint32_t deviceType = 0; + std::uint32_t calibrationType = 0; + std::uint32_t reserved[5]{}; +}; + +std::uint8_t g_onion[16]; +std::uint8_t g_garlic[16]; +std::uint8_t g_work[16]; + +void Require(bool value) { if (!value) std::abort(); } + +QueryResult FreshResult() { + QueryResult result; + for (auto& word : result.words) word = Untouched; + return result; +} + +InitParam ValidInit() { + InitParam init; + init.onion = g_onion; + init.garlic = g_garlic; + init.work = g_work; + return init; +} + +int Recalibrate(std::uint32_t deviceType, std::uint32_t size = sizeof(RecalibrateParam)) { + RecalibrateParam param; + param.size = size; + param.deviceType = deviceType; + return sceVrTrackerRecalibrate(¶m); +} + +void CheckQueryMemory() { + QueryParam query; + auto result = FreshResult(); + Require(sceVrTrackerQueryMemory(&query, &result) == 0); + Require(result.words[0] == Untouched); + Require(result.words[1] == 0x400000 && result.words[2] == 0x10000); + Require(result.words[3] == 0x3000000 && result.words[4] == 0x10000); + Require(result.words[5] == 0x1000000 && result.words[6] == 0x10000); + Require(result.words[7] == Untouched); + + query.calibration.move = 1; + query.profile = 100; + Require(sceVrTrackerQueryMemory(&query, &result) == 0 && result.words[1] == 0x800000); + + auto rejected = FreshResult(); + QueryParam bad = query; + bad.profile = 1; + Require(sceVrTrackerQueryMemory(&bad, &rejected) == ArgumentInvalid); + bad = query; + bad.calibration.hmd = 1; + Require(sceVrTrackerQueryMemory(&bad, &rejected) == ArgumentInvalid); + bad = query; + bad.calibration.pad = 2; + Require(sceVrTrackerQueryMemory(&bad, &rejected) == ArgumentInvalid); + bad = query; + bad.size = sizeof(QueryParam) - 1; + Require(sceVrTrackerQueryMemory(&bad, &rejected) == ArgumentInvalid); + Require(sceVrTrackerQueryMemory(nullptr, &rejected) == ArgumentInvalid); + Require(sceVrTrackerQueryMemory(&query, nullptr) == ArgumentInvalid); + Require(rejected.words[1] == Untouched); +} + +void CheckBeforeInit() { + Require(sceVrTrackerTerm() == NotInit); + Require(sceVrTrackerRegisterDevice(Pad, 5) == NotInit); + Require(sceVrTrackerUnregisterDevice(5) == NotInit); + Require(Recalibrate(Pad) == NotInit); + Require(sceVrTrackerSetDurationUntilStatusNotTracking(Pad, 10) == NotInit); + Require(sceVrTrackerGpuWaitAndCpuProcess() == NotInit); +} + +void CheckInit() { + auto bad = ValidInit(); + bad.size = sizeof(InitParam) - 8; + Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + bad = ValidInit(); + bad.calibration.move = 1; + Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + bad.onionSize = 0x800000; + bad.pipe = 7; + Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + bad.pipe = 0; + bad.queue = 8; + Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + bad.queue = 0; + bad.work = nullptr; + Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + bad.work = g_work; + bad.garlicAlignment = 0x1000; + Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + Require(sceVrTrackerInit(nullptr) == ArgumentInvalid); + Require(sceVrTrackerTerm() == NotInit); + + const auto init = ValidInit(); + Require(sceVrTrackerInit(&init) == 0); + Require(sceVrTrackerInit(&init) == AlreadyInitialized); + Require(sceVrTrackerInit(nullptr) == AlreadyInitialized); + Require(sceVrTrackerGpuWaitAndCpuProcess() == NotExecuteGpuSubmit); +} + +void CheckDevices() { + Require(sceVrTrackerRegisterDevice(Pad, 5) == 0); + Require(sceVrTrackerRegisterDevice(Pad, 6) == DeviceAlreadyRegistered); + Require(sceVrTrackerRegisterDevice2(Move, 7) == 0); + Require(sceVrTrackerRegisterDeviceInternal(Gun, 8, 5, 0) == ArgumentInvalid); + Require(sceVrTrackerRegisterDevice(4, 8) == ArgumentInvalid); + Require(sceVrTrackerRegisterDevice(-1, 8) == ArgumentInvalid); + Require(sceVrTrackerRegisterDevice(Hmd, 9) == 0); + + Require(Recalibrate(Pad) == 0); + Require(Recalibrate(Move) == 0); + Require(Recalibrate(Hmd) == DeviceNotRegistered); + Require(Recalibrate(Gun) == DeviceNotRegistered); + Require(Recalibrate(4) == ArgumentInvalid); + Require(Recalibrate(Pad, sizeof(RecalibrateParam) + 4) == ArgumentInvalid); + Require(sceVrTrackerRecalibrate(nullptr) == ArgumentInvalid); + + Require(sceVrTrackerSetDurationUntilStatusNotTracking(Gun, 10) == 0); + Require(sceVrTrackerSetDurationUntilStatusNotTracking(4, 10) == ArgumentInvalid); + + Require(sceVrTrackerUnregisterDevice(5) == 0); + Require(sceVrTrackerUnregisterDevice(5) == ArgumentInvalid); + Require(sceVrTrackerUnregisterDevice(-1) == ArgumentInvalid); + Require(Recalibrate(Pad) == DeviceNotRegistered); + Require(sceVrTrackerRegisterDevice(Pad, 6) == 0); + Require(sceVrTrackerUnregisterDevice(9) == 0); +} + +void CheckTerm() { + Require(sceVrTrackerTerm() == 0); + Require(sceVrTrackerTerm() == NotInit); + Require(sceVrTrackerRegisterDevice(Gun, 8) == NotInit); + const auto init = ValidInit(); + Require(sceVrTrackerInit(&init) == 0); + Require(sceVrTrackerRegisterDevice(Move, 11) == DeviceAlreadyRegistered); + Require(sceVrTrackerTerm() == 0); +} + +} // namespace + +int main() { + CheckQueryMemory(); + CheckBeforeInit(); + CheckInit(); + CheckDevices(); + CheckTerm(); +} From ab22ac3c4e1a4fff09f61b26c321e63a3c7492a7 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:30:36 +0300 Subject: [PATCH 0995/1360] docs(debt): record the libSceVrTracker lifecycle and device calls --- docs/dev/debt/libSceVrTracker.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/libSceVrTracker.md b/docs/dev/debt/libSceVrTracker.md index 138b88a370..a3e50959cb 100644 --- a/docs/dev/debt/libSceVrTracker.md +++ b/docs/dev/debt/libSceVrTracker.md @@ -3,7 +3,11 @@ ### Unknown function info - [VrTracker parameters](../../../core/libs/prx/libSceVrTracker/Export.cpp): argument slots are recovered. CPU processing and GPU waiting consume 32-byte records, result queries consume 56-byte records, and time queries write 64 bits. Initialization accepts 128-byte and 144-byte versions; the 144-byte layout includes three address/size/alignment records, but configuration selector semantics and the older field layout remain unresolved. GPU submission accepts 552-byte and 648-byte versions; submission and result records still require field recovery; result storage has 1520-byte and 3640-byte variants. Result-query records have a signed handle, 64-bit time, 32-bit selectors and an extended-result flag; selector meanings and the four-byte value copied unchanged to result offset 448 remain unresolved. CPU mode/argument semantics are unresolved. Protected bodies remain unchanged. -- [sceVrTrackerCpuPopMarker, sceVrTrackerCpuPushMarker, sceVrTrackerGetLiveCaptureId, sceVrTrackerGpuWaitAndCpuProcess, sceVrTrackerResetAll, sceVrTrackerSaveInternalBuffers and sceVrTrackerSetDeviceRejection](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. -- [sceVrTrackerSetExtendedMode, sceVrTrackerSetLEDBrightness, sceVrTrackerSetRestingMode, sceVrTrackerStartLiveCapture, sceVrTrackerStopLiveCapture and sceVrTrackerTerm](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. -- [sceVrTrackerGetPlayAreaWarningInfo, sceVrTrackerNotifyEndOfCpuProcess, sceVrTrackerQueryMemory, sceVrTrackerRecalibrate, sceVrTrackerRegisterDevice and sceVrTrackerRegisterDevice2](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. -- [sceVrTrackerRegisterDeviceInternal, sceVrTrackerResetOrientationRelative, sceVrTrackerSetDurationUntilStatusNotTracking, sceVrTrackerUnregisterDevice and sceVrTrackerUpdateMotionSensorData](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. +- [sceVrTrackerCpuPopMarker, sceVrTrackerCpuPushMarker, sceVrTrackerGetLiveCaptureId, sceVrTrackerResetAll, sceVrTrackerSaveInternalBuffers and sceVrTrackerSetDeviceRejection](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. +- [sceVrTrackerSetExtendedMode, sceVrTrackerSetLEDBrightness, sceVrTrackerSetRestingMode, sceVrTrackerStartLiveCapture and sceVrTrackerStopLiveCapture](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. +- [sceVrTrackerGetPlayAreaWarningInfo and sceVrTrackerNotifyEndOfCpuProcess](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. +- [sceVrTrackerResetOrientationRelative and sceVrTrackerUpdateMotionSensorData](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. + +### Functional + +- [tracker lifecycle and devices](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - `sceVrTrackerQueryMemory`, `sceVrTrackerInit`, `sceVrTrackerTerm`, the three `sceVrTrackerRegisterDevice*` calls, `sceVrTrackerUnregisterDevice`, `sceVrTrackerRecalibrate`, `sceVrTrackerSetDurationUntilStatusNotTracking` and `sceVrTrackerGpuWaitAndCpuProcess` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/vr_tracker/vr_tracker.cpp), a PS4 implementation: its struct layouts, memory sizes, argument checks, one handle per device type and `DEVICE_NOT_REGISTERED` for an HMD recalibration. Only the 128-byte init parameter is read; the 144-byte one throws. Unlike shadPS4, the init memory ranges are not looked up, a null init parameter and negative device types are refused. Terminating keeps the registered handles, as there. With no camera there is no GPU submission, so the GPU wait returns `NOT_EXECUTE_GPU_SUBMIT`. Not measured on a PS5 From 24447ef99e5e4684a5502f417fb1e8355edbe9d7 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Thu, 22 Oct 2026 08:28:19 +0300 Subject: [PATCH 0996/1360] test(libSceVrTracker): check that a 144-byte init parameter throws --- core/libs/tests/GuestVrTrackerDevices.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestVrTrackerDevices.cpp b/core/libs/tests/GuestVrTrackerDevices.cpp index 7377eaf528..bfffa872c9 100644 --- a/core/libs/tests/GuestVrTrackerDevices.cpp +++ b/core/libs/tests/GuestVrTrackerDevices.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdint> #include <cstdlib> +#include <stdexcept> extern "C" { int APS5_VABI sceVrTrackerQueryMemory(const void*, void*); @@ -79,6 +80,12 @@ std::uint8_t g_work[16]; void Require(bool value) { if (!value) std::abort(); } +template <typename TFunction> +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + QueryResult FreshResult() { QueryResult result; for (auto& word : result.words) word = Untouched; @@ -145,6 +152,8 @@ void CheckInit() { auto bad = ValidInit(); bad.size = sizeof(InitParam) - 8; Require(sceVrTrackerInit(&bad) == ArgumentInvalid); + bad.size = 144; + RequireThrows([&] { sceVrTrackerInit(&bad); }); bad = ValidInit(); bad.calibration.move = 1; Require(sceVrTrackerInit(&bad) == ArgumentInvalid); From 36d4939b4e3c8c20d3776b163391c7285019af9e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:25:04 +0300 Subject: [PATCH 0997/1360] feat(libkernel): fill SceKernelModuleInfoEx by module handle --- .../prx/libkernel/Module/src/ModuleInfo.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 63451dbcad..c240b13f9d 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -356,20 +356,22 @@ bool SystemImage(const GuestImage& image) { } #endif -int FillModuleInfo(KernelModule handle, ModuleInfo* info, bool titleOnly) { +template<typename TInfo> +int FillModuleInfo(KernelModule handle, TInfo* info, bool titleOnly) { if (!info) return SCE_KERNEL_ERROR_EFAULT; - if (info->st_size != sizeof(ModuleInfo)) return SCE_KERNEL_ERROR_EINVAL; + if (info->st_size != sizeof(TInfo)) return SCE_KERNEL_ERROR_EINVAL; for (const auto& image : GuestImages()) { if (ModuleIdForImage_nid_no_patch(image.native) != handle) continue; if (titleOnly && SystemImage(image)) return SCE_KERNEL_ERROR_EPERM; - ModuleInfo result{}; - result.st_size = sizeof(ModuleInfo); + TInfo result{}; + result.st_size = sizeof(TInfo); #ifdef _WIN32 if (!Fill(image.address, result)) return SCE_KERNEL_ERROR_ESRCH; #else - ImageSearch<ModuleInfo> search{image.address, &result, false}; - dl_iterate_phdr(FindImage<ModuleInfo>, &search); + ImageSearch<TInfo> search{image.address, &result, false}; + dl_iterate_phdr(FindImage<TInfo>, &search); if (!search.found) return SCE_KERNEL_ERROR_ESRCH; + if constexpr (std::is_same_v<TInfo, ModuleInfoEx>) result.id = handle; #endif *info = result; return 0; @@ -428,6 +430,10 @@ int APS5_VABI sceKernelGetModuleInfo2(KernelModule handle, ModuleInfo* info) { return FillModuleInfo(handle, info, true); } +int GetModuleInfoExByHandle_nid_no_patch(KernelModule handle, ModuleInfoEx* info) { + return FillModuleInfo(handle, info, false); +} + std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void** address, std::size_t* size) { if (!address || !size) throw std::invalid_argument("sceKernelGetLibkernelTextLocation: null argument"); const auto images = GuestImages(); From 9ca103df57e15d6cfc14f5fca08369c5739e8787 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:25:04 +0300 Subject: [PATCH 0998/1360] feat(libkernel_sys): implement sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 10 ---------- core/libs/prx/libkernel_sys/Export.cpp | 11 +++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 702eff446b..6d82c2dea4 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1103,16 +1103,6 @@ int APS5_VABI sceKernelGetMemoryPstate(void) { return 0; } -int APS5_VABI sceKernelGetModuleInfoInternal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelGetModuleListInternal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetOpenPsIdForSystem(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a3beeb2fe6..7d41506fc4 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -273,6 +273,17 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { return _fstatfs_nid_postfix(d, buf); } +int GetModuleInfoExByHandle_nid_no_patch(KernelModule handle, ModuleInfoEx* info); +int APS5_VABI sceKernelGetModuleList(KernelModule* handles, std::size_t count, std::size_t* actual); + +int APS5_VABI sceKernelGetModuleInfoInternal(KernelModule handle, ModuleInfoEx* info) { + return GetModuleInfoExByHandle_nid_no_patch(handle, info); +} + +int APS5_VABI sceKernelGetModuleListInternal(KernelModule* handles, std::size_t count, std::size_t* actual) { + return sceKernelGetModuleList(handles, count, actual); +} + std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { if (size > static_cast<std::size_t>(std::numeric_limits<std::int64_t>::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); From c63ef260668f12a6b158753df276a5aedf03bbe2 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:25:04 +0300 Subject: [PATCH 0999/1360] test(libkernel_sys): cover the internal module info and list calls --- core/libs/CMakeLists.txt | 10 +++ .../tests/GuestKernelSysModuleInternal.cpp | 88 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 core/libs/tests/GuestKernelSysModuleInternal.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..35d2ccdaa3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -327,6 +327,16 @@ if(BUILD_TESTING) add_subdirectory(tests/write_watch) add_subdirectory(tests/guest_arena) endif() + add_test_executable(guest_kernel_sys_module_internal_tests tests/GuestKernelSysModuleInternal.cpp) + target_include_directories(guest_kernel_sys_module_internal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_module_internal_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_module_internal_tests) + add_test(NAME guest_kernel_sys_module_internal COMMAND guest_kernel_sys_module_internal_tests) + set_tests_properties(guest_kernel_sys_module_internal PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_module_internal PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") + endif() if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/tests/GuestKernelSysModuleInternal.cpp b/core/libs/tests/GuestKernelSysModuleInternal.cpp new file mode 100644 index 0000000000..0d477ebd9f --- /dev/null +++ b/core/libs/tests/GuestKernelSysModuleInternal.cpp @@ -0,0 +1,88 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <string> +#include <vector> + +extern "C" { +int APS5_VABI sceKernelGetModuleInfoInternal(KernelModule, ModuleInfoEx*); +int APS5_VABI sceKernelGetModuleListInternal(KernelModule*, std::size_t, std::size_t*); +int APS5_VABI sceKernelGetModuleList(KernelModule*, std::size_t, std::size_t*); +int APS5_VABI sceKernelGetModuleInfo(KernelModule, ModuleInfo*); +int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t, int, ModuleInfoEx*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static std::vector<KernelModule> List(int (APS5_VABI* list)(KernelModule*, std::size_t, std::size_t*)) { + std::vector<KernelModule> handles(256, -1); + std::size_t count = 0; + Require(list(handles.data(), handles.size(), &count) == 0); + Require(count > 0 && count < handles.size()); + handles.resize(count); + return handles; +} + +static void CheckList() { + const auto internal = List(sceKernelGetModuleListInternal); + Require(internal == List(sceKernelGetModuleList)); + std::vector<KernelModule> shorter(internal.size(), -1); + std::size_t untouched = 99; + Require(sceKernelGetModuleListInternal(shorter.data(), internal.size() - 1, &untouched) == SCE_KERNEL_ERROR_ENOMEM); + Require(untouched == 99 && shorter.back() == -1); + Require(sceKernelGetModuleListInternal(nullptr, 0, &untouched) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetModuleListInternal(shorter.data(), shorter.size(), nullptr) == SCE_KERNEL_ERROR_EFAULT); +} + +static void CheckEachModule() { + for (const auto handle : List(sceKernelGetModuleListInternal)) { + ModuleInfoEx extended{}; + extended.st_size = sizeof(ModuleInfoEx); + Require(sceKernelGetModuleInfoInternal(handle, &extended) == 0); + ModuleInfo basic{}; + basic.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo(handle, &basic) == 0); + Require(extended.st_size == sizeof(ModuleInfoEx) && extended.id == handle); + Require(std::string(extended.name) == basic.name && extended.segment_count == basic.segment_count); + for (std::uint32_t i = 0; i < basic.segment_count; ++i) + Require(extended.segments[i].address == basic.segments[i].address && extended.segments[i].size == basic.segments[i].size && + extended.segments[i].prot == basic.segments[i].prot); + } +} + +static void CheckMatchesAddressLookup() { + ModuleInfoEx byAddress{}; + byAddress.st_size = sizeof(ModuleInfoEx); + Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast<std::uintptr_t>(&sceKernelGetModuleInfo), 2, &byAddress) == 0); + ModuleInfoEx byHandle{}; + byHandle.st_size = sizeof(ModuleInfoEx); + Require(sceKernelGetModuleInfoInternal(byAddress.id, &byHandle) == 0); + Require(std::memcmp(&byHandle, &byAddress, sizeof(ModuleInfoEx)) == 0); +} + +static void CheckRefusals() { + const auto handle = List(sceKernelGetModuleListInternal).front(); + for (const std::uint64_t size : {0x160, 0x1a4, 0x1b0, 0}) { + ModuleInfoEx sized{}; + sized.st_size = size; + sized.name[0] = 'x'; + Require(sceKernelGetModuleInfoInternal(handle, &sized) == SCE_KERNEL_ERROR_EINVAL); + Require(sized.st_size == size && sized.name[0] == 'x'); + } + Require(sceKernelGetModuleInfoInternal(handle, nullptr) == SCE_KERNEL_ERROR_EFAULT); + ModuleInfoEx unknown{}; + unknown.st_size = sizeof(ModuleInfoEx); + Require(sceKernelGetModuleInfoInternal(-1, &unknown) == SCE_KERNEL_ERROR_ESRCH && unknown.name[0] == '\0'); +} + +int main() { + CheckList(); + CheckEachModule(); + CheckMatchesAddressLookup(); + CheckRefusals(); + return 0; +} From 26893b36b92a742dc9bfa7522b5f857f44c777d1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 20:25:04 +0300 Subject: [PATCH 1000/1360] docs(debt): record the sources of the internal module calls --- docs/dev/debt/libkernel_sys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 0d634c1296..4da397f5de 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -16,6 +16,7 @@ - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask +- [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 From f53d2d74c2c67637b12afe9e157055fa010ac63b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:20:29 +0300 Subject: [PATCH 1001/1360] feat(libSceGnmDriverForNeoMode): write the Neo default hardware state --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 34 +++++--- .../include/InitSequences.hpp | 77 +++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 9b7f2684c4..37159bb113 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -1,7 +1,26 @@ #include <cstdint> +#include <algorithm> #include <cstddef> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp" + +namespace { + +constexpr std::uint32_t InitPacketWords = 0x100; + +template <std::size_t Words> +int WriteHardwareState(std::uint32_t* cmdbuf, std::uint32_t size, const std::array<std::uint32_t, Words>& state) { + if (size < InitPacketWords) return 0; + auto* at = std::copy(GnmNeo::ClearState.begin(), GnmNeo::ClearState.end(), cmdbuf); + at = std::copy(state.begin(), state.end(), at); + const auto padding = static_cast<std::uint32_t>(cmdbuf + InitPacketWords - at - 1); + at[0] = 0xc0001000u | ((padding - 1) << 16); + at[1] = 0; + return InitPacketWords; +} + +} // namespace namespace { @@ -116,19 +135,16 @@ int APS5_VABI sceGnmDrawIndirectMulti(void) { return 0; } -int APS5_VABI sceGnmDrawInitDefaultHardwareState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitDefaultHardwareState(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState); } -int APS5_VABI sceGnmDrawInitDefaultHardwareState200(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState200); } -int APS5_VABI sceGnmDrawInitDefaultHardwareState350(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState350); } int APS5_VABI sceGnmDrawInitToDefaultContextState(void) { diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp new file mode 100644 index 0000000000..4ba1654296 --- /dev/null +++ b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEGNMDRIVERFORNEOMODE_INCLUDE_INITSEQUENCES_HPP +#define CORE_LIBS_PRX_LIBSCEGNMDRIVERFORNEOMODE_INCLUDE_INITSEQUENCES_HPP + +#include <array> +#include <cstdint> + +namespace GnmNeo { + +constexpr std::array<std::uint32_t, 12> ClearState{ + 0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0055800u, 0x2ec47fc0u, 0xffffffffu, + 0x0u, 0x0u, 0x0u, 0xau, +}; + +constexpr std::array<std::uint32_t, 115> HardwareState{ + 0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x215u, + 0x0u, 0xc0016900u, 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, + 0x280u, 0x80008u, 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, + 0xc0016900u, 0x206u, 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, + 0x10u, 0xc0016900u, 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, + 0x2fbu, 0x3f800000u, 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, + 0xc0016900u, 0x30eu, 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, + 0xc0017600u, 0x7u, 0x1ffu, 0xc0017600u, 0x46u, 0x1ffu, 0xc0017600u, 0x87u, + 0x1ffu, 0xc0017600u, 0xc7u, 0x1ffu, 0xc0017600u, 0x107u, 0x0u, 0xc0017600u, + 0x147u, 0x1ffu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, 0x0u, + 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, 0x284u, + 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, 0xc0016900u, + 0x292u, 0x0u, 0xc0016900u, 0x293u, 0x6000000u, 0xc0016900u, 0x2f8u, 0x0u, + 0xc0016900u, 0x2deu, 0x1e9u, 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, + 0xc0017900u, 0x200u, 0xe0000000u, +}; + +constexpr std::array<std::uint32_t, 131> HardwareState200{ + 0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x219u, + 0xffffffffu, 0xc0017600u, 0x21au, 0xffffffffu, 0xc0017600u, 0x215u, 0x0u, 0xc0016900u, + 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, + 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, + 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, + 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, + 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, + 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0017600u, 0x7u, + 0x1701ffu, 0xc0017600u, 0x46u, 0x1701fdu, 0xc0017600u, 0x87u, 0x1701ffu, 0xc0017600u, + 0xc7u, 0x1701fdu, 0xc0017600u, 0x107u, 0x17u, 0xc0017600u, 0x147u, 0x1701fdu, + 0xc0017600u, 0x47u, 0x1cu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, + 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, + 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, + 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, 0x293u, 0x6020000u, 0xc0016900u, 0x2f8u, + 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, 0xc0026900u, 0xebu, 0xff00ff00u, 0xff00u, + 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, 0xc0017900u, 0x200u, 0xe0000000u, + 0xc0017900u, 0x40000258u, 0x6d007fu, +}; + +constexpr std::array<std::uint32_t, 134> HardwareState350{ + 0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x219u, + 0xffffffffu, 0xc0017600u, 0x21au, 0xffffffffu, 0xc0017600u, 0x215u, 0x0u, 0xc0016900u, + 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, + 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, + 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, + 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, + 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, + 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0017600u, 0x7u, + 0x1701ffu, 0xc0017600u, 0x46u, 0x1701fdu, 0xc0017600u, 0x87u, 0x1701ffu, 0xc0017600u, + 0xc7u, 0x1701fdu, 0xc0017600u, 0x107u, 0x17u, 0xc0017600u, 0x147u, 0x1701fdu, + 0xc0017600u, 0x47u, 0x1cu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, + 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, + 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, + 0xc0016900u, 0x102u, 0x0u, 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, 0x293u, + 0x6020000u, 0xc0016900u, 0x2f8u, 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, 0xc0026900u, + 0xebu, 0xff00ff00u, 0xff00u, 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, + 0xc0017900u, 0x200u, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu, +}; + +} // namespace GnmNeo + +#endif From aa61cdc839218fb0590c233f9ad6fdcecff42729 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:20:29 +0300 Subject: [PATCH 1002/1360] feat(libSceGnmDriverForNeoMode): write the Neo default context state --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 19 ++++++++-------- .../include/InitSequences.hpp | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 37159bb113..7b715efbb1 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -20,9 +20,12 @@ int WriteHardwareState(std::uint32_t* cmdbuf, std::uint32_t size, const std::arr return InitPacketWords; } -} // namespace - -namespace { +template <std::size_t Words> +int WriteContextState(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t packetWords, const std::array<std::uint32_t, Words>& state) { + if (size != packetWords) return 0; + std::copy(state.begin(), state.end(), cmdbuf); + return static_cast<int>(packetWords); +} constexpr int Failure = static_cast<int>(0x8EEE00FFu); constexpr int NoInterface = static_cast<int>(0x80000000u); @@ -147,14 +150,12 @@ int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t* cmdbuf, std:: return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState350); } -int APS5_VABI sceGnmDrawInitToDefaultContextState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteContextState(cmdbuf, size, 0x20, GnmNeo::ContextState); } -int APS5_VABI sceGnmDrawInitToDefaultContextState400(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteContextState(cmdbuf, size, 0x100, GnmNeo::ContextState400); } int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void) { diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp index 4ba1654296..5768dd1c92 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp @@ -72,6 +72,28 @@ constexpr std::array<std::uint32_t, 134> HardwareState350{ 0xc0017900u, 0x200u, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu, }; +constexpr std::array<std::uint32_t, 19> ContextState{ + 0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0002f00u, 0x1u, 0xc0016900u, + 0x102u, 0x0u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0026900u, 0xebu, 0xff00ff00u, + 0xff00u, 0xc00d1000u, 0x0u, +}; + +constexpr std::array<std::uint32_t, 102> ContextState400{ + 0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0016900u, 0x2f9u, 0x2du, + 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, 0xc0016900u, 0x281u, + 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, 0x43fu, 0xc0016900u, + 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, 0x2fau, 0x3f800000u, + 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, 0xc0016900u, 0x2fdu, + 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, 0xffffffffu, 0xc0016900u, + 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, + 0x101u, 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, + 0xc0016900u, 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, + 0x0u, 0xc0016900u, 0x102u, 0x0u, 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, + 0x293u, 0x6020000u, 0xc0016900u, 0x2f8u, 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, + 0xc0026900u, 0xebu, 0xff00ff00u, 0xff00u, 0xc0036900u, 0x295u, 0x100u, 0x100u, + 0x4u, 0xc0017900u, 0x40000258u, 0x6d007fu, 0xc09a1000u, 0x0u, +}; + } // namespace GnmNeo #endif From 1caaded886dd2602d6122bd3cb8a987479ea8b8b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:21:05 +0300 Subject: [PATCH 1003/1360] test(libSceGnmDriverForNeoMode): check the default hardware and context state packets --- core/libs/CMakeLists.txt | 10 +++ core/libs/tests/GuestGnmNeoDefaultState.cpp | 76 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/libs/tests/GuestGnmNeoDefaultState.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..cf86fab599 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -720,4 +720,14 @@ if(BUILD_TESTING) set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVorbisDec>") endif() + + add_test_executable(guest_gnm_neo_default_state_tests tests/GuestGnmNeoDefaultState.cpp) + target_include_directories(guest_gnm_neo_default_state_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_gnm_neo_default_state_tests PRIVATE libSceGnmDriverForNeoMode libc) + configure_windows_unwind(guest_gnm_neo_default_state_tests) + add_test(NAME guest_gnm_neo_default_state COMMAND guest_gnm_neo_default_state_tests) + if(WIN32) + set_tests_properties(guest_gnm_neo_default_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>") + endif() endif() diff --git a/core/libs/tests/GuestGnmNeoDefaultState.cpp b/core/libs/tests/GuestGnmNeoDefaultState.cpp new file mode 100644 index 0000000000..7288e795c4 --- /dev/null +++ b/core/libs/tests/GuestGnmNeoDefaultState.cpp @@ -0,0 +1,76 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdlib> +#include <initializer_list> + +extern "C" { +int APS5_VABI sceGnmDrawInitDefaultHardwareState(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t*, std::uint32_t); +} + +namespace { + +using StateWriter = int (APS5_VABI *)(std::uint32_t*, std::uint32_t); + +constexpr std::uint32_t Untouched = 0xa5a5a5a5u; +constexpr std::uint32_t BufferWords = 0x110; + +std::uint32_t g_words[BufferWords]; + +void Require(bool value) { if (!value) std::abort(); } + +std::uint32_t* Fresh() { + for (auto& word : g_words) word = Untouched; + return g_words; +} + +void RequireAt(std::uint32_t at, std::initializer_list<std::uint32_t> expected) { + for (const auto word : expected) Require(g_words[at++] == word); +} + +void RequireUntouchedFrom(std::uint32_t at) { + for (; at < BufferWords; ++at) Require(g_words[at] == Untouched); +} + +void CheckHardwareState(StateWriter writer, std::uint32_t stateWords, std::initializer_list<std::uint32_t> tail) { + Require(writer(Fresh(), 0xff) == 0); + RequireUntouchedFrom(0); + + Require(writer(Fresh(), 0x100) == 0x100); + RequireAt(0, {0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0, 0xc0055800u, 0x2ec47fc0u, 0xffffffffu, 0, 0, 0, 10}); + RequireAt(12, {0xc0017600u, 0x216, 0xffffffffu, 0xc0017600u, 0x217, 0xffffffffu}); + const std::uint32_t end = 12 + stateWords; + RequireAt(end - static_cast<std::uint32_t>(tail.size()), tail); + const std::uint32_t padding = 0x100 - end - 1; + RequireAt(end, {0xc0001000u | ((padding - 1) << 16), 0}); + for (std::uint32_t at = end + 2; at < 0x100; ++at) Require(g_words[at] == Untouched); + RequireUntouchedFrom(0x100); + + Require(writer(Fresh(), 0x10f) == 0x100); + RequireUntouchedFrom(0x100); +} + +void CheckContextState(StateWriter writer, std::uint32_t packetWords, std::uint32_t stateWords, std::initializer_list<std::uint32_t> tail) { + Require(writer(Fresh(), packetWords - 1) == 0); + RequireUntouchedFrom(0); + Require(writer(Fresh(), packetWords + 1) == 0); + RequireUntouchedFrom(0); + + Require(writer(Fresh(), packetWords) == static_cast<int>(packetWords)); + RequireAt(0, {0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0}); + RequireAt(stateWords - static_cast<std::uint32_t>(tail.size()), tail); + RequireUntouchedFrom(stateWords); +} + +} // namespace + +int main() { + CheckHardwareState(sceGnmDrawInitDefaultHardwareState, 115, {0x100, 4, 0xc0017900u, 0x200, 0xe0000000u}); + CheckHardwareState(sceGnmDrawInitDefaultHardwareState200, 131, {0x200, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu}); + CheckHardwareState(sceGnmDrawInitDefaultHardwareState350, 134, {0x200, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu}); + CheckContextState(sceGnmDrawInitToDefaultContextState, 0x20, 19, {0xff00ff00u, 0xff00, 0xc00d1000u, 0}); + CheckContextState(sceGnmDrawInitToDefaultContextState400, 0x100, 102, {0x40000258u, 0x6d007fu, 0xc09a1000u, 0}); +} From f6d5447d54828e0ed1af667c787c4ab3de0ea00d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:21:05 +0300 Subject: [PATCH 1004/1360] docs(debt): record the libSceGnmDriverForNeoMode default state packets --- docs/dev/debt/libSceGnmDriverForNeoMode.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 46f92c14e9..44ebce9512 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,8 +2,9 @@ ### Unknown function info -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 70 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 65 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read +- [default state packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmDrawInitDefaultHardwareState`, `sceGnmDrawInitDefaultHardwareState200`, `sceGnmDrawInitDefaultHardwareState350`, `sceGnmDrawInitToDefaultContextState` and `sceGnmDrawInitToDefaultContextState400` write the sequences in [InitSequences.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp), taken from [shadPS4's gnmdriver_init.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver_init.h): its Neo sequences, except the plain hardware state, which shadPS4 also writes unchanged in Neo mode. The size checks and returned sizes are shadPS4's. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet From 6e1df42286c306dbdd444960695d37e828b7c698 Mon Sep 17 00:00:00 2001 From: LeandroLP23 <leandroplacidi@gmail.com> Date: Thu, 22 Oct 2026 12:55:31 +0200 Subject: [PATCH 1005/1360] feat(libkernel): answer kern.proc.env for the calling process PS5VR (PPSA99177) reads sysctl {CTL_KERN, KERN_PROC, KERN_PROC_ENV, pid} for its own pid at start-up and stopped on "unsupported MIB 1.14.35.<pid>". The call now returns the empty start-up environment that environ and libc's getenv also begin with, and follows FreeBSD 11's sysctl_kern_proc_env for the errors: EINVAL for a MIB without exactly one pid, ESRCH for another pid or -1, EPERM for a write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../System/src/SystemConfiguration.cpp | 15 +++++++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 22 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 38 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index da87fe7bb2..c0f33a4450 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -323,6 +323,21 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void static constexpr char path[] = "/app0/eboot.bin"; return CopyOut(path, sizeof(path), oldValue, oldLength); } + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 35) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (name[3] != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + return CopyOut(nullptr, 0, oldValue, oldLength); + } if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 32) { if (newValue != nullptr) { *__error_nid_postfix() = 1; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index c6726534a9..707fcd5ea7 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -636,6 +636,27 @@ static void CheckVmmapSysctl() { const int tooLong[] = {1, 14, 32, self, 0}; Require(sysctl_nid_postfix(tooLong, 5, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); } +static void CheckEnvSysctl() { + const int self = getpid_nid_postfix(); + const int mib[] = {1, 14, 35, self}; + *__error_nid_postfix() = 13; + std::size_t length = 7; + Require(sysctl_nid_postfix(mib, 4, nullptr, &length, nullptr, 0) == 0 && length == 0); + unsigned char buffer[16]; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mib, 4, buffer, &length, nullptr, 0) == 0 && length == 0 && buffer[0] == 0xaa); + Require(*__error_nid_postfix() == 13); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mib, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + const int foreign[] = {1, 14, 35, self + 1000}; + Require(sysctl_nid_postfix(foreign, 4, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + const int current[] = {1, 14, 35, -1}; + Require(sysctl_nid_postfix(current, 4, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + Require(sysctl_nid_postfix(mib, 3, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + const int tooLong[] = {1, 14, 35, self, 0}; + Require(sysctl_nid_postfix(tooLong, 5, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); +} static void CheckPseudoRandomMibSysctl() { const int mib[] = {1, 50}; *__error_nid_postfix() = 13; @@ -802,6 +823,7 @@ int main() { CheckOvercommitSysctl(); CheckArandomSysctl(); CheckVmmapSysctl(); + CheckEnvSysctl(); CheckPseudoRandomMibSysctl(); CheckNameToMib(); *__error_nid_postfix() = 13; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 1f218e02a6..566f867126 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -175,6 +175,7 @@ - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. +- [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified - [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. From d62377dd75ebb86eeac6bd3f4d6be986ba84c8cb Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Thu, 22 Oct 2026 16:03:20 +0300 Subject: [PATCH 1006/1360] fix(agc): settle queued HTILE clears before native depth use --- .../Execution/src/VulkanDevice.cpp | 4 +++ .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 29 +++++++++++++++++++ .../tests/execution/NativeDepthBdaClear.cpp | 27 +++++++++++++++-- .../execution/NativeMultisampleDepth.cpp | 10 ++++++- 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d52ba209aa..483f535a97 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4931,6 +4931,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordBind); if (record.htileClear.has_value()) Graphics::ArmHtileDepthClear(record.htileClear->first, record.htileClear->second); resources.MarkGpuWrites(recorder); + if (record.htileClear.has_value()) { + Graphics::QueueHtileDepthClear(record.htileClear->first, record.htileClear->second); + recorder.NotePendingWrite(record.htileClear->first, record.htileClear->second); + } recordStep(PhaseRecordMarks); // Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them // the batch can signal its labels from the GPU. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 81fee305e1..66855322a7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -56,6 +56,7 @@ std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::s std::size_t HtileDepthClearBytes_nid_no_patch(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span<const std::uint32_t> packet, std::uint64_t indirectArguments); bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); +void QueueHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index dd83557641..da334bfdb3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -913,6 +913,7 @@ public: DepthSamplingState sampling; bool clearPending = false; std::uint64_t clearGeneration = 0u; + bool clearCompletionQueued = false; std::shared_ptr<OwnedImage> owned; std::shared_ptr<OwnedImage> sampleShadow; bool useSampleShadow = false; @@ -1529,6 +1530,21 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } +void settlePendingHtileClear(const Context& context, std::uint64_t address) { + if (Recorder::InCompletion()) return; + auto* recorder = RecorderFor(context); + if (recorder == nullptr) return; + std::pair<std::uint64_t, std::size_t> metadata; + { + std::lock_guard lock(surfacesMutex()); + const auto* surface = planeSurface(context.device, address); + if (surface == nullptr || !surface->samplingActive || surface->clearGeneration == 0u || !surface->clearCompletionQueued || !surface->sampling.refusal.empty()) return; + const auto& range = surface->sampling.initial[1]; + metadata = {range.address, range.bytes}; + } + if (recorder->PendingWriteOverlaps(metadata.first, metadata.second)) recorder->SyncThrough(metadata.first, metadata.second); +} + std::map<std::uint64_t, std::size_t>& clearedHtiles() { static auto* cleared = new std::map<std::uint64_t, std::size_t>(); return *cleared; @@ -1902,6 +1918,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); + settlePendingHtileClear(context, target.address); std::unique_lock lock(surfacesMutex()); const bool cleared = takeClearedHtile(target); DepthSurface* surface = nullptr; @@ -2199,6 +2216,7 @@ bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz else if (surface->clearPending || surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); else { surface->clearPending = true; + surface->clearCompletionQueued = false; completionCandidate = true; } existing = true; @@ -2225,6 +2243,14 @@ void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { } } +void QueueHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u || !surface->sampling.refusal.empty()) continue; + if (bytes >= surface->sampling.initial[1].bytes) surface->clearCompletionQueued = true; + } +} + void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { std::lock_guard lock(surfacesMutex()); for (const auto& surface : surfaces()) { @@ -2233,6 +2259,7 @@ void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { if (bytes < metadata.bytes) continue; Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, surface->clearGeneration), "CPU writes raced native multisample depth clear"); GuestMemory::MarkWritten(metadata.address, metadata.bytes); + surface->clearCompletionQueued = false; } } @@ -2261,6 +2288,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c if (std::none_of(surfaces().begin(), surfaces().end(), held)) return nullptr; } std::lock_guard gpu(GuestMemory::GpuMutex()); + settlePendingHtileClear(context, resource.baseAddress); std::lock_guard lock(surfacesMutex()); auto* found = planeSurface(context.device, resource.baseAddress); if (found == nullptr) return nullptr; @@ -2323,6 +2351,7 @@ bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resou if ((resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DMsaa) || resource.baseArray != 0) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); const auto address = resource.baseAddress; + settlePendingHtileClear(context, address); std::lock_guard lock(surfacesMutex()); auto* found = planeSurface(context.device, address); if (found == nullptr || !found->Holds(texture)) return false; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp index d6b99df4f7..6df1dfa361 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -95,7 +95,9 @@ private: #endif }; -int RunNativeClear(AgcDriver::VulkanDevice& device) { +enum class ClearConsumer { Owner, Texture, Attachment }; + +int RunNativeClear(AgcDriver::VulkanDevice& device, ClearConsumer consumer) { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; const std::lock_guard lock(Memory::GpuMutex()); @@ -161,6 +163,22 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { const auto generation = Memory::TrackerGeneration(); userData[4] = 0xfffffff0u; device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast<std::uintptr_t>(code.data()), nullptr, nullptr, htileClear); + std::puts("Native full BDA clear: checking sampled owner before explicit WaitIdle"); + Require(context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the recognized HTILE clear is missing its pending write range"); + switch (consumer) { + case ClearConsumer::Owner: + Require(DepthSurfaceHolds(context, DecodeTextureResource(words), sampled.get()), "a pending full BDA clear lost its native sampled owner"); + break; + case ClearConsumer::Texture: + Require(CachedSampledTexture(context, words) == sampled, "a pending full BDA clear lost its native sampled view"); + break; + case ClearConsumer::Attachment: + Require(DepthSurfaceView(context, target) == view, "a pending full BDA clear lost its native attachment"); + break; + } + std::printf("Native full BDA clear: consumer=%u settled the recognized clear before WaitIdle\n", static_cast<unsigned>(consumer)); + Require(!context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the native HTILE consumer did not settle its clear's pending write"); + Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the native HTILE consumer did not complete its recognized clear"); device.WaitIdle(); for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) { std::uint32_t value = 0; @@ -168,6 +186,7 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { Require(value == (offset < metadataBytes ? 0xfffffff0u : 0u), "the native GPU clear did not write its exact metadata range"); } std::printf("Native full BDA clear: exact %zu-byte metadata readback passed; complete=%d\n", metadataBytes, Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation)); + Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the completed recognized native clear did not record exact coverage"); Require(CachedSampledTexture(context, words) == sampled, "a completed full BDA clear lost its native sampled view"); Require(DepthSurfaceView(context, target) == view, "a completed full BDA clear replaced its native attachment"); device.WaitIdle(); @@ -182,7 +201,11 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - return RunNativeClear(*device); + for (const auto consumer : {ClearConsumer::Owner, ClearConsumer::Texture, ClearConsumer::Attachment}) { + const auto result = RunNativeClear(*device, consumer); + if (result != 0) return result; + } + return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index d8bc68faf4..e99a24a5b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -872,7 +872,15 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, }); if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; - if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); + if (fault == ClearFault::PendingWrite) { + QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); + std::string pendingRefusal; + try { CachedSampledTexture(context, words); } + catch (const std::exception& error) { pendingRefusal = error.what(); } + const bool refused = pendingRefusal.find("external GPU writes") != std::string::npos; + if (!refused) std::fprintf(stderr, "Pending HTILE texture refusal: %s\n", pendingRefusal.c_str()); + Require(refused, "an unrecognized pending HTILE write was accepted or refused for another reason"); + } } Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view"); recorder.Sync(); From d811e7abf025ba75c1874fb1461a330c01cf5240 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Fri, 23 Oct 2026 02:15:34 +1300 Subject: [PATCH 1007/1360] fix(libc): set strtof ERANGE as the titles' libc.prx does strtof and wcstof now set ERANGE for a decimal input whose nonzero subnormal result is inexact, never for a hexadecimal nonzero subnormal, and keep it for underflow to zero and overflow. This is what the libc.prx bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509) do when run side by side with this libc. The earlier commit followed libSceLibcInternal's _Stof, which leaves errno alone for hexadecimal under- and overflow, but titles import these NIDs from their own libc.prx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/Strings.cpp | 8 ++++++++ core/libs/tests/GuestMath.cpp | 13 +++++++++++-- core/libs/tests/GuestWideStrings.cpp | 6 +++++- docs/dev/debt/libc.md | 1 + 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index c83297b840..79a99b756e 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -195,6 +195,14 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha } if (*subject == '+' || *subject == '-') ++subject; const bool hexadecimal = subject[0] == '0' && (subject[1] == 'x' || subject[1] == 'X') && parsedEnd > subject + 2; + if constexpr (std::is_same_v<TFloat, float>) { + const float magnitude = value < 0 ? -value : value; + if (value != 0 && magnitude < std::numeric_limits<float>::min()) { + const long double exact = std::strtold(text, nullptr); + errno = !hexadecimal && static_cast<long double>(value) != exact ? ERANGE : saved; + } + return value; + } if (!hexadecimal) return value; if constexpr (std::is_same_v<TFloat, double>) { constexpr double smallest = std::numeric_limits<double>::denorm_min(); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index bd3c75e4c2..aff8179bea 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -486,7 +486,6 @@ static void CheckFloatUnderflowErrno() { errno = 7; Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == 7); Require(strtod_nid_postfix("-4.9406564584124654e-324", &end) == -0x1p-1074 && errno == 7); - Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == 7); Require(strtold_nid_postfix("1e-4940", &end) == 1e-4940L && errno == 7); Require(strtod_nid_postfix("1e-330", &end) == 0.0 && errno == ERANGE); errno = 0; @@ -504,8 +503,18 @@ static void CheckFloatUnderflowErrno() { errno = 0; Require(strtod_nid_postfix("0x1p+1100", &end) == HUGE_VAL && errno == ERANGE); errno = 7; - Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == 7); Require(strtold_nid_postfix("0x1p-16446", &end) == 0.0L && strtold_nid_postfix("0x1p+20000", &end) == HUGE_VALL && errno == 7); + Require(strtof_nid_postfix("0x1p-149", &end) == 0x1p-149f && strtof_nid_postfix("0x1.8p-149", &end) == 0x1p-148f && strtof_nid_postfix("0x1.000001p-149", &end) == 0x1p-149f && errno == 7); + Require(strtof_nid_postfix("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", &end) == 0x1p-149f && strtof_nid_postfix("1.17549435e-38", &end) == 0x1p-126f && errno == 7); + Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("-1e-45", &end) == -0x1p-149f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("3e-45", &end) == 0x1p-148f && errno == ERANGE); Require(strtof_nid_postfix("1e39", &end) == HUGE_VALF && errno == ERANGE); errno = 0; } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 35c4e9cc0b..e36c75fcee 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -344,7 +344,11 @@ int main() { const char16_t* sign = u" -x"; require(std::signbit(wcstof_nid_postfix(sign, &end)) && end == sign && std::signbit(wcstold_nid_postfix(sign, &end)) && !std::signbit(wcstod_nid_postfix(sign, &end))); *__error_nid_postfix() = 0; - require(wcstof_nid_postfix(u"0x1p+200", nullptr) == HUGE_VALF && *__error_nid_postfix() == 0); + require(wcstof_nid_postfix(u"0x1p+200", nullptr) == HUGE_VALF && *__error_nid_postfix() == ERANGE); + *__error_nid_postfix() = 0; + require(wcstof_nid_postfix(u"1e-45", nullptr) == 0x1p-149f && *__error_nid_postfix() == ERANGE); + *__error_nid_postfix() = 0; + require(wcstof_nid_postfix(u"0x1p-149", nullptr) == 0x1p-149f && *__error_nid_postfix() == 0); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 7c631d55b5..1c9275dbac 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -59,6 +59,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one never does, and underflow to zero and overflow set it. Both builds also read a `NAN(...)` sequence in `strtof` as their `strtod` does, keep the sign and take a payload from its leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); that is not reproduced. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. From eb7ed110b5297e0dc82ffe40b8b207ec3b4938ca Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:52:52 +0000 Subject: [PATCH 1008/1360] feat(libSceHttp): implement auth-enabled option state --- core/libs/prx/libSceHttp/Export.cpp | 19 +++++++++++++------ core/libs/prx/libSceHttp/src/Options.hpp | 1 + core/libs/tests/GuestHttpOptions.cpp | 21 +++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 6599dbf379..6583f78a1d 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -317,9 +317,8 @@ int APS5_VABI sceHttpSendRequest(int request_id, const void* post_data, size_t s } int APS5_VABI sceHttpSetAuthEnabled(int id, int enable) { - (void)id; - (void)enable; - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::authEnabled, enable != 0) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetAuthInfoCallback(int id, HttpAuthInfoCallback callback, void* userArg) { @@ -731,8 +730,12 @@ int APS5_VABI sceHttpRemoveRequestHeader(int id, const char* name) { return 0; } -int APS5_VABI sceHttpGetAuthEnabled(int id, int* enabled) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpGetAuthEnabled(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->authEnabled ? 1 : 0; return 0; } @@ -752,7 +755,11 @@ int APS5_VABI sceHttpGetCookie(int contextId, const char* url, char* cookie, std } int APS5_VABI sceHttpGetCookieEnabled(int id, int* enabled) { - NotImplemented_nid_no_patch(__func__); + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enabled) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enabled = options->cookieEnabled ? 1 : 0; return 0; } diff --git a/core/libs/prx/libSceHttp/src/Options.hpp b/core/libs/prx/libSceHttp/src/Options.hpp index beca67b395..c92168167a 100644 --- a/core/libs/prx/libSceHttp/src/Options.hpp +++ b/core/libs/prx/libSceHttp/src/Options.hpp @@ -6,6 +6,7 @@ struct HttpOptions { bool autoRedirect = true; + bool authEnabled = false; bool nonblock = false; bool acceptEncodingGzip = false; HttpEpollHandle epoll = nullptr; diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp index 461c44658f..3950e73953 100644 --- a/core/libs/tests/GuestHttpOptions.cpp +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -16,6 +16,8 @@ int APS5_VABI sceHttpCreateRequestWithURL(int, int, const char*, std::uint64_t); int APS5_VABI sceHttpDeleteRequest(int); int APS5_VABI sceHttpSetAutoRedirect(int, int); int APS5_VABI sceHttpGetAutoRedirect(int, int*); +int APS5_VABI sceHttpSetAuthEnabled(int, int); +int APS5_VABI sceHttpGetAuthEnabled(int, int*); int APS5_VABI sceHttpSetNonblock(int, int); int APS5_VABI sceHttpGetNonblock(int, int*); int APS5_VABI sceHttpTrySetNonblock(int, int); @@ -73,6 +75,23 @@ void CheckAutoRedirect(int context) { Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); } +void CheckAuthEnabled(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAuthEnabled, pattern) == 0, "authentication is enabled by default"); + Require(sceHttpSetAuthEnabled(pattern, 1) == 0 && Get(sceHttpGetAuthEnabled, pattern) == 1, "sceHttpGetAuthEnabled did not return the set value"); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(Get(sceHttpGetAuthEnabled, connection) == 1 && Get(sceHttpGetAuthEnabled, request) == 1, "a child did not inherit its parent's authentication option"); + Require(sceHttpSetAuthEnabled(request, 0) == 0 && Get(sceHttpGetAuthEnabled, request) == 0, "authentication could not be disabled on a request"); + Require(Get(sceHttpGetAuthEnabled, connection) == 1 && Get(sceHttpGetAuthEnabled, pattern) == 1, "setting a request changed its parent options"); + int value = -1; + Require(sceHttpGetAuthEnabled(request, nullptr) == InvalidValue, "a null authentication output was not refused"); + Require(sceHttpGetAuthEnabled(UnknownId, nullptr) == InvalidValue, "the authentication output pointer is not checked before the id"); + Require(sceHttpGetAuthEnabled(UnknownId, &value) == InvalidId && sceHttpSetAuthEnabled(UnknownId, 1) == InvalidId, "an unknown id was not refused for authentication options"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpGetAuthEnabled(request, &value) == InvalidId, "a deleted request still has authentication options"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + void CheckNonblock(int context) { const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); Require(Get(sceHttpGetNonblock, pattern) == 0 && Get(sceHttpTryGetNonblock, pattern) == 0, "a template is nonblocking by default"); @@ -145,6 +164,7 @@ void CheckTermEndsOptions() { int value = -1; HttpEpollHandle bound = nullptr; Require(sceHttpGetAutoRedirect(pattern, &value) == BeforeInit && sceHttpSetAutoRedirect(pattern, 0) == BeforeInit, "the redirect option outlived the last sceHttpTerm"); + Require(sceHttpGetAuthEnabled(pattern, &value) == BeforeInit && sceHttpSetAuthEnabled(pattern, 0) == BeforeInit, "the authentication option outlived the last sceHttpTerm"); Require(sceHttpGetNonblock(connection, &value) == BeforeInit && sceHttpTryGetNonblock(connection, &value) == BeforeInit, "the nonblocking getters work after the last sceHttpTerm"); Require(sceHttpSetNonblock(connection, 1) == BeforeInit && sceHttpTrySetNonblock(connection, 1) == BeforeInit, "the nonblocking setters work after the last sceHttpTerm"); Require(sceHttpGetAcceptEncodingGZIPEnabled(pattern, &value) == BeforeInit && sceHttpSetAcceptEncodingGZIPEnabled(pattern, 1) == BeforeInit, "the gzip option outlived the last sceHttpTerm"); @@ -188,6 +208,7 @@ int main() { const int context = sceHttpInit_nid_postfix(1, 1, 0x10000); Require(context > 0, "sceHttpInit failed"); CheckAutoRedirect(context); + CheckAuthEnabled(context); CheckNonblock(context); CheckAcceptEncodingGzip(context); CheckEpoll(context); From 26ea36f114eff3992403a7899a849fd79e58a19a Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:00:21 +0000 Subject: [PATCH 1009/1360] feat(libSceSsl): implement full-buffer send and receive --- core/libs/prx/libSceSsl/Export.cpp | 6 ++---- core/libs/tests/GuestSslTransport.cpp | 11 +++++------ docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index bfc982dcdc..ceb7654ba3 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1337,8 +1337,7 @@ int APS5_VABI sceSslLoadRootCACert() { } int APS5_VABI sceSslRecv(int sslConnectionId, void* buf, std::size_t fulllen) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceSslRead(sslConnectionId, buf, fulllen, 0); } int APS5_VABI sceSslReuseConnection(int sslConnectionId, int sockId) { @@ -1347,8 +1346,7 @@ int APS5_VABI sceSslReuseConnection(int sslConnectionId, int sockId) { } int APS5_VABI sceSslSend(int sslConnectionId, void* msg, std::size_t fulllen) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceSslWrite(sslConnectionId, msg, fulllen, 0); } int APS5_VABI sceSslSetSslVersion() { diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 927dc47414..4d956c68be 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -39,6 +39,8 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslRecv(int, void*, std::size_t); +int APS5_VABI sceSslSend(int, void*, std::size_t); int APS5_VABI sceSslClose(int); int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); @@ -458,13 +460,10 @@ int main(int argc, char** argv) { Require(selected && std::strcmp(selected, "http/1.1") == 0, "the negotiated ALPN protocol is not reported"); Require(sceSslWrite(connection, buffer.data(), 1, 1) == ErrorInvalidValue, "unknown write flags were accepted"); char request[] = "ping"; - Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); - Require(sceSslRead(connection, buffer.data(), buffer.size(), 4) == ErrorInvalidValue, "unknown read flags were accepted"); - Require(sceSslRead(0, nullptr, 1, -1) == ErrorInvalidValue, "read flags did not precede other validation"); - Require(sceSslRead(connection, buffer.data(), 2, 3) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "short peek failed"); - Require(sceSslRead(connection, buffer.data(), 4, 3) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "repeated larger peek consumed bytes"); + Require(sceSslSend(connection, request, 4) == 4, "send the request"); Require(sceSslRead(connection, buffer.data(), 2, 1) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "a partial read did not return the available bytes"); - Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "a full read did not stop at the end of the stream"); + Require(sceSslRecv(connection, buffer.data(), 2) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "receive did not fill the remaining reply"); + Require(sceSslRecv(connection, buffer.data(), buffer.size()) == 0, "receive did not stop at the end of the stream"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); Require(sceSslClose(connection) == 0, "close the connection"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 0, "a read after the close did not return 0"); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 23190fe3ef..20e9e19b1d 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -4,7 +4,7 @@ - [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, CRYPTO_uninitAsymmetricKey, RSA_verifySignature, VLONG_freeVlongQueue, sceSslCheckRecvPending, sceSslCreateSslConnection and sceSslDeleteSslConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. -- [sceSslRecv, sceSslReuseConnection and sceSslSend](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Their parameter shapes follow [SharpProspero's pinned bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L222-L262), and are not measured on a PS5. +- [sceSslReuseConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - its name and NID appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); its parameter shape follows [SharpProspero's pinned binding](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L2231-L2235). No console observation or reliable implementation source for session reuse was found. - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown From 13eea0a93f09b6f7f2034c9eab1c2c949d741d1a Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:14:01 +0000 Subject: [PATCH 1010/1360] feat(libSceHttp): implement request header removal stub --- core/libs/prx/libSceHttp/Export.cpp | 3 ++- core/libs/tests/GuestHttpOptions.cpp | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 6599dbf379..f96232d6ff 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -727,7 +727,8 @@ int APS5_VABI sceHttpAddRequestHeaderRaw(int id, const char* header, int mode) { } int APS5_VABI sceHttpRemoveRequestHeader(int id, const char* name) { - NotImplemented_nid_no_patch(__func__); + (void)id; + (void)name; return 0; } diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp index 461c44658f..a8a1f507ad 100644 --- a/core/libs/tests/GuestHttpOptions.cpp +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -33,6 +33,8 @@ int APS5_VABI sceHttpsEnableOptionPrivate(int, std::uint32_t); int APS5_VABI sceHttpsDisableOptionPrivate(int, std::uint32_t); int APS5_VABI sceHttpAbortRequestForce(int); int APS5_VABI sceHttpSetProxy(int, int, int, const char*, std::uint16_t); +int APS5_VABI sceHttpAddRequestHeader(int, const char*, const char*, std::uint32_t); +int APS5_VABI sceHttpRemoveRequestHeader(int, const char*); } namespace { @@ -163,6 +165,8 @@ void CheckUnobservedOptions(int context) { const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(sceHttpAddRequestHeader(request, "X-Test", "value", 0) == 0 && sceHttpRemoveRequestHeader(request, "X-Test") == 0, "offline request header calls failed"); + Require(sceHttpRemoveRequestHeader(UnknownId, nullptr) == 0, "offline header removal started validating arguments"); Require(sceHttpCacheRedirectedConnectionEnabled(pattern, 0) == 0 && sceHttpCacheRedirectedConnectionEnabled(request, 1) == 0, "sceHttpCacheRedirectedConnectionEnabled refused a live object"); Require(sceHttpCacheRedirectedConnectionEnabled(UnknownId, 1) == InvalidId, "sceHttpCacheRedirectedConnectionEnabled took an unknown id"); for (const auto option : {sceHttpsEnableOptionPrivate, sceHttpsDisableOptionPrivate}) { From 0aac538d003aab7753a2bddb0e9ac59f5c86aad4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:10:07 +0000 Subject: [PATCH 1011/1360] fix(libSceHttp): keep auth option PR scoped --- core/libs/prx/libSceHttp/Export.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 6583f78a1d..94917d1915 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -755,11 +755,7 @@ int APS5_VABI sceHttpGetCookie(int contextId, const char* url, char* cookie, std } int APS5_VABI sceHttpGetCookieEnabled(int id, int* enabled) { - if (!HttpInitialised()) return ERROR_BEFORE_INIT; - if (!enabled) return ERROR_INVALID_VALUE; - const auto options = FindHttpOptions(id); - if (!options) return ERROR_INVALID_ID; - *enabled = options->cookieEnabled ? 1 : 0; + NotImplemented_nid_no_patch(__func__); return 0; } From 94ea385b5115bca534a8cdb04caa08409731c4e1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:07:31 +0000 Subject: [PATCH 1012/1360] feat(libSceHttp): implement cookie-enabled option state --- core/libs/prx/libSceHttp/Export.cpp | 13 ++++++++----- core/libs/prx/libSceHttp/src/Options.hpp | 1 + core/libs/tests/GuestHttpOptions.cpp | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 6599dbf379..1c3598b6af 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -330,10 +330,9 @@ int APS5_VABI sceHttpSetAuthInfoCallback(int id, HttpAuthInfoCallback callback, } int APS5_VABI sceHttpSetCookieEnabled(int id, int enable) { - (void)id; if (static_cast<uint32_t>(enable) > 1) return ERROR_INVALID_VALUE; - if (enable != 0) NotImplemented_nid_no_patch(__func__); - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::cookieEnabled, enable != 0) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetAutoRedirect(int id, int enable) { @@ -751,8 +750,12 @@ int APS5_VABI sceHttpGetCookie(int contextId, const char* url, char* cookie, std return 0; } -int APS5_VABI sceHttpGetCookieEnabled(int id, int* enabled) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpGetCookieEnabled(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->cookieEnabled ? 1 : 0; return 0; } diff --git a/core/libs/prx/libSceHttp/src/Options.hpp b/core/libs/prx/libSceHttp/src/Options.hpp index beca67b395..173053936d 100644 --- a/core/libs/prx/libSceHttp/src/Options.hpp +++ b/core/libs/prx/libSceHttp/src/Options.hpp @@ -6,6 +6,7 @@ struct HttpOptions { bool autoRedirect = true; + bool cookieEnabled = false; bool nonblock = false; bool acceptEncodingGzip = false; HttpEpollHandle epoll = nullptr; diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp index 461c44658f..4a910a6f2d 100644 --- a/core/libs/tests/GuestHttpOptions.cpp +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -16,6 +16,8 @@ int APS5_VABI sceHttpCreateRequestWithURL(int, int, const char*, std::uint64_t); int APS5_VABI sceHttpDeleteRequest(int); int APS5_VABI sceHttpSetAutoRedirect(int, int); int APS5_VABI sceHttpGetAutoRedirect(int, int*); +int APS5_VABI sceHttpSetCookieEnabled(int, int); +int APS5_VABI sceHttpGetCookieEnabled(int, int*); int APS5_VABI sceHttpSetNonblock(int, int); int APS5_VABI sceHttpGetNonblock(int, int*); int APS5_VABI sceHttpTrySetNonblock(int, int); @@ -73,6 +75,23 @@ void CheckAutoRedirect(int context) { Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); } +void CheckCookieEnabled(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetCookieEnabled, pattern) == 0, "a template enables cookies by default"); + Require(sceHttpSetCookieEnabled(pattern, 1) == 0 && Get(sceHttpGetCookieEnabled, pattern) == 1, "the cookie-enabled getter does not return what was set"); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(Get(sceHttpGetCookieEnabled, connection) == 1 && Get(sceHttpGetCookieEnabled, request) == 1, "a child did not inherit its parent's cookie option"); + Require(sceHttpSetCookieEnabled(request, 0) == 0 && Get(sceHttpGetCookieEnabled, request) == 0, "the cookie option could not be disabled on a child"); + Require(Get(sceHttpGetCookieEnabled, connection) == 1 && Get(sceHttpGetCookieEnabled, pattern) == 1, "changing a child changed its parent or sibling"); + int value = -1; + Require(sceHttpGetCookieEnabled(request, nullptr) == InvalidValue, "a null cookie output was not refused"); + Require(sceHttpSetCookieEnabled(pattern, -1) == InvalidValue && sceHttpSetCookieEnabled(pattern, 2) == InvalidValue, "an invalid cookie-enabled value was accepted"); + Require(sceHttpGetCookieEnabled(UnknownId, &value) == InvalidId && sceHttpSetCookieEnabled(UnknownId, 1) == InvalidId, "an unknown id was not refused for cookie options"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpGetCookieEnabled(request, &value) == InvalidId, "a deleted request retained its cookie option"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + void CheckNonblock(int context) { const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); Require(Get(sceHttpGetNonblock, pattern) == 0 && Get(sceHttpTryGetNonblock, pattern) == 0, "a template is nonblocking by default"); @@ -145,6 +164,7 @@ void CheckTermEndsOptions() { int value = -1; HttpEpollHandle bound = nullptr; Require(sceHttpGetAutoRedirect(pattern, &value) == BeforeInit && sceHttpSetAutoRedirect(pattern, 0) == BeforeInit, "the redirect option outlived the last sceHttpTerm"); + Require(sceHttpGetCookieEnabled(pattern, &value) == BeforeInit && sceHttpSetCookieEnabled(pattern, 0) == BeforeInit, "the cookie option outlived the last sceHttpTerm"); Require(sceHttpGetNonblock(connection, &value) == BeforeInit && sceHttpTryGetNonblock(connection, &value) == BeforeInit, "the nonblocking getters work after the last sceHttpTerm"); Require(sceHttpSetNonblock(connection, 1) == BeforeInit && sceHttpTrySetNonblock(connection, 1) == BeforeInit, "the nonblocking setters work after the last sceHttpTerm"); Require(sceHttpGetAcceptEncodingGZIPEnabled(pattern, &value) == BeforeInit && sceHttpSetAcceptEncodingGZIPEnabled(pattern, 1) == BeforeInit, "the gzip option outlived the last sceHttpTerm"); @@ -188,6 +208,7 @@ int main() { const int context = sceHttpInit_nid_postfix(1, 1, 0x10000); Require(context > 0, "sceHttpInit failed"); CheckAutoRedirect(context); + CheckCookieEnabled(context); CheckNonblock(context); CheckAcceptEncodingGzip(context); CheckEpoll(context); From b6dc6a5d4df3559eecfbab37613b70b8d52c535f Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:26:04 +0100 Subject: [PATCH 1013/1360] feat(libSceVideodec): implement sceVideodecQueryResourceInfo Validate the configuration and resource information as sceVideodecCreateDecoder does, map the configuration the same way and report the memory and frame buffer requirements that sceVideodec2QueryDecoderMemoryInfo gives for it. --- core/libs/prx/libSceVideodec/Export.cpp | 46 ++++++++++++---- core/libs/tests/GuestVideodec.cpp | 73 +++++++++++++++++++++++++ docs/dev/debt/libSceVideodec.md | 8 +-- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index 56919ce362..56ae352fcf 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -168,6 +168,10 @@ struct Decoder2Crop { }; static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(offsetof(ConfigInfo, maxFrameWidth) == 20 && offsetof(ConfigInfo, videodecFlags) == 32); +static_assert(offsetof(ResourceInfo, cpuGpuMemorySize) == 24 && offsetof(ResourceInfo, maxFrameBufferSize) == 40); +static_assert(offsetof(ResourceInfo, frameBufferAlignment) == 48); +static_assert(sizeof(Decoder2ConfigInfo) == 40 && sizeof(Decoder2MemoryInfo) == 72); static_assert(sizeof(FrameBuffer) == 24 && sizeof(InputData) == 48 && sizeof(PictureInfo) == 112); static_assert(sizeof(CodecInfo) == 64 && offsetof(PictureInfo, codec) == 48); @@ -212,6 +216,20 @@ struct Geometry { Decoder2Crop crop{}; }; +Decoder2ConfigInfo MapConfig(const ConfigInfo& config) { + Decoder2ConfigInfo decoderConfig{}; + decoderConfig.thisSize = sizeof(Decoder2ConfigInfo); + decoderConfig.resourceType = 1; + decoderConfig.codecType = CodecAvc; + decoderConfig.profile = config.profile; + decoderConfig.maxLevel = config.maxLevel; + decoderConfig.maxFrameWidth = config.maxFrameWidth; + decoderConfig.maxFrameHeight = config.maxFrameHeight; + decoderConfig.maxDpbFrameCount = config.maxDpbFrameCount; + decoderConfig.decodePipelineDepth = 1; + return decoderConfig; +} + Geometry Measure(const Decoder2OutputInfo& output, const Decoder2Crop& crop) { Geometry geometry; geometry.width = output.frameWidth; @@ -266,16 +284,7 @@ extern "C" { int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo* config, const ResourceInfo* resource, Ctrl* ctrl) { if (config == nullptr || resource == nullptr || ctrl == nullptr) return VideodecErrorArgumentPointer; if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; - Decoder2ConfigInfo decoderConfig{}; - decoderConfig.thisSize = sizeof(Decoder2ConfigInfo); - decoderConfig.resourceType = 1; - decoderConfig.codecType = CodecAvc; - decoderConfig.profile = config->profile; - decoderConfig.maxLevel = config->maxLevel; - decoderConfig.maxFrameWidth = config->maxFrameWidth; - decoderConfig.maxFrameHeight = config->maxFrameHeight; - decoderConfig.maxDpbFrameCount = config->maxDpbFrameCount; - decoderConfig.decodePipelineDepth = 1; + const auto decoderConfig = MapConfig(*config); Decoder2MemoryInfo decoderMemory{}; decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); decoderMemory.cpuMemorySize = resource->cpuMemorySize; @@ -407,8 +416,21 @@ int APS5_VABI sceVideodecMapMemory(void) { return 0; } -int APS5_VABI sceVideodecQueryResourceInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVideodecQueryResourceInfo(const ConfigInfo* config, ResourceInfo* resource) { + if (config == nullptr || resource == nullptr) return VideodecErrorArgumentPointer; + if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; + const auto decoderConfig = MapConfig(*config); + Decoder2MemoryInfo decoderMemory{}; + decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); + try { + sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&decoderConfig, &decoderMemory); + } catch (const std::exception&) { + return VideodecErrorConfigInfo; + } + resource->cpuMemorySize = decoderMemory.cpuMemorySize; + resource->cpuGpuMemorySize = decoderMemory.cpuGpuMemorySize; + resource->maxFrameBufferSize = decoderMemory.maxFrameBufferSize; + resource->frameBufferAlignment = decoderMemory.frameBufferAlignment; return 0; } diff --git a/core/libs/tests/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp index f4ad8e987e..894d6f96ee 100644 --- a/core/libs/tests/GuestVideodec.cpp +++ b/core/libs/tests/GuestVideodec.cpp @@ -83,8 +83,33 @@ struct InputData { std::uint64_t attachedData; }; +struct Decoder2ConfigInfo { + std::uint64_t thisSize; + std::uint32_t resourceType; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint32_t decodePipelineDepth; +}; + +struct Decoder2MemoryInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t gpuMemorySize; + void* gpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + static_assert(sizeof(PictureInfo) == 112 && sizeof(AvcInfo) == 36); static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(sizeof(Decoder2ConfigInfo) == 40 && sizeof(Decoder2MemoryInfo) == 72); extern "C" { int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo*, const ResourceInfo*, Ctrl*); @@ -92,6 +117,8 @@ int APS5_VABI sceVideodecDecode(Ctrl*, const InputData*, FrameBuffer*, PictureIn int APS5_VABI sceVideodecDeleteDecoder(Ctrl*); int APS5_VABI sceVideodecFlush(Ctrl*, FrameBuffer*, PictureInfo*); int APS5_VABI sceVideodecReset(Ctrl*); +int APS5_VABI sceVideodecQueryResourceInfo(const ConfigInfo*, ResourceInfo*); +int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const Decoder2ConfigInfo*, Decoder2MemoryInfo*); } namespace { @@ -105,6 +132,7 @@ constexpr int ErrorAuSize = static_cast<int>(0x80C10009); constexpr int ErrorAuPointer = static_cast<int>(0x80C1000A); constexpr int ErrorFrameBufferSize = static_cast<int>(0x80C1000B); constexpr int ErrorFrameBufferPointer = static_cast<int>(0x80C1000C); +constexpr int ErrorConfigInfo = static_cast<int>(0x80C1000E); constexpr int ErrorArgumentPointer = static_cast<int>(0x80C1000F); constexpr std::uint32_t Pitch = 64; @@ -230,6 +258,50 @@ void testReset() { check(!picture.isValid, "a reset decoder produced a picture"); } +void testQueryResourceInfo(std::int32_t width, std::int32_t height, std::uint32_t profile, std::uint32_t level) { + const ConfigInfo config{sizeof(ConfigInfo), 1, profile, level, width, height, 4, 0}; + ResourceInfo resource{sizeof(ResourceInfo)}; + check(sceVideodecQueryResourceInfo(&config, &resource) == 0, "the resource query failed"); + const Decoder2ConfigInfo mapped{sizeof(Decoder2ConfigInfo), 1, 1, profile, level, width, height, 4, 1}; + Decoder2MemoryInfo memory{sizeof(Decoder2MemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&mapped, &memory) == 0, "the Videodec2 memory query failed"); + check(resource.thisSize == sizeof(ResourceInfo), "the resource information size was changed"); + check(resource.cpuMemorySize == memory.cpuMemorySize, "the CPU memory size differs from libSceVideodec2"); + check(resource.cpuGpuMemorySize == memory.cpuGpuMemorySize, "the CPU-GPU memory size differs from libSceVideodec2"); + check(resource.maxFrameBufferSize == memory.maxFrameBufferSize, "the frame buffer size differs from libSceVideodec2"); + check(resource.frameBufferAlignment == memory.frameBufferAlignment, "the frame buffer alignment differs from libSceVideodec2"); +} + +void testQueryResourceInfo() { + testQueryResourceInfo(static_cast<std::int32_t>(Width), static_cast<std::int32_t>(CodedHeight), 100, 42); + testQueryResourceInfo(1920, 1080, 100, 42); + testQueryResourceInfo(3840, 2160, 100, 51); + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast<std::int32_t>(Width), + static_cast<std::int32_t>(CodedHeight), 4, 0}; + ResourceInfo resource{sizeof(ResourceInfo)}; + check(sceVideodecQueryResourceInfo(&config, &resource) == 0, "the resource query failed"); + check(resource.maxFrameBufferSize >= FrameBytes, "the frame buffer size does not hold a decoded picture"); + Ctrl ctrl{sizeof(Ctrl)}; + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "the queried resources were refused"); + check(sceVideodecDeleteDecoder(&ctrl) == 0, "the decoder was not deleted"); + check(sceVideodecQueryResourceInfo(nullptr, &resource) == ErrorArgumentPointer, "a null configuration was accepted"); + check(sceVideodecQueryResourceInfo(&config, nullptr) == ErrorArgumentPointer, "null resource information was accepted"); + const ResourceInfo sentinel{sizeof(ResourceInfo), 7, nullptr, 7, nullptr, 7, 7}; + auto refused = sentinel; + auto shortConfig = config; + shortConfig.thisSize = sizeof(ConfigInfo) - 8; + check(sceVideodecQueryResourceInfo(&shortConfig, &refused) == ErrorStructSize, "a short configuration was accepted"); + auto shortResource = sentinel; + shortResource.thisSize = sizeof(ResourceInfo) - 8; + check(sceVideodecQueryResourceInfo(&config, &shortResource) == ErrorStructSize, "short resource information was accepted"); + auto unsized = config; + unsized.maxFrameWidth = 0; + check(sceVideodecQueryResourceInfo(&unsized, &refused) == ErrorConfigInfo, "a configuration without a frame size was accepted"); + check(refused.cpuMemorySize == 7 && refused.cpuGpuMemorySize == 7 && refused.maxFrameBufferSize == 7 && refused.frameBufferAlignment == 7, + "a refused query wrote the resource information"); + check(shortResource.cpuMemorySize == 7 && shortResource.maxFrameBufferSize == 7, "a refused query wrote the resource information"); +} + } int main() { @@ -238,6 +310,7 @@ int main() { testDecodeCropped(); testErrors(); testReset(); + testQueryResourceInfo(); std::puts("Videodec tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md index c24087ab78..8abac004cf 100644 --- a/docs/dev/debt/libSceVideodec.md +++ b/docs/dev/debt/libSceVideodec.md @@ -2,14 +2,14 @@ ### Unknown function info -- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush and sceVideodecReset](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the structure layouts and the error codes follow [shadPS4's PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.h) and its [implementation](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.cpp), not measured on a PS5: the console's error for a rejected argument and its own version of the structures are unknown. The decoder runs through libSceVideodec2, which decodes H.264 with FFmpeg, and the picture is copied into the caller's frame buffer with the pitch aligned to 64 and the coded height, as the reference writes it - -- [libSceVideodec](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the module's other two exports, `sceVideodecMapMemory` and `sceVideodecQueryResourceInfo`, are not provided, so a title that imports them fails to start +- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush, sceVideodecQueryResourceInfo and sceVideodecReset](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the structure layouts and the error codes follow [shadPS4's PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.h) and its [implementation](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.cpp), not measured on a PS5: the console's error for a rejected argument and its own version of the structures are unknown. The decoder runs through libSceVideodec2, which decodes H.264 with FFmpeg, and the picture is copied into the caller's frame buffer with the pitch aligned to 64 and the coded height, as the reference writes it ### Functional - [sceVideodecCreateDecoder](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the codec type is not checked and the decoder always decodes H.264, as the PS4 library does; the console's codec ids are not documented. A configuration without a maximum frame size is refused with the configuration error, and the resource information the caller passes is not used +- [sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the sizes it reports are libSceVideodec2's, not the console's: the CPU memory, CPU-GPU memory and frame buffer sizes and the frame buffer alignment that `sceVideodec2QueryDecoderMemoryInfo` gives for the configuration mapped as `sceVideodecCreateDecoder` maps it. The GPU memory size that query also gives has no field in this structure and is dropped. The memory pointers and `thisSize` are left as the caller passed them, where [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/videodec/videodec.cpp#L137-L187) clears the pointers; what the console writes there is not known. A larger structure is accepted, as by the module's other exports, where shadPS4 requires the exact size, and a configuration without a maximum frame size is refused with the configuration error, where shadPS4 assumes 1920x1080 or 3840x2160 + - [sceVideodecDecode and sceVideodecFlush](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the picture information's video usability information (the tick rate, the sample aspect ratio and the colour description) stays zero, as the decoder does not report it, and the crop offsets are the decoded picture's crop halved, the unit the H.264 offsets use. The frame buffer holds the coded picture the decoder reports, so the rows the crop covers are in it and the caller crops them -- [sceVideodecMapMemory and sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - their names are listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Their signatures and behaviour are unverified; these no-parameter declarations only resolve imports and throw through NotImplemented_nid_no_patch. +- [sceVideodecMapMemory](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its name is listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Its signature and behaviour are unverified; this no-parameter declaration only resolves the import and throws through NotImplemented_nid_no_patch. From f0e63df88592c9db9dea9dd13be03506ef5e6abd Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:28:00 +0100 Subject: [PATCH 1014/1360] feat(libSceNpManager): answer the user ID account queries for a signed-out user sceNpCheckNpAvailabilityA, sceNpGetAccountLanguageA, sceNpGetParentalControlInfoA and sceNpGetAccountDateOfBirthA return SIGNED_OUT without writing, like sceNpGetAccountCountryA. The game presence queries report OFFLINE. A null pointer or user id -1 is INVALID_ARGUMENT, and the request forms refuse request id 0 and an unknown request as sceNpSetGamePresenceOnline does. Signatures and checks follow shadPS4 and Orbital; sceNpIsPlusMember keeps throwing. --- core/libs/prx/libSceNpManager/Export.cpp | 47 ++++++++++------- core/libs/tests/GuestNpManager.cpp | 66 ++++++++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 3 +- 3 files changed, 96 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 93a1095e2e..15fd862b97 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -20,6 +20,7 @@ static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast<int>(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; +static constexpr uint32_t NP_GAME_PRESENCE_STATUS_OFFLINE = 0; static std::atomic<int> g_nextRequest{1}; @@ -37,6 +38,12 @@ int AddRequest() { return reqId; } +int SignedOutRequest(int reqId) { + if (reqId == 0) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(requestMutex); + return liveRequests.count(reqId) ? SCE_NP_ERROR_SIGNED_OUT : SCE_NP_ERROR_REQUEST_NOT_FOUND; +} + using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata); using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata); @@ -253,29 +260,31 @@ int APS5_VABI sceNpAsmTerminate(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpCheckNpAvailabilityA(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpCheckNpAvailabilityA(int reqId, int userId) { + if (userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpGetAccountDateOfBirthA(int userId, void* dateOfBirth) { + if (!dateOfBirth || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} +int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language) { + if (!language || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, uint32_t* status) { + if (!onlineId || !status) return SCE_NP_ERROR_INVALID_ARGUMENT; + *status = NP_GAME_PRESENCE_STATUS_OFFLINE; return 0; } -int APS5_VABI sceNpGetAccountDateOfBirthA(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpGetGamePresenceStatusA(int userId, uint32_t* status) { + if (!status || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + *status = NP_GAME_PRESENCE_STATUS_OFFLINE; return 0; } -int APS5_VABI sceNpGetAccountLanguageA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpGetGamePresenceStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpGetGamePresenceStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpGetParentalControlInfoA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, int8_t* age, void* info) { + if (!age || !info || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); } int APS5_VABI sceNpIdMapperAbortRequest(void) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 9c51efbe00..67e63ff3b0 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -49,6 +49,15 @@ void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); } +extern "C" { +int APS5_VABI sceNpCheckNpAvailabilityA(int reqId, int userId); +int APS5_VABI sceNpGetAccountDateOfBirthA(int userId, void* dateOfBirth); +int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language); +int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, std::uint32_t* status); +int APS5_VABI sceNpGetGamePresenceStatusA(int userId, std::uint32_t* status); +int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, std::int8_t* age, void* info); +} + namespace { constexpr int InvalidArgument = static_cast<int>(0x80550003u); @@ -281,6 +290,62 @@ void CheckOnlineIdQueries() { } } +void CheckUserIdQueries() { + constexpr int InvalidUserId = -1; + constexpr int OtherUserId = 0x10000; + constexpr int NeverCreated = 0x7fffffff; + unsigned char output[64]; + std::memset(output, 0x5a, sizeof(output)); + unsigned char untouched[64]; + std::memset(untouched, 0x5a, sizeof(untouched)); + std::int8_t age = 0x5a; + const int request = sceNpCreateRequest(); + Require(request > 0, "sceNpCreateRequest must return a positive request id"); + for (const int userId : {InitialUserId, OtherUserId}) { + Require(sceNpCheckNpAvailabilityA(request, userId) == SignedOut, "sceNpCheckNpAvailabilityA must report the user as signed out"); + Require(sceNpGetAccountLanguageA(request, userId, output) == SignedOut, "sceNpGetAccountLanguageA must report the user as signed out"); + Require(sceNpGetParentalControlInfoA(request, userId, &age, output) == SignedOut, "sceNpGetParentalControlInfoA must report the user as signed out"); + Require(sceNpGetAccountDateOfBirthA(userId, output) == SignedOut, "sceNpGetAccountDateOfBirthA must report the user as signed out"); + } + Require(sceNpCheckNpAvailabilityA(request, InvalidUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must reject the invalid user id"); + Require(sceNpCheckNpAvailabilityA(0, InitialUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must reject request id 0"); + Require(sceNpCheckNpAvailabilityA(NeverCreated, InitialUserId) == RequestNotFound, "sceNpCheckNpAvailabilityA must reject a request that was never created"); + Require(sceNpCheckNpAvailabilityA(NeverCreated, InvalidUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must check the user before the request"); + Require(sceNpGetAccountLanguageA(request, InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountLanguageA must reject a null output"); + Require(sceNpGetAccountLanguageA(request, InvalidUserId, output) == InvalidArgument, "sceNpGetAccountLanguageA must reject the invalid user id"); + Require(sceNpGetAccountLanguageA(0, InitialUserId, output) == InvalidArgument, "sceNpGetAccountLanguageA must reject request id 0"); + Require(sceNpGetAccountLanguageA(NeverCreated, InitialUserId, output) == RequestNotFound, "sceNpGetAccountLanguageA must reject a request that was never created"); + Require(sceNpGetAccountLanguageA(NeverCreated, InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountLanguageA must check the output before the request"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, nullptr, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject a null age"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, &age, nullptr) == InvalidArgument, "sceNpGetParentalControlInfoA must reject a null info"); + Require(sceNpGetParentalControlInfoA(request, InvalidUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject the invalid user id"); + Require(sceNpGetParentalControlInfoA(0, InitialUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject request id 0"); + Require(sceNpGetParentalControlInfoA(NeverCreated, InitialUserId, &age, output) == RequestNotFound, "sceNpGetParentalControlInfoA must reject a request that was never created"); + Require(sceNpGetParentalControlInfoA(NeverCreated, InvalidUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must check the user before the request"); + Require(sceNpGetAccountDateOfBirthA(InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountDateOfBirthA must reject a null output"); + Require(sceNpGetAccountDateOfBirthA(InvalidUserId, output) == InvalidArgument, "sceNpGetAccountDateOfBirthA must reject the invalid user id"); + Require(sceNpDeleteRequest(request) == 0, "sceNpDeleteRequest failed"); + Require(sceNpCheckNpAvailabilityA(request, InitialUserId) == RequestNotFound, "sceNpCheckNpAvailabilityA must reject a deleted request"); + Require(sceNpGetAccountLanguageA(request, InitialUserId, output) == RequestNotFound, "sceNpGetAccountLanguageA must reject a deleted request"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, &age, output) == RequestNotFound, "sceNpGetParentalControlInfoA must reject a deleted request"); + Require(std::memcmp(output, untouched, sizeof(output)) == 0 && age == 0x5a, "a signed-out account query wrote its output"); + constexpr std::uint32_t Sentinel = 0x5a5a5a5au; + constexpr std::uint32_t Offline = 0; + NpOnlineId onlineId{}; + std::memcpy(onlineId.data, "player", 6); + for (const int userId : {InitialUserId, OtherUserId}) { + std::uint32_t status = Sentinel; + Require(sceNpGetGamePresenceStatusA(userId, &status) == 0 && status == Offline, "sceNpGetGamePresenceStatusA must report the user as offline"); + } + std::uint32_t status = Sentinel; + Require(sceNpGetGamePresenceStatusA(InvalidUserId, &status) == InvalidArgument, "sceNpGetGamePresenceStatusA must reject the invalid user id"); + Require(sceNpGetGamePresenceStatusA(InitialUserId, nullptr) == InvalidArgument, "sceNpGetGamePresenceStatusA must reject a null status"); + Require(sceNpGetGamePresenceStatus(nullptr, &status) == InvalidArgument, "sceNpGetGamePresenceStatus must reject a null online id"); + Require(sceNpGetGamePresenceStatus(&onlineId, nullptr) == InvalidArgument, "sceNpGetGamePresenceStatus must reject a null status"); + Require(status == Sentinel, "a rejected presence query wrote its status"); + Require(sceNpGetGamePresenceStatus(&onlineId, &status) == 0 && status == Offline, "sceNpGetGamePresenceStatus must report the online id as offline"); +} + } struct ContentRestrictionFields { @@ -325,6 +390,7 @@ void CheckPlusAndContentRestriction() { int main() { CheckOnlineIdQueries(); + CheckUserIdQueries(); CheckStateCallbacksA(); CheckLegacyStateCallback(); CheckRequestTimeouts(); diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 96633c6c2b..a484f93b3a 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -6,7 +6,7 @@ ### Unknown function info -- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 149 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. +- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 143 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp): the original name remains unknown. The native wrapper takes a user ID and a read-only 20-byte online-ID record, validates its first byte and sends the record through IPC. The implementation remains a throwing stub. @@ -18,6 +18,7 @@ ### Functional - [sceNpGetAccountCountry, sceNpGetAccountDateOfBirth, sceNpGetAccountLanguage, sceNpGetParentalControlInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms taking an online id, not measured on a console. Signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp): a null pointer is `INVALID_ARGUMENT`; no user is signed in, so no online id matches one and the rest return `USER_NOT_FOUND` without writing, like `sceNpCheckNpAvailability`. Request ids are not checked +- [sceNpCheckNpAvailabilityA, sceNpGetAccountDateOfBirthA, sceNpGetAccountLanguageA, sceNpGetGamePresenceStatus, sceNpGetGamePresenceStatusA, sceNpGetParentalControlInfoA](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - answer as if no PSN account is signed in; the signed-in behaviour is not implemented and none of it is measured on a console. Checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_manager.cpp): a null pointer or user id -1 is `INVALID_ARGUMENT`, then request id 0 is `INVALID_ARGUMENT` and an unknown one `REQUEST_NOT_FOUND`. Other users get `SIGNED_OUT` with nothing written, as from `sceNpGetAccountCountryA`; shadPS4's `sceNpGetAccountDateOfBirthA` returns `USER_NOT_FOUND` for a user it does not know. An async request gets `SIGNED_OUT` from the call; shadPS4 returns 0 and leaves it to `sceNpPollAsync`. The presence queries write `OFFLINE` (0) and return 0 - [sceNpGetUserIdByOnlineId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(online id, user id output)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L703): a null online id or output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output, as `sceNpGetUserIdByAccountId` does - [sceNpCheckPlus](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(request id, parameter, result)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L246): request id 0 or a null parameter or result returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and every other call returns `SCE_NP_ERROR_SIGNED_OUT` without writing the result, as `sceNpCheckPremium` does. The parameter's size and feature fields, which shadPS4 checks, are not read: the errors the console returns for them are not known - [sceNpGetAccountId, sceNpSetGamePresenceOnline](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms; the `(online id, account id output)` and `(request id, online id, presence)` signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1092-L1096). A null online id or output, or request id 0 for `sceNpSetGamePresenceOnline`, returns `SCE_NP_ERROR_INVALID_ARGUMENT`. `sceNpSetGamePresenceOnline` then returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` for a request that was never created or was deleted, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_manager.cpp#L1098-L1104) does. Otherwise both return `SCE_NP_ERROR_SIGNED_OUT` without writing, as `sceNpGetAccountIdA` does. The console's result for a signed-out user is not measured From 3cf419d4eab3a07a9e84f76e5f6062d224cc0259 Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Tue, 20 Oct 2026 21:04:58 +0200 Subject: [PATCH 1015/1360] fix(libSceRtc): handle out-of-range time values as the PS4 does Measured on a PS4 (firmware 10.71) with an OpenOrbis homebrew that loads a probe PRX calling libSceRtc; every output starts as a sentinel, so stores and their width are visible. - sceRtcSetTime_t takes the low 32 bits of its argument as unsigned seconds and returns 0 for every value: -1 is 2106-02-07 06:28:15, 2^32 + 1 is 1970-01-01 00:00:01 and -2^31 is 2038-01-19 03:14:08. sceRtcGetTime_t stays 64-bit (2106-02-07 06:28:16 is 2^32). - sceRtcGetTime_t before 1970 and sceRtcGetWin32FileTime before 1601 store 0 and return 0x80B50008. Field validation still comes first and leaves the output unchanged (1969-13-01 returns 0x80B50009). - sceRtcSetTick and sceRtcSetWin32FileTime have no upper limit: the tick after 9999-12-31 23:59:59.999999 is 10000-01-01 and the file time 2^64 - 1 is 60056-05-28 05:36:10.955161. sceRtcGetDayOfWeek accepts years past 9999 (10000-01-01 is 6). - sceRtcFormatRFC3339 returns 0x80B50008 when the zone-shifted tick is outside years 1 to 9999. sceRtcFormatRFC2822 returns 0x80B50003 when it is negative as a signed value and 0x80B50008 when it is past 9999. The PS5-measured cases of GuestRtc.cpp that the probe repeated (RFC 3339 and RFC 2822 parsing, named and military zones, DOS time, TickAdd) give the same results on the PS4. The three expectations changed here (SetTick past 9999, SetTime_t(-1), RFC 2822 at the limit with +1 minute) came from commits that cite no measurement. Not yet confirmed on PS5. Written with AI assistance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceRtc/Export.cpp | 26 ++++++++++++---------- core/libs/tests/GuestRtc.cpp | 35 +++++++++++++++++++++++++++--- docs/dev/debt/libSceRtc.md | 1 + 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 3cc3b045de..31c561305f 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -370,7 +370,7 @@ int APS5_VABI sceRtcGetDaysInMonth(int year, int month) { } int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { - if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; if (day < 1 || day > daysInMonth(year, month)) return SCE_RTC_ERROR_INVALID_DAY; return static_cast<int>((daysFromCivil(year, static_cast<unsigned>(month), static_cast<unsigned>(day)) + 1) % 7); @@ -389,7 +389,6 @@ int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick) { int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick) { if (!time || !tick) return SCE_RTC_ERROR_INVALID_POINTER; - if (tick->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; *time = fromTick(tick->tick); return 0; } @@ -442,15 +441,17 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { if (!seconds) return SCE_RTC_ERROR_INVALID_POINTER; if (const int result = validate(time); result != 0) return result; const std::uint64_t tick = toTick(*time); - if (tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + if (tick < UNIX_EPOCH_TICK) { + *seconds = 0; + return SCE_RTC_ERROR_INVALID_YEAR; + } *seconds = static_cast<int64_t>((tick - UNIX_EPOCH_TICK) / TICKS_PER_SECOND); return 0; } int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (seconds < 0 || static_cast<std::uint64_t>(seconds) > (MAX_TICK - UNIX_EPOCH_TICK) / TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_VALUE; - *time = fromTick(UNIX_EPOCH_TICK + static_cast<std::uint64_t>(seconds) * TICKS_PER_SECOND); + *time = fromTick(UNIX_EPOCH_TICK + static_cast<std::uint64_t>(static_cast<std::uint32_t>(seconds)) * TICKS_PER_SECOND); return 0; } @@ -458,14 +459,16 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_ti if (!win32_time) return SCE_RTC_ERROR_INVALID_POINTER; if (const int result = validate(time); result != 0) return result; const std::uint64_t tick = toTick(*time); - if (tick < WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + if (tick < WIN32_EPOCH_TICK) { + *win32_time = 0; + return SCE_RTC_ERROR_INVALID_YEAR; + } *win32_time = (tick - WIN32_EPOCH_TICK) * 10; return 0; } int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32_time) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (win32_time / 10 > MAX_TICK - WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; *time = fromTick(WIN32_EPOCH_TICK + win32_time / 10); return 0; } @@ -587,8 +590,8 @@ int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZo utc = &now; } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; - RtcTick local{}; - if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcTick local{utc->tick + static_cast<std::uint64_t>(static_cast<std::int64_t>(timeZoneMinutes) * TICKS_PER_MINUTE)}; + if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); if (timeZoneMinutes == 0) { @@ -619,8 +622,9 @@ int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZo utc = &now; } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; - RtcTick local{}; - if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcTick local{utc->tick + static_cast<std::uint64_t>(static_cast<std::int64_t>(timeZoneMinutes) * TICKS_PER_MINUTE)}; + if (static_cast<std::int64_t>(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; + if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 53e55fd327..36c8411c72 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -134,6 +134,7 @@ int main() { Require(sceRtcGetDaysInMonth(10000, 1) == 31); Require(sceRtcGetDaysInMonth(9999, 1) == 31); Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); + Require(sceRtcGetDayOfWeek(10000, 1, 1) == 6); Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); RtcDateTime leapDay{2024, 2, 29, 12, 34, 56, 789000}; @@ -156,7 +157,7 @@ int main() { tick.tick = maxTick; Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{9999, 12, 31, 23, 59, 59, 999999})); tick.tick = maxTick + 1; - Require(sceRtcSetTick(&converted, &tick) == invalidValue); + Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); @@ -171,10 +172,24 @@ int main() { RtcDateTime millennium{2000, 1, 1, 0, 0, 0, 0}; Require(sceRtcGetTime_t(&millennium, &seconds) == 0 && seconds == 946684800); Require(sceRtcSetTime_t(&converted, 946684800) == 0 && Equal(converted, millennium)); - Require(sceRtcSetTime_t(&converted, -1) == invalidValue); + Require(sceRtcSetTime_t(&converted, -1) == 0 && Equal(converted, RtcDateTime{2106, 2, 7, 6, 28, 15, 0})); + Require(sceRtcSetTime_t(&converted, 0x100000001ll) == 0 && Equal(converted, RtcDateTime{1970, 1, 1, 0, 0, 1, 0})); + Require(sceRtcSetTime_t(&converted, -0x80000000ll) == 0 && Equal(converted, RtcDateTime{2038, 1, 19, 3, 14, 8, 0})); + const RtcDateTime past2106{2106, 2, 7, 6, 28, 16, 0}; + Require(sceRtcGetTime_t(&past2106, &seconds) == 0 && seconds == 0x100000000ll); + const RtcDateTime beforeEpoch{1969, 12, 31, 23, 59, 59, 0}; + seconds = -1; + Require(sceRtcGetTime_t(&beforeEpoch, &seconds) == invalidYear && seconds == 0); + const RtcDateTime badMonth{1969, 13, 1, 0, 0, 0, 0}; + seconds = -1; + Require(sceRtcGetTime_t(&badMonth, &seconds) == invalidMonth && seconds == -1); std::uint64_t fileTime = 0; Require(sceRtcGetWin32FileTime(&epoch, &fileTime) == 0 && fileTime == 116444736000000000ull); Require(sceRtcSetWin32FileTime(&converted, 116444736000000000ull) == 0 && Equal(converted, epoch)); + const RtcDateTime before1601{1600, 12, 31, 23, 59, 59, 0}; + fileTime = 1; + Require(sceRtcGetWin32FileTime(&before1601, &fileTime) == invalidYear && fileTime == 0); + Require(sceRtcSetWin32FileTime(&converted, std::numeric_limits<std::uint64_t>::max()) == 0 && Equal(converted, RtcDateTime{60056, 5, 28, 5, 36, 10, 955161})); std::uint32_t dosTime = 0xffffffffu; const RtcDateTime dosDate{2024, 2, 29, 12, 34, 57, 789000}; Require(sceRtcGetDosTime(&dosDate, &dosTime) == 0 && dosTime == 0x585d645cu); @@ -270,6 +285,16 @@ int main() { Require(sceRtcFormatRFC3339(text, &tick, offset) == invalidValue); Require(std::memcmp(text, original, sizeof(text)) == 0); } + RtcTick edge{maxTick + 1}; + Require(sceRtcFormatRFC3339(text, &edge, 0) == invalidYear); + edge.tick = std::numeric_limits<std::uint64_t>::max(); + Require(sceRtcFormatRFC3339(text, &edge, 0) == invalidYear); + edge.tick = maxTick; + Require(sceRtcFormatRFC3339(text, &edge, 1) == invalidYear); + edge.tick = 59999999; + Require(sceRtcFormatRFC3339(text, &edge, -1) == invalidYear); + edge.tick = 60000000; + Require(sceRtcFormatRFC3339(text, &edge, -1) == 0 && std::strcmp(text, "0001-01-01T00:00:00.00-00:01") == 0); Require(sceRtcFormatRFC3339(nullptr, &tick, 1440) == invalidPointer); Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidValue); { @@ -775,7 +800,11 @@ int main() { 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); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidYear); + tick.tick = maxTick + 1; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidYear); + tick.tick = std::numeric_limits<std::uint64_t>::max(); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidValue); tick.tick = leapDayTick; for (int offset : {1440, -1440, std::numeric_limits<int>::min(), std::numeric_limits<int>::max()}) { std::memset(rfc2822, 'x', sizeof(rfc2822)); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 56985f25a2..0139c6860b 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -7,6 +7,7 @@ ### Unknown function info - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_VALUE` when the zone-shifted tick is negative as a signed value, `INVALID_YEAR` when it is past 9999, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick returning `INVALID_POINTER` (shadPS4 formats the current time instead) and offsets outside ±1439 minutes returning `INVALID_VALUE` are not measured for these three functions - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 2d1ecaf29e3b4d9038925c020edd3de39d65fa40 Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Tue, 20 Oct 2026 21:22:23 +0200 Subject: [PATCH 1016/1360] fix(libSceRtc): reject a negative source tick in sceRtcFormatRFC2822 Measured on a PS4 (firmware 10.71) for the review of the zone-shift overflow: sceRtcFormatRFC2822 returns 0x80B50003 for UINT64_MAX with +1 and +1439 minutes and for 2^64 - 1 minute with +1, where the shifted tick lands in year 1. The console returns 0x80B50008 when the shifted tick is past 9999 as a signed value (2^63 with -1 minute) and otherwise 0x80B50003 when the source or the shifted tick is negative as a signed value (2^63 with 0, 0 with -1); the tick after 9999 with -1 minute still formats. sceRtcFormatRFC3339 does wrap on the console: UINT64_MAX with +1 minute returns 0 and writes 0001-01-01T00:00:59.99+00:01, as the implementation already does. Tests now cover both. Written with AI assistance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceRtc/Export.cpp | 4 ++-- core/libs/tests/GuestRtc.cpp | 14 ++++++++++++++ docs/dev/debt/libSceRtc.md | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 31c561305f..a1dffea9ac 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -623,8 +623,8 @@ int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZo } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; const RtcTick local{utc->tick + static_cast<std::uint64_t>(static_cast<std::int64_t>(timeZoneMinutes) * TICKS_PER_MINUTE)}; - if (static_cast<std::int64_t>(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; - if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; + if (static_cast<std::int64_t>(local.tick) > static_cast<std::int64_t>(MAX_TICK)) return SCE_RTC_ERROR_INVALID_YEAR; + if (static_cast<std::int64_t>(utc->tick) < 0 || static_cast<std::int64_t>(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; const RtcDateTime time = fromTick(local.tick); const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 36c8411c72..20a921e854 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -295,6 +295,12 @@ int main() { Require(sceRtcFormatRFC3339(text, &edge, -1) == invalidYear); edge.tick = 60000000; Require(sceRtcFormatRFC3339(text, &edge, -1) == 0 && std::strcmp(text, "0001-01-01T00:00:00.00-00:01") == 0); + edge.tick = std::numeric_limits<std::uint64_t>::max(); + Require(sceRtcFormatRFC3339(text, &edge, 1) == 0 && std::strcmp(text, "0001-01-01T00:00:59.99+00:01") == 0); + edge.tick = 1ull << 63; + Require(sceRtcFormatRFC3339(text, &edge, -1) == invalidYear); + edge.tick = maxTick + 1; + Require(sceRtcFormatRFC3339(text, &edge, -1) == 0 && std::strcmp(text, "9999-12-31T23:59:00.00-00:01") == 0); Require(sceRtcFormatRFC3339(nullptr, &tick, 1440) == invalidPointer); Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidValue); { @@ -805,6 +811,14 @@ int main() { Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidYear); tick.tick = std::numeric_limits<std::uint64_t>::max(); Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidValue); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = 0ull - 60000000ull; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = 1ull << 63; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidValue); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == invalidYear); + tick.tick = maxTick + 1; + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == 0 && std::strcmp(rfc2822, "Fri, 31 Dec 9999 23:59:00 -0001") == 0); tick.tick = leapDayTick; for (int offset : {1440, -1440, std::numeric_limits<int>::min(), std::numeric_limits<int>::max()}) { std::memset(rfc2822, 'x', sizeof(rfc2822)); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 0139c6860b..affc0d5d23 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -7,7 +7,7 @@ ### Unknown function info - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. -- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_VALUE` when the zone-shifted tick is negative as a signed value, `INVALID_YEAR` when it is past 9999, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick returning `INVALID_POINTER` (shadPS4 formats the current time instead) and offsets outside ±1439 minutes returning `INVALID_VALUE` are not measured for these three functions +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 35d36eabbca27475ebc83a1c40c040ba2504f3e5 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:05:42 +0800 Subject: [PATCH 1017/1360] feat(libSceUlt): implement sceUltConditionVariableCreate and condition variable option initializers --- core/libs/prx/libSceUlt/Export.cpp | 32 +++++++++++++-------------- core/libs/tests/GuestUltCondition.cpp | 4 ++++ core/libs/tests/GuestUltSync.cpp | 5 ++++- docs/dev/debt/libSceUlt.md | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 63344b81f8..f876cfba21 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -236,6 +236,15 @@ int APS5_VABI sceUltUlthreadOptParamInitialize(UltOptParam* optParam, std::uint3 return InitializeOptParam(optParam); } +int APS5_VABI sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI _sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + return sceUltConditionVariableOptParamInitialize(optParam, buildVersion); +} + int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -369,7 +378,9 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { return ULT_OK; } -int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* mutex, const void* optParam, std::uint32_t) { +int APS5_VABI sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { + (void)name; + (void)buildVersion; if (condition == nullptr || mutex == nullptr) return ULT_ERROR_NULL; if (optParam != nullptr) throw std::runtime_error("ULT condition variable options are not implemented"); std::lock_guard lock(gMutex); @@ -385,6 +396,10 @@ int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* return ULT_OK; } +int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { + return sceUltConditionVariableCreate(condition, name, mutex, optParam, buildVersion); +} + int APS5_VABI sceUltConditionVariableDestroy(void* condition) { std::lock_guard lock(gMutex); const auto it = gConditions.find(condition); @@ -1081,20 +1096,5 @@ int APS5_VABI sceUltUlthreadFactoryInitialize() { return 0; } -int APS5_VABI sceUltConditionVariableCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltConditionVariableOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - - -int APS5_VABI _sceUltConditionVariableOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} } diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp index 5bec09da1b..3922c753ef 100644 --- a/core/libs/tests/GuestUltCondition.cpp +++ b/core/libs/tests/GuestUltCondition.cpp @@ -15,6 +15,7 @@ int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptPara int APS5_VABI sceUltMutexLock(void*); int APS5_VABI sceUltMutexUnlock(void*); int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); int APS5_VABI _sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); int APS5_VABI sceUltConditionVariableWait(void*); int APS5_VABI sceUltConditionVariableSignal(void*); @@ -65,6 +66,9 @@ static void Validation() { Object unused; CHECK(sceUltMutexDestroy(nullptr) == Null); CHECK(sceUltMutexDestroy(&unused) == State); + CHECK(sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); + CHECK(sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); CHECK(_sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); CHECK(_sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 2a368aa57b..7b2e1e51ed 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -25,6 +25,8 @@ int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltConditionVariableOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltConditionVariableOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); int APS5_VABI sceUltReaderWriterLockDestroy(void*); int APS5_VABI sceUltReaderWriterLockLockRead(void*); @@ -98,7 +100,8 @@ void MutexTryLock() { void OptParams() { using Initializer = int (APS5_VABI *)(void*, std::uint32_t); for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, - sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize}) { + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize, + sceUltConditionVariableOptParamInitialize, _sceUltConditionVariableOptParamInitialize}) { std::uint8_t bytes[136]; std::memset(bytes, 0xa5, sizeof(bytes)); CHECK(initialize(nullptr, 0) == Null); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 12002369d2..d539ce9772 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -6,6 +6,7 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. +- [`sceUltConditionVariableCreate` and `sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) are absent from the console export list, where only the underscored variants `_sceUltConditionVariableCreate` and `_sceUltConditionVariableOptParamInitialize` appear; their non-underscored signatures are assumed. Condition variable option parameter initialization only zeroes 128 bytes, and its layout without the common header remains unverified on hardware. - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here @@ -13,4 +14,3 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_STATE` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). From f04af479e67cdd8df2fe1f5d1831970322786ebb Mon Sep 17 00:00:00 2001 From: Retr0 <gurau.eduard@ichb.ro> Date: Tue, 20 Oct 2026 19:02:49 +0300 Subject: [PATCH 1018/1360] test(libkernel): require EINPROGRESS from a nonblocking connect in progress FreeBSD 11's kern_connectat fails a nonblocking connect that is still connecting with EINPROGRESS (36), a second one while it is still connecting with EALREADY, and soconnectat fails one on a connected stream socket with EISCONN. On Windows connect has reported WSAEWOULDBLOCK as EINPROGRESS since 79e162110, but the poll test still accepted EAGAIN (35) and the socket error numbers debt entry still listed the gap. Require 36 in the poll test and cut the stale sentence. --- core/libs/tests/GuestPoll.cpp | 2 +- docs/dev/debt/libkernel.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 131e75eb9d..9ff6cfc5c4 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -177,7 +177,7 @@ static void PollsUrgentData() { const int client = socket_nid_postfix(2, 1, 0); Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0); if (connect_nid_postfix(client, address.data(), address.size()) != 0) - Require(*__error_nid_postfix() == 35 || *__error_nid_postfix() == 36); + Require(*__error_nid_postfix() == 36); PollDescriptor connecting{client, Priority | Out, -1}; Require(poll_nid_postfix(&connecting, 1, 1000) == 1 && connecting.revents == Out); const int server = accept_nid_postfix(listener, nullptr, nullptr); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 72a0e537c4..ce5c416a99 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -202,7 +202,7 @@ - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console -- [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 +- [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 are sent, where `udp_output()` fails with 49 on the unbound path but FreeBSD can send to port 0 from a socket bound to a specific address and port. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed From 22f2ff3a009f1b3fa0ebce71fc82394b25ed0cad Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Thu, 22 Oct 2026 16:57:25 +0400 Subject: [PATCH 1019/1360] fix(libc): fail freopen of an empty name with ENOENT on Windows freopen skipped path resolution for an empty file name and handed it to the host, so the MS CRT answered EINVAL where FreeBSD's freopen, which opens the name and keeps that errno (lib/libc/stdio/freopen.c lines 153, 162 and 196), gives ENOENT, as fopen of an empty name already does. An empty name now fails like an unresolvable one: ENOENT, the guest stream is closed and a host standard stream is left as it was. --- core/libs/prx/libc/src/FileAndHeap.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 0b6e86284c..08e751acd9 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -361,14 +361,12 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!parsed) { errno = 22; return nullptr; } try { std::filesystem::path path; - if (*filename) { - if (const int error = ResolvePathError_nid_no_patch(filename, path)) { - if (stream->IsHostStandard()) stream->Detach(); - else stream->Close(); - if (stream->IsDynamic()) delete stream; - errno = error; - return nullptr; - } + if (const int error = *filename ? ResolvePathError_nid_no_patch(filename, path) : 2) { + if (stream->IsHostStandard()) stream->Detach(); + else stream->Close(); + if (stream->IsDynamic()) delete stream; + errno = error; + return nullptr; } if (stream->Reopen(path, parsed->native.c_str(), OpenNative)) { #ifdef _WIN32 From e50621261ab875d9efa60f1fd736de43bb9e2f97 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Thu, 22 Oct 2026 16:57:37 +0400 Subject: [PATCH 1020/1360] test(libc): cover freopen of an empty name on a host standard stream freopen of an empty name on stdout fails with ENOENT as fopen of an empty name does, the guest stream is closed afterwards and the host standard output is still writable and still reaches its destination. Before the previous commit the call failed with EINVAL. --- core/libs/prx/libc/CMakeLists.txt | 10 +++ core/libs/tests/GuestFreopenHostStandard.cpp | 77 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 core/libs/tests/GuestFreopenHostStandard.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d53c2d0461..c3e818e786 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -652,6 +652,16 @@ if(BUILD_TESTING) set_tests_properties(file_streams PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test_executable(guest_freopen_host_standard_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreopenHostStandard.cpp) + target_include_directories(guest_freopen_host_standard_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freopen_host_standard_tests PRIVATE libc) + configure_windows_unwind(guest_freopen_host_standard_tests) + add_test(NAME guest_freopen_host_standard COMMAND guest_freopen_host_standard_tests) + set_tests_properties(guest_freopen_host_standard PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freopen_host_standard PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test_executable(guest_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestErrors.cpp) target_include_directories(guest_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_error_tests PRIVATE libc) diff --git a/core/libs/tests/GuestFreopenHostStandard.cpp b/core/libs/tests/GuestFreopenHostStandard.cpp new file mode 100644 index 0000000000..24669afdc8 --- /dev/null +++ b/core/libs/tests/GuestFreopenHostStandard.cpp @@ -0,0 +1,77 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cerrno> +#include <chrono> +#include <cstdio> +#include <cstdlib> +#include <filesystem> +#include <fstream> +#include <iterator> +#include <string> +#include <fcntl.h> +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <io.h> +#else +#include <unistd.h> +#endif + +extern "C" { +FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream); +int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); +extern FileStream* __stdoutp_nid_postfix; +} + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "freopen host standard check failed at line %d, errno %d\n", line, errno); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +#ifdef _WIN32 +static int HostDup(int descriptor) { return ::_dup(descriptor); } +static int HostDup2(int from, int to) { return ::_dup2(from, to); } +static int HostClose(int descriptor) { return ::_close(descriptor); } +static int HostWrite(int descriptor, const char* text, unsigned size) { return ::_write(descriptor, text, size); } +static int HostFileno(std::FILE* stream) { return ::_fileno(stream); } +#else +static int HostDup(int descriptor) { return ::dup(descriptor); } +static int HostDup2(int from, int to) { return ::dup2(from, to) == to ? 0 : -1; } +static int HostClose(int descriptor) { return ::close(descriptor); } +static int HostWrite(int descriptor, const char* text, unsigned size) { return static_cast<int>(::write(descriptor, text, size)); } +static int HostFileno(std::FILE* stream) { return ::fileno(stream); } +#endif + +static std::string Contents(const std::string& name) { + std::ifstream stream(name, std::ios::binary); + return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()); +} + +int main() { + const auto sinkName = "anyps5-freopen-host-standard-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"; + std::FILE* sink = std::fopen(sinkName.c_str(), "wb"); + Check(sink != nullptr); + const int original = HostDup(1); + Check(original >= 0 && HostDup2(HostFileno(sink), 1) == 0); + + errno = 0; + FileStream* const reopened = freopen_nid_postfix("", "w", __stdoutp_nid_postfix); + const int error = errno; + + const int written = HostWrite(1, "host", 4); + const bool restored = HostDup2(original, 1) == 0 && HostClose(original) == 0; + Check(std::fclose(sink) == 0); + Check(restored); + Check(reopened == nullptr); + Check(error == ENOENT); + Check(written == 4); + Check(Contents(sinkName) == "host"); + + errno = 0; + Check(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && errno == EBADF); + Check(std::filesystem::remove(sinkName)); + return 0; +} From 235ea5fd2260c6327eab43a82f08fd70c2236925 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:37:19 +0100 Subject: [PATCH 1021/1360] fix(libkernel): remove duplicate timed join discovery export --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 702eff446b..83217edc19 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -518,11 +518,6 @@ int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_timedjoin_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ptrace_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; From 37ba44ce4d6e20e88fcc3be77c84c778eccb98f3 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:54:57 -0700 Subject: [PATCH 1022/1360] test(libkernel_sys): link through a symbolic link loop on Windows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestKernelSys.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 9a00cffdc8..ff8b4bf277 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -705,6 +705,11 @@ int main() { const auto dotLink = (root / "dot-link").string(); Require(symlink_nid_postfix(".", dotLink.c_str()) == 0); Require(link_nid_postfix(dotLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); +#ifdef _WIN32 + Require(symlink_nid_postfix("loop-b", (root / "loop-a").string().c_str()) == 0); + Require(symlink_nid_postfix("loop-a", (root / "loop-b").string().c_str()) == 0); + Require(link_nid_postfix((root / "loop-a").string().c_str(), throughLink.c_str()) == -1 && Error() == 62); +#endif Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } RemoveTree(root); From e9e345fbf358d83dd477b8349d0178f64e9ceb54 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:54:59 -0700 Subject: [PATCH 1023/1360] docs(libkernel_sys): record that link follows longer link chains on Windows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel_sys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..e45282d96c 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -12,6 +12,7 @@ - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified From 32a95a35dde0e8449f799b0c650c540bf1974fed Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:19:34 -0700 Subject: [PATCH 1024/1360] fix(hmd): answer handle 0 before sceHmdInitialize only below SDK 4.50 BunnyFl's firmware 13.42 check shows that the distortion, wide-near and 2D VR command queries give NOT_INITIALIZED for handle 0 before sceHmdInitialize only when the title's compiled SDK version is below 4.50. From 4.50 the library first checks state it sets up when it starts, which isn't measured, so handle 0 before initialization now throws there, as it does after initialization and as all six queries do on main. The SDK version is read from the process parameters, as libSceHmd2 does. guest_hmd checks handle 0 before initialization at SDK 0x044fffff and 0x04500000. Source: https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6343495321 (BunnyFl) --- core/libs/prx/libSceHmd/CMakeLists.txt | 1 + core/libs/prx/libSceHmd/Export.cpp | 13 ++++++-- core/libs/prx/libSceHmd/Hmd.hpp | 1 + core/libs/tests/GuestHmd.cpp | 42 ++++++++++++++++++++------ docs/dev/debt/libSceHmd.md | 2 +- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceHmd/CMakeLists.txt b/core/libs/prx/libSceHmd/CMakeLists.txt index 8db3fc4a98..5bf180e478 100644 --- a/core/libs/prx/libSceHmd/CMakeLists.txt +++ b/core/libs/prx/libSceHmd/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceHmd SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHmd PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceHmd PRIVATE libc) configure_windows_unwind(libSceHmd) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index e0e935f610..d32073ea63 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include <atomic> @@ -68,10 +69,18 @@ std::int32_t Initialize(const Hmd::InitializeParam* param) { return initialized.exchange(true) ? Hmd::AlreadyInitialized : 0; } +std::uint32_t CompiledSdkVersion() { + static const auto* processParameters = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, processParameters + 0x10, sizeof(version)); + return version; +} + std::int32_t CommandQueryHandle(std::int32_t handle, const char* query) { if (handle != 0) return Hmd::InvalidHandle; - if (!initialized.load()) return Hmd::NotInitialized; - NotImplemented_nid_no_patch((std::string(query) + " with handle 0 after sceHmdInitialize").c_str()); + if (initialized.load()) NotImplemented_nid_no_patch((std::string(query) + " with handle 0 after sceHmdInitialize").c_str()); + if (CompiledSdkVersion() >= Hmd::StartupStateSdkVersion) NotImplemented_nid_no_patch((std::string(query) + " with handle 0 before sceHmdInitialize from SDK 4.50").c_str()); + return Hmd::NotInitialized; } template <typename T> diff --git a/core/libs/prx/libSceHmd/Hmd.hpp b/core/libs/prx/libSceHmd/Hmd.hpp index 439c94a5af..22f812c774 100644 --- a/core/libs/prx/libSceHmd/Hmd.hpp +++ b/core/libs/prx/libSceHmd/Hmd.hpp @@ -15,6 +15,7 @@ inline constexpr std::int32_t DeviceDisconnected = static_cast<std::int32_t>(0x8 inline constexpr std::int32_t ParameterNull = static_cast<std::int32_t>(0x81110008); inline constexpr std::int32_t ParameterInvalid = static_cast<std::int32_t>(0x81110009); inline constexpr std::int32_t UnsupportedFeature = static_cast<std::int32_t>(0x81110016); +inline constexpr std::uint32_t StartupStateSdkVersion = 0x04500000; enum class DeviceStatus : std::uint32_t { Ready = 0, diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index 0bccb53a85..257e9f381c 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -1,6 +1,9 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libSceHmd/Hmd.hpp" +#include <array> #include <cstddef> +#include <cstdint> #include <cstdio> #include <cstdlib> #include <cstring> @@ -9,6 +12,11 @@ namespace { +constexpr std::uint32_t LastSdkBeforeStartupState = 0x044fffff; +constexpr std::uint32_t FirstSdkWithStartupState = 0x04500000; + +alignas(16) std::array<std::byte, 0x40> process{}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "HMD: %s\n", message); @@ -16,6 +24,18 @@ void Require(bool condition, const char* message) { } } +void InstallProcessParameters() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +void SetCompiledSdkVersion(std::uint32_t version) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); +} + void CheckDisconnected() { Hmd::DeviceStatus status = Hmd::DeviceStatus::Ready; Require(sceHmdInternalGetDeviceStatus(&status) == 0, "status query failed"); @@ -69,7 +89,7 @@ bool Unimplemented(Call call) { return false; } -void CheckCommandQueries(bool afterInitialize) { +void CheckCommandQueries(bool handleZeroMeasured) { unsigned char zeroed[64] = {}; unsigned char vr2dBytes[64] = {}; unsigned char commands[16]; @@ -112,13 +132,13 @@ void CheckCommandQueries(bool afterInitialize) { }); } } - if (afterInitialize) { - Require(Unimplemented([&] { return sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer); }), "distortion command answered handle 0 after initialize"); - Require(Unimplemented([&] { return sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer); }), "aliased distortion command answered handle 0 after initialize"); - Require(Unimplemented([&] { return sceHmdGetWideNearDistortionCorrectionCommand(0, wideNear, commandBuffer); }), "wide-near distortion command answered handle 0 after initialize"); - Require(Unimplemented([&] { return sceHmdDistortionGetWideNearCorrectionCommand(0, wideNear, commandBuffer); }), "aliased wide-near distortion command answered handle 0 after initialize"); - Require(Unimplemented([&] { return sceHmdGet2dVrCommand(0, vr2d, commandBuffer); }), "2D VR command answered handle 0 after initialize"); - Require(Unimplemented([&] { return sceHmdDistortionGet2dVrCommand(0, vr2d, commandBuffer); }), "aliased 2D VR command answered handle 0 after initialize"); + if (!handleZeroMeasured) { + Require(Unimplemented([&] { return sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer); }), "distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer); }), "aliased distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdGetWideNearDistortionCorrectionCommand(0, wideNear, commandBuffer); }), "wide-near distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdDistortionGetWideNearCorrectionCommand(0, wideNear, commandBuffer); }), "aliased wide-near distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdGet2dVrCommand(0, vr2d, commandBuffer); }), "2D VR command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdDistortionGet2dVrCommand(0, vr2d, commandBuffer); }), "aliased 2D VR command answered handle 0 where its answer is not measured"); } else { Require(sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer) == Hmd::NotInitialized, "distortion command before initialize"); Require(sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer) == Hmd::NotInitialized, "aliased distortion command before initialize"); @@ -146,6 +166,7 @@ int main() { std::memset(&info, 0xa5, sizeof(info)); const auto untouched = info; + InstallProcessParameters(); CheckDisconnected(); CheckBufferQueries(); Require(sceHmdTerminate() == Hmd::NotInitialized, "terminate before initialize"); @@ -155,6 +176,9 @@ int main() { Require(sceHmdGetDeviceInformation(&info) == Hmd::NotInitialized, "query before initialize"); Require(std::memcmp(&info, &untouched, sizeof(info)) == 0, "failed query modified output"); CheckHandleQueries(Hmd::NotInitialized); + SetCompiledSdkVersion(LastSdkBeforeStartupState); + CheckCommandQueries(true); + SetCompiledSdkVersion(FirstSdkWithStartupState); CheckCommandQueries(false); CheckAssyError(Hmd::NotInitialized); Require(sceHmdGetDeviceInformationByHandle(0, &info) == Hmd::NotInitialized, @@ -194,7 +218,7 @@ int main() { Require(std::memcmp(&info, &expected, sizeof(info)) == 0, "invalid handle modified output"); } CheckHandleQueries(Hmd::InvalidHandle); - CheckCommandQueries(true); + CheckCommandQueries(false); CheckAssyError(Hmd::DeviceDisconnected); Require(sceHmdGetDeviceInformationByHandle(0, nullptr) == Hmd::InvalidHandle, "null query accepted invalid handle"); diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 59c79396ff..cf169b2329 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -21,6 +21,6 @@ [Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. -[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Without a headset `sceHmdOpen` never issues a handle, so the stored handle stays 0. As firmware 13.42's libSceHmd does when its exports are run under an emulator ([#9577](https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6336871370)), the distortion queries return `HANDLE_INVALID` for any other handle before checking initialization, and `NOT_INITIALIZED` for handle 0 before it. The 2D VR query first returns `PARAMETER_NULL` for a null descriptor or output or a null pointer at descriptor offset 0, 8 or 0x20, and `PARAMETER_INVALID` for a non-zero field at 0x34 (32-bit) or 0x38 (64-bit). None writes the output. What handle 0 gets after initialization is not measured, so it throws. +[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Without a headset `sceHmdOpen` never issues a handle, so the stored handle stays 0. As firmware 13.42's libSceHmd does when its exports are run under an emulator ([#9577](https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6336871370)), the distortion queries return `HANDLE_INVALID` for any other handle before checking initialization, and `NOT_INITIALIZED` for handle 0 before it when the title's compiled SDK version is below 4.50. The 2D VR query first returns `PARAMETER_NULL` for a null descriptor or output or a null pointer at descriptor offset 0, 8 or 0x20, and `PARAMETER_INVALID` for a non-zero field at 0x34 (32-bit) or 0x38 (64-bit). None writes the output. From SDK 4.50 the firmware first checks state the library sets up when it starts, which is not measured ([#9577](https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6343495321)), and what handle 0 gets after initialization is not measured either, so both throw. The ELF export table aliases the [Distortion correction/2D command and work-memory queries](../../../core/libs/prx/libSceHmd/Export.cpp) to their corresponding HMD queries, confirming identical signatures. Distortion initialization consumes only a work-memory pointer. No shader code or calibration constants are copied; each aliased command query answers as its twin, and distortion initialization keeps its protected body. From 78d13f849b137e26e7e3b9a1d472971721c7e1d2 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:57:15 +0300 Subject: [PATCH 1025/1360] fix(tools): wrap entrypoint in main with stream reconfigure in pr_labels Wrap the pr_labels entrypoint in main(argv=None) with UTF-8 stream reconfiguration using backslashreplace error handling. Catch CalledProcessError when querying repository or pull request details and report cleanly via parser.error rather than propagating an unhandled traceback. --- tools/pr_labels.py | 24 ++++++++++++------- tools/tests/test_pr_labels.py | 44 +++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/tools/pr_labels.py b/tools/pr_labels.py index 4ca5c70a9b..f6f0039090 100644 --- a/tools/pr_labels.py +++ b/tools/pr_labels.py @@ -121,18 +121,25 @@ def process(repo, pr, dry_run, comment=True): return add, remove, post -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Label pull requests from their title and template fields.") which = parser.add_mutually_exclusive_group(required=True) which.add_argument("--pr", type=int, help="label one pull request") which.add_argument("--all", action="store_true", help="label every open pull request") parser.add_argument("--dry-run", action="store_true", help="print the planned changes and write nothing") - args = parser.parse_args() - repo = repository() - if args.pr is not None: - prs = [pull(json.loads(gh("api", f"repos/{repo}/pulls/{args.pr}")))] - else: - prs = [pull(data) for data in decode_pages(gh("api", "--paginate", f"repos/{repo}/pulls?state=open&base=main&per_page=100"))] + args = parser.parse_args(argv) + try: + repo = repository() + if args.pr is not None: + prs = [pull(json.loads(gh("api", f"repos/{repo}/pulls/{args.pr}")))] + else: + prs = [pull(data) for data in decode_pages(gh("api", "--paginate", f"repos/{repo}/pulls?state=open&base=main&per_page=100"))] + except subprocess.CalledProcessError as error: + parser.error((error.stderr or str(error)).strip()) failed = [] for pr in prs: try: @@ -148,7 +155,8 @@ def main(): time.sleep(WRITE_PAUSE) if failed: sys.exit(f"{len(failed)} pull requests not labeled: {', '.join(f'#{number}' for number in failed)}") + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py index eec489a51a..76298e7657 100644 --- a/tools/tests/test_pr_labels.py +++ b/tools/tests/test_pr_labels.py @@ -1,4 +1,6 @@ +import contextlib import copy +import io import json import sys import unittest @@ -174,12 +176,50 @@ class MainTests(unittest.TestCase): return "" with patch.object(pr_labels, "gh", side_effect=fake), patch.object(pr_labels, "WRITE_PAUSE", 0), \ - patch.object(sys, "argv", ["pr_labels.py", "--all"]), patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}): + patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}): with self.assertRaises(SystemExit) as raised: - pr_labels.main() + pr_labels.main(["--all"]) self.assertEqual(edited, ["1", "2"]) self.assertIn("#1", str(raised.exception.code)) + def test_main_handles_called_process_error_when_fetching_pr(self): + def fake(*args): + raise pr_labels.subprocess.CalledProcessError(1, "gh", stderr="PR not found") + + with patch.object(pr_labels, "gh", side_effect=fake), \ + patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}), \ + contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as raised: + pr_labels.main(["--pr", "999"]) + self.assertEqual(raised.exception.code, 2) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(pr_labels.sys, "stdout", out), patch.object(pr_labels.sys, "stderr", err): + try: + pr_labels.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From 810d2a29763a8d2511f3e8b6e01e80d1b21bcf7b Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 21:38:02 +0300 Subject: [PATCH 1026/1360] fix(ci): decode CMakeCache with utf-8-sig and wrap entrypoint in check_test_registration --- tools/check_test_registration.py | 24 ++++++++++++++++-------- tools/tests/test_test_registration.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index fcce86cbd1..bbc80508d8 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -16,7 +16,7 @@ SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") def built(build): program, generator = None, None - for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_MAKE_PROGRAM:"): program = line.partition("=")[2] elif line.startswith("CMAKE_GENERATOR:"): @@ -157,21 +157,24 @@ def unrun_scripts(root): return scripts, [name for name in scripts if not any(name == path or name.endswith("/" + path) for path in run)] -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") parser.add_argument("--scripts", action="store_true", help="instead check that an add_test or a workflow runs every test_*.py of the source tree") - args = parser.parse_args() + args = parser.parse_args(argv) if args.scripts: scripts, unrun = unrun_scripts(Path(__file__).resolve().parents[1]) for name in unrun: print(f"error: {name} is a test script that no add_test or workflow runs") if unrun: print(f"{len(unrun)} test script(s) would never run: register each with add_test, or run it from a workflow") - raise SystemExit(1) + return 1 print(f"{len(scripts)} test script(s), each run by an add_test or a workflow") - raise SystemExit + return 0 try: every = sorted(built(args.build)) run = registered(args.build) @@ -180,11 +183,11 @@ if __name__ == "__main__": if args.list: for name in every: print(f"{'run' if name in run else 'NOT RUN'} {name}") - raise SystemExit + return 0 unrun = sorted(set(every) - run) if not every: print(f"no test executables found under {args.build}; is it configured with -DBUILD_TESTING=ON?") - raise SystemExit(1) + return 1 for name in sorted(set(unrun) & KNOWN_UNRUN): print(f"note: {name} is built and not run, as recorded") stale = sorted(KNOWN_UNRUN & run) @@ -196,5 +199,10 @@ if __name__ == "__main__": if fresh: print(f"{len(fresh)} test executable(s) would never run: ctest reports a full pass over what it knows, so a test nobody registered is invisible in CI") if stale or fresh: - raise SystemExit(1) + return 1 print(f"{len(every)} test executable(s), {len(unrun)} built and not run as recorded, none new") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 106d3bc3ae..de64bbec73 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import os import shutil import subprocess @@ -343,5 +345,24 @@ class ScriptRegistrationTests(unittest.TestCase): "tools/tests/test_f.py"]) +class CheckRegistrationCliTests(unittest.TestCase): + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + cache = build / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_MAKE_PROGRAM:FILEPATH=ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + output = "tests/dummy_tests: EXECUTABLE\n" + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)): + names = check_test_registration.built(build) + self.assertEqual(names, {"dummy_tests"}) + + def test_main_scripts_with_explicit_argv(self): + with patch.object(check_test_registration, "unrun_scripts", return_value=(["test_a.py"], [])): + with contextlib.redirect_stdout(io.StringIO()): + code = check_test_registration.main(["--scripts"]) + self.assertEqual(code, 0) + + if __name__ == "__main__": unittest.main() From 4561711b753a913f44d9114398e24a0c79fb4ada Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Mon, 19 Oct 2026 19:45:31 +0300 Subject: [PATCH 1027/1360] fix(libc): check overlap in strncpy_s and strncat_s --- core/libs/prx/libc/src/Strings.cpp | 14 ++++++++++++++ core/libs/tests/GuestStrings.cpp | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 55ee19e5b1..7d4cd1e704 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -467,6 +467,13 @@ int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } + const auto destination = reinterpret_cast<std::uintptr_t>(dest); + const auto source = reinterpret_cast<std::uintptr_t>(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1)) { + dest[0] = '\0'; + return GuestEinval; + } std::memcpy(dest, src, length); dest[length] = '\0'; return 0; @@ -494,6 +501,13 @@ int APS5_VABI strncat_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } + const auto destination = reinterpret_cast<std::uintptr_t>(dest + used); + const auto source = reinterpret_cast<std::uintptr_t>(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1)) { + dest[0] = '\0'; + return GuestEinval; + } std::memcpy(dest + used, src, length); dest[used + length] = '\0'; return 0; diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index ef2f2f0a69..c970b45b6f 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -131,6 +131,10 @@ static void CheckBoundsCheckedFunctions() { Require(memcpy_s_nid_postfix(bytes, 2, source, 4) == 34 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 3); char overlap[6] = "abcde"; Require(memmove_s_nid_postfix(overlap + 1, 5, overlap, 3) == 0 && std::strcmp(overlap, "aabce") == 0); + char strOverlap[16] = "abcdef"; + Require(strncpy_s_nid_postfix(strOverlap + 1, 10, strOverlap, 3) == 22 && strOverlap[1] == '\0'); + char catOverlap[16] = "abcdef"; + Require(strncat_s_nid_postfix(catOverlap + 3, 10, catOverlap, 3) == 22 && catOverlap[3] == '\0'); Require(memset_s_nid_postfix(bytes, sizeof(bytes), 9, 8) == 22 && bytes[0] == 9 && bytes[3] == 9); char sentinel[4] = {1, 2, 3, 4}; Require(memset_s_nid_postfix(sentinel, sizeof(sentinel), 7, 2) == 0 && sentinel[1] == 7 && sentinel[2] == 3); From a9989872787b6b91aff66a50a513b36f8f5b7f70 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Wed, 21 Oct 2026 19:40:45 +0300 Subject: [PATCH 1028/1360] fix(libc): compare destination object range with source in strncpy_s and strncat_s --- core/libs/prx/libc/src/Strings.cpp | 8 +++----- core/libs/tests/GuestStrings.cpp | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 7d4cd1e704..b3d173267a 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -469,8 +469,7 @@ int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, } const auto destination = reinterpret_cast<std::uintptr_t>(dest); const auto source = reinterpret_cast<std::uintptr_t>(src); - const auto distance = destination < source ? source - destination : destination - source; - if (length != 0 && distance < (length + 1)) { + if (destination < source + (length + 1) && source < destination + length + 1) { dest[0] = '\0'; return GuestEinval; } @@ -501,10 +500,9 @@ int APS5_VABI strncat_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } - const auto destination = reinterpret_cast<std::uintptr_t>(dest + used); + const auto destination = reinterpret_cast<std::uintptr_t>(dest); const auto source = reinterpret_cast<std::uintptr_t>(src); - const auto distance = destination < source ? source - destination : destination - source; - if (length != 0 && distance < (length + 1)) { + if (destination < source + (length + 1) && source < destination + used + length + 1) { dest[0] = '\0'; return GuestEinval; } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index c970b45b6f..a30f57eb0a 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -135,6 +135,9 @@ static void CheckBoundsCheckedFunctions() { Require(strncpy_s_nid_postfix(strOverlap + 1, 10, strOverlap, 3) == 22 && strOverlap[1] == '\0'); char catOverlap[16] = "abcdef"; Require(strncat_s_nid_postfix(catOverlap + 3, 10, catOverlap, 3) == 22 && catOverlap[3] == '\0'); + char nonOverlap[16] = {}; + std::strcpy(nonOverlap + 8, "cd"); + Require(strcpy_s_nid_postfix(nonOverlap, SIZE_MAX >> 1, nonOverlap + 8) == 0 && std::strcmp(nonOverlap, "cd") == 0); Require(memset_s_nid_postfix(bytes, sizeof(bytes), 9, 8) == 22 && bytes[0] == 9 && bytes[3] == 9); char sentinel[4] = {1, 2, 3, 4}; Require(memset_s_nid_postfix(sentinel, sizeof(sentinel), 7, 2) == 0 && sentinel[1] == 7 && sentinel[2] == 3); From 2824bf21eb39854ff49931d9dac4143a984d3bd5 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Thu, 22 Oct 2026 18:33:56 +0300 Subject: [PATCH 1029/1360] fix(libc): check size limits, overlap, and invoke handler in wcscpy_s --- core/libs/prx/libc/include/SonyCrt.hpp | 40 +++++++++++++++++++------- core/libs/tests/GuestWideStrings.cpp | 16 +++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index cf1879e2c0..db53cd3d54 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -138,20 +138,38 @@ inline void* SonySetConstraintHandlerS(void* handler) { } inline int SonyWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { - if (!dest || !src || size == 0) { - SonyInvokeConstraintHandler("wcscpy_s", dest, EINVAL); - return EINVAL; + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr std::size_t rsizeMax = (SIZE_MAX >> 1) / sizeof(std::uint16_t); + if (!dest || size == 0) { + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; } - std::size_t i = 0; - while (i + 1 < size && src[i] != 0) { - dest[i] = src[i]; - ++i; + if (size > rsizeMax) { + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestErange); + return GuestErange; } - dest[i] = 0; - if (src[i] != 0) { - SonyInvokeConstraintHandler("wcscpy_s", dest, ERANGE); - return ERANGE; + if (!src) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; } + std::size_t length = 0; + while (length < size && src[length] != 0) ++length; + if (length >= size) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestErange); + return GuestErange; + } + const auto destination = reinterpret_cast<std::uintptr_t>(dest); + const auto source = reinterpret_cast<std::uintptr_t>(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1) * sizeof(std::uint16_t)) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; + } + std::memcpy(dest, src, (length + 1) * sizeof(std::uint16_t)); return 0; } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 30f48c4efb..c55aac8ebb 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -32,6 +32,9 @@ char16_t* APS5_VABI wcsncpy_nid_postfix(char16_t* dest, const char16_t* src, std int APS5_VABI wmemcpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); int APS5_VABI wmemmove_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); int APS5_VABI wcsncpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); +int APS5_VABI wcscpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src); +void* APS5_VABI set_constraint_handler_s_nid_postfix(void* handler); +void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error); char16_t* APS5_VABI wcsncat_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); int APS5_VABI wcsncat_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); int APS5_VABI wcscat_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src); @@ -205,6 +208,19 @@ int main() { wcsncpy_s_nid_postfix(strBuf, 16, u"abcdef", 6); require(wcsncpy_s_nid_postfix(strBuf + 1, 10, strBuf, 3) == 22 && strBuf[1] == 0); + char16_t cpyBuf[16]; + require(wcscpy_s_nid_postfix(cpyBuf, 16, u"wide") == 0 && same(cpyBuf, u"wide", 5)); + void* previousHandler = set_constraint_handler_s_nid_postfix(reinterpret_cast<void*>(&ignore_handler_s_nid_postfix)); + require(wcscpy_s_nid_postfix(nullptr, 16, u"wide") == 22); + require(wcscpy_s_nid_postfix(cpyBuf, 0, u"wide") == 22); + require(wcscpy_s_nid_postfix(cpyBuf, 16, nullptr) == 22 && cpyBuf[0] == 0); + require(wcscpy_s_nid_postfix(cpyBuf, 4, u"wide") == 34 && cpyBuf[0] == 0); + cpyBuf[0] = 0x1234; + require(wcscpy_s_nid_postfix(cpyBuf, ((SIZE_MAX >> 1) / sizeof(char16_t)) + 1, u"a") == 34 && cpyBuf[0] == 0x1234); + wcscpy_s_nid_postfix(cpyBuf, 16, u"abcdef"); + require(wcscpy_s_nid_postfix(cpyBuf + 1, 10, cpyBuf) == 22 && cpyBuf[1] == 0); + set_constraint_handler_s_nid_postfix(previousHandler); + char16_t ncatBuf[16]; wmemset_nid_postfix(ncatBuf, 0xaaaa, 16); ncatBuf[0] = 0; From b4c6da5c07d9c19d39636743f5b64d2f1de79c7d Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Mon, 19 Oct 2026 21:10:01 +0300 Subject: [PATCH 1030/1360] fix(tools): write progress history files with utf-8 encoding --- tools/progress_history.py | 16 ++++++++++------ tools/tests/test_progress_history.py | 20 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/tools/progress_history.py b/tools/progress_history.py index ca8460652a..b223ab7c58 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -26,7 +26,7 @@ def git(*args): def daily_commits(ref): seen = {} - for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode("utf-8").splitlines(): sha, timestamp = line.split() seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) @@ -221,12 +221,12 @@ def page(points): ]) + "\n" -if __name__ == "__main__": +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") parser.add_argument("--cache", type=Path, help="reuse per-commit counts while the scanner, ISA list and Python version match") - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.cache: outputs = [args.output / name for name in ("history.json", "history.svg", "history.html")] @@ -239,7 +239,11 @@ if __name__ == "__main__": if not points: parser.error("no commit with a measurable tree") args.output.mkdir(parents=True, exist_ok=True) - (args.output / "history.json").write_text(json.dumps(points, indent=2)) - (args.output / "history.svg").write_text(chart(points) + "\n") - (args.output / "history.html").write_text(page(points)) + (args.output / "history.json").write_text(json.dumps(points, indent=2), encoding="utf-8") + (args.output / "history.svg").write_text(chart(points) + "\n", encoding="utf-8") + (args.output / "history.html").write_text(page(points), encoding="utf-8") print(f"{len(points)} days, {points[0]['date']} .. {points[-1]['date']}") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index 813b596990..38b9f5de86 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -1,3 +1,4 @@ +import json import os import subprocess import sys @@ -17,9 +18,9 @@ OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcod def commit(root, day, message, body, time="12:00:00Z"): library = root / "core/libs/prx/libSceExample" library.mkdir(parents=True, exist_ok=True) - (library / "Export.cpp").write_text(body) + (library / "Export.cpp").write_text(body, encoding="utf-8") (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) - (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") + (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n", encoding="utf-8") env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T{time}", "GIT_COMMITTER_DATE": f"{day}T{time}"} subprocess.run(["git", "-C", str(root), "add", "-A"], check=True) subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", message], @@ -140,6 +141,21 @@ class ProgressHistoryTests(unittest.TestCase): for label in ("Aug 3", "Aug 10", "Aug 17", "Aug 24", "Aug 31"): self.assertIn(f">{label}<", svg) + def test_main_writes_utf8_output_files(self): + point = {"date": "2026-01-01", "commit": "abc1234", + "libraries": {"done": 1, "total": 2, "percent": 50.0}, + "shaders": {"done": 3, "total": 4, "percent": 75.0}} + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + with patch.object(progress_history, "collect", return_value=[point]): + progress_history.main([str(output)]) + svg = (output / "history.svg").read_text(encoding="utf-8") + self.assertIn("\u25cf", svg) + html = (output / "history.html").read_text(encoding="utf-8") + self.assertIn("<!DOCTYPE html>", html) + data = json.loads((output / "history.json").read_text(encoding="utf-8")) + self.assertEqual(data, [point]) + if __name__ == "__main__": From ca6c91ff9832afca546b93e98158e00f3d5bfda8 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Thu, 22 Oct 2026 18:56:17 +0300 Subject: [PATCH 1031/1360] fix(tools): reconfigure streams with backslashreplace in stubgen --- tools/stubgen.py | 6 +++--- tools/tests/test_stubgen.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index a701cb5472..7426cd8aa2 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -211,6 +211,9 @@ def write_outputs(outputs): def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace", newline="\n") ap = argparse.ArgumentParser(description="Generate AnyPS5 Export.cpp skeletons and TechnicalDebt lines") ap.add_argument("--module", required=True, help="target module name, e.g. libSceAmpr, used for UnknownNN prefix and default TechnicalDebt path core/libs/prx/MODULE/Export.cpp") ap.add_argument("--nids", required=True, help="path to missing-NIDs file, one 11-char NID per line") @@ -221,9 +224,6 @@ def main(argv=None): ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, which go in docs/dev/debt/MODULE.md, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", newline="\n") try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index bc4bf37488..eedd1e3a09 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -6,6 +6,7 @@ import tempfile from pathlib import Path import sys import unittest +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_conventions @@ -200,5 +201,37 @@ class ReadOnlyOutputTests(unittest.TestCase): self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + self.newline = None + + def reconfigure(self, encoding=None, errors=None, newline=None): + self.encoding = encoding + self.errors = errors + self.newline = newline + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(stubgen.sys, "stdout", out), patch.object(stubgen.sys, "stderr", err): + try: + stubgen.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(out.newline, "\n") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + self.assertEqual(err.newline, "\n") + + if __name__ == "__main__": unittest.main() From e8d8b1c725c491d17b87077e500938498e78977d Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Thu, 22 Oct 2026 18:57:44 +0300 Subject: [PATCH 1032/1360] fix(tools): reconfigure streams with backslashreplace in performance_compare --- tools/performance_compare.py | 2 +- tools/tests/test_performance_compare.py | 29 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 4642a983ff..6a6c599351 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -166,7 +166,7 @@ def markdown(report): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Compare two aps5-summary.jsonl captures. " "Reports numerical differences, not statistical significance or an automatic pass/fail verdict.") parser.add_argument("baseline", type=Path, help="Baseline summary JSONL file") diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 1cabac3fe2..4016f14439 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -5,6 +5,7 @@ import subprocess import sys import tempfile import unittest +from unittest.mock import patch from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -317,5 +318,33 @@ class PerformanceCompareTests(unittest.TestCase): + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(performance_compare.sys, "stdout", out), patch.object(performance_compare.sys, "stderr", err): + try: + performance_compare.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From eb102eac57b58165efbd47290e673b9446418eb7 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Thu, 22 Oct 2026 19:00:10 +0300 Subject: [PATCH 1033/1360] fix(tools): wrap entrypoint in main with stream reconfigure in pr_overlap --- tools/pr_overlap.py | 17 +++++++++++++--- tools/tests/test_pr_overlap_errors.py | 28 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 6699df9ebf..398ee2c7b5 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,6 +3,7 @@ import json import os import re import subprocess +import sys from concurrent.futures import ThreadPoolExecutor from itertools import combinations from pathlib import Path @@ -252,18 +253,23 @@ def describe_failure(error): return f"pr_overlap: {command} failed with exit code {error.returncode}: {message}" -if __name__ == "__main__": +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="report open pull requests that overlap each other") parser.add_argument("--base", default="main") parser.add_argument("--out", type=Path, help="write <number>.md for every open pull request (empty when clean)") parser.add_argument("--branch", help="check a local branch against the open pull requests instead") parser.add_argument("--repo", help="owner/name; defaults to the upstream of the current checkout") parser.add_argument("--jobs", type=int, default=4, help="number of pull request comparisons to run at once") - args = parser.parse_args() + args = parser.parse_args(argv) if bool(args.out) == bool(args.branch): parser.error("pass exactly one of --out or --branch") if args.jobs < 1: parser.error("--jobs must be at least 1") + global REPO try: REPO = args.repo or upstream() prs = open_prs(args.base) @@ -283,5 +289,10 @@ if __name__ == "__main__": raise SystemExit(describe_failure(error)) if args.branch: print(report(rows[0], blob) or "No overlap with open pull requests.", end="") - raise SystemExit + return 0 write_reports(args.out, rows, blob) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/test_pr_overlap_errors.py b/tools/tests/test_pr_overlap_errors.py index c5bb5e0b8e..f85c7c3267 100644 --- a/tools/tests/test_pr_overlap_errors.py +++ b/tools/tests/test_pr_overlap_errors.py @@ -37,5 +37,33 @@ class ErrorReportTests(unittest.TestCase): self.assertIn(MISSING_REF, str(raised.exception.code)) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(pr_overlap.sys, "stdout", out), patch.object(pr_overlap.sys, "stderr", err): + try: + pr_overlap.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From b939663075b23bb7f0e1df0eb307c3f465a12947 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:42:58 +0200 Subject: [PATCH 1034/1360] fix(libSceNgs2): return the SDK pan error codes from sceNgs2PanInit An unsupported speaker count returns 0x804A8451, a unit angle other than 360 or 2*pi with angles returns 0x804A8450 and a null work returns 0x804A8010, leaving the work untouched, instead of throwing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../libSceNgs2.native/include/Ngs2Types.hpp | 2 ++ core/libs/prx/libSceNgs2.native/src/Pan.cpp | 9 ++++-- .../libSceNgs2.native/tests/Ngs2Render.cpp | 29 +++++++++++++++++-- docs/dev/debt/libSceNgs2.native.md | 2 +- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index a604adba3d..1bf747b71c 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -15,6 +15,8 @@ static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast<int>(0x804 static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast<int>(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast<int>(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast<int>(0x804A8432); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE = static_cast<int>(0x804A8450); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_SPEAKER = static_cast<int>(0x804A8451); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast<int>(0x804A8203); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast<int>(0x804A8360); diff --git a/core/libs/prx/libSceNgs2.native/src/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp index af88b23204..e1a4d70548 100644 --- a/core/libs/prx/libSceNgs2.native/src/Pan.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -55,10 +55,13 @@ static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, f } int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { - if (work == nullptr) APS5_INVALID_ARG_EX; - if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) APS5_INVALID_ARG_EX; + if (work == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; const auto* layout = LayoutFor(num_speakers); - if (layout == nullptr) APS5_INVALID_ARG_EX; + if (layout == nullptr) { + if (speaker_angles == nullptr && num_speakers == 0) APS5_INVALID_ARG_EX; + return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + } *work = {}; work->num_speakers = num_speakers; if (speaker_angles == nullptr) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 4186d67946..d76c464a6c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -6,6 +6,7 @@ #include <cmath> #include <cstdint> #include <cstdlib> +#include <cstring> #include <iterator> #include <numbers> #include <stdexcept> @@ -262,10 +263,32 @@ static void TestPan() { work.speaker_angles[0] = -INFINITY; Require(PanThrows(&work, &front, 1, 2)); Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); - Require(PanInitThrows(nullptr, nullptr, 360.0f, 2)); - for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u}) Require(PanInitThrows(&work, nullptr, 360.0f, count)); + Ngs2PanWork marked; + std::memset(&marked, 0x5a, sizeof(marked)); + Ngs2PanWork expected; + std::memcpy(&expected, &marked, sizeof(marked)); + const auto unchanged = [&] { return std::memcmp(&marked, &expected, sizeof(marked)) == 0; }; const float pair[] = {-30.0f, 30.0f}; - Require(PanInitThrows(&work, pair, 180.0f, 2) && !PanInitThrows(&work, pair, 2.0f * pi, 2)); + Require(sceNgs2PanInit(nullptr, nullptr, 360.0f, 2) == static_cast<int>(0x804A8010)); + Require(sceNgs2PanInit(nullptr, pair, 180.0f, 3) == static_cast<int>(0x804A8010)); + Require(sceNgs2PanInit(nullptr, nullptr, 100.0f, 3) == static_cast<int>(0x804A8010)); + for (std::uint32_t count : {1u, 3u, 6u, 8u, 255u}) { + Require(sceNgs2PanInit(&marked, nullptr, 360.0f, count) == static_cast<int>(0x804A8451)); + Require(unchanged()); + } + Require(PanInitThrows(&marked, nullptr, 360.0f, 0)); + Require(unchanged()); + for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u, 255u}) { + Require(sceNgs2PanInit(&marked, pair, 360.0f, count) == static_cast<int>(0x804A8451)); + Require(sceNgs2PanInit(&marked, pair, 2.0f * pi, count) == static_cast<int>(0x804A8451)); + Require(unchanged()); + } + for (const float unitAngle : {180.0f, 0.0f, 359.0f, 361.0f, 90.0f, pi}) { + Require(sceNgs2PanInit(&marked, pair, unitAngle, 2) == static_cast<int>(0x804A8450)); + Require(sceNgs2PanInit(&marked, pair, unitAngle, 3) == static_cast<int>(0x804A8450)); + Require(unchanged()); + } + Require(sceNgs2PanInit(&work, pair, 2.0f * pi, 2) == SCE_NGS2_OK); const float invalid[] = {0.0f, NAN}; Require(PanInitThrows(&work, invalid, 360.0f, 2)); const float onUnit[] = {720.0f, 30.0f}; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..2656b7902a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -31,7 +31,7 @@ - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown -- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason +- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. A speaker count of 0 without angles, which the firmware accepts, a non-finite speaker angle and a speaker angle equal to the unit angle, on which that library loops forever, throw. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Unknown flags, zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. From 7fe0013eacdd2fd33be8ff82c5d360cff7f2ba63 Mon Sep 17 00:00:00 2001 From: Alvaro <alvaropobladoresteban9@gmail.com> Date: Thu, 22 Oct 2026 02:49:43 +0200 Subject: [PATCH 1035/1360] feat(libSceJxr): implement PKFree as jxrlib does --- core/libs/prx/libSceJxr/CMakeLists.txt | 15 +++++ core/libs/prx/libSceJxr/Export.cpp | 15 ++++- core/libs/tests/GuestJxr.cpp | 78 ++++++++++++++++++++++++++ docs/dev/debt/libSceJxr.md | 6 +- 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceJxr/CMakeLists.txt create mode 100644 core/libs/tests/GuestJxr.cpp diff --git a/core/libs/prx/libSceJxr/CMakeLists.txt b/core/libs/prx/libSceJxr/CMakeLists.txt new file mode 100644 index 0000000000..251402ba35 --- /dev/null +++ b/core/libs/prx/libSceJxr/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceJxr SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceJxr PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceJxr) + +if(BUILD_TESTING) + add_test_executable(guest_jxr_tests ${LIBS_INCLUDE_DIR}/tests/GuestJxr.cpp) + target_include_directories(guest_jxr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jxr_tests PRIVATE libSceJxr libc) + configure_windows_unwind(guest_jxr_tests) + add_test(NAME guest_jxr COMMAND guest_jxr_tests) + if(WIN32) + set_tests_properties(guest_jxr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceJxr>") + endif() +endif() diff --git a/core/libs/prx/libSceJxr/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp index 608aedc418..de2c55aa4e 100644 --- a/core/libs/prx/libSceJxr/Export.cpp +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -3,12 +3,21 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { +constexpr std::int64_t WmpErrSuccess = 0; +} + extern "C" { +void APS5_VABI free_nid_postfix(void* memory); + APS5_DUMMY_FUN -int APS5_VABI PKFree_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI PKFree_nid_postfix(void** memory) { + if (memory != nullptr) { + free_nid_postfix(*memory); + *memory = nullptr; + } + return WmpErrSuccess; } int APS5_VABI PixelFormatLookup_nid_postfix(void) { diff --git a/core/libs/tests/GuestJxr.cpp b/core/libs/tests/GuestJxr.cpp new file mode 100644 index 0000000000..1bcda397b1 --- /dev/null +++ b/core/libs/tests/GuestJxr.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <cstring> + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t size); +std::int64_t APS5_VABI PKFree_nid_postfix(void** memory); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void* lastFreed = nullptr; +int frees = 0; + +void* APS5_VABI Allocate(std::size_t size) { return std::malloc(size); } +void APS5_VABI Free(void* pointer) { + if (pointer != nullptr) { + lastFreed = pointer; + ++frees; + } + std::free(pointer); +} +void* APS5_VABI Reallocate(void* pointer, std::size_t size) { return std::realloc(pointer, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* pointer, std::size_t size, std::size_t) { return std::realloc(pointer, size); } +int APS5_VABI PosixAlign(void** pointer, std::size_t, std::size_t size) { + *pointer = std::malloc(size); + return *pointer ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array<std::byte, 0x40> process{}; + alignas(16) static std::array<std::byte, 0x38> libc{}; + alignas(16) static std::array<std::byte, 0x78> replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast<void*>(&Allocate)); + write(replacement, 0x28, reinterpret_cast<void*>(&Free)); + write(replacement, 0x30, reinterpret_cast<void*>(&Calloc)); + write(replacement, 0x38, reinterpret_cast<void*>(&Reallocate)); + write(replacement, 0x40, reinterpret_cast<void*>(&Align)); + write(replacement, 0x48, reinterpret_cast<void*>(&Realign)); + write(replacement, 0x50, reinterpret_cast<void*>(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + +} + +int main() { + InitializeHeap(); + void* memory = malloc_nid_postfix(64); + Require(memory != nullptr); + void* const allocated = memory; + Require(PKFree_nid_postfix(&memory) == 0); + Require(memory == nullptr); + Require(frees == 1 && lastFreed == allocated); + Require(PKFree_nid_postfix(&memory) == 0); + Require(memory == nullptr); + Require(frees == 1); + Require(PKFree_nid_postfix(nullptr) == 0); + Require(frees == 1); + return 0; +} diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md index 20faabeefb..0fbc29eb6c 100644 --- a/docs/dev/debt/libSceJxr.md +++ b/docs/dev/debt/libSceJxr.md @@ -2,4 +2,8 @@ ### Unknown function info -- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.c) and were cross-checked against the same module in PS5 firmware 13.60. `PixelFormatLookup`, `readIS` and `writeIS` have unverified signatures and behaviour; their declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [PKFree](../../../core/libs/prx/libSceJxr/Export.cpp) (libSceJxr) follows Microsoft's jxrlib [JXRGlue.c](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/jxrgluelib/JXRGlue.c#L53-L62), whose four exported names the module shares: a non-null `ppv` has `*ppv` released with the guest `free` and set to null, and the result is always `WMP_errSuccess` (0) as a 64-bit `ERR` (`long`). That the console's module is built from jxrlib, and that its `PKFree` releases through libc's `free`, is inferred from the export names and not checked against the console's code. Closes when: the console's `PKFree` is disassembled or measured on a PS5 From 0edbb0d2fbae72e1a615b9dc0a30d84670554c07 Mon Sep 17 00:00:00 2001 From: m335671 <110462702+m335671@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:28:13 +0200 Subject: [PATCH 1036/1360] refactor(agc): split a dispatch's device call into a complete and a record step VulkanDevice::dispatch now ends after the resources are complete and the pipeline is resolved, and hands a DispatchCompletion (references to the context, shader, push constants, pipeline objects, resources and the cache outcome) to recordCompletedDispatch, which records the dispatch and runs what follows it: the lease sync, the traces and the recipe. No behaviour changes; the code of both halves is unchanged. This is the dispatch-side counterpart of the Graphics::Draw prepare and record split discussed in #7895, so that later steps can complete a dispatch apart from recording it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Execution/include/VulkanDevice.hpp | 2 + .../Execution/src/VulkanDevice.cpp | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 52303ede78..252ec943c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -32,6 +32,7 @@ struct ComputePipelineObjects; struct RecipeHit; // The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch). struct RecordedDispatch; +struct DispatchCompletion; // The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline). struct ComputePipelineObjects; @@ -359,6 +360,7 @@ private: Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, const std::array<std::uint32_t, 3>& threadGroup = {}, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear = std::nullopt); + IndirectOutcome recordCompletedDispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, std::shared_ptr<const Recipe>* recipeOut, const std::array<std::uint32_t, 3>& threadGroup, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear, DispatchCompletion& completion); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span<const Graphics::PresyncSurface> surfaces); std::shared_ptr<ComputePipelineObjects> computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 55e54a39bb..a15542c847 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4423,6 +4423,24 @@ struct RecordedDispatch { std::array<std::uint32_t, 3> threadLimit{}; }; +struct DispatchCompletion { + const Graphics::Context& context; + const std::array<Graphics::CompiledShader, 1>& shaders; + VkShaderStageFlags pushStages; + const std::array<std::byte, Graphics::PipelinePushConstantBytes>& pushBytes; + const std::shared_ptr<ComputePipelineObjects>& objects; + const std::shared_ptr<Graphics::ShaderResources>& resources; + const ResourceCache::Key& contentKey; + bool cacheable; + bool fromCache; + double revalidateMs; + const std::shared_ptr<Graphics::ShaderResources::DrawBindings>& overlay; + std::uint64_t lookupsBefore; + DispatchTimer& timer; + PerformanceTimer& timing; + char* groupsText; +}; + VkDevice VulkanDevice::Device() const { return state->device; } @@ -5338,6 +5356,27 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec timing.Mark("pipeline_create"); timer.phase(PhasePipeline); } + DispatchCompletion completion{context, shaders, pushStages, pushBytes, objects, resources, contentKey, cacheable, fromCache, revalidateMs, overlay, lookupsBefore, timer, timing, groupsText}; + return recordCompletedDispatch(shader, x, y, z, arguments, programAddress, recipeOut, threadGroup, htileClear, completion); +} + +VulkanDevice::IndirectOutcome VulkanDevice::recordCompletedDispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, std::shared_ptr<const Recipe>* recipeOut, const std::array<std::uint32_t, 3>& threadGroup, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear, DispatchCompletion& completion) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto& context = completion.context; + const auto& shaders = completion.shaders; + const auto pushStages = completion.pushStages; + const auto& pushBytes = completion.pushBytes; + const auto& objects = completion.objects; + const auto& resources = completion.resources; + const auto& contentKey = completion.contentKey; + const bool cacheable = completion.cacheable; + const bool fromCache = completion.fromCache; + const double revalidateMs = completion.revalidateMs; + const auto& overlay = completion.overlay; + const auto lookupsBefore = completion.lookupsBefore; + auto& timer = completion.timer; + auto& timing = completion.timing; + auto* groupsText = completion.groupsText; // Recorded into the device's open batch: the CPU moves on to the next command while the GPU // works; the batch is waited for where the guest expects results (labels, flips, CPU reads). auto& recorder = *state->recorder; From 611db84ca5dc5a74477e0a7ce11fc7f4359e2ac1 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:45:33 +0200 Subject: [PATCH 1037/1360] fix(libSceAudioOut): validate the debug state id and size in sceAudioOut2SetSystemDebugState sceAudioOut2SetSystemDebugState accepts only the loudness debug state (id 1, 8-byte param); other ids and sizes return the invalid param error and a null pointer returns the invalid pointer error. The one debug state the console also accepts, id 0x20 with a 4-byte param, throws. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../libSceAudioOut/src/AudioOut2System.cpp | 11 +++++++- .../libSceAudioOut/tests/AudioOut2Latency.cpp | 25 +++++++++++++++---- docs/dev/debt/libSceAudioOut.md | 2 +- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index b9d9d152cf..1b5986e9c8 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -1,4 +1,5 @@ #include <atomic> +#include <cstddef> #include <cstdint> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -28,7 +29,15 @@ int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState* state) { } int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStateParam* param) { - if (param == nullptr) return static_cast<int>(0x80260502); + constexpr int ErrorInvalidPointer = static_cast<int>(0x8026800C); + constexpr std::uint32_t DebugStateIdLoudness = 1; + constexpr std::uint32_t DebugStateIdUnmodelled = 0x20; + constexpr std::size_t LoudnessParamSize = 8; + constexpr std::size_t UnmodelledParamSize = 4; + if (param == nullptr) return ErrorInvalidPointer; + if (param->debug_state_id == DebugStateIdUnmodelled && param->param_size == UnmodelledParamSize) NotImplemented_nid_no_patch(__func__); + if (param->debug_state_id != DebugStateIdLoudness || param->param_size != LoudnessParamSize) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; + if (param->param == nullptr) NotImplemented_nid_no_patch(__func__); AUDIOOUT2_TRACE("t=%.3f SetSystemDebugState: debug state %u recorded; the host has no system mix to apply it to\n", AudioOut2TraceSeconds(), param->debug_state_id); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 3ab1cd4a0f..78537a1c9f 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -48,7 +48,7 @@ static_assert(sizeof(AudioOut2MasteringStatesV2) == 400); constexpr int systemUser = 0xFF; constexpr int user = 0x10000000; constexpr int errorInvalidParam = static_cast<int>(0x80268001); -constexpr int errorInvalidArgument = static_cast<int>(0x80260502); +constexpr int errorInvalidPointer = static_cast<int>(0x8026800C); constexpr int errorMasteringInvalidApiParam = static_cast<int>(0x80268201); constexpr int errorMasteringInvalidStatesId = static_cast<int>(0x80268204); @@ -108,10 +108,25 @@ void TestMasteringGetState() { } void TestSetSystemDebugState() { - const std::uint64_t param[4] = {1, 0, 0, 0}; - Require(sceAudioOut2SetSystemDebugState(reinterpret_cast<const void*>(param)) == 0, "a debug state must be accepted"); - Require(sceAudioOut2SetSystemDebugState(reinterpret_cast<const void*>(param)) == 0, "a debug state must be accepted again"); - Require(sceAudioOut2SetSystemDebugState(nullptr) == errorInvalidArgument, "a null debug state is refused"); + std::uint32_t loudness[2] = {1, 0}; + const auto set = [&loudness](std::uint32_t id, void* data, std::size_t size) { + const AudioOut2SystemDebugStateParam param{id, 0, data, size}; + return sceAudioOut2SetSystemDebugState(¶m); + }; + Require(set(1, loudness, 8) == 0, "the loudness debug state must be accepted"); + Require(set(1, loudness, 8) == 0, "the loudness debug state must be accepted again"); + Require(set(1, loudness, 0) == errorInvalidParam, "a loudness debug state of size 0 is refused"); + Require(set(1, loudness, 4) == errorInvalidParam, "a loudness debug state of size 4 is refused"); + Require(set(1, loudness, 16) == errorInvalidParam, "a loudness debug state of size 16 is refused"); + Require(set(0, loudness, 8) == errorInvalidParam, "debug state id 0 is refused"); + Require(set(2, loudness, 8) == errorInvalidParam, "debug state id 2 is refused"); + Require(set(0x21, loudness, 8) == errorInvalidParam, "debug state id 0x21 is refused"); + Require(set(0xFFFFFFFFu, loudness, 8) == errorInvalidParam, "a large debug state id is refused"); + Require(set(0x20, loudness, 8) == errorInvalidParam, "debug state id 0x20 of size 8 is refused"); + Require(set(0x20, loudness, 0) == errorInvalidParam, "debug state id 0x20 of size 0 is refused"); + Require(ThrowsRuntimeError([&] { set(0x20, loudness, 4); }), "debug state id 0x20 of size 4 must throw"); + Require(ThrowsRuntimeError([&] { set(1, nullptr, 8); }), "a loudness debug state without a param must throw"); + Require(sceAudioOut2SetSystemDebugState(nullptr) == errorInvalidPointer, "a null debug state is refused"); } void TestEnableChat() { diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 792d6e1a9e..facdb641ee 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -10,7 +10,7 @@ - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) - native initialization requires AudioOut2 initialization (0x80268006 otherwise), returns success immediately when already initialized, and otherwise rejects nonzero flags with 0x80268201. Term returns 0x80268203 without mastering initialization and clears the mastering state after a successful service call. The protected Init body remains unchanged; host Term still returns success without this lifecycle or native mastering allocations. - [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the errors `0x80268201` and `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does; a null parameter returns `0x80268201`, the mastering invalid-API-parameter error KytyPS5 returns for a null state, which is an inference rather than a measurement. The meaning of `output` and `user` is unknown and both are ignored -- [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. The debug state ids are unknown, so every id is accepted, and a null param returns `0x80260502` like the other AudioOut2 calls. Not measured on a console +- [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. Id `0x20` with a 4-byte param throws, and the not-initialized error is not returned. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [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 - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device From 795c3903392405b49849bcd90eccb2a6d6cb2380 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:11:12 +0200 Subject: [PATCH 1038/1360] fix(libSceAgc): take the semaphore address second in Acb/Dcb MemSemaphore PPSA03396 calls sceAgcAcbMemSemaphore and sceAgcDcbMemSemaphore as (cb, address, waitForMailbox, signalType, operation). Both read the address from the fourth argument instead, so the call aborted with "null or misaligned address 0x1". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/Acb/src/Sync.cpp | 5 +-- core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp | 5 +-- core/libs/prx/libSceAgc/tests/Semaphore.cpp | 38 ++++++++++++++++++++ docs/dev/debt/libSceAgc.md | 1 + 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Sync.cpp b/core/libs/prx/libSceAgc/Acb/src/Sync.cpp index eee05b37c4..cb4ff7f5a6 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Sync.cpp @@ -44,10 +44,7 @@ std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize() { return 32; } -std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, std::uint8_t action, std::uint8_t clientCode, const volatile void* address) { - const std::uint8_t operation = action != 0 ? 7u : 6u; - const std::uint8_t signalType = clientCode & 1u; - const std::uint8_t waitForMailbox = (clientCode >> 1u) & 1u; +std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation) { return Agc::Command::WriteMemSemaphore(buf, address, waitForMailbox, signalType, operation, __func__); } diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index 96c45f0d3c..d3fb4b534b 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -52,10 +52,7 @@ std::uint32_t APS5_VABI sceAgcDcbStallCommandBufferParserGetSize() { return 8; } -std::uint32_t* APS5_VABI sceAgcDcbMemSemaphore(CommandBuffer* buf, std::uint8_t action, std::uint8_t clientCode, const volatile void* address) { - const std::uint8_t operation = action != 0 ? 7u : 6u; - const std::uint8_t signalType = clientCode & 1u; - const std::uint8_t waitForMailbox = (clientCode >> 1u) & 1u; +std::uint32_t* APS5_VABI sceAgcDcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation) { return Agc::Command::WriteMemSemaphore(buf, address, waitForMailbox, signalType, operation, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/Semaphore.cpp b/core/libs/prx/libSceAgc/tests/Semaphore.cpp index 789a303ad8..362d180ad6 100644 --- a/core/libs/prx/libSceAgc/tests/Semaphore.cpp +++ b/core/libs/prx/libSceAgc/tests/Semaphore.cpp @@ -15,6 +15,8 @@ extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes); extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label); extern "C" std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation); namespace { @@ -180,6 +182,40 @@ void testMemSemaphore() { expectFailure([&] { sceAgcCbMemSemaphore(nullptr, label, 0, 0, 0); }); } +using QueueMemSemaphore = std::uint32_t* APS5_VABI (*)(CommandBuffer*, const volatile void*, std::uint8_t, std::uint8_t, std::uint8_t); + +void testQueueMemSemaphore(QueueMemSemaphore write) { + const auto* semaphore = reinterpret_cast<const volatile void*>(0x102f221d98ull); + + { + std::array<std::uint32_t, 16> words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = write(&buffer, semaphore, 0, 1, 6); + check(packet == words.data(), "packet address mismatch"); + check(packet[0] == 0xc0023900u, "MEM_SEMAPHORE header opcode mismatch"); + check(packet[1] == 0x2f221d98u, "MEM_SEMAPHORE address low mismatch"); + check(packet[2] == 0x10u, "MEM_SEMAPHORE address high mismatch"); + check(packet[3] == 0xc0100000u, "MEM_SEMAPHORE signal control word mismatch"); + check(buffer.cursor_up == words.data() + 4, "cursor did not advance by 4 words"); + check(words[4] == 0xdeadbeefu, "words past packet modified"); + } + + { + std::array<std::uint32_t, 16> words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = write(&buffer, semaphore, 1, 0, 7); + check(packet[3] == 0xe0010000u, "MEM_SEMAPHORE wait control word mismatch"); + } + + std::array<std::uint32_t, 16> words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + expectFailure([&] { write(&buffer, reinterpret_cast<const volatile void*>(1ull), 0, 1, 6); }); + expectFailure([&] { write(&buffer, semaphore, 0, 1, 5); }); + expectFailure([&] { write(&buffer, semaphore, 2, 0, 7); }); +} + } int main() { @@ -191,6 +227,8 @@ int main() { testRegistration(); testLabels(); testMemSemaphore(); + testQueueMemSemaphore(sceAgcAcbMemSemaphore); + testQueueMemSemaphore(sceAgcDcbMemSemaphore); testLabelRejections(); testRepeatedRegistration(); storage.reset(); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index ab5eaacf4e..cb177fed98 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -24,6 +24,7 @@ - [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware +- [sceAgcAcbMemSemaphore, sceAgcDcbMemSemaphore](../../../core/libs/prx/libSceAgc/Acb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, waitForMailbox, signalType, operation) inferred from PPSA03396, whose wrapper passes the address second and operation 6 (signal) or 7 (wait) last; the three trailing arguments go to the packet as-is - [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown From fef3c4e35ce0eca2c0fdc049c9541751034befa3 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Tue, 20 Oct 2026 13:32:44 +0300 Subject: [PATCH 1039/1360] test(agc): bound array sample expectations by the selected view --- .../tests/execution/ImageArrayWindowPastLayerLimit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp index 528cad640e..d6a5e73941 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -120,7 +120,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const float u = std::bit_cast<float>(Input[tid * 4u + 0u]); - const float layer = std::clamp(std::nearbyint(std::bit_cast<float>(Input[tid * 4u + 1u])), 0.0f, static_cast<float>(Layers - 1u)); + const float layer = std::clamp(std::nearbyint(std::bit_cast<float>(Input[tid * 4u + 1u])), 0.0f, static_cast<float>(LastArray - baseArray)); const auto x = static_cast<std::uint32_t>(std::clamp(static_cast<int>(std::floor(u * static_cast<float>(Width))), 0, static_cast<int>(Width) - 1)); Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast<std::uint32_t>(layer))); Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u])); From 1bf43f9b02a7e544cc332185598e4deb99035361 Mon Sep 17 00:00:00 2001 From: LeandroLP23 <leandroplacidi@gmail.com> Date: Tue, 20 Oct 2026 17:10:37 +0200 Subject: [PATCH 1040/1360] fix(recompiler): read P20 of a flat input as the provoking vertex A FLAT_SHADE input without pass-through that a pixel shader reads through v_interp_mov_f32 P20 and not P10 stayed an explicit per-vertex input and read vertex 2 minus vertex 0. For a flat value that is the same on every vertex the difference is 0, and with the first provoking vertex nothing refused it. ps5-opengl lowers every flat input to a load of one vertex: vertex 1 (P10) by default, which #4030 reads as the provoking vertex, and vertex 2 (P20) for triangles when GL's flatshade_first is set. SuperTuxKart's deferred point lights are drawn that way, so all twelve flat inputs of pointlight.frag read 0, the attenuation 0 * -inf was NaN, nothing was discarded, and the starting-line referee's light quad blacked out the scene behind it (#8364). A P20-only read now returns the provoking vertex's value, as P10 and P0 do. The lane was not measured on the console either, so FragmentParameter records it and a pipeline using the last provoking vertex throws. An input read through both P10 and P20 keeps its per-vertex differences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 ++++ core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 8 ++++++++ .../Optimization/src/ShaderInfoCollector.cpp | 6 +++++- core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 ++ .../src/SpirvMemory/SpirvInputOutput.cpp | 12 +++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 7 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 746a4c53ce..aaa08ba07f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -112,6 +112,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } return; } + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flatP20; }), "v_interp_mov p20 of a flat input with the last provoking vertex is not measured"); + } if (BarycentricPatches(state, shaders)) { for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d9d764d50d..41b0fb6ec4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9210,6 +9210,14 @@ void ProvokingVertexTests() { const std::array<CompiledShader, 1> flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; ValidateProvokingVertex(supported, last, flatP10Shaders); expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + const std::array<std::uint32_t, 7> flatP20Color{0xc8120001u, 0xc8160101u, 0xc81a0201u, 0xc81e0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatP20Pixel = recompilePixel({0x400u}, flatP20Color); + const auto p20Inputs = locatedInputs(flatP20Pixel.spirv.Words()); + Require(p20Inputs.size() == 1 && p20Inputs[0].location == 0 && p20Inputs[0].flat && !p20Inputs[0].perVertex, "flat v_interp_mov P20 did not compile to a flat input"); + Require(flatP20Pixel.fragmentParameters.size() == 1 && flatP20Pixel.fragmentParameters[0].flatP20 && !flatP20Pixel.fragmentParameters[0].flatP10 && !flatP20Pixel.fragmentParameters[0].perVertex && !flatP10Pixel.fragmentParameters[0].flatP20, "flat v_interp_mov P20 was not recorded apart from P10"); + const std::array<CompiledShader, 1> flatP20Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP20Pixel, 0}}}; + ValidateProvokingVertex(supported, first, flatP20Shaders); + expectFailure([&] { ValidateProvokingVertex(supported, last, flatP20Shaders); }, "v_interp_mov p20 of a flat input with the last provoking vertex is not measured"); auto lastStrip = last; lastStrip.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; auto lastList = last; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index b7f35bb4c1..aa37749e51 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -285,6 +285,8 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi std::array<bool, 32> perVertex {}; std::array<bool, 32> interpolated {}; std::array<bool, 32> halfRead {}; + std::array<bool, 32> readsP10 {}; + std::array<bool, 32> readsP20 {}; for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() == IrOpcode::GetAttribute) { @@ -298,7 +300,9 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const auto mode = inst->Argument(2)->Resolve()->ImmediateU32(); read[input] = true; - perVertex[input] = perVertex[input] || mode == 1u || !IsPixelParameterFlat(*pixel, input); + readsP10[input] = readsP10[input] || mode == 0u; + readsP20[input] = readsP20[input] || mode == 1u; + perVertex[input] = perVertex[input] || (readsP10[input] && readsP20[input]) || !IsPixelParameterFlat(*pixel, input); } else if (inst->Opcode() == IrOpcode::GetInterpolationParameterF16) { const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const bool high = inst->Argument(3)->Resolve()->ImmediateU32() != 0u; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 127ed88d5b..23c4658d37 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -377,6 +377,7 @@ struct FragmentParameter { bool custom = false; bool flatP10 = false; bool vertexOrder = false; + bool flatP20 = false; bool operator==(const FragmentParameter&) const = default; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 912e93e844..2362ce871a 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -504,6 +504,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.custom); out.Value(parameter.flatP10); out.Value(parameter.vertexOrder); + out.Value(parameter.flatP20); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); @@ -562,6 +563,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.custom); in.Value(parameter.flatP10); in.Value(parameter.vertexOrder); + in.Value(parameter.flatP20); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 4c05ffe094..645d3d1677 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -49,11 +49,15 @@ namespace ShaderRecompiler const auto& pixel = *inputInfo.pixel; std::vector<FragmentParameter> result; std::array<bool, 32> readsP10{}; + std::array<bool, 32> readsP20{}; for (const auto& block : program.Blocks()) { for (const auto* instruction : block->Instructions()) { - if (instruction->Opcode() != IrOpcode::GetInterpolationParameter || instruction->Argument(2)->Resolve()->ImmediateU32() != 0u) continue; + if (instruction->Opcode() != IrOpcode::GetInterpolationParameter) continue; + const auto mode = instruction->Argument(2)->Resolve()->ImmediateU32(); const auto attribute = instruction->Argument(0)->Resolve()->ImmediateU32(); - if (attribute < readsP10.size()) readsP10[attribute] = true; + if (attribute >= readsP10.size()) continue; + readsP10[attribute] = readsP10[attribute] || mode == 0u; + readsP20[attribute] = readsP20[attribute] || mode == 1u; } } for (const auto& input : program.Info().inputs) { @@ -61,14 +65,16 @@ namespace ShaderRecompiler const auto location = PixelInputLocation(pixel, input.location); const bool flat = PixelInputIsFlat(pixel, input.location); const bool flatP10 = flat && !input.perVertex && input.location < readsP10.size() && readsP10[input.location]; + const bool flatP20 = flat && !input.perVertex && input.location < readsP20.size() && readsP20[input.location]; const bool vertexOrder = input.perVertex && (input.location >= 32u || (program.Metadata().pixelVertexOrderInputs >> input.location & 1u) != 0u); const auto existing = std::find_if(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; }); if (existing != result.end()) { existing->flatP10 = existing->flatP10 || flatP10; + existing->flatP20 = existing->flatP20 || flatP20; existing->vertexOrder = existing->vertexOrder || vertexOrder; continue; } - result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder}); + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder, flatP20}); } return result; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 40077e5d76..a2628074c5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -135,7 +135,7 @@ - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only through `v_interp_mov_f32` P10 gives the provoking vertex's value (ps5-opengl, #1413; first mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). - [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format.<format>.states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). From 414ed51714957d137086fccf19d6f2e55f94be82 Mon Sep 17 00:00:00 2001 From: "T. Vuokko" <taovuokko@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:30:33 +0300 Subject: [PATCH 1041/1360] fix(relinker): skip absolute function symbols in code analysis --- .../src/analysis/CodeInstructionCollector.cpp | 4 ++- .../tests/test_guest_absolute_symbols.py | 28 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 645dd46f3a..2e7d216a7f 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -6,6 +6,7 @@ #include <codegen/IInstructionScanner.hpp> #include <codegen/x86/X64InstructionDecoder.hpp> #include <codegen/CodegenException.hpp> +#include <elfpatcher/general/ElfConstants.hpp> #include <io/BufferUtils.hpp> #include <algorithm> #include <bit> @@ -272,7 +273,8 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve symbols = table(6, 0x61000039, symbolCount * 24); for (std::uint64_t index = 0; index < symbolCount; ++index) { const auto offset = symbols + index * 24; - if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) { + const auto section = Io::ReadU16(bytes, offset + 6); + if ((bytes[offset + 4] & 15) == 2 && section != 0 && section != Elfpatcher::SHN_ABS) { const auto address = Io::ReadU64(bytes, offset + 8); addRoot(address); addFunction(address, Io::ReadU64(bytes, offset + 16), true); diff --git a/core/relinker/relinker/tests/test_guest_absolute_symbols.py b/core/relinker/relinker/tests/test_guest_absolute_symbols.py index a0c0056e57..77a57f1fba 100644 --- a/core/relinker/relinker/tests/test_guest_absolute_symbols.py +++ b/core/relinker/relinker/tests/test_guest_absolute_symbols.py @@ -12,13 +12,14 @@ from test_macos_output import MachO ABSOLUTE = 0xFFF1 COMMON = 0xFFF2 VALUE = 0x1234 +SIZE = 0x10 ADDEND = 0x10 TARGET = 0x2320 -def module_with_section(section): +def module_with_section(section, size=0): image = module_with_symbol(True) - struct.pack_into("<HQQ", image, 0x898 + 6, section, VALUE, 0) + struct.pack_into("<HQQ", image, 0x898 + 6, section, VALUE, size) struct.pack_into("<Q", image, 0x668, 24) struct.pack_into("<QQq", image, 0x900, TARGET, (1 << 32) | 1, ADDEND) return image @@ -73,10 +74,10 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-guest-absolute-") as directory: work = Path(directory) - def convert(name, system, section): + def convert(name, system, section, size=0): case = work / f"{name}-{system}" (case / "sce_module").mkdir(parents=True) - (case / "sce_module" / "module.prx").write_bytes(module_with_section(section)) + (case / "sce_module" / "module.prx").write_bytes(module_with_section(section, size)) source = case / "input.elf" source.write_bytes(main_fixture()) output = case / ("output.exe" if system == "windows" else "output.elf") @@ -86,15 +87,16 @@ def main(): return result, case / "app0" / "sce_module" / "module.prx.guest.prx" for system in ("linux", "windows", "macos"): - result, artifact = convert("absolute", system, ABSOLUTE) - assert result.returncode == 0, (system, result.stdout, result.stderr) - data = artifact.read_bytes() - if system == "windows": - assert any(value == VALUE + ADDEND for _, value in pe_qwords(data)), "absolute relocation value missing" - elif system == "macos": - assert any(value == VALUE + ADDEND for _, value in macho_qwords(data)), "absolute relocation value missing" - else: - assert elf_relocation(data) == (1, ABSOLUTE, VALUE, ADDEND) + for size in (0, SIZE): + result, artifact = convert(f"absolute-{size}", system, ABSOLUTE, size) + assert result.returncode == 0, (system, size, result.stdout, result.stderr) + data = artifact.read_bytes() + if system == "windows": + assert any(value == VALUE + ADDEND for _, value in pe_qwords(data)), "absolute relocation value missing" + elif system == "macos": + assert any(value == VALUE + ADDEND for _, value in macho_qwords(data)), "absolute relocation value missing" + else: + assert elf_relocation(data) == (1, ABSOLUTE, VALUE, ADDEND) result, artifact = convert("common", system, COMMON) assert result.returncode == 2 and "Unsupported special symbol section" in result.stderr, result.stderr From be4fd86249fb6ee3217c411d640f2e72afbb0775 Mon Sep 17 00:00:00 2001 From: Retr0 <gurau.eduard@ichb.ro> Date: Wed, 21 Oct 2026 15:16:49 +0300 Subject: [PATCH 1042/1360] fix(libkernel): disconnect a connected datagram socket on connect to port 0 FreeBSD 11.4's soconnectat() (sys/kern/uipc_socket.c) disconnects a connected socket of a protocol without PR_CONNREQUIRED before it calls pru_connect. udp_disconnect() and udp6_disconnect() drop the peer and reset the local address, then in_pcbconnect_setup() and in6_pcbladdr() fail port 0 with EADDRNOTAVAIL, so the socket is left unconnected. connect to port 0 on a connected datagram socket now dissolves the association through the AF_UNSPEC disconnect path before failing with 49, where it kept the old peer. Stream sockets keep EISCONN. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 24 +++++++++++++--------- core/libs/tests/GuestSocketPortZero.cpp | 24 +++++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 938d7d48a6..d1505081d0 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -302,16 +302,6 @@ bool Listening(const Socket& socket) { bool listening = false; return GuestSockets::NativeListening(static_cast<std::uintptr_t>(socket.value), listening) && listening; } -bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t length) { - if (!address || (socket.type != 1 && socket.type != 2) || (socket.family != 2 && socket.family != 28)) return false; - const auto* bytes = static_cast<const unsigned char*>(address); - if (length < (socket.family == 2 ? 16u : 28u) || bytes[1] != socket.family || (bytes[2] | bytes[3]) != 0) return false; - sockaddr_storage peer{}; - socklen_t peerSize = sizeof(peer); - if (::getpeername(socket.value, reinterpret_cast<sockaddr*>(&peer), &peerSize) == 0) return false; - Fail(49); - return true; -} constexpr std::int64_t NotAnswered = -2; std::int64_t ZeroPortSend(const Socket& socket, const sockaddr_storage& destination, std::uint64_t length) { if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return NotAnswered; @@ -353,6 +343,20 @@ int Disconnect(const Socket& socket) { if (::bind(socket.value, reinterpret_cast<sockaddr*>(&wildcard), localSize) != 0) return Fail(NativeError()); return 0; } +bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t length) { + if (!address || (socket.type != 1 && socket.type != 2) || (socket.family != 2 && socket.family != 28)) return false; + const auto* bytes = static_cast<const unsigned char*>(address); + if (length < (socket.family == 2 ? 16u : 28u) || bytes[1] != socket.family || (bytes[2] | bytes[3]) != 0) return false; + if (socket.type == 2) { + if (Disconnect(socket) != 0) return true; + } else { + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast<sockaddr*>(&peer), &peerSize) == 0) return false; + } + Fail(49); + return true; +} void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* length) { unsigned char bytes[28]{}; bytes[0] = native.ss_family == AF_INET ? 16 : 28; diff --git a/core/libs/tests/GuestSocketPortZero.cpp b/core/libs/tests/GuestSocketPortZero.cpp index 070b9c8982..36857ee65e 100644 --- a/core/libs/tests/GuestSocketPortZero.cpp +++ b/core/libs/tests/GuestSocketPortZero.cpp @@ -11,6 +11,7 @@ int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI listen_nid_postfix(int, int); int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); @@ -45,7 +46,28 @@ static void CheckDatagram(int family) { Require(connect_nid_postfix(probe, address.data(), size) == -1 && *__error_nid_postfix() == 49, "connect to port 0 on an unconnected datagram socket must fail with EADDRNOTAVAIL (49)"); Require(getpeername_nid_postfix(probe, name.data(), &length) == -1 && *__error_nid_postfix() == 57, "still unconnected"); - Require(close_nid_postfix(probe) == 0, "close"); + const int peer = socket_nid_postfix(family, 2, 0); + Require(peer >= 0, "peer"); + const int bound = bind_nid_postfix(peer, address.data(), size); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(peer) == 0 && close_nid_postfix(probe) == 0, "close"); + return; + } + Require(bound == 0, "bind peer"); + Address target{}; + length = target.size(); + Require(getsockname_nid_postfix(peer, target.data(), &length) == 0, "peer name"); + Require(connect_nid_postfix(probe, target.data(), size) == 0, "connect to the peer"); + length = name.size(); + Require(getpeername_nid_postfix(probe, name.data(), &length) == 0, "connected"); + Require(connect_nid_postfix(probe, address.data(), size) == -1 && *__error_nid_postfix() == 49, + "connect to port 0 on a connected datagram socket must fail with EADDRNOTAVAIL (49)"); + length = name.size(); + Require(getpeername_nid_postfix(probe, name.data(), &length) == -1 && *__error_nid_postfix() == 57, + "connect to port 0 must disconnect a connected datagram socket"); + Require(send_nid_postfix(probe, "x", 1, 0) == -1 && *__error_nid_postfix() == 39, + "send without an address after the disconnect must fail with EDESTADDRREQ (39)"); + Require(close_nid_postfix(peer) == 0 && close_nid_postfix(probe) == 0, "close"); } static void CheckStream(int family) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 89fbd344b2..3cf9e7fb94 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -206,7 +206,7 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed +- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do; a connected datagram socket is disconnected first, as `soconnect()` does, and then fails with 49. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it From ff2b4ba4cc1542a45d98d7b77292041ab6389a54 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:32:15 +0200 Subject: [PATCH 1043/1360] fix(libkernel): report EPIPE for a stream send on a connection the peer closed on Windows sosend_generic fails a send on a socket that can send no more with EPIPE before it looks at so_error. Winsock reports the same send as WSAECONNABORTED or WSAECONNRESET, which reached the guest as ECONNABORTED or ECONNRESET. A stream send now maps both to EPIPE. The send-after-close checks of guest_tcp, guest_socket_read_write and guest_socket_pair run on Windows too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 +++- core/libs/tests/GuestSocketPair.cpp | 6 +++--- core/libs/tests/GuestSocketReadWrite.cpp | 2 -- core/libs/tests/GuestTcp.cpp | 2 -- docs/dev/debt/libkernel.md | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 938d7d48a6..22a664494c 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -106,7 +106,9 @@ int ConnectError() { } int SendError(int type) { #ifdef _WIN32 - if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; + const int error = WSAGetLastError(); + if (type == 2 && error == WSAENOTCONN) return 39; + if (type == 1 && (error == WSAECONNABORTED || error == WSAECONNRESET)) return 32; #else static_cast<void>(type); #endif diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index ec71ffc24c..0b827364c4 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -213,9 +213,9 @@ int main(int argc, char** argv) { Require(Exchange(pair[1], pair[0], "still open")); Require(close_nid_postfix(pair[0]) == 0); Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0); -#ifndef _WIN32 - Require(send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal) == -1 && *__error_nid_postfix() == 32); -#endif + std::int64_t sent = 0; + for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal); + Require(sent == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(pair[1]) == 0); Require(close_nid_postfix(pair[1]) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestSocketReadWrite.cpp b/core/libs/tests/GuestSocketReadWrite.cpp index 5a8d48781b..4af5913b09 100644 --- a/core/libs/tests/GuestSocketReadWrite.cpp +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -148,11 +148,9 @@ int main() { Require(close_nid_postfix(client) == 0); Require(Readable(server, In | Hup) && read_nid_postfix(server, received, sizeof(received)) == 0); Require(sceKernelRead(server, received, sizeof(received)) == 0); -#ifndef _WIN32 std::int64_t written = 0; for (int i = 0; i < 100 && written >= 0; ++i) written = write_nid_postfix(server, request, sizeof(request)); Require(written == -1 && *__error_nid_postfix() == 32); -#endif Require(close_nid_postfix(server) == 0); *__error_nid_postfix() = 0; Require(read_nid_postfix(server, received, sizeof(received)) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 332b9ebede..11fe4a9366 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -277,11 +277,9 @@ int main() { Require(send_nid_postfix(client, request, sizeof(request), 0x20000) == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(accepted) == 0); -#ifndef _WIN32 std::int64_t sent = 0; for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(client, request, sizeof(request), 0x20000); Require(sent == -1 && *__error_nid_postfix() == 32); -#endif Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 89fbd344b2..9791a9aae4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -186,7 +186,7 @@ - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; `RLIMIT_NPROC` always throws; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443, as FreeBSD 11 would if the console's `maxproc` were 1604: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` lowers an `RLIMIT_NPROC` limit above `maxprocperuid` to it ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)); 1604 × 9 / 10 gives 1443. Neither value is measured: `kern.maxproc` fails with `ENOENT` in the payload process. Here that call throws, because the limit is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. -- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported +- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and the first stream `send` after the peer closed succeeds on the loopback connection, where the console returns `EPIPE`; the sends after it report `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported - Guest sockets on macOS ([the equeue wait, kevent, fstat and ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp)): macOS has no `ppoll`, so the wait uses `poll` with the timeout rounded up to a millisecond; it has no `POLLRDHUP`, so a stream that the peer closed is found by peeking one byte when it is readable; `FIONREAD` on a datagram socket, and the byte count that `fstat` and `kevent` report for it, is `SO_NREAD`, the size of the next datagram, because macOS adds the source address to `FIONREAD`; and `gethostbyname` replaces `gethostbyname_r`, which macOS lacks. - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws From 2cfe0735a94d3564fe8a1aea75ffeb98f7142578 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Thu, 22 Oct 2026 14:17:57 -0300 Subject: [PATCH 1044/1360] feat(libSceFreeType): implement FT_Request_Size --- core/libs/prx/libSceFreeType/CMakeLists.txt | 14 ++ .../libSceFreeType/include/GuestFreeType.hpp | 10 + .../prx/libSceFreeType/src/SizeRequest.cpp | 24 +++ core/libs/tests/GuestFreeTypeRequestSize.cpp | 188 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 238 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/SizeRequest.cpp create mode 100644 core/libs/tests/GuestFreeTypeRequestSize.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e0a3b3e116..77db545270 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -4,6 +4,20 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceFreeType PRIVATE freetype) +target_sources(libSceFreeType PRIVATE src/SizeRequest.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_request_size_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeRequestSize.cpp) + target_include_directories(guest_freetype_request_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_request_size_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_request_size_tests) + add_test(NAME guest_freetype_request_size COMMAND guest_freetype_request_size_tests) + if(WIN32) + set_tests_properties(guest_freetype_request_size PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") + endif() +endif() + if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceFreeType PRIVATE LINKER:--exclude-libs,ALL) endif() diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 1a2a074378..4c948985ea 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -58,6 +58,14 @@ struct GuestSizeRec { void* internal; }; +struct GuestSizeRequestRec { + std::uint32_t type; + std::int64_t width; + std::int64_t height; + std::uint32_t horiResolution; + std::uint32_t vertResolution; +}; + struct GuestGlyphMetrics { std::int64_t width; std::int64_t height; @@ -154,6 +162,8 @@ static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) = static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); static_assert(sizeof(GuestSizeMetrics) == 56 && offsetof(GuestSizeMetrics, x_scale) == 8); static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 24); +static_assert(sizeof(GuestSizeRequestRec) == 32 && offsetof(GuestSizeRequestRec, width) == 8 && offsetof(GuestSizeRequestRec, height) == 16); +static_assert(offsetof(GuestSizeRequestRec, horiResolution) == 24 && offsetof(GuestSizeRequestRec, vertResolution) == 28); static_assert(sizeof(GuestBitmap) == 40 && offsetof(GuestBitmap, buffer) == 16 && offsetof(GuestBitmap, palette) == 32); static_assert(sizeof(GuestOutline) == 40 && offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, flags) == 32); static_assert(offsetof(GuestGlyphSlotRec, glyph_index) == 0x18 && offsetof(GuestGlyphSlotRec, metrics) == 0x30); diff --git a/core/libs/prx/libSceFreeType/src/SizeRequest.cpp b/core/libs/prx/libSceFreeType/src/SizeRequest.cpp new file mode 100644 index 0000000000..6ef487fa6a --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/SizeRequest.cpp @@ -0,0 +1,24 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/General.hpp" + +#include <limits> + +using namespace FreeType; + +extern "C" { + +int APS5_VABI FT_Request_Size_nid_postfix(GuestFaceRec* face, const GuestSizeRequestRec* request) { + GuestFace* owner = FaceOf(face); + if (!owner || !owner->host->size || !request) return FT_Request_Size(owner ? owner->host : nullptr, nullptr); + if (request->width < 0 || request->height < 0 || request->type >= FT_SIZE_REQUEST_TYPE_MAX) return FT_Err_Invalid_Argument; + if (request->width > std::numeric_limits<FT_Long>::max() || request->height > std::numeric_limits<FT_Long>::max()) { + NotImplemented_nid_no_patch(__func__); + } + FT_Size_RequestRec hostRequest{static_cast<FT_Size_Request_Type>(request->type), static_cast<FT_Long>(request->width), + static_cast<FT_Long>(request->height), request->horiResolution, request->vertResolution}; + const FT_Error error = FT_Request_Size(owner->host, &hostRequest); + SyncSize(*owner); + return error; +} + +} diff --git a/core/libs/tests/GuestFreeTypeRequestSize.cpp b/core/libs/tests/GuestFreeTypeRequestSize.cpp new file mode 100644 index 0000000000..536d8493b1 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeRequestSize.cpp @@ -0,0 +1,188 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include <ft2build.h> +#include FT_FREETYPE_H + +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <limits> +#include <stdexcept> +#include <string> +#include <utility> +#include <vector> + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); +int APS5_VABI FT_Request_Size_nid_postfix(GuestFaceRec*, const GuestSizeRequestRec*); +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType size request check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct Fixture { + std::vector<unsigned char> bytes; + FT_Library library = nullptr; + FT_Face native = nullptr; + void* guestLibrary = nullptr; + GuestFaceRec* guest = nullptr; + + explicit Fixture(std::vector<unsigned char> font, int faceIndex = 0) : bytes(std::move(font)) { + Require(FT_Init_FreeType(&library) == 0); + Require(FT_New_Memory_Face(library, bytes.data(), static_cast<FT_Long>(bytes.size()), faceIndex, &native) == 0); + Require(FT_Init_FreeType_nid_postfix(&guestLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(guestLibrary, bytes.data(), static_cast<std::int64_t>(bytes.size()), faceIndex, &guest) == 0); + } + + ~Fixture() { + Require(FT_Done_FreeType_nid_postfix(guestLibrary) == 0); + Require(FT_Done_FreeType(library) == 0); + } + + void CheckMetrics() const { + const auto& actual = guest->size->metrics; + const auto& expected = native->size->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); + } + + int Request(GuestSizeRequestRec request) { + FT_Size_RequestRec hostRequest{static_cast<FT_Size_Request_Type>(request.type), static_cast<FT_Long>(request.width), + static_cast<FT_Long>(request.height), request.horiResolution, request.vertResolution}; + const auto before = request; + const auto* size = guest->size; + const auto generic = size->generic; + const int error = FT_Request_Size(native, &hostRequest); + Require(FT_Request_Size_nid_postfix(guest, &request) == error); + Require(request.type == before.type && request.width == before.width && request.height == before.height); + Require(request.horiResolution == before.horiResolution && request.vertResolution == before.vertResolution); + Require(guest->size == size && size->face == guest && size->generic.data == generic.data && size->generic.finalizer == generic.finalizer); + CheckMetrics(); + return error; + } + + void Load() const { + Require(FT_Load_Glyph(native, 1, FT_LOAD_DEFAULT) == 0); + Require(FT_Load_Glyph_nid_postfix(guest, 1, FT_LOAD_DEFAULT) == 0); + const auto& actual = guest->glyph->metrics; + const auto& expected = native->glyph->metrics; + Require(actual.width == expected.width && actual.height == expected.height); + Require(actual.horiBearingX == expected.horiBearingX && actual.horiBearingY == expected.horiBearingY); + Require(actual.horiAdvance == expected.horiAdvance && actual.vertAdvance == expected.vertAdvance); + Require(guest->glyph->advance.x == native->glyph->advance.x && guest->glyph->advance.y == native->glyph->advance.y); + } +}; + +static void CheckScalable() { + Fixture fixture(SquareGlyphFont('A', 913)); + fixture.guest->size->generic.data = &fixture; + for (std::uint32_t type = FT_SIZE_REQUEST_TYPE_NOMINAL; type < FT_SIZE_REQUEST_TYPE_MAX; ++type) { + Require(fixture.Request({type, 0, 0, 0, 0}) == FT_Err_Invalid_PPem); + for (const auto& dimensions : std::array<std::array<std::int64_t, 2>, 4>{{{0, 23 * 64 + 17}, + {31 * 64 + 49, 0}, {17 * 64 + 31, 29 * 64 + 33}, {48 * 64, 12 * 64}}}) { + for (const auto& dpi : std::array<std::array<std::uint32_t, 2>, 4>{{{0, 0}, {72, 144}, {96, 0}, {0, 120}}}) { + const auto factor = type == FT_SIZE_REQUEST_TYPE_SCALES ? 64 : 1; + Require(fixture.Request({type, dimensions[0] * factor, dimensions[1] * factor, dpi[0], dpi[1]}) == 0); + fixture.Load(); + } + } + } + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_SCALES, 0x18000, 0x28000, 0, 0}) == 0); + const auto scales = fixture.guest->size->metrics; + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_SCALES, 0x18000, 0x28000, + std::numeric_limits<std::uint32_t>::max(), std::numeric_limits<std::uint32_t>::max()}) == 0); + Require(fixture.guest->size->metrics.x_scale == scales.x_scale && fixture.guest->size->metrics.y_scale == scales.y_scale); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 65536LL * 64, 17 * 64, 0, 0}) == FT_Err_Invalid_Pixel_Size); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 19 * 64, 27 * 64, 0, 0}) == 0); + fixture.Load(); + auto* slot = fixture.guest->glyph; + Require(slot->outline.n_points > 0); + slot->outline.points[0].x += 137; + slot->outline.flags ^= 2; + const auto point = slot->outline.points[0]; + const auto savedSlot = *slot; + for (const auto width : {21LL * 64, 65536LL * 64}) { + fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, width, 15 * 64, 0, 0}); + Require(std::memcmp(slot, &savedSlot, sizeof(savedSlot)) == 0); + Require(slot->outline.points[0].x == point.x && slot->outline.points[0].y == point.y); + } +} + +static void CheckInvalid() { + Fixture fixture(SquareGlyphFont('A', 913)); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 17 * 64, 23 * 64, 0, 0}) == 0); + for (const auto& request : std::array<GuestSizeRequestRec, 6>{{ + {FT_SIZE_REQUEST_TYPE_NOMINAL, -1, 1024, 0, 0}, + {FT_SIZE_REQUEST_TYPE_NOMINAL, 1024, std::numeric_limits<std::int64_t>::min(), 0, 0}, + {FT_SIZE_REQUEST_TYPE_MAX, 1024, 1024, 0, 0}, + {std::numeric_limits<std::uint32_t>::max(), 1024, 1024, 0, 0}, + {FT_SIZE_REQUEST_TYPE_MAX, std::numeric_limits<std::int64_t>::max(), 1024, 0, 0}, + {FT_SIZE_REQUEST_TYPE_NOMINAL, std::numeric_limits<std::int64_t>::max(), -1, 0, 0}}}) { + Require(FT_Request_Size_nid_postfix(fixture.guest, &request) == FT_Err_Invalid_Argument); + Require(FT_Request_Size_nid_postfix(nullptr, &request) == FT_Err_Invalid_Face_Handle); + fixture.CheckMetrics(); + } + Require(FT_Request_Size_nid_postfix(nullptr, nullptr) == FT_Err_Invalid_Face_Handle); + Require(FT_Request_Size_nid_postfix(fixture.guest, nullptr) == FT_Request_Size(fixture.native, nullptr)); + fixture.CheckMetrics(); + + Fixture probe(SquareGlyphFont('A', 913), -1); + Require(probe.guest->size == nullptr && probe.native->size == nullptr); + GuestSizeRequestRec request{FT_SIZE_REQUEST_TYPE_NOMINAL, -1, -1, 0, 0}; + Require(FT_Request_Size_nid_postfix(probe.guest, &request) == FT_Err_Invalid_Size_Handle); + Require(FT_Request_Size_nid_postfix(probe.guest, nullptr) == FT_Err_Invalid_Size_Handle); + + for (const auto& dimensions : std::array<std::array<std::int64_t, 2>, 2>{{{0x100000000LL + 1024, 1024}, {1024, 0x100000000LL + 1024}}}) { + request = {FT_SIZE_REQUEST_TYPE_NOMINAL, dimensions[0], dimensions[1], 0, 0}; + if constexpr (sizeof(FT_Long) == sizeof(std::int64_t)) { + Require(fixture.Request(request) == FT_Err_Invalid_Pixel_Size); + } else { + bool threw = false; + try { + FT_Request_Size_nid_postfix(fixture.guest, &request); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + fixture.CheckMetrics(); + } + } +} + +static void CheckBitmap() { + const std::string bdf = "STARTFONT 2.1\nFONT -AnyPS5-Fixed-Medium-R-Normal--8-80-72-72-C-80-ISO10646-1\n" + "SIZE 8 72 72\nFONTBOUNDINGBOX 8 8 0 0\nSTARTPROPERTIES 4\nFONT_ASCENT 8\nFONT_DESCENT 0\n" + "CHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\nENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\n" + "SWIDTH 1000 0\nDWIDTH 8 0\nBBX 8 8 0 0\nBITMAP\n18\n24\n42\n7E\n42\n42\n42\n00\nENDCHAR\nENDFONT\n"; + Fixture fixture({bdf.begin(), bdf.end()}); + Require(!FT_IS_SCALABLE(fixture.native) && fixture.guest->num_fixed_sizes == 1); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 8 * 64, 8 * 64, 0, 0}) == 0); + fixture.Load(); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 0, 8 * 64, 72, 72}) == 0); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_REAL_DIM, 8 * 64, 8 * 64, 0, 0}) == 0); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 0, 17 * 64, 0, 0}) == FT_Err_Invalid_Pixel_Size); + for (const auto type : {FT_SIZE_REQUEST_TYPE_BBOX, FT_SIZE_REQUEST_TYPE_CELL, FT_SIZE_REQUEST_TYPE_SCALES}) { + Require(fixture.Request({static_cast<std::uint32_t>(type), 8 * 64, 8 * 64, 0, 0}) == FT_Err_Unimplemented_Feature); + } +} + +int main() { + CheckScalable(); + CheckInvalid(); + CheckBitmap(); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..3b4eeb8f25 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -6,6 +6,8 @@ - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions +- [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. + - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. ### Functional From 7e2cc7ec6b8002a3c564984d9c615eeb8b1981dd Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:26:24 +0200 Subject: [PATCH 1045/1360] fix(agc): accept the standard component order for 11_11_10 float color targets PPSA12361 binds color format 7 (COLOR_11_11_10, float) with COMP_SWAP 0 and stops on "unsupported color format 7 number type 7 component swap 0". Only the reversed order (COMP_SWAP 2) was accepted. The format keeps its 10-bit component in the low bits. The standard swap puts red in the first slot, as Mesa pairs SWAP_STD with COLOR_10_11_11 for R11G11B10_FLOAT, so COMP_SWAP 0 puts red there and COMP_SWAP 2 blue. Swap 0 now decodes with the identity export mapping 0xe4 and the same packing; the other swaps still throw. The decode test accepts swap 0 and keeps refusing swaps 1 and 3. The execution test renders both orders into tiled and linear targets and compares each stored word with the packing of the order. Its program pool grows to fit the second pass. The debt entry names the accepted orders and says the swap 0 layout is derived, not measured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 7 ++++- .../execution/PackedFloatColorTarget.cpp | 26 +++++++++++-------- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 1c58ed2c10..2619dbabe2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -929,8 +929,8 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11}; return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4}; case 7: - if (swap != 2 || number != floating) return fail(); - return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10}; + if ((swap != 0 && swap != 2) || number != floating) return fail(); + return {VK_FORMAT_R32_UINT, 4, static_cast<std::uint8_t>(swap == 2 ? 0xc6u : 0xe4u), ShaderRecompiler::ColorExportPacking::Float11_11_10}; case 9: // COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing. if (number != unorm) return fail(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ac6fb3cb21..0854c3f7ad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9584,7 +9584,12 @@ void float11_11_10TargetTests() { auto changed = queue; changed.context[0x31c] &= ~0x40000u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets"); - for (const auto swap : {0u, 1u, 3u}) { + changed = queue; + changed.context[0x31c] &= ~0x1800u; + const auto standard = AgcDriver::Graphics::DecodeState(changed); + Require(standard.color.format == VK_FORMAT_R32_UINT && standard.color.componentMapping == 0xe4u && standard.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target with the standard component order did not decode"); + Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xe4u, "export 0 reordered its components into the standard-order 11_11_10 float target"); + for (const auto swap : {1u, 3u}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u); expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp index 9b322cb6d0..238cb5ce89 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp @@ -45,6 +45,7 @@ constexpr std::uint32_t Height = 128; constexpr std::uint32_t ScissorWidth = 128; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t Float11_11_10Info = 0x4171cu; +constexpr std::uint32_t StandardOrderInfo = Float11_11_10Info & ~0x1800u; constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t LinearAttrib3 = 0x09000000u; constexpr std::uint32_t Float32Export = 9; @@ -58,7 +59,7 @@ constexpr std::array<std::array<float, 4>, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -alignas(256) std::array<std::array<std::uint32_t, 64>, 32> Programs{}; +alignas(256) std::array<std::array<std::uint32_t, 64>, 64> Programs{}; std::size_t programCount = 0; std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { @@ -78,7 +79,8 @@ std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { return (static_cast<std::uint32_t>(exponent - 1 + 15) << mantissaBits) | static_cast<std::uint32_t>(std::floor((fraction * 2.0 - 1.0) * scale)); } -std::uint32_t Packed(std::array<float, 3> rgb) { +std::uint32_t Packed(std::array<float, 3> rgb, bool standardOrder) { + if (standardOrder) return SmallFloat(rgb[0], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[2], 6) << 21u); return SmallFloat(rgb[2], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[0], 6) << 21u); } @@ -170,17 +172,17 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3, bool standardOrder) { AgcDriver::Registers cx; cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u); cx[0x31b] = 0; - cx[0x31c] = Float11_11_10Info; + cx[0x31c] = standardOrder ? StandardOrderInfo : Float11_11_10Info; cx[0x31d] = 0; cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); cx[0x3b8] = attrib3; cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u); auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); - Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == 0xc6u && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface"); + Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == (standardOrder ? 0xe4u : 0xc6u) && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface"); return color; } @@ -290,10 +292,10 @@ std::string Describe(std::array<float, 3> rgb) { return text; } -void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind, bool standardOrder) { const ColorTargetLayout layout(Width, Height, mode, 4); Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); - const auto color = DecodeTarget(block, attrib3); + const auto color = DecodeTarget(block, attrib3, standardOrder); auto before = Fill(block); const float nan = std::numeric_limits<float>::quiet_NaN(); const float infinity = std::numeric_limits<float>::infinity(); @@ -311,12 +313,12 @@ void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t at }}; for (const auto& rgb : cases) { Draw(device, color, Float32Export, Float32Program(rgb)); - ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); + ExpectSurface(device, block, layout, before, Packed(rgb, standardOrder), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); before = ReadBack(device, block); } const std::array<float, 3> halves{0.5f, 0.75f, 0.125f}; Draw(device, color, Float16Export, Float16Program(halves)); - ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); + ExpectSurface(device, block, layout, before, Packed(halves, standardOrder), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); } } @@ -327,8 +329,10 @@ int main() { if (!device) return VulkanTestSkipped; Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes()); Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes()); - TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); - TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + for (const bool standardOrder : {false, true}) { + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, standardOrder ? "tiled standard-order" : "tiled", standardOrder); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, standardOrder ? "linear standard-order" : "linear", standardOrder); + } std::puts("11_11_10 float color target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 56658c3e82..9fc09de1a9 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -136,7 +136,7 @@ - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. +- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the standard (COMP_SWAP 0) and reversed (COMP_SWAP 2) component orders are accepted, putting red and blue in the 10-bit low field. That follows from the format's layout and from the standard swap Mesa pairs with COLOR_10_11_11 for R11G11B10_FLOAT (ac_formats.c); it was not measured on the console. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). - [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. From dc84bec7b91f5d9474633c0ce1a1f6fed38fa5cb Mon Sep 17 00:00:00 2001 From: Johan David <johancarras16@gmail.com> Date: Thu, 22 Oct 2026 11:03:12 -0500 Subject: [PATCH 1046/1360] fix(libkernel): fail dlopen of an unrelinked module without the Windows Bad Image dialog On Windows, dlopen passed a module that was never relinked to LoadLibraryExW, which raised the modal Bad Image hard-error dialog and blocked the calling thread until someone answered it. The load now runs with SEM_FAILCRITICALERRORS set for that thread only, so it fails at once with the same result as before. Every failed load is reported once per file on stderr with a [module] prefix; a file that exists with no relinked .guest.prx names the relinker's --module-dir option. --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- .../libkernel/Module/src/DynamicLoader.cpp | 44 ++++++++++++++++--- core/libs/tests/GuestDynamicLoader.cpp | 35 ++++++++++++++- docs/user/USAGE.md | 2 +- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3289c98e99..169f885128 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -239,7 +239,7 @@ if(BUILD_TESTING) target_sources(guest_dynamic_loader_tests PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/GuestDynamicLoader.def") endif() add_test(NAME guest_dynamic_loader COMMAND guest_dynamic_loader_tests $<TARGET_FILE_NAME:guest_module_fixture>) - set_tests_properties(guest_dynamic_loader PROPERTIES + set_tests_properties(guest_dynamic_loader PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "$<TARGET_FILE_DIR:guest_module_fixture>" RESOURCE_LOCK guest_dynamic_loader_files) if(WIN32) set_tests_properties(guest_dynamic_loader PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index fdead458de..29765cb8e6 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -11,6 +11,7 @@ #include <memory> #include <limits> #include <mutex> +#include <set> #include <string> #include <stdexcept> #include <system_error> @@ -36,6 +37,29 @@ void Error(const char* message) { std::snprintf(loaderError.data(), loaderError.size(), "%s", message); pendingError = true; } +void LoadFailure(const std::filesystem::path& source, const std::filesystem::path& resolved, const std::string& reason) { + std::error_code status; + const auto message = resolved == source && std::filesystem::is_regular_file(source, status) + ? "dlopen: " + source.string() + " was not relinked: no " + source.filename().string() + ".guest.prx was found (" + reason + + "); relink its directory with the relinker's --module-dir <dir> option" + : "dlopen: cannot load " + resolved.string() + " (" + reason + ")"; + Error(message.c_str()); + static std::mutex reportedLock; + static std::set<std::filesystem::path> reported; + std::lock_guard lock(reportedLock); + if (reported.insert(resolved).second) std::fprintf(stderr, "[module] %s\n", message.c_str()); +} +#ifdef _WIN32 +HMODULE LoadWithoutErrorDialog(const std::filesystem::path& path) { + const auto mode = GetThreadErrorMode(); + SetThreadErrorMode(mode | SEM_FAILCRITICALERRORS, nullptr); + const auto loaded = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + const auto error = GetLastError(); + SetThreadErrorMode(mode, nullptr); + SetLastError(error); + return loaded; +} +#endif struct Module { void* native = nullptr; bool owned = true; @@ -179,9 +203,15 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { module->owned = false; } else { if (!*path) { Error("dlopen: empty module path"); return nullptr; } - const auto resolved = RelinkedModulePath_nid_no_patch(ResolvePath_nid_no_patch(path)); - module->native = LoadLibraryExW(resolved.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); - if (module->native) InitializeDeferredModule(static_cast<HMODULE>(module->native)); + const auto source = ResolvePath_nid_no_patch(path); + const auto resolved = RelinkedModulePath_nid_no_patch(source); + module->native = LoadWithoutErrorDialog(resolved); + if (!module->native) { + const auto error = GetLastError(); + LoadFailure(source, resolved, "Windows loader error " + std::to_string(error)); + return nullptr; + } + InitializeDeferredModule(static_cast<HMODULE>(module->native)); } if (!module->native) { char message[128]; @@ -189,11 +219,15 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { Error(message); return nullptr; } #else - const auto resolved = path ? RelinkedModulePath_nid_no_patch(ResolvePath_nid_no_patch(path)).string() : std::string{}; + const auto source = path ? ResolvePath_nid_no_patch(path) : std::filesystem::path{}; + const auto resolved = path ? RelinkedModulePath_nid_no_patch(source) : std::filesystem::path{}; const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); - if (!module->native) { Error(::dlerror()); return nullptr; } + if (!module->native) { + if (path) LoadFailure(source, resolved, ::dlerror()); else Error(::dlerror()); + return nullptr; + } #endif std::lock_guard lock(modulesMutex); const auto handle = nextHandle++; diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 8ea9fe7c93..1307264c64 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -3,9 +3,15 @@ #include <cstdlib> #include <cstdint> #include <cstdio> +#include <cstring> #include <filesystem> #include <fstream> -#ifndef _WIN32 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#else #include <dlfcn.h> #endif #include <stdexcept> @@ -37,6 +43,28 @@ static bool ThrowsRuntimeError(TFunction function) { } return false; } +static bool FailsAsUnrelinked(const std::filesystem::path& module) { + void* handle = dlopen_nid_postfix(module.string().c_str(), 2); + const char* error = dlerror_nid_postfix(); + return handle == nullptr && error != nullptr && std::strstr(error, module.filename().string().c_str()) != nullptr && + std::strstr(error, "--module-dir") != nullptr; +} +static void CheckUnrelinkedModule(const std::filesystem::path& module) { +#ifdef _WIN32 + const UINT processMode = SetErrorMode(0); + DWORD threadMode = 0; + const bool threadModeSet = SetThreadErrorMode(SEM_NOGPFAULTERRORBOX, &threadMode) != 0; +#endif + const bool first = FailsAsUnrelinked(module); + const bool again = FailsAsUnrelinked(module); +#ifdef _WIN32 + const DWORD threadModeAfter = GetThreadErrorMode(); + SetThreadErrorMode(threadMode, nullptr); + SetErrorMode(processMode); + Require(threadModeSet && threadModeAfter == SEM_NOGPFAULTERRORBOX); +#endif + Require(first && again); +} #ifdef _WIN32 static void CheckGlobalLookupOrder(const char* path) { const std::filesystem::path other = "anyps5-global-order-for-test.guest.prx"; @@ -108,7 +136,8 @@ int main(int argc, char** argv) { Require(dlerror_nid_postfix() != nullptr); Require(dlerror_nid_postfix() == nullptr); Require(dlopen_nid_postfix("anyps5-missing-module-for-test.prx", 2) == nullptr); - Require(dlerror_nid_postfix() != nullptr); + const char* missing = dlerror_nid_postfix(); + Require(missing != nullptr && std::strstr(missing, "--module-dir") == nullptr); void* mainSymbol = nullptr; const int mainResult = sceKernelDlsym(0, "GuestDefaultScopeOnly", &mainSymbol); std::fprintf(stderr, "sceKernelDlsym(0, GuestDefaultScopeOnly): result=0x%08x, symbol=%p\n", static_cast<unsigned>(mainResult), mainSymbol); @@ -155,10 +184,12 @@ int main(int argc, char** argv) { const std::filesystem::path guest = "anyps5-relinked-module-for-test.prx"; auto relinked = guest; relinked += ".guest.prx"; + std::filesystem::remove(relinked); { std::ofstream elf(guest, std::ios::binary); elf.write("\x7f" "ELF", 4); } + CheckUnrelinkedModule(guest); std::filesystem::copy_file(argv[1], relinked, std::filesystem::copy_options::overwrite_existing); void* redirected = dlopen_nid_postfix(guest.string().c_str(), 2); Require(redirected != nullptr); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa2bb8c9c..3c8671c2fb 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -172,7 +172,7 @@ Some applications also need their original `app0/sce_sys/param.json` at runtime. Resources can also be packed into `app0.zar` beside `app0/`, a [ZArchive](https://github.com/Exzap/ZArchive) whose root is the `app0/` directory. A file present in `app0/` is read from there and any other file from the archive; the executable, `sce_module/` and `sce_sys/` stay in `app0/`. The archive is read only through `sceKernelOpen`/`open`, the read, seek and stat calls on its descriptors and `sceKernelStat`; other calls on an archived path, listing a directory that the archive also contains and opening a file in it for writing fail as described in [technical debt](../dev/debt/libkernel.md). An archive that cannot be opened stops the title at its first file access. -Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). +Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). A module the title loads while it runs that was not relinked, such as one in a directory not given to `--module-dir`, is not loaded either: the title gets an error and continues, and the runtime prints `[module] dlopen: <path> was not relinked: ...` once for each file. Relink that module's directory with `--module-dir <dir>`. On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. From 4a67c165b6ce6dd2a02475d9ec098d14085d1946 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:31:24 +0000 Subject: [PATCH 1047/1360] fix(libc): match reported wide scanf edge cases --- core/libs/prx/libc/src/ScanningWide.cpp | 7 ++++--- core/libs/tests/GuestWideConversion.cpp | 6 ++++-- core/libs/tests/GuestWideStreamScan.cpp | 8 ++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 0b95de6e78..fbf15519cc 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -127,8 +127,9 @@ Fit FloatFit(std::string_view text) { if (digits == 0) return Fit::Invalid; if (Lower(text[index]) != (hex ? 'p' : 'e')) return Fit::Invalid; ++index; - if (index < text.size() && (text[index] == '+' || text[index] == '-')) ++index; - if (index == text.size()) return Fit::Prefix; + if (index == text.size()) return Fit::Valid; + if (text[index] == '+' || text[index] == '-') ++index; + if (index == text.size()) return Fit::Valid; for (; index < text.size(); ++index) { if (!IsDecimal(text[index])) return Fit::Invalid; } @@ -256,7 +257,7 @@ private: } Outcome Literal(char16_t expected) { - if (input.Peek() < 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Matching; if (input.Peek() != expected) return Outcome::Matching; input.Advance(); return Outcome::Done; diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 07b28e68ca..8fa0a926f9 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -139,13 +139,14 @@ static void CheckScan() { double real = 0; Require(swscanf_s_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "%lf"); - Require(swscanf_s_nid_postfix(u"1e+x", u"%lf", &real) == 0, "%lf incomplete exponent is a matching failure"); + Require(swscanf_s_nid_postfix(u"1e+", u"%lf", &real) == 1 && real == 1.0, "%lf converts the mantissa of an incomplete exponent"); long double extended = 0; Require(swscanf_s_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "%Lf"); float single = 0; Require(swscanf_s_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits<float>::infinity(), "%f infinity"); Require(swscanf_s_nid_postfix(u"a", u"b%d", &first) == 0, "literal mismatch is a matching failure"); + Require(swscanf_s_nid_postfix(u"", u"x") == 0, "literal at end of input returns zero"); Require(swscanf_s_nid_postfix(u"", u"%d", &first) == -1, "empty input is an input failure"); Require(swscanf_s_nid_postfix(u" ", u"%ls", text, std::size_t{6}) == -1, "white space only is an input failure"); Require(swscanf_s_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "input failure after a conversion"); @@ -203,13 +204,14 @@ static void CheckUnsizedScan() { double real = 0; Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); - Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + Require(swscanf_nid_postfix(u"1e+", u"%lf", &real) == 1 && real == 1.0, "swscanf %lf converts the mantissa of an incomplete exponent"); long double extended = 0; Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); float single = 0; Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits<float>::infinity(), "swscanf %f infinity"); Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"", u"x") == 0, "swscanf literal at end of input returns zero"); Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 0da21861b0..07bcf46372 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -101,6 +101,10 @@ static void CheckConsumption() { Require(fgetwc_nid_postfix(stream) == u'b', "the mismatching character is pushed back"); Close(stream); + stream = Open(""); + Require(ScanStream(stream, u"x") == 0, "a literal at end of input returns zero"); + Close(stream); + stream = Open("x"); Require(ScanStream(stream, u"%d", &value) == 0, "a letter is not an integer"); Require(fgetwc_nid_postfix(stream) == u'x', "the rejected letter is pushed back"); @@ -120,6 +124,10 @@ static void CheckConsumption() { Require(ScanStream(stream, u"%lf", &real) == 1 && real == 2.5, "a float"); Require(fgetwc_nid_postfix(stream) == u'x', "the character after the float"); Close(stream); + + stream = Open("1e+"); + Require(ScanStream(stream, u"%lf", &real) == 1 && real == 1.0, "an incomplete exponent converts its mantissa"); + Close(stream); } static void CheckWideText() { From 0d70810529be29070eea2f7808b957fabc7b9237 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:31:24 +0000 Subject: [PATCH 1048/1360] docs(libc): record remaining wide scanner evidence gap --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5c54a50a14..cadc475ea8 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -126,7 +126,7 @@ - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:<priority>] <message>`, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title +- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal at end of input returns the number of assignments, including zero, rather than `EOF`. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` reports an input failure for it. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds From e8c73627f6098ee20977e845ec03ffcf5200e403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Thu, 22 Oct 2026 20:11:40 +0200 Subject: [PATCH 1049/1360] fix(libc): keep %% at end of input an input failure in the wide scanner Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libc/src/ScanningWide.cpp | 1 + core/libs/tests/GuestWideConversion.cpp | 2 ++ core/libs/tests/GuestWideStreamScan.cpp | 4 ++++ docs/dev/debt/libc.md | 2 +- 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index fbf15519cc..759e475c90 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -304,6 +304,7 @@ private: if (*format == u'%') { ++format; SkipSpace(); + if (input.Peek() < 0) return Outcome::Input; return Literal(u'%'); } const Directive directive = Parse(); diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 8fa0a926f9..1ade68c23f 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -147,6 +147,7 @@ static void CheckScan() { Require(swscanf_s_nid_postfix(u"a", u"b%d", &first) == 0, "literal mismatch is a matching failure"); Require(swscanf_s_nid_postfix(u"", u"x") == 0, "literal at end of input returns zero"); + Require(swscanf_s_nid_postfix(u"", u"%%") == -1, "percent literal at end of input is an input failure"); Require(swscanf_s_nid_postfix(u"", u"%d", &first) == -1, "empty input is an input failure"); Require(swscanf_s_nid_postfix(u" ", u"%ls", text, std::size_t{6}) == -1, "white space only is an input failure"); Require(swscanf_s_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "input failure after a conversion"); @@ -212,6 +213,7 @@ static void CheckUnsizedScan() { Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); Require(swscanf_nid_postfix(u"", u"x") == 0, "swscanf literal at end of input returns zero"); + Require(swscanf_nid_postfix(u"", u"%%") == -1, "swscanf percent literal at end of input is an input failure"); Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 07bcf46372..632a989759 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -105,6 +105,10 @@ static void CheckConsumption() { Require(ScanStream(stream, u"x") == 0, "a literal at end of input returns zero"); Close(stream); + stream = Open(""); + Require(ScanStream(stream, u"%%") == -1, "a percent literal at end of input is an input failure"); + Close(stream); + stream = Open("x"); Require(ScanStream(stream, u"%d", &value) == 0, "a letter is not an integer"); Require(fgetwc_nid_postfix(stream) == u'x', "the rejected letter is pushed back"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index cadc475ea8..2be933d9b6 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -126,7 +126,7 @@ - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:<priority>] <message>`, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal at end of input returns the number of assignments, including zero, rather than `EOF`. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` reports an input failure for it. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title +- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal at end of input returns the number of assignments, including zero, rather than `EOF` as FreeBSD does, as reported from the console scanner in #8479; `%%` at end of input still returns `EOF`. A literal mismatch is a matching failure, where FreeBSD's `vfwscanf` reports an input failure. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` pushes back the `x` and converts the `0`. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds From e1605dda52513fa3f0d5266df0b3cfa27f521c4e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:03:32 +0200 Subject: [PATCH 1050/1360] fix(libkernel_sys): fail fchdir on a pipe or socket with EINVAL FreeBSD 11.4's sys_fchdir resolves the descriptor with getvnode, which fails with EINVAL for a descriptor that has no vnode; only a vnode that is not a directory gives ENOTDIR from change_dir. fchdir returned ENOTDIR for a pipe or socket. A descriptor that is neither a vnode nor a random device now gets EINVAL; the debt entry records the order of the three errors. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 3 +++ core/libs/tests/GuestKernelSys.cpp | 11 +++++++++++ docs/dev/debt/libkernel_sys.md | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a58f086ccd..e931ab41c4 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -21,6 +21,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -357,6 +359,7 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (path == nullptr && error == GuestEnotdir && !File::IsVnode(fd) && !File::IsRandomDevice(fd)) return static_cast<int>(Fail(GuestEinval)); if (path == nullptr) return static_cast<int>(Fail(error)); #ifndef _WIN32 if (::faccessat(fd, ".", X_OK, AT_EACCESS) != 0) return static_cast<int>(Fail(errno)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 1c0ce5c4f9..a1c3783fd8 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -32,6 +32,8 @@ int APS5_VABI fstatfs_nid_postfix(int, void*); int APS5_VABI _fstatfs_nid_postfix(int, void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); @@ -430,6 +432,15 @@ int main() { Require(fileDescriptor >= 0); Require(fchdir_nid_postfix(-1) == -1 && Error() == 9); Require(fchdir_nid_postfix(fileDescriptor) == -1 && Error() == 20); + int pipeEnds[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeEnds) == 0); + Require(fchdir_nid_postfix(pipeEnds[0]) == -1 && Error() == 22); + Require(fchdir_nid_postfix(pipeEnds[1]) == -1 && Error() == 22); + Require(close_nid_postfix(pipeEnds[0]) == 0 && close_nid_postfix(pipeEnds[1]) == 0); + const int socketDescriptor = socket_nid_postfix(2, 2, 0); + Require(socketDescriptor >= 0); + Require(fchdir_nid_postfix(socketDescriptor) == -1 && Error() == 22); + Require(close_nid_postfix(socketDescriptor) == 0); Require(fchdir_nid_postfix(directory) == 0); char directoryPath[1024]; Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 0d634c1296..b68bdbf99a 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -5,7 +5,7 @@ ### Functional -- [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified +- [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open, `EINVAL` for a pipe or socket, as FreeBSD `getvnode` does for a descriptor without a vnode, and `ENOTDIR` for any other descriptor that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` From e56c6d2ba8dc9b8003716a820d0c248697bde4f5 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:48:24 +0200 Subject: [PATCH 1051/1360] fix(libkernel_sys): keep ENOTDIR from fchdir on a character device The EINVAL for a descriptor without a vnode now applies only to a pipe or socket. A character device is a vnode on FreeBSD, so getvnode succeeds and change_dir fails with ENOTDIR; File::IsCharacterDevice keeps that answer for the host character devices (a tty, NUL or /dev/null) and the guest random device. guest_kernel_sys pins it with /dev/random. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../prx/libkernel/File/include/DescriptorControl.hpp | 1 + .../libs/prx/libkernel/File/src/DescriptorControl.cpp | 11 +++++++++++ core/libs/prx/libkernel_sys/Export.cpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 4 ++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 341324638c..662c64d319 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -17,6 +17,7 @@ void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); bool IsVnode(int fd); +bool IsCharacterDevice(int fd); } diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index a83f899801..e8367e7f88 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -3,6 +3,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/FileLock.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -583,6 +584,16 @@ bool Appends(int fd) { return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; } +bool IsCharacterDevice(int fd) { + if (fd < 0) return false; +#ifdef _WIN32 + return DescriptorIsOpen_nid_no_patch(fd) && !GuestSockets::IsOpen(fd) && !File::DirectoryDescriptorPath(fd) && ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_CHAR; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && S_ISCHR(status.st_mode); +#endif +} + bool IsVnode(int fd) { if (fd < 0 || File::IsRandomDevice(fd)) return false; if (File::IsArchivedFile(fd)) return true; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index e931ab41c4..7cf58ca59c 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -359,7 +359,7 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); - if (path == nullptr && error == GuestEnotdir && !File::IsVnode(fd) && !File::IsRandomDevice(fd)) return static_cast<int>(Fail(GuestEinval)); + if (path == nullptr && error == GuestEnotdir && !File::IsVnode(fd) && !File::IsCharacterDevice(fd) && !File::IsRandomDevice(fd)) return static_cast<int>(Fail(GuestEinval)); if (path == nullptr) return static_cast<int>(Fail(error)); #ifndef _WIN32 if (::faccessat(fd, ".", X_OK, AT_EACCESS) != 0) return static_cast<int>(Fail(errno)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a1c3783fd8..ee21e0d49a 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -437,6 +437,10 @@ int main() { Require(fchdir_nid_postfix(pipeEnds[0]) == -1 && Error() == 22); Require(fchdir_nid_postfix(pipeEnds[1]) == -1 && Error() == 22); Require(close_nid_postfix(pipeEnds[0]) == 0 && close_nid_postfix(pipeEnds[1]) == 0); + const int characterDevice = open_nid_postfix("/dev/random", 0, 0); + Require(characterDevice >= 0); + Require(fchdir_nid_postfix(characterDevice) == -1 && Error() == 20); + Require(close_nid_postfix(characterDevice) == 0); const int socketDescriptor = socket_nid_postfix(2, 2, 0); Require(socketDescriptor >= 0); Require(fchdir_nid_postfix(socketDescriptor) == -1 && Error() == 22); From 093a5ba1956b2f2ac4a1ec3f7bdd6ad529a664ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Thu, 22 Oct 2026 20:15:39 +0200 Subject: [PATCH 1052/1360] fix(libkernel_sys): reach the fchdir vnode check through a _nid_no_patch export Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libkernel/File/include/DescriptorControl.hpp | 2 ++ core/libs/prx/libkernel/File/src/DescriptorControl.cpp | 4 ++++ core/libs/prx/libkernel_sys/Export.cpp | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 662c64d319..4358f8e67d 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -21,4 +21,6 @@ bool IsCharacterDevice(int fd); } +extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd); + #endif diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index e8367e7f88..d9cf12e8bc 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -607,3 +607,7 @@ bool IsVnode(int fd) { } } + +extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd) { + return !File::IsVnode(fd) && !File::IsCharacterDevice(fd) && !File::IsRandomDevice(fd); +} diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 7cf58ca59c..526a3a63bb 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -22,7 +22,6 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" -#include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -359,7 +358,7 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); - if (path == nullptr && error == GuestEnotdir && !File::IsVnode(fd) && !File::IsCharacterDevice(fd) && !File::IsRandomDevice(fd)) return static_cast<int>(Fail(GuestEinval)); + if (path == nullptr && error == GuestEnotdir && DescriptorHasNoVnode_nid_no_patch(fd)) return static_cast<int>(Fail(GuestEinval)); if (path == nullptr) return static_cast<int>(Fail(error)); #ifndef _WIN32 if (::faccessat(fd, ".", X_OK, AT_EACCESS) != 0) return static_cast<int>(Fail(errno)); From be37cb907a4f769c8872d9f4ebdcc0055198edfc Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:03:07 +0100 Subject: [PATCH 1053/1360] feat(libSceJson2): declare 74 PS5 pending exports --- core/libs/prx/libSceJson2/CMakeLists.txt | 2 +- .../libs/prx/libSceJson2/InventoryExports.cpp | 452 ++++++++++++++++++ docs/dev/debt/libSceJson2.md | 77 +++ 3 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceJson2/InventoryExports.cpp diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index dbc704366f..cf4780cf80 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceJson2 SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceJson2 SHARED EXCLUDE_FROM_ALL Export.cpp InventoryExports.cpp) target_include_directories(libSceJson2 PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceJson2) diff --git a/core/libs/prx/libSceJson2/InventoryExports.cpp b/core/libs/prx/libSceJson2/InventoryExports.cpp new file mode 100644 index 0000000000..5c89743172 --- /dev/null +++ b/core/libs/prx/libSceJson2/InventoryExports.cpp @@ -0,0 +1,452 @@ +#include <cstddef> +#include <cstdint> +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceJson2Unknown_001_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json11Initializer24setAllocatorInfoCallBackEPFviNS0_9ValueTypeEPvES3_", sceJson2Unknown_001_nid_no_patch); + +int APS5_VABI sceJson2Unknown_002_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json11Initializer28setGlobalSpecialFloatHandlerEPFKNS0_5ValueENS0_12FunctionTypeEdPS3_PvPbES6_", sceJson2Unknown_002_nid_no_patch); + +int APS5_VABI sceJson2Unknown_003_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json11Initializer28setGlobalTypeMismatchHandlerEPFRKNS0_5ValueENS0_9ValueTypeEPS3_S6_PvES7_", sceJson2Unknown_003_nid_no_patch); + +int APS5_VABI sceJson2Unknown_004_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json11Initializer36setGlobalElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKNS0_5ValueES9_PvESA_", sceJson2Unknown_004_nid_no_patch); + +int APS5_VABI sceJson2Unknown_005_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json11InitializerC2Ev", sceJson2Unknown_005_nid_no_patch); + +int APS5_VABI sceJson2Unknown_006_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json11InitializerD2Ev", sceJson2Unknown_006_nid_no_patch); + +int APS5_VABI sceJson2Unknown_007_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json12MemAllocatorD0Ev", sceJson2Unknown_007_nid_no_patch); + +int APS5_VABI sceJson2Unknown_008_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json12MemAllocatorD1Ev", sceJson2Unknown_008_nid_no_patch); + +int APS5_VABI sceJson2Unknown_009_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json17InitParameterRttiC2EPNS0_14AllocParamRttiEPvm", sceJson2Unknown_009_nid_no_patch); + +int APS5_VABI sceJson2Unknown_010_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json19InternalInitializer19terminate_staticlibEv", sceJson2Unknown_010_nid_no_patch); + +int APS5_VABI sceJson2Unknown_011_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json19InternalInitializer20initialize_staticlibEPKNS0_13InitParameterENS1_10InitOptionE", sceJson2Unknown_011_nid_no_patch); + +int APS5_VABI sceJson2Unknown_012_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json19InternalInitializerC1Ev", sceJson2Unknown_012_nid_no_patch); + +int APS5_VABI sceJson2Unknown_013_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json19InternalInitializerC2Ev", sceJson2Unknown_013_nid_no_patch); + +int APS5_VABI sceJson2Unknown_014_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json19InternalInitializerD1Ev", sceJson2Unknown_014_nid_no_patch); + +int APS5_VABI sceJson2Unknown_015_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json19InternalInitializerD2Ev", sceJson2Unknown_015_nid_no_patch); + +int APS5_VABI sceJson2Unknown_016_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json4FreeEPv", sceJson2Unknown_016_nid_no_patch); + +int APS5_VABI sceJson2Unknown_017_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array10push_frontERKNS0_5ValueE", sceJson2Unknown_017_nid_no_patch); + +int APS5_VABI sceJson2Unknown_018_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array5eraseERKNS1_8iteratorE", sceJson2Unknown_018_nid_no_patch); + +int APS5_VABI sceJson2Unknown_019_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array6insertERKNS1_8iteratorERKNS0_5ValueE", sceJson2Unknown_019_nid_no_patch); + +int APS5_VABI sceJson2Unknown_020_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array8iteratorC2ERKS2_", sceJson2Unknown_020_nid_no_patch); + +int APS5_VABI sceJson2Unknown_021_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array8iteratorC2Ev", sceJson2Unknown_021_nid_no_patch); + +int APS5_VABI sceJson2Unknown_022_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array8iteratorD2Ev", sceJson2Unknown_022_nid_no_patch); + +int APS5_VABI sceJson2Unknown_023_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array8pop_backEv", sceJson2Unknown_023_nid_no_patch); + +int APS5_VABI sceJson2Unknown_024_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Array9pop_frontEv", sceJson2Unknown_024_nid_no_patch); + +int APS5_VABI sceJson2Unknown_025_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ArrayC2ERKS1_", sceJson2Unknown_025_nid_no_patch); + +int APS5_VABI sceJson2Unknown_026_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ArrayC2Ev", sceJson2Unknown_026_nid_no_patch); + +int APS5_VABI sceJson2Unknown_027_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ArrayD2Ev", sceJson2Unknown_027_nid_no_patch); + +int APS5_VABI sceJson2Unknown_028_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ArrayaSERKS1_", sceJson2Unknown_028_nid_no_patch); + +int APS5_VABI sceJson2Unknown_029_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value11referStringEv", sceJson2Unknown_029_nid_no_patch); + +int APS5_VABI sceJson2Unknown_030_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value12referBooleanEv", sceJson2Unknown_030_nid_no_patch); + +int APS5_VABI sceJson2Unknown_031_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value12referIntegerEv", sceJson2Unknown_031_nid_no_patch); + +int APS5_VABI sceJson2Unknown_032_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value13referUIntegerEv", sceJson2Unknown_032_nid_no_patch); + +int APS5_VABI sceJson2Unknown_033_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value9referRealEv", sceJson2Unknown_033_nid_no_patch); + +int APS5_VABI sceJson2Unknown_034_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value9serializeEPFiRNS0_6StringEPvES4_", sceJson2Unknown_034_nid_no_patch); + +int APS5_VABI sceJson2Unknown_035_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5Value9serializeERNS0_6StringEPFiS3_PvES4_", sceJson2Unknown_035_nid_no_patch); + +int APS5_VABI sceJson2Unknown_036_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2ENS0_9ValueTypeE", sceJson2Unknown_036_nid_no_patch); + +int APS5_VABI sceJson2Unknown_037_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2EPKc", sceJson2Unknown_037_nid_no_patch); + +int APS5_VABI sceJson2Unknown_038_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2ERKNS0_5ArrayE", sceJson2Unknown_038_nid_no_patch); + +int APS5_VABI sceJson2Unknown_039_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2ERKNS0_6ObjectE", sceJson2Unknown_039_nid_no_patch); + +int APS5_VABI sceJson2Unknown_040_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2ERKNS0_6StringE", sceJson2Unknown_040_nid_no_patch); + +int APS5_VABI sceJson2Unknown_041_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2ERKS1_", sceJson2Unknown_041_nid_no_patch); + +int APS5_VABI sceJson2Unknown_042_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2Eb", sceJson2Unknown_042_nid_no_patch); + +int APS5_VABI sceJson2Unknown_043_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2Ed", sceJson2Unknown_043_nid_no_patch); + +int APS5_VABI sceJson2Unknown_044_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2El", sceJson2Unknown_044_nid_no_patch); + +int APS5_VABI sceJson2Unknown_045_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2Em", sceJson2Unknown_045_nid_no_patch); + +int APS5_VABI sceJson2Unknown_046_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueC2Ev", sceJson2Unknown_046_nid_no_patch); + +int APS5_VABI sceJson2Unknown_047_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json5ValueD2Ev", sceJson2Unknown_047_nid_no_patch); + +int APS5_VABI sceJson2Unknown_048_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6MallocEm", sceJson2Unknown_048_nid_no_patch); + +int APS5_VABI sceJson2Unknown_049_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Object4PairC2ERKNS0_6StringERKNS0_5ValueE", sceJson2Unknown_049_nid_no_patch); + +int APS5_VABI sceJson2Unknown_050_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Object4PairC2Ev", sceJson2Unknown_050_nid_no_patch); + +int APS5_VABI sceJson2Unknown_051_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Object4PairD2Ev", sceJson2Unknown_051_nid_no_patch); + +int APS5_VABI sceJson2Unknown_052_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Object8iteratorC2ERKS2_", sceJson2Unknown_052_nid_no_patch); + +int APS5_VABI sceJson2Unknown_053_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Object8iteratorC2Ev", sceJson2Unknown_053_nid_no_patch); + +int APS5_VABI sceJson2Unknown_054_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Object8iteratorD2Ev", sceJson2Unknown_054_nid_no_patch); + +int APS5_VABI sceJson2Unknown_055_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6ObjectC2ERKS1_", sceJson2Unknown_055_nid_no_patch); + +int APS5_VABI sceJson2Unknown_056_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6ObjectC2Ev", sceJson2Unknown_056_nid_no_patch); + +int APS5_VABI sceJson2Unknown_057_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6ObjectD2Ev", sceJson2Unknown_057_nid_no_patch); + +int APS5_VABI sceJson2Unknown_058_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6Parser5parseERNS0_5ValueEPKc", sceJson2Unknown_058_nid_no_patch); + +int APS5_VABI sceJson2Unknown_059_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6StringC2EPKc", sceJson2Unknown_059_nid_no_patch); + +int APS5_VABI sceJson2Unknown_060_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6StringC2ERKS1_", sceJson2Unknown_060_nid_no_patch); + +int APS5_VABI sceJson2Unknown_061_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6StringC2Ev", sceJson2Unknown_061_nid_no_patch); + +int APS5_VABI sceJson2Unknown_062_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce4Json6StringD2Ev", sceJson2Unknown_062_nid_no_patch); + +int APS5_VABI sceJson2Unknown_063_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json14InitParameter211getUserDataEv", sceJson2Unknown_063_nid_no_patch); + +int APS5_VABI sceJson2Unknown_064_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json14InitParameter212getAllocatorEv", sceJson2Unknown_064_nid_no_patch); + +int APS5_VABI sceJson2Unknown_065_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json14InitParameter217getFileBufferSizeEv", sceJson2Unknown_065_nid_no_patch); + +int APS5_VABI sceJson2Unknown_066_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json14InitParameter225getSpecialFloatFormatTypeEv", sceJson2Unknown_066_nid_no_patch); + +int APS5_VABI sceJson2Unknown_067_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json5ValueixERKNS0_6StringE", sceJson2Unknown_067_nid_no_patch); + +int APS5_VABI sceJson2Unknown_068_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String2atEm", sceJson2Unknown_068_nid_no_patch); + +int APS5_VABI sceJson2Unknown_069_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String4findEPKcm", sceJson2Unknown_069_nid_no_patch); + +int APS5_VABI sceJson2Unknown_070_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String4findEPKcmm", sceJson2Unknown_070_nid_no_patch); + +int APS5_VABI sceJson2Unknown_071_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String4findERKS1_m", sceJson2Unknown_071_nid_no_patch); + +int APS5_VABI sceJson2Unknown_072_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String5rfindEPKcm", sceJson2Unknown_072_nid_no_patch); + +int APS5_VABI sceJson2Unknown_073_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String5rfindEPKcmm", sceJson2Unknown_073_nid_no_patch); + +int APS5_VABI sceJson2Unknown_074_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce4Json6String5rfindERKS1_m", sceJson2Unknown_074_nid_no_patch); + +} diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 706f8c4594..98aaf0b4f2 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -11,3 +11,80 @@ - [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - storage uses `std::string`; capacity and allocation growth do not match the system 32-bit capacity and allocator settings. Appending null text with a nonzero length throws instead of dereferencing it. - [sce::Json::Array::iterator::advance and Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return type of `advance` remains unverified. Object iterator advancement past the end throws instead of asserting. Dereferencing an empty array iterator throws instead of returning the system null-value pointer or an invalid address. - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. + +### Unknown 3.20 import declarations + +- [_ZN3sce4Json11Initializer24setAllocatorInfoCallBackEPFviNS0_9ValueTypeEPvES3_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `R996H0YIhiQ` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json11Initializer28setGlobalSpecialFloatHandlerEPFKNS0_5ValueENS0_12FunctionTypeEdPS3_PvPbES6_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `i1393UBWu1U` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json11Initializer28setGlobalTypeMismatchHandlerEPFRKNS0_5ValueENS0_9ValueTypeEPS3_S6_PvES7_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `tck9g-yOqPg` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json11Initializer36setGlobalElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKNS0_5ValueES9_PvESA_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `j8Q7SBgMm28` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json11InitializerC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `6qFqND4iwPA` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json11InitializerD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `qQZGdYkEytk` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json12MemAllocatorD0Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `ypazoTrKYhE` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json12MemAllocatorD1Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `O1uwoqazS2A` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json17InitParameterRttiC2EPNS0_14AllocParamRttiEPvm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Mp8DPb3M4I0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json19InternalInitializer19terminate_staticlibEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `dvp2zGSQzMs` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json19InternalInitializer20initialize_staticlibEPKNS0_13InitParameterENS1_10InitOptionE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `BPSPc3pUMJo` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json19InternalInitializerC1Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `WntRKhYTmZw` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json19InternalInitializerC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `2n+fxNdSZfs` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json19InternalInitializerD1Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `yudT4-bglko` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json19InternalInitializerD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `caP-y4fH-zQ` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json4FreeEPv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `bnk+edDbqMk` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array10push_frontERKNS0_5ValueE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `MQxDxBzWz40` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array5eraseERKNS1_8iteratorE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `2mbVAtshWRI` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array6insertERKNS1_8iteratorERKNS0_5ValueE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `ixJAz92H1uE` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array8iteratorC2ERKS2_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Hdc3DSIsG2I` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array8iteratorC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `JSg1MSo7NKE` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array8iteratorD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `9niNrXsF3Bc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array8pop_backEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `woec1toi4Xw` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Array9pop_frontEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `pPs3qZTZQvQ` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ArrayC2ERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `6NlbpyIfUuY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ArrayC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `t6AHhkWu4mY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ArrayD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `MIyMPw7YTGM` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ArrayaSERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `w5UbvPLGye0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value11referStringEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `m5J28iw-QPs` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value12referBooleanEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `QxjfcfBhEdc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value12referIntegerEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `R0ac5QOdlpo` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value13referUIntegerEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Nx5tez4siL4` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value9referRealEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `t7UEJrhojhk` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value9serializeEPFiRNS0_6StringEPvES4_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `PyMbL9F2oBY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5Value9serializeERNS0_6StringEPFiS3_PvES4_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `l1URiQJlSoU` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2ENS0_9ValueTypeE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `PWm9MyJJVqU` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2EPKc](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `+hHa4QYtLY8` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2ERKNS0_5ArrayE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `VH9dsKpQTmk` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2ERKNS0_6ObjectE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `JXlrCLLCLmc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2ERKNS0_6StringE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `veNviXt6wC0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2ERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `TZyuFeGuw9Y` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2Eb](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `OK4Ot0ue7J0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2Ed](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `23nm4oXHlfI` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2El](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `EjxUpzR2Yx8` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2Em](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `cqucBPSeVDA` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `-wa17B7TGnw` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json5ValueD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `0eUrW9JAxM0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6MallocEm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Xzy0onDzSAc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Object4PairC2ERKNS0_6StringERKNS0_5ValueE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `lt3J4dPvEtM` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Object4PairC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Wu-urs6MMpQ` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Object4PairD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `TM0Sj+Ebins` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Object8iteratorC2ERKS2_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `r5CShmcua+M` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Object8iteratorC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `OxO+V0ouoS4` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Object8iteratorD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `patTzc7zBjw` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6ObjectC2ERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `tSRqQ-+Y4PE` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6ObjectC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `SkKczWHdklc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6ObjectD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `1siRBursJjw` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6Parser5parseERNS0_5ValueEPKc](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `LB3jxppxyKU` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6StringC2EPKc](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `N78gkQzXRL4` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6StringC2ERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `gALFgNfF+9I` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6StringC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `eG9E9M6XvTM` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZN3sce4Json6StringD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Ui7YFnSTCBw` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json14InitParameter211getUserDataEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `GWtzpTKbZ+0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json14InitParameter212getAllocatorEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `BykPm1ajcM0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json14InitParameter217getFileBufferSizeEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Pcg6xQUhq9E` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json14InitParameter225getSpecialFloatFormatTypeEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `PDkib0zyABg` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json5ValueixERKNS0_6StringE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `clF7J7N9xXE` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String2atEm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `KrKM-5SdQrA` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String4findEPKcm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `yjsf6SvXA08` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String4findEPKcmm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `wwDnDDsQVc0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String4findERKS1_m](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `awkeNR6Ukp8` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String5rfindEPKcm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `VpQBXbaHXOo` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String5rfindEPKcmm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `A29RHrlfTM0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. +- [_ZNK3sce4Json6String5rfindERKS1_m](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `-R9mdXBspeQ` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. From c8febff30673b50d3b571aef5773ba8f8cae1041 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:52:07 +0100 Subject: [PATCH 1054/1360] feat(libSceNKWebKit): declare pending exports --- core/libs/prx/libSceNKWebKit/Export.cpp | 750 +++++++++++++++++++++++- docs/dev/debt/libSceNKWebKit.md | 5 + 2 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceNKWebKit.md diff --git a/core/libs/prx/libSceNKWebKit/Export.cpp b/core/libs/prx/libSceNKWebKit/Export.cpp index d81f4a5e32..a6caa403e8 100644 --- a/core/libs/prx/libSceNKWebKit/Export.cpp +++ b/core/libs/prx/libSceNKWebKit/Export.cpp @@ -5,6 +5,754 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceNKWebKitUnknown____URPJNZ18_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++-URPJNZ18", sceNKWebKitUnknown____URPJNZ18_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___67uYkhqBU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++67uYkhqBU", sceNKWebKitUnknown___67uYkhqBU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___8RYr2gjrw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++8RYr2gjrw", sceNKWebKitUnknown___8RYr2gjrw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___BntMcao1o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++BntMcao1o", sceNKWebKitUnknown___BntMcao1o_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___R8IXi10PI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++R8IXi10PI", sceNKWebKitUnknown___R8IXi10PI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___j2h_QO22w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++j2h+QO22w", sceNKWebKitUnknown___j2h_QO22w_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown____7XwmYLnI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+-+7XwmYLnI", sceNKWebKitUnknown____7XwmYLnI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___wmVyLhgm0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+-wmVyLhgm0", sceNKWebKitUnknown___wmVyLhgm0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0QEt8ZQWgg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0QEt8ZQWgg", sceNKWebKitUnknown__0QEt8ZQWgg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0YNxcA_duk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0YNxcA-duk", sceNKWebKitUnknown__0YNxcA_duk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0ZZzGYxx_I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0ZZzGYxx+I", sceNKWebKitUnknown__0ZZzGYxx_I_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0rdnkSicqk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0rdnkSicqk", sceNKWebKitUnknown__0rdnkSicqk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1Ex5GCP8Xs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1Ex5GCP8Xs", sceNKWebKitUnknown__1Ex5GCP8Xs_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1FfZ_0sxvQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1FfZ+0sxvQ", sceNKWebKitUnknown__1FfZ_0sxvQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1ObAtzCrYE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1ObAtzCrYE", sceNKWebKitUnknown__1ObAtzCrYE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1n_h4FUu9E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1n+h4FUu9E", sceNKWebKitUnknown__1n_h4FUu9E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__26aZpK535w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+26aZpK535w", sceNKWebKitUnknown__26aZpK535w_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__2XuBaAXb_4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+2XuBaAXb+4", sceNKWebKitUnknown__2XuBaAXb_4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__2rTX2KM7Ms_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+2rTX2KM7Ms", sceNKWebKitUnknown__2rTX2KM7Ms_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__31dbB3S_6Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+31dbB3S-6Y", sceNKWebKitUnknown__31dbB3S_6Y_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__3p6wRzzzlQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+3p6wRzzzlQ", sceNKWebKitUnknown__3p6wRzzzlQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__4_GNOUIBt0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+4-GNOUIBt0", sceNKWebKitUnknown__4_GNOUIBt0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__45BiDnCTxw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+45BiDnCTxw", sceNKWebKitUnknown__45BiDnCTxw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__4lVmXsIwqM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+4lVmXsIwqM", sceNKWebKitUnknown__4lVmXsIwqM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__5E2YZZIstM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+5E2YZZIstM", sceNKWebKitUnknown__5E2YZZIstM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__5V_UOk8zS4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+5V-UOk8zS4", sceNKWebKitUnknown__5V_UOk8zS4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__63IB3UdMOY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+63IB3UdMOY", sceNKWebKitUnknown__63IB3UdMOY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__6boXsT6XFA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+6boXsT6XFA", sceNKWebKitUnknown__6boXsT6XFA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__79w68212vg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+79w68212vg", sceNKWebKitUnknown__79w68212vg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__7HpqmfJz48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+7HpqmfJz48", sceNKWebKitUnknown__7HpqmfJz48_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__8T1y8Y_lgQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+8T1y8Y-lgQ", sceNKWebKitUnknown__8T1y8Y_lgQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__8VuTEFp13s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+8VuTEFp13s", sceNKWebKitUnknown__8VuTEFp13s_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9TKACifR2g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9TKACifR2g", sceNKWebKitUnknown__9TKACifR2g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9ZBRWNf4og_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9ZBRWNf4og", sceNKWebKitUnknown__9ZBRWNf4og_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9us0nrDaCU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9us0nrDaCU", sceNKWebKitUnknown__9us0nrDaCU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__AIEKk6K05M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+AIEKk6K05M", sceNKWebKitUnknown__AIEKk6K05M_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Abk7CfTivU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Abk7CfTivU", sceNKWebKitUnknown__Abk7CfTivU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__AnsPWo12m8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+AnsPWo12m8", sceNKWebKitUnknown__AnsPWo12m8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BDqCDede1o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BDqCDede1o", sceNKWebKitUnknown__BDqCDede1o_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BSUP6XSeAw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BSUP6XSeAw", sceNKWebKitUnknown__BSUP6XSeAw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BZCJi3OIiQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BZCJi3OIiQ", sceNKWebKitUnknown__BZCJi3OIiQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ByVxWl1pW8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ByVxWl1pW8", sceNKWebKitUnknown__ByVxWl1pW8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__CgSI_4D534_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+CgSI-4D534", sceNKWebKitUnknown__CgSI_4D534_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__CpPA9_CKUA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+CpPA9+CKUA", sceNKWebKitUnknown__CpPA9_CKUA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__DcUw5YwkSY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DcUw5YwkSY", sceNKWebKitUnknown__DcUw5YwkSY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__DpDBrSZWUc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DpDBrSZWUc", sceNKWebKitUnknown__DpDBrSZWUc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__E3D_W4kpl8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+E3D-W4kpl8", sceNKWebKitUnknown__E3D_W4kpl8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__EN6MgNmVv4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+EN6MgNmVv4", sceNKWebKitUnknown__EN6MgNmVv4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ErgockOJwc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ErgockOJwc", sceNKWebKitUnknown__ErgockOJwc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FKudG3uS8A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FKudG3uS8A", sceNKWebKitUnknown__FKudG3uS8A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FWUl_T8yG8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FWUl-T8yG8", sceNKWebKitUnknown__FWUl_T8yG8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Fca5Eyr4BU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Fca5Eyr4BU", sceNKWebKitUnknown__Fca5Eyr4BU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FcvpbbdrZM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FcvpbbdrZM", sceNKWebKitUnknown__FcvpbbdrZM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FvUhrQaiJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FvUhrQaiJQ", sceNKWebKitUnknown__FvUhrQaiJQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__GOwQvxqztE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+GOwQvxqztE", sceNKWebKitUnknown__GOwQvxqztE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Gp0M0sPj9I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Gp0M0sPj9I", sceNKWebKitUnknown__Gp0M0sPj9I_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__H_bnpXdWtk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+H-bnpXdWtk", sceNKWebKitUnknown__H_bnpXdWtk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__H5PTMieXNE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+H5PTMieXNE", sceNKWebKitUnknown__H5PTMieXNE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__HuPyGuX_jY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+HuPyGuX+jY", sceNKWebKitUnknown__HuPyGuX_jY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__IMGO_dXHis_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+IMGO+dXHis", sceNKWebKitUnknown__IMGO_dXHis_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ImJ026_DAw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ImJ026-DAw", sceNKWebKitUnknown__ImJ026_DAw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__IoqHgIw0PM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+IoqHgIw0PM", sceNKWebKitUnknown__IoqHgIw0PM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__JnngM7WyHI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+JnngM7WyHI", sceNKWebKitUnknown__JnngM7WyHI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Kb7j57jDzs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Kb7j57jDzs", sceNKWebKitUnknown__Kb7j57jDzs_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Kx_IV_21hk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Kx-IV+21hk", sceNKWebKitUnknown__Kx_IV_21hk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LUabdo811U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LUabdo811U", sceNKWebKitUnknown__LUabdo811U_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LiOmqon2ZI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LiOmqon2ZI", sceNKWebKitUnknown__LiOmqon2ZI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LoAenYdWlM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LoAenYdWlM", sceNKWebKitUnknown__LoAenYdWlM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LrmM3KN3N8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LrmM3KN3N8", sceNKWebKitUnknown__LrmM3KN3N8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__MzGFuNN348_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+MzGFuNN348", sceNKWebKitUnknown__MzGFuNN348_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__N_GwpCV4yk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+N-GwpCV4yk", sceNKWebKitUnknown__N_GwpCV4yk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__N6UhRaLrJE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+N6UhRaLrJE", sceNKWebKitUnknown__N6UhRaLrJE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NGrXdCa0q8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NGrXdCa0q8", sceNKWebKitUnknown__NGrXdCa0q8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NiLTGGKF_E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NiLTGGKF+E", sceNKWebKitUnknown__NiLTGGKF_E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Nqfy8HVG84_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Nqfy8HVG84", sceNKWebKitUnknown__Nqfy8HVG84_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NuhuZH75ak_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NuhuZH75ak", sceNKWebKitUnknown__NuhuZH75ak_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OCxbaKiBUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OCxbaKiBUo", sceNKWebKitUnknown__OCxbaKiBUo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OOjOkuWL6g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OOjOkuWL6g", sceNKWebKitUnknown__OOjOkuWL6g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OkTn3_PBxM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OkTn3+PBxM", sceNKWebKitUnknown__OkTn3_PBxM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__PDx3sfB_l4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+PDx3sfB-l4", sceNKWebKitUnknown__PDx3sfB_l4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__POsWAWjXo8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+POsWAWjXo8", sceNKWebKitUnknown__POsWAWjXo8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__PeIrQMgYpw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+PeIrQMgYpw", sceNKWebKitUnknown__PeIrQMgYpw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Q7xLvpc0PA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Q7xLvpc0PA", sceNKWebKitUnknown__Q7xLvpc0PA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__QL3zQ6ZNy4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+QL3zQ6ZNy4", sceNKWebKitUnknown__QL3zQ6ZNy4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__QqzEnc5c80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+QqzEnc5c80", sceNKWebKitUnknown__QqzEnc5c80_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__R0GXLrsPM0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+R0GXLrsPM0", sceNKWebKitUnknown__R0GXLrsPM0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__R2eXISeSvw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+R2eXISeSvw", sceNKWebKitUnknown__R2eXISeSvw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__RkwSDm5kGU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RkwSDm5kGU", sceNKWebKitUnknown__RkwSDm5kGU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__RtBREW92M0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RtBREW92M0", sceNKWebKitUnknown__RtBREW92M0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__S72K_fa2_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+S72K+fa2-8", sceNKWebKitUnknown__S72K_fa2_8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__S7ftAAWhoQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+S7ftAAWhoQ", sceNKWebKitUnknown__S7ftAAWhoQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__SHzgUiuQDE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+SHzgUiuQDE", sceNKWebKitUnknown__SHzgUiuQDE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Sgy63JgEgQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Sgy63JgEgQ", sceNKWebKitUnknown__Sgy63JgEgQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Sozphy1Wa8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Sozphy1Wa8", sceNKWebKitUnknown__Sozphy1Wa8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TPntL1OK80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TPntL1OK80", sceNKWebKitUnknown__TPntL1OK80_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TT_AplhQWA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TT+AplhQWA", sceNKWebKitUnknown__TT_AplhQWA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TjNmuhxdBQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TjNmuhxdBQ", sceNKWebKitUnknown__TjNmuhxdBQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__U_nxwdrnDY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+U-nxwdrnDY", sceNKWebKitUnknown__U_nxwdrnDY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__UKI_MUuiCU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+UKI-MUuiCU", sceNKWebKitUnknown__UKI_MUuiCU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__VvktcfIouQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+VvktcfIouQ", sceNKWebKitUnknown__VvktcfIouQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__WS2yRv5aBI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+WS2yRv5aBI", sceNKWebKitUnknown__WS2yRv5aBI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__XZfnHByVO0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+XZfnHByVO0", sceNKWebKitUnknown__XZfnHByVO0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YBfWz5udq4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YBfWz5udq4", sceNKWebKitUnknown__YBfWz5udq4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YPwmEWTZwE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YPwmEWTZwE", sceNKWebKitUnknown__YPwmEWTZwE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YYEOsi3IMk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YYEOsi3IMk", sceNKWebKitUnknown__YYEOsi3IMk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YyqQTUIQJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YyqQTUIQJQ", sceNKWebKitUnknown__YyqQTUIQJQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ZgGbFykhls_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ZgGbFykhls", sceNKWebKitUnknown__ZgGbFykhls_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ZwjLJ74Y38_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ZwjLJ74Y38", sceNKWebKitUnknown__ZwjLJ74Y38_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__b1xwU7aUwU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+b1xwU7aUwU", sceNKWebKitUnknown__b1xwU7aUwU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__b5TfVQ3_4s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+b5TfVQ3-4s", sceNKWebKitUnknown__b5TfVQ3_4s_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__bDb7iby5NI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bDb7iby5NI", sceNKWebKitUnknown__bDb7iby5NI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__baqz7ahOng_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+baqz7ahOng", sceNKWebKitUnknown__baqz7ahOng_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__bvtn3pCtek_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bvtn3pCtek", sceNKWebKitUnknown__bvtn3pCtek_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__byORTSkWQo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+byORTSkWQo", sceNKWebKitUnknown__byORTSkWQo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__dAwquupevQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+dAwquupevQ", sceNKWebKitUnknown__dAwquupevQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__dTlroLZao8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+dTlroLZao8", sceNKWebKitUnknown__dTlroLZao8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__eVN0UdRT8c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+eVN0UdRT8c", sceNKWebKitUnknown__eVN0UdRT8c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__fTKZiGqo8E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+fTKZiGqo8E", sceNKWebKitUnknown__fTKZiGqo8E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__feeFP8Y2_c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+feeFP8Y2+c", sceNKWebKitUnknown__feeFP8Y2_c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__iLBOBrdx5A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+iLBOBrdx5A", sceNKWebKitUnknown__iLBOBrdx5A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__iLdIVuhDhY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+iLdIVuhDhY", sceNKWebKitUnknown__iLdIVuhDhY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__jG4wqqA8yg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jG4wqqA8yg", sceNKWebKitUnknown__jG4wqqA8yg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__jLvgfDA97c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jLvgfDA97c", sceNKWebKitUnknown__jLvgfDA97c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ji7IBGwnCM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ji7IBGwnCM", sceNKWebKitUnknown__ji7IBGwnCM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__k3JGU_UUXo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+k3JGU-UUXo", sceNKWebKitUnknown__k3JGU_UUXo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__kX_CdbguB0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+kX-CdbguB0", sceNKWebKitUnknown__kX_CdbguB0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__keSg8ay8cE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+keSg8ay8cE", sceNKWebKitUnknown__keSg8ay8cE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__lI9zCCS_HQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+lI9zCCS-HQ", sceNKWebKitUnknown__lI9zCCS_HQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__lwLqZJLo3g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+lwLqZJLo3g", sceNKWebKitUnknown__lwLqZJLo3g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__mS029ncrbo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+mS029ncrbo", sceNKWebKitUnknown__mS029ncrbo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__mYy22Ucg7Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+mYy22Ucg7Y", sceNKWebKitUnknown__mYy22Ucg7Y_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nHWcLWOmb4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nHWcLWOmb4", sceNKWebKitUnknown__nHWcLWOmb4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nSg5kHtXkg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nSg5kHtXkg", sceNKWebKitUnknown__nSg5kHtXkg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nc3IouJMEA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nc3IouJMEA", sceNKWebKitUnknown__nc3IouJMEA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ndmq8LhAjA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ndmq8LhAjA", sceNKWebKitUnknown__ndmq8LhAjA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nrEMsyYHiw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nrEMsyYHiw", sceNKWebKitUnknown__nrEMsyYHiw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__oJHrfcyJJM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+oJHrfcyJJM", sceNKWebKitUnknown__oJHrfcyJJM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ovAafdG0O8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ovAafdG0O8", sceNKWebKitUnknown__ovAafdG0O8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__pWGccNXto8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+pWGccNXto8", sceNKWebKitUnknown__pWGccNXto8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__q0GELad_mY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+q0GELad-mY", sceNKWebKitUnknown__q0GELad_mY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__qXuNlEFSRQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+qXuNlEFSRQ", sceNKWebKitUnknown__qXuNlEFSRQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__qsTbJFS86A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+qsTbJFS86A", sceNKWebKitUnknown__qsTbJFS86A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r_TJDA2yIM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r+TJDA2yIM", sceNKWebKitUnknown__r_TJDA2yIM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r0UH41I1dI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r0UH41I1dI", sceNKWebKitUnknown__r0UH41I1dI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r8OGkuL4dc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r8OGkuL4dc", sceNKWebKitUnknown__r8OGkuL4dc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__rFA8ZR_qaI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+rFA8ZR+qaI", sceNKWebKitUnknown__rFA8ZR_qaI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__rPvFDsPbXM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+rPvFDsPbXM", sceNKWebKitUnknown__rPvFDsPbXM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__sUvE9LF80k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+sUvE9LF80k", sceNKWebKitUnknown__sUvE9LF80k_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__suuwfNT5Oc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+suuwfNT5Oc", sceNKWebKitUnknown__suuwfNT5Oc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__t_MaTLADe4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+t-MaTLADe4", sceNKWebKitUnknown__t_MaTLADe4_nid_no_patch); } diff --git a/docs/dev/debt/libSceNKWebKit.md b/docs/dev/debt/libSceNKWebKit.md new file mode 100644 index 0000000000..9e723c646b --- /dev/null +++ b/docs/dev/debt/libSceNKWebKit.md @@ -0,0 +1,5 @@ +# libSceNKWebKit technical debt + +### Unknown function info + +- The 150 `sceNKWebKitUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceNKWebKit/Export.cpp) are present in the PS5 3.20 `libSceNKWebKit` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from exported mangled symbols. Prototypes and behavior are unverified; each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. These declarations make no ABI or behavior claim. From 36a7a09dae4605d0fb8e84cdad2a5591e2d68c4a Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:49:17 +0100 Subject: [PATCH 1055/1360] feat(libSceAgcDriver): implement diagnostic fallbacks --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 10 ++++++++++ core/libs/prx/libSceAgcDriver/Unimplemented.cpp | 8 +++----- .../libSceAgcDriver/tests/UnsupportedDiagnostics.cpp | 10 ++++++++++ docs/dev/debt/libSceAgcDriver.md | 4 +++- 4 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6525b1a42e..007eff4f1e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -433,6 +433,16 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() if(BUILD_TESTING) + add_test_executable(agc_driver_unsupported_diagnostics_tests tests/UnsupportedDiagnostics.cpp) + target_include_directories(agc_driver_unsupported_diagnostics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_unsupported_diagnostics_tests PRIVATE libSceAgcDriver) + configure_windows_unwind(agc_driver_unsupported_diagnostics_tests) + add_test(NAME agc_driver_unsupported_diagnostics COMMAND agc_driver_unsupported_diagnostics_tests) + if(WIN32) + set_tests_properties(agc_driver_unsupported_diagnostics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAgcDriver>") + endif() + add_test_executable(agc_driver_draw_program_trace_tests tests/DrawProgramTrace.cpp Execution/src/Driver/Draw/DrawProgramTrace.cpp diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index be639faf70..404c324f30 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -154,9 +154,8 @@ int APS5_VABI sceAgcDriverAcquireHDRScopesACQ() { return 0; } -int APS5_VABI sceAgcDriverGetCoredumpAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceAgcDriverGetCoredumpAddress() { + return nullptr; } int APS5_VABI sceAgcDriverHp3dLockEnd() { @@ -185,8 +184,7 @@ int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() { } int APS5_VABI sceAgcDriverSetTargetRingForDiag() { - NotImplemented_nid_no_patch(__func__); - return 0; + return DRIVER_FEATURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverSetValueToSharedGds() { diff --git a/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp new file mode 100644 index 0000000000..db2f77f1ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp @@ -0,0 +1,10 @@ +#include "SceTypes.hpp" + +extern "C" void* APS5_VABI sceAgcDriverGetCoredumpAddress(); +extern "C" int APS5_VABI sceAgcDriverSetTargetRingForDiag(); + +int main() { + if (sceAgcDriverGetCoredumpAddress() != nullptr) return 1; + if (sceAgcDriverSetTargetRingForDiag() != static_cast<int>(0x8A6D0001)) return 2; + return 0; +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 9fc09de1a9..d9064b3932 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -11,7 +11,9 @@ - [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured -- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverGetCoredumpAddress, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetTargetRingForDiag, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverGetCoredumpAddress](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns the driver's shared coredump-area pointer, which is null when no coredump area is allocated. AnyPS5 reports coredump mode disabled and does not allocate this area; the export returns null. Enabling and populating an AGC coredump area is not implemented. +- [sceAgcDriverSetTargetRingForDiag](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns `0x8A6D0001` (`DRIVER_FEATURE_UNAVAILABLE`), matching the retail PS5 export. The retail diagnostic log message is not reproduced. - [Initial point context](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - new queues and CLEAR_STATE seed PA_SU_POINT_SIZE 0x00080008 and PA_SU_POINT_MINMAX 0xffff0000, matching every public context table in libSceAgc/Command/include/RegisterDefaultsData.hpp. Cocoon (PPSA08766) reaches geometry point output without writing these registers in the observed startup. Applying the SDK context defaults to initial/CLEAR_STATE driver context is inferred; GPU power-on reset state is not measured and is not the source of these values. Explicit writes and pushed/restored contexts retain their own values. From 1a1bb75b1e8362932d43d41539871d576667b700 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:38:25 +0100 Subject: [PATCH 1056/1360] feat(libSceAgcDriver): build NOP data-write packets --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++ .../prx/libSceAgcDriver/Unimplemented.cpp | 16 +++++--- .../tests/NopDataWritePacket.cpp | 38 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6525b1a42e..cba05219e5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -459,6 +459,12 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(agc_driver_nop_data_packet_tests tests/NopDataWritePacket.cpp) + target_include_directories(agc_driver_nop_data_packet_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_nop_data_packet_tests PRIVATE libSceAgcDriver) + configure_windows_unwind(agc_driver_nop_data_packet_tests) + add_test(NAME agc_driver_nop_data_packet COMMAND agc_driver_nop_data_packet_tests) + add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp) target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_frame_dump_io_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index be639faf70..aad0e65475 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -57,6 +57,17 @@ int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) { return 0; } +int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes) { + const auto alignedBytes = (static_cast<std::uint64_t>(bytes) + 3) & ~std::uint64_t{3}; + const auto totalBytes = alignedBytes + 8; + std::memset(destination, 0, static_cast<std::size_t>(totalBytes)); + const auto header = (static_cast<std::uint32_t>(totalBytes) << 14) + 0x3ffe0000U | 0xc0001000U; + std::memcpy(destination, &header, sizeof(header)); + std::memcpy(static_cast<std::byte*>(destination) + 4, &address, sizeof(address)); + if (data != nullptr && bytes != 0) std::memcpy(static_cast<std::byte*>(destination) + 8, data, bytes); + return data == nullptr || bytes == 0 ? 2 : static_cast<int>(totalBytes / 4); +} + int APS5_VABI sceAgcDriverSetValidationDisabled() { return DEBUG_UNAVAILABLE; @@ -174,11 +185,6 @@ int APS5_VABI sceAgcDriverIsEsvmProcess() { return 0; } -int APS5_VABI sceAgcDriverNopDataWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp new file mode 100644 index 0000000000..587bc082bc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp @@ -0,0 +1,38 @@ +#include "SceTypes.hpp" +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstring> + +extern "C" int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes); + +int main() { + std::array<std::byte, 24> storage; + storage.fill(std::byte{0xaa}); + const std::array<std::byte, 5> data{std::byte{0x11}, std::byte{0x22}, std::byte{0x33}, std::byte{0x44}, std::byte{0x55}}; + const auto words = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x12345678, data.data(), static_cast<std::uint32_t>(data.size())); + std::uint32_t header = 0; + std::uint32_t address = 0; + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (words != 4 || header != 0xc0021000U || address != 0x12345678U) return 1; + if (!std::equal(data.begin(), data.end(), storage.begin() + 12)) return 2; + if (storage[17] != std::byte{0} || storage[18] != std::byte{0} || storage[19] != std::byte{0}) return 3; + if (storage[3] != std::byte{0xaa} || storage[20] != std::byte{0xaa}) return 4; + + storage.fill(std::byte{0xaa}); + const auto emptyWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x87654321, nullptr, 5); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (emptyWords != 2 || header != 0xc0021000U || address != 0x87654321U) return 5; + if (storage[12] != std::byte{0} || storage[19] != std::byte{0} || storage[20] != std::byte{0xaa}) return 6; + + storage.fill(std::byte{0xaa}); + const auto zeroBytesWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x01020304, data.data(), 0); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (zeroBytesWords != 2 || header != 0xc0001000U || address != 0x01020304U) return 7; + if (storage[12] != std::byte{0xaa}) return 8; + return 0; +} From 9b40e1e9b61280aa393b88a3350139cec86f84aa Mon Sep 17 00:00:00 2001 From: Braxton Atkinson <grcfpbnnzg@privaterelay.appleid.com> Date: Wed, 21 Oct 2026 12:41:11 -0500 Subject: [PATCH 1057/1360] fix(libs): keep byte case comparisons in the guest C locale --- core/libs/prx/libScePosixForWebKit/Export.cpp | 4 +- core/libs/prx/libc/src/Strings.cpp | 10 +-- core/libs/tests/GuestFnmatch.cpp | 71 +++++++++++++++++++ docs/dev/debt/libScePosixForWebKit.md | 2 + docs/dev/debt/libc.md | 2 + 5 files changed, 83 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index ca47a2ecf7..fec1b01c2e 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -161,8 +161,8 @@ char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { const char* candidate = text; const char* match = needle; while (*candidate != '\0' && *match != '\0' && - std::tolower(static_cast<unsigned char>(*candidate)) == - std::tolower(static_cast<unsigned char>(*match))) { + FoldCase(static_cast<unsigned char>(*candidate), FnmCasefold) == + FoldCase(static_cast<unsigned char>(*match), FnmCasefold)) { ++candidate; ++match; } diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index b3d173267a..cc906bfddd 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -421,18 +421,20 @@ char16_t* APS5_VABI wmemmove_nid_postfix(char16_t* dest, const char16_t* src, si extern "C" { +int APS5_VABI tolower_nid_postfix(int); + int APS5_VABI strcasecmp_nid_postfix(const char* s1, const char* s2) { for (;; ++s1, ++s2) { - const int a = std::tolower(static_cast<unsigned char>(*s1)); - const int b = std::tolower(static_cast<unsigned char>(*s2)); + const int a = tolower_nid_postfix(static_cast<unsigned char>(*s1)); + const int b = tolower_nid_postfix(static_cast<unsigned char>(*s2)); if (a != b || *s1 == 0) return a - b; } } int APS5_VABI strncasecmp_nid_postfix(const char* s1, const char* s2, size_t n) { for (; n != 0; ++s1, ++s2, --n) { - const int a = std::tolower(static_cast<unsigned char>(*s1)); - const int b = std::tolower(static_cast<unsigned char>(*s2)); + const int a = tolower_nid_postfix(static_cast<unsigned char>(*s1)); + const int b = tolower_nid_postfix(static_cast<unsigned char>(*s2)); if (a != b) return a - b; if (*s1 == 0) break; } diff --git a/core/libs/tests/GuestFnmatch.cpp b/core/libs/tests/GuestFnmatch.cpp index 32b4e5b16e..fbac3b5ae8 100644 --- a/core/libs/tests/GuestFnmatch.cpp +++ b/core/libs/tests/GuestFnmatch.cpp @@ -2,14 +2,85 @@ #include <cstdio> #include <cstdlib> #include <stdexcept> +#include <clocale> +#include <cctype> +#include <string> extern "C" int APS5_VABI fnmatch_nid_postfix(const char*, const char*, int); +extern "C" int APS5_VABI strcasecmp_nid_postfix(const char*, const char*); +extern "C" int APS5_VABI strncasecmp_nid_postfix(const char*, const char*, std::size_t); +extern "C" char* APS5_VABI strcasestr_nid_postfix(const char*, const char*); struct Case { const char* pattern; const char* string; int flags; int result; }; +static void Require(bool value) { + if (!value) { + std::fputs("Guest byte case check failed\n", stderr); + std::abort(); + } +} + +static int FoldCByte(int byte) { + return byte >= 'A' && byte <= 'Z' ? byte + 'a' - 'A' : byte; +} + +static void CheckCaseInsensitiveBytePairs() { + for (int a = 0; a < 256; ++a) { + for (int b = 0; b < 256; ++b) { + char left[] = {static_cast<char>(a), 0}; + char right[] = {static_cast<char>(b), 0}; + const int expected = FoldCByte(a) - FoldCByte(b); + Require(strcasecmp_nid_postfix(left, right) == expected); + Require(strncasecmp_nid_postfix(left, right, 0) == 0); + Require(strncasecmp_nid_postfix(left, right, 1) == expected); + Require(strncasecmp_nid_postfix(left, right, 2) == expected); + const char* match = b == 0 || expected == 0 ? left : nullptr; + Require(strcasestr_nid_postfix(left, right) == match); + const char pattern[] = {'\\', static_cast<char>(a), 0}; + Require(fnmatch_nid_postfix(a == 0 ? pattern + 1 : pattern, right, 0x10) == (expected == 0 ? 0 : 1)); + Require(static_cast<unsigned char>(left[0]) == a && left[1] == 0); + Require(static_cast<unsigned char>(right[0]) == b && right[1] == 0); + } + } + const char left = 'A', right = 'a'; + Require(strncasecmp_nid_postfix(&left, &right, 0) == 0); + Require(strncasecmp_nid_postfix(&left, &right, 1) == 0); + const char repeated[] = "AaAaBAaAaC"; + Require(strcasestr_nid_postfix(repeated, "aaac") == repeated + 6); + Require(strcasestr_nid_postfix(repeated, "aAa") == repeated); + Require(strcasestr_nid_postfix(repeated, "aaad") == nullptr); + Require(strcasecmp_nid_postfix("AbC", "aBcD") == -'d'); + Require(strncasecmp_nid_postfix("AbC", "aBcD", 3) == 0); + Require(strncasecmp_nid_postfix("AbC", "aBcD", 4) == -'d'); +} + +static void CheckCaseInsensitiveHostLocale() { + const char* original = std::setlocale(LC_CTYPE, nullptr); + Require(original != nullptr); + struct RestoreLocale { + std::string name; + ~RestoreLocale() { Require(std::setlocale(LC_CTYPE, name.c_str()) != nullptr); } + } restore{original}; + Require(std::setlocale(LC_CTYPE, "C") != nullptr); + CheckCaseInsensitiveBytePairs(); + bool exercised = false; + for (const char* name : {".1252", "English_United States.1252", "en_US.ISO8859-1", "de_DE.ISO8859-1", + "en_US.iso88591", "tr_TR.ISO8859-9", "Turkish_Turkey.1254", "tr_TR.UTF-8"}) { + if (!std::setlocale(LC_CTYPE, name)) continue; + bool changed = false; + for (int byte = 0; byte < 256; ++byte) changed |= std::tolower(byte) != FoldCByte(byte); + if (!changed) continue; + std::fprintf(stderr, "Checking guest byte comparisons with host LC_CTYPE=%s\n", name); + CheckCaseInsensitiveBytePairs(); + exercised = true; + } + if (!exercised) std::fputs("No installed locale changes host byte case folding; host-locale regression skipped\n", stderr); +} + int main() { + CheckCaseInsensitiveHostLocale(); static const Case cases[] = { {"*.txt", "notes.txt", 0x00, 0}, {"*.txt", "notes.TXT", 0x00, 1}, diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index 681a24a3d5..d6dbc848b6 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -7,6 +7,8 @@ ### Functional +- [strcasestr](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - console locale behavior is unmeasured. It follows FreeBSD 11.4's [strcasestr.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasestr.c) in the guest's supported C locale; other guest locales are not implemented. + - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..43a8870660 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -4,6 +4,8 @@ - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. +- [strcasecmp and strncasecmp](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - console locale behavior is unmeasured. They use the guest's C-locale case conversion and the folded unsigned-byte difference of FreeBSD 11.4's [strcasecmp.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasecmp.c); other guest locales are not implemented. + - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. From ada8c0b9249797a8a704e74f26b194075b126f40 Mon Sep 17 00:00:00 2001 From: Braxton Atkinson <grcfpbnnzg@privaterelay.appleid.com> Date: Wed, 21 Oct 2026 19:58:25 -0500 Subject: [PATCH 1058/1360] fix(png): mask low-bit grayscale transparency keys --- core/Decoder/Png/src/Png.cpp | 33 +++++++ core/Decoder/Png/tests/Png.cpp | 172 +++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/core/Decoder/Png/src/Png.cpp b/core/Decoder/Png/src/Png.cpp index 434a589d6c..8fb90270e9 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -48,6 +48,37 @@ bool isChunkType(const std::uint8_t* bytes, const char* type) { return std::equal(bytes, bytes + 4, type); } +std::optional<std::uint16_t> readGrayscaleTransparencyKey(std::span<const std::uint8_t> png) { + std::size_t offset = SIGNATURE.size(); + if (png.size() < offset || !std::equal(SIGNATURE.begin(), SIGNATURE.end(), png.begin())) return std::nullopt; + std::optional<std::uint16_t> key; + while (png.size() - offset >= CHUNK_OVERHEAD) { + const std::uint32_t length = readBigEndian32(&png[offset]); + const std::uint8_t* type = &png[offset + 4]; + if (length > png.size() - offset - CHUNK_OVERHEAD) return std::nullopt; + if (isChunkType(type, "tRNS")) { + if (length != 2) return std::nullopt; + key = static_cast<std::uint16_t>(png[offset + 8] << 8 | png[offset + 9]); + } + if (isChunkType(type, "IDAT") || isChunkType(type, "IEND")) return key; + offset += CHUNK_OVERHEAD + length; + } + return std::nullopt; +} + +void repairGrayscaleTransparency(const Header& header, std::span<const std::uint8_t> png, Image& image) { + if (header.colorType != ColorType::Grayscale || !header.hasTransparency) return; + if (header.bitDepth != 1 && header.bitDepth != 2 && header.bitDepth != 4) return; + const std::optional<std::uint16_t> key = readGrayscaleTransparencyKey(png); + if (!key.has_value()) return; + const std::uint16_t mask = static_cast<std::uint16_t>((1u << header.bitDepth) - 1); + if ((*key & 0xFF) == (*key & mask)) return; + const std::uint8_t transparent = static_cast<std::uint8_t>((*key & mask) * (255u / mask)); + for (std::size_t offset = 3; offset < image.pixels.size(); offset += 4) { + image.pixels[offset] = static_cast<std::uint8_t>(image.pixels[offset - 3] == transparent ? 0 : 255); + } +} + bool isValidFormat(std::uint8_t bitDepth, std::uint8_t colorType) { switch (colorType) { case 0: @@ -158,6 +189,8 @@ std::optional<Image> Decode(std::span<const std::uint8_t> png) { Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), {}}; image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * 4); + const std::optional<Header> header = ParseHeader(png); + if (header.has_value()) repairGrayscaleTransparency(*header, png, image); return image; } diff --git a/core/Decoder/Png/tests/Png.cpp b/core/Decoder/Png/tests/Png.cpp index 0c1ac81d1e..fbd18d353f 100644 --- a/core/Decoder/Png/tests/Png.cpp +++ b/core/Decoder/Png/tests/Png.cpp @@ -3,6 +3,7 @@ #include <algorithm> #include <cstdint> #include <cstdlib> +#include <optional> #include <stdexcept> #include <vector> @@ -50,6 +51,17 @@ static const std::uint8_t GRAY1[] = { 0x42, 0x60, 0x82, }; +static const std::uint8_t GRAY1_TRNS[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x59, 0x42, + 0x27, 0x00, 0x00, 0x00, 0x02, 0x74, 0x52, 0x4E, 0x53, 0x00, 0x02, 0x98, 0x9D, 0xAC, 0x14, 0x00, + 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0x63, 0x70, 0x00, 0x00, 0x00, 0x42, 0x00, + 0x41, 0x84, 0xBF, 0x8E, 0x62, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, + 0x82, +}; + +static const std::uint8_t PNG_SIGNATURE[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; + static void AppendBigEndian32(std::vector<std::uint8_t>& bytes, std::uint32_t value) { for (int shift = 24; shift >= 0; shift -= 8) bytes.push_back(static_cast<std::uint8_t>(value >> shift)); } @@ -64,6 +76,130 @@ static std::vector<std::uint8_t> HeaderOnly(std::uint32_t width, std::uint32_t h return bytes; } +static std::uint32_t Crc32(std::span<const std::uint8_t> bytes) { + std::uint32_t crc = 0xFFFFFFFFu; + for (const std::uint8_t byte : bytes) { + crc ^= byte; + for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1) != 0 ? 0xEDB88320u : 0u); + } + return ~crc; +} + +static std::uint32_t Adler32(std::span<const std::uint8_t> bytes) { + std::uint32_t low = 1; + std::uint32_t high = 0; + for (const std::uint8_t byte : bytes) { + low = (low + byte) % 65521; + high = (high + low) % 65521; + } + return high << 16 | low; +} + +static void AppendTestChunk(std::vector<std::uint8_t>& png, const char* type, std::span<const std::uint8_t> data) { + AppendBigEndian32(png, static_cast<std::uint32_t>(data.size())); + const std::size_t start = png.size(); + png.insert(png.end(), type, type + 4); + png.insert(png.end(), data.begin(), data.end()); + AppendBigEndian32(png, Crc32(std::span<const std::uint8_t>(png.data() + start, png.size() - start))); +} + +static std::vector<std::uint8_t> ZlibStore(std::span<const std::uint8_t> bytes) { + Require(bytes.size() <= 0xFFFF); + const std::uint16_t length = static_cast<std::uint16_t>(bytes.size()); + const std::uint16_t inverse = static_cast<std::uint16_t>(~length); + std::vector<std::uint8_t> output{0x78, 0x01, 0x01, static_cast<std::uint8_t>(length), static_cast<std::uint8_t>(length >> 8), + static_cast<std::uint8_t>(inverse), static_cast<std::uint8_t>(inverse >> 8)}; + output.insert(output.end(), bytes.begin(), bytes.end()); + AppendBigEndian32(output, Adler32(bytes)); + return output; +} + +static std::vector<std::uint8_t> GrayLowBitPng(std::uint8_t bitDepth, std::span<const std::uint8_t> samples, + std::optional<std::uint16_t> transparency, + std::optional<std::uint16_t> precedingTransparency = std::nullopt) { + Require(bitDepth == 1 || bitDepth == 2 || bitDepth == 4); + std::vector<std::uint8_t> rows(1 + (samples.size() * bitDepth + 7) / 8, 0); + std::size_t bit = 0; + for (const std::uint8_t sample : samples) { + Require(sample < (1u << bitDepth)); + rows[1 + bit / 8] |= static_cast<std::uint8_t>(sample << (8 - bitDepth - bit % 8)); + bit += bitDepth; + } + + std::vector<std::uint8_t> ihdr; + AppendBigEndian32(ihdr, static_cast<std::uint32_t>(samples.size())); + AppendBigEndian32(ihdr, 1); + ihdr.insert(ihdr.end(), {bitDepth, 0, 0, 0, 0}); + + std::vector<std::uint8_t> png(PNG_SIGNATURE, PNG_SIGNATURE + sizeof(PNG_SIGNATURE)); + AppendTestChunk(png, "IHDR", ihdr); + if (precedingTransparency.has_value()) { + const std::uint8_t key[2] = {static_cast<std::uint8_t>(*precedingTransparency >> 8), static_cast<std::uint8_t>(*precedingTransparency)}; + AppendTestChunk(png, "tRNS", key); + } + if (transparency.has_value()) { + const std::uint8_t key[2] = {static_cast<std::uint8_t>(*transparency >> 8), static_cast<std::uint8_t>(*transparency)}; + AppendTestChunk(png, "tRNS", key); + } + AppendTestChunk(png, "IDAT", ZlibStore(rows)); + AppendTestChunk(png, "IEND", std::span<const std::uint8_t>()); + return png; +} + +static std::uint8_t GrayScale(std::uint8_t bitDepth) { + if (bitDepth == 1) return 255; + if (bitDepth == 2) return 85; + return 17; +} + +static void RequireGrayTransparency(std::uint8_t bitDepth, std::span<const std::uint8_t> samples, std::uint16_t key, + std::uint8_t transparentSample, std::optional<std::uint16_t> precedingKey = std::nullopt) { + const std::vector<std::uint8_t> png = GrayLowBitPng(bitDepth, samples, key, precedingKey); + const std::vector<std::uint8_t> original = png; + const auto header = Decoder::Png::ParseHeader(png); + Require(header.has_value()); + Require(header->width == static_cast<std::uint32_t>(samples.size()) && header->height == 1 && header->bitDepth == bitDepth); + Require(header->colorType == Decoder::Png::ColorType::Grayscale && header->hasTransparency); + + const auto image = Decoder::Png::Decode(png); + Require(image.has_value() && image->pixels.size() == samples.size() * 4); + const std::uint8_t scale = GrayScale(bitDepth); + for (std::size_t i = 0; i < samples.size(); ++i) { + const std::uint8_t gray = static_cast<std::uint8_t>(samples[i] * scale); + Require(image->pixels[i * 4] == gray && image->pixels[i * 4 + 1] == gray && image->pixels[i * 4 + 2] == gray); + Require(image->pixels[i * 4 + 3] == (samples[i] == transparentSample ? 0 : 255)); + } + Require(png == original); +} + +static void RequireGrayOpaque(std::uint8_t bitDepth, std::span<const std::uint8_t> samples) { + const std::vector<std::uint8_t> png = GrayLowBitPng(bitDepth, samples, std::nullopt); + const std::vector<std::uint8_t> original = png; + const auto header = Decoder::Png::ParseHeader(png); + Require(header.has_value() && !header->hasTransparency); + const auto image = Decoder::Png::Decode(png); + Require(image.has_value() && image->pixels.size() == samples.size() * 4); + const std::uint8_t scale = GrayScale(bitDepth); + for (std::size_t i = 0; i < samples.size(); ++i) { + const std::uint8_t gray = static_cast<std::uint8_t>(samples[i] * scale); + Require(image->pixels[i * 4] == gray && image->pixels[i * 4 + 1] == gray && image->pixels[i * 4 + 2] == gray); + Require(image->pixels[i * 4 + 3] == 255); + } + Require(png == original); +} + +static void RequireTruncatedTransparency() { + const std::uint8_t samples[] = {0, 1}; + const std::vector<std::uint8_t> png = GrayLowBitPng(1, samples, 0x0002); + const std::size_t transparencyOffset = sizeof(PNG_SIGNATURE) + 25; + const std::vector<std::uint8_t> truncated(png.begin(), png.begin() + transparencyOffset + 9); + const std::vector<std::uint8_t> original = truncated; + const auto header = Decoder::Png::ParseHeader(truncated); + Require(header.has_value() && !header->hasTransparency); + (void)Decoder::Png::Decode(truncated); + Require(truncated == original); +} + static void RequireRoundTrip(std::uint32_t channels, Decoder::Png::ColorType colorType) { constexpr std::uint32_t width = 20; constexpr std::uint32_t height = 10; @@ -176,6 +312,42 @@ int main() { const std::uint8_t levels[8] = {0, 255, 0, 255, 255, 0, 0, 255}; for (int i = 0; i < 8; ++i) Require(gray1->pixels[i * 4] == levels[i] && gray1->pixels[i * 4 + 3] == 255); + const auto gray1TrnsHeader = Decoder::Png::ParseHeader(GRAY1_TRNS); + Require(gray1TrnsHeader.has_value()); + Require(gray1TrnsHeader->width == 2 && gray1TrnsHeader->height == 1 && gray1TrnsHeader->bitDepth == 1); + Require(gray1TrnsHeader->colorType == Decoder::Png::ColorType::Grayscale && gray1TrnsHeader->hasTransparency); + const auto gray1Trns = Decoder::Png::Decode(GRAY1_TRNS); + Require(gray1Trns.has_value()); + Require(gray1Trns->pixels == std::vector<std::uint8_t>({0, 0, 0, 0, 255, 255, 255, 255})); + + const std::uint8_t gray1Samples[] = {0, 1, 0, 1, 1, 0, 0, 1}; + RequireGrayTransparency(1, gray1Samples, 0x0000, 0); + RequireGrayTransparency(1, gray1Samples, 0x0002, 0); + RequireGrayTransparency(1, gray1Samples, 0x0100, 0); + RequireGrayTransparency(1, gray1Samples, 0xFF02, 0); + RequireGrayTransparency(1, gray1Samples, 0xFFFF, 1); + RequireGrayTransparency(1, gray1Samples, 0x0001, 1, 0x0002); + RequireGrayOpaque(1, gray1Samples); + + const std::uint8_t gray2Samples[] = {0, 1, 2, 3}; + RequireGrayTransparency(2, gray2Samples, 0x0001, 1); + RequireGrayTransparency(2, gray2Samples, 0x0005, 1); + RequireGrayTransparency(2, gray2Samples, 0x0100, 0); + RequireGrayTransparency(2, gray2Samples, 0x0101, 1); + RequireGrayTransparency(2, gray2Samples, 0xFF05, 1); + RequireGrayTransparency(2, gray2Samples, 0xFFFF, 3); + RequireGrayOpaque(2, gray2Samples); + + const std::uint8_t gray4Samples[] = {0, 3, 10, 15, 1}; + RequireGrayTransparency(4, gray4Samples, 0x0003, 3); + RequireGrayTransparency(4, gray4Samples, 0x0013, 3); + RequireGrayTransparency(4, gray4Samples, 0x0100, 0); + RequireGrayTransparency(4, gray4Samples, 0x0103, 3); + RequireGrayTransparency(4, gray4Samples, 0xFF13, 3); + RequireGrayTransparency(4, gray4Samples, 0xFFFF, 15); + RequireGrayOpaque(4, gray4Samples); + RequireTruncatedTransparency(); + const auto interlaced = Decoder::Png::ParseHeader(HeaderOnly(3, 4, 8, 6, 1)); Require(interlaced.has_value() && interlaced->interlaced && !interlaced->hasTransparency); Require(!Decoder::Png::ParseHeader(HeaderOnly(0, 4, 8, 6, 0)).has_value()); From cd0c8c1cf07d6e818673b6124c82c6c70616015d Mon Sep 17 00:00:00 2001 From: DangerChamp <77676230+DangerChamp@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:07:17 +0100 Subject: [PATCH 1059/1360] fix(shader): reuse completed pixel lane proofs Memoize successful entry-lane proofs without retaining results that depend on active Phi cycles. Add regression coverage for saved EXEC-mask loops on narrower host subgroups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Ondra97 <Ondra97@users.noreply.github.com> --- .../tests/SavedExecMaskLoop.hpp | 19 +++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 13 ++ .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 137 ++++++++++++------ 3 files changed, 127 insertions(+), 42 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp diff --git a/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp new file mode 100644 index 0000000000..d6aa73bc18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP + +#include <cstdint> +#include <vector> + +namespace AgcTest { + +inline std::vector<std::uint32_t> SavedExecMaskLoop(std::uint32_t rounds) { + std::vector<std::uint32_t> code{0xbea2047eu, 0xbea40380u}; + for (std::uint32_t step = 0; step < rounds; ++step) code.insert(code.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu}); + const auto branch = 0xbf850000u | static_cast<std::uint16_t>(2 - static_cast<int>(code.size() + 3)); + code.insert(code.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u}); + return code; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 16e54ee281..5fe0acd0e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -20,6 +20,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" +#include "SavedExecMaskLoop.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif @@ -4088,6 +4089,18 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto loopScan = scan(0xffffffffu, 0x26000000u, 0x83800000u); + const auto savedMaskLoop = AgcTest::SavedExecMaskLoop(8u); + loopScan.insert(loopScan.begin(), savedMaskLoop.begin(), savedMaskLoop.end()); + auto helperLoop = loopScan; + helperLoop[0] = 0xbea204c1u; + expectFailure([&] { static_cast<void>(pixel(helperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a loop initialized with helper lanes was proven to exclude them"); + auto upperHelperLoop = loopScan; + upperHelperLoop[0] = 0xbea2037eu; + upperHelperLoop.insert(upperHelperLoop.begin() + 1, 0xbea303c1u); + expectFailure([&] { static_cast<void>(pixel(upperHelperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: the low-word loop proof was reused for helper-inclusive upper lanes"); + require(reductions(pixel(loopScan, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a loop-carried saved EXEC mask exhausted the entry-lane proof on 32 lanes"); + require(halves(pixel(loopScan, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a loop-carried saved EXEC mask did not preserve the two half-wave UMin reductions"); auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; comparedScan.insert(comparedScan.begin(), 0x7c080080u); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index d609c27286..0f4f8ac116 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -7,6 +7,7 @@ #include <algorithm> #include <array> #include <initializer_list> +#include <map> #include <optional> #include <string> #include <unordered_set> @@ -322,74 +323,126 @@ public: explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} bool ZeroBit(const IrValue* bit) { - bit = Resolved(bit); - if (bit == nullptr || !Spend()) return false; - if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); - if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit) || ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); - if (Is(bit, IrOpcode::SelectU1)) return ZeroBit(bit->Argument(1)) && ZeroBit(bit->Argument(2)); - if (Is(bit, IrOpcode::Phi)) return Incoming(bit, [this](const IrValue* incoming) { return ZeroBit(incoming); }); - if (Is(bit, IrOpcode::IEqual32)) return HelperTest(bit); - const auto words = LaneBitWords(bit); - return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + return Prove(bit, Proof::ZeroBit, [this](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::Bool && !value->ImmediateBool(); + if (Is(value, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(value) || ZeroBit(value->Argument(0)) || ZeroBit(value->Argument(1)); + if (Is(value, IrOpcode::SelectU1)) return ZeroBit(value->Argument(1)) && ZeroBit(value->Argument(2)); + if (Is(value, IrOpcode::Phi)) return Incoming(value, [this](const IrValue* incoming) { return ZeroBit(incoming); }); + if (Is(value, IrOpcode::IEqual32)) return HelperTest(value); + const auto words = LaneBitWords(value); + return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + }); } private: + enum class Proof : std::uint8_t { ZeroBit, NotHelper, LowWord, HighWord, LowWord64, HighWord64 }; + using Key = std::pair<const IrValue*, Proof>; + struct Assumption { + std::size_t depth; + std::uint64_t order; + }; + static constexpr std::size_t Unbounded = static_cast<std::size_t>(-1); + bool Spend() { if (budget == 0u) return false; --budget; return true; } - bool NotHelper(const IrValue* predicate) { - predicate = Resolved(predicate); - if (predicate == nullptr || !Spend()) return false; - if (predicate->HasImmediate()) return everyInvocationEnters && predicate->Type() == IrType::Bool && predicate->ImmediateBool(); - switch (predicate->Opcode()) { - case IrOpcode::LogicalAnd: return NotHelper(predicate->Argument(0)) || NotHelper(predicate->Argument(1)); - case IrOpcode::IEqual32: return HelperTest(predicate); - default: return ZeroBit(predicate); + template<typename TCheck> + bool Prove(const IrValue* value, Proof proof, TCheck&& check) { + value = Resolved(value); + if (value == nullptr) return false; + const Key key{value, proof}; + if (const auto found = settled.find(key); found != settled.end()) return found->second; + if (const auto found = active.find(key); found != active.end()) return Assume(found->second); + if (const auto found = assumed.find(key); found != assumed.end()) return Assume(found->second.depth); + if (!Spend()) return false; + const auto outer = dependency; + const auto depth = active.size(); + const auto mark = created; + dependency = Unbounded; + active.emplace(key, depth); + const bool result = check(value); + active.erase(key); + const auto inner = dependency; + const bool holds = result && inner >= depth; + for (auto entry = assumed.begin(); entry != assumed.end();) { + if (entry->second.order < mark) { + ++entry; + continue; + } + if (result && !holds) { + entry->second.depth = std::min(entry->second.depth, inner); + ++entry; + continue; + } + if (holds) settled.emplace(entry->first, true); + entry = assumed.erase(entry); } + if (result && !holds) assumed.emplace(key, Assumption{inner, created++}); + else settled.emplace(key, result); + dependency = result && !holds ? std::min(outer, inner) : outer; + return result; + } + + bool Assume(std::size_t depth) { + dependency = std::min(dependency, depth); + return true; + } + + bool NotHelper(const IrValue* predicate) { + return Prove(predicate, Proof::NotHelper, [this](const IrValue* value) { + if (value->HasImmediate()) return everyInvocationEnters && value->Type() == IrType::Bool && value->ImmediateBool(); + switch (value->Opcode()) { + case IrOpcode::LogicalAnd: return NotHelper(value->Argument(0)) || NotHelper(value->Argument(1)); + case IrOpcode::IEqual32: return HelperTest(value); + default: return ZeroBit(value); + } + }); } bool Word(const IrValue* word, std::uint32_t half) { - word = Resolved(word); - if (word == nullptr || !Spend()) return false; - if (word->HasImmediate()) return IsU32(word, 0u); - switch (word->Opcode()) { - case IrOpcode::CompositeExtractU32x4: { - const auto* ballot = Resolved(word->Argument(0)); - return IsU32(word->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + return Prove(word, half == 0u ? Proof::LowWord : Proof::HighWord, [this, half](const IrValue* value) { + if (value->HasImmediate()) return IsU32(value, 0u); + switch (value->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + const auto* ballot = Resolved(value->Argument(0)); + return IsU32(value->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + } + case IrOpcode::CompositeExtractU64: return IsU32(value->Argument(1), half) && Word64(value->Argument(0), half); + case IrOpcode::BitwiseAnd32: return Word(value->Argument(0), half) || Word(value->Argument(1), half); + case IrOpcode::BitwiseOr32: return Word(value->Argument(0), half) && Word(value->Argument(1), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word(incoming, half); }); + default: return false; } - case IrOpcode::CompositeExtractU64: return IsU32(word->Argument(1), half) && Word64(word->Argument(0), half); - case IrOpcode::BitwiseAnd32: return Word(word->Argument(0), half) || Word(word->Argument(1), half); - case IrOpcode::BitwiseOr32: return Word(word->Argument(0), half) && Word(word->Argument(1), half); - case IrOpcode::Phi: return Incoming(word, [this, half](const IrValue* incoming) { return Word(incoming, half); }); - default: return false; - } + }); } bool Word64(const IrValue* mask, std::uint32_t half) { - mask = Resolved(mask); - if (mask == nullptr || !Spend()) return false; - if (mask->HasImmediate()) return mask->Type() == IrType::U64 && static_cast<std::uint32_t>(mask->ImmediateU64() >> (32u * half)) == 0u; - switch (mask->Opcode()) { - case IrOpcode::CompositeConstructU64: return Word(mask->Argument(half), half); - case IrOpcode::Phi: return Incoming(mask, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); - default: return false; - } + return Prove(mask, half == 0u ? Proof::LowWord64 : Proof::HighWord64, [this, half](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::U64 && static_cast<std::uint32_t>(value->ImmediateU64() >> (32u * half)) == 0u; + switch (value->Opcode()) { + case IrOpcode::CompositeConstructU64: return Word(value->Argument(half), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); + default: return false; + } + }); } template<typename TCheck> - bool Incoming(const IrValue* phi, TCheck&& check) { - if (!phis.insert(phi).second) return true; + static bool Incoming(const IrValue* phi, TCheck&& check) { bool result = phi->ArgumentCount() != 0u; for (std::size_t index = 0; result && index < phi->ArgumentCount(); ++index) result = check(phi->Argument(index)); - phis.erase(phi); return result; } bool everyInvocationEnters; - std::unordered_set<const IrValue*> phis; + std::map<Key, std::size_t> active; + std::map<Key, Assumption> assumed; + std::map<Key, bool> settled; + std::size_t dependency = Unbounded; + std::uint64_t created = 0u; std::uint32_t budget = 2048u; }; From 812928af241e0b07d1510d586025a2f649eed5d9 Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" <harry.z.kang@outlook.com> Date: Wed, 14 Oct 2026 21:37:08 -0700 Subject: [PATCH 1060/1360] fix(agc): support packed 16-bit storage images --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 27 ++++++- .../tests/execution/ImageConvertedUnorm.cpp | 80 ++++++++++++++++++- .../Optimization/src/ResourceMaterializer.cpp | 10 ++- .../RdnaDecoder/RdnaDescriptorFormat.hpp | 1 + .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 11 +++ .../SpirvBackend/src/SpirvBufferFormat.cpp | 9 +++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 11 ++- 8 files changed, 146 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6525b1a42e..c9551aaaa8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2211,6 +2211,14 @@ target_include_directories(agc_driver_prepared_shaders_tests PRIVATE if(BUILD_TESTING) set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(packedStorageFormat IN ITEMS 133 134 136) + set(packedStorageTest agc_driver_image_packed16_storage_${packedStorageFormat}) + add_test(NAME ${packedStorageTest} COMMAND agc_driver_image_converted_unorm_tests --packed16 ${packedStorageFormat}) + set_tests_properties(${packedStorageTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(${packedStorageTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() foreach(prebuildPolicyMode IN ITEMS unset 0 1) set(prebuildPolicyTest agc_driver_prepared_graphics_prebuild_${prebuildPolicyMode}) add_test(NAME ${prebuildPolicyTest} COMMAND agc_driver_prepared_graphics_tests --prebuild-${prebuildPolicyMode}) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1e10ed8d97..e186c47858 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1035,6 +1035,9 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source->Image(); + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + viewInfo.pNext = &usage; viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = vkFormat; viewFormat = vkFormat; @@ -1192,6 +1195,17 @@ VkBufferMemoryBarrier WholeBufferBarrier(VkBuffer buffer, VkAccessFlags from, Vk return barrier; } +VkFormat PackedStorageViewFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R5G6B5_UNORM_PACK16: + case VK_FORMAT_A1R5G5B5_UNORM_PACK16: + case VK_FORMAT_B4G4R4A4_UNORM_PACK16: + return VK_FORMAT_R16_UINT; + default: + return VK_FORMAT_UNDEFINED; + } +} + // Storage images cannot use sRGB formats; the shader works on the raw encoded values either way. // The answer is a property of the physical device, so it is computed once per format (every storage // image lookup asks: a live vkGetPhysicalDeviceFormatProperties call each time was measurable) and @@ -1215,7 +1229,10 @@ VkFormat StorageFormatOrUndefined(const Context& context, VkFormat format) { return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; }; VkFormat storage = VK_FORMAT_UNDEFINED; - if (supports(format)) { + const auto packed = PackedStorageViewFormat(format); + if (packed != VK_FORMAT_UNDEFINED) { + if (supports(packed)) storage = format; + } else if (supports(format)) { storage = format; } else { VkFormat linear = VK_FORMAT_UNDEFINED; @@ -1793,9 +1810,10 @@ VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFor Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface"); const auto viewLayerCount = firstLayer ? 1u : geometry.imageLayers - descriptor.baseArray; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; - usage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + const bool packed = PackedStorageViewFormat(storageFormat) != VK_FORMAT_UNDEFINED; + usage.usage = packed && format == storageFormat ? VK_IMAGE_USAGE_SAMPLED_BIT | (attachable ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0u) : VK_IMAGE_USAGE_STORAGE_BIT; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - viewInfo.pNext = format == storageFormat ? nullptr : &usage; + viewInfo.pNext = format == storageFormat && !packed ? nullptr : &usage; viewInfo.image = image; // Storage views address one mip; cube faces are written as array layers. viewInfo.viewType = firstLayer ? VK_IMAGE_VIEW_TYPE_2D : descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : descriptor.dimension == TextureDimension::k2D ? VK_IMAGE_VIEW_TYPE_2D : descriptor.dimension == TextureDimension::k3D ? VK_IMAGE_VIEW_TYPE_3D : descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_1D_ARRAY : VK_IMAGE_VIEW_TYPE_2D_ARRAY; @@ -1971,7 +1989,8 @@ VkImageView StorageTexture::FirstLayerView(std::uint32_t mip) { } VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer) { - const auto format = UintFormatOfSint(storageFormat); + const auto packed = PackedStorageViewFormat(storageFormat); + const auto format = packed != VK_FORMAT_UNDEFINED ? packed : 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}); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index 94de1a8025..f840ade501 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -442,14 +442,92 @@ void RequireRefused(AgcDriver::VulkanDevice& device, std::span<const std::uint32 Require(refusal.find(reason) != std::string::npos, what + " of a converted image was not refused: " + refusal); } +void CheckPacked16(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t format) { + const std::array<std::uint32_t, 4> widths = format == 133u ? std::array{5u, 6u, 5u, 0u} : format == 134u ? std::array{5u, 5u, 5u, 1u} : std::array{4u, 4u, 4u, 4u}; + const std::array<std::uint32_t, 4> offsets = format == 133u ? std::array{0u, 5u, 11u, 0u} : format == 134u ? std::array{0u, 5u, 10u, 15u} : std::array{0u, 4u, 8u, 12u}; + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + const auto value = static_cast<std::uint16_t>(index * 4051u + 0x39d7u); + std::memcpy(texels + index * 2u, &value, 2u); + } + Run(device, LoadXyzw, TextureDescriptor(texels, format, SwizzleXYZW, LoadWidth), {}, LoadGroups); + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + const auto value = static_cast<std::uint16_t>(index * 4051u + 0x39d7u); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto bits = widths[component] == 0u ? widths[0] : widths[component]; + const auto maximum = (1u << bits) - 1u; + const auto actual = Output[index * 4u + component]; + Require(std::isfinite(std::bit_cast<float>(actual)) && StoreField(actual, maximum) == ((value >> offsets[component]) & maximum), "packed image_load has the wrong channel for format " + std::to_string(format)); + } + } + auto* target = texels + RoundTripOffset; + alignas(256) static constexpr std::array<std::uint32_t, 10> roundTrip{ + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x7e3e0280u, 0xf0001f08u, 0x00010a1eu, 0xbf8c3f70u, 0xf0201f08u, + 0x00040a1eu, 0xbf810000u, + }; + Run(device, roundTrip, TextureDescriptor(texels, format, SwizzleXYZW, LoadWidth), {}, LoadGroups, TextureDescriptor(target, format, SwizzleXYZW, LoadWidth)); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(target), LoadWidth * 2u, nullptr, "test"); + device.WaitIdle(); + Require(std::memcmp(texels, target, LoadWidth * 2u) == 0, "packed image load/store changed guest bytes for format " + std::to_string(format)); + alignas(256) static constexpr auto store = StoreCode(0xfu, 0u); + auto* written = texels + StoreOffset; + alignas(256) static constexpr auto storeHalf = StoreCode(0xfu, D16); + constexpr std::array halfValues{0x0000u, 0x3c00u, 0x3800u, 0x3400u, 0xbc00u, 0x4000u, 0x7c00u, 0x7e00u}; + constexpr std::array halfFloats{0.0f, 1.0f, 0.5f, 0.25f, -1.0f, 2.0f, INFINITY, NAN}; + for (std::uint32_t pass = 0; pass < 5u; ++pass) { + const auto swizzle = pass == 3u ? SwizzleZYX1 : SwizzleXYZW; + const auto dmask = pass == 4u ? 0x5u : 0xfu; + std::array<std::uint16_t, Threads> expected{}; + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::uint32_t slot = 0; + for (std::uint32_t component = 0; component < 4u; ++component) { + if (((dmask >> component) & 1u) == 0u) continue; + auto value = StoreValues[(tid + component * 3u + pass * 4u) % StoreValues.size()]; + if (tid == 0u) value = std::bit_cast<std::uint32_t>(pass == 0u ? std::array{1.0f, 0.5f, 0.25f, 1.0f}[component] : std::array{0.25f, 1.0f, 0.5f, 0.0f}[component]); + if (pass == 2u) { + const auto index = (tid + component * 3u) % halfValues.size(); + value = std::bit_cast<std::uint32_t>(halfFloats[index]); + Input[tid * 4u + slot / 2u] |= halfValues[index] << ((slot % 2u) * 16u); + } else { + Input[tid * 4u + slot] = value; + } + ++slot; + const auto selected = (swizzle >> (component * 3u)) & 7u; + if (selected >= 4u && widths[selected - 4u] != 0u) expected[tid] |= static_cast<std::uint16_t>(StoreField(value, (1u << widths[selected - 4u]) - 1u) << offsets[selected - 4u]); + } + } + auto* reference = texels + FloatOffset; + std::memcpy(reference, expected.data(), Threads * 2u); + Run(device, SampleLz, TextureDescriptor(reference, format, SwizzleXYZW, Threads), PointSampler, 1); + std::array<std::uint32_t, 4> sampled{}; + std::copy_n(Output.begin(), 4u, sampled.begin()); + const auto descriptor = TextureDescriptor(written, format, SwizzleXYZW, Threads); + Run(device, pass == 2u ? storeHalf : pass == 4u ? StoreXz : store, TextureDescriptor(written, format, swizzle, Threads), BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)), 1); + Run(device, SampleLz, descriptor, PointSampler, 1); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) Require(Output[tid * 4u + component] == sampled[component], "sample after packed store differs for format " + std::to_string(format)); + } + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(written), Threads * 2u, nullptr, "test"); + device.WaitIdle(); + Require(std::memcmp(written, expected.data(), Threads * 2u) == 0, "packed image_store write-back differs for format " + std::to_string(format)); + } } -int main() { +} + +int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; GuestBlock block; auto* texels = block.Data(); + if (argc == 3 && std::string(argv[1]) == "--packed16") { + const auto format = static_cast<std::uint32_t>(std::stoul(argv[2])); + Require(format == 133u || format == 134u || format == 136u, "unsupported packed storage test format"); + CheckPacked16(*device, texels, format); + std::cout << "Packed 16-bit storage image tests passed\n"; + return 0; + } for (std::uint32_t index = 0; index < LoadWidth; ++index) { const auto texel = LoadTexel(index); std::memcpy(texels + index * 4u, &texel, 4u); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 58654e3f25..4d0b4cbb78 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -250,7 +250,8 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag } } const bool nativeRepacked = format == IrBufferFormat::Format10_11_11Float && !storage && !base.packed; - decoded.conversionFormat = RemapTextureFormat(format) != format && !nativeRepacked ? format : IrBufferFormat::Invalid; + const auto remappedFormat = storage ? RemapStorageTextureFormat(format) : RemapTextureFormat(format); + decoded.conversionFormat = remappedFormat != format && !nativeRepacked ? format : IrBufferFormat::Invalid; if (format == IrBufferFormat::Format11_11_10UNorm || (format == IrBufferFormat::Format10_11_11Float && !nativeRepacked)) { const bool floating = format == IrBufferFormat::Format10_11_11Float; if (!base.srgbDecodeCompatible) { @@ -275,7 +276,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag const bool wideSint = format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt; const bool narrowSint = format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8SInt || format == IrBufferFormat::Format16_16SInt || format == IrBufferFormat::Format8_8_8_8SInt || format == IrBufferFormat::Format16_16_16_16SInt; const bool rawSintStorage = storage && (wideSint || (narrowSint && !base.packed)) && base.written && !base.read && !base.atomic; - decoded.numericClass = base.atomic || decoded.fmask ? IrTextureNumericClass::Uint : nativeRepacked ? IrTextureNumericClass::Float : SampledTextureNumericClass(format); + decoded.numericClass = base.atomic || decoded.fmask ? IrTextureNumericClass::Uint : nativeRepacked ? IrTextureNumericClass::Float : SampledTextureNumericClass(remappedFormat); if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { decoded.depthBits = true; if (!base.depthBitsCompatible) return rejectImage(PoisonReason::DepthBits16, "runtime image reads depth bits as unsupported 16-bit results"); @@ -1948,6 +1949,11 @@ std::vector<ImageResource> ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UInt, IrBufferFormat::Invalid, false, false); if ((image.srgbDecodeCompatible || (image.convertedSamples && convertedSampleCompatible(image))) && image.depthBitsCompatible) append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UNorm, IrBufferFormat::Invalid, false, false); if (image.srgbDecodeCompatible && image.depthBitsCompatible) append(IrTextureNumericClass::Uint, IrBufferFormat::Format10_11_11Float, IrBufferFormat::Invalid, false, false); + if (storage) { + for (const auto format : {IrBufferFormat::Format5_6_5UNorm, IrBufferFormat::Format5_5_5_1UNorm, IrBufferFormat::Format4_4_4_4UNorm}) { + append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); + } + } if (!storage) { if (image.depthBitsCompatible) { append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, false); diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index ae3cbe0839..e36217d958 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -23,6 +23,7 @@ enum class ImageType : std::uint32_t { [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); +[[nodiscard]] IrBufferFormat RemapStorageTextureFormat(IrBufferFormat format); [[nodiscard]] std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3); [[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index f28258ca1e..763deb31fb 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -127,6 +127,17 @@ IrBufferFormat RemapTextureFormat(IrBufferFormat format) { return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt || format == IrBufferFormat::Format10_11_11Float ? IrBufferFormat::Format32UInt : format; } +IrBufferFormat RemapStorageTextureFormat(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Format5_6_5UNorm: + case IrBufferFormat::Format5_5_5_1UNorm: + case IrBufferFormat::Format4_4_4_4UNorm: + return IrBufferFormat::Format16UInt; + default: + return RemapTextureFormat(format); + } +} + std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3) { const auto format = static_cast<IrBufferFormat>((word1 >> 20u) & 0x1ffu); const auto swizzle = (word3 >> 20u) & 0x1fu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp index 76ecba7a65..cc3168fec5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp @@ -55,6 +55,9 @@ SpirvFormatComponentType GetFormatComponentType(IrBufferFormat format) { case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format8_8_8_8UNorm: case IrBufferFormat::Format16_16_16_16UNorm: + case IrBufferFormat::Format5_6_5UNorm: + case IrBufferFormat::Format5_5_5_1UNorm: + case IrBufferFormat::Format4_4_4_4UNorm: return SpirvFormatComponentType::Unorm; case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: @@ -110,6 +113,12 @@ SpirvBufferFormatInfo GetFormatInfo(IrBufferFormat format) { } const auto type = GetFormatComponentType(format); switch (format) { + case IrBufferFormat::Format5_6_5UNorm: + return {format, type, 3u, 2u, {5u, 6u, 5u, 0u}, {0u, 5u, 11u, 0u}, true}; + case IrBufferFormat::Format5_5_5_1UNorm: + return {format, type, 4u, 2u, {5u, 5u, 5u, 1u}, {0u, 5u, 10u, 15u}, true}; + case IrBufferFormat::Format4_4_4_4UNorm: + return {format, type, 4u, 2u, {4u, 4u, 4u, 4u}, {0u, 4u, 8u, 12u}, true}; case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format8UScaled: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 772f4f8f4c..dad7138b19 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -521,19 +521,20 @@ SpirvBufferFormatInfo ImageConversionFormat(const ImageResource& image) { return {}; } const auto info = GetFormatInfo(format); - if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || (info.type != SpirvFormatComponentType::Uint && info.type != SpirvFormatComponentType::Unorm && info.type != SpirvFormatComponentType::Float) || !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, unorm or float format"); + const auto remapped = image.resourceClass == ImageResourceClass::Storage ? RemapStorageTextureFormat(format) : RemapTextureFormat(format); + if (SampledTextureNumericClass(remapped) != IrTextureNumericClass::Uint || remapped == format || (info.type != SpirvFormatComponentType::Uint && info.type != SpirvFormatComponentType::Unorm && info.type != SpirvFormatComponentType::Float) || !info.packedBitfield || (info.byteSize != 2u && info.byteSize != 4u) || info.componentCount == 0u || info.componentCount > 4u) { + throw std::runtime_error("image conversion format is not a packed 16-bit or 32-bit unsigned integer, unorm or float format"); } return info; } void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SpirvBufferFormatInfo& info) { - if (access.mem.dataBits == 16u) { + if (access.mem.dataBits == 16u && info.byteSize != 2u) { ctx.Fail(access.inst, info.type == SpirvFormatComponentType::Float ? "reads or writes a converted float image with 16-bit data, which is not implemented" : "reads or writes a converted unorm image with 16-bit data, which is not implemented"); } 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) { + if (selector >= 4u && selector - 4u >= info.componentCount && info.byteSize != 2u) { ctx.Fail(access.inst, "selects a channel the converted image format does not have"); } } @@ -753,6 +754,7 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce auto& state = ctx.state; const auto& mem = access.mem; const auto sint = SintStorageFormat(access.image); + const bool packedUnorm = access.image.conversionFormat != IrBufferFormat::Invalid && RemapStorageTextureFormat(access.image.conversionFormat) == IrBufferFormat::Format16UInt; std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); for (std::uint32_t component = 0; component < 4u; component++) { @@ -773,6 +775,7 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce } raw = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), RuntimeImageSwizzle(state, mem.resource, source), ConstantU32(state, component + 4u)), candidate, raw); } + if (packedUnorm && mem.dataBits == 16u) raw = Unary(state, spv::OpBitcast, TypeU32(state), EmitF16BitsToF32(state, raw)); values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); if (component < sint.componentCount) { values[component] = mem.dataBits == 16u ? EmitD16StoreComponent(state, sint, component, values[component]) : EmitFormatStoreComponent(state, sint, component, values[component]); From 4f26c7e35f4d8e997ff1e5a6799c23cd77b86ef3 Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" <harry.z.kang@outlook.com> Date: Wed, 14 Oct 2026 22:44:19 -0700 Subject: [PATCH 1061/1360] refactor(shader): isolate packed storage conversion from SINT stores --- .../shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index dad7138b19..7313dfc792 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -723,7 +723,8 @@ std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, texel, component); auto clamped = value; if (unorm) { - clamped = EmitFormatStoreComponent(state, info, component, value); + if (access.mem.dataBits == 16u) clamped = Unary(state, spv::OpBitcast, TypeU32(state), EmitF16BitsToF32(state, value)); + clamped = EmitFormatStoreComponent(state, info, component, clamped); } 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); @@ -754,7 +755,6 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce auto& state = ctx.state; const auto& mem = access.mem; const auto sint = SintStorageFormat(access.image); - const bool packedUnorm = access.image.conversionFormat != IrBufferFormat::Invalid && RemapStorageTextureFormat(access.image.conversionFormat) == IrBufferFormat::Format16UInt; std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); for (std::uint32_t component = 0; component < 4u; component++) { @@ -775,7 +775,6 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce } raw = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), RuntimeImageSwizzle(state, mem.resource, source), ConstantU32(state, component + 4u)), candidate, raw); } - if (packedUnorm && mem.dataBits == 16u) raw = Unary(state, spv::OpBitcast, TypeU32(state), EmitF16BitsToF32(state, raw)); values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); if (component < sint.componentCount) { values[component] = mem.dataBits == 16u ? EmitD16StoreComponent(state, sint, component, values[component]) : EmitFormatStoreComponent(state, sint, component, values[component]); From f30acac3cf67100eaa8f00338460e2214b310558 Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" <harry.z.kang@outlook.com> Date: Thu, 15 Oct 2026 09:25:01 -0700 Subject: [PATCH 1062/1360] docs(agc): keep packed texture limitations with driver debt --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 20522c7b50..641cda6b47 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -186,7 +186,7 @@ - [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices, and so does a `CB_COLOR_VIEW` slice range on a 2D array target through the array's resident image, or on a surface of one slice as an array of the view's last slice + 1 slices, one slice pitch apart: a layered draw into an array without its resident image, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. -- [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. +- [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. Storage images use compatible R16_UINT views with the [buffer UNORM store conversion](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp); quantization uses nearest even and maps NaN to zero. Rounding ties and NaN handling for packed stores have not been checked on AMD or PS5 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 a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. - [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for an SNORM `8`, `8_8` or `8_8_8_8` texture, which RDNA2 (gfx1031) reads bit for bit as the sRGB format of the same bytes, and for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering); `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. From 7c63ee6970f592e1a1f6ccf5e523a3a1a406a4d6 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Mon, 19 Oct 2026 16:26:43 +0000 Subject: [PATCH 1063/1360] fix(agc): give a pipeline linked from libraries its attachment sample counts An overrasterized draw (a multisampled rasterizer into one-sample color targets, VK_NV_framebuffer_mixed_samples) chains VkAttachmentSampleCountInfoNV into its pipeline. When the pipeline is linked from graphics pipeline libraries, only the fragment output library carried it, and the link and the background link-time-optimized relink did not. The Khronos validation layer (1.4.363) then reports VUID-vkCmdDraw-multisampledRenderToSingleSampled-07285 for every such draw, as it does in agc_driver_pixel_sample_coverage, and not with APS5_NO_GPL=1, where the pipeline is built whole. Both links now chain a copy of the sample counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/PipelineLibrary.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 7670cde694..b2d4927aee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -255,7 +255,17 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } if (partsOnly) return VK_NULL_HANDLE; PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); - VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; + std::optional<std::pair<std::vector<VkSampleCountFlagBits>, VkSampleCountFlagBits>> sampleCounts; + for (auto* next = static_cast<const VkBaseInStructure*>(rendering.pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV) continue; + const auto& counts = *reinterpret_cast<const VkAttachmentSampleCountInfoNV*>(next); + sampleCounts.emplace(std::vector<VkSampleCountFlagBits>(counts.pColorAttachmentSamples, counts.pColorAttachmentSamples + counts.colorAttachmentCount), counts.depthStencilAttachmentSamples); + } + const auto attachmentSamples = [](const std::pair<std::vector<VkSampleCountFlagBits>, VkSampleCountFlagBits>& counts) { + return VkAttachmentSampleCountInfoNV{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV, nullptr, static_cast<std::uint32_t>(counts.first.size()), counts.first.empty() ? nullptr : counts.first.data(), counts.second}; + }; + const auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; + VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR, sampleCounts ? &linkedSamples : nullptr}; linked.libraryCount = static_cast<std::uint32_t>(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; @@ -277,9 +287,10 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip const auto cache = context.pipelineCache; const auto cacheSynchronization = context.pipelineCacheSynchronization; const auto libraryLayout = fullLayout; - std::function<void()> relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { + std::function<void()> relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, sampleCounts, attachmentSamples, &store, devicePointer = &device] { if (slot->released.load()) return; - VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; + const auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; + VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR, sampleCounts ? &linkedSamples : nullptr}; linked.libraryCount = static_cast<std::uint32_t>(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; From 2fbab7aff4714b96116ca09e286886429ed9cb3d Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Mon, 19 Oct 2026 22:13:15 +0000 Subject: [PATCH 1064/1360] fix(agc): chain a linked pipeline's attachment sample counts to its create info Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/PipelineLibrary.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index b2d4927aee..77119254d5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -264,11 +264,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip const auto attachmentSamples = [](const std::pair<std::vector<VkSampleCountFlagBits>, VkSampleCountFlagBits>& counts) { return VkAttachmentSampleCountInfoNV{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV, nullptr, static_cast<std::uint32_t>(counts.first.size()), counts.first.empty() ? nullptr : counts.first.data(), counts.second}; }; - const auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; - VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR, sampleCounts ? &linkedSamples : nullptr}; + auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; + VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast<std::uint32_t>(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + if (sampleCounts) { + linkedSamples.pNext = create.pNext; + create.pNext = &linkedSamples; + } create.layout = info.layout; VkPipeline pipeline = VK_NULL_HANDLE; lock.unlock(); @@ -289,11 +293,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip const auto libraryLayout = fullLayout; std::function<void()> relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, sampleCounts, attachmentSamples, &store, devicePointer = &device] { if (slot->released.load()) return; - const auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; - VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR, sampleCounts ? &linkedSamples : nullptr}; + auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; + VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast<std::uint32_t>(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + if (sampleCounts) { + linkedSamples.pNext = create.pNext; + create.pNext = &linkedSamples; + } create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT; create.layout = libraryLayout; VkPipeline result = VK_NULL_HANDLE; From 36559da793a3827f8a7039746668ab1f1b8270d4 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Mon, 19 Oct 2026 21:54:56 +0000 Subject: [PATCH 1065/1360] feat(agc): draw one-sample color targets beside a multisampled depth target with sample 0's coverage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 13 ++- .../Graphics/include/Context.hpp | 2 + .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 13 ++- .../Graphics/src/PipelineJournal.cpp | 7 +- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 23 +++++ .../tests/execution/PixelSampleCoverage.cpp | 99 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 3 +- 11 files changed, 154 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index dd66e54ebf..74fd6ea2c1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -263,7 +263,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false; - if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.truncatedCoverage != t.truncatedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 55e54a39bb..c1e000a91c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -386,6 +386,8 @@ struct VulkanDevice::State { bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; VkSampleCountFlags mergedCoverageSampleCounts = 0; + VkSampleCountFlags truncatedCoverageSampleCounts = 0; + bool mixedAttachmentSamples = false; bool fragmentShaderPixelInterlock = false; bool conservativeRasterization = false; bool provokingVertexLast = false; @@ -1515,12 +1517,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(combinations(selected, &count, supported.data()), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); for (const auto& combination : supported) { if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_MERGE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == 0) state->mergedCoverageSampleCounts |= combination.rasterizationSamples; + if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == combination.rasterizationSamples) state->truncatedCoverageSampleCounts |= combination.rasterizationSamples; } } - if (state->mergedCoverageSampleCounts != 0) { + if (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0) { deviceExtensions.push_back(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME); deviceExtensions.push_back(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME); } + } else if (hasExtension(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME)) { + state->mixedAttachmentSamples = true; + state->truncatedCoverageSampleCounts = state->properties.limits.framebufferDepthSampleCounts & state->properties.limits.framebufferColorSampleCounts & ~static_cast<VkSampleCountFlags>(VK_SAMPLE_COUNT_1_BIT); + deviceExtensions.push_back(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME); } coverageReductionFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV}; coverageReductionFeatures.coverageReductionMode = VK_TRUE; @@ -1811,7 +1818,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : nonSeamlessCubeFeatures.pNext = const_cast<void*>(deviceInfo.pNext); deviceInfo.pNext = &nonSeamlessCubeFeatures; } - if (state->mergedCoverageSampleCounts != 0) { + if (!state->mixedAttachmentSamples && (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0)) { coverageReductionFeatures.pNext = const_cast<void*>(deviceInfo.pNext); deviceInfo.pNext = &coverageReductionFeatures; } @@ -3814,6 +3821,8 @@ Graphics::Context VulkanDevice::buildContext() const { context.samplerFilterMinmax = state->samplerFilterMinmax; context.nonSeamlessCubeMap = state->nonSeamlessCubeMap; context.mergedCoverageSampleCounts = state->mergedCoverageSampleCounts; + context.truncatedCoverageSampleCounts = state->truncatedCoverageSampleCounts; + context.mixedAttachmentSamples = state->mixedAttachmentSamples; context.conservativeRasterization = state->conservativeRasterization; context.meshWave32 = state->meshWave32; context.provokingVertexLast = state->provokingVertexLast; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 4167955e8c..2e9a481930 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -213,6 +213,8 @@ struct Context { bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; VkSampleCountFlags mergedCoverageSampleCounts = 0; + VkSampleCountFlags truncatedCoverageSampleCounts = 0; + bool mixedAttachmentSamples = false; bool conservativeRasterization = false; bool meshWave32 = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index a064d18d72..c934bd4e04 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -142,6 +142,7 @@ struct State { bool ditheredAlphaToCoverage = false; bool perSampleShading = false; bool mergedCoverage = false; + bool truncatedCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index be7ae30687..7926925ff3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -55,6 +55,7 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.renderExtent.height); mix(state.samples); mix(state.mergedCoverage); + mix(state.truncatedCoverage); mix(state.customSampleLocations); if (state.customSampleLocations) { mix(state.sampleLocationGrid); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index aaa08ba07f..2eb908e307 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -263,6 +263,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.fragmentOutput, state.perSampleShading); appendKey(keys.fragmentShader, state.mergedCoverage); appendKey(keys.fragmentOutput, state.mergedCoverage); + appendKey(keys.fragmentShader, state.truncatedCoverage); + appendKey(keys.fragmentOutput, state.truncatedCoverage); const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); appendKey(keys.fragmentShader, sampleLocationsEnabled); appendKey(keys.fragmentOutput, sampleLocationsEnabled); @@ -345,6 +347,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + Require(!state.truncatedCoverage || (context.truncatedCoverageSampleCounts & state.samples) != 0, "one-sample color targets beside a multisampled depth target require VK_AMD_mixed_attachment_samples or VK_NV_framebuffer_mixed_samples with truncated coverage reduction"); Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); @@ -407,7 +410,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = state.mergedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast<VkSampleCountFlagBits>(state.samples); + color.samples = state.mergedCoverage || state.truncatedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast<VkSampleCountFlagBits>(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -444,7 +447,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput VkAttachmentSampleCountInfoNV attachmentSamples{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV}; attachmentSamples.colorAttachmentCount = static_cast<std::uint32_t>(mergedColorSamples.size()); attachmentSamples.pColorAttachmentSamples = mergedColorSamples.empty() ? nullptr : mergedColorSamples.data(); - if (state.mergedCoverage) rendering.pNext = &attachmentSamples; + if (state.truncatedCoverage) attachmentSamples.depthStencilAttachmentSamples = static_cast<VkSampleCountFlagBits>(state.samples); + if (state.mergedCoverage || state.truncatedCoverage) rendering.pNext = &attachmentSamples; if (state.depth) { rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format); if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat; @@ -518,8 +522,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, grid, state.samples * grid.width * grid.height, locations.data()}; if (sampleLocationsEnabled) samples.pNext = &sampleLocations; VkPipelineCoverageReductionStateCreateInfoNV coverageReduction{VK_STRUCTURE_TYPE_PIPELINE_COVERAGE_REDUCTION_STATE_CREATE_INFO_NV}; - coverageReduction.coverageReductionMode = VK_COVERAGE_REDUCTION_MODE_MERGE_NV; - if (state.mergedCoverage) { + coverageReduction.coverageReductionMode = state.truncatedCoverage ? VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV : VK_COVERAGE_REDUCTION_MODE_MERGE_NV; + if (state.mergedCoverage || (state.truncatedCoverage && !context.mixedAttachmentSamples)) { coverageReduction.pNext = samples.pNext; samples.pNext = &coverageReduction; } @@ -897,6 +901,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat append(key, state.samples); append(key, state.perSampleShading); append(key, state.mergedCoverage); + append(key, state.truncatedCoverage); append(key, state.customSampleLocations); if (state.customSampleLocations) { append(key, state.sampleLocationGrid); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 383220e4b3..82d330d68d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 800, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 808, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 6u; +constexpr std::uint32_t FileFormat = 7u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -217,6 +217,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.sampleLocationGrid); writer.Bool(state.perSampleShading); writer.Bool(state.mergedCoverage); + writer.Bool(state.truncatedCoverage); for (const auto mask : state.quadSampleMask) writer.Raw(mask); writer.U32(state.depthRemap.mode); } @@ -278,6 +279,7 @@ void decodeState(Reader& reader, State& state) { state.sampleLocationGrid = reader.Bool(); state.perSampleShading = reader.Bool(); state.mergedCoverage = reader.Bool(); + state.truncatedCoverage = reader.Bool(); for (auto& mask : state.quadSampleMask) mask = reader.Raw<std::uint16_t>(); state.depthRemap.mode = reader.U32(); } @@ -1051,6 +1053,7 @@ State CanonicalPipelineState(const State& state) { canonical.sampleLocationGrid = state.sampleLocationGrid; canonical.perSampleShading = state.perSampleShading; canonical.mergedCoverage = state.mergedCoverage; + canonical.truncatedCoverage = state.truncatedCoverage; canonical.quadSampleMask = state.quadSampleMask; canonical.depthRemap.mode = state.depthRemap.mode; return canonical; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 45b587e63d..f6faccd0e9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1435,8 +1435,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } const bool merged = color.samples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples); if (!merged && color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); - Require((color.samples == result.samples && !result.mergedCoverage) || merged, "a color target's sample count differs from the rasterizer's"); + const bool truncated = color.samples == 1u && result.samples > 1u && result.depth && result.depth->samples == result.samples && (result.colors.empty() || result.truncatedCoverage) && !overrasterized(cx, result.samples); + Require((color.samples == result.samples && !result.mergedCoverage && !result.truncatedCoverage) || merged || truncated, "a color target's sample count differs from the rasterizer's"); result.mergedCoverage = merged; + result.truncatedCoverage = truncated; APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast<unsigned long long>(color.address), color.extent.width, color.extent.height, static_cast<unsigned long long>(color.bytes), static_cast<unsigned>(color.format)); if (result.colors.empty()) { result.renderExtent = color.extent; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index dedf745126..4dae15015e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4151,6 +4151,29 @@ void multisampledDepthTests() { Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan"); queue = singleSampleScan(); queue.context[0x31d] = 0; + state = DecodeState(queue); + Require(state.truncatedCoverage && !state.mergedCoverage && state.colors.size() == 1 && state.colors[0].samples == 1, "a single-sample scan into a 4x depth target and a one-sample color target did not take the color from sample 0"); + requireCentered(state, "a single-sample scan into a 4x depth target and a one-sample color target"); + const auto truncatedTwoSample = [&] { + auto queue = singleSampleScan(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x010] = 0x22900987; + queue.context[0x31d] = 0; + return queue; + }; + queue = truncatedTwoSample(); + state = DecodeState(queue); + Require(state.samples == 2 && state.depth && state.depth->samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.truncatedCoverage && !state.mergedCoverage && !state.perSampleShading, "a one-sample color target beside a 2x depth target under a 2x rasterizer with MSAA_ENABLE (PPSA02225's registers) did not take the color from sample 0"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a one-sample color target beside a 2x depth target"); + queue = truncatedTwoSample(); + queue.context[0x293] = 0x10000u; + queue.context[0x1b3] = queue.context[0x1b4] = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); + state = DecodeState(queue); + Require(state.truncatedCoverage && state.perSampleShading && state.samples == 2 && state.colors[0].samples == 1, "PS_ITER_SAMPLE into a one-sample color target beside a 2x depth target (PPSA02225's registers) did not run per sample with sample 0's color"); + queue = truncatedTwoSample(); + queue.context[0x31d] = 0x12000; expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); queue = singleSampleScan(); queue.context[0x010] = 0x22900983; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp index 7c6d5398c2..32b2aff942 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -20,6 +20,7 @@ #include <algorithm> #include <array> #include <bit> +#include <cstddef> #include <cstdint> #include <cstdio> #include <cstdlib> @@ -54,7 +55,12 @@ constexpr std::array<std::array<float, 4>, 3> HalfTriangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} }}; -alignas(256) std::array<std::array<std::uint32_t, 16>, 4> Programs{}; +constexpr std::array<std::array<float, 4>, 3> OtherHalfTriangle{{ + {1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array<std::array<std::uint32_t, 16>, 8> Programs{}; +alignas(256) std::array<std::byte, 256 * 1024> DepthMemory{}; std::size_t programCount = 0; std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -76,6 +82,22 @@ std::span<const std::uint32_t> Program(std::uint32_t coverageVgpr) { return std::span<const std::uint32_t>(code.data(), at); } +std::span<const std::uint32_t> SampleIndexProgram() { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0xd5480018u; + code[at++] = 0x02111100u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 24u); + code[at++] = 0x7e000280u | (21u << 17u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span<const std::uint32_t>(code.data(), at); +} + struct Block { explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { #ifdef _WIN32 @@ -150,7 +172,7 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t return color; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span<const std::uint32_t> pixelCode, std::span<const std::array<float, 4>, 3> triangle = Triangle, std::uint32_t rasterSamples = 1) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span<const std::uint32_t> pixelCode, std::span<const std::array<float, 4>, 3> triangle = Triangle, std::uint32_t rasterSamples = 1, const AgcDriver::Graphics::DepthTarget* depth = nullptr, bool perSample = false) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector<std::uint32_t> vertexUserData(4, 0u); @@ -171,6 +193,8 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge pixel.inputAddr = inputAddr; pixel.ancillary = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0u; pixel.hasPerspectiveCenterVgpr = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter)) != 0u; + pixel.sampleShading = perSample; + pixel.iteratesSamples = perSample; pixel.targetOutputMode.at(0) = Float32AbgrExport; pixel.targetExportMapping.fill(0xe4u); const std::vector<std::uint32_t> pixelUserData(8, 0u); @@ -207,7 +231,15 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge state.blend = blend; state.blendConstants = {}; state.samples = rasterSamples; - state.mergedCoverage = rasterSamples > 1; + state.mergedCoverage = rasterSamples > 1 && depth == nullptr; + state.truncatedCoverage = rasterSamples > 1 && depth != nullptr; + state.perSampleShading = perSample; + if (depth != nullptr) { + state.depth = *depth; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + } const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(triangle.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); } @@ -273,6 +305,66 @@ bool ExpectMerged(AgcDriver::VulkanDevice& device) { return true; } +bool ExpectTruncated(AgcDriver::VulkanDevice& device) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + depth.samples = 2; + std::array<std::uint32_t, 4> written{}; + std::uint32_t untouched = 0; + for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) { + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + try { + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), *triangle, 2u, &depth); + } catch (const std::exception& error) { + if (std::string_view(error.what()).find("truncated coverage reduction") == std::string_view::npos) throw; + return false; + } + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast<std::size_t>(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++untouched; + continue; + } + std::array<float, 4> value{}; + std::memcpy(value.data(), texel, sizeof(value)); + const auto mask = static_cast<std::uint32_t>(value[0]); + Require(value[0] == static_cast<float>(mask) && mask >= 1u && mask <= 3u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "truncated coverage: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + ")"); + ++written[mask]; + } + } + } + Require(written[2] == 0, "truncated coverage: " + std::to_string(written[2]) + " pixels that cover only sample 1 were written into the one-sample target"); + Require(written[1] != 0 && written[3] != 0 && untouched != 0, "truncated coverage: two half-covering triangles wrote " + std::to_string(written[3]) + " fully covered and " + std::to_string(written[1]) + " sample-0-only pixels and left " + std::to_string(untouched)); + std::uint32_t perSampleWritten = 0; + std::uint32_t perSampleUntouched = 0; + for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) { + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary), SampleIndexProgram(), *triangle, 2u, &depth, true); + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast<std::size_t>(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++perSampleUntouched; + continue; + } + std::array<float, 4> value{}; + std::memcpy(value.data(), texel, sizeof(value)); + Require(value == std::array<float, 4>{0.0f, 0.0f, 0.0f, 1.0f}, "truncated coverage per sample: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + "), not sample 0's output"); + ++perSampleWritten; + } + } + } + Require(perSampleWritten == written[1] + written[3] && perSampleUntouched == untouched, "truncated coverage per sample: the draws wrote " + std::to_string(perSampleWritten) + " pixels where one invocation per pixel wrote " + std::to_string(written[1] + written[3])); + return true; +} + } int main() { @@ -285,6 +377,7 @@ int main() { Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center"); Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY"); if (!ExpectMerged(*device)) std::puts("merged coverage not tested: the device lacks VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + if (!ExpectTruncated(*device)) std::puts("truncated coverage not tested: the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction"); std::puts("pixel sample coverage tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ea88f074a2..ed15035dc9 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -165,7 +165,8 @@ - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each count, per count and for every count; an RTX 3070 Laptop GPU (driver 617.42) reports `maxSampleLocationGridSize` 1x1 for every count (`VkPhysicalDeviceSampleLocationsPropertiesEXT`), but per count (`VkMultisamplePropertiesEXT`, which the device setup reads and logs) a 2x4 grid at 2 samples, 2x2 at 4 and 1x2 at 8. So the 3070's 2x patterns, the 2x checkerboard among them, take the native grid, and the stand-in plus the exact path serve its 8-sample patterns and devices without a grid. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, is drawn exactly through that stand-in: before every render pass that draws with it each pixel's samples are put in the stand-in's order and after it back in the guest's, through a storage view of a multisampled color target and through a copy and a per-sample pass for depth (writing stencil through VK_EXT_shader_stencil_export, or one pass per bit without it), while the pixel shader reads SampleID, applies the sample mask and exports coverage in the guest's order, and the second log line names the reordering. A uniform partial sample mask that the order moves is applied in the pixel shader. With early Z, a depth or stencil test and memory writes in the pixel program, the draw throws, where #6451's stand-in alone ran it with odd rows' sample indices swapped. A fix would choose the stand-in after compilation. That costs two passes over each multisampled attachment per render pass, and on those devices every multisampled color target is created with a mutable format and storage usage for its view, which a driver may answer by not compressing it. It needs shaderStorageImageMultisample with a storage view of the color format at the sample count, sampleRateShading, and a depth format a FragDepth write gives back exactly (16-bit, or 32-bit float; not 24-bit depth, the stand-in for 16-bit depth with stencil where the device has neither); without them, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. A device without shaderStorageImageMultisample takes the stand-in alone for every pass, depth-only passes included, so a depth target that a depth-only pass and a color pass both draw keeps one order. On a device that has it, a pass is still refused for a color format without a storage view at its sample count, while a depth-only pass on the same depth target is reordered, so that target's samples change order between those passes; a fix would decide the order per target. Then the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A same-set pattern keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values, but sample i of an odd row holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. A pass that draws depth at custom sample locations ends with a transition to TRANSFER_SRC_OPTIMAL and back that names them. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. -- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw +- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index and a sample-mask export throw +- [One-sample color targets beside a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a one-sample color target drawn with a depth target of the rasterizer's sample count is taken to keep the coverage of sample 0 after the depth and stencil tests. That is the rule of [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516), and it is not measured on hardware. The draw needs `VK_AMD_mixed_attachment_samples`, or `VK_NV_framebuffer_mixed_samples` with truncated coverage reduction, for its sample count and throws without them. With `PS_ITER_SAMPLE` the color is the output of sample 0's invocation. Overrasterization and color targets of different sample counts throw. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. A range a queued draw commit still has to read is left pending whatever the stamps say ([`DrawPipeline::HeldReads`](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp)), so a title that recycles one allocation between an index buffer and a storage image sees the draw's own indices; the trade is a title that reads a surface as index data and means the image gets the pre-image bytes instead, since the reservation does not compare the command-stream positions of the draw and of the image's write. `APS5_NO_QUEUED_DRAW_READ_ORDER=1` restores the old behaviour, and the `[storage]` line counts the listings the hold turned away (`held for a queued draw read`) and the `[draw] pipelined` line the stores it held. The hold covers what the commit reads of guest memory: the index range, the vertex input ranges as their descriptor extents ([`QueuedDrawReadRanges`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp), since the bytes a draw fetches depend on the highest index, which only the commit knows) and the source of a queued DMA copy. Every other commit-time read goes through the flush hook, so it sees the bytes the hold protects. Two commit-time reads are outside the hold: the texels a register or cmask clear materialization reads from the draw's own colour target (through the hook, so ordered, but the hold does not cover a render target), and a CPU read whose store the hold turned away, which sees the pre-image bytes. Nothing retries a refused store: the image reaches guest memory with the next flush of that range (the queue worker's idle store, a `WAIT_REG_MEM` poll over it, a later read or write of it). A kept 64 KiB unit that lies outside every (slice, level) window stores nothing: the tracking grid is flat over the guest range while 3D slices taper in depth with the mip level (`HasLayer`), so the higher depth slices' higher-level blocks belong to no window. The console's tracking of such units is not measured. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. From 4ba0175ba23f2cb7f9c4fd7974bbf25f2a9f0e56 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Wed, 21 Oct 2026 12:20:46 +0000 Subject: [PATCH 1066/1360] test(agc): skip the truncated coverage case without two-sample depth targets ExpectTruncated draws with a two-sample D32 depth target. On a device whose framebufferDepthSampleCounts lacks 2, as lavapipe, the pipeline's existing sample count check refused that depth target before the truncated coverage check, and the test failed. The case now reports why it was not tested: no two-sample depth targets, or neither mixed sample extension with truncated coverage reduction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/PixelSampleCoverage.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp index 32b2aff942..ce79a9b375 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -31,6 +31,14 @@ #include <string_view> #include <vector> +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context Context(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -305,7 +313,8 @@ bool ExpectMerged(AgcDriver::VulkanDevice& device) { return true; } -bool ExpectTruncated(AgcDriver::VulkanDevice& device) { +std::string ExpectTruncated(AgcDriver::VulkanDevice& device) { + if ((AgcDriver::VulkanDeviceTestAccess::Context(device).limits.framebufferDepthSampleCounts & VK_SAMPLE_COUNT_2_BIT) == 0) return "the device has no two-sample depth targets (framebufferDepthSampleCounts)"; constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; AgcDriver::Graphics::DepthTarget depth{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; depth.samples = 2; @@ -319,7 +328,7 @@ bool ExpectTruncated(AgcDriver::VulkanDevice& device) { Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), *triangle, 2u, &depth); } catch (const std::exception& error) { if (std::string_view(error.what()).find("truncated coverage reduction") == std::string_view::npos) throw; - return false; + return "the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction"; } const auto stored = ReadBack(device, block.Address(), bytes); for (std::uint32_t y = 0; y < Height; ++y) { @@ -362,7 +371,7 @@ bool ExpectTruncated(AgcDriver::VulkanDevice& device) { } } Require(perSampleWritten == written[1] + written[3] && perSampleUntouched == untouched, "truncated coverage per sample: the draws wrote " + std::to_string(perSampleWritten) + " pixels where one invocation per pixel wrote " + std::to_string(written[1] + written[3])); - return true; + return {}; } } @@ -377,7 +386,7 @@ int main() { Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center"); Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY"); if (!ExpectMerged(*device)) std::puts("merged coverage not tested: the device lacks VK_NV_framebuffer_mixed_samples with merged coverage reduction"); - if (!ExpectTruncated(*device)) std::puts("truncated coverage not tested: the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction"); + if (const auto skipped = ExpectTruncated(*device); !skipped.empty()) std::printf("truncated coverage not tested: %s\n", skipped.c_str()); std::puts("pixel sample coverage tests passed"); return 0; } catch (const std::exception& error) { From ee057ea15547642d0d7c251b0076f3a2275cb7a4 Mon Sep 17 00:00:00 2001 From: Braxton Atkinson <grcfpbnnzg@privaterelay.appleid.com> Date: Thu, 22 Oct 2026 07:31:23 -0500 Subject: [PATCH 1067/1360] fix(libc): preserve errno across fmodf domain errors --- core/libs/prx/libc/src/Math.cpp | 7 ++++- core/libs/tests/GuestMath.cpp | 49 +++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..646f4c89c9 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -290,7 +290,12 @@ std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t x) { return labs_nid_postfix(x); } -float APS5_VABI fmodf_nid_postfix(float x, float y) { return std::fmod(x, y); } +float APS5_VABI fmodf_nid_postfix(float x, float y) { + const int saved_errno = errno; + const float result = std::fmod(x, y); + errno = saved_errno; + return result; +} float APS5_VABI asinf_nid_postfix(float x) { const int saved_errno = errno; const float result = std::asin(x); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..0ad1273c16 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -524,6 +524,13 @@ static void CheckPowfSpecialValue() { Require(std::bit_cast<std::uint32_t>(powf_nid_postfix(1.0f, std::bit_cast<float>(0x7f800001u))) == 0x3f800000u); } +struct RestoreFloatingPointState { + std::fenv_t environment; + int error = errno; + RestoreFloatingPointState() { Require(std::fegetenv(&environment) == 0); } + ~RestoreFloatingPointState() { Require(std::fesetenv(&environment) == 0); errno = error; } +}; + static void CheckAcosfErrno() { errno = 0; Require(std::bit_cast<std::uint32_t>(acosf_nid_postfix(2.0f)) == 0xffc00000u); @@ -534,6 +541,47 @@ static void CheckAcosfErrno() { Require(errno == 0); } +static void CheckFmodfErrno() { + const RestoreFloatingPointState saved; + const struct { std::uint32_t dividend; std::uint32_t divisor; std::uint32_t expected; bool invalid; bool nan; } cases[] = { + {0x3f800000u, 0x00000000u, 0u, true, true}, {0x3f800000u, 0x80000000u, 0u, true, true}, + {0x7f800000u, 0x40000000u, 0u, true, true}, {0x7fc00000u, 0x40000000u, 0u, false, true}, + {0x3f800000u, 0x7f800000u, 0x3f800000u, false, false}, {0xbf800000u, 0x7f800000u, 0xbf800000u, false, false}, + {0x41280000u, 0x40000000u, 0x3f000000u, false, false}, {0xc1280000u, 0x40000000u, 0xbf000000u, false, false}, + {0x41280000u, 0xc0000000u, 0x3f000000u, false, false}, {0xc1280000u, 0xc0000000u, 0xbf000000u, false, false}, + {0x00000000u, 0x40000000u, 0x00000000u, false, false}, {0x80000000u, 0x40000000u, 0x80000000u, false, false}, + {0x00000000u, 0x80000000u, 0u, true, true}, + {0x3f800000u, 0x7fc00001u, 0u, false, true}, + {0x00000000u, 0x7fc00001u, 0u, false, true}, + {0x7f800001u, 0x40000000u, 0u, true, true}, + {0x3f800000u, 0x7f800001u, 0u, true, true}, + {0x00000003u, 0x00000002u, 0x00000001u, false, false}, + {0x00800001u, 0x00800000u, 0x00000001u, false, false}, + {0x80000001u, 0x3f800000u, 0x80000001u, false, false}, + }; + for (const auto& test : cases) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 73; + const float result = fmodf_nid_postfix(std::bit_cast<float>(test.dividend), std::bit_cast<float>(test.divisor)); + const std::uint32_t bits = std::bit_cast<std::uint32_t>(result); + const int invalid = std::fetestexcept(FE_INVALID) != 0; + const bool nan = std::isnan(result); + if (nan != test.nan || (!nan && bits != test.expected) || invalid != test.invalid || *__error_nid_postfix() != 73) { + std::fprintf(stderr, "Guest fmodf failed for %08x and %08x: got %08x, invalid %d, errno %d\n", + test.dividend, test.divisor, bits, invalid, *__error_nid_postfix()); + std::abort(); + } + } + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + *__error_nid_postfix() = 73; + (void)fmodf_nid_postfix(1.0f, 0.0f); + if (*__error_nid_postfix() != 73 || std::fetestexcept(FE_DIVBYZERO) == 0) { + std::fprintf(stderr, "Guest fmodf dropped the floating-point environment\n"); + std::abort(); + } +} + static void CheckLongDouble() { Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); @@ -1592,6 +1640,7 @@ int main() { CheckHypotSpecialValue(); CheckFloatUnderflowErrno(); CheckFmodErrno(); + CheckFmodfErrno(); CheckDoubleClassification(); CheckIntegerAbsAndDivision(); CheckScaling(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..84f92e3f06 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -57,6 +57,7 @@ ### Functional - [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console. +- [fmodf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - console `errno`, exception flags and NaN payloads remain unverified. The wrapper preserves `errno` like the existing double `fmod`, following FreeBSD 11.4 [`e_fmodf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_fmodf.c); host `std::fmod` still supplies the result and exception flags. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. From 07bec523ad261351d1e6b506f14ca87651b8e643 Mon Sep 17 00:00:00 2001 From: Braxton Atkinson <grcfpbnnzg@privaterelay.appleid.com> Date: Wed, 21 Oct 2026 18:39:10 -0500 Subject: [PATCH 1068/1360] docs(libc): separate byte-case compatibility note --- docs/dev/debt/libScePosixForWebKit.md | 1 - docs/dev/debt/libc.md | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index d6dbc848b6..654d2d229e 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -8,7 +8,6 @@ ### Functional - [strcasestr](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - console locale behavior is unmeasured. It follows FreeBSD 11.4's [strcasestr.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasestr.c) in the guest's supported C locale; other guest locales are not implemented. - - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 43a8870660..b90d215f8f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -4,8 +4,6 @@ - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. -- [strcasecmp and strncasecmp](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - console locale behavior is unmeasured. They use the guest's C-locale case conversion and the folded unsigned-byte difference of FreeBSD 11.4's [strcasecmp.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasecmp.c); other guest locales are not implemented. - - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. @@ -70,6 +68,7 @@ - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [strcasecmp and strncasecmp](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - console locale behavior is unmeasured. They use the guest's C-locale case conversion and the folded unsigned-byte difference of FreeBSD 11.4's [strcasecmp.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasecmp.c); other guest locales are not implemented. - [feof_unlocked and ferror_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) have no console measurement. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NIDs `NuydofHcR1w` and `yxbGzBQC5xA`. They follow FreeBSD 11 [feof.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/feof.c) and [ferror.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/ferror.c), returning 0 or 1 from the stream status without acquiring the stream lock. They inherit the host CRT limitations of the unlocked byte operations above. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. From d37f11e42460f2faa05e7c9beee7f959c4045c7c Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:00:46 +0000 Subject: [PATCH 1069/1360] test(net): preserve real address-family failure coverage --- core/libs/tests/GuestSceNet.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 599f79d3ec..05156d38df 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1815,10 +1815,7 @@ int main() { Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast<int>(0x80410116)); const auto resolved6BeforeFailure = resolved6; - { - ScopedResolverLookupOverride lookupOverride(FailResolverLookup); - Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast<int>(0x804101E1)); - } + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast<int>(0x804101E1)); Require(resolved6 == resolved6BeforeFailure); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast<int>(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); From 53d3d8674ea127ae9af3ebe3046bb4675599d075 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:06:55 +0000 Subject: [PATCH 1070/1360] test(net): constrain deterministic resolver failures --- core/libs/tests/GuestSceNet.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 05156d38df..3060f3485b 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -147,7 +147,10 @@ static void Require(bool condition) { if (!condition) std::abort(); } -static bool FailResolverLookup(const char*, int, std::vector<std::array<std::uint8_t, 16>>&) { +static bool FailResolverLookup(const char* hostname, int family, std::vector<std::array<std::uint8_t, 16>>&) { + Require(hostname != nullptr && (std::strcmp(hostname, "guest-sce-net.invalid.") == 0 || + std::strcmp(hostname, "guest-sce-net.invalid") == 0)); + Require(family == AF_INET || family == AF_INET6); return false; } From 2f8177d7b4a983cacd072d22c1221db344a2fa6c Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Wed, 21 Oct 2026 12:24:36 -0300 Subject: [PATCH 1071/1360] fix(libkernel): keep the range sceKernelReleaseFlexibleMemory releases mapped Measured on firmware 13.60: the released range stays mapped with its protection and contents, and a fixed no-overwrite map over it fails with ENOMEM; a null or unaligned address, an unaligned length or a range that wraps returns EINVAL, checked before a zero length, which returns 0, as does a range with nothing mapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libkernel/DirectMemory/Export.cpp | 6 +++++- core/libs/tests/GuestMemory.cpp | 21 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index bd93f135d1..1afee54e9f 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -320,7 +320,11 @@ int APS5_VABI sceKernelMmap(void* addr, size_t len, int prot, int flags, int fd, } int APS5_VABI sceKernelReleaseFlexibleMemory(void* addr, size_t len) { - return sceKernelMunmap(reinterpret_cast<uint64_t>(addr), len); + const auto start = reinterpret_cast<uintptr_t>(addr); + if (start == 0 || (start & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || len > std::numeric_limits<uintptr_t>::max() - start) + return SCE_KERNEL_ERROR_EINVAL; + if (len != 0) _releaseFlexible(start, len); + return 0; } int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d03a35de70..72532181e1 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -113,11 +113,30 @@ static void CheckReleaseFlexibleMemory() { Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); static_cast<volatile unsigned char*>(mapped)[length - 1] = 1; Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + auto* bytes = static_cast<volatile unsigned char*>(mapped); + Require(sceKernelMprotect(mapped, 0x4000, 1) == 0); Require(sceKernelReleaseFlexibleMemory(mapped, length) == 0); Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0 && info.protection == 1); + Require(sceKernelVirtualQuery(static_cast<unsigned char*>(mapped) + length - 1, 0, &info, sizeof(info)) == 0 && info.protection == 3); + Require(bytes[length - 1] == 1); + bytes[length - 2] = 2; + Require(sceKernelReleaseFlexibleMemory(mapped, length) == 0); + Require(sceKernelReleaseFlexibleMemory(mapped, 0) == 0); + Require(sceKernelReleaseFlexibleMemory(reinterpret_cast<void*>(std::uintptr_t{0x7ff000000000}), length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + Require(sceKernelReleaseFlexibleMemory(nullptr, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(static_cast<unsigned char*>(mapped) + 1, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(mapped, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(reinterpret_cast<void*>(~std::uintptr_t{0x3fff}), 0x8000) == SCE_KERNEL_ERROR_EINVAL); void* again = mapped; - Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == 0 && again == mapped); + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == SCE_KERNEL_ERROR_ENOMEM); + Require(bytes[length - 1] == 1 && bytes[length - 2] == 2); + again = mapped; + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x10) == 0 && again == mapped); Require(static_cast<volatile unsigned char*>(again)[length - 1] == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); Require(sceKernelMunmap(again, length) == 0); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 08c6612c5f..62f1f84860 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -50,7 +50,7 @@ - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. -- [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 +- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [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 From 0db5b78405844f06a9f8bcd12444afeaa9831fa0 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Wed, 21 Oct 2026 12:44:03 -0300 Subject: [PATCH 1072/1360] test(libkernel): cover 4 KiB offsets in sceKernelReleaseFlexibleMemory Measured on firmware 13.60: an address or length aligned to 4 KiB but not to 16 KiB returns EINVAL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 72532181e1..70aede6349 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -129,6 +129,8 @@ static void CheckReleaseFlexibleMemory() { Require(sceKernelReleaseFlexibleMemory(nullptr, 0) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseFlexibleMemory(static_cast<unsigned char*>(mapped) + 1, 0) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseFlexibleMemory(mapped, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(static_cast<unsigned char*>(mapped) + 0x1000, 0x4000) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(mapped, 0x6000) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseFlexibleMemory(reinterpret_cast<void*>(~std::uintptr_t{0x3fff}), 0x8000) == SCE_KERNEL_ERROR_EINVAL); void* again = mapped; Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == SCE_KERNEL_ERROR_ENOMEM); From c399333e7aab4af43bd7a991b567bc80fd158dca Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Wed, 21 Oct 2026 13:03:46 -0300 Subject: [PATCH 1073/1360] docs(libkernel): record that released flexible pages keep their host memory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 62f1f84860..48a25923c7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -50,7 +50,7 @@ - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. -- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. +- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [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 From f8233a2738788437b6f0c19924c2db269a711f19 Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Thu, 22 Oct 2026 22:09:31 +0200 Subject: [PATCH 1074/1360] fix(libScePad): open the special port with a handle that reads as disconnected scePadOpen returned DEVICE_NOT_CONNECTED (0x80920007) for a special port (type 2) since #3838, which guessed the console behaviour. A PS4 Pro on firmware 10.71 with no special controller connected opens the special port of any valid user and returns a handle: scePadRead on it returns one record with connected false, scePadGetControllerInformation succeeds, and scePadRead(-1) is INVALID_HANDLE. Unity's PS4 input module opens the standard and the special port of a user and keeps the pad handle only if both opens succeed; otherwise it calls scePadRead with handle -1 every frame and the title never sees a button. Knowledge is Power (CUSA09558) and Knowledge is Power: Decades (CUSA12435) had no controller input for that reason. The special port now opens PAD_SPECIAL_HANDLE, tracked apart from the standard handle by scePadClose and scePadGetHandle. scePadRead and scePadReadState on it return a zeroed record, scePadGetControllerInformation and scePadDeviceClassParseData zeroed structures, and the other pad calls reject it with INVALID_HANDLE. The standard handle still reads its own queue, so the double read of one queue that #3838 fixed does not return. scePadOpenExt is unchanged. The unmeasured parts are recorded in docs/dev/debt/libScePad.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- core/libs/prx/libScePad/Export.cpp | 34 ++++++++++++---- core/libs/prx/libScePad/include/Pad.hpp | 1 + core/libs/tests/GuestPad.cpp | 53 +++++++++++++++++++++++-- docs/dev/debt/libScePad.md | 2 +- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 232ba2f6d3..635a68d135 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -108,8 +108,13 @@ constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast<int>(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast<int>(0x80920008); bool g_opened = false; +bool g_specialOpened = false; std::atomic_bool g_initialized{false}; +bool SpecialHandle(int handle) { + return g_specialOpened && handle == PAD_SPECIAL_HANDLE; +} + bool ValidPort(int userId, int type, int index) { const bool personalPort = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL; const bool systemRemote = userId == PAD_USER_ID_SYSTEM && type == PAD_PORT_TYPE_REMOTE; @@ -137,6 +142,10 @@ static_assert(sizeof(PadMotionSensorPosition) == 12); extern "C" { int APS5_VABI scePadClose_nid_postfix(int handle) { + if (SpecialHandle(handle)) { + g_specialOpened = false; + return PAD_OK; + } if (handle != PAD_HANDLE) { return PAD_ERROR_INVALID_HANDLE; } @@ -168,22 +177,24 @@ int APS5_VABI scePadDeviceClassGetExtendedInformation(int handle, PadDeviceClass } int APS5_VABI scePadDeviceClassParseData(int handle, const PadData* data, PadDeviceClassData* class_data) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!SpecialHandle(handle) && handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr || class_data == nullptr) return PAD_ERROR_INVALID_ARG; std::memset(class_data, 0, sizeof(*class_data)); + if (SpecialHandle(handle)) return PAD_OK; class_data->deviceClass = PAD_DEVICE_CLASS_STANDARD; class_data->dataValid = data->connected; return PAD_OK; } int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformation* info) { - if (handle != PAD_HANDLE) { + if (!SpecialHandle(handle) && handle != PAD_HANDLE) { return PAD_ERROR_INVALID_HANDLE; } if (info == nullptr) { return PAD_ERROR_INVALID_ARG; } std::memset(info, 0, sizeof(*info)); + if (SpecialHandle(handle)) return PAD_OK; info->touchPadInfo.pixelDensity = 44.86f; info->touchPadInfo.resolution.x = kTouchPadWidth; info->touchPadInfo.resolution.y = kTouchPadHeight; @@ -197,7 +208,9 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } int APS5_VABI scePadGetHandle(int user_id, int type, int index) { - if (!g_opened || !ValidPort(user_id, type, index) || type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NO_HANDLE; + if (!ValidPort(user_id, type, index)) return PAD_ERROR_DEVICE_NO_HANDLE; + if (type == PAD_PORT_TYPE_SPECIAL) return g_specialOpened ? PAD_SPECIAL_HANDLE : PAD_ERROR_DEVICE_NO_HANDLE; + if (!g_opened) return PAD_ERROR_DEVICE_NO_HANDLE; return PAD_HANDLE; } @@ -261,7 +274,10 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void if (!ValidPort(userId, type, index)) { return PAD_ERROR_INVALID_ARG; } - if (type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NOT_CONNECTED; + if (type == PAD_PORT_TYPE_SPECIAL) { + g_specialOpened = true; + return PAD_SPECIAL_HANDLE; + } g_opened = true; return PAD_HANDLE; } @@ -283,16 +299,20 @@ int APS5_VABI scePadSetFeatureReport(std::int32_t handle, std::uint8_t reportId, int APS5_VABI scePadOutputReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!SpecialHandle(handle) && handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr || num <= 0) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) { + data[0] = PadData{}; + return 1; + } return Pad::Read(data, num); } int APS5_VABI scePadReadState(int handle, PadData* data) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!SpecialHandle(handle) && handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr) return PAD_ERROR_INVALID_ARG; - *data = Pad::ReadState(); + *data = SpecialHandle(handle) ? PadData{} : Pad::ReadState(); return 0; } diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index 8a4a7406a5..1f759f4fff 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -13,6 +13,7 @@ constexpr int PAD_PORT_TYPE_SPECIAL = 2; constexpr int PAD_PORT_TYPE_REMOTE = 16; constexpr int PAD_USER_ID_SYSTEM = 0xff; constexpr int PAD_HANDLE = 1; +constexpr int PAD_SPECIAL_HANDLE = 0x20; constexpr int PAD_CONNECTION_TYPE_LOCAL = 0; constexpr int PAD_DEVICE_CLASS_STANDARD = 0; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 3cc56175b8..e741ba4ddd 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -31,6 +31,7 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadDeviceClassParseData(int, const PadData*, PadDeviceClassData*); int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); @@ -307,6 +308,48 @@ static void CheckReadStateHandle(int handle) { Require(scePadReadState(handle, nullptr) == PAD_ERROR_INVALID_ARG); } +static void CheckSpecialPort(int user, int handle) { + constexpr int noHandle = static_cast<int>(0x80920008); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special > 0 && special != handle); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == special); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == special); + Require(scePadGetHandle(user, 0, 0) == handle); + PadData data[2]; + std::memset(data, 0xff, sizeof(data)); + Require(scePadRead_nid_postfix(special, nullptr, 1) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(special, data, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(special, data, 2) == 1); + Require(!data[0].connected && data[0].connected_count == 0 && data[0].buttons == 0 && data[0].timestamp == 0); + Require(data[1].buttons == 0xffffffff); + std::memset(data, 0xff, sizeof(data[0])); + Require(scePadReadState(special, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadReadState(special, data) == PAD_OK); + Require(!data[0].connected && data[0].connected_count == 0 && data[0].buttons == 0); + PadControllerInformation info; + std::memset(&info, 0xff, sizeof(info)); + Require(scePadGetControllerInformation(special, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetControllerInformation(special, &info) == PAD_OK); + Require(!info.connected && info.connectedCount == 0 && info.deviceClass == 0); + PadDeviceClassData classData; + std::memset(&classData, 0xff, sizeof(classData)); + Require(scePadDeviceClassParseData(special, data, &classData) == PAD_OK); + Require(!classData.dataValid && classData.deviceClass == 0); + Require(scePadResetLightBar(special) == PAD_ERROR_INVALID_HANDLE); + Require(scePadReadState(handle, data) == PAD_OK); + Require(data[0].connected); + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.connected && info.connectedCount == 1); + Require(scePadClose_nid_postfix(special) == PAD_OK); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + Require(scePadClose_nid_postfix(special) == PAD_ERROR_INVALID_HANDLE); + Require(scePadRead_nid_postfix(special, data, 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadReadState(special, data) == PAD_ERROR_INVALID_HANDLE); + Require(scePadGetControllerInformation(special, &info) == PAD_ERROR_INVALID_HANDLE); + Require(scePadGetHandle(user, 0, 0) == handle); + Require(scePadReadState(handle, data) == PAD_OK); +} + static void CheckRemoteController(int handle) { bool remote = true; Require(scePadIsRemoteController(handle + 1, &remote) == PAD_ERROR_INVALID_HANDLE); @@ -437,17 +480,19 @@ int main() { Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 1, wheel) == PAD_ERROR_INVALID_ARG); Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(user, 0, 0) == noHandle); - Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast<int>(0x80920007)); + const int earlySpecial = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(earlySpecial > 0); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == earlySpecial); + Require(scePadClose_nid_postfix(earlySpecial) == PAD_OK); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); Require(scePadInit_nid_postfix() == 0); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadGetHandle(user, 0, 0) == handle); - Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast<int>(0x80920007)); - Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); - Require(scePadGetHandle(user, 0, 0) == handle); + CheckSpecialPort(user, handle); CheckTiltCorrection(handle); CheckAngularVelocityDeadband(handle); CheckTouchContact(); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 31435d7262..6638a0ed21 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -12,7 +12,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists -- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - `scePadOpen` opens a special port (type 2) with `PAD_SPECIAL_HANDLE`, whose reads return a zeroed record (`connected` false), whose controller information and device class data are zeroed, and which the other pad calls reject with `INVALID_HANDLE`. A PS4 Pro (firmware 10.71) with no special controller opens the port for any valid user, reads one record with `connected` false, succeeds in `scePadGetControllerInformation` and rejects handle -1; the handle value, the other fields, the other calls and a PS5 are not measured. `scePadOpenExt` handles only special ports and returns `DEVICE_NOT_CONNECTED` (0x80920007), unmeasured; PPSA01317 passes a 16-byte param naming the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect From 3aee4b3b09efb3a18657ac8a6231851bd95ed3b3 Mon Sep 17 00:00:00 2001 From: 3mro777o <3mro777o@gmail.com> Date: Sun, 18 Oct 2026 12:40:50 +0200 Subject: [PATCH 1075/1360] feat(libSceVdecsw): implement sceVdecswGetHevcPictureInfo Vdecsw read every decoded picture's info through Videodec2's AVC picture info, which throws for HEVC, so an HEVC decoder failed on its first picture. Videodec2 now hands Vdecsw a codec-independent picture header, records whether an HEVC access unit starts with an IDR slice, and Vdecsw keeps the AVC or the 0xb8-byte HEVC picture info for each picture. The HEVC test stream moves to HevcFixture.hpp so the Vdecsw test can decode it. --- core/libs/prx/libSceVdecsw/Export.cpp | 182 ++++++++++++++++------- core/libs/prx/libSceVideodec2/Export.cpp | 40 ++++- core/libs/tests/GuestVdecsw.cpp | 113 +++++++++++++- core/libs/tests/GuestVideodec2.cpp | 81 +--------- core/libs/tests/HevcFixture.hpp | 88 +++++++++++ docs/dev/debt/libSceVdecsw.md | 6 +- 6 files changed, 364 insertions(+), 146 deletions(-) create mode 100644 core/libs/tests/HevcFixture.hpp diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index 4c70113346..f417b3a8d4 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -8,6 +8,7 @@ #include <mutex> #include <stdexcept> #include <thread> +#include <variant> #include <vector> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -19,6 +20,8 @@ constexpr int VdecswErrorInputQueueEmpty = static_cast<int>(0x81510116u); constexpr int VdecswErrorDecodePending = static_cast<int>(0x81510117u); constexpr std::uint32_t CodecVp9 = 2382845; constexpr std::size_t MaxQueuedPictures = 4; +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t CodecHevc = 974921; struct InputData { std::uint64_t thisSize; @@ -99,10 +102,55 @@ struct AvcPictureInfo { std::uint8_t vui[44]; }; +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresentFlag; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresentFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignal[8]; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicateFlag; + std::uint32_t conformanceWindowFlag; + std::uint32_t confWinLeftOffset; + std::uint32_t confWinRightOffset; + std::uint32_t confWinTopOffset; + std::uint32_t confWinBottomOffset; + std::uint32_t defaultDisplayWindow[5]; + std::uint8_t chromaLocation[4]; + std::uint8_t presentFlags[11]; + std::uint8_t idrPictureFlag; + std::uint8_t irapPictureFlag; + std::uint8_t generalProfileSpace; + std::uint8_t generalTierFlag; + std::uint8_t generalProfileIdc; + std::uint8_t generalSourceFlags[3]; + std::uint8_t generalLevelIdc; + std::uint8_t subLayer[9]; + std::uint8_t frameCroppingFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + static_assert(sizeof(InputData) == 0x30 && sizeof(InputResult) == 0x18 && sizeof(FrameBuffer) == 0x18); static_assert(sizeof(Vp9PictureInfo) == 0x58 && offsetof(Vp9PictureInfo, ptsData) == 0x10 && offsetof(Vp9PictureInfo, profile) == 0x28 && offsetof(Vp9PictureInfo, bitDepth) == 0x38 && offsetof(Vp9PictureInfo, colorSpace) == 0x3c && offsetof(Vp9PictureInfo, renderHeight) == 0x50); static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == 9 && offsetof(OutputInfo, pictureCount) == 0xb && offsetof(OutputInfo, isDiscardedFrame) == 0x1c); static_assert(sizeof(AvcPictureInfo) == 0x78 && offsetof(AvcPictureInfo, picWidthInMbsMinus1) == 0x2c && offsetof(AvcPictureInfo, frameCropBottomOffset) == 0x44); +static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, picWidthInLumaSamples) == 0x28 && offsetof(HevcPictureInfo, sarWidth) == 0x42 && offsetof(HevcPictureInfo, picStruct) == 0x50 && offsetof(HevcPictureInfo, conformanceWindowFlag) == 0x5c && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, generalProfileIdc) == 0x97 && offsetof(HevcPictureInfo, generalLevelIdc) == 0x9b && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5 && offsetof(HevcPictureInfo, frameCropBottomOffset) == 0xb4); struct Videodec2FrameBuffer { std::uint64_t thisSize; @@ -126,19 +174,6 @@ struct Videodec2OutputInfo { std::uint32_t framePitchInBytes; }; -struct Videodec2AvcPictureInfo { - std::uint64_t thisSize; - bool isValid; - std::uint64_t ptsData; - std::uint64_t dtsData; - std::uint64_t attachedData; - std::uint8_t idrPictureFlag; - std::uint8_t profileIdc; - std::uint8_t levelIdc; - std::uint32_t picWidthInLumaSamples; - std::uint32_t picHeightInLumaSamples; -}; - struct Videodec2Crop { std::uint32_t left; std::uint32_t right; @@ -146,6 +181,17 @@ struct Videodec2Crop { std::uint32_t bottom; }; +struct Videodec2PictureHeader { + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t attached; + bool keyFrame; + bool hevcIdr; + std::uint32_t profile; + std::uint32_t level; + Videodec2Crop crop; +}; + struct DecoderMemoryInfo { std::uint64_t thisSize; std::uint64_t cpuMemorySize; @@ -171,8 +217,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, Videodec2FrameBuffer* frame, Videodec2OutputInfo* output); int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const Videodec2OutputInfo* output, Vp9PictureInfo* info); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle); -int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Videodec2OutputInfo* output, Videodec2AvcPictureInfo* first, Videodec2AvcPictureInfo* second); -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2Crop* crop); +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2PictureHeader* header); } namespace { @@ -187,10 +232,12 @@ struct ConsumedInput { std::uint32_t frames; }; +using PictureInfo = std::variant<AvcPictureInfo, HevcPictureInfo>; + struct Picture { std::vector<std::uint8_t> pixels; Videodec2OutputInfo output; - AvcPictureInfo info; + PictureInfo info; }; struct Decoder { @@ -200,7 +247,7 @@ struct Decoder { std::deque<ConsumedInput> consumed; std::deque<FrameBuffer> outputs; std::deque<Picture> pictures; - std::map<const void*, AvcPictureInfo> delivered; + std::map<const void*, PictureInfo> delivered; bool finalized = false; bool drained = false; std::map<const void*, Vp9PictureInfo> deliveredVp9; @@ -216,27 +263,52 @@ std::shared_ptr<Decoder> findDecoder(std::uint64_t handle) { return found->second; } -AvcPictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) { - Videodec2AvcPictureInfo source{sizeof(Videodec2AvcPictureInfo)}; - sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &source, nullptr); - if (!source.isValid) throw std::runtime_error("Vdecsw: decoded picture has no picture info"); - Videodec2Crop crop{}; - Videodec2PictureCrop_nid_no_patch(handle, output.frameBuffer, &crop); +PictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) { + Videodec2PictureHeader header{}; + Videodec2PictureHeader_nid_no_patch(handle, output.frameBuffer, &header); + const auto& crop = header.crop; + const bool cropped = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; + if (output.codecType == CodecHevc) { + HevcPictureInfo info{}; + info.thisSize = sizeof(HevcPictureInfo); + info.isValid = true; + info.ptsData = header.pts; + info.dtsData = header.dts; + info.attachedData = header.attached; + info.picWidthInLumaSamples = output.frameWidth; + info.picHeightInLumaSamples = output.frameHeight; + info.conformanceWindowFlag = cropped; + info.confWinLeftOffset = crop.left / 2u; + info.confWinRightOffset = crop.right / 2u; + info.confWinTopOffset = crop.top / 2u; + info.confWinBottomOffset = crop.bottom / 2u; + info.idrPictureFlag = header.hevcIdr; + info.irapPictureFlag = header.keyFrame; + info.generalProfileIdc = static_cast<std::uint8_t>(header.profile); + info.generalLevelIdc = static_cast<std::uint8_t>(header.level); + info.frameCroppingFlag = cropped; + info.frameCropLeftOffset = info.confWinLeftOffset; + info.frameCropRightOffset = info.confWinRightOffset; + info.frameCropTopOffset = info.confWinTopOffset; + info.frameCropBottomOffset = info.confWinBottomOffset; + return info; + } + if (output.codecType != CodecAvc) throw std::runtime_error("Vdecsw: picture info is only implemented for H.264 and HEVC"); const auto widthInMbs = (output.frameWidth + 15u) / 16u; const auto heightInMbs = (output.frameHeight + 15u) / 16u; AvcPictureInfo info{}; info.thisSize = sizeof(AvcPictureInfo); info.isValid = true; - info.ptsData = source.ptsData; - info.dtsData = source.dtsData; - info.attachedData = source.attachedData; - info.idrPictureFlag = source.idrPictureFlag; - info.profileIdc = source.profileIdc; - info.levelIdc = source.levelIdc; + info.ptsData = header.pts; + info.dtsData = header.dts; + info.attachedData = header.attached; + info.idrPictureFlag = header.keyFrame; + info.profileIdc = static_cast<std::uint8_t>(header.profile); + info.levelIdc = static_cast<std::uint8_t>(header.level); info.picWidthInMbsMinus1 = widthInMbs - 1u; info.picHeightInMapUnitsMinus1 = heightInMbs - 1u; info.frameMbsOnlyFlag = 1; - info.frameCroppingFlag = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; + info.frameCroppingFlag = cropped; info.frameCropLeftOffset = crop.left / 2u; info.frameCropRightOffset = crop.right / 2u; info.frameCropTopOffset = crop.top / 2u; @@ -251,6 +323,29 @@ Vp9PictureInfo vp9PictureInfo(const Videodec2OutputInfo& output) { return info; } +template <typename TInfo> +int deliveredPictureInfo(const OutputInfo* output, TInfo* info) { + if (output == nullptr || info == nullptr || info->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); + if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); + if (output->pictureCount == 0) return 0; + std::vector<std::shared_ptr<Decoder>> all; + { + std::lock_guard lock(decodersMutex); + for (const auto& [handle, decoder] : decoders) all.push_back(decoder); + } + for (const auto& decoder : all) { + std::lock_guard lock(decoder->mutex); + const auto found = decoder->delivered.find(output->frameBuffer); + if (found == decoder->delivered.end()) continue; + const auto* source = std::get_if<TInfo>(&found->second); + if (source == nullptr) throw std::runtime_error("Vdecsw: picture info requested for another codec"); + const auto bytes = std::min<std::uint64_t>(info->thisSize, sizeof(TInfo)) - sizeof(std::uint64_t); + std::memcpy(reinterpret_cast<std::uint8_t*>(info) + sizeof(std::uint64_t), reinterpret_cast<const std::uint8_t*>(source) + sizeof(std::uint64_t), bytes); + return 0; + } + throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); +} + void advance(std::uint64_t handle, Decoder& decoder) { while (decoder.pictures.size() < MaxQueuedPictures) { std::vector<std::uint8_t> pixels(decoder.frameBytes); @@ -270,7 +365,7 @@ void advance(std::uint64_t handle, Decoder& decoder) { return; } if (output.isValid) { - const auto info = output.codecType == CodecVp9 ? AvcPictureInfo{} : pictureInfo(handle, output); + const auto info = output.codecType == CodecVp9 ? PictureInfo{} : pictureInfo(handle, output); decoder.pictures.push_back({std::move(pixels), output, info}); } } @@ -414,31 +509,12 @@ int APS5_VABI sceVdecswGetPictureInfo(const OutputInfo* output, void* first, voi } int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { - if (output == nullptr || first == nullptr || first->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); - if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); - if (output->pictureCount == 0) return 0; static_cast<void>(second); - std::vector<std::shared_ptr<Decoder>> all; - { - std::lock_guard lock(decodersMutex); - for (const auto& [handle, decoder] : decoders) all.push_back(decoder); - } - for (const auto& decoder : all) { - std::lock_guard lock(decoder->mutex); - const auto found = decoder->delivered.find(output->frameBuffer); - if (found == decoder->delivered.end()) continue; - const auto bytes = std::min<std::uint64_t>(first->thisSize, sizeof(AvcPictureInfo)) - sizeof(std::uint64_t); - std::memcpy(reinterpret_cast<std::uint8_t*>(first) + sizeof(std::uint64_t), reinterpret_cast<const std::uint8_t*>(&found->second) + sizeof(std::uint64_t), bytes); - return 0; - } - throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); + return deliveredPictureInfo(output, first); } -int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, void* picture) { - (void)output; - (void)picture; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, HevcPictureInfo* picture) { + return deliveredPictureInfo(output, picture); } int APS5_VABI sceVdecswGetVp9PictureInfo(const OutputInfo* output, Vp9PictureInfo* picture) { diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 2d51f7cb70..ebf22265b1 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -283,6 +283,18 @@ struct Picture { bool idr = false; Vp9PictureInfo vp9{}; Crop crop{}; + bool hevcIdr = false; +}; + +struct PictureHeader { + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t attached; + bool keyFrame; + bool hevcIdr; + std::uint32_t profile; + std::uint32_t level; + Crop crop; }; struct FrameDeleter { @@ -437,6 +449,16 @@ bool CarriesAvcSlice(const std::vector<std::uint8_t>& unit) { return false; } +bool StartsHevcIdr(const std::vector<std::uint8_t>& unit) { + for (std::size_t index = 0; index + 3 < unit.size(); ++index) { + if (unit[index] != 0 || unit[index + 1] != 0 || unit[index + 2] != 1) continue; + const auto type = (unit[index + 3] >> 1u) & 0x3fu; + if (type < 32) return type == 19 || type == 20; + index += 2; + } + return false; +} + void Receive(Decoder& decoder) { for (;;) { FramePointer frame(av_frame_alloc()); @@ -664,7 +686,9 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa std::memcpy(packet->data + decoder.parameterSets.size(), unit->data(), unit->size()); decoder.parameterSets.clear(); packet->pts = decoder.nextKey; - decoder.inputs.emplace(decoder.nextKey++, picture); + auto tagged = picture; + tagged.hevcIdr = decoder.codec == CodecHevc && StartsHevcIdr(*unit); + decoder.inputs.emplace(decoder.nextKey++, tagged); Send(decoder, packet); EmitDecoded(handle, decoder, frame, output); return 0; @@ -776,15 +800,23 @@ int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output return 0; } -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { - if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, PictureHeader* header) { + if (!header) throw std::runtime_error("Videodec2: invalid argument pointer"); const auto found = FindDecoder(handle); if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); const auto picture = decoder.pictures.find(frameBuffer); if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); - *crop = picture->second.crop; + const auto& stored = picture->second; + *header = {stored.pts, stored.dts, stored.attached, stored.idr, stored.hevcIdr, decoder.profile, decoder.level, stored.crop}; +} + +void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { + if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); + PictureHeader header{}; + Videodec2PictureHeader_nid_no_patch(handle, frameBuffer, &header); + *crop = header.crop; } } diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 14fdca44a9..2cedcdc2ee 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -9,6 +9,7 @@ #include "prx/libc/include/General.hpp" #include "Vp9Fixture.hpp" #include "H264Fixture.hpp" +#include "HevcFixture.hpp" struct ComputeMemoryInfo { std::uint64_t thisSize; @@ -133,6 +134,51 @@ struct AvcPictureInfo { std::uint8_t vui[44]; }; +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresentFlag; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresentFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignal[8]; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicateFlag; + std::uint32_t conformanceWindowFlag; + std::uint32_t confWinLeftOffset; + std::uint32_t confWinRightOffset; + std::uint32_t confWinTopOffset; + std::uint32_t confWinBottomOffset; + std::uint32_t defaultDisplayWindow[5]; + std::uint8_t chromaLocation[4]; + std::uint8_t presentFlags[11]; + std::uint8_t idrPictureFlag; + std::uint8_t irapPictureFlag; + std::uint8_t generalProfileSpace; + std::uint8_t generalTierFlag; + std::uint8_t generalProfileIdc; + std::uint8_t generalSourceFlags[3]; + std::uint8_t generalLevelIdc; + std::uint8_t subLayer[9]; + std::uint8_t frameCroppingFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5); static_assert(sizeof(DecoderConfigInfo) == 0x50 && sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == 9 && offsetof(OutputInfo, pictureCount) == 0xb && sizeof(AvcPictureInfo) == 0x78); extern "C" { @@ -150,7 +196,7 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswGetVp9PictureInfo(const OutputInfo*, Vp9PictureInfo*); int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); -int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, void*); +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, HevcPictureInfo*); int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t); int APS5_VABI sceVdecswResetDecoder(std::uint64_t); } @@ -158,11 +204,15 @@ int APS5_VABI sceVdecswResetDecoder(std::uint64_t); namespace { using namespace H264Fixture; +using namespace HevcFixture; using namespace Vp9Fixture; constexpr int OutputPending = static_cast<int>(0x81510115u); constexpr int InputQueueEmpty = static_cast<int>(0x81510116u); +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; +constexpr std::array<std::size_t, 6> HevcDisplayOrderUnits{0, 3, 2, 1, 5, 4}; void check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -173,12 +223,12 @@ struct Session { std::uint64_t decoder = 0; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; - Session() { + explicit Session(std::uint32_t codec = CodecAvc, std::uint32_t profile = 100, std::uint32_t level = 42) { ComputeMemoryInfo compute{sizeof(ComputeMemoryInfo)}; check(sceVdecswQueryComputeMemoryInfo(&compute) == 0 && compute.cpuGpuMemorySize != 0, "compute memory query failed"); const ComputeConfigInfo computeConfig{sizeof(ComputeConfigInfo), 3, 3, true}; check(sceVdecswAllocateComputeQueue(&computeConfig, &compute, &queue) == 0, "compute queue allocation failed"); - DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, 1, 100, 42, static_cast<std::int32_t>(Width), static_cast<std::int32_t>(Height), 4, 3, queue, 0x3f, 0, true, {}}; + DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, level, static_cast<std::int32_t>(Width), static_cast<std::int32_t>(Height), 4, 3, queue, 0x3f, 0, true, {}}; check(sceVdecswQueryDecoderMemoryInfo(&config, &memory) == 0 && memory.maxFrameBufferSize != 0, "decoder memory query failed"); check(sceVdecswCreateDecoder(&config, &memory, &decoder) == 0, "decoder creation failed"); } @@ -245,6 +295,57 @@ void testDecode() { check(sceVdecswTrySyncDecodeInput(session.decoder, &result) == InputQueueEmpty, "a finalized decoder did not report an empty input queue"); } +void testHevc() { + Session session(CodecHevc, 1, 30); + const auto units = hevcUnits(HevcStream.data(), HevcStream.size()); + check(units.size() == HevcHashes.size(), "unexpected HEVC access unit count"); + std::vector<std::uint8_t> buffer(session.memory.maxFrameBufferSize); + std::size_t pictures = 0; + bool ended = false; + const auto drain = [&] { + for (;;) { + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "HEVC output buffer was rejected"); + OutputInfo output{sizeof(OutputInfo)}; + const auto status = sceVdecswTrySyncDecodeOutput(session.decoder, &output); + if (status == OutputPending) return; + check(status == 0, "HEVC output sync failed"); + if (!output.isValid) { + ended = true; + return; + } + check(pictures < HevcHashes.size() && output.codecType == CodecHevc && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height, "unexpected HEVC picture geometry"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "HEVC picture info missing"); + const auto unit = HevcDisplayOrderUnits[pictures]; + check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "HEVC pictures out of display order"); + check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.irapPictureFlag == (pictures == 0 ? 1 : 0), "HEVC random access flags differ"); + check(info.generalProfileIdc == 1 && info.generalLevelIdc == 30 && info.bitDepthLumaMinus8 == 0 && info.bitDepthChromaMinus8 == 0, "HEVC profile, level or bit depth differs"); + check(info.picWidthInLumaSamples == Width && info.picHeightInLumaSamples == Height && info.conformanceWindowFlag == 0 && info.frameCroppingFlag == 0, "HEVC picture geometry in the picture info is wrong"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + bool avcThrows = false; + try { + sceVdecswGetAvcPictureInfo(&output, &avc, nullptr); + } catch (const std::exception&) { + avcThrows = true; + } + check(avcThrows, "AVC picture info was returned for an HEVC picture"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcHashes[pictures], "HEVC picture " + std::to_string(pictures) + " differs from the reference"); + ++pictures; + } + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswTrySyncDecodeInput(session.decoder, &consumed) == 0 && consumed.decodedAu == units[unit].data(), "an HEVC access unit was not consumed"); + drain(); + } + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "HEVC finalize failed"); + while (!ended) drain(); + check(pictures == HevcHashes.size(), "missing HEVC pictures after finalizing the sequence"); +} + void testWithoutOutput() { Session session; const auto units = accessUnits(false); @@ -278,13 +379,14 @@ void testSyncWrappers() { check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "finalize failed"); OutputInfo output{sizeof(OutputInfo)}; check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.isLastFrame, "sync output did not return the finalized picture"); + HevcPictureInfo hevc{sizeof(HevcPictureInfo)}; bool hevcThrows = false; try { - sceVdecswGetHevcPictureInfo(&output, &result); + sceVdecswGetHevcPictureInfo(&output, &hevc); } catch (const std::exception&) { hevcThrows = true; } - check(hevcThrows, "GetHevcPictureInfo must still throw"); + check(hevcThrows, "HEVC picture info was returned for an H.264 picture"); } struct Vp9Session { @@ -460,6 +562,7 @@ void testVp9PictureInfoOfAvcPicture() { int main() { try { testDecode(); + testHevc(); testWithoutOutput(); testSyncWrappers(); testVp9PictureInfo(); diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 58b3f5240e..c5bc9dea25 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -11,6 +11,7 @@ #include <vector> #include "prx/libc/include/General.hpp" #include "H264Fixture.hpp" +#include "HevcFixture.hpp" #include "Vp9Fixture.hpp" struct DecoderConfigInfo { @@ -119,78 +120,11 @@ int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo*, AvcPi namespace { using namespace H264Fixture; - +using namespace HevcFixture; constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; -constexpr std::array<std::uint8_t, 933> HevcStream{ - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, - 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, - 0x95, 0x90, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x96, 0x59, 0x59, 0x2e, - 0x4c, 0xaf, 0x01, 0x68, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x40, - 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, - 0x08, 0xe0, 0xbb, 0xcb, 0xc1, 0x71, 0x26, 0x0f, 0xbf, 0x6c, 0xcb, 0x4d, 0x2b, 0x2c, 0xf4, 0x08, - 0xe0, 0x17, 0xeb, 0x16, 0xcc, 0x07, 0xd8, 0xdd, 0x11, 0xf8, 0x5e, 0x73, 0x79, 0x57, 0xcb, 0x69, - 0xa3, 0x1c, 0x9c, 0x43, 0x14, 0xdb, 0x61, 0x0e, 0xc5, 0xc6, 0x9c, 0x9c, 0xf8, 0xf2, 0x07, 0x35, - 0xf1, 0x98, 0x6c, 0x1a, 0x86, 0x53, 0x5a, 0xcd, 0x4d, 0xda, 0xff, 0xfb, 0x01, 0xda, 0xc3, 0x61, - 0xb7, 0x8c, 0xae, 0xc9, 0x76, 0xe4, 0x05, 0xd6, 0xfd, 0xe4, 0xcd, 0x93, 0x2c, 0xb1, 0x41, 0x29, - 0xce, 0x22, 0xf7, 0x77, 0x64, 0x0f, 0x1f, 0xd2, 0xc3, 0x68, 0x32, 0x2f, 0x83, 0xd7, 0x53, 0xbe, - 0x23, 0xff, 0x8c, 0x46, 0x18, 0x33, 0x90, 0x28, 0xa3, 0xef, 0xd7, 0xb1, 0x46, 0x84, 0xa0, 0xc2, - 0xd4, 0xd9, 0xce, 0xdf, 0x0a, 0x05, 0xec, 0xad, 0x63, 0xa4, 0x78, 0x27, 0x80, 0x4e, 0x3b, 0x4c, - 0xa6, 0x42, 0xbc, 0xe3, 0x39, 0x4d, 0xc4, 0xd3, 0x44, 0xf1, 0x22, 0xd0, 0x64, 0x9f, 0xd0, 0x88, - 0x7f, 0x39, 0x6d, 0x06, 0xc7, 0x79, 0x75, 0x2a, 0x44, 0x3d, 0x21, 0x41, 0x53, 0x60, 0x8d, 0x08, - 0x9f, 0xd0, 0x34, 0xa6, 0x72, 0x70, 0x3b, 0xa9, 0xfa, 0x62, 0xb1, 0x19, 0x9b, 0x0b, 0xa2, 0xfc, - 0xb4, 0xb3, 0x2c, 0x58, 0xad, 0xc6, 0xf3, 0xea, 0xc4, 0xd0, 0xf9, 0xea, 0x9e, 0xa7, 0xa4, 0x87, - 0xb8, 0x19, 0x68, 0xf1, 0xc1, 0x6b, 0x87, 0xa5, 0x99, 0xa0, 0x69, 0x24, 0x28, 0x80, 0xba, 0xd2, - 0x57, 0xc6, 0xc0, 0x1e, 0x69, 0x1b, 0xce, 0x96, 0xcd, 0x85, 0x9d, 0x59, 0x32, 0x92, 0x71, 0xe7, - 0x15, 0xf2, 0x9b, 0x71, 0x4b, 0xd4, 0xb6, 0x99, 0xb0, 0x71, 0x9b, 0x9b, 0xdb, 0x33, 0x28, 0x86, - 0x5e, 0x44, 0xf3, 0x5d, 0x5d, 0x7b, 0x87, 0x6c, 0x99, 0x8a, 0x6d, 0xaf, 0xf8, 0xdd, 0x39, 0x14, - 0xc0, 0x10, 0x85, 0xaf, 0x3a, 0x77, 0x60, 0x8b, 0xb7, 0x5f, 0x67, 0x45, 0xb4, 0xa7, 0xe7, 0x44, - 0x77, 0x0a, 0xb9, 0xe1, 0xb2, 0xbe, 0xde, 0x1b, 0x5a, 0xb8, 0xc8, 0x9b, 0x1f, 0x15, 0x37, 0xdc, - 0xeb, 0x55, 0x12, 0x37, 0xa1, 0xab, 0x9c, 0xcd, 0xdd, 0x42, 0x5d, 0x90, 0x89, 0xc5, 0x99, 0x54, - 0x6e, 0x9a, 0xd2, 0x71, 0x07, 0xe3, 0x81, 0x20, 0xad, 0xd4, 0x7c, 0xad, 0x69, 0xaf, 0xc7, 0x2d, - 0x7c, 0x84, 0xf1, 0x5d, 0x43, 0x4a, 0x3c, 0xf6, 0xf8, 0x7e, 0x6a, 0xb2, 0xdd, 0x00, 0xda, 0x30, - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x19, 0x5f, 0x84, - 0x31, 0x84, 0x50, 0x62, 0xcd, 0x44, 0xa6, 0x4e, 0xbb, 0x2e, 0x87, 0xda, 0x21, 0x25, 0x1c, 0x13, - 0xfe, 0xc2, 0x97, 0xa3, 0xd5, 0xfd, 0xe2, 0x62, 0xd6, 0xe5, 0x50, 0x7a, 0xda, 0x98, 0x8d, 0xcf, - 0xbe, 0x40, 0x5d, 0x69, 0xb6, 0x5f, 0xdc, 0x32, 0x68, 0x05, 0x67, 0x70, 0xfd, 0xa0, 0xd3, 0xcd, - 0x88, 0xeb, 0x12, 0x2e, 0xac, 0xf5, 0x57, 0xa2, 0xc2, 0x81, 0x22, 0x6b, 0x8d, 0x69, 0x6d, 0xac, - 0x41, 0x05, 0x5b, 0x0b, 0x79, 0xe0, 0x86, 0x05, 0x86, 0x8b, 0x02, 0x51, 0x7d, 0x6e, 0x13, 0x63, - 0x36, 0xe6, 0x89, 0x22, 0x75, 0xd9, 0xb0, 0x39, 0x27, 0xcd, 0xac, 0x78, 0xe0, 0x3a, 0x6a, 0x17, - 0x15, 0x05, 0x83, 0x3a, 0xe9, 0x38, 0x7c, 0x75, 0x38, 0xf3, 0xd8, 0x42, 0x57, 0xa1, 0xef, 0x68, - 0xb8, 0xb5, 0x44, 0xfe, 0x4d, 0x66, 0xa1, 0x09, 0xc5, 0x62, 0xff, 0x87, 0x7e, 0xdb, 0x41, 0x6f, - 0x4d, 0x5f, 0x16, 0x49, 0xef, 0xca, 0xda, 0x67, 0x01, 0x29, 0x0b, 0xd6, 0xd8, 0xfc, 0x0b, 0x22, - 0xc8, 0x28, 0xff, 0xb4, 0xb3, 0xa9, 0xf8, 0x9b, 0x78, 0x7d, 0x33, 0xbf, 0xa6, 0x9b, 0x39, 0x64, - 0x57, 0x41, 0x1f, 0x27, 0x05, 0xfc, 0xcd, 0x44, 0xcd, 0xb2, 0xea, 0x79, 0xe0, 0x00, 0x00, 0x00, - 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x02, 0x01, 0xe0, 0x44, 0x97, 0xe1, 0x8f, 0x40, 0x3d, - 0x73, 0xde, 0xdf, 0xf5, 0x1c, 0xff, 0xf6, 0xe2, 0x12, 0x16, 0x3d, 0xb6, 0x75, 0xad, 0x9e, 0xb5, - 0x2b, 0xe7, 0x4a, 0x40, 0xe9, 0x91, 0xd6, 0x86, 0x86, 0x2d, 0xcb, 0xe4, 0xb6, 0x58, 0x4a, 0x2d, - 0x7b, 0x1b, 0xdb, 0xc5, 0x48, 0x2d, 0x09, 0x4d, 0x32, 0x30, 0xde, 0xaa, 0x61, 0xc2, 0x27, 0x64, - 0x54, 0x71, 0x89, 0x32, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, - 0x00, 0x01, 0xe0, 0x24, 0xff, 0xfa, 0x2c, 0x6e, 0xdc, 0x78, 0x3d, 0x08, 0x6d, 0x3b, 0xcf, 0xcc, - 0xb6, 0x16, 0xeb, 0x9d, 0x5f, 0x46, 0x0b, 0x24, 0x0e, 0x97, 0x99, 0x06, 0x00, 0x00, 0x00, 0x01, - 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x28, 0x95, 0xd7, 0xdc, 0x41, 0xc0, 0x61, - 0x14, 0xd6, 0xee, 0xd4, 0xe8, 0x24, 0x3e, 0x7f, 0x82, 0x22, 0xb0, 0x20, 0xb9, 0xf1, 0x69, 0x9b, - 0xfe, 0xcc, 0xb7, 0x2e, 0xbd, 0xdb, 0x20, 0xfa, 0xe5, 0x97, 0xd9, 0x95, 0x1d, 0x7e, 0x49, 0x35, - 0x69, 0xdb, 0x40, 0xa3, 0x5c, 0x0f, 0x74, 0x6f, 0x00, 0x3d, 0x36, 0x05, 0xc0, 0xc4, 0x37, 0x15, - 0xf9, 0x01, 0x2d, 0xdf, 0xcb, 0x70, 0x3d, 0xfa, 0x47, 0xec, 0x80, 0x07, 0x48, 0x08, 0xc8, 0x54, - 0xd3, 0xe3, 0x00, 0xa3, 0xe4, 0xce, 0x40, 0x05, 0x0b, 0x51, 0x73, 0x56, 0x33, 0x82, 0xa8, 0x0a, - 0xc0, 0xd3, 0xc5, 0xf1, 0xeb, 0x32, 0x8e, 0x4e, 0xa7, 0x6f, 0xb2, 0x64, 0x43, 0x54, 0x71, 0xd4, - 0xaf, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x00, 0x01, 0xe0, 0x82, - 0x2f, 0x5f, 0xdc, 0x63, 0x70, 0x3f, 0xaf, 0x66, 0x75, 0x52, 0xe7, 0x7f, 0x62, 0xfb, 0x84, 0x9c, - 0xef, 0x60, 0x8f, 0xfa, 0x7e, 0x5f, 0x8b, 0x40, 0x15, 0x34, 0x69, 0xe2, 0xc4, 0xfc, 0xff, 0x53, - 0xb8, 0x1e, 0xc7, 0x20, 0x55, 0xdb, 0x5e, 0x05, 0xbd, 0x7a, 0x85, 0xb4, 0xb7, 0xc1, 0x8d, 0xdb, - 0x0f, 0xa6, 0xab, 0xb9, 0x00, 0x2a, 0xdb, 0x83, 0xa0, 0xb0, 0xc5, 0x76, 0x6e, 0xe1, 0xb8, 0x72, - 0xfd, 0x09, 0x61, 0x19, 0xe0, -}; - -constexpr std::array<std::uint64_t, 6> HevcHashes{ - 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, - 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, -}; - constexpr std::array<std::uint8_t, 658> AvcIpppStream{ 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x0a, 0xda, 0x11, 0xec, 0x04, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x0f, 0x03, 0xc4, 0x89, 0xa8, 0x00, @@ -307,17 +241,6 @@ void testFailures() { check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } -std::vector<std::vector<std::uint8_t>> hevcUnits(const std::uint8_t* stream, std::size_t size) { - std::vector<std::size_t> starts; - for (std::size_t index = 0; index + 4 < size; ++index) { - if (stream[index] == 0 && stream[index + 1] == 0 && stream[index + 2] == 0 && stream[index + 3] == 1 && ((stream[index + 4] >> 1u) & 0x3fu) == 35) starts.push_back(index); - } - starts.push_back(size); - std::vector<std::vector<std::uint8_t>> units; - for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) units.emplace_back(stream + starts[unit], stream + starts[unit + 1]); - return units; -} - using Vp9Fixture::Vp9Hashes; using Vp9Fixture::ivfFrames; diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp new file mode 100644 index 0000000000..3803431734 --- /dev/null +++ b/core/libs/tests/HevcFixture.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include <array> +#include <cstddef> +#include <cstdint> +#include <vector> + +namespace HevcFixture { + +inline constexpr std::array<std::uint8_t, 933> HevcStream{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, + 0x95, 0x90, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x96, 0x59, 0x59, 0x2e, + 0x4c, 0xaf, 0x01, 0x68, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x40, + 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, + 0x08, 0xe0, 0xbb, 0xcb, 0xc1, 0x71, 0x26, 0x0f, 0xbf, 0x6c, 0xcb, 0x4d, 0x2b, 0x2c, 0xf4, 0x08, + 0xe0, 0x17, 0xeb, 0x16, 0xcc, 0x07, 0xd8, 0xdd, 0x11, 0xf8, 0x5e, 0x73, 0x79, 0x57, 0xcb, 0x69, + 0xa3, 0x1c, 0x9c, 0x43, 0x14, 0xdb, 0x61, 0x0e, 0xc5, 0xc6, 0x9c, 0x9c, 0xf8, 0xf2, 0x07, 0x35, + 0xf1, 0x98, 0x6c, 0x1a, 0x86, 0x53, 0x5a, 0xcd, 0x4d, 0xda, 0xff, 0xfb, 0x01, 0xda, 0xc3, 0x61, + 0xb7, 0x8c, 0xae, 0xc9, 0x76, 0xe4, 0x05, 0xd6, 0xfd, 0xe4, 0xcd, 0x93, 0x2c, 0xb1, 0x41, 0x29, + 0xce, 0x22, 0xf7, 0x77, 0x64, 0x0f, 0x1f, 0xd2, 0xc3, 0x68, 0x32, 0x2f, 0x83, 0xd7, 0x53, 0xbe, + 0x23, 0xff, 0x8c, 0x46, 0x18, 0x33, 0x90, 0x28, 0xa3, 0xef, 0xd7, 0xb1, 0x46, 0x84, 0xa0, 0xc2, + 0xd4, 0xd9, 0xce, 0xdf, 0x0a, 0x05, 0xec, 0xad, 0x63, 0xa4, 0x78, 0x27, 0x80, 0x4e, 0x3b, 0x4c, + 0xa6, 0x42, 0xbc, 0xe3, 0x39, 0x4d, 0xc4, 0xd3, 0x44, 0xf1, 0x22, 0xd0, 0x64, 0x9f, 0xd0, 0x88, + 0x7f, 0x39, 0x6d, 0x06, 0xc7, 0x79, 0x75, 0x2a, 0x44, 0x3d, 0x21, 0x41, 0x53, 0x60, 0x8d, 0x08, + 0x9f, 0xd0, 0x34, 0xa6, 0x72, 0x70, 0x3b, 0xa9, 0xfa, 0x62, 0xb1, 0x19, 0x9b, 0x0b, 0xa2, 0xfc, + 0xb4, 0xb3, 0x2c, 0x58, 0xad, 0xc6, 0xf3, 0xea, 0xc4, 0xd0, 0xf9, 0xea, 0x9e, 0xa7, 0xa4, 0x87, + 0xb8, 0x19, 0x68, 0xf1, 0xc1, 0x6b, 0x87, 0xa5, 0x99, 0xa0, 0x69, 0x24, 0x28, 0x80, 0xba, 0xd2, + 0x57, 0xc6, 0xc0, 0x1e, 0x69, 0x1b, 0xce, 0x96, 0xcd, 0x85, 0x9d, 0x59, 0x32, 0x92, 0x71, 0xe7, + 0x15, 0xf2, 0x9b, 0x71, 0x4b, 0xd4, 0xb6, 0x99, 0xb0, 0x71, 0x9b, 0x9b, 0xdb, 0x33, 0x28, 0x86, + 0x5e, 0x44, 0xf3, 0x5d, 0x5d, 0x7b, 0x87, 0x6c, 0x99, 0x8a, 0x6d, 0xaf, 0xf8, 0xdd, 0x39, 0x14, + 0xc0, 0x10, 0x85, 0xaf, 0x3a, 0x77, 0x60, 0x8b, 0xb7, 0x5f, 0x67, 0x45, 0xb4, 0xa7, 0xe7, 0x44, + 0x77, 0x0a, 0xb9, 0xe1, 0xb2, 0xbe, 0xde, 0x1b, 0x5a, 0xb8, 0xc8, 0x9b, 0x1f, 0x15, 0x37, 0xdc, + 0xeb, 0x55, 0x12, 0x37, 0xa1, 0xab, 0x9c, 0xcd, 0xdd, 0x42, 0x5d, 0x90, 0x89, 0xc5, 0x99, 0x54, + 0x6e, 0x9a, 0xd2, 0x71, 0x07, 0xe3, 0x81, 0x20, 0xad, 0xd4, 0x7c, 0xad, 0x69, 0xaf, 0xc7, 0x2d, + 0x7c, 0x84, 0xf1, 0x5d, 0x43, 0x4a, 0x3c, 0xf6, 0xf8, 0x7e, 0x6a, 0xb2, 0xdd, 0x00, 0xda, 0x30, + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x19, 0x5f, 0x84, + 0x31, 0x84, 0x50, 0x62, 0xcd, 0x44, 0xa6, 0x4e, 0xbb, 0x2e, 0x87, 0xda, 0x21, 0x25, 0x1c, 0x13, + 0xfe, 0xc2, 0x97, 0xa3, 0xd5, 0xfd, 0xe2, 0x62, 0xd6, 0xe5, 0x50, 0x7a, 0xda, 0x98, 0x8d, 0xcf, + 0xbe, 0x40, 0x5d, 0x69, 0xb6, 0x5f, 0xdc, 0x32, 0x68, 0x05, 0x67, 0x70, 0xfd, 0xa0, 0xd3, 0xcd, + 0x88, 0xeb, 0x12, 0x2e, 0xac, 0xf5, 0x57, 0xa2, 0xc2, 0x81, 0x22, 0x6b, 0x8d, 0x69, 0x6d, 0xac, + 0x41, 0x05, 0x5b, 0x0b, 0x79, 0xe0, 0x86, 0x05, 0x86, 0x8b, 0x02, 0x51, 0x7d, 0x6e, 0x13, 0x63, + 0x36, 0xe6, 0x89, 0x22, 0x75, 0xd9, 0xb0, 0x39, 0x27, 0xcd, 0xac, 0x78, 0xe0, 0x3a, 0x6a, 0x17, + 0x15, 0x05, 0x83, 0x3a, 0xe9, 0x38, 0x7c, 0x75, 0x38, 0xf3, 0xd8, 0x42, 0x57, 0xa1, 0xef, 0x68, + 0xb8, 0xb5, 0x44, 0xfe, 0x4d, 0x66, 0xa1, 0x09, 0xc5, 0x62, 0xff, 0x87, 0x7e, 0xdb, 0x41, 0x6f, + 0x4d, 0x5f, 0x16, 0x49, 0xef, 0xca, 0xda, 0x67, 0x01, 0x29, 0x0b, 0xd6, 0xd8, 0xfc, 0x0b, 0x22, + 0xc8, 0x28, 0xff, 0xb4, 0xb3, 0xa9, 0xf8, 0x9b, 0x78, 0x7d, 0x33, 0xbf, 0xa6, 0x9b, 0x39, 0x64, + 0x57, 0x41, 0x1f, 0x27, 0x05, 0xfc, 0xcd, 0x44, 0xcd, 0xb2, 0xea, 0x79, 0xe0, 0x00, 0x00, 0x00, + 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x02, 0x01, 0xe0, 0x44, 0x97, 0xe1, 0x8f, 0x40, 0x3d, + 0x73, 0xde, 0xdf, 0xf5, 0x1c, 0xff, 0xf6, 0xe2, 0x12, 0x16, 0x3d, 0xb6, 0x75, 0xad, 0x9e, 0xb5, + 0x2b, 0xe7, 0x4a, 0x40, 0xe9, 0x91, 0xd6, 0x86, 0x86, 0x2d, 0xcb, 0xe4, 0xb6, 0x58, 0x4a, 0x2d, + 0x7b, 0x1b, 0xdb, 0xc5, 0x48, 0x2d, 0x09, 0x4d, 0x32, 0x30, 0xde, 0xaa, 0x61, 0xc2, 0x27, 0x64, + 0x54, 0x71, 0x89, 0x32, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, + 0x00, 0x01, 0xe0, 0x24, 0xff, 0xfa, 0x2c, 0x6e, 0xdc, 0x78, 0x3d, 0x08, 0x6d, 0x3b, 0xcf, 0xcc, + 0xb6, 0x16, 0xeb, 0x9d, 0x5f, 0x46, 0x0b, 0x24, 0x0e, 0x97, 0x99, 0x06, 0x00, 0x00, 0x00, 0x01, + 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x28, 0x95, 0xd7, 0xdc, 0x41, 0xc0, 0x61, + 0x14, 0xd6, 0xee, 0xd4, 0xe8, 0x24, 0x3e, 0x7f, 0x82, 0x22, 0xb0, 0x20, 0xb9, 0xf1, 0x69, 0x9b, + 0xfe, 0xcc, 0xb7, 0x2e, 0xbd, 0xdb, 0x20, 0xfa, 0xe5, 0x97, 0xd9, 0x95, 0x1d, 0x7e, 0x49, 0x35, + 0x69, 0xdb, 0x40, 0xa3, 0x5c, 0x0f, 0x74, 0x6f, 0x00, 0x3d, 0x36, 0x05, 0xc0, 0xc4, 0x37, 0x15, + 0xf9, 0x01, 0x2d, 0xdf, 0xcb, 0x70, 0x3d, 0xfa, 0x47, 0xec, 0x80, 0x07, 0x48, 0x08, 0xc8, 0x54, + 0xd3, 0xe3, 0x00, 0xa3, 0xe4, 0xce, 0x40, 0x05, 0x0b, 0x51, 0x73, 0x56, 0x33, 0x82, 0xa8, 0x0a, + 0xc0, 0xd3, 0xc5, 0xf1, 0xeb, 0x32, 0x8e, 0x4e, 0xa7, 0x6f, 0xb2, 0x64, 0x43, 0x54, 0x71, 0xd4, + 0xaf, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x00, 0x01, 0xe0, 0x82, + 0x2f, 0x5f, 0xdc, 0x63, 0x70, 0x3f, 0xaf, 0x66, 0x75, 0x52, 0xe7, 0x7f, 0x62, 0xfb, 0x84, 0x9c, + 0xef, 0x60, 0x8f, 0xfa, 0x7e, 0x5f, 0x8b, 0x40, 0x15, 0x34, 0x69, 0xe2, 0xc4, 0xfc, 0xff, 0x53, + 0xb8, 0x1e, 0xc7, 0x20, 0x55, 0xdb, 0x5e, 0x05, 0xbd, 0x7a, 0x85, 0xb4, 0xb7, 0xc1, 0x8d, 0xdb, + 0x0f, 0xa6, 0xab, 0xb9, 0x00, 0x2a, 0xdb, 0x83, 0xa0, 0xb0, 0xc5, 0x76, 0x6e, 0xe1, 0xb8, 0x72, + 0xfd, 0x09, 0x61, 0x19, 0xe0, +}; + +inline constexpr std::array<std::uint64_t, 6> HevcHashes{ + 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, + 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, +}; + +inline std::vector<std::vector<std::uint8_t>> hevcUnits(const std::uint8_t* stream, std::size_t size) { + std::vector<std::size_t> starts; + for (std::size_t index = 0; index + 4 < size; ++index) { + if (stream[index] == 0 && stream[index + 1] == 0 && stream[index + 2] == 0 && stream[index + 3] == 1 && ((stream[index + 4] >> 1u) & 0x3fu) == 35) starts.push_back(index); + } + starts.push_back(size); + std::vector<std::vector<std::uint8_t>> units; + for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) units.emplace_back(stream + starts[unit], stream + starts[unit + 1]); + return units; +} + +} diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index 0bf65cb433..e3f2f41379 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -2,10 +2,6 @@ ### Unknown function info -- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. `sceVdecswGetHevcPictureInfo` still throws: the 0xb8-byte HEVC picture layout is in shadPS4's `vdecsw.h`, but this wrapper only keeps AVC picture info from Videodec2 +- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. The 0xb8-byte HEVC picture info follows shadPS4's `vdecsw.h` and is filled with the timestamps, the coded size, the IDR flag (from the first slice NAL type), the IRAP flag (FFmpeg's key frame flag), `general_profile_idc` and `general_level_idc` (FFmpeg's profile and level), and the conformance window in chroma samples; the same window is repeated in the frame cropping fields, whose unit on the console is unknown. Bit depth is always 8, since Videodec2 throws for deeper frames. Timing, VUI, SEI presence, tier, sub-layer and default display window fields stay zero. Picture info for codecs other than H.264 and HEVC throws - [sceVdecswGetVp9PictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - no console behaviour is recorded; the signature and 0x58-byte layout come from [SharpProspero's Vdecsw.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Vdecsw.cs#L892-L952), and shadPS4 has no VP9 getter. It copies the leading `thisSize - 8` bytes of the picture's info, as [shadPS4's `sceVdecswGetPictureInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/videodec/vdecsw.cpp#L303-L334) does for AVC and HEVC, from Videodec2's VP9 info (`level` is 0 there). An output with no picture returns 0 and writes nothing. Null pointers, `thisSize` below 8, two pictures and a frame buffer no VP9 decoder returned (an H.264 picture too) throw; console error codes are unknown. VP9 decoding goes through Videodec2 and rejects pictures deeper than 10 bits - -### Functional - -- [sceVdecswGetHevcPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - throws until HEVC picture info is filled from the decoder From 6a6395474c45225a97892c5c0d2cb080f23a7687 Mon Sep 17 00:00:00 2001 From: 3mro777o <3mro777o@gmail.com> Date: Mon, 19 Oct 2026 07:52:16 +0200 Subject: [PATCH 1076/1360] test(libSceVdecsw): check the HEVC conformance window Decode a 64x36 HEVC picture, coded as 64x40, and check the conformance window and frame cropping offsets in its picture info. --- core/libs/tests/GuestVdecsw.cpp | 20 ++++++++++++++++++++ core/libs/tests/HevcFixture.hpp | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 2cedcdc2ee..9e49605eb6 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -346,6 +346,25 @@ void testHevc() { check(pictures == HevcHashes.size(), "missing HEVC pictures after finalizing the sequence"); } +void testHevcCrop() { + Session session(CodecHevc, 1, 30); + std::vector<std::uint8_t> buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "cropped HEVC output buffer was rejected"); + const InputData input{sizeof(InputData), HevcCroppedStream.data(), HevcCroppedStream.size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "cropped HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "the cropped HEVC access unit was not consumed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "cropped HEVC finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.frameWidth == 64 && output.frameHeight == 40, "unexpected cropped HEVC picture geometry"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "cropped HEVC picture info missing"); + check(info.picWidthInLumaSamples == 64 && info.picHeightInLumaSamples == 40, "cropped HEVC coded size differs"); + check(info.conformanceWindowFlag == 1 && info.confWinLeftOffset == 0 && info.confWinRightOffset == 0 && info.confWinTopOffset == 0 && info.confWinBottomOffset == 2, "HEVC conformance window differs"); + check(info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == 2, "HEVC frame cropping differs"); +} + void testWithoutOutput() { Session session; const auto units = accessUnits(false); @@ -563,6 +582,7 @@ int main() { try { testDecode(); testHevc(); + testHevcCrop(); testWithoutOutput(); testSyncWrappers(); testVp9PictureInfo(); diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp index 3803431734..cc3134543b 100644 --- a/core/libs/tests/HevcFixture.hpp +++ b/core/libs/tests/HevcFixture.hpp @@ -69,6 +69,33 @@ inline constexpr std::array<std::uint8_t, 933> HevcStream{ 0xfd, 0x09, 0x61, 0x19, 0xe0, }; +inline constexpr std::array<std::uint8_t, 378> HevcCroppedStream{ + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x9f, 0x79, 0x65, 0x66, 0xab, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, + 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, + 0xd1, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, 0x59, 0xf2, 0xec, 0x80, 0xf9, 0x31, 0xa3, + 0xfa, 0x90, 0xca, 0x97, 0x42, 0xb5, 0x6f, 0xcb, 0xc8, 0xd2, 0x51, 0xa2, 0x3f, 0xe0, 0x0d, 0x82, + 0x6f, 0x1f, 0x37, 0xd4, 0x65, 0x18, 0x78, 0x60, 0x51, 0x3b, 0x98, 0xd0, 0x44, 0xfa, 0xff, 0x0b, + 0xd5, 0xdc, 0x09, 0xea, 0xf7, 0x79, 0xb4, 0xd2, 0xbc, 0x97, 0x53, 0xf2, 0x7d, 0x10, 0x5c, 0xc8, + 0x3a, 0xe0, 0xa7, 0xd8, 0xe2, 0xfc, 0x1d, 0x80, 0x09, 0xcd, 0x31, 0x09, 0xa4, 0xa9, 0x05, 0x5d, + 0x08, 0x74, 0x50, 0xf5, 0xbe, 0xf9, 0xc8, 0xa6, 0xef, 0xe4, 0xff, 0xd6, 0x86, 0x02, 0xb6, 0xa2, + 0x3f, 0x73, 0x80, 0xdb, 0x4e, 0x76, 0xa8, 0xc7, 0xac, 0x4b, 0x5b, 0xbf, 0xe1, 0x88, 0x18, 0xce, + 0x14, 0x5e, 0xf4, 0xb3, 0x57, 0xe6, 0x83, 0xe1, 0x94, 0x8f, 0x55, 0x77, 0xbb, 0x1c, 0x77, 0xd3, + 0x9e, 0xdc, 0xd5, 0x8a, 0x61, 0xb2, 0xf4, 0x77, 0xb8, 0xd1, 0x27, 0x5c, 0x41, 0x75, 0x0a, 0x82, + 0xf5, 0xe8, 0x32, 0x45, 0xae, 0x94, 0x07, 0xcb, 0xde, 0x35, 0x53, 0x3c, 0x0d, 0xf6, 0x5d, 0x1f, + 0x97, 0x0b, 0x04, 0x90, 0x4b, 0xd3, 0xf6, 0x04, 0x58, 0x26, 0xe2, 0x08, 0xe5, 0xb3, 0xa3, 0x05, + 0x5a, 0x8f, 0x6b, 0x03, 0x4b, 0x0a, 0xd4, 0x63, 0x9d, 0x2e, 0xe2, 0x19, 0xa3, 0xde, 0xb5, 0x53, + 0xf2, 0x07, 0xdc, 0xc8, 0xda, 0x2c, 0xfa, 0x2e, 0x62, 0x39, 0xf3, 0xfa, 0x2a, 0x77, 0xc2, 0xb9, + 0x76, 0x63, 0x58, 0xd3, 0xf8, 0xa6, 0xcb, 0x2b, 0xa4, 0xa8, 0xc8, 0x4a, 0xed, 0xbe, 0x8e, 0xf0, + 0xc9, 0x12, 0xf4, 0xb2, 0x91, 0xad, 0xd2, 0x18, 0xcc, 0x0c, 0x4d, 0x56, 0xd9, 0xa3, 0x09, 0x6c, + 0x01, 0x3a, 0xa4, 0xa5, 0x10, 0x98, 0x4d, 0x85, 0x92, 0xa2, 0xfa, 0x50, 0xfe, 0x2d, 0x42, 0xab, + 0xf3, 0x98, 0xe7, 0x16, 0xf1, 0x0b, 0xac, 0x74, 0x5d, 0xcd, 0xfa, 0xe3, 0x75, 0xf3, 0x66, 0x1e, + 0xf0, 0xa3, 0xad, 0x82, 0x8e, 0x25, 0x2c, 0x8f, 0xe1, 0xac, 0xcb, 0xca, 0x0b, 0x19, 0x32, 0x83, + 0x4a, 0xfd, 0x30, 0xc4, 0xe5, 0x97, 0x73, 0xfd, 0x43, 0xd0, +}; + inline constexpr std::array<std::uint64_t, 6> HevcHashes{ 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, From e036b873ee0196758e61a65f7d34bfeed9419893 Mon Sep 17 00:00:00 2001 From: 3mro777o <3mro777o@gmail.com> Date: Wed, 21 Oct 2026 14:38:02 +0200 Subject: [PATCH 1077/1360] fix(libSceVdecsw): report the HEVC picture bit depth Videodec2 now outputs 10-bit HEVC Main 10 frames, so the HEVC picture info takes bitDepthLumaMinus8 and bitDepthChromaMinus8 from the decoded frame instead of leaving them 0. The Main 10 stream moves to HevcFixture.hpp so the Vdecsw test can decode it. --- core/libs/prx/libSceVdecsw/Export.cpp | 3 +++ core/libs/prx/libSceVideodec2/Export.cpp | 5 ++++- core/libs/tests/GuestVdecsw.cpp | 18 ++++++++++++++++++ core/libs/tests/GuestVideodec2.cpp | 18 ------------------ core/libs/tests/HevcFixture.hpp | 18 ++++++++++++++++++ docs/dev/debt/libSceVdecsw.md | 2 +- 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index f417b3a8d4..d6fa384070 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -190,6 +190,7 @@ struct Videodec2PictureHeader { std::uint32_t profile; std::uint32_t level; Videodec2Crop crop; + std::uint8_t bitDepthMinus8; }; struct DecoderMemoryInfo { @@ -277,6 +278,8 @@ PictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) info.attachedData = header.attached; info.picWidthInLumaSamples = output.frameWidth; info.picHeightInLumaSamples = output.frameHeight; + info.bitDepthLumaMinus8 = header.bitDepthMinus8; + info.bitDepthChromaMinus8 = header.bitDepthMinus8; info.conformanceWindowFlag = cropped; info.confWinLeftOffset = crop.left / 2u; info.confWinRightOffset = crop.right / 2u; diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index ebf22265b1..583b18c0ff 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -284,6 +284,7 @@ struct Picture { Vp9PictureInfo vp9{}; Crop crop{}; bool hevcIdr = false; + std::uint8_t bitDepthMinus8 = 0; }; struct PictureHeader { @@ -295,6 +296,7 @@ struct PictureHeader { std::uint32_t profile; std::uint32_t level; Crop crop; + std::uint8_t bitDepthMinus8; }; struct FrameDeleter { @@ -548,6 +550,7 @@ void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, Out if (decoder.context->profile > 0) decoder.profile = static_cast<std::uint32_t>(decoder.context->profile) & 0xffu; if (decoder.context->level > 0) decoder.level = static_cast<std::uint32_t>(decoder.context->level); if (!WriteNv12(decoder, *picture, static_cast<std::uint8_t*>(frame->frameBuffer), frame->frameBufferSize)) throw std::runtime_error("Videodec2: NV12 conversion failed"); + timestamps.bitDepthMinus8 = decoder.sampleBytes == 2 ? 2 : 0; const auto pitch = PitchBytes(decoder.width, decoder.sampleBytes); frame->isAccepted = true; output->isValid = true; @@ -809,7 +812,7 @@ void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frame const auto picture = decoder.pictures.find(frameBuffer); if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); const auto& stored = picture->second; - *header = {stored.pts, stored.dts, stored.attached, stored.idr, stored.hevcIdr, decoder.profile, decoder.level, stored.crop}; + *header = {stored.pts, stored.dts, stored.attached, stored.idr, stored.hevcIdr, decoder.profile, decoder.level, stored.crop, stored.bitDepthMinus8}; } void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 9e49605eb6..6bb929a1e0 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -578,11 +578,29 @@ void testVp9PictureInfoOfAvcPicture() { } +void testHevcMain10() { + Session session(CodecHevc, 2, 30); + std::vector<std::uint8_t> buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "Main 10 HEVC output buffer was rejected"); + const InputData input{sizeof(InputData), HevcMain10Stream.data(), HevcMain10Stream.size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "Main 10 HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "the Main 10 HEVC access unit was not consumed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "Main 10 HEVC finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid, "the Main 10 HEVC picture was not output"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "Main 10 HEVC picture info missing"); + check(info.generalProfileIdc == 2 && info.bitDepthLumaMinus8 == 2 && info.bitDepthChromaMinus8 == 2, "Main 10 HEVC profile or bit depth differs"); +} + int main() { try { testDecode(); testHevc(); testHevcCrop(); + testHevcMain10(); testWithoutOutput(); testSyncWrappers(); testVp9PictureInfo(); diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index c5bc9dea25..565f4454b4 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -169,24 +169,6 @@ constexpr std::array<std::uint8_t, 658> AvcIpppStream{ 0x43, 0xd5, 0x7b, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x60, 0x35, 0xc6, 0xc0}; -constexpr std::array<std::uint8_t, 234> HevcMain10Stream{ - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, - 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, - 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x93, 0x65, 0x95, 0x9a, - 0xe4, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, - 0x60, 0xf2, 0xfb, 0x04, 0xef, 0x67, 0x67, 0xdc, 0xc7, 0x8e, 0xc7, 0x42, 0x63, 0xeb, 0x7d, 0x28, - 0x73, 0x95, 0x14, 0x0c, 0x71, 0x10, 0x8b, 0x65, 0x04, 0xbd, 0x03, 0xe6, 0x04, 0x35, 0xb9, 0x9b, - 0xec, 0x60, 0x2e, 0xea, 0xcf, 0xd0, 0xdd, 0x2d, 0x3a, 0x62, 0x33, 0x9d, 0x7a, 0x74, 0xb0, 0xeb, - 0x0c, 0x06, 0xe6, 0xa5, 0x30, 0xa7, 0xea, 0xed, 0xc2, 0xa5, 0x8e, 0x7c, 0xda, 0x1b, 0x9d, 0xe4, - 0xac, 0x2b, 0x38, 0xdb, 0x9c, 0xe8, 0x51, 0x34, 0x46, 0xd2, 0x04, 0x1a, 0x77, 0x87, 0x7a, 0x19, - 0x70, 0xf9, 0x4f, 0xcb, 0x03, 0x3c, 0x74, 0x25, 0xb9, 0x92, 0x3b, 0x95, 0xe9, 0xd8, 0xaa, 0x9b, - 0x6b, 0x9b, 0x1f, 0x70, 0x25, 0x2a, 0xe7, 0x6c, 0xc3, 0x7e, 0x55, 0x39, 0xf5, 0x06, 0x5a, 0x23, - 0xb1, 0x2e, 0xf6, 0x01, 0xac, 0x71, 0xb9, 0x20, 0xb4, 0xef, 0x1d, 0x42, 0x7e, 0x0e, 0x01, 0x1d, - 0xaf, 0x0a, 0x13, 0x04, 0x8e, 0x03, 0x4d, 0x1e, 0xc4, 0xe0, -}; - void check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp index cc3134543b..7bacee3a02 100644 --- a/core/libs/tests/HevcFixture.hpp +++ b/core/libs/tests/HevcFixture.hpp @@ -69,6 +69,24 @@ inline constexpr std::array<std::uint8_t, 933> HevcStream{ 0xfd, 0x09, 0x61, 0x19, 0xe0, }; +inline constexpr std::array<std::uint8_t, 234> HevcMain10Stream{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, + 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x93, 0x65, 0x95, 0x9a, + 0xe4, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, + 0x60, 0xf2, 0xfb, 0x04, 0xef, 0x67, 0x67, 0xdc, 0xc7, 0x8e, 0xc7, 0x42, 0x63, 0xeb, 0x7d, 0x28, + 0x73, 0x95, 0x14, 0x0c, 0x71, 0x10, 0x8b, 0x65, 0x04, 0xbd, 0x03, 0xe6, 0x04, 0x35, 0xb9, 0x9b, + 0xec, 0x60, 0x2e, 0xea, 0xcf, 0xd0, 0xdd, 0x2d, 0x3a, 0x62, 0x33, 0x9d, 0x7a, 0x74, 0xb0, 0xeb, + 0x0c, 0x06, 0xe6, 0xa5, 0x30, 0xa7, 0xea, 0xed, 0xc2, 0xa5, 0x8e, 0x7c, 0xda, 0x1b, 0x9d, 0xe4, + 0xac, 0x2b, 0x38, 0xdb, 0x9c, 0xe8, 0x51, 0x34, 0x46, 0xd2, 0x04, 0x1a, 0x77, 0x87, 0x7a, 0x19, + 0x70, 0xf9, 0x4f, 0xcb, 0x03, 0x3c, 0x74, 0x25, 0xb9, 0x92, 0x3b, 0x95, 0xe9, 0xd8, 0xaa, 0x9b, + 0x6b, 0x9b, 0x1f, 0x70, 0x25, 0x2a, 0xe7, 0x6c, 0xc3, 0x7e, 0x55, 0x39, 0xf5, 0x06, 0x5a, 0x23, + 0xb1, 0x2e, 0xf6, 0x01, 0xac, 0x71, 0xb9, 0x20, 0xb4, 0xef, 0x1d, 0x42, 0x7e, 0x0e, 0x01, 0x1d, + 0xaf, 0x0a, 0x13, 0x04, 0x8e, 0x03, 0x4d, 0x1e, 0xc4, 0xe0, +}; + inline constexpr std::array<std::uint8_t, 378> HevcCroppedStream{ 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index e3f2f41379..4818e0bfea 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -2,6 +2,6 @@ ### Unknown function info -- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. The 0xb8-byte HEVC picture info follows shadPS4's `vdecsw.h` and is filled with the timestamps, the coded size, the IDR flag (from the first slice NAL type), the IRAP flag (FFmpeg's key frame flag), `general_profile_idc` and `general_level_idc` (FFmpeg's profile and level), and the conformance window in chroma samples; the same window is repeated in the frame cropping fields, whose unit on the console is unknown. Bit depth is always 8, since Videodec2 throws for deeper frames. Timing, VUI, SEI presence, tier, sub-layer and default display window fields stay zero. Picture info for codecs other than H.264 and HEVC throws +- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. The 0xb8-byte HEVC picture info (shadPS4's `vdecsw.h`) carries only the timestamps, coded size, bit depth, IDR and IRAP flags, profile, level and conformance window (in chroma samples, repeated in the frame cropping fields, whose console unit is unknown) - [sceVdecswGetVp9PictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - no console behaviour is recorded; the signature and 0x58-byte layout come from [SharpProspero's Vdecsw.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Vdecsw.cs#L892-L952), and shadPS4 has no VP9 getter. It copies the leading `thisSize - 8` bytes of the picture's info, as [shadPS4's `sceVdecswGetPictureInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/videodec/vdecsw.cpp#L303-L334) does for AVC and HEVC, from Videodec2's VP9 info (`level` is 0 there). An output with no picture returns 0 and writes nothing. Null pointers, `thisSize` below 8, two pictures and a frame buffer no VP9 decoder returned (an H.264 picture too) throw; console error codes are unknown. VP9 decoding goes through Videodec2 and rejects pictures deeper than 10 bits From ea6aa0a8091f3ecd4edcb83e1fa0cd6226e84f66 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Wed, 21 Oct 2026 23:56:32 -0400 Subject: [PATCH 1078/1360] fix(libc): follow the console fgetws for short buffers and read errors The fgetws in the libc.prx that titles ship (the same 133 bytes in the copies of PPSA13934, PPSA07923 and PPSA05916, read from disassembly) differs from this one in two cases: - A size below 2 returns null at once, without writing the buffer, errno or the stream (cmp $0x2,%esi; jge, else the null result). Here a size of 1 stored a NUL and returned the buffer, and a size of 0 or less set the stream error and errno 22. - When fgetwc returns -1 after characters were stored, the console null-terminates and returns the buffer; it returns null only when nothing was stored, and never checks ferror. Here a read error after some characters returned null. The rest already matches: each fgetwc result is stored as its low 16 bits (mov %ax,(%rbx)), and the newline test compares all 32 bits. A null buffer or stream, which the console dereferences, keeps failing with errno 22. guest_standard_streams now expects the console results for sizes 1, 0 and -1 and for an invalid byte after a stored character; its fgetwc error indicator check is unchanged. The libc debt entry cites the reading and what it leaves unverified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/FormattingWide.cpp | 10 ++++------ core/libs/tests/GuestStandardStreams.cpp | 10 ++++++---- docs/dev/debt/libc.md | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index a2b3d0fe7d..ac1f820c1d 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -399,7 +399,8 @@ int APS5_VABI putwchar_nid_postfix(char16_t value) { } char16_t* APS5_VABI fgetws_nid_postfix(char16_t* buffer, int size, FileStream* stream) { - if (buffer == nullptr || stream == nullptr || size <= 0) { + if (size < 2) return nullptr; + if (buffer == nullptr || stream == nullptr) { if (stream != nullptr) stream->SetError(); errno = 22; return nullptr; @@ -407,14 +408,11 @@ char16_t* APS5_VABI fgetws_nid_postfix(char16_t* buffer, int size, FileStream* s int count = 0; while (count < size - 1) { const std::int32_t character = fgetwc_nid_postfix(stream); - if (character == -1) { - if (count == 0) return nullptr; - break; - } + if (character == -1) break; buffer[count++] = static_cast<char16_t>(character); if (character == static_cast<std::int32_t>(u'\n')) break; } - if (ferror_nid_postfix(stream) != 0) return nullptr; + if (count == 0) return nullptr; buffer[count] = 0; return buffer; } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index c37c580e6b..9e4530a690 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -744,9 +744,11 @@ int main(int argc, char** argv) { Require(wideBuf[0] == u'E' && wideBuf[1] == u'n' && wideBuf[2] == u'd' && wideBuf[3] == 0); Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == nullptr); Require(feof_nid_postfix(&wideIo)); - Require(fgetws_nid_postfix(wideBuf, 0, &wideIo) == nullptr && *__error_nid_postfix() == 22); - Require(ferror_nid_postfix(&wideIo)); clearerr_nid_postfix(&wideIo); + *__error_nid_postfix() = 0; + Require(fgetws_nid_postfix(wideBuf, 0, &wideIo) == nullptr && *__error_nid_postfix() == 0); + Require(fgetws_nid_postfix(wideBuf, -1, &wideIo) == nullptr && *__error_nid_postfix() == 0); + Require(!ferror_nid_postfix(&wideIo)); Require(fgetws_nid_postfix(nullptr, 16, &wideIo) == nullptr && *__error_nid_postfix() == 22); Require(ferror_nid_postfix(&wideIo)); clearerr_nid_postfix(&wideIo); @@ -754,7 +756,7 @@ int main(int argc, char** argv) { wideBuf[0] = u'X'; wideBuf[1] = u'Y'; std::rewind(wideIo.GetHandle()); - Require(fgetws_nid_postfix(wideBuf, 1, &wideIo) == wideBuf && wideBuf[0] == 0 && wideBuf[1] == u'Y'); + Require(fgetws_nid_postfix(wideBuf, 1, &wideIo) == nullptr && wideBuf[0] == u'X' && wideBuf[1] == u'Y'); Require(fgetc_nid_postfix(&wideIo) == 'H'); wideIo.Close(); @@ -778,7 +780,7 @@ int main(int argc, char** argv) { Require(fputc_nid_postfix(0xff, &midErr) == 0xff); std::rewind(midErr.GetHandle()); char16_t midBuf[16]{}; - Require(fgetws_nid_postfix(midBuf, 16, &midErr) == nullptr); + Require(fgetws_nid_postfix(midBuf, 16, &midErr) == midBuf && midBuf[0] == u'A' && midBuf[1] == 0); Require(ferror_nid_postfix(&midErr)); midErr.Close(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..45ca2c1e89 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -69,7 +69,7 @@ - Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [feof_unlocked and ferror_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) have no console measurement. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NIDs `NuydofHcR1w` and `yxbGzBQC5xA`. They follow FreeBSD 11 [feof.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/feof.c) and [ferror.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/ferror.c), returning 0 or 1 from the stream status without acquiring the stream lock. They inherit the host CRT limitations of the unlocked byte operations above. -- [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. +- [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` follows the `libc.prx` that titles ship (read from disassembly of three titles' copies, PPSA13934, PPSA07923 and PPSA05916, not measured on a console): it returns null for a size below 2 without writing the buffer, `errno` or the stream, stores each `fgetwc` result as its low 16 bits until a newline (compared on all 32 bits), `-1` or a full buffer, and null-terminates and returns the buffer whenever it stored a character, also after a read error. A null buffer or stream, which the console dereferences, sets `errno` to 22 here. The calls the console makes before and after reading, and the one it makes when bits 0xC of the stream's second flag byte are set, are not reproduced. Console wide stream buffering and `fgetwc`'s decoding remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. - [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it From 5e03230c28a3f466f6007ea735884074cc30a794 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 10:15:25 -0400 Subject: [PATCH 1079/1360] fix(libc): compute atanf with FreeBSD msun's algorithm, as titles' libc.prx does atanf returned the host atan, so its last bit depended on the host: on Windows it differs from the libc.prx that titles ship on 6,481,820 of the 2^32 floats. That libc.prx is msun's s_atanf.c, which gives the same bits as PPSA07923's, PPSA05916's and PPSA13934's copies for every argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 45 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 16 ++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..15073bc0c0 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -539,7 +539,50 @@ void APS5_VABI sincos_nid_postfix(double x, double* sinp, double* cosp) { *cosp = std::cos(x); } -float APS5_VABI atanf_nid_postfix(float x) { return std::atan(x); } +float APS5_VABI atanf_nid_postfix(float x) { + static constexpr float atanhi[] = {static_cast<float>(4.6364760399e-01), static_cast<float>(7.8539812565e-01), static_cast<float>(9.8279368877e-01), static_cast<float>(1.5707962513e+00)}; + static constexpr float atanlo[] = {static_cast<float>(5.0121582440e-09), static_cast<float>(3.7748947079e-08), static_cast<float>(3.4473217170e-08), static_cast<float>(7.5497894159e-08)}; + static constexpr float aT[] = {static_cast<float>(3.3333328366e-01), static_cast<float>(-1.9999158382e-01), static_cast<float>(1.4253635705e-01), static_cast<float>(-1.0648017377e-01), static_cast<float>(6.1687607318e-02)}; + const std::int32_t hx = std::bit_cast<std::int32_t>(x); + const std::int32_t ix = hx & 0x7fffffff; + if (ix >= 0x4c800000) { + if (ix > 0x7f800000) { + return x + x; + } + const volatile float lo = atanlo[3]; + return hx > 0 ? atanhi[3] + lo : -atanhi[3] - lo; + } + int id = -1; + if (ix < 0x3ee00000) { + if (ix < 0x39800000) { + return x; + } + } else { + x = std::fabs(x); + if (ix < 0x3f300000) { + id = 0; + x = (2.0f * x - 1.0f) / (2.0f + x); + } else if (ix < 0x3f980000) { + id = 1; + x = (x - 1.0f) / (x + 1.0f); + } else if (ix < 0x401c0000) { + id = 2; + x = (x - 1.5f) / (1.0f + 1.5f * x); + } else { + id = 3; + x = -1.0f / x; + } + } + const float z = x * x; + const float w = z * z; + const float s1 = z * (aT[0] + w * (aT[2] + w * aT[4])); + const float s2 = w * (aT[1] + w * aT[3]); + if (id < 0) { + return x - x * (s1 + s2); + } + const float result = atanhi[id] - ((x * (s1 + s2) - atanlo[id]) - x); + return hx < 0 ? -result : result; +} double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } float APS5_VABI powf_nid_postfix(float base, float exp) { if (base == 1.0f) return 1.0f; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..be1b0c1df6 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -83,6 +83,7 @@ float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, float); +float APS5_VABI atanf_nid_postfix(float); float APS5_VABI hypotf_nid_postfix(float, float); double APS5_VABI hypot_nid_postfix(double, double); double APS5_VABI fdim_nid_postfix(double, double); @@ -498,6 +499,20 @@ static void CheckFloatUnderflowErrno() { Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); } +static void CheckAtanfConsoleBits() { + constexpr std::uint32_t cases[][2] = { + {0x3e80000bu, 0x3e7adbc5u}, {0x3e0007c9u, 0x3dfebd2au}, {0xbdccce4cu, 0xbdcc2090u}, + {0x3ef00334u, 0x3ee07046u}, {0x3f40077eu, 0x3f24c148u}, {0x3fa009a5u, 0x3f656b6au}, + {0x4080055cu, 0x3fa9b5a8u}, {0xbf12345eu, 0xbf04d718u}, {0x3f800000u, 0x3f490fdbu}, + {0x4c800000u, 0x3fc90fdbu}, {0xcc800000u, 0xbfc90fdbu}, {0x7f800000u, 0x3fc90fdbu}, + {0xff800000u, 0xbfc90fdbu}, {0x7fc00001u, 0x7fc00001u}, {0x80000000u, 0x80000000u}, + {0x00000001u, 0x00000001u}, {0x39000000u, 0x39000000u}, + }; + for (const auto& [input, expected] : cases) { + Require(std::bit_cast<std::uint32_t>(atanf_nid_postfix(std::bit_cast<float>(input))) == expected); + } +} + static void CheckHypotSpecialValue() { Require(std::bit_cast<std::uint64_t>(hypot_nid_postfix(INFINITY, std::bit_cast<double>(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast<std::uint64_t>(hypot_nid_postfix(std::bit_cast<double>(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -1590,6 +1605,7 @@ int main() { CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); + CheckAtanfConsoleBits(); CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckDoubleClassification(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..c05f88b683 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -79,6 +79,7 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console +- [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified From a22e959cd8b1cec415812d625b17ffd983d57a68 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 11:46:40 -0400 Subject: [PATCH 1080/1360] fix(libc): compute logf with FreeBSD msun's algorithm, as titles' libc.prx does logf returned the host log, so its last bit and errno depended on the host: on Windows the bits differ from the libc.prx that titles ship on 13,225,175 of the 2^32 floats. That libc.prx is msun's e_logf.c, setting ERANGE for +-0 and EDOM for every argument with the sign bit set. The port gives the same bits and errno as PPSA07923's and PPSA05916's copies for every argument; the FMA-contracted copy of PPSA13934 differs by 1 ulp on 961,881. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 57 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 22 +++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..d14b4118d3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -572,7 +572,62 @@ double APS5_VABI __powidf2_nid_postfix(double base, int exponent) { } float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } -float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } +float APS5_VABI logf_nid_postfix(float x) { + const float ln2_hi = static_cast<float>(6.9313812256e-01); + const float ln2_lo = static_cast<float>(9.0580006145e-06); + const float two25 = static_cast<float>(3.355443200e+07); + const float Lg1 = 0xaaaaaa.0p-24f; + const float Lg2 = 0xccce13.0p-25f; + const float Lg3 = 0x91e9ee.0p-25f; + const float Lg4 = 0xf89e26.0p-26f; + static volatile float vzero = 0.0f; + std::int32_t k = 0; + std::int32_t ix = std::bit_cast<std::int32_t>(x); + if (ix < 0x00800000) { + if ((ix & 0x7fffffff) == 0) { + errno = ERANGE; + return -two25 / vzero; + } + if (ix < 0) { + errno = EDOM; + return (x - x) / 0.0f; + } + k -= 25; + x *= two25; + ix = std::bit_cast<std::int32_t>(x); + } + if (ix >= 0x7f800000) { + return x + x; + } + k += (ix >> 23) - 127; + ix &= 0x007fffff; + std::int32_t i = (ix + (0x95f64 << 3)) & 0x800000; + x = std::bit_cast<float>(ix | (i ^ 0x3f800000)); + k += i >> 23; + const float f = x - 1.0f; + const float dk = static_cast<float>(k); + if ((0x007fffff & (0x8000 + ix)) < 0xc000) { + if (f == 0.0f) { + return k == 0 ? 0.0f : dk * ln2_hi + dk * ln2_lo; + } + const float R = f * f * (0.5f - static_cast<float>(0.33333333333333333) * f); + return k == 0 ? f - R : dk * ln2_hi - ((R - dk * ln2_lo) - f); + } + const float s = f / (2.0f + f); + const float z = s * s; + i = ix - (0x6147a << 3); + const float w = z * z; + const std::int32_t j = (0x6b851 << 3) - ix; + const float t1 = w * (Lg2 + w * Lg4); + const float t2 = z * (Lg1 + w * Lg3); + i |= j; + const float R = t2 + t1; + if (i > 0) { + const float hfsq = 0.5f * f * f; + return k == 0 ? f - (hfsq - s * (hfsq + R)) : dk * ln2_hi - ((hfsq - (s * (hfsq + R) + dk * ln2_lo)) - f); + } + return k == 0 ? f - s * (f - R) : dk * ln2_hi - ((s * (f - R) - dk * ln2_lo) - f); +} float APS5_VABI log2f_nid_postfix(float x) { return std::log2(x); } double APS5_VABI log10_nid_postfix(double x) { return std::log10(x); } float APS5_VABI ldexpf_nid_postfix(float x, int exp) { return std::ldexp(x, exp); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..cf6dacc48f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -125,6 +125,7 @@ float APS5_VABI nextafterf_nid_postfix(float, float); double APS5_VABI nextafter_nid_postfix(double, double); double APS5_VABI frexp_nid_postfix(double, int*); float APS5_VABI frexpf_nid_postfix(float, int*); +float APS5_VABI logf_nid_postfix(float); std::int64_t APS5_VABI lround_nid_postfix(double); std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); @@ -1058,6 +1059,26 @@ static void RequireBytes(const void* constant, std::initializer_list<unsigned ch while (index < 16) Require(bytes[index++] == 0); } +static void CheckLogfConsoleBits() { + constexpr struct { + std::uint32_t input; + std::uint32_t expected; + int error; + } cases[] = { + {0x00000983u, 0xc2bef664u, 0}, {0x3e003791u, 0xc004f9cfu, 0}, {0x3f7f0a10u, 0xbb76666fu, 0}, {0x3f81086bu, 0x3c03adacu, 0}, + {0x40400de0u, 0x3f8ca894u, 0}, {0x5f00a041u, 0x422eb14eu, 0}, {0x3f800000u, 0x00000000u, 0}, {0x40000000u, 0x3f317218u, 0}, + {0x00000001u, 0xc2ce8ed0u, 0}, {0x00800000u, 0xc2aeac50u, 0}, {0x7f7fffffu, 0x42b17218u, 0}, {0x7f800000u, 0x7f800000u, 0}, + {0x3f800001u, 0x33ffffffu, 0}, {0x3f7fffffu, 0xb3800000u, 0}, {0x7fc00001u, 0x7fc00001u, 0}, + {0x00000000u, 0xff800000u, ERANGE}, {0x80000000u, 0xff800000u, ERANGE}, {0x80000001u, 0xffc00000u, EDOM}, + {0xbf800000u, 0xffc00000u, EDOM}, {0xff800000u, 0xffc00000u, EDOM}, {0xffc00002u, 0xffc00002u, EDOM}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast<std::uint32_t>(logf_nid_postfix(std::bit_cast<float>(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + static void CheckDinkumwareConstants() { RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); RequireBytes(&_Denorm_nid_postfix, {1}); @@ -1589,6 +1610,7 @@ int main() { CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); + CheckLogfConsoleBits(); CheckHypotSpecialValue(); CheckFloatUnderflowErrno(); CheckFmodErrno(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..bf4c6ce2ba 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -64,6 +64,7 @@ - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. +- [logf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_logf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_logf.c), with `vzero` read at run time as msun's `volatile` read does. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` for ±0 and to `EDOM` for every argument with the sign bit set, negative NaNs and -inf included; positive NaNs and +inf leave it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `logf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments, run on a Windows host. The FMA-contracted build bundled with PPSA13934 differs from it by 1 ulp on 961,881 arguments, so no single port matches every title's last bit. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. From ac92cc6281131d8499a23c2d1f6ae47296027230 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 15:18:48 -0400 Subject: [PATCH 1081/1360] fix(libc): compute cbrtf with FreeBSD msun's algorithm, as titles' libc.prx does cbrtf returned the host cbrt, so its result depended on the host: on Windows it differs from the libc.prx that titles ship on 1,303,357,444 of the 2^32 floats. That libc.prx is msun's s_cbrtf.c, which gives the same bits as PPSA07923's, PPSA05916's and PPSA13934's copies for every argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 28 +++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 17 +++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..9ea3cd7363 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -584,7 +584,33 @@ double APS5_VABI fmod_nid_postfix(double x, double y) { } float APS5_VABI roundf_nid_postfix(float x) { return std::round(x); } double APS5_VABI round_nid_postfix(double x) { return std::round(x); } -float APS5_VABI cbrtf_nid_postfix(float x) { return std::cbrt(x); } +float APS5_VABI cbrtf_nid_postfix(float x) { + const std::uint32_t B1 = 709958130; + const std::uint32_t B2 = 642849266; + std::uint32_t hx = std::bit_cast<std::uint32_t>(x); + const std::uint32_t sign = hx & 0x80000000u; + hx ^= sign; + if (hx >= 0x7f800000u) { + return x + x; + } + float t; + if (hx < 0x00800000u) { + if (hx == 0) { + return x; + } + t = 0x1p24f * x; + const std::uint32_t high = std::bit_cast<std::uint32_t>(t); + t = std::bit_cast<float>(sign | ((high & 0x7fffffffu) / 3 + B2)); + } else { + t = std::bit_cast<float>(sign | (hx / 3 + B1)); + } + double T = t; + double r = T * T * T; + T = T * (static_cast<double>(x) + x + r) / (x + r + r); + r = T * T * T; + T = T * (static_cast<double>(x) + x + r) / (x + r + r); + return static_cast<float>(T); +} float APS5_VABI remainderf_nid_postfix(float x, float y) { return std::remainder(x, y); } float APS5_VABI nextafterf_nid_postfix(float x, float y) { return std::nextafter(x, y); } double APS5_VABI nextafter_nid_postfix(double x, double y) { return std::nextafter(x, y); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..f3124549f3 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -96,6 +96,7 @@ float APS5_VABI remquof_nid_postfix(float, float, int*); float APS5_VABI fmaf_nid_postfix(float, float, float); long double APS5_VABI ceill_nid_postfix(long double); long double APS5_VABI floorl_nid_postfix(long double); +float APS5_VABI cbrtf_nid_postfix(float); float APS5_VABI tanf_nid_postfix(float); int APS5_VABI rand_nid_postfix(); void APS5_VABI srand_nid_postfix(unsigned int); @@ -623,6 +624,21 @@ static void CheckAsinl() { *__error_nid_postfix() = 0; } +static void CheckCbrtfConsoleBits() { + constexpr std::uint32_t cases[][2] = { + {0x00000123u, 0x28059605u}, {0x00400327u, 0x2a4b334bu}, {0x3f000555u, 0x3f4b32c7u}, {0x3fc00778u, 0x3f9287e5u}, + {0x42000999u, 0x404b3509u}, {0x7e000bbcu, 0x544b362au}, {0xbf400dddu, 0xbf689d01u}, {0x80201002u, 0xaa215ff8u}, + {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x00000001u, 0x26a14518u}, {0x007fffffu, 0x2a7fffffu}, + {0x00800000u, 0x2a800000u}, {0x3f800000u, 0x3f800000u}, {0x41000000u, 0x40000000u}, {0xc1000000u, 0xc0000000u}, + {0x7f7fffffu, 0x54cb2ff5u}, {0x7f800000u, 0x7f800000u}, {0xff800000u, 0xff800000u}, {0x7fc00001u, 0x7fc00001u}, + {0xffc00002u, 0xffc00002u}, + }; + for (const auto& [input, expected] : cases) { + errno = 1234; + Require(std::bit_cast<std::uint32_t>(cbrtf_nid_postfix(std::bit_cast<float>(input))) == expected && errno == 1234); + } +} + static void CheckIntegerConversions() { for (const long long numerator : {4294967301LL, -4294967301LL}) { for (const long long denominator : {3LL, -3LL}) { @@ -1309,6 +1325,7 @@ int main() { CheckMissingClassificationExports(); CheckFloatMathExports(); CheckIntegerConversions(); + CheckCbrtfConsoleBits(); CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..76a49e45e2 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -63,6 +63,7 @@ - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. +- [cbrtf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_cbrtf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrtf.c): a 5-bit first guess from the exponent bits, then two Newton steps in double precision. It sets no `errno`, as the `libc.prx` that titles ship does not. Floating-point exceptions are not compared. It gives the same bits as the `cbrtf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. From 76b318a75cc168a83dbb22fa16b40badcbbe6b7d Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Thu, 22 Oct 2026 22:30:59 +0200 Subject: [PATCH 1082/1360] fix(libScePad): keep special port state per user A single flag and one handle served every user's special port, so a second user's scePadClose dropped the first user's handle from scePadGetHandle. Each user now gets a handle of its own (PAD_SPECIAL_HANDLE + slot, four slots), a repeated open returns it again, and close frees only that slot. Whether the console shares or separates the handles is not measured yet; the debt entry says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- core/libs/prx/libScePad/Export.cpp | 28 +++++++++++++++++++------ core/libs/prx/libScePad/include/Pad.hpp | 1 + core/libs/tests/GuestPad.cpp | 18 ++++++++++++++++ docs/dev/debt/libScePad.md | 2 +- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 635a68d135..2191f00696 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -1,5 +1,6 @@ #include <cstddef> #include <cstdint> +#include <array> #include <atomic> #include <cstring> #include <stdexcept> @@ -108,11 +109,19 @@ constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast<int>(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast<int>(0x80920008); bool g_opened = false; -bool g_specialOpened = false; +std::array<int, PAD_SPECIAL_HANDLE_COUNT> g_specialUsers{}; std::atomic_bool g_initialized{false}; bool SpecialHandle(int handle) { - return g_specialOpened && handle == PAD_SPECIAL_HANDLE; + const int slot = handle - PAD_SPECIAL_HANDLE; + return slot >= 0 && slot < PAD_SPECIAL_HANDLE_COUNT && g_specialUsers[slot] != 0; +} + +int SpecialHandleOf(int userId) { + for (int slot = 0; slot < PAD_SPECIAL_HANDLE_COUNT; ++slot) { + if (g_specialUsers[slot] == userId) return PAD_SPECIAL_HANDLE + slot; + } + return PAD_ERROR_DEVICE_NO_HANDLE; } bool ValidPort(int userId, int type, int index) { @@ -143,7 +152,7 @@ extern "C" { int APS5_VABI scePadClose_nid_postfix(int handle) { if (SpecialHandle(handle)) { - g_specialOpened = false; + g_specialUsers[handle - PAD_SPECIAL_HANDLE] = 0; return PAD_OK; } if (handle != PAD_HANDLE) { @@ -209,7 +218,7 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio int APS5_VABI scePadGetHandle(int user_id, int type, int index) { if (!ValidPort(user_id, type, index)) return PAD_ERROR_DEVICE_NO_HANDLE; - if (type == PAD_PORT_TYPE_SPECIAL) return g_specialOpened ? PAD_SPECIAL_HANDLE : PAD_ERROR_DEVICE_NO_HANDLE; + if (type == PAD_PORT_TYPE_SPECIAL) return SpecialHandleOf(user_id); if (!g_opened) return PAD_ERROR_DEVICE_NO_HANDLE; return PAD_HANDLE; } @@ -275,8 +284,15 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void return PAD_ERROR_INVALID_ARG; } if (type == PAD_PORT_TYPE_SPECIAL) { - g_specialOpened = true; - return PAD_SPECIAL_HANDLE; + const int opened = SpecialHandleOf(userId); + if (opened != PAD_ERROR_DEVICE_NO_HANDLE) return opened; + for (int slot = 0; slot < PAD_SPECIAL_HANDLE_COUNT; ++slot) { + if (g_specialUsers[slot] == 0) { + g_specialUsers[slot] = userId; + return PAD_SPECIAL_HANDLE + slot; + } + } + NotImplemented_nid_no_patch(__func__); } g_opened = true; return PAD_HANDLE; diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index 1f759f4fff..2f4baae78e 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -14,6 +14,7 @@ constexpr int PAD_PORT_TYPE_REMOTE = 16; constexpr int PAD_USER_ID_SYSTEM = 0xff; constexpr int PAD_HANDLE = 1; constexpr int PAD_SPECIAL_HANDLE = 0x20; +constexpr int PAD_SPECIAL_HANDLE_COUNT = 4; constexpr int PAD_CONNECTION_TYPE_LOCAL = 0; constexpr int PAD_DEVICE_CLASS_STANDARD = 0; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e741ba4ddd..2e8572d975 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -348,6 +348,24 @@ static void CheckSpecialPort(int user, int handle) { Require(scePadGetControllerInformation(special, &info) == PAD_ERROR_INVALID_HANDLE); Require(scePadGetHandle(user, 0, 0) == handle); Require(scePadReadState(handle, data) == PAD_OK); + const int other = user + 1; + const int first = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + const int second = scePadOpen_nid_postfix(other, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(first > 0 && second > 0 && first != second && second != handle); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == first); + Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == second); + Require(scePadOpen_nid_postfix(other, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == second); + Require(scePadClose_nid_postfix(first) == PAD_OK); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == second); + Require(scePadRead_nid_postfix(first, data, 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadRead_nid_postfix(second, data, 1) == 1); + Require(!data[0].connected); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == first); + Require(scePadClose_nid_postfix(first) == PAD_OK); + Require(scePadClose_nid_postfix(second) == PAD_OK); + Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + Require(scePadGetHandle(user, 0, 0) == handle); } static void CheckRemoteController(int handle) { diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 6638a0ed21..58850fc66a 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -12,7 +12,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists -- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - `scePadOpen` opens a special port (type 2) with `PAD_SPECIAL_HANDLE`, whose reads return a zeroed record (`connected` false), whose controller information and device class data are zeroed, and which the other pad calls reject with `INVALID_HANDLE`. A PS4 Pro (firmware 10.71) with no special controller opens the port for any valid user, reads one record with `connected` false, succeeds in `scePadGetControllerInformation` and rejects handle -1; the handle value, the other fields, the other calls and a PS5 are not measured. `scePadOpenExt` handles only special ports and returns `DEVICE_NOT_CONNECTED` (0x80920007), unmeasured; PPSA01317 passes a 16-byte param naming the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - `scePadOpen` opens a special port (type 2) with a handle per user (`PAD_SPECIAL_HANDLE` + slot, four slots, a fifth user throws), whose reads return a zeroed record (`connected` false), whose controller information and device class data are zeroed, and which the other pad calls reject with `INVALID_HANDLE`; a user's second open returns its handle again. A PS4 Pro (firmware 10.71) with no special controller opens the port for any valid user, reads one record with `connected` false, succeeds in `scePadGetControllerInformation` and rejects handle -1; the handle value, the other fields, the other calls, two users holding the port at once, a repeated open and a PS5 are not measured. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device by vendor and product id and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect From 7e97092226eecfc3f77dd58996b93ee524d8e080 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Thu, 22 Oct 2026 22:35:54 +0200 Subject: [PATCH 1083/1360] feat(libSceNpSignaling): model contexts and connections offline The eight exports threw. They now run shadPS4's argument checks in its order, keep up to eight contexts with ids allocated as shadPS4 does, validate NpIds with the online ID rule, and answer a connection request with SCE_NP_ERROR_SIGNED_OUT without creating a connection. --- .../libs/prx/libSceNpSignaling/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpSignaling/Export.cpp | 159 +++++++++++++++--- core/libs/tests/GuestNpSignalingOffline.cpp | 132 +++++++++++++++ docs/dev/debt/libSceNpSignaling.md | 5 + 4 files changed, 290 insertions(+), 22 deletions(-) create mode 100644 core/libs/prx/libSceNpSignaling/CMakeLists.txt create mode 100644 core/libs/tests/GuestNpSignalingOffline.cpp create mode 100644 docs/dev/debt/libSceNpSignaling.md diff --git a/core/libs/prx/libSceNpSignaling/CMakeLists.txt b/core/libs/prx/libSceNpSignaling/CMakeLists.txt new file mode 100644 index 0000000000..a5ecdbd944 --- /dev/null +++ b/core/libs/prx/libSceNpSignaling/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpSignaling SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpSignaling PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpSignaling) + +if(BUILD_TESTING) + add_test_executable(guest_np_signaling_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpSignalingOffline.cpp) + target_include_directories(guest_np_signaling_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_signaling_offline_tests PRIVATE libSceNpSignaling) + configure_windows_unwind(guest_np_signaling_offline_tests) + add_test(NAME guest_np_signaling_offline COMMAND guest_np_signaling_offline_tests) + set_tests_properties(guest_np_signaling_offline PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_signaling_offline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpSignaling>") + endif() +endif() diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 8ab2a25170..97c6a7875b 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -1,46 +1,161 @@ +#include <cctype> +#include <cstdint> +#include <cstring> +#include <mutex> +#include <unordered_map> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast<int>(0x80550006); +static constexpr int SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED = static_cast<int>(0x80552701); +static constexpr int SCE_NP_SIGNALING_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80552702); +static constexpr int SCE_NP_SIGNALING_ERROR_CTXID_NOT_AVAILABLE = static_cast<int>(0x80552704); +static constexpr int SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND = static_cast<int>(0x80552705); +static constexpr int SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast<int>(0x8055270E); +static constexpr int SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80552715); +static constexpr int SCE_NP_SIGNALING_ERROR_OWN_NP_ID = static_cast<int>(0x80552716); +static constexpr int CONTEXT_OPTION_FLAG = 1; +static constexpr int MAX_CONTEXT_ID = 8; +static constexpr std::uint32_t CONTEXT_FLAG_BIT = 0x2; + +namespace { +struct SignalingNpId { + char handle[16]; + char term; + char dummy[3]; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; +static_assert(sizeof(SignalingNpId) == 36); + +struct Context { + SignalingNpId owner{}; + std::uint32_t flags = CONTEXT_FLAG_BIT; +}; + +std::mutex signalingMutex; +bool initialized = false; +std::unordered_map<int, Context> contexts; +int lastContextId = 0; + +bool ReadNpId(const void* npId, SignalingNpId& copy) { + std::memcpy(©, npId, sizeof(copy)); + if (copy.term != 0) return false; + const std::size_t length = strnlen(copy.handle, sizeof(copy.handle)); + if (length < 3 || !std::isalnum(static_cast<unsigned char>(copy.handle[0]))) return false; + for (std::size_t index = 1; index < length; ++index) { + const auto value = static_cast<unsigned char>(copy.handle[index]); + if (!std::isalnum(value) && value != '_' && value != '-') return false; + } + return true; +} + +int AllocateContextId() { + int candidate = lastContextId == MAX_CONTEXT_ID ? 1 : lastContextId + 1; + for (int tried = 0; tried < MAX_CONTEXT_ID; ++tried) { + if (!contexts.contains(candidate)) { + lastContextId = candidate; + return candidate; + } + candidate = candidate == MAX_CONTEXT_ID ? 1 : candidate + 1; + } + return -1; +} + +Context* FindContext(int ctxId) { + const auto context = contexts.find(ctxId); + return context == contexts.end() ? nullptr : &context->second; +} +} + extern "C" { -int APS5_VABI sceNpSignalingActivateConnection() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSignalingActivateConnection(int ctxId, const void* peerNpId, int* outConnId) { + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + if (!peerNpId || !outConnId) return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + SignalingNpId peer{}; + if (!ReadNpId(peerNpId, peer)) return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + const Context* context = FindContext(ctxId); + if (!context) return SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + if (std::memcmp(&context->owner, &peer, sizeof(peer)) == 0) return SCE_NP_SIGNALING_ERROR_OWN_NP_ID; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpSignalingCreateContext(const void* npId, void* callback, void* callbackArg, int* outContextId) { + (void)callback; + (void)callbackArg; + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + if (!npId || !outContextId) return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + SignalingNpId owner{}; + if (!ReadNpId(npId, owner)) return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + const int ctxId = AllocateContextId(); + if (ctxId < 0) return SCE_NP_SIGNALING_ERROR_CTXID_NOT_AVAILABLE; + contexts[ctxId] = Context{owner}; + *outContextId = ctxId; return 0; } -int APS5_VABI sceNpSignalingCreateContext() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSignalingDeleteContext(int ctxId) { + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + contexts.erase(ctxId); return 0; } -int APS5_VABI sceNpSignalingDeleteContext() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSignalingGetConnectionInfo(int ctxId, int connId, int infoCode, void* outInfo) { + (void)connId; + (void)infoCode; + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + if (!outInfo) return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(ctxId)) return SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + return SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND; +} + +int APS5_VABI sceNpSignalingInitialize(std::int64_t memorySize, int threadPriority, int cpuAffinityMask, std::int64_t threadStackSize) { + (void)memorySize; + (void)threadPriority; + (void)cpuAffinityMask; + (void)threadStackSize; + std::lock_guard lock(signalingMutex); + if (initialized) return SCE_NP_SIGNALING_ERROR_ALREADY_INITIALIZED; + initialized = true; return 0; } -int APS5_VABI sceNpSignalingGetConnectionInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpSignalingInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpSignalingSetContextOption() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSignalingSetContextOption(int ctxId, int optionId, int optionValue) { + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + Context* context = FindContext(ctxId); + if (!context) return SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + if (optionId != CONTEXT_OPTION_FLAG) return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + if (optionValue == 0) { + context->flags &= ~CONTEXT_FLAG_BIT; + } else if (optionValue == 1) { + context->flags |= CONTEXT_FLAG_BIT; + } else { + return SCE_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } return 0; } int APS5_VABI sceNpSignalingTerminate() { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + initialized = false; + contexts.clear(); + lastContextId = 0; return 0; } -int APS5_VABI sceNpSignalingTerminateConnection() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSignalingTerminateConnection(int ctxId, int connId) { + (void)connId; + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + if (!FindContext(ctxId)) return SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + return SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND; } } diff --git a/core/libs/tests/GuestNpSignalingOffline.cpp b/core/libs/tests/GuestNpSignalingOffline.cpp new file mode 100644 index 0000000000..8e0098017a --- /dev/null +++ b/core/libs/tests/GuestNpSignalingOffline.cpp @@ -0,0 +1,132 @@ +#include "SceTypes.hpp" +#include <cstdint> +#include <cstdio> +#include <cstring> + +extern "C" { +int APS5_VABI sceNpSignalingInitialize(std::int64_t memorySize, int threadPriority, int cpuAffinityMask, std::int64_t threadStackSize); +int APS5_VABI sceNpSignalingTerminate(); +int APS5_VABI sceNpSignalingCreateContext(const void* npId, void* callback, void* callbackArg, int* outContextId); +int APS5_VABI sceNpSignalingDeleteContext(int ctxId); +int APS5_VABI sceNpSignalingSetContextOption(int ctxId, int optionId, int optionValue); +int APS5_VABI sceNpSignalingActivateConnection(int ctxId, const void* peerNpId, int* outConnId); +int APS5_VABI sceNpSignalingTerminateConnection(int ctxId, int connId); +int APS5_VABI sceNpSignalingGetConnectionInfo(int ctxId, int connId, int infoCode, void* outInfo); +} + +namespace { + +constexpr int SignedOut = static_cast<int>(0x80550006u); +constexpr int NotInitialized = static_cast<int>(0x80552701u); +constexpr int AlreadyInitialized = static_cast<int>(0x80552702u); +constexpr int CtxIdNotAvailable = static_cast<int>(0x80552704u); +constexpr int CtxNotFound = static_cast<int>(0x80552705u); +constexpr int ConnNotFound = static_cast<int>(0x8055270Eu); +constexpr int InvalidArgument = static_cast<int>(0x80552715u); +constexpr int OwnNpId = static_cast<int>(0x80552716u); + +struct SignalingNpId { + char handle[16]; + char term; + char dummy[3]; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; +static_assert(sizeof(SignalingNpId) == 36); + +int failures = 0; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +SignalingNpId MakeNpId(const char* handle) { + SignalingNpId id{}; + std::memcpy(id.handle, handle, std::strlen(handle)); + return id; +} + +} + +int main() { + const SignalingNpId owner = MakeNpId("player_1"); + const SignalingNpId peer = MakeNpId("peer-2"); + const SignalingNpId maximum = MakeNpId("abcdefghijklmnop"); + const SignalingNpId tooShort = MakeNpId("ab"); + const SignalingNpId leadingUnderscore = MakeNpId("_player"); + const SignalingNpId withDot = MakeNpId("pl.ayer"); + SignalingNpId unterminated = MakeNpId("player_1"); + unterminated.term = 'x'; + unsigned char info[64]; + std::memset(info, 0xA5, sizeof(info)); + int context = -7; + int connection = -7; + + Require(sceNpSignalingCreateContext(nullptr, nullptr, nullptr, nullptr) == NotInitialized, "sceNpSignalingCreateContext", "a call before initialization did not return NOT_INITIALIZED first"); + Require(sceNpSignalingDeleteContext(1) == NotInitialized, "sceNpSignalingDeleteContext", "a call before initialization did not return NOT_INITIALIZED"); + Require(sceNpSignalingSetContextOption(1, 1, 1) == NotInitialized, "sceNpSignalingSetContextOption", "a call before initialization did not return NOT_INITIALIZED"); + Require(sceNpSignalingActivateConnection(1, nullptr, nullptr) == NotInitialized, "sceNpSignalingActivateConnection", "a call before initialization did not return NOT_INITIALIZED first"); + Require(sceNpSignalingTerminateConnection(1, 1) == NotInitialized, "sceNpSignalingTerminateConnection", "a call before initialization did not return NOT_INITIALIZED"); + Require(sceNpSignalingGetConnectionInfo(1, 1, 1, nullptr) == NotInitialized, "sceNpSignalingGetConnectionInfo", "a call before initialization did not return NOT_INITIALIZED first"); + Require(sceNpSignalingTerminate() == NotInitialized, "sceNpSignalingTerminate", "terminating before initialization did not return NOT_INITIALIZED"); + + Require(sceNpSignalingInitialize(0, 0, 0, 0) == 0, "sceNpSignalingInitialize", "initialization failed"); + Require(sceNpSignalingInitialize(0, 0, 0, 0) == AlreadyInitialized, "sceNpSignalingInitialize", "a second initialization did not return ALREADY_INITIALIZED"); + + Require(sceNpSignalingCreateContext(nullptr, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "a null SignalingNpId was taken"); + Require(sceNpSignalingCreateContext(&owner, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpSignalingCreateContext", "a null output was taken"); + Require(sceNpSignalingCreateContext(&tooShort, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "a two-character online ID was taken"); + Require(sceNpSignalingCreateContext(&leadingUnderscore, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an online ID starting with '_' was taken"); + Require(sceNpSignalingCreateContext(&withDot, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an online ID with '.' was taken"); + Require(sceNpSignalingCreateContext(&unterminated, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an unterminated online ID was taken"); + Require(context == -7, "sceNpSignalingCreateContext", "a refused call wrote the context id"); + + Require(sceNpSignalingCreateContext(&owner, nullptr, nullptr, &context) == 0 && context == 1, "sceNpSignalingCreateContext", "the first context is not id 1"); + int created = 0; + Require(sceNpSignalingCreateContext(&maximum, nullptr, nullptr, &created) == 0 && created == 2, "sceNpSignalingCreateContext", "the second context is not id 2"); + Require(sceNpSignalingDeleteContext(2) == 0, "sceNpSignalingDeleteContext", "deleting context 2 failed"); + for (int expected = 3; expected <= 8; ++expected) { + Require(sceNpSignalingCreateContext(&maximum, nullptr, nullptr, &created) == 0 && created == expected, "sceNpSignalingCreateContext", "a context id was reused before the ids up to 8 were handed out"); + } + Require(sceNpSignalingCreateContext(&maximum, nullptr, nullptr, &created) == 0 && created == 2, "sceNpSignalingCreateContext", "after id 8 the allocation did not wrap to the free id 2"); + created = -7; + Require(sceNpSignalingCreateContext(&owner, nullptr, nullptr, &created) == CtxIdNotAvailable && created == -7, "sceNpSignalingCreateContext", "a ninth context did not return CTXID_NOT_AVAILABLE"); + + Require(sceNpSignalingSetContextOption(99, 2, 5) == CtxNotFound, "sceNpSignalingSetContextOption", "an unknown context was not refused before the option"); + Require(sceNpSignalingSetContextOption(context, 2, 0) == InvalidArgument, "sceNpSignalingSetContextOption", "option id 2 was taken"); + Require(sceNpSignalingSetContextOption(context, 1, 2) == InvalidArgument, "sceNpSignalingSetContextOption", "flag value 2 was taken"); + Require(sceNpSignalingSetContextOption(context, 1, 0) == 0, "sceNpSignalingSetContextOption", "clearing the flag failed"); + Require(sceNpSignalingSetContextOption(context, 1, 1) == 0, "sceNpSignalingSetContextOption", "setting the flag failed"); + + Require(sceNpSignalingActivateConnection(context, nullptr, &connection) == InvalidArgument, "sceNpSignalingActivateConnection", "a null peer was taken"); + Require(sceNpSignalingActivateConnection(context, &peer, nullptr) == InvalidArgument, "sceNpSignalingActivateConnection", "a null output was taken"); + Require(sceNpSignalingActivateConnection(99, &tooShort, &connection) == InvalidArgument, "sceNpSignalingActivateConnection", "an invalid peer was not refused before the context"); + Require(sceNpSignalingActivateConnection(99, &peer, &connection) == CtxNotFound, "sceNpSignalingActivateConnection", "an unknown context was taken"); + Require(sceNpSignalingActivateConnection(context, &owner, &connection) == OwnNpId, "sceNpSignalingActivateConnection", "the context's own SignalingNpId did not return OWN_NP_ID"); + Require(sceNpSignalingActivateConnection(context, &peer, &connection) == SignedOut, "sceNpSignalingActivateConnection", "a valid peer did not return SIGNED_OUT"); + Require(connection == -7, "sceNpSignalingActivateConnection", "a connection id was written"); + + Require(sceNpSignalingTerminateConnection(99, 1) == CtxNotFound, "sceNpSignalingTerminateConnection", "an unknown context was taken"); + Require(sceNpSignalingTerminateConnection(context, 1) == ConnNotFound, "sceNpSignalingTerminateConnection", "a connection that was never created was found"); + Require(sceNpSignalingGetConnectionInfo(99, 1, 1, nullptr) == InvalidArgument, "sceNpSignalingGetConnectionInfo", "a null output was not refused before the context"); + Require(sceNpSignalingGetConnectionInfo(99, 1, 1, info) == CtxNotFound, "sceNpSignalingGetConnectionInfo", "an unknown context was taken"); + Require(sceNpSignalingGetConnectionInfo(context, 1, 1, info) == ConnNotFound, "sceNpSignalingGetConnectionInfo", "a connection that was never created was found"); + Require(info[0] == 0xA5 && info[63] == 0xA5, "sceNpSignalingGetConnectionInfo", "connection information was written"); + + Require(sceNpSignalingDeleteContext(99) == 0, "sceNpSignalingDeleteContext", "deleting an unknown context failed"); + Require(sceNpSignalingDeleteContext(context) == 0, "sceNpSignalingDeleteContext", "deleting a context failed"); + Require(sceNpSignalingSetContextOption(context, 1, 1) == CtxNotFound, "sceNpSignalingSetContextOption", "a deleted context was found"); + + Require(sceNpSignalingTerminate() == 0, "sceNpSignalingTerminate", "termination failed"); + Require(sceNpSignalingCreateContext(&owner, nullptr, nullptr, &created) == NotInitialized, "sceNpSignalingCreateContext", "a context was created after termination"); + Require(sceNpSignalingInitialize(0, 0, 0, 0) == 0, "sceNpSignalingInitialize", "initializing again failed"); + Require(sceNpSignalingSetContextOption(2, 1, 1) == CtxNotFound, "sceNpSignalingTerminate", "a context created before termination is still found"); + Require(sceNpSignalingCreateContext(&owner, nullptr, nullptr, &created) == 0 && created == 1, "sceNpSignalingCreateContext", "termination did not release the contexts and restart the ids at 1"); + Require(sceNpSignalingTerminate() == 0, "sceNpSignalingTerminate", "the second termination failed"); + + if (failures != 0) return 1; + std::puts("NpSignaling offline tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md new file mode 100644 index 0000000000..e792963043 --- /dev/null +++ b/docs/dev/debt/libSceNpSignaling.md @@ -0,0 +1,5 @@ +# libSceNpSignaling technical debt + +### Functional + +- [libSceNpSignaling](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - offline only, not measured on a PS5: the checks, their order, context ids 1 to 8 and the NpId rule (`term` 0, 3 to 16 characters, first alphanumeric, the rest alphanumeric, `_` or `-`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling.cpp#L118-L170) and its [state helpers](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling_state.cpp#L120-L170). `sceNpSignalingInitialize` reserves no heap and starts no thread, so its arguments are not checked, and callbacks are never called. `sceNpSignalingActivateConnection` returns `SCE_NP_ERROR_SIGNED_OUT` after shadPS4's checks up to `OWN_NP_ID`, without its rate limit, and creates no connection, so the connection calls answer `CONN_NOT_FOUND` From fd7d1810742b36efd78f2891e174e82af37f61ec Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Thu, 22 Oct 2026 22:41:30 +0200 Subject: [PATCH 1084/1360] feat(libSceNpUtility): model the lookup and word filter requests offline The 14 lookup and word filter exports threw. They now run shadPS4's argument checks in its order over local title contexts and requests, and the lookup, censor and sanitize calls finish with SCE_NP_ERROR_SIGNED_OUT without writing an NpId or a comment. --- core/libs/prx/libSceNpUtility/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpUtility/Export.cpp | 198 ++++++++++++++----- core/libs/tests/GuestNpUtilityOffline.cpp | 145 ++++++++++++++ docs/dev/debt/libSceNpUtility.md | 5 + 4 files changed, 313 insertions(+), 51 deletions(-) create mode 100644 core/libs/prx/libSceNpUtility/CMakeLists.txt create mode 100644 core/libs/tests/GuestNpUtilityOffline.cpp create mode 100644 docs/dev/debt/libSceNpUtility.md diff --git a/core/libs/prx/libSceNpUtility/CMakeLists.txt b/core/libs/prx/libSceNpUtility/CMakeLists.txt new file mode 100644 index 0000000000..982e5ab620 --- /dev/null +++ b/core/libs/prx/libSceNpUtility/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpUtility SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpUtility PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpUtility) + +if(BUILD_TESTING) + add_test_executable(guest_np_utility_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpUtilityOffline.cpp) + target_include_directories(guest_np_utility_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_utility_offline_tests PRIVATE libSceNpUtility) + configure_windows_unwind(guest_np_utility_offline_tests) + add_test(NAME guest_np_utility_offline COMMAND guest_np_utility_offline_tests) + set_tests_properties(guest_np_utility_offline PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_utility_offline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpUtility>") + endif() +endif() diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 2247cdf62b..f2f1a6a84a 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -1,10 +1,97 @@ #include <cstdint> #include <cstddef> +#include <cstring> +#include <mutex> +#include <unordered_map> +#include <unordered_set> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550003); static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast<int>(0x80550006); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550704); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = static_cast<int>(0x80550706); +static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast<int>(0x80550707); +static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast<int>(0x8055070C); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast<int>(0x8055070E); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_TYPE = static_cast<int>(0x80550711); +static constexpr int POLL_FINISHED = 0; +static constexpr int POLL_RUNNING = 1; +static constexpr std::size_t MAX_TITLE_CONTEXTS = 32; +static constexpr std::size_t MAX_REQUESTS = 32; +static constexpr int WORD_FILTER_TITLE_CTX_ID_OFFSET = 0x01000000; +static constexpr int WORD_FILTER_REQUEST_ID_OFFSET = 0x02000000; +static constexpr std::size_t WORD_FILTER_COMMENT_MAX_LENGTH = 0x400; +static constexpr std::uint64_t WORD_FILTER_ASYNC_PARAMETER_SIZE = 0x18; + +namespace { +struct Request { + int titleCtxId = 0; + bool async = false; + bool used = false; + bool aborted = false; + bool finished = false; + int result = 0; +}; + +struct Service { + Service(int firstTitleContext, int firstRequest) : nextTitleContext(firstTitleContext), nextRequest(firstRequest) {} + std::mutex mutex; + std::unordered_set<int> titleContexts; + std::unordered_map<int, Request> requests; + int nextTitleContext; + int nextRequest; +}; + +Service lookup(1, 1); +Service wordFilter(WORD_FILTER_TITLE_CTX_ID_OFFSET + 1, WORD_FILTER_REQUEST_ID_OFFSET + 1); + +int CreateTitleContext(Service& service, const void* selfNpId) { + if (!selfNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + std::lock_guard lock(service.mutex); + if (service.titleContexts.size() >= MAX_TITLE_CONTEXTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int titleCtxId = service.nextTitleContext++; + service.titleContexts.insert(titleCtxId); + return titleCtxId; +} + +int DeleteTitleContext(Service& service, int titleCtxId) { + std::lock_guard lock(service.mutex); + if (!service.titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(service.requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + return 0; +} + +int CreateRequest(Service& service, int titleCtxId, bool async) { + std::lock_guard lock(service.mutex); + if (!service.titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (service.requests.size() >= MAX_REQUESTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int reqId = service.nextRequest++; + service.requests.emplace(reqId, Request{titleCtxId, async}); + return reqId; +} + +int DeleteRequest(Service& service, int reqId) { + std::lock_guard lock(service.mutex); + return service.requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; +} + +int StartOffline(Service& service, int reqId) { + std::lock_guard lock(service.mutex); + const auto request = service.requests.find(reqId); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (request->second.aborted) return SCE_NP_COMMUNITY_ERROR_ABORTED; + if (request->second.used) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + request->second.used = true; + request->second.finished = true; + request->second.result = SCE_NP_ERROR_SIGNED_OUT; + return request->second.async ? 0 : SCE_NP_ERROR_SIGNED_OUT; +} + +int CheckComment(const char* comment) { + return strnlen(comment, WORD_FILTER_COMMENT_MAX_LENGTH + 1) > WORD_FILTER_COMMENT_MAX_LENGTH ? SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT : 0; +} +} extern "C" { @@ -46,74 +133,83 @@ int APS5_VABI sceNpBandwidthTestInitStart() { return 0; } -int APS5_VABI sceNpLookupCreateRequest() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpLookupCreateRequest(int titleCtxId) { + return CreateRequest(lookup, titleCtxId, false); +} + +int APS5_VABI sceNpLookupCreateTitleCtx(const void* selfNpId) { + return CreateTitleContext(lookup, selfNpId); +} + +int APS5_VABI sceNpLookupDeleteRequest(int reqId) { + return DeleteRequest(lookup, reqId); +} + +int APS5_VABI sceNpLookupDeleteTitleCtx(int titleCtxId) { + return DeleteTitleContext(lookup, titleCtxId); +} + +int APS5_VABI sceNpLookupNpId(int reqId, const NpOnlineId* onlineId, void* npId, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!onlineId || !npId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(lookup, reqId); +} + +int APS5_VABI sceNpWordFilterAbortRequest(int reqId) { + std::lock_guard lock(wordFilter.mutex); + const auto request = wordFilter.requests.find(reqId); + if (request == wordFilter.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + request->second.aborted = true; + if (!request->second.finished) { + request->second.finished = true; + request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; + } return 0; } -int APS5_VABI sceNpLookupCreateTitleCtx() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterCensorComment(int reqId, const char* comment, void* option) { + if (!comment) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (const int error = CheckComment(comment)) return error; + return StartOffline(wordFilter, reqId); } -int APS5_VABI sceNpLookupDeleteRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterCreateAsyncRequest(int titleCtxId, const std::uint64_t* param) { + if (!param || *param != WORD_FILTER_ASYNC_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return CreateRequest(wordFilter, titleCtxId, true); } -int APS5_VABI sceNpLookupDeleteTitleCtx() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterCreateRequest(int titleCtxId) { + return CreateRequest(wordFilter, titleCtxId, false); } -int APS5_VABI sceNpLookupNpId() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterCreateTitleCtx(const void* selfNpId) { + return CreateTitleContext(wordFilter, selfNpId); } -int APS5_VABI sceNpWordFilterAbortRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterDeleteRequest(int reqId) { + return DeleteRequest(wordFilter, reqId); } -int APS5_VABI sceNpWordFilterCensorComment() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId) { + return DeleteTitleContext(wordFilter, titleCtxId); } -int APS5_VABI sceNpWordFilterCreateAsyncRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result) { + if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(wordFilter.mutex); + const auto request = wordFilter.requests.find(reqId); + if (request == wordFilter.requests.end() || !request->second.async) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.finished) return POLL_RUNNING; + *result = request->second.result; + return POLL_FINISHED; } -int APS5_VABI sceNpWordFilterCreateRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpWordFilterCreateTitleCtx() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpWordFilterDeleteRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpWordFilterDeleteTitleCtx() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpWordFilterPollAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpWordFilterSanitizeComment() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option) { + if (!comment || !sanitizedComment) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (const int error = CheckComment(comment)) return error; + return StartOffline(wordFilter, reqId); } } diff --git a/core/libs/tests/GuestNpUtilityOffline.cpp b/core/libs/tests/GuestNpUtilityOffline.cpp new file mode 100644 index 0000000000..08cd2b84e2 --- /dev/null +++ b/core/libs/tests/GuestNpUtilityOffline.cpp @@ -0,0 +1,145 @@ +#include "SceTypes.hpp" +#include <cstdint> +#include <cstdio> +#include <cstring> + +extern "C" { +int APS5_VABI sceNpLookupCreateTitleCtx(const void* selfNpId); +int APS5_VABI sceNpLookupDeleteTitleCtx(int titleCtxId); +int APS5_VABI sceNpLookupCreateRequest(int titleCtxId); +int APS5_VABI sceNpLookupDeleteRequest(int reqId); +int APS5_VABI sceNpLookupNpId(int reqId, const NpOnlineId* onlineId, void* npId, void* option); +int APS5_VABI sceNpWordFilterCreateTitleCtx(const void* selfNpId); +int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId); +int APS5_VABI sceNpWordFilterCreateRequest(int titleCtxId); +int APS5_VABI sceNpWordFilterCreateAsyncRequest(int titleCtxId, const std::uint64_t* param); +int APS5_VABI sceNpWordFilterDeleteRequest(int reqId); +int APS5_VABI sceNpWordFilterAbortRequest(int reqId); +int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result); +int APS5_VABI sceNpWordFilterCensorComment(int reqId, const char* comment, void* option); +int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option); +} + +namespace { + +constexpr int SignedOut = static_cast<int>(0x80550006u); +constexpr int InvalidArgument = static_cast<int>(0x80550704u); +constexpr int TooManyObjects = static_cast<int>(0x80550706u); +constexpr int Aborted = static_cast<int>(0x80550707u); +constexpr int InsufficientArgument = static_cast<int>(0x8055070Cu); +constexpr int InvalidId = static_cast<int>(0x8055070Eu); +constexpr int InvalidType = static_cast<int>(0x80550711u); +constexpr int PollFinished = 0; +constexpr int PollRunning = 1; +constexpr int WordFilterTitleCtxBase = 0x01000000; +constexpr int WordFilterRequestBase = 0x02000000; + +int failures = 0; +unsigned char selfNpId[36]; +unsigned char npId[36]; +char sanitized[0x402]; +char longComment[0x402]; +const std::uint64_t asyncParam[3] = {0x18, 0, 0}; +const std::uint64_t wrongAsyncParam[3] = {0x10, 0, 0}; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +void CheckLookup() { + NpOnlineId onlineId{}; + std::strcpy(onlineId.data, "player_1"); + Require(sceNpLookupCreateTitleCtx(nullptr) == InsufficientArgument, "sceNpLookupCreateTitleCtx", "a null NpId was taken"); + const int context = sceNpLookupCreateTitleCtx(selfNpId); + Require(context == 1, "sceNpLookupCreateTitleCtx", "the first lookup context is not id 1"); + Require(sceNpLookupCreateRequest(context + 100) == InvalidId, "sceNpLookupCreateRequest", "an unknown context was taken"); + const int req = sceNpLookupCreateRequest(context); + Require(req == 1, "sceNpLookupCreateRequest", "the first lookup request is not id 1"); + std::memset(npId, 0xA5, sizeof(npId)); + Require(sceNpLookupNpId(req, nullptr, nullptr, &onlineId) == InvalidArgument, "sceNpLookupNpId", "an option was not refused before the null arguments"); + Require(sceNpLookupNpId(req, nullptr, npId, nullptr) == InsufficientArgument, "sceNpLookupNpId", "a null online ID was taken"); + Require(sceNpLookupNpId(req, &onlineId, nullptr, nullptr) == InsufficientArgument, "sceNpLookupNpId", "a null NpId output was taken"); + Require(sceNpLookupNpId(req + 100, &onlineId, npId, nullptr) == InvalidId, "sceNpLookupNpId", "an unknown request was taken"); + Require(sceNpLookupNpId(req, &onlineId, npId, nullptr) == SignedOut, "sceNpLookupNpId", "the lookup did not return SIGNED_OUT"); + Require(sceNpLookupNpId(req, &onlineId, npId, nullptr) == InvalidType, "sceNpLookupNpId", "a used request did not return INVALID_TYPE"); + Require(npId[0] == 0xA5 && npId[35] == 0xA5, "sceNpLookupNpId", "an NpId was written"); + Require(sceNpLookupDeleteRequest(req) == 0, "sceNpLookupDeleteRequest", "deleting the request failed"); + Require(sceNpLookupDeleteRequest(req) == InvalidId, "sceNpLookupDeleteRequest", "a deleted request was found"); + int requests[32]; + for (int& request : requests) request = sceNpLookupCreateRequest(context); + Require(requests[31] > 0, "sceNpLookupCreateRequest", "32 requests could not be created"); + Require(sceNpLookupCreateRequest(context) == TooManyObjects, "sceNpLookupCreateRequest", "a 33rd request did not return TOO_MANY_OBJECTS"); + Require(sceNpLookupDeleteTitleCtx(context) == 0, "sceNpLookupDeleteTitleCtx", "deleting the context failed"); + Require(sceNpLookupDeleteRequest(requests[0]) == InvalidId, "sceNpLookupDeleteTitleCtx", "a request of the deleted context was kept"); + Require(sceNpLookupDeleteTitleCtx(context) == InvalidId, "sceNpLookupDeleteTitleCtx", "a deleted context was found"); + int contexts[32]; + for (int& entry : contexts) entry = sceNpLookupCreateTitleCtx(selfNpId); + Require(contexts[0] == 2 && contexts[31] == 33, "sceNpLookupCreateTitleCtx", "lookup context ids do not keep counting from 2"); + Require(sceNpLookupCreateTitleCtx(selfNpId) == TooManyObjects, "sceNpLookupCreateTitleCtx", "a 33rd context did not return TOO_MANY_OBJECTS"); + for (const int entry : contexts) sceNpLookupDeleteTitleCtx(entry); +} + +void CheckWordFilter() { + Require(sceNpWordFilterCreateTitleCtx(nullptr) == InsufficientArgument, "sceNpWordFilterCreateTitleCtx", "a null NpId was taken"); + const int context = sceNpWordFilterCreateTitleCtx(selfNpId); + Require(context == WordFilterTitleCtxBase + 1, "sceNpWordFilterCreateTitleCtx", "the first word filter context is not 0x01000001"); + Require(sceNpWordFilterCreateAsyncRequest(context, nullptr) == InvalidArgument, "sceNpWordFilterCreateAsyncRequest", "a null parameter was taken"); + Require(sceNpWordFilterCreateAsyncRequest(context, wrongAsyncParam) == InvalidArgument, "sceNpWordFilterCreateAsyncRequest", "a parameter size other than 0x18 was taken"); + Require(sceNpWordFilterCreateAsyncRequest(context + 100, asyncParam) == InvalidId, "sceNpWordFilterCreateAsyncRequest", "an unknown context was taken"); + const int sync = sceNpWordFilterCreateRequest(context); + Require(sync == WordFilterRequestBase + 1, "sceNpWordFilterCreateRequest", "the first word filter request is not 0x02000001"); + std::memset(longComment, 'a', 0x400); + longComment[0x400] = 0; + Require(sceNpWordFilterCensorComment(sync, nullptr, &sanitized) == InsufficientArgument, "sceNpWordFilterCensorComment", "a null comment was not refused before the option"); + Require(sceNpWordFilterCensorComment(sync, "word", &sanitized) == InvalidArgument, "sceNpWordFilterCensorComment", "an option was taken"); + longComment[0x400] = 'a'; + Require(sceNpWordFilterCensorComment(sync, longComment, nullptr) == InvalidArgument, "sceNpWordFilterCensorComment", "a 0x401-byte comment was taken"); + Require(sceNpWordFilterCensorComment(sync + 100, "word", nullptr) == InvalidId, "sceNpWordFilterCensorComment", "an unknown request was taken"); + longComment[0x400] = 0; + Require(sceNpWordFilterCensorComment(sync, longComment, nullptr) == SignedOut, "sceNpWordFilterCensorComment", "a 0x400-byte comment did not return SIGNED_OUT"); + Require(sceNpWordFilterCensorComment(sync, "word", nullptr) == InvalidType, "sceNpWordFilterCensorComment", "a used request did not return INVALID_TYPE"); + int result = 7; + Require(sceNpWordFilterPollAsync(sync, &result) == InvalidId && result == 7, "sceNpWordFilterPollAsync", "a synchronous request was polled"); + Require(sceNpWordFilterDeleteRequest(sync) == 0, "sceNpWordFilterDeleteRequest", "deleting the request failed"); + + const int async = sceNpWordFilterCreateAsyncRequest(context, asyncParam); + Require(async == WordFilterRequestBase + 2, "sceNpWordFilterCreateAsyncRequest", "the asynchronous request did not take the next id"); + Require(sceNpWordFilterPollAsync(async, nullptr) == InvalidArgument, "sceNpWordFilterPollAsync", "a null result was taken"); + Require(sceNpWordFilterPollAsync(async + 100, &result) == InvalidId, "sceNpWordFilterPollAsync", "an unknown request was taken"); + Require(sceNpWordFilterPollAsync(async, &result) == PollRunning && result == 7, "sceNpWordFilterPollAsync", "an unstarted request was not running"); + std::memset(sanitized, 'z', sizeof(sanitized)); + Require(sceNpWordFilterSanitizeComment(async, "word", nullptr, nullptr) == InsufficientArgument, "sceNpWordFilterSanitizeComment", "a null output was taken"); + Require(sceNpWordFilterSanitizeComment(async, nullptr, sanitized, &result) == InsufficientArgument, "sceNpWordFilterSanitizeComment", "a null comment was not refused before the option"); + Require(sceNpWordFilterSanitizeComment(async, "word", sanitized, &result) == InvalidArgument, "sceNpWordFilterSanitizeComment", "an option was taken"); + Require(sceNpWordFilterSanitizeComment(async, "word", sanitized, nullptr) == 0, "sceNpWordFilterSanitizeComment", "the asynchronous request did not start"); + Require(sceNpWordFilterPollAsync(async, &result) == PollFinished && result == SignedOut, "sceNpWordFilterPollAsync", "the request did not finish with SIGNED_OUT"); + Require(sanitized[0] == 'z' && sanitized[4] == 'z', "sceNpWordFilterSanitizeComment", "a sanitized comment was written"); + Require(sceNpWordFilterAbortRequest(async) == 0, "sceNpWordFilterAbortRequest", "aborting a finished request failed"); + Require(sceNpWordFilterPollAsync(async, &result) == PollFinished && result == SignedOut, "sceNpWordFilterAbortRequest", "aborting a finished request changed its result"); + + const int aborted = sceNpWordFilterCreateAsyncRequest(context, asyncParam); + Require(sceNpWordFilterAbortRequest(aborted + 100) == InvalidId, "sceNpWordFilterAbortRequest", "an unknown request was taken"); + Require(sceNpWordFilterAbortRequest(aborted) == 0, "sceNpWordFilterAbortRequest", "aborting failed"); + Require(sceNpWordFilterPollAsync(aborted, &result) == PollFinished && result == Aborted, "sceNpWordFilterAbortRequest", "an aborted request did not finish with ABORTED"); + Require(sceNpWordFilterCensorComment(aborted, "word", nullptr) == Aborted, "sceNpWordFilterCensorComment", "an aborted request did not return ABORTED"); + + const int kept = sceNpWordFilterCreateRequest(context); + Require(sceNpWordFilterDeleteTitleCtx(context) == 0, "sceNpWordFilterDeleteTitleCtx", "deleting the context failed"); + Require(sceNpWordFilterDeleteRequest(kept) == InvalidId, "sceNpWordFilterDeleteTitleCtx", "a request of the deleted context was kept"); + Require(sceNpWordFilterDeleteTitleCtx(context) == InvalidId, "sceNpWordFilterDeleteTitleCtx", "a deleted context was found"); + Require(sceNpLookupDeleteTitleCtx(context) == InvalidId, "sceNpLookupDeleteTitleCtx", "a word filter context was found among the lookup contexts"); +} + +} + +int main() { + std::memset(selfNpId, 0, sizeof(selfNpId)); + std::strcpy(reinterpret_cast<char*>(selfNpId), "player_1"); + CheckLookup(); + CheckWordFilter(); + if (failures != 0) return 1; + std::puts("NpUtility offline tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpUtility.md b/docs/dev/debt/libSceNpUtility.md new file mode 100644 index 0000000000..b1cd969543 --- /dev/null +++ b/docs/dev/debt/libSceNpUtility.md @@ -0,0 +1,5 @@ +# libSceNpUtility technical debt + +### Functional + +- [NpLookup and NpWordFilter](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline only, not measured on a PS5: the argument checks and their order, the limit of 32 title contexts and 32 requests, the word filter id bases `0x01000001` and `0x02000001`, the 0x400-byte comment limit and single-use requests (`INVALID_TYPE`) follow shadPS4's [lookup](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L41-L222) and [word filter](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L705-L909) calls. `sceNpLookupNpId`, `sceNpWordFilterCensorComment` and `sceNpWordFilterSanitizeComment` then finish with `SCE_NP_ERROR_SIGNED_OUT` and write no NpId or comment; shadPS4's word filter accepts every comment as a stand-in for the server, so its result is not used From 7be1132713abd5a6d3bf0a2fb704fed9a62dc7ef Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Thu, 22 Oct 2026 22:38:29 +0200 Subject: [PATCH 1085/1360] fix(libScePad): make the special port handle behave as the PS4 measured The ps4probe libScePad survey (PS4 Pro, firmware 10.71) measured the special port with one user: a second scePadOpen returns ALREADY_OPENED (0x80920004); scePadRead and scePadReadState give the rest record (sticks 0x80, orientation w 1, acceleration y 1, connected 0); scePadGetControllerInformation is zeroed; scePadDeviceClassGetExtendedInformation reports device class -1; and scePadSetVibration, scePadSetLightBar, scePadResetLightBar, scePadSetMotionSensorState and scePadResetOrientation return 0. The special handle is now accepted by every pad call: setters succeed without touching the emulated pad, getters return the measured records. Calls not covered by the survey follow the same rule and the debt entry lists them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- core/libs/prx/libScePad/Export.cpp | 70 ++++++++++++++++++++---------- core/libs/tests/GuestPad.cpp | 32 +++++++++++--- docs/dev/debt/libScePad.md | 2 +- 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 2191f00696..6f3bc59ece 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -107,6 +107,7 @@ namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast<int>(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast<int>(0x80920008); +constexpr int PAD_ERROR_ALREADY_OPENED = static_cast<int>(0x80920004); bool g_opened = false; std::array<int, PAD_SPECIAL_HANDLE_COUNT> g_specialUsers{}; @@ -124,6 +125,18 @@ int SpecialHandleOf(int userId) { return PAD_ERROR_DEVICE_NO_HANDLE; } +bool KnownHandle(int handle) { + return handle == PAD_HANDLE || SpecialHandle(handle); +} + +PadData SpecialData() { + PadData data{}; + data.left_stick_x = data.left_stick_y = data.right_stick_x = data.right_stick_y = 0x80; + data.orientation_w = 1.0f; + data.acceleration_y = 1.0f; + return data; +} + bool ValidPort(int userId, int type, int index) { const bool personalPort = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL; const bool systemRemote = userId == PAD_USER_ID_SYSTEM && type == PAD_PORT_TYPE_REMOTE; @@ -178,15 +191,15 @@ int APS5_VABI scePadVrControllerReadCombinedState(std::int32_t leftHandle, std:: } int APS5_VABI scePadDeviceClassGetExtendedInformation(int handle, PadDeviceClassExtendedInformation* info) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; std::memset(info, 0, sizeof(*info)); - info->deviceClass = PAD_DEVICE_CLASS_STANDARD; + info->deviceClass = SpecialHandle(handle) ? -1 : PAD_DEVICE_CLASS_STANDARD; return PAD_OK; } int APS5_VABI scePadDeviceClassParseData(int handle, const PadData* data, PadDeviceClassData* class_data) { - if (!SpecialHandle(handle) && handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr || class_data == nullptr) return PAD_ERROR_INVALID_ARG; std::memset(class_data, 0, sizeof(*class_data)); if (SpecialHandle(handle)) return PAD_OK; @@ -196,7 +209,7 @@ int APS5_VABI scePadDeviceClassParseData(int handle, const PadData* data, PadDev } int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformation* info) { - if (!SpecialHandle(handle) && handle != PAD_HANDLE) { + if (!KnownHandle(handle)) { return PAD_ERROR_INVALID_HANDLE; } if (info == nullptr) { @@ -239,16 +252,17 @@ int APS5_VABI scePadGetInfo_nid_postfix(PadInfo* info) { } int APS5_VABI scePadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; std::memset(info, 0, sizeof(*info)); return PAD_OK; } int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; std::memset(info, 0, 0x2c); + if (SpecialHandle(handle)) return PAD_OK; PadControllerInformation* base = static_cast<PadControllerInformation*>(info); base->touchPadInfo.pixelDensity = 44.86f; base->touchPadInfo.resolution.x = kTouchPadWidth; @@ -284,8 +298,7 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void return PAD_ERROR_INVALID_ARG; } if (type == PAD_PORT_TYPE_SPECIAL) { - const int opened = SpecialHandleOf(userId); - if (opened != PAD_ERROR_DEVICE_NO_HANDLE) return opened; + if (SpecialHandleOf(userId) != PAD_ERROR_DEVICE_NO_HANDLE) return PAD_ERROR_ALREADY_OPENED; for (int slot = 0; slot < PAD_SPECIAL_HANDLE_COUNT; ++slot) { if (g_specialUsers[slot] == 0) { g_specialUsers[slot] = userId; @@ -315,64 +328,71 @@ int APS5_VABI scePadSetFeatureReport(std::int32_t handle, std::uint8_t reportId, int APS5_VABI scePadOutputReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { - if (!SpecialHandle(handle) && handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr || num <= 0) return PAD_ERROR_INVALID_ARG; if (SpecialHandle(handle)) { - data[0] = PadData{}; + data[0] = SpecialData(); return 1; } return Pad::Read(data, num); } int APS5_VABI scePadReadState(int handle, PadData* data) { - if (!SpecialHandle(handle) && handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr) return PAD_ERROR_INVALID_ARG; - *data = SpecialHandle(handle) ? PadData{} : Pad::ReadState(); + *data = SpecialHandle(handle) ? SpecialData() : Pad::ReadState(); return 0; } int APS5_VABI scePadResetLightBar(int handle) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetLightBar(false, 0, 0, 0); return PAD_OK; } int APS5_VABI scePadResetOrientation(int handle) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; Pad::ResetOrientation(); return PAD_OK; } int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetAngularVelocityDeadband(enable); return PAD_OK; } int APS5_VABI scePadSetLightBar(int handle, const PadLightBarParam* param) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (param == nullptr) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetLightBar(true, param->r, param->g, param->b); return PAD_OK; } int APS5_VABI scePadSetMotionSensorState(int handle, bool enable) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetMotionEnabled(enable); return PAD_OK; } int APS5_VABI scePadSetTiltCorrectionState(int handle, bool enabled) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetTiltCorrection(enabled); return PAD_OK; } int APS5_VABI scePadSetTriggerEffect(int handle, const void* param) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (param == nullptr) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) return PAD_OK; const std::uint8_t* bytes = static_cast<const std::uint8_t*>(param); const std::uint8_t mask = bytes[0]; if ((mask & ~0x3u) != 0) { @@ -390,8 +410,9 @@ int APS5_VABI scePadSetTriggerEffect(int handle, const void* param) { } int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (param == nullptr) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetVibration(param->large_motor, param->small_motor); return PAD_OK; } @@ -399,8 +420,9 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { int APS5_VABI scePadSetVibrationMode(int handle, int mode) { if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; if (mode <= 0) return PAD_ERROR_INVALID_ARG; - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (mode > 3 && mode != 5) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) return PAD_OK; Pad::SetVibrationMode(mode); return PAD_OK; } @@ -443,7 +465,7 @@ int APS5_VABI scePadVrControllerSetTriggerEffects(std::int32_t leftHandle, std:: } int APS5_VABI scePadIsRemoteController(int handle, bool* remote) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (remote == nullptr) return PAD_ERROR_INVALID_ARG; *remote = false; return 0; @@ -451,7 +473,7 @@ int APS5_VABI scePadIsRemoteController(int handle, bool* remote) { int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabled) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (enabled) NotImplemented_nid_no_patch(__func__); return PAD_OK; } @@ -469,7 +491,7 @@ int APS5_VABI scePadVrControllerGetTriggerEffectStates(std::int32_t leftHandle, int APS5_VABI scePadGetAccessibilityControllerState(int handle, std::uint32_t* state) { if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; if (state == nullptr) return PAD_ERROR_INVALID_ARG; - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; *state = 0; return PAD_OK; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 2e8572d975..b3cd0776c2 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -32,6 +32,8 @@ int APS5_VABI scePadIsRemoteController(int, bool*); int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); int APS5_VABI scePadDeviceClassParseData(int, const PadData*, PadDeviceClassData*); +int APS5_VABI scePadDeviceClassGetExtendedInformation(int, PadDeviceClassExtendedInformation*); +int APS5_VABI scePadSetVibration(int, const PadVibrationParam*); int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); @@ -310,9 +312,10 @@ static void CheckReadStateHandle(int handle) { static void CheckSpecialPort(int user, int handle) { constexpr int noHandle = static_cast<int>(0x80920008); + constexpr int alreadyOpened = static_cast<int>(0x80920004); const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); Require(special > 0 && special != handle); - Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == special); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == alreadyOpened); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == special); Require(scePadGetHandle(user, 0, 0) == handle); PadData data[2]; @@ -321,11 +324,15 @@ static void CheckSpecialPort(int user, int handle) { Require(scePadRead_nid_postfix(special, data, 0) == PAD_ERROR_INVALID_ARG); Require(scePadRead_nid_postfix(special, data, 2) == 1); Require(!data[0].connected && data[0].connected_count == 0 && data[0].buttons == 0 && data[0].timestamp == 0); + Require(data[0].left_stick_x == 0x80 && data[0].left_stick_y == 0x80 && data[0].right_stick_x == 0x80 && data[0].right_stick_y == 0x80); + Require(data[0].analog_buttons_l2 == 0 && data[0].orientation_x == 0.0f && data[0].orientation_w == 1.0f); + Require(data[0].acceleration_x == 0.0f && data[0].acceleration_y == 1.0f && data[0].acceleration_z == 0.0f && data[0].touch_data_touch_num == 0); Require(data[1].buttons == 0xffffffff); std::memset(data, 0xff, sizeof(data[0])); Require(scePadReadState(special, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadReadState(special, data) == PAD_OK); Require(!data[0].connected && data[0].connected_count == 0 && data[0].buttons == 0); + Require(data[0].left_stick_x == 0x80 && data[0].orientation_w == 1.0f && data[0].acceleration_y == 1.0f); PadControllerInformation info; std::memset(&info, 0xff, sizeof(info)); Require(scePadGetControllerInformation(special, nullptr) == PAD_ERROR_INVALID_ARG); @@ -335,7 +342,20 @@ static void CheckSpecialPort(int user, int handle) { std::memset(&classData, 0xff, sizeof(classData)); Require(scePadDeviceClassParseData(special, data, &classData) == PAD_OK); Require(!classData.dataValid && classData.deviceClass == 0); - Require(scePadResetLightBar(special) == PAD_ERROR_INVALID_HANDLE); + PadDeviceClassExtendedInformation extended; + std::memset(&extended, 0xff, sizeof(extended)); + Require(scePadDeviceClassGetExtendedInformation(special, &extended) == PAD_OK); + Require(extended.deviceClass == -1 && extended.reserved[0] == 0); + Require(scePadDeviceClassGetExtendedInformation(handle, &extended) == PAD_OK); + Require(extended.deviceClass == 0); + const PadVibrationParam vibration{0, 0}; + const PadLightBarParam lightBar{0, 0, 255}; + Require(scePadSetVibration(special, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibration(special, &vibration) == PAD_OK); + Require(scePadSetLightBar(special, &lightBar) == PAD_OK); + Require(scePadResetLightBar(special) == PAD_OK); + Require(scePadSetMotionSensorState(special, true) == PAD_OK); + Require(scePadResetOrientation(special) == PAD_OK); Require(scePadReadState(handle, data) == PAD_OK); Require(data[0].connected); Require(scePadGetControllerInformation(handle, &info) == PAD_OK); @@ -354,15 +374,17 @@ static void CheckSpecialPort(int user, int handle) { Require(first > 0 && second > 0 && first != second && second != handle); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == first); Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == second); - Require(scePadOpen_nid_postfix(other, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == second); + Require(scePadOpen_nid_postfix(other, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == alreadyOpened); Require(scePadClose_nid_postfix(first) == PAD_OK); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == second); Require(scePadRead_nid_postfix(first, data, 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadRead_nid_postfix(second, data, 1) == 1); Require(!data[0].connected); - Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == first); - Require(scePadClose_nid_postfix(first) == PAD_OK); + const int again = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(again > 0 && again != second); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == again); + Require(scePadClose_nid_postfix(again) == PAD_OK); Require(scePadClose_nid_postfix(second) == PAD_OK); Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); Require(scePadGetHandle(user, 0, 0) == handle); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 58850fc66a..afc13ecf9a 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -12,7 +12,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists -- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - `scePadOpen` opens a special port (type 2) with a handle per user (`PAD_SPECIAL_HANDLE` + slot, four slots, a fifth user throws), whose reads return a zeroed record (`connected` false), whose controller information and device class data are zeroed, and which the other pad calls reject with `INVALID_HANDLE`; a user's second open returns its handle again. A PS4 Pro (firmware 10.71) with no special controller opens the port for any valid user, reads one record with `connected` false, succeeds in `scePadGetControllerInformation` and rejects handle -1; the handle value, the other fields, the other calls, two users holding the port at once, a repeated open and a PS5 are not measured. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device by vendor and product id and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, four slots, a fifth user throws) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: two users at once, the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, one user, no special controller) did not cover; it measured open, `ALREADY_OPENED` on a second open, `scePadGetHandle`, both reads, both informations, vibration, light bar, motion sensor and orientation setters, and close. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect From 1499ae961d82ea603149ff4816624c10002e9cfb Mon Sep 17 00:00:00 2001 From: Joey Fisher <fisherjoey@ymail.com> Date: Thu, 22 Oct 2026 09:56:06 -0600 Subject: [PATCH 1086/1360] test(agc): collect the idle publish test block before the label retile agc_idle_publish_wait fails on RADV (RX 570, Steam Deck, #9781) at the label flush check: the publish was not waited, it was dropped as stale. The idle flush's submission is the first to reference the test block's host import, and amdgpu validates a user pointer's pages with write faults at every submission, which clears the asynchronous userfaultfd write protection of the whole import. The test forces APS5_WRITE_WATCH_IMPORTS=watch (the probe in decideImportWatch would leave such imports compared), so the next collect reads every unit as stored by the CPU. The test retiled unit 5 without collecting first, so the label publish's own collect stamped the unit newer than the retile's generation and publishUnits dropped it; no copy was recorded and PendingWriteOverlaps was false. The test now collects before that retile, as it already did before the idle ones, and models the condition on every driver with an uncollected store older than the retile. The checks run in an order that names the dropped publish before the wait. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 97f02e7f2f..8a0b4d0ddc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4804,11 +4804,14 @@ void idlePublishWaitTests(const Device& device, Recorder& recorder) { recorder.Sync(); Require(words[2 * unit] == 0x44 && words[4 * unit - 1] == 0x45, "a CPU store made after the idle flush returned was overwritten by its publish"); Require(words[2 * unit + 1] == 0x22 && words[3 * unit - 1] == 0x22 && words[3 * unit] == 0x33 && words[1 * unit] == 0x11 && words[4 * unit] == 0x11, "the idle publish's bytes are wrong"); + words[5 * unit] = 0x11; + CollectWritesUncached(address, bytes); retile(address + 5 * unit, 0x55); StorageTexture::FlushAllPending("label"); - Require(recorder.PendingWriteOverlaps(address + 5 * unit, unit), "a publish of a flush other than the idle one was waited"); + const bool pending = recorder.PendingWriteOverlaps(address + 5 * unit, unit); recorder.Sync(); - Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land"); + Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land: the unit read as written after its retile, so the publish dropped it as stale"); + Require(pending, "a publish of a flush other than the idle one was waited"); std::cout << "idle publish wait verified\n"; } From 7678df544f36389059afc1c5ba24129ef523ce62 Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Thu, 22 Oct 2026 22:45:14 +0200 Subject: [PATCH 1087/1360] fix(libScePad): answer a 17th special-port user with the unknown-user code The special port table had four slots and threw for a fifth user, which the progress report counted as scePadOpen going back to a stub. A PS4 holds at most 16 accounts, so the table now has 16 slots and a 17th distinct user id gets 0x809b0081, the code the console returned for an unknown user id in the libScePad survey (PS4 Pro, firmware 10.71). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- core/libs/prx/libScePad/Export.cpp | 3 ++- core/libs/prx/libScePad/include/Pad.hpp | 2 +- core/libs/tests/GuestPad.cpp | 10 ++++++++++ docs/dev/debt/libScePad.md | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 6f3bc59ece..0927793b8c 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -108,6 +108,7 @@ namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast<int>(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast<int>(0x80920008); constexpr int PAD_ERROR_ALREADY_OPENED = static_cast<int>(0x80920004); +constexpr int PAD_ERROR_UNKNOWN_USER = static_cast<int>(0x809b0081); bool g_opened = false; std::array<int, PAD_SPECIAL_HANDLE_COUNT> g_specialUsers{}; @@ -305,7 +306,7 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void return PAD_SPECIAL_HANDLE + slot; } } - NotImplemented_nid_no_patch(__func__); + return PAD_ERROR_UNKNOWN_USER; } g_opened = true; return PAD_HANDLE; diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index 2f4baae78e..135bd54b59 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -14,7 +14,7 @@ constexpr int PAD_PORT_TYPE_REMOTE = 16; constexpr int PAD_USER_ID_SYSTEM = 0xff; constexpr int PAD_HANDLE = 1; constexpr int PAD_SPECIAL_HANDLE = 0x20; -constexpr int PAD_SPECIAL_HANDLE_COUNT = 4; +constexpr int PAD_SPECIAL_HANDLE_COUNT = 16; constexpr int PAD_CONNECTION_TYPE_LOCAL = 0; constexpr int PAD_DEVICE_CLASS_STANDARD = 0; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index b3cd0776c2..760a727db4 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -387,6 +387,16 @@ static void CheckSpecialPort(int user, int handle) { Require(scePadClose_nid_postfix(again) == PAD_OK); Require(scePadClose_nid_postfix(second) == PAD_OK); Require(scePadGetHandle(other, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + std::array<int, 16> handles{}; + for (int i = 0; i < 16; ++i) { + handles[i] = scePadOpen_nid_postfix(user + i, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(handles[i] > 0 && handles[i] != handle && (i == 0 || handles[i] != handles[i - 1])); + } + Require(scePadOpen_nid_postfix(user + 16, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast<int>(0x809b0081)); + Require(scePadGetHandle(user + 16, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + Require(scePadGetHandle(user + 15, PAD_PORT_TYPE_SPECIAL, 0) == handles[15]); + for (int i = 0; i < 16; ++i) Require(scePadClose_nid_postfix(handles[i]) == PAD_OK); + Require(scePadGetHandle(user + 15, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); Require(scePadGetHandle(user, 0, 0) == handle); } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index afc13ecf9a..fea6b7fe4b 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -12,7 +12,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists -- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, four slots, a fifth user throws) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: two users at once, the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, one user, no special controller) did not cover; it measured open, `ALREADY_OPENED` on a second open, `scePadGetHandle`, both reads, both informations, vibration, light bar, motion sensor and orientation setters, and close. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: two users at once, the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, one user, no special controller) did not cover; it measured open, `ALREADY_OPENED` on a second open, `scePadGetHandle`, both reads, both informations, vibration, light bar, motion sensor and orientation setters, and close. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect From 7140ec1591c1d8569448abd85d70e7d97231b1a0 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros <savvaniss@yahoo.gr> Date: Thu, 22 Oct 2026 14:56:53 +0300 Subject: [PATCH 1088/1360] feat(np): answer and declare the PS4 NP imports of CUSA03312 NpMatching2 keeps its initialized state. sceNpSignalingDeactivateConnection and sceInvitationDialogClose, which these titles also import, are declared and throw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 10 +++ .../prx/libSceInvitationDialog/Export.cpp | 5 ++ core/libs/prx/libSceNpMatching2/Export.cpp | 39 +++++++++-- core/libs/prx/libSceNpSignaling/Export.cpp | 5 ++ core/libs/tests/GuestNpPs4Imports.cpp | 66 +++++++++++++++++++ docs/dev/debt/libSceInvitationDialog.md | 5 ++ docs/dev/debt/libSceNpMatching2.md | 5 ++ docs/dev/debt/libSceNpSignaling.md | 5 ++ 8 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestNpPs4Imports.cpp create mode 100644 docs/dev/debt/libSceInvitationDialog.md create mode 100644 docs/dev/debt/libSceNpMatching2.md create mode 100644 docs/dev/debt/libSceNpSignaling.md diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 0472176eb2..586f216d20 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -408,6 +408,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpParty>") endif() + add_test_executable(guest_np_ps4_imports_tests tests/GuestNpPs4Imports.cpp) + target_include_directories(guest_np_ps4_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_ps4_imports_tests PRIVATE libSceNpMatching2 libSceNpSignaling libc) + configure_windows_unwind(guest_np_ps4_imports_tests) + add_test(NAME guest_np_ps4_imports COMMAND guest_np_ps4_imports_tests) + if(WIN32) + set_tests_properties(guest_np_ps4_imports PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpMatching2>") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc) diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp index 8ae801bc26..6292323d0c 100644 --- a/core/libs/prx/libSceInvitationDialog/Export.cpp +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -28,4 +28,9 @@ int APS5_VABI sceInvitationDialogUpdateStatus() { return 0; } +int APS5_VABI sceInvitationDialogClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp index 2746971c77..d7fdbb3817 100644 --- a/core/libs/prx/libSceNpMatching2/Export.cpp +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -1,6 +1,32 @@ +#include <cstddef> +#include <cstdint> +#include <mutex> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_NP_MATCHING2_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80550C02); +static constexpr int SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED = static_cast<int>(0x80550C03); +static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550C0A); + +namespace { + +struct NpMatching2InitializeParameter { + std::uint64_t poolSize; + std::uint64_t cpuAffinityMask; + std::int32_t threadPriority; + std::uint8_t padding[4]; + std::uint64_t threadStackSize; + std::uint64_t size; + std::uint64_t sslPoolSize; +}; +static_assert(offsetof(NpMatching2InitializeParameter, size) == 0x20); +static_assert(sizeof(NpMatching2InitializeParameter) == 0x30); + +std::mutex matching2Mutex; +bool matching2Initialized = false; + +} + extern "C" { int APS5_VABI sceNpMatching2ContextStart() { @@ -58,8 +84,11 @@ int APS5_VABI sceNpMatching2GetWorldInfoList() { return 0; } -int APS5_VABI sceNpMatching2Initialize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2Initialize(const NpMatching2InitializeParameter* param) { + std::lock_guard lock(matching2Mutex); + if (matching2Initialized) return SCE_NP_MATCHING2_ERROR_ALREADY_INITIALIZED; + if (!param || (param->size != offsetof(NpMatching2InitializeParameter, sslPoolSize) && param->size != sizeof(NpMatching2InitializeParameter))) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + matching2Initialized = true; return 0; } @@ -143,8 +172,10 @@ int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { return 0; } -int APS5_VABI sceNpMatching2Terminate() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2Terminate(void) { + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + matching2Initialized = false; return 0; } diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 8ab2a25170..fb5963de6e 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -43,4 +43,9 @@ int APS5_VABI sceNpSignalingTerminateConnection() { return 0; } +int APS5_VABI sceNpSignalingDeactivateConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestNpPs4Imports.cpp b/core/libs/tests/GuestNpPs4Imports.cpp new file mode 100644 index 0000000000..9f2497a2f1 --- /dev/null +++ b/core/libs/tests/GuestNpPs4Imports.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <stdexcept> +#include <string> + +extern "C" { +int APS5_VABI sceNpMatching2Initialize(const void* param); +int APS5_VABI sceNpMatching2Terminate(void); +int APS5_VABI sceNpSignalingDeactivateConnection(void); +} + +namespace { + +constexpr int Matching2AlreadyInitialized = static_cast<int>(0x80550C02u); +constexpr int Matching2NotInitialized = static_cast<int>(0x80550C03u); +constexpr int Matching2InvalidArgument = static_cast<int>(0x80550C0Au); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpPs4Imports: %s\n", message); + std::abort(); + } +} + +template <typename TCall> +void RequireNotImplemented(TCall call, const std::string& name) { + try { + call(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == name + " not implemented", "a declared function threw another error"); + return; + } + Require(false, "a declared function returned"); +} + +void CheckMatching2() { + unsigned char param[0x30]{}; + const auto setSize = [&](std::uint64_t size) { std::memcpy(param + 0x20, &size, sizeof(size)); }; + Require(sceNpMatching2Terminate() == Matching2NotInitialized, "NpMatching2 terminated before initialization"); + Require(sceNpMatching2Initialize(nullptr) == Matching2InvalidArgument, "a null NpMatching2 parameter was accepted"); + setSize(0x20u); + Require(sceNpMatching2Initialize(param) == Matching2InvalidArgument, "an NpMatching2 parameter of size 0x20 was accepted"); + setSize(0x28u); + Require(sceNpMatching2Initialize(param) == 0, "an NpMatching2 parameter of size 0x28 was refused"); + Require(sceNpMatching2Initialize(param) == Matching2AlreadyInitialized, "NpMatching2 initialized twice"); + Require(sceNpMatching2Terminate() == 0, "NpMatching2 did not terminate"); + setSize(0x30u); + Require(sceNpMatching2Initialize(param) == 0, "an NpMatching2 parameter with an SSL pool was refused"); + Require(sceNpMatching2Terminate() == 0, "NpMatching2 did not terminate after the SSL form"); +} + +void CheckSignaling() { + RequireNotImplemented([] { sceNpSignalingDeactivateConnection(); }, "sceNpSignalingDeactivateConnection"); +} + +} + +int main() { + CheckMatching2(); + CheckSignaling(); + return 0; +} diff --git a/docs/dev/debt/libSceInvitationDialog.md b/docs/dev/debt/libSceInvitationDialog.md new file mode 100644 index 0000000000..e7687fa477 --- /dev/null +++ b/docs/dev/debt/libSceInvitationDialog.md @@ -0,0 +1,5 @@ +# libSceInvitationDialog technical debt + +### Unknown function info + +- [sceInvitationDialogClose](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws diff --git a/docs/dev/debt/libSceNpMatching2.md b/docs/dev/debt/libSceNpMatching2.md new file mode 100644 index 0000000000..15a5abb1e0 --- /dev/null +++ b/docs/dev/debt/libSceNpMatching2.md @@ -0,0 +1,5 @@ +# libSceNpMatching2 technical debt + +### Functional + +- [sceNpMatching2Initialize, sceNpMatching2Terminate](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - only the library state is kept: initialization checks the parameter's `size` field (0x28, or 0x30 with an SSL pool) as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_matching2/np_matching2.cpp) does and starts no thread; a second initialization returns `ALREADY_INITIALIZED` and a termination without one `NOT_INITIALIZED`. The pool, thread and SSL settings are not used: no matching server is reached. Not measured on a console diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md new file mode 100644 index 0000000000..132922c319 --- /dev/null +++ b/docs/dev/debt/libSceNpSignaling.md @@ -0,0 +1,5 @@ +# libSceNpSignaling technical debt + +### Unknown function info + +- [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws From 26469c711a98a4da01b32b92a39150c1dd8c85c3 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros <savvaniss@yahoo.gr> Date: Tue, 20 Oct 2026 05:43:49 +0300 Subject: [PATCH 1089/1360] feat(libSceNpTrophy): answer game, base group and trophy info from the trophy package Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 2 +- core/libs/prx/libSceNpTrophy/Export.cpp | 153 +++++++++++++++++++++++- core/libs/tests/GuestNpPs4Imports.cpp | 131 ++++++++++++++++++++ docs/dev/debt/libSceNpTrophy.md | 8 +- 4 files changed, 286 insertions(+), 8 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 586f216d20..51ea18a582 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -410,7 +410,7 @@ if(BUILD_TESTING) add_test_executable(guest_np_ps4_imports_tests tests/GuestNpPs4Imports.cpp) target_include_directories(guest_np_ps4_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_np_ps4_imports_tests PRIVATE libSceNpMatching2 libSceNpSignaling libc) + target_link_libraries(guest_np_ps4_imports_tests PRIVATE libSceNpMatching2 libSceNpSignaling libSceNpTrophy libc) configure_windows_unwind(guest_np_ps4_imports_tests) add_test(NAME guest_np_ps4_imports COMMAND guest_np_ps4_imports_tests) if(WIN32) diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp index aad30b5af0..c7823fd4c5 100644 --- a/core/libs/prx/libSceNpTrophy/Export.cpp +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -31,10 +31,83 @@ constexpr int NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED = static_cast<int>(0x8055161au constexpr int NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED = static_cast<int>(0x80551626u); constexpr int NP_TROPHY_INVALID_ID = -1; constexpr int NP_TROPHY_NUM_MAX = 128; +constexpr int NP_TROPHY_GRADE_UNKNOWN = 0; +constexpr int NP_TROPHY_BASE_GAME_GROUP_ID = -1; constexpr std::uint32_t TrpMagic = 0xdca24d00u; constexpr std::size_t TrpHeaderBytes = 0x60; constexpr std::size_t TrpEntryBytes = 0x40; +struct GameDetails { + std::uint64_t size; + std::uint32_t numGroups; + std::uint32_t numTrophies; + std::uint32_t numPlatinum; + std::uint32_t numGold; + std::uint32_t numSilver; + std::uint32_t numBronze; + char title[128]; + char description[1024]; +}; +static_assert(sizeof(GameDetails) == 0x4a0); + +struct GameData { + std::uint64_t size; + std::uint32_t unlockedTrophies; + std::uint32_t unlockedPlatinum; + std::uint32_t unlockedGold; + std::uint32_t unlockedSilver; + std::uint32_t unlockedBronze; + std::uint32_t progressPercentage; +}; +static_assert(sizeof(GameData) == 0x20); + +struct GroupDetails { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t numTrophies; + std::uint32_t numPlatinum; + std::uint32_t numGold; + std::uint32_t numSilver; + std::uint32_t numBronze; + char title[128]; + char description[1024]; +}; +static_assert(sizeof(GroupDetails) == 0x4a0); + +struct GroupData { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t unlockedTrophies; + std::uint32_t unlockedPlatinum; + std::uint32_t unlockedGold; + std::uint32_t unlockedSilver; + std::uint32_t unlockedBronze; + std::uint32_t progressPercentage; + std::uint8_t reserved[4]; +}; +static_assert(sizeof(GroupData) == 0x28); + +struct TrophyDetails { + std::uint64_t size; + std::int32_t trophyId; + std::int32_t trophyGrade; + std::int32_t groupId; + bool hidden; + std::uint8_t reserved[3]; + char name[128]; + char description[1024]; +}; +static_assert(sizeof(TrophyDetails) == 0x498); + +struct TrophyData { + std::uint64_t size; + std::int32_t trophyId; + bool unlocked; + std::uint8_t reserved[3]; + std::uint64_t timestamp; +}; +static_assert(sizeof(TrophyData) == 0x18); + struct TrophyFlagArray { std::uint32_t bits[NP_TROPHY_NUM_MAX / 32]; }; @@ -114,6 +187,21 @@ int Lookup(int contextId, int handle, Context*& context) { return 0; } +int Groups(const Context& context) { + int groups = 0; + for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { + char icon[16]; + std::snprintf(icon, sizeof(icon), "GR%03d.PNG", id); + if (!context.entries.contains(icon)) break; + groups = id + 1; + } + return groups; +} + +std::uint32_t Progress(const Context& context) { + return context.trophies == 0 ? 0u : static_cast<std::uint32_t>(context.unlocked.size() * 100 / static_cast<std::size_t>(context.trophies)); +} + int CopyIcon(const Context& context, const std::string& name, void* buffer, std::uint64_t* size) { const auto entry = context.entries.find(name); if (entry == context.entries.end()) return NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; @@ -250,18 +338,71 @@ int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, return CopyIcon(*context, icon, buffer, size); } -int APS5_VABI sceNpTrophyGetGameInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpTrophyGetGameInfo(int contextId, int handle, GameDetails* details, GameData* data) { + if (!details && !data) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + if ((details && details->size != sizeof(GameDetails)) || (data && data->size != sizeof(GameData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (details) { + std::memset(reinterpret_cast<std::uint8_t*>(details) + sizeof(details->size), 0, sizeof(GameDetails) - sizeof(details->size)); + details->numGroups = static_cast<std::uint32_t>(Groups(*context)); + details->numTrophies = static_cast<std::uint32_t>(context->trophies); + } + if (data) { + std::memset(reinterpret_cast<std::uint8_t*>(data) + sizeof(data->size), 0, sizeof(GameData) - sizeof(data->size)); + data->unlockedTrophies = static_cast<std::uint32_t>(context->unlocked.size()); + data->progressPercentage = Progress(*context); + } return 0; } -int APS5_VABI sceNpTrophyGetGroupInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpTrophyGetGroupInfo(int contextId, int handle, int groupId, GroupDetails* details, GroupData* data) { + if (!details && !data) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + if ((details && details->size != sizeof(GroupDetails)) || (data && data->size != sizeof(GroupData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (groupId != NP_TROPHY_BASE_GAME_GROUP_ID) { + if (groupId < 0 || groupId >= Groups(*context)) return NP_TROPHY_ERROR_INVALID_GROUP_ID; + NotImplemented_nid_no_patch(__func__); + } + if (details) { + std::memset(reinterpret_cast<std::uint8_t*>(details) + sizeof(details->size), 0, sizeof(GroupDetails) - sizeof(details->size)); + details->groupId = groupId; + details->numTrophies = static_cast<std::uint32_t>(context->trophies); + } + if (data) { + std::memset(reinterpret_cast<std::uint8_t*>(data) + sizeof(data->size), 0, sizeof(GroupData) - sizeof(data->size)); + data->groupId = groupId; + data->unlockedTrophies = static_cast<std::uint32_t>(context->unlocked.size()); + data->progressPercentage = Progress(*context); + } return 0; } -int APS5_VABI sceNpTrophyGetTrophyInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpTrophyGetTrophyInfo(int contextId, int handle, int trophyId, TrophyDetails* details, TrophyData* data) { + if (!details && !data) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + if ((details && details->size != sizeof(TrophyDetails)) || (data && data->size != sizeof(TrophyData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (Groups(*context) != 0) NotImplemented_nid_no_patch(__func__); + if (details) { + std::memset(reinterpret_cast<std::uint8_t*>(details) + sizeof(details->size), 0, sizeof(TrophyDetails) - sizeof(details->size)); + details->trophyId = trophyId; + details->trophyGrade = NP_TROPHY_GRADE_UNKNOWN; + details->groupId = NP_TROPHY_BASE_GAME_GROUP_ID; + } + if (data) { + std::memset(reinterpret_cast<std::uint8_t*>(data) + sizeof(data->size), 0, sizeof(TrophyData) - sizeof(data->size)); + data->trophyId = trophyId; + data->unlocked = context->unlocked.contains(trophyId); + } return 0; } diff --git a/core/libs/tests/GuestNpPs4Imports.cpp b/core/libs/tests/GuestNpPs4Imports.cpp index 9f2497a2f1..dd30ac0629 100644 --- a/core/libs/tests/GuestNpPs4Imports.cpp +++ b/core/libs/tests/GuestNpPs4Imports.cpp @@ -4,10 +4,21 @@ #include <cstdio> #include <cstdlib> #include <cstring> +#include <filesystem> +#include <fstream> #include <stdexcept> #include <string> +#include <system_error> +#include <vector> extern "C" { +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); +int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); +int APS5_VABI sceNpTrophyUnlockTrophy(int context, int handle, int trophyId, int* platinumId); +int APS5_VABI sceNpTrophyGetGameInfo(int context, int handle, void* details, void* data); +int APS5_VABI sceNpTrophyGetGroupInfo(int context, int handle, int groupId, void* details, void* data); +int APS5_VABI sceNpTrophyGetTrophyInfo(int context, int handle, int trophyId, void* details, void* data); int APS5_VABI sceNpMatching2Initialize(const void* param); int APS5_VABI sceNpMatching2Terminate(void); int APS5_VABI sceNpSignalingDeactivateConnection(void); @@ -18,6 +29,54 @@ namespace { constexpr int Matching2AlreadyInitialized = static_cast<int>(0x80550C02u); constexpr int Matching2NotInitialized = static_cast<int>(0x80550C03u); constexpr int Matching2InvalidArgument = static_cast<int>(0x80550C0Au); +constexpr int TrophyInvalidArgument = static_cast<int>(0x80551604u); +constexpr int TrophyInvalidTrophyId = static_cast<int>(0x8055160au); +constexpr int TrophyInvalidGroupId = static_cast<int>(0x8055160bu); +constexpr int TrophyNotRegistered = static_cast<int>(0x8055160fu); + +struct TrophyGameDetails { + std::uint64_t size; + std::uint32_t numGroups, numTrophies, numPlatinum, numGold, numSilver, numBronze; + char title[128]; + char description[1024]; +}; + +struct TrophyGameData { + std::uint64_t size; + std::uint32_t unlockedTrophies, unlockedPlatinum, unlockedGold, unlockedSilver, unlockedBronze, progressPercentage; +}; + +struct TrophyGroupDetails { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t numTrophies, numPlatinum, numGold, numSilver, numBronze; + char title[128]; + char description[1024]; +}; + +struct TrophyGroupData { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t unlockedTrophies, unlockedPlatinum, unlockedGold, unlockedSilver, unlockedBronze, progressPercentage; + std::uint8_t reserved[4]; +}; + +struct TrophyDetails { + std::uint64_t size; + std::int32_t trophyId, trophyGrade, groupId; + bool hidden; + std::uint8_t reserved[3]; + char name[128]; + char description[1024]; +}; + +struct TrophyData { + std::uint64_t size; + std::int32_t trophyId; + bool unlocked; + std::uint8_t reserved[3]; + std::uint64_t timestamp; +}; void Require(bool condition, const char* message) { if (!condition) { @@ -37,6 +96,77 @@ void RequireNotImplemented(TCall call, const std::string& name) { Require(false, "a declared function returned"); } +void WriteBigEndian(std::vector<std::uint8_t>& bytes, std::size_t at, std::uint64_t value, std::size_t width) { + for (std::size_t index = 0; index < width; ++index) bytes[at + index] = static_cast<std::uint8_t>(value >> (8u * (width - 1u - index))); +} + +void WriteTrophyPackage(const std::filesystem::path& path) { + const char* const names[] = {"TROPCONF.ESFM", "TROP000.PNG", "TROP001.PNG", "TROP002.PNG", "ICON0.PNG"}; + constexpr std::size_t count = sizeof(names) / sizeof(names[0]); + std::vector<std::uint8_t> bytes(0x60 + count * 0x40 + count * 16, 0u); + WriteBigEndian(bytes, 0x00, 0xdca24d00u, 4); + WriteBigEndian(bytes, 0x08, bytes.size(), 8); + WriteBigEndian(bytes, 0x10, count, 4); + WriteBigEndian(bytes, 0x14, 0x40u, 4); + for (std::size_t index = 0; index < count; ++index) { + const auto entry = 0x60 + index * 0x40; + std::memcpy(bytes.data() + entry, names[index], std::strlen(names[index])); + WriteBigEndian(bytes, entry + 32, 0x60 + count * 0x40 + index * 16, 8); + WriteBigEndian(bytes, entry + 40, 16u, 8); + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary).write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size())); +} + +void CheckTrophyInfo() { + constexpr int user = 0x10000000; + constexpr std::uint32_t label = 7u; + const std::filesystem::path package = "app0/sce_sys/trophy/trophy07.trp"; + const std::filesystem::path state = "_trophy/trophy07.268435456"; + std::filesystem::remove(state); + WriteTrophyPackage(package); + int context = -1; + int handle = -1; + Require(sceNpTrophyCreateContext(&context, user, label, 0u) == 0 && sceNpTrophyCreateHandle(&handle) == 0, "trophy context or handle not created"); + TrophyGameDetails game{}; + TrophyGameData gameData{}; + game.size = sizeof(game); + gameData.size = sizeof(gameData); + Require(sceNpTrophyGetGameInfo(context, handle, &game, &gameData) == TrophyNotRegistered, "game info answered before registration"); + Require(sceNpTrophyRegisterContext(context, handle, 0u) == 0, "trophy context not registered"); + Require(sceNpTrophyGetGameInfo(context, handle, nullptr, nullptr) == TrophyInvalidArgument, "game info without outputs accepted"); + game.size = sizeof(game) - 8u; + Require(sceNpTrophyGetGameInfo(context, handle, &game, nullptr) == TrophyInvalidArgument, "game details of a wrong size accepted"); + game.size = sizeof(game); + std::memset(game.title, 'x', sizeof(game.title)); + Require(sceNpTrophyGetGameInfo(context, handle, &game, &gameData) == 0, "game info refused"); + Require(game.numTrophies == 3u && game.numGroups == 0u && game.numPlatinum + game.numGold + game.numSilver + game.numBronze == 0u && game.title[0] == '\0', "game details are not the package's three trophies with unknown grades"); + Require(gameData.unlockedTrophies == 0u && gameData.progressPercentage == 0u, "game data reports progress before an unlock"); + int platinum = 0; + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == 0, "trophy 1 not unlocked"); + Require(sceNpTrophyGetGameInfo(context, handle, nullptr, &gameData) == 0 && gameData.unlockedTrophies == 1u && gameData.progressPercentage == 33u, "game data does not count the unlock"); + TrophyDetails trophy{}; + TrophyData trophyData{}; + trophy.size = sizeof(trophy); + trophyData.size = sizeof(trophyData); + Require(sceNpTrophyGetTrophyInfo(context, handle, 1, &trophy, &trophyData) == 0, "trophy info refused"); + Require(trophy.trophyId == 1 && trophy.trophyGrade == 0 && trophy.groupId == -1 && !trophy.hidden && trophy.name[0] == '\0' && trophyData.trophyId == 1 && trophyData.unlocked, "trophy 1 info is wrong"); + Require(sceNpTrophyGetTrophyInfo(context, handle, 0, nullptr, &trophyData) == 0 && trophyData.trophyId == 0 && !trophyData.unlocked, "trophy 0 reported unlocked"); + Require(sceNpTrophyGetTrophyInfo(context, handle, 3, &trophy, &trophyData) == TrophyInvalidTrophyId, "a trophy past the package accepted"); + TrophyGroupDetails group{}; + TrophyGroupData groupData{}; + group.size = sizeof(group); + groupData.size = sizeof(groupData); + Require(sceNpTrophyGetGroupInfo(context, handle, -1, &group, &groupData) == 0, "base game group refused"); + Require(group.groupId == -1 && group.numTrophies == 3u && groupData.groupId == -1 && groupData.unlockedTrophies == 1u && groupData.progressPercentage == 33u, "base game group info is wrong"); + Require(sceNpTrophyGetGroupInfo(context, handle, 0, &group, &groupData) == TrophyInvalidGroupId, "a group the package does not have accepted"); + std::filesystem::remove(state); + std::filesystem::remove(package); + std::error_code ignored; + for (auto directory = package.parent_path(); !directory.empty(); directory = directory.parent_path()) std::filesystem::remove(directory, ignored); + std::filesystem::remove(state.parent_path(), ignored); +} + void CheckMatching2() { unsigned char param[0x30]{}; const auto setSize = [&](std::uint64_t size) { std::memcpy(param + 0x20, &size, sizeof(size)); }; @@ -60,6 +190,7 @@ void CheckSignaling() { } int main() { + CheckTrophyInfo(); CheckMatching2(); CheckSignaling(); return 0; diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md index c445f4e8d8..82ca475ea5 100644 --- a/docs/dev/debt/libSceNpTrophy.md +++ b/docs/dev/debt/libSceNpTrophy.md @@ -2,4 +2,10 @@ ### Unknown function info -- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`, and the calls that return names and details (`sceNpTrophyGetGameInfo`, `sceNpTrophyGetGroupInfo`, `sceNpTrophyGetTrophyInfo`) throw. Unlocks are kept per user and service label in `_trophy/trophyNN.<user id>` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. +- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`. Unlocks are kept per user and service label in `_trophy/trophyNN.<user id>` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. + +### Functional + +- [sceNpTrophyGetGameInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - the trophy configuration `TROPCONF.ESFM` is encrypted and not read, so the game details report the trophy count taken from the `TROPnnn.PNG` icons and the group count taken from the `GRnnn.PNG` icons of the trophy package; the per-grade counts are 0, the title and description are empty, and the progress percentage is unlocked trophies over all trophies instead of the console's grade-weighted points +- [sceNpTrophyGetGroupInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - only the base game group (id -1) is answered, with the same counts as the game; an id past the `GRnnn.PNG` icons returns `INVALID_GROUP_ID`, and an existing group throws because the trophies of a group are listed only in the encrypted configuration +- [sceNpTrophyGetTrophyInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - the grade is reported as unknown, the trophy is never hidden, the name and description are empty and the unlock timestamp is 0; a package with groups throws, since a trophy's group is listed only in the encrypted configuration From 6710ba6702604ebd8f191ebdc76625e5ccae4e42 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Thu, 22 Oct 2026 22:49:04 +0200 Subject: [PATCH 1090/1360] feat(libSceNpScoreRanking): model the score requests offline The 15 exports threw. They now keep title contexts and requests, run shadPS4's argument checks in its order, and answer the record, ranking and board calls with SCE_NP_ERROR_SIGNED_OUT, which also finishes the request, as shadPS4 does for a user with no connection. --- .../prx/libSceNpScoreRanking/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpScoreRanking/Export.cpp | 247 ++++++++++++++---- core/libs/tests/GuestNpScoreOffline.cpp | 223 ++++++++++++++++ docs/dev/debt/libSceNpScoreRanking.md | 5 + 4 files changed, 444 insertions(+), 47 deletions(-) create mode 100644 core/libs/prx/libSceNpScoreRanking/CMakeLists.txt create mode 100644 core/libs/tests/GuestNpScoreOffline.cpp create mode 100644 docs/dev/debt/libSceNpScoreRanking.md diff --git a/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt b/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt new file mode 100644 index 0000000000..d73264a441 --- /dev/null +++ b/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpScoreRanking SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpScoreRanking PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpScoreRanking) + +if(BUILD_TESTING) + add_test_executable(guest_np_score_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpScoreOffline.cpp) + target_include_directories(guest_np_score_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_score_offline_tests PRIVATE libSceNpScoreRanking) + configure_windows_unwind(guest_np_score_offline_tests) + add_test(NAME guest_np_score_offline COMMAND guest_np_score_offline_tests) + set_tests_properties(guest_np_score_offline PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_score_offline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpScoreRanking>") + endif() +endif() diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp index 1bb8bc6df6..47690794d0 100644 --- a/core/libs/prx/libSceNpScoreRanking/Export.cpp +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -1,81 +1,234 @@ +#include <cstddef> +#include <cstdint> +#include <mutex> +#include <optional> +#include <stdexcept> +#include <unordered_map> +#include <unordered_set> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast<int>(0x80550006); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550704); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = static_cast<int>(0x80550706); +static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast<int>(0x80550707); +static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast<int>(0x8055070C); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast<int>(0x8055070E); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT = static_cast<int>(0x80550714); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID = static_cast<int>(0x80550719); +static constexpr std::uint32_t INVALID_SERVICE_LABEL = 0xFFFFFFFF; +static constexpr std::size_t MAX_OBJECTS = 32; +static constexpr std::uint64_t MAX_RANGE_PER_REQUEST = 100; +static constexpr std::uint64_t MAX_NPID_PER_REQUEST = 101; +static constexpr std::uint64_t GAME_INFO_MAX_SIZE = 189; +static constexpr std::uint64_t NP_ID_SIZE = 36; +static constexpr std::uint64_t RANK_DATA_SIZE = 0x80; +static constexpr std::uint64_t PLAYER_RANK_DATA_SIZE = 0x88; +static constexpr std::uint64_t COMMENT_SIZE = 64; +static constexpr std::uint64_t GAME_INFO_SIZE = 200; +static constexpr std::uint64_t FRIEND_RANKING_OPTION_SIZE = 24; +static constexpr int POLL_FINISHED = 0; +static constexpr int POLL_RUNNING = 1; + +struct NpScoreGameInfo { + std::uint64_t infoSize; + std::uint8_t data[189]; + std::uint8_t pad[3]; +}; +static_assert(sizeof(NpScoreGameInfo) == GAME_INFO_SIZE); + +struct NpScoreFriendRankingOption { + std::uint64_t size; + const std::uint32_t* startSerialRank; + const std::uint32_t* hits; +}; +static_assert(sizeof(NpScoreFriendRankingOption) == FRIEND_RANKING_OPTION_SIZE); + +namespace { +struct Request { + int titleCtxId = 0; + std::optional<int> result; +}; + +std::mutex scoreMutex; +std::unordered_set<int> titleContexts; +std::unordered_map<int, Request> requests; +int nextTitleContext = 1; +int nextRequest = 1; + +int CheckArrays(std::uint64_t arrayNum, const void* commentArray, std::uint64_t commentArraySize, const void* infoArray, std::uint64_t infoArraySize) { + if (commentArray && commentArraySize != arrayNum * COMMENT_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (infoArray && infoArraySize != arrayNum * GAME_INFO_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + return 0; +} + +int Offline(int reqId) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (request->second.result == SCE_NP_COMMUNITY_ERROR_ABORTED) return SCE_NP_COMMUNITY_ERROR_ABORTED; + if (!request->second.result) request->second.result = SCE_NP_ERROR_SIGNED_OUT; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int RecordScore(int reqId, const NpScoreGameInfo* gameInfo, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (gameInfo && gameInfo->infoSize > GAME_INFO_MAX_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return Offline(reqId); +} + +int GetRankingByRange(int reqId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum > MAX_RANGE_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (startSerialRank == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!rankArray || arrayNum == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (rankArraySize != arrayNum * RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} + +int GetRankingByNpId(int reqId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum > MAX_NPID_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (!npIdArray || !rankArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (npIdArraySize != arrayNum * NP_ID_SIZE || rankArraySize != arrayNum * PLAYER_RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} + +int GetFriendsRanking(int reqId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, const NpScoreFriendRankingOption* option) { + if (!rankArray) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (rankArraySize != arrayNum * RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + std::uint32_t startSerialRank = 1; + if (includeSelf == 0) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } else if (option) { + if (option->size != FRIEND_RANKING_OPTION_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (option->startSerialRank) startSerialRank = *option->startSerialRank; + } + if (arrayNum > MAX_RANGE_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (startSerialRank == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} +} + extern "C" { -int APS5_VABI sceNpScoreAbortRequest() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpScoreAbortRequest(int reqId) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.result) request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; return 0; } -int APS5_VABI sceNpScoreCreateNpTitleCtx() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpScoreCreateNpTitleCtx(std::uint32_t serviceLabel, const void* selfNpId) { + if (serviceLabel == INVALID_SERVICE_LABEL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!selfNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + std::lock_guard lock(scoreMutex); + if (titleContexts.size() >= MAX_OBJECTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int titleCtxId = nextTitleContext++; + titleContexts.insert(titleCtxId); + return titleCtxId; +} + +int APS5_VABI sceNpScoreCreateRequest(int titleCtxId) { + std::lock_guard lock(scoreMutex); + if (!titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (requests.size() >= MAX_OBJECTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int reqId = nextRequest++; + requests.emplace(reqId, Request{titleCtxId}); + return reqId; +} + +int APS5_VABI sceNpScoreDeleteNpTitleCtx(int titleCtxId) { + std::lock_guard lock(scoreMutex); + if (!titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); return 0; } -int APS5_VABI sceNpScoreCreateRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreDeleteRequest(int reqId) { + std::lock_guard lock(scoreMutex); + return requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; } -int APS5_VABI sceNpScoreDeleteNpTitleCtx() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetBoardInfo(int reqId, std::uint32_t boardId, void* boardInfo, void* option) { + (void)boardId; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!boardInfo) throw std::runtime_error("sceNpScoreGetBoardInfo: the answer for a null board info depends on the title's SDK version"); + return Offline(reqId); } -int APS5_VABI sceNpScoreDeleteRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetFriendsRanking(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetBoardInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetFriendsRankingAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetFriendsRanking() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByNpId(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByNpId(reqId, npIdArray, npIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetFriendsRankingAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByNpIdAsync(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByNpId(reqId, npIdArray, npIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetRankingByNpId() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByRange(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetRankingByNpIdAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByRangeAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetRankingByRange() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScorePollAsync(int reqId, int* result) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.result) return POLL_RUNNING; + if (result) *result = *request->second.result; + return POLL_FINISHED; } -int APS5_VABI sceNpScoreGetRankingByRangeAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreRecordScore(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const NpScoreGameInfo* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option) { + (void)boardId; + (void)score; + (void)scoreComment; + (void)tmpRank; + (void)compareDate; + return RecordScore(reqId, gameInfo, option); } -int APS5_VABI sceNpScorePollAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreRecordScore() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreRecordScoreAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreRecordScoreAsync(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const NpScoreGameInfo* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option) { + (void)boardId; + (void)score; + (void)scoreComment; + (void)tmpRank; + (void)compareDate; + return RecordScore(reqId, gameInfo, option); } } diff --git a/core/libs/tests/GuestNpScoreOffline.cpp b/core/libs/tests/GuestNpScoreOffline.cpp new file mode 100644 index 0000000000..cb6adb87eb --- /dev/null +++ b/core/libs/tests/GuestNpScoreOffline.cpp @@ -0,0 +1,223 @@ +#include "SceTypes.hpp" +#include <cstdint> +#include <cstdio> +#include <cstring> + +extern "C" { +int APS5_VABI sceNpScoreCreateNpTitleCtx(std::uint32_t serviceLabel, const void* selfNpId); +int APS5_VABI sceNpScoreDeleteNpTitleCtx(int titleCtxId); +int APS5_VABI sceNpScoreCreateRequest(int titleCtxId); +int APS5_VABI sceNpScoreDeleteRequest(int reqId); +int APS5_VABI sceNpScoreAbortRequest(int reqId); +int APS5_VABI sceNpScorePollAsync(int reqId, int* result); +int APS5_VABI sceNpScoreRecordScore(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const void* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option); +int APS5_VABI sceNpScoreRecordScoreAsync(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const void* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option); +int APS5_VABI sceNpScoreGetRankingByRange(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByRangeAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByNpId(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByNpIdAsync(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetFriendsRanking(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const void* option); +int APS5_VABI sceNpScoreGetFriendsRankingAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const void* option); +int APS5_VABI sceNpScoreGetBoardInfo(int reqId, std::uint32_t boardId, void* boardInfo, void* option); +} + +namespace { + +constexpr int SignedOut = static_cast<int>(0x80550006u); +constexpr int InvalidArgument = static_cast<int>(0x80550704u); +constexpr int TooManyObjects = static_cast<int>(0x80550706u); +constexpr int Aborted = static_cast<int>(0x80550707u); +constexpr int InsufficientArgument = static_cast<int>(0x8055070Cu); +constexpr int InvalidId = static_cast<int>(0x8055070Eu); +constexpr int InvalidAlignment = static_cast<int>(0x80550714u); +constexpr int TooManyNpId = static_cast<int>(0x80550719u); +constexpr int PollFinished = 0; +constexpr int PollRunning = 1; + +struct GameInfo { + std::uint64_t infoSize; + std::uint8_t data[189]; + std::uint8_t pad[3]; +}; + +struct FriendOption { + std::uint64_t size; + const std::uint32_t* startSerialRank; + const std::uint32_t* hits; +}; + +int failures = 0; +int context = 0; +unsigned char selfNpId[36]; +unsigned char npIds[2 * 36]; +unsigned char ranks[2 * 0x88]; +unsigned char comments[2 * 64]; +unsigned char infos[2 * 200]; +unsigned char board[24]; +unsigned char manyNpIds[101 * 36]; +unsigned char manyRanks[101 * 0x88]; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +int NewRequest() { + return sceNpScoreCreateRequest(context); +} + +bool Untouched() { + for (const unsigned char value : ranks) if (value != 0xA5) return false; + for (const unsigned char value : comments) if (value != 0xA5) return false; + for (const unsigned char value : infos) if (value != 0xA5) return false; + for (const unsigned char value : board) if (value != 0xA5) return false; + return true; +} + +void CheckOffline(const char* name, int status, int req) { + Require(status == SignedOut, name, "valid arguments did not return SIGNED_OUT"); + int result = 0; + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == SignedOut, name, "the request did not finish with SIGNED_OUT"); +} + +void CheckContexts() { + Require(sceNpScoreCreateNpTitleCtx(0xFFFFFFFFu, nullptr) == InvalidArgument, "sceNpScoreCreateNpTitleCtx", "the invalid service label was not refused before the null NpId"); + Require(sceNpScoreCreateNpTitleCtx(0, nullptr) == InsufficientArgument, "sceNpScoreCreateNpTitleCtx", "a null NpId was taken"); + context = sceNpScoreCreateNpTitleCtx(0, selfNpId); + Require(context == 1, "sceNpScoreCreateNpTitleCtx", "the first context is not id 1"); + Require(sceNpScoreCreateRequest(context + 100) == InvalidId, "sceNpScoreCreateRequest", "an unknown context was taken"); + int extra[31]; + for (int& entry : extra) entry = sceNpScoreCreateNpTitleCtx(1, selfNpId); + Require(extra[30] == 32, "sceNpScoreCreateNpTitleCtx", "32 contexts could not be created"); + Require(sceNpScoreCreateNpTitleCtx(1, selfNpId) == TooManyObjects, "sceNpScoreCreateNpTitleCtx", "a 33rd context did not return TOO_MANY_OBJECTS"); + const int kept = sceNpScoreCreateRequest(extra[0]); + Require(kept == 1, "sceNpScoreCreateRequest", "the first request is not id 1"); + for (const int entry : extra) Require(sceNpScoreDeleteNpTitleCtx(entry) == 0, "sceNpScoreDeleteNpTitleCtx", "deleting a context failed"); + Require(sceNpScoreDeleteRequest(kept) == InvalidId, "sceNpScoreDeleteNpTitleCtx", "a request of a deleted context was kept"); + Require(sceNpScoreDeleteNpTitleCtx(extra[0]) == InvalidId, "sceNpScoreDeleteNpTitleCtx", "a deleted context was found"); + int requests[32]; + for (int& entry : requests) entry = NewRequest(); + Require(requests[31] > 0, "sceNpScoreCreateRequest", "32 requests could not be created"); + Require(NewRequest() == TooManyObjects, "sceNpScoreCreateRequest", "a 33rd request did not return TOO_MANY_OBJECTS"); + for (const int entry : requests) sceNpScoreDeleteRequest(entry); + Require(sceNpScoreDeleteRequest(requests[0]) == InvalidId, "sceNpScoreDeleteRequest", "a deleted request was found"); +} + +void CheckRequests() { + int req = NewRequest(); + int result = 7; + Require(sceNpScorePollAsync(req + 100, &result) == InvalidId, "sceNpScorePollAsync", "an unknown request was taken"); + Require(sceNpScorePollAsync(req, &result) == PollRunning && result == 7, "sceNpScorePollAsync", "an unstarted request was not running"); + Require(sceNpScoreAbortRequest(req + 100) == InvalidId, "sceNpScoreAbortRequest", "an unknown request was taken"); + Require(sceNpScoreAbortRequest(req) == 0, "sceNpScoreAbortRequest", "aborting failed"); + Require(sceNpScorePollAsync(req, nullptr) == PollFinished, "sceNpScorePollAsync", "a null result was not accepted"); + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == Aborted, "sceNpScoreAbortRequest", "an aborted request did not finish with ABORTED"); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == Aborted, "sceNpScoreRecordScore", "an aborted request did not return ABORTED"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreRecordScore", "the first call did not return SIGNED_OUT"); + Require(sceNpScoreAbortRequest(req) == 0, "sceNpScoreAbortRequest", "aborting a finished request failed"); + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == SignedOut, "sceNpScoreAbortRequest", "aborting a finished request changed its result"); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreRecordScore", "a finished request was not reused"); + sceNpScoreDeleteRequest(req); +} + +void CheckRecord() { + GameInfo info{}; + std::uint32_t tmpRank = 77; + const int req = NewRequest(); + info.infoSize = 190; + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, &info, &tmpRank, nullptr, &info) == InvalidArgument, "sceNpScoreRecordScore", "an option was taken"); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr) == InvalidArgument, "sceNpScoreRecordScore", "190 bytes of game info were taken"); + Require(sceNpScoreRecordScoreAsync(req, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr) == InvalidArgument, "sceNpScoreRecordScoreAsync", "190 bytes of game info were taken"); + info.infoSize = 189; + Require(sceNpScoreRecordScore(req + 100, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr) == InvalidId, "sceNpScoreRecordScore", "an unknown request was taken"); + Require(sceNpScorePollAsync(req, nullptr) == PollRunning, "sceNpScoreRecordScore", "a refused call finished the request"); + CheckOffline("sceNpScoreRecordScoreAsync", sceNpScoreRecordScoreAsync(req, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr), req); + Require(tmpRank == 77, "sceNpScoreRecordScoreAsync", "a rank was written"); + sceNpScoreDeleteRequest(req); +} + +void CheckRange() { + const int req = NewRequest(); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, board) == InvalidArgument, "sceNpScoreGetRankingByRange", "an option was taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 101 * 0x80, nullptr, 0, nullptr, 0, 101, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRange", "101 ranks were taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 0, nullptr, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRange", "rank 0 was not refused before the null array"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, nullptr, 3, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByRange", "a null rank array was not refused before its size"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByRange", "zero ranks were taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRange", "a rank array size other than 0x80 per rank was taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x100, comments, 64, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRange", "a comment array size other than 64 per rank was taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x100, comments, 128, infos, 200, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRange", "an info array size other than 200 per rank was taken"); + Require(sceNpScoreGetRankingByRange(req + 100, 1, 1, ranks, 0x100, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr) == InvalidId, "sceNpScoreGetRankingByRange", "an unknown request was taken"); + CheckOffline("sceNpScoreGetRankingByRange", sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x100, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(sceNpScoreGetRankingByRangeAsync(req, 1, 1, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRangeAsync", "valid arguments did not return SIGNED_OUT"); + Require(sceNpScoreGetRankingByRangeAsync(req, 1, 0, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRangeAsync", "rank 0 was taken"); + Require(sceNpScoreGetRankingByRangeAsync(req, 1, 1, manyRanks, 100 * 0x80, nullptr, 0, nullptr, 0, 100, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRangeAsync", "100 ranks did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckNpId() { + const int req = NewRequest(); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, board) == InvalidArgument, "sceNpScoreGetRankingByNpId", "an option was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0, 102, nullptr, nullptr, nullptr) == TooManyNpId, "sceNpScoreGetRankingByNpId", "102 NpIds were not refused before the null arrays"); + Require(sceNpScoreGetRankingByNpId(req, 1, nullptr, 36, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByNpId", "a null NpId array was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, nullptr, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByNpId", "a null rank array was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 24, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpId", "an NpId array size other than 36 per NpId was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpId", "a rank array size other than 0x88 per NpId was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, ranks, 0x88, comments, 128, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpId", "a comment array size other than 64 per NpId was taken"); + CheckOffline("sceNpScoreGetRankingByNpId", sceNpScoreGetRankingByNpId(req, 1, npIds, 72, ranks, 0x110, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(sceNpScoreGetRankingByNpIdAsync(req, 1, npIds, 0, ranks, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByNpIdAsync", "zero NpIds did not return SIGNED_OUT"); + Require(sceNpScoreGetRankingByNpIdAsync(req, 1, manyNpIds, sizeof(manyNpIds), manyRanks, sizeof(manyRanks), nullptr, 0, nullptr, 0, 101, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByNpIdAsync", "101 NpIds did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckFriends() { + const int req = NewRequest(); + const std::uint32_t zero = 0; + const std::uint32_t five = 5; + const FriendOption shortOption{16, nullptr, nullptr}; + const FriendOption rankZero{24, &zero, nullptr}; + const FriendOption rankFive{24, &five, nullptr}; + Require(sceNpScoreGetFriendsRanking(req, 1, 0, nullptr, 0x80, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, &shortOption) == InvalidArgument, "sceNpScoreGetFriendsRanking", "a null rank array was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 0, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &shortOption) == InvalidAlignment, "sceNpScoreGetFriendsRanking", "the rank array size was not checked before the option"); + Require(sceNpScoreGetFriendsRanking(req, 1, 0, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &rankFive) == InvalidArgument, "sceNpScoreGetFriendsRanking", "an option without includeSelf was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &shortOption) == InvalidArgument, "sceNpScoreGetFriendsRanking", "an option size other than 24 was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &rankZero) == InvalidArgument, "sceNpScoreGetFriendsRanking", "start rank 0 was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 101 * 0x80, nullptr, 0, nullptr, 0, 101, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetFriendsRanking", "101 friends were taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetFriendsRanking", "zero friends were taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, nullptr, 0, infos, 400, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetFriendsRanking", "an info array size other than 200 per friend was taken"); + CheckOffline("sceNpScoreGetFriendsRanking", sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, comments, 64, infos, 200, 1, nullptr, nullptr, &rankFive), req); + Require(sceNpScoreGetFriendsRankingAsync(req, 1, 0, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetFriendsRankingAsync", "valid arguments did not return SIGNED_OUT"); + Require(sceNpScoreGetFriendsRankingAsync(req, 1, 0, manyRanks, 100 * 0x80, nullptr, 0, nullptr, 0, 100, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetFriendsRankingAsync", "100 friends did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckBoard() { + const int req = NewRequest(); + Require(sceNpScoreGetBoardInfo(req, 1, board, board) == InvalidArgument, "sceNpScoreGetBoardInfo", "an option was taken"); + Require(sceNpScoreGetBoardInfo(req + 100, 1, board, nullptr) == InvalidId, "sceNpScoreGetBoardInfo", "an unknown request was taken"); + CheckOffline("sceNpScoreGetBoardInfo", sceNpScoreGetBoardInfo(req, 1, board, nullptr), req); + sceNpScoreDeleteRequest(req); +} + +} + +int main() { + std::memset(ranks, 0xA5, sizeof(ranks)); + std::memset(comments, 0xA5, sizeof(comments)); + std::memset(infos, 0xA5, sizeof(infos)); + std::memset(board, 0xA5, sizeof(board)); + CheckContexts(); + CheckRequests(); + CheckRecord(); + CheckRange(); + CheckNpId(); + CheckFriends(); + CheckBoard(); + Require(Untouched(), "libSceNpScoreRanking", "an output array was written"); + if (failures != 0) return 1; + std::puts("NpScoreRanking offline tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpScoreRanking.md b/docs/dev/debt/libSceNpScoreRanking.md new file mode 100644 index 0000000000..d373e91695 --- /dev/null +++ b/docs/dev/debt/libSceNpScoreRanking.md @@ -0,0 +1,5 @@ +# libSceNpScoreRanking technical debt + +### Functional + +- [libSceNpScoreRanking](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - offline only, not measured on a PS5: the argument checks, their order, the limit of 32 title contexts and 32 requests and the array sizes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_score/np_score.cpp#L64-L1210), and a valid call then returns `SCE_NP_ERROR_SIGNED_OUT` and finishes its request with it, as shadPS4 does when the user [has no connection](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_handler/np_handler_score.cpp#L18-L26). No output is written, where shadPS4 first clears the rank, comment and info arrays. A null `boardInfo` throws: shadPS4 answers it by the title's SDK version From 9f58485aad93fa840e2f2ef2d9d0b7038c4903f5 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Thu, 22 Oct 2026 22:59:08 +0200 Subject: [PATCH 1091/1360] fix(libSceNpSignaling): check online IDs as ASCII whatever the locale --- core/libs/prx/libSceNpSignaling/Export.cpp | 11 +++++++---- core/libs/tests/GuestNpSignalingOffline.cpp | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 97c6a7875b..77fb201139 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -1,4 +1,3 @@ -#include <cctype> #include <cstdint> #include <cstring> #include <mutex> @@ -38,14 +37,18 @@ bool initialized = false; std::unordered_map<int, Context> contexts; int lastContextId = 0; +bool IsAsciiAlphanumeric(char value) { + return (value >= '0' && value <= '9') || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'); +} + bool ReadNpId(const void* npId, SignalingNpId& copy) { std::memcpy(©, npId, sizeof(copy)); if (copy.term != 0) return false; const std::size_t length = strnlen(copy.handle, sizeof(copy.handle)); - if (length < 3 || !std::isalnum(static_cast<unsigned char>(copy.handle[0]))) return false; + if (length < 3 || !IsAsciiAlphanumeric(copy.handle[0])) return false; for (std::size_t index = 1; index < length; ++index) { - const auto value = static_cast<unsigned char>(copy.handle[index]); - if (!std::isalnum(value) && value != '_' && value != '-') return false; + const char value = copy.handle[index]; + if (!IsAsciiAlphanumeric(value) && value != '_' && value != '-') return false; } return true; } diff --git a/core/libs/tests/GuestNpSignalingOffline.cpp b/core/libs/tests/GuestNpSignalingOffline.cpp index 8e0098017a..dd246473a7 100644 --- a/core/libs/tests/GuestNpSignalingOffline.cpp +++ b/core/libs/tests/GuestNpSignalingOffline.cpp @@ -57,6 +57,8 @@ int main() { const SignalingNpId tooShort = MakeNpId("ab"); const SignalingNpId leadingUnderscore = MakeNpId("_player"); const SignalingNpId withDot = MakeNpId("pl.ayer"); + SignalingNpId latin = MakeNpId("pl\xE9yer"); + SignalingNpId upper = MakeNpId("Player9"); SignalingNpId unterminated = MakeNpId("player_1"); unterminated.term = 'x'; unsigned char info[64]; @@ -81,9 +83,11 @@ int main() { Require(sceNpSignalingCreateContext(&leadingUnderscore, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an online ID starting with '_' was taken"); Require(sceNpSignalingCreateContext(&withDot, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an online ID with '.' was taken"); Require(sceNpSignalingCreateContext(&unterminated, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an unterminated online ID was taken"); + Require(sceNpSignalingCreateContext(&latin, nullptr, nullptr, &context) == InvalidArgument, "sceNpSignalingCreateContext", "an online ID with a byte outside ASCII was taken"); Require(context == -7, "sceNpSignalingCreateContext", "a refused call wrote the context id"); Require(sceNpSignalingCreateContext(&owner, nullptr, nullptr, &context) == 0 && context == 1, "sceNpSignalingCreateContext", "the first context is not id 1"); + Require(sceNpSignalingActivateConnection(context, &upper, &connection) == SignedOut, "sceNpSignalingActivateConnection", "an online ID with upper-case letters and digits was refused"); int created = 0; Require(sceNpSignalingCreateContext(&maximum, nullptr, nullptr, &created) == 0 && created == 2, "sceNpSignalingCreateContext", "the second context is not id 2"); Require(sceNpSignalingDeleteContext(2) == 0, "sceNpSignalingDeleteContext", "deleting context 2 failed"); From 99356384a238223c88dfa7e2cf94c7d8705007dd Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Thu, 22 Oct 2026 22:59:24 +0200 Subject: [PATCH 1092/1360] docs(debt): say that libSceNpSignaling checks online IDs as ASCII --- docs/dev/debt/libSceNpSignaling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md index e792963043..40dc465615 100644 --- a/docs/dev/debt/libSceNpSignaling.md +++ b/docs/dev/debt/libSceNpSignaling.md @@ -2,4 +2,4 @@ ### Functional -- [libSceNpSignaling](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - offline only, not measured on a PS5: the checks, their order, context ids 1 to 8 and the NpId rule (`term` 0, 3 to 16 characters, first alphanumeric, the rest alphanumeric, `_` or `-`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling.cpp#L118-L170) and its [state helpers](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling_state.cpp#L120-L170). `sceNpSignalingInitialize` reserves no heap and starts no thread, so its arguments are not checked, and callbacks are never called. `sceNpSignalingActivateConnection` returns `SCE_NP_ERROR_SIGNED_OUT` after shadPS4's checks up to `OWN_NP_ID`, without its rate limit, and creates no connection, so the connection calls answer `CONN_NOT_FOUND` +- [libSceNpSignaling](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - offline only, not measured on a PS5: the checks, their order, context ids 1 to 8 and the NpId rule (`term` 0, 3 to 16 characters, the first an ASCII letter or digit, the rest ASCII letters, digits, `_` or `-`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling.cpp#L118-L170) and its [state helpers](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling_state.cpp#L120-L170). `sceNpSignalingInitialize` reserves no heap and starts no thread, so its arguments are not checked, and callbacks are never called. `sceNpSignalingActivateConnection` returns `SCE_NP_ERROR_SIGNED_OUT` after shadPS4's checks up to `OWN_NP_ID`, without its rate limit, and creates no connection, so the connection calls answer `CONN_NOT_FOUND` From e16e59a109c9ee76cb8adaa5d0d84bd17fad4dd4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <MarcZX10@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:56:06 +0000 Subject: [PATCH 1093/1360] test(libkernel): allow Windows raise exception timing variance --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3289c98e99..1b9c5d5348 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1033,7 +1033,7 @@ if(BUILD_TESTING) target_link_libraries(guest_raise_exception_tests PRIVATE libkernel libc) configure_windows_unwind(guest_raise_exception_tests) add_test(NAME guest_raise_exception COMMAND guest_raise_exception_tests) - set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 30) + set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 60) add_asan_test_options(guest_raise_exception detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_raise_exception PROPERTIES ENVIRONMENT_MODIFICATION From 93ab8d3514fba3e02280b27fd8aeb3164dcce7ba Mon Sep 17 00:00:00 2001 From: Dom3005 <dominik.goertz.05@gmail.com> Date: Thu, 22 Oct 2026 19:10:48 +0200 Subject: [PATCH 1094/1360] fix(libkernel): answer fstatfs on a character device as devfs does FreeBSD keeps every character device on devfs, whose devfs_statfs reports 512-byte blocks, 2 blocks and nothing free. Windows threw for any console, NUL or the guest random device, and Linux described the host's /dev mount. A kqueue now returns EINVAL, as getvnode does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/File/src/Stdio.cpp | 12 ++++++++++- core/libs/tests/GuestFstatfs.cpp | 24 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fa57cf492d..af3a52401b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -498,6 +498,14 @@ static std::int64_t PosixResult(std::int64_t result) { return result < 0 ? PosixFailure(static_cast<int>(result) & 0xffff) : result; } +static int DeviceFilesystem(KernelStatfs* fs) { + fs->f_bsize = 512; + fs->f_iosize = 512; + fs->f_blocks = 2; + fs->f_namemax = 255; + return 0; +} + #ifdef _WIN32 static int NativeFstatfs(int descriptor, KernelStatfs* fs) { if (!File::IsDirectoryDescriptor(descriptor)) { @@ -505,6 +513,7 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { if (handle == INVALID_HANDLE_VALUE) return GUEST_EBADF; const DWORD type = ::GetFileType(handle); if (type == FILE_TYPE_PIPE) return GUEST_EINVAL; + if (type == FILE_TYPE_CHAR) return DeviceFilesystem(fs); if (type != FILE_TYPE_DISK) throw std::runtime_error("_fstatfs: descriptor " + std::to_string(descriptor) + " is not a file on a Windows volume"); } @@ -552,6 +561,7 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; if (NativeIsPipe(descriptor)) return GUEST_EINVAL; + if (S_ISCHR(file.st_mode)) return DeviceFilesystem(fs); struct statvfs host{}; if (::fstatvfs(descriptor, &host) != 0) return SceErrorFromErrno(errno) & 0xffff; if (host.f_flag & ST_RDONLY) fs->f_flags |= KERNEL_MNT_RDONLY; @@ -883,7 +893,7 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { } int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { - if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d)) return PosixFailure(GUEST_EINVAL); if (d < 0) return PosixFailure(GUEST_EBADF); File::RefuseArchivedFile(d, "fstatfs"); KernelStatfs result{}; diff --git a/core/libs/tests/GuestFstatfs.cpp b/core/libs/tests/GuestFstatfs.cpp index 0c86546ccb..282ddd978d 100644 --- a/core/libs/tests/GuestFstatfs.cpp +++ b/core/libs/tests/GuestFstatfs.cpp @@ -42,6 +42,7 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int APS5_VABI kqueue_nid_postfix(); int* APS5_VABI __error_nid_postfix(); } @@ -83,6 +84,19 @@ static void RequireVolume(int descriptor, const std::filesystem::path& path) { Require(fs.f_fstypename[0] == '\0' && fs.f_mntfromname[0] == '\0' && fs.f_mntonname[0] == '\0'); } +static void RequireDeviceFilesystem(int descriptor) { + GuestStatfs fs; + std::memset(&fs, 0x5a, sizeof(fs)); + Require(_fstatfs_nid_postfix(descriptor, &fs) == 0); + GuestStatfs expected{}; + expected.f_version = StatfsVersion; + expected.f_bsize = 512; + expected.f_iosize = 512; + expected.f_blocks = 2; + expected.f_namemax = 255; + Require(std::memcmp(&fs, &expected, sizeof(fs)) == 0); +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-fstatfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -122,6 +136,16 @@ int main() { Require(close_nid_postfix(sockets[1]) == 0); RequireFailure(sockets[0], GuestEbadf); + const int random = sceKernelOpen("/dev/urandom", SCE_KERNEL_O_RDONLY, 0); + Require(random >= 0); + RequireDeviceFilesystem(random); + Require(close_nid_postfix(random) == 0); + + const int kqueue = kqueue_nid_postfix(); + Require(kqueue >= 0); + RequireFailure(kqueue, GuestEinval); + Require(close_nid_postfix(kqueue) == 0); + RequireFailure(-1, GuestEbadf); Require(close_nid_postfix(file) == 0); #ifndef _WIN32 diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b1ebcecbb9..24860e64d5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -228,7 +228,7 @@ - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same -- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields +- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks and transfers, 2 blocks, nothing free and a maximum name length of 255 from `kern_do_statfs`. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 From d426d2f9ef0a9be0dc05e8e67d87c73b146ab913 Mon Sep 17 00:00:00 2001 From: Dom3005 <dominik.goertz.05@gmail.com> Date: Thu, 22 Oct 2026 20:07:21 +0200 Subject: [PATCH 1095/1360] docs(debt): keep the fstatfs entry under the debt length limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 24860e64d5..fbd8ce08ec 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -228,7 +228,7 @@ - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same -- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks and transfers, 2 blocks, nothing free and a maximum name length of 255 from `kern_do_statfs`. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields +- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports the host volume of a file, not a console mount: block sizes, block counts, name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0). The type, mount names, owner, filesystem id and I/O counters are 0 or empty. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks, 2 blocks, nothing free and a 255-byte name limit. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 From fcec4981a595386a8cbadaa6651eee1bd2fd4301 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:15:40 +0100 Subject: [PATCH 1096/1360] docs: distinguish known exports from unverified ABI --- docs/dev/debt/libSceNKWebKit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNKWebKit.md b/docs/dev/debt/libSceNKWebKit.md index 9e723c646b..e515ec10a7 100644 --- a/docs/dev/debt/libSceNKWebKit.md +++ b/docs/dev/debt/libSceNKWebKit.md @@ -1,5 +1,5 @@ # libSceNKWebKit technical debt -### Unknown function info +### Known export names; ABI prototypes unverified - The 150 `sceNKWebKitUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceNKWebKit/Export.cpp) are present in the PS5 3.20 `libSceNKWebKit` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from exported mangled symbols. Prototypes and behavior are unverified; each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. These declarations make no ABI or behavior claim. From b553950efd79fa645fc311ddc679d801826b6f4e Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:17:40 +0400 Subject: [PATCH 1097/1360] fix(ngs2): correct UserFx ids and preserve the Delay compatibility path --- .../libSceNgs2.native/include/Ngs2Types.hpp | 15 +- .../libs/prx/libSceNgs2.native/src/Custom.cpp | 26 ++- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 161 +++++++++++++++--- docs/dev/debt/libSceNgs2.native.md | 6 +- 4 files changed, 177 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 8358917c7b..c1102877b5 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -77,7 +77,8 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x400 static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN = 0x40001500; -static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001d00; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001900; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_DELAY = 0x40001d00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; @@ -91,7 +92,8 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE = 8; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_GAIN = 0x15; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_REVERB = 0x1b; -static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x1d; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x19; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_DELAY = 0x1d; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT = 0x20; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION = 0x23; @@ -206,6 +208,15 @@ struct Ngs2CustomModuleOption { std::uint32_t size; }; +struct Ngs2CustomDelayModuleOption { + Ngs2CustomModuleOption custom_module_option; + std::uint32_t type; + std::uint32_t max_taps; + float max_length; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2CustomDelayModuleOption) == 20); + struct Ngs2UserFx2SetupContext { void* common; void* param; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index aaad9fdb18..865a844cd7 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -30,7 +30,7 @@ static std::uint32_t OptionWord(const Ngs2CustomModuleOption& option) { static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { const auto stateSizeIs = [&info](std::uint32_t size) { return info.state_size == 0 || info.state_size == size; }; - if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN || info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_REVERB) { if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; @@ -45,6 +45,21 @@ static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { return SCE_NGS2_OK; } +static void CheckDelayModule(std::uint32_t rackId, const Ngs2CustomRackModuleInfo& info) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || info.state_size != 0 || info.state_offset != 0) { + throw std::runtime_error("NGS2: this custom Delay rack or module state is not implemented"); + } + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomDelayModuleOption)) { + throw std::runtime_error("NGS2: this custom Delay module option size is not implemented"); + } + Ngs2CustomDelayModuleOption delay{}; + std::memcpy(&delay, info.option, sizeof(delay)); + if (delay.type > 1 || delay.max_taps == 0 || delay.max_taps > 8 || + !(delay.max_length >= 1.0f && delay.max_length <= 20000.0f) || delay.reserved != 0) { + throw std::runtime_error("NGS2: these custom Delay module option values are not implemented"); + } +} + int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs) { if (option.num_buffers == 0 || option.num_buffers > SCE_NGS2_CUSTOM_MAX_BUFFERS || option.num_buffers < maxInputs) { return SCE_NGS2_ERROR_INVALID_NUM_BUFFERS; @@ -58,7 +73,10 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { - if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX; + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: the custom UserFx module on a custom sampler rack is not implemented"); + if (info.option != nullptr) throw std::runtime_error("NGS2: a custom UserFx module option is not implemented"); + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) { + CheckDelayModule(rackId, info); } else if (BuiltInModuleName(info.module_id) == nullptr) { throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); } @@ -186,6 +204,9 @@ static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std:: void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { const auto rackId = voice.rack->rackId; + if ((param.id & ~0xffu) == SCE_NGS2_CUSTOM_VOICE_PARAM_DELAY) { + throw std::runtime_error("NGS2: the custom Delay module parameters are not implemented"); + } if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); SetupCustomSubmixer(voice, ParamAs<Ngs2CustomSubmixerVoiceSetupParam>(param)); @@ -242,6 +263,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp std::vector<float*> channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast<std::size_t>(c) * grain; for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) { + if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) continue; if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { ProcessGain(voice, voice.customGain[m], channels, grain); } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 5b6acd7ad2..bb72a549bb 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -2,7 +2,9 @@ #include <cstdint> #include <cstring> +#include <limits> #include <stdexcept> +#include <string> #include <vector> struct Calls { @@ -434,6 +436,7 @@ static void TestCustomRackChecks() { option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS; Require(CreateThrows<std::runtime_error>(option)); + Require(create(BuiltInRackOption(userFx2, 0x19, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); Require(!CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); const ModuleOptionWords gainOption{sizeof(Ngs2CustomModuleOption), {}}; @@ -468,7 +471,7 @@ static void TestCustomRackChecks() { option.custom_rack_option.module[1] = good.custom_rack_option.module[0]; option.custom_rack_option.module[1].dest_buffer_id = 1; Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); - Require(CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, 0x19, nullptr, 0))); + Require(CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, 0x1d, nullptr, 0))); option = good; option.custom_rack_option.num_buffers = 0; @@ -500,6 +503,29 @@ static void TestCustomRackChecks() { Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; Require(CreateSamplerThrows<std::runtime_error>(sampler)); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + for (const auto count : {1u, 2u, 24u}) { + sampler.custom_rack_option.num_modules = count; + for (std::uint32_t index = 0; index < count; ++index) { + sampler.custom_rack_option.module[index].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + } + sampler.custom_rack_option.module[count - 1].module_id = 0x19; + Require(CreateSamplerThrows<std::runtime_error>(sampler)); + for (const bool useAllocator : {false, true}) { + rack = 0; + rejected = false; + try { + if (useAllocator) { + sceNgs2RackCreateWithAllocator(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &allocator, &rack); + } else { + sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack); + } + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && rack == 0 && allocCalls == 0); + } + } Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -512,6 +538,8 @@ struct LegacyUserFxState { }; static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) { + Require(context->user_data1 == 123 && context->user_data2 == 456); + Require(context->num_channels == 2 && context->num_grain_samples == Grain && context->sample_rate == 48000); auto& state = *reinterpret_cast<LegacyUserFxState*>(context->user_data0); state.calls++; state.flags = context->flags; @@ -523,7 +551,7 @@ static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) { return SCE_NGS2_OK; } -static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option) { +static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option, std::uint32_t moduleIndex) { Ngs2CustomSubmixerRackOption rack{}; auto& common = rack.custom_rack_option.rack_option; common.size = sizeof(rack); @@ -532,27 +560,28 @@ static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOptio common.max_matrices = 1; common.max_ports = 1; rack.custom_rack_option.num_buffers = 1; - rack.custom_rack_option.num_modules = 1; - rack.custom_rack_option.module[0].option = option; - rack.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; - rack.custom_rack_option.module[0].state_size = 16; + rack.custom_rack_option.num_modules = moduleIndex + 1; + for (std::uint32_t index = 0; index <= moduleIndex; ++index) { + rack.custom_rack_option.module[index].option = option; + rack.custom_rack_option.module[index].module_id = 0x19; + } rack.max_channels = 2; rack.max_inputs = 1; return rack; } -static void TestCustomLegacyUserFx() { +static void TestCustomLegacyUserFx(std::uint32_t moduleIndex) { const auto system = CreateSystem(); const auto master = Mastering(system, 2); const Ngs2CustomModuleOption header{sizeof(Ngs2CustomModuleOption)}; - const auto rack = CustomRack(system, LegacyRackOption(&header)); + const auto rack = CustomRack(system, LegacyRackOption(nullptr, moduleIndex)); 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); LegacyUserFxState state{}; - Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Control(custom, 0x40001900 | moduleIndex, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state), 123, 456}); auto out = Render(system); @@ -568,20 +597,11 @@ static void TestCustomLegacyUserFx() { std::uint8_t dummy[10]; }; Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, BadSizeParam{})); - Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 2, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0})); + Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 31, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0})); - Ngs2CustomModuleOption badSize{2}; - Require(CreateThrows<std::invalid_argument>(LegacyRackOption(&badSize))); - - struct ExtendedOption { - std::uint32_t size = 20; - std::uint32_t reserved = 0; - std::uint32_t channels = 4; - float param0 = 1000.0f; - float param1 = 0.0f; - } extendedOption; - const auto rackExtended = CustomRack(system, LegacyRackOption(reinterpret_cast<const Ngs2CustomModuleOption*>(&extendedOption))); - Require(sceNgs2RackDestroy(rackExtended, nullptr) == SCE_NGS2_OK); + Require(CreateThrows<std::runtime_error>(LegacyRackOption(&header, moduleIndex))); + Require(ControlThrows<std::runtime_error>(custom, 0x40001d00, + Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state), 123, 456})); state.result = -1; bool failed = false; @@ -595,7 +615,7 @@ static void TestCustomLegacyUserFx() { Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); - const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr)); + const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr, moduleIndex)); const auto voiceNullOpt = Voice(rackNullOpt); Control(voiceNullOpt, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); Patch(voiceNullOpt, master); @@ -604,7 +624,7 @@ static void TestCustomLegacyUserFx() { LegacyUserFxState state2{}; state2.val0 = 0.125f; state2.val1 = 0.75f; - Control(voiceNullOpt, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Control(voiceNullOpt, 0x40001900 | moduleIndex, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state2), 123, 456}); out = Render(system); Require(state2.calls == 1 && state2.flags == 1); @@ -614,6 +634,92 @@ static void TestCustomLegacyUserFx() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +struct DelayOption { + std::uint32_t size = 20; + std::uint32_t type = 0; + std::uint32_t maxTaps = 4; + float maxLength = 1000.0f; + std::uint32_t reserved = 0; +}; + +static Ngs2CustomSubmixerRackOption DelayRackOption(const DelayOption& delay, std::uint32_t count) { + auto option = LegacyRackOption(nullptr, count - 1); + option.custom_rack_option.rack_option.max_ports = 8; + for (std::uint32_t index = 0; index < count; ++index) { + option.custom_rack_option.module[index].module_id = 0x1d; + option.custom_rack_option.module[index].option = reinterpret_cast<const Ngs2CustomModuleOption*>(&delay); + } + return option; +} + +static void TestCustomDelay(std::uint32_t count, const DelayOption& delay) { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + auto option = DelayRackOption(delay, count); + const auto rack = CustomRack(system, option); + 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); + for (const float sample : Render(system)) Require(sample == 0.0f); + + std::vector<std::int16_t> pcm(Grain * 2 * 8); + for (std::uint32_t i = 0; i < pcm.size() / 2; ++i) { + pcm[i * 2] = static_cast<std::int16_t>(1000 + i * 17); + pcm[i * 2 + 1] = static_cast<std::int16_t>(-2000 + static_cast<int>(i) * 11); + } + const auto sampler = Sampler(system, pcm); + Patch(sampler, custom); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto checkGrain = [&](std::uint32_t grain) { + const auto out = Render(system); + for (std::uint32_t i = 0; i < Grain * 2; ++i) { + Require(out[i] == static_cast<float>(pcm[grain * Grain * 2 + i]) / 32768.0f); + } + }; + for (std::uint32_t grain = 0; grain < 3; ++grain) checkGrain(grain); + + LegacyUserFxState state{}; + for (std::uint32_t index = 0; index < 256; ++index) { + Ngs2CustomVoiceUserFxParam param{{sizeof(Ngs2CustomVoiceUserFxParam), 0, 0x40001d00 | index}, + ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state), 123, 456}; + bool rejected = false; + try { + sceNgs2VoiceControl(custom, ¶m.header); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + rejected = message.find("Delay") != std::string::npos && message.find("not implemented") != std::string::npos; + } + Require(rejected); + } + checkGrain(3); + Require(state.calls == 0); + Event(sampler, SCE_NGS2_VOICE_EVENT_STOP); + for (const float sample : Render(system)) Require(sample == 0.0f); + + option.custom_rack_option.module[0].state_size = 4; + Require(CreateThrows<std::runtime_error>(option)); + option.custom_rack_option.module[0].state_size = 0; + option.custom_rack_option.module[0].state_offset = 4; + Require(CreateThrows<std::runtime_error>(option)); + option.custom_rack_option.module[0].state_offset = 0; + option.custom_rack_option.module[0].option = nullptr; + Require(CreateThrows<std::runtime_error>(option)); + Ngs2CustomSamplerRackOption samplerOption{}; + samplerOption.custom_rack_option = DelayRackOption(delay, count).custom_rack_option; + samplerOption.custom_rack_option.rack_option.size = sizeof(samplerOption); + samplerOption.max_channel_works = 1; + Require(CreateSamplerThrows<std::runtime_error>(samplerOption)); + for (const auto invalid : {DelayOption{19}, DelayOption{24}, DelayOption{20, 2}, DelayOption{20, 0, 0}, + DelayOption{20, 0, 9}, DelayOption{20, 0, 4, 0.0f}, DelayOption{20, 0, 4, 20001.0f}, + DelayOption{20, 0, 4, std::numeric_limits<float>::quiet_NaN()}, + DelayOption{20, 0, 4, std::numeric_limits<float>::infinity()}, DelayOption{20, 0, 4, 1000.0f, 1}}) { + Require(CreateThrows<std::runtime_error>(DelayRackOption(invalid, count))); + } + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestCustomGain() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -664,12 +770,17 @@ static void TestCustomGain() { } int main() { + for (const auto count : {1u, 2u, 24u}) { + for (const auto delay : {DelayOption{}, DelayOption{20, 1, 1, 1.0f, 0}, DelayOption{20, 1, 8, 20000.0f, 0}}) { + TestCustomDelay(count, delay); + } + } TestUserFxChain(); TestRackOptions(); TestCustomRackChecks(); TestCustomSampler(); TestCustomFilter(); - TestCustomLegacyUserFx(); + for (const auto moduleIndex : {0u, 1u, 23u}) TestCustomLegacyUserFx(moduleIndex); TestCustomGain(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 37afdd32a1..57b0113258 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -19,8 +19,10 @@ - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules and Gain on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input -- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed, nor Gain (0x15) on a custom sampler rack: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - the 48-byte process context, process flags, callback error handling and null-handler behavior retain the existing submixer callback model and are not console-verified. Non-null module options throw because their console error is unknown. UserFx on custom sampler rack 0x4001 throws: its parameter indexing and routing are not implemented. Module id 0x19, the 40-byte voice parameter (handler and three user-data words), parameter id `0x40001900 | index`, and lack of option and module state follow [fma-rc's PS5 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943), including parameter dispatch at 0x25ec4. +- [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) - [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it - [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts From 928588bbb8cd27e9f8fcc4b2bfdc266b8100eb83 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:01:52 +0200 Subject: [PATCH 1098/1360] fix(tools): compare libraries before looking for a duplicate export's body --- tools/check_conventions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 09cb309937..675981e57b 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -220,12 +220,12 @@ class Check: assembly = asm_exports(text) for number, line in enumerate(source): names = [match.group(1) for match in EXPORT.finditer(line) - if match.group(1) in exports and function_body(source, number) is not None] - names += [name for name in assembly.get(number + 1, ()) if name in exports] + if match.group(1) in exports and library(path) != library(exports[match.group(1)][0]) + and function_body(source, number) is not None] + names += [name for name in assembly.get(number + 1, ()) if name in exports and library(path) != library(exports[name][0])] for name in names: own, own_number = exports[name] - if library(path) != library(own): - self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") + self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def unknown_nids(self, unknowns, symbols): if not unknowns and not symbols: From 721a2e4b734160a48945407c786df9b07357325d Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Tue, 20 Oct 2026 23:52:10 +0100 Subject: [PATCH 1099/1360] fix(opus): decode at the requested rate with bundled libopus --- .gitmodules | 3 + 3rdparty/opus | 1 + CMakeLists.txt | 1 + cmake/Opus.cmake | 17 +++ .../libs/prx/libSceOpusCeltDec/CMakeLists.txt | 4 +- core/libs/prx/libSceOpusDec/CMakeLists.txt | 4 +- .../prx/libSceOpusDec/include/OpusDecoder.hpp | 100 +++--------------- core/libs/tests/GuestOpusCeltDec.cpp | 36 +++++-- core/libs/tests/GuestOpusDec.cpp | 47 ++++++++ docs/dev/debt/libSceOpusCeltDec.md | 2 +- docs/dev/debt/libSceOpusDec.md | 4 +- tools/package_release.py | 2 + tools/tests/test_package_release.py | 19 ++++ 13 files changed, 136 insertions(+), 104 deletions(-) create mode 160000 3rdparty/opus create mode 100644 cmake/Opus.cmake diff --git a/.gitmodules b/.gitmodules index 64a98a2f94..d867f24e45 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,9 @@ [submodule "3rdparty/ffmpeg-core"] path = 3rdparty/ffmpeg-core url = https://github.com/KytyPS5/ext-ffmpeg-core.git +[submodule "3rdparty/opus"] + path = 3rdparty/opus + url = https://github.com/xiph/opus.git [submodule "3rdparty/freetype"] path = 3rdparty/freetype url = https://github.com/freetype/freetype.git diff --git a/3rdparty/opus b/3rdparty/opus new file mode 160000 index 0000000000..22244de5a7 --- /dev/null +++ b/3rdparty/opus @@ -0,0 +1 @@ +Subproject commit 22244de5a79bd1d6d623c32e72bf1954b56235be diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eaa66c8db..9136b46104 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,6 +347,7 @@ add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL) set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON) include(cmake/ZArchive.cmake) +include(cmake/Opus.cmake) if(WIN32) set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE) set(MI_BUILD_STATIC OFF CACHE BOOL "" FORCE) diff --git a/cmake/Opus.cmake b/cmake/Opus.cmake new file mode 100644 index 0000000000..4f655afc78 --- /dev/null +++ b/cmake/Opus.cmake @@ -0,0 +1,17 @@ +function(add_bundled_opus) + set(BUILD_SHARED_LIBS OFF) + set(BUILD_TESTING OFF) + set(OPUS_BUILD_SHARED_LIBRARY OFF) + set(OPUS_BUILD_TESTING OFF) + set(OPUS_BUILD_PROGRAMS OFF) + set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF) + set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF) + add_subdirectory(3rdparty/opus EXCLUDE_FROM_ALL) + configure_file(3rdparty/opus/COPYING ${CMAKE_BINARY_DIR}/core/libs/licenses/COPYING.opus COPYONLY) + set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON) + if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a) + endif() +endfunction() + +add_bundled_opus() diff --git a/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt index 32be01d2dd..38a71d09b5 100644 --- a/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt +++ b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceOpusCeltDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceOpusCeltDec PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceOpusCeltDec PRIVATE ffmpeg) +target_link_libraries(libSceOpusCeltDec PRIVATE opus) configure_windows_unwind(libSceOpusCeltDec) if(BUILD_TESTING) add_test_executable(guest_opus_celt_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusCeltDec.cpp) target_include_directories(guest_opus_celt_dec_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc) + target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc opus) configure_windows_unwind(guest_opus_celt_dec_tests) add_test(NAME guest_opus_celt_dec COMMAND guest_opus_celt_dec_tests) if(WIN32) diff --git a/core/libs/prx/libSceOpusDec/CMakeLists.txt b/core/libs/prx/libSceOpusDec/CMakeLists.txt index 4179bce7ff..01bf008dd3 100644 --- a/core/libs/prx/libSceOpusDec/CMakeLists.txt +++ b/core/libs/prx/libSceOpusDec/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceOpusDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceOpusDec PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceOpusDec PRIVATE ffmpeg) +target_link_libraries(libSceOpusDec PRIVATE opus) configure_windows_unwind(libSceOpusDec) if(BUILD_TESTING) add_test_executable(guest_opus_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusDec.cpp) target_include_directories(guest_opus_dec_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc) + target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc opus) configure_windows_unwind(guest_opus_dec_tests) add_test(NAME guest_opus_dec COMMAND guest_opus_dec_tests) if(WIN32) diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp index fee4c48957..fa9cf66b31 100644 --- a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -1,12 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP #define CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP -#include <algorithm> -#include <cmath> #include <cstddef> #include <cstdint> #include <cstring> -#include <limits> #include <memory> #include <mutex> #include <stdexcept> @@ -15,10 +12,7 @@ #include <utility> #include <vector> -extern "C" { -#include <libavcodec/avcodec.h> -#include <libswresample/swresample.h> -} +#include <opus.h> namespace { @@ -65,20 +59,10 @@ public: decoder->context = *context; decoder->channels = channels; decoder->sampleRate = sampleRate; - const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); - if (!codec) fail("FFmpeg Opus decoder unavailable"); - decoder->codec = avcodec_alloc_context3(codec); - if (!decoder->codec) throw std::bad_alloc(); - av_channel_layout_default(&decoder->codec->ch_layout, channels); - decoder->codec->sample_rate = 48000; - if (avcodec_open2(decoder->codec, codec, nullptr) < 0) fail("cannot open FFmpeg decoder"); - if (sampleRate != 48000) { - AVChannelLayout layout{}; - av_channel_layout_default(&layout, channels); - if (swr_alloc_set_opts2(&decoder->resampler, &layout, AV_SAMPLE_FMT_FLT, sampleRate, &layout, AV_SAMPLE_FMT_FLT, 48000, 0, nullptr) < 0 || - swr_init(decoder->resampler) < 0) - fail("cannot open FFmpeg resampler"); - } + int error = OPUS_OK; + decoder->codec.reset(opus_decoder_create(sampleRate, channels, &error)); + if (error == OPUS_ALLOC_FAIL) throw std::bad_alloc(); + if (!decoder->codec || error != OPUS_OK) fail("cannot create libopus decoder"); decoders.emplace(state, std::move(decoder)); return 0; } @@ -92,49 +76,18 @@ public: std::lock_guard lock(mutex); const auto found = decoders.find(state); if (found == decoders.end()) return error(1); - if (bytes == 0 || bytes > std::numeric_limits<int>::max() - AV_INPUT_BUFFER_PADDING_SIZE) fail("packet loss concealment or oversized packet is not implemented"); + if (bytes == 0) fail("packet loss concealment is not implemented"); auto& decoder = *found->second; if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); - const int samples = packetSamples(data, bytes) / (48000 / decoder.sampleRate); + const int samples = opus_packet_get_nb_samples(data, bytes, decoder.sampleRate); + if (samples <= 0) fail("invalid packet duration"); const int outputBytes = samples * decoder.channels * 2; - if (capacity / 2 * 2 < outputBytes) fail("PCM buffer is too small"); - std::unique_ptr<AVPacket, void (*)(AVPacket*)> packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); - std::unique_ptr<AVFrame, void (*)(AVFrame*)> frame(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); - if (!packet || !frame || av_new_packet(packet.get(), bytes) < 0) throw std::bad_alloc(); - std::memcpy(packet->data, data, bytes); - if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0) - fail("FFmpeg rejected the packet"); - const auto format = static_cast<AVSampleFormat>(frame->format); - if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || - (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) - fail("unexpected FFmpeg PCM format"); - std::vector<float> decoded; - decoded.reserve(static_cast<std::size_t>(frame->nb_samples) * decoder.channels); - for (int sample = 0; sample < frame->nb_samples; ++sample) { - for (int channel = 0; channel < decoder.channels; ++channel) { - const auto* source = reinterpret_cast<const float*>(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - decoded.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); - } - } - if (decoder.resampler) { - const int capacitySamples = swr_get_out_samples(decoder.resampler, frame->nb_samples); - if (capacitySamples < 0) fail("FFmpeg resampler rejected the frame"); - std::vector<float> resampled(static_cast<std::size_t>(capacitySamples) * decoder.channels); - auto* output = reinterpret_cast<std::uint8_t*>(resampled.data()); - const auto* input = reinterpret_cast<const std::uint8_t*>(decoded.data()); - const int converted = swr_convert(decoder.resampler, &output, capacitySamples, &input, frame->nb_samples); - if (converted < 0) fail("FFmpeg resampler rejected the frame"); - resampled.resize(static_cast<std::size_t>(converted) * decoder.channels); - decoded = std::move(resampled); - } - decoder.pending.insert(decoder.pending.end(), decoded.begin(), decoded.end()); - const auto count = static_cast<std::size_t>(samples) * decoder.channels; - if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); - for (std::size_t index = 0; index < count; ++index) { - const auto converted = static_cast<std::int16_t>(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f))); - std::memcpy(reinterpret_cast<std::uint8_t*>(pcm) + index * 2, &converted, 2); - } - decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast<std::ptrdiff_t>(count)); + if (capacity < outputBytes) fail("PCM buffer is too small"); + std::vector<std::int16_t> decoded(static_cast<std::size_t>(samples) * decoder.channels); + const int result = opus_decode(decoder.codec.get(), data, bytes, decoded.data(), samples, 0); + if (result < 0) fail("libopus rejected the packet"); + if (result != samples) fail("unexpected libopus PCM duration"); + std::memcpy(pcm, decoded.data(), static_cast<std::size_t>(outputBytes)); return outputBytes; } @@ -150,13 +103,7 @@ private: std::uint32_t context; int channels; int sampleRate; - AVCodecContext* codec = nullptr; - SwrContext* resampler = nullptr; - std::vector<float> pending; - ~Decoder() { - swr_free(&resampler); - avcodec_free_context(&codec); - } + std::unique_ptr<OpusDecoder, void (*)(OpusDecoder*)> codec{nullptr, opus_decoder_destroy}; }; const char* name; @@ -173,23 +120,6 @@ private: [[noreturn]] void fail(const char* message) const { throw std::runtime_error(std::string(name) + ": " + message); } - - int packetSamples(const std::uint8_t* packet, int size) const { - const unsigned config = packet[0] >> 3; - const int frameSamples = config < 12 ? ((config & 3) == 3 ? 2880 : 480 << (config & 3)) - : config < 16 ? 480 << (config & 1) : 120 << (config & 3); - int frames = 1; - switch (packet[0] & 3) { - case 1: - case 2: frames = 2; break; - case 3: - if (size < 2) fail("truncated packet frame count"); - frames = packet[1] & 63; - break; - } - if (frames == 0 || frames * frameSamples > 5760) fail("invalid packet duration"); - return frames * frameSamples; - } }; } // namespace diff --git a/core/libs/tests/GuestOpusCeltDec.cpp b/core/libs/tests/GuestOpusCeltDec.cpp index 3245cc824e..b0dc8ae383 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -3,7 +3,9 @@ #include <array> #include <cstdint> #include <cstdlib> +#include <cstdio> #include <stdexcept> +#include <opus.h> extern "C" { int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*); @@ -128,18 +130,30 @@ int main() { for (int rate : {0, 22050, 44100, 96000}) Require(sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast<int>(errorBase + 2)); for (int rate : {8000, 12000, 16000, 24000, 48000}) { - std::array<std::uint8_t, 640> toneState{}; - Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0); - const int samples = 960 * rate / 48000; - std::array<std::int16_t, 1922> tone{}; - for (int frame = 0; frame < 4; ++frame) { - tone.fill(12345); - RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4 - 1); }); - Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4) == samples * 4); - Require(tone[0] == 12345 && tone[samples * 2 + 1] == 12345); - if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, 2, rate); + for (int channels : {1, 2}) { + std::array<std::uint8_t, 640> toneState{}; + Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, channels) == 0); + int error = OPUS_OK; + auto* reference = opus_decoder_create(rate, channels, &error); + Require(reference && error == OPUS_OK); + const int samples = 960 * rate / 48000; + std::array<std::int16_t, 1922> tone{}; + for (int frame = 0; frame < 4; ++frame) { + tone.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * channels * 2 - 1); }); + Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * channels * 2) == samples * channels * 2); + Require(tone[0] == 12345 && tone[samples * channels + 1] == 12345); + std::array<std::int16_t, 1920> expected{}; + Require(opus_decode(reference, CELT_TONE[frame], 60, expected.data(), samples, 0) == samples); + if (!std::equal(expected.begin(), expected.begin() + samples * channels, tone.begin() + 1)) { + std::fprintf(stderr, "CELT PCM differs from libopus at %d Hz, %d channels, frame %d\n", rate, channels, frame); + std::abort(); + } + if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, channels, rate); + } + opus_decoder_destroy(reference); + Require(sceOpusCeltDecDestroy(toneState.data()) == 0); } - Require(sceOpusCeltDecDestroy(toneState.data()) == 0); } Require(sceOpusCeltDecDestroy(state.data()) == 0); diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index e19fb78141..6446a6e8e3 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -4,7 +4,10 @@ #include <cmath> #include <cstdint> #include <cstdlib> +#include <cstdio> +#include <span> #include <stdexcept> +#include <opus.h> extern "C" { int APS5_VABI sceOpusDecInitialize(std::uint32_t*); @@ -46,12 +49,56 @@ const std::uint8_t OPUS_NARROWBAND[] = { 10, 0, 0x08, 0x85, 0x89, 0x9D, 0x73, 0x0F, 0xC2, 0xB1, 0xE2, 0x48, }; +void CheckReference(int rate, int channels) { + std::uint32_t context = 0; + std::array<std::uint8_t, 640> state{}; + Require(sceOpusDecInitialize(&context) == 0); + Require(sceOpusDecCreateEx(&context, state.data(), rate, channels) == 0); + int error = OPUS_OK; + auto* reference = opus_decoder_create(rate, channels, &error); + Require(reference && error == OPUS_OK); + std::array<std::uint8_t, 1500> longPacket{}; + auto* repacketizer = opus_repacketizer_create(); + Require(repacketizer != nullptr); + for (int i = 0; i < 6; ++i) Require(opus_repacketizer_cat(repacketizer, OPUS_STEREO + 2, OPUS_STEREO[0]) == OPUS_OK); + const int longSize = opus_repacketizer_out(repacketizer, longPacket.data() + 2, static_cast<opus_int32>(longPacket.size() - 2)); + Require(longSize > 0); + opus_repacketizer_destroy(repacketizer); + longPacket[0] = static_cast<std::uint8_t>(longSize); + longPacket[1] = static_cast<std::uint8_t>(longSize >> 8); + for (auto packets : {std::span<const std::uint8_t>(OPUS_NARROWBAND), std::span<const std::uint8_t>(OPUS_STEREO), + std::span<const std::uint8_t>(OPUS_NARROWBAND), std::span<const std::uint8_t>(longPacket.data(), longSize + 2)}) { + for (std::size_t offset = 0; offset < packets.size();) { + const int bytes = packets[offset] | (packets[offset + 1] << 8); + const auto* packet = packets.data() + offset + 2; + std::array<std::int16_t, 11520> expected{}; + const int samples = opus_decode(reference, packet, bytes, expected.data(), 5760, 0); + Require(samples > 0); + const int count = samples * channels; + std::array<std::int16_t, 11522> actual{}; + actual.fill(12345); + Require(sceOpusDecDecode(state.data(), packet, bytes, actual.data() + 1, count * 2 + 1) == count * 2); + if (!std::equal(expected.begin(), expected.begin() + count, actual.begin() + 1)) { + std::fprintf(stderr, "Opus PCM differs from libopus at %d Hz, %d channels, packet offset %zu\n", rate, channels, offset); + std::abort(); + } + Require(actual.front() == 12345 && std::all_of(actual.begin() + count + 1, actual.end(), [](auto value) { return value == 12345; })); + offset += bytes + 2; + } + } + opus_decoder_destroy(reference); + Require(sceOpusDecDestroy(state.data()) == 0); + Require(sceOpusDecTerminate(&context) == 0); +} + } // namespace int main() { constexpr std::uint32_t errorBase = 0x81740000u; Require(sceOpusDecInitialize(nullptr) == static_cast<int>(errorBase + 13)); Require(sceOpusDecDestroy(nullptr) == static_cast<int>(errorBase)); + for (int rate : {8000, 12000, 16000, 24000, 48000}) + for (int channels : {1, 2}) CheckReference(rate, channels); std::uint32_t context = 0, otherContext = 0; Require(sceOpusDecInitialize(&context) == 0); std::array<std::uint8_t, 640> invalidState{}; diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 2b3827fa86..4ba1fc6999 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -8,4 +8,4 @@ ### Functional - [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. AJM error propagation, resource ownership and termination with live decoders are not implemented. Wrapper validation errors and their precedence are recovered. -- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; these inputs still throw. Packet loss concealment, asynchronous AJM execution, exact PCM conversion and resampler buffering remain outside the verified behavior of the FFmpeg implementation in [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp). +- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; packets whose TOC configuration is below 16 still throw. Decoding uses the bundled libopus decoder of [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) and shares its limits. Packet loss concealment and asynchronous AJM execution are not implemented. diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 6dbf108315..d98513d94b 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -6,6 +6,4 @@ ### Functional -- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout. -- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap -- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured +- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses bundled libopus 1.6.1 for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout. diff --git a/tools/package_release.py b/tools/package_release.py index 7c19c061cd..4279009b47 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -60,6 +60,8 @@ def package(platform, build, output, version): runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) files.extend(build / "core/libs/libs" / name for name in ("mimalloc.dll", "mimalloc-redirect.dll")) + if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}: + files.append(build / "core/libs/licenses/COPYING.opus") files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable for file in [*files, binary, *(source for source, _ in licenses)]: diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index e20eb74bd2..0ff7a04454 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -83,6 +83,25 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(archive.extractfile("libs/" + name).read(), name.encode()) self.assertEqual((self.output / "relinker-v1.exe").read_bytes(), b"Windows executable") + def test_opus_library_archives_include_the_bundled_license(self): + (self.source / "core/libs/prx/libSceOpusDec").mkdir() + (self.library.parent / "libSceOpusDec.prx").write_bytes(b"Opus library bytes") + license_file = self.build / "core/libs/licenses/COPYING.opus" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(b"Opus redistribution notice\n") + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/COPYING.opus"), license_file.read_bytes()) + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/COPYING.opus").read(), license_file.read_bytes()) + + def test_opus_release_without_its_license_is_rejected(self): + (self.source / "core/libs/prx/libSceOpusCeltDec").mkdir() + (self.library.parent / "libSceOpusCeltDec.prx").write_bytes(b"CELT library bytes") + with self.assertRaisesRegex(RuntimeError, "COPYING"): + package_release.package("linux", self.build, self.output, "v1") + self.assertFalse(self.output.exists()) + def test_windows_package_without_configured_compiler_preserves_existing_output(self): (self.build / "CMakeCache.txt").write_text("CMAKE_CXX_COMPILER:FILEPATH=compiler-NOTFOUND\n", encoding="utf-8") (self.build / "core/relinker/relinker.exe").write_bytes(b"Windows executable") From a6e9733c1783ebed7950f500370943f915979f13 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Thu, 22 Oct 2026 23:09:15 +0100 Subject: [PATCH 1100/1360] fix(opus): hide libopus symbols and package its notice as a license Build the bundled libopus with hidden visibility and an empty OPUS_EXPORT, so its symbols stay internal without relying on --exclude-libs (which Apple's linker lacks). Turn off OPUS_STACK_PROTECTOR and OPUS_FORTIFY_SOURCE so MinGW builds do not import libssp-0.dll, as in 27c0acabe2 from #9764. Ship the notice through the licenses list as libs/licenses/opus.txt, like vgmstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- cmake/Opus.cmake | 6 ++++-- tools/package_release.py | 4 ++-- tools/tests/test_package_release.py | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cmake/Opus.cmake b/cmake/Opus.cmake index 4f655afc78..d6bc3e3f78 100644 --- a/cmake/Opus.cmake +++ b/cmake/Opus.cmake @@ -6,9 +6,11 @@ function(add_bundled_opus) set(OPUS_BUILD_PROGRAMS OFF) set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF) set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF) + set(OPUS_STACK_PROTECTOR OFF) + set(OPUS_FORTIFY_SOURCE OFF) add_subdirectory(3rdparty/opus EXCLUDE_FROM_ALL) - configure_file(3rdparty/opus/COPYING ${CMAKE_BINARY_DIR}/core/libs/licenses/COPYING.opus COPYONLY) - set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON) + set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) + target_compile_definitions(opus PRIVATE OPUS_EXPORT=) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a) endif() diff --git a/tools/package_release.py b/tools/package_release.py index 4279009b47..4e0de7b406 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -56,12 +56,12 @@ def package(platform, build, output, version): licenses = [] if "libSceAjm.native.prx" in expected: licenses.append((ROOT / "3rdparty/vgmstream/COPYING", "libs/licenses/vgmstream.txt")) + if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}: + licenses.append((ROOT / "3rdparty/opus/COPYING", "libs/licenses/opus.txt")) if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) files.extend(build / "core/libs/libs" / name for name in ("mimalloc.dll", "mimalloc-redirect.dll")) - if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}: - files.append(build / "core/libs/licenses/COPYING.opus") files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable for file in [*files, binary, *(source for source, _ in licenses)]: diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 0ff7a04454..87c65025f7 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -86,14 +86,14 @@ class ReleasePackageTests(unittest.TestCase): def test_opus_library_archives_include_the_bundled_license(self): (self.source / "core/libs/prx/libSceOpusDec").mkdir() (self.library.parent / "libSceOpusDec.prx").write_bytes(b"Opus library bytes") - license_file = self.build / "core/libs/licenses/COPYING.opus" + license_file = self.source / "3rdparty/opus/COPYING" license_file.parent.mkdir(parents=True) license_file.write_bytes(b"Opus redistribution notice\n") package_release.package("linux", self.build, self.output, "v1") with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: - self.assertEqual(archive.read("libs/COPYING.opus"), license_file.read_bytes()) + self.assertEqual(archive.read("libs/licenses/opus.txt"), license_file.read_bytes()) with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: - self.assertEqual(archive.extractfile("libs/COPYING.opus").read(), license_file.read_bytes()) + self.assertEqual(archive.extractfile("libs/licenses/opus.txt").read(), license_file.read_bytes()) def test_opus_release_without_its_license_is_rejected(self): (self.source / "core/libs/prx/libSceOpusCeltDec").mkdir() From 3aed8761aca665de5662b99bf988172f54c6816a Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 18:13:09 -0400 Subject: [PATCH 1101/1360] test(libkernel): skip the read-only shm unlink case when running as root Root passes the faccessat check in shm_unlink, so the EACCES expectation fails under root as the read-only cases of GuestTruncate would. Skip it there with the same message GuestTruncate prints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestNamedShmConcurrency.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestNamedShmConcurrency.cpp b/core/libs/tests/GuestNamedShmConcurrency.cpp index 60d91794a2..5dc2e7e350 100644 --- a/core/libs/tests/GuestNamedShmConcurrency.cpp +++ b/core/libs/tests/GuestNamedShmConcurrency.cpp @@ -143,9 +143,13 @@ void Run() { if (fchmod_nid_postfix(descriptor, 0400) != 0) throw std::runtime_error("changing shared-object permissions to read-only failed"); - *__error_nid_postfix() = 0; - if (unlinkObject(object.name.c_str()) != -1 || *__error_nid_postfix() != 13) - throw std::runtime_error("unlink of a read-only shared object did not return EACCES"); + if (::geteuid() != 0) { + *__error_nid_postfix() = 0; + if (unlinkObject(object.name.c_str()) != -1 || *__error_nid_postfix() != 13) + throw std::runtime_error("unlink of a read-only shared object did not return EACCES"); + } else { + std::fprintf(stderr, "running as root, which ignores file permissions: the read-only unlink case not tested\n"); + } if (fchmod_nid_postfix(descriptor, 0600) != 0) throw std::runtime_error("restoring shared-object permissions failed"); if (unlinkObject(object.name.c_str()) != 0) From c0dbd1871c3e13c1a2f0813737234ea538baf050 Mon Sep 17 00:00:00 2001 From: raghav314 <samraghav2@gmail.com> Date: Sat, 17 Oct 2026 01:56:48 +0530 Subject: [PATCH 1102/1360] perf(agc): draw finisher pipeline (APS5_DRAW_PIPELINE) The second half of a pipelined draw (stage materialization, cache insert, enqueue) runs on a finisher thread, overlapping the next draw's packet work; draws whose stages could write memory are held back. Commits skip work whose inputs did not change since the last commit, color metadata passes are ordered through the pipeline, resource and storage-image proofs are reused within one collect epoch until a write breaks them, and a shape entry is refreshed in place at the second sight of the same read user words. APS5_VERIFY_DRAW_PIPELINE checks reused proofs against a full check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 +- .../include/Driver/Draw/DrawCache.hpp | 2 +- .../include/Driver/Draw/DrawPipeline.hpp | 60 ++- .../Execution/include/Driver/Driver.hpp | 11 +- .../include/Driver/Shaders/ShaderRegistry.hpp | 2 + .../Execution/include/GuestMemory.hpp | 3 + .../src/Driver/Dispatch/DispatchAhead.cpp | 2 + .../src/Driver/Dispatch/VariantValidation.cpp | 2 +- .../Execution/src/Driver/Draw/Draw.cpp | 479 +++++++++++++++--- .../Execution/src/Driver/Draw/DrawCache.cpp | 18 +- .../src/Driver/Draw/DrawCacheStatistics.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 9 +- .../src/Driver/Draw/DrawPipeline.cpp | 266 +++++++++- .../src/Driver/Draw/DrawPrecheck.cpp | 20 + .../src/Driver/Packets/PacketExecution.cpp | 35 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 210 ++++++++ .../Execution/src/GuestMemory.cpp | 25 +- .../Execution/src/ShaderMemory.cpp | 36 ++ .../libSceAgcDriver/Graphics/include/Draw.hpp | 3 + .../Graphics/include/ShaderResources.hpp | 27 +- .../Graphics/include/Texture.hpp | 4 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 58 ++- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 3 + .../Graphics/src/ShaderAddressResources.cpp | 4 + .../Graphics/src/ShaderInputState.cpp | 47 +- .../Graphics/src/ShaderResources.cpp | 48 ++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 38 +- .../Graphics/src/UnitShadow.cpp | 23 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 6 + .../tests/execution/DrawDataHitUserWords.cpp | 15 + .../execution/DrawUnsupportedBufferType.cpp | 2 + .../tests/execution/PipelinedMetadataPass.cpp | 173 +++++++ .../tests/execution/ProgramWritesMemory.cpp | 48 ++ core/shader/recompiler/ShaderDiskCache.cpp | 2 +- docs/dev/ARCHITECTURE.md | 1 + docs/dev/ENVIRONMENT.md | 2 + docs/dev/debt/libSceAgcDriver.md | 4 + docs/user/USAGE.md | 4 + 38 files changed, 1594 insertions(+), 113 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ProgramWritesMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2bb2a650aa..94f8ca3aa1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1990,6 +1990,7 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) add_test_executable(${agcExecutionTest} EXCLUDE_FROM_ALL ${agcExecutionTestSource}) target_include_directories(${agcExecutionTest} PRIVATE ${LIBS_INCLUDE_DIR} + $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES> ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} @@ -2344,6 +2345,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) + set(agcDrawPipelineTests agc_driver_draw_write_ranges agc_driver_draw_unsupported_buffer_type agc_driver_bda_span_reads agc_driver_copied_writer_copy agc_driver_image_table_reuse agc_driver_depth_storage_reuse agc_driver_staged_clear_sampled_read agc_driver_storage_write_back_overhang agc_driver_write_data_written_ahead agc_driver_gated_dispatch_capture agc_driver_buffer_load_store agc_driver_pipelined_dma agc_driver_pipelined_fill agc_driver_pipelined_predication_read agc_driver_pipelined_predicated_label_order agc_driver_pipelined_metadata_pass agc_driver_draw_cache_reuse agc_driver_draw_data_hit_user_words) + foreach(agcTestName IN LISTS agcDrawPipelineTests) + add_test(NAME ${agcTestName}_draw_pipeline COMMAND ${agcTestName}_tests) + set_tests_properties(${agcTestName}_draw_pipeline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=32;APS5_PIPELINE_DISPATCH=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${agcTestName}_draw_pipeline PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile depth_retile) @@ -2748,8 +2757,10 @@ if(BUILD_TESTING) set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests) set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_draw_data_hit_user_words_refresh COMMAND agc_driver_draw_data_hit_user_words_tests) + set_tests_properties(agc_driver_draw_data_hit_user_words_refresh PROPERTIES ENVIRONMENT "APS5_DRAW_PIPELINE=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify agc_driver_draw_data_hit_user_words_refresh PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 3e1f59e9f4..0ff895330e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -88,7 +88,7 @@ struct StageDataHit { enum class DrawMiss : std::size_t { FrontDiffering, FragmentDiffering, OtherDiffering, Layout, Gate, Stages, Count }; struct DrawEntryCounters { - std::uint64_t lookups = 0, absent = 0, hits = 0, stageValidations = 0, stageEqual = 0, variantsCompared = 0, inserts = 0, variantsInserted = 0, variantsEvicted = 0, present = 0, unstable = 0, touches = 0, verifyHits = 0, verifyMismatches = 0; + std::uint64_t lookups = 0, absent = 0, hits = 0, stageValidations = 0, stageEqual = 0, variantsCompared = 0, inserts = 0, shapeUpdates = 0, variantsInserted = 0, variantsEvicted = 0, present = 0, unstable = 0, touches = 0, verifyHits = 0, verifyMismatches = 0; std::array<std::uint64_t, static_cast<std::size_t>(DrawMiss::Count)> misses{}; std::array<std::uint64_t, MaxDispatchVariants> variantHitsByRank{}; double validateUs = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index 9ca8b03dd5..98199f8560 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -13,6 +13,8 @@ #include <mutex> #include <optional> #include <thread> +#include <span> +#include <string> #include <utility> #include <vector> @@ -32,17 +34,21 @@ public: static void FollowEpoch(std::uint64_t token); static unsigned Committers(); - void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {}, std::vector<Range> reads = {}); + void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {}, std::vector<Range> reads = {}, bool draw = false); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); void Stop(); static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode); - bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; } + bool Busy() const; std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); + bool OverlapsAny(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges); std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes); static bool HeldReads(std::uint64_t address, std::size_t bytes); void ReleaseRunningReads(); + static bool MetadataPassesPipelined(); + static void CountPipelinedMetadataPass(); + static std::uint64_t PipelinedMetadataPasses(); private: struct Item { Commit commit; @@ -50,22 +56,29 @@ private: std::uint64_t labelAddress = 0; std::vector<std::byte> labelBytes; std::vector<Range> reads; + bool draw = false; }; DrawPipeline() = default; void run(); void rethrowFailure(); void report(std::chrono::steady_clock::time_point now); + void countWrites(const std::vector<Range>& writes, bool added); std::mutex mutex; std::condition_variable wake; std::condition_variable idle; std::deque<Item> items; + std::vector<std::pair<Range, std::uint32_t>> queuedWrites; + std::vector<Item> retired; std::exception_ptr failure; std::atomic<std::size_t> outstanding{0}; std::uint32_t idleWaiters = 0; + std::size_t resumeAt = 0; bool committerWaiting = false; bool stopped = false; bool closing = false; + std::uint64_t wakes = 0; + std::uint64_t lockWaitNs = 0; std::thread thread; std::uint64_t commits = 0; std::uint64_t commitNs = 0; @@ -80,6 +93,49 @@ private: std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; +class DrawFinisher { +public: + using Job = std::function<void()>; + using Range = std::pair<std::uint64_t, std::uint64_t>; + struct Pending { + std::uint64_t key = 0; + std::uint64_t shape = 0; + bool unknown = true; + std::vector<Range> writes; + }; + static bool Overlap(); + bool PendingOverlaps(std::uint64_t begin, std::uint64_t end); + bool PendingOverlapsAny(std::span<const Range> ranges); + bool PendingKey(std::uint64_t key, std::uint64_t shape); + enum class DrainSite : std::uint32_t { Lookup, Reads, Packet, Kept, Count }; + static void NoteDrain(DrainSite site, bool drained); + static std::string DrainText(); + static void NoteDrainedOpcode(std::uint32_t opcode, bool besideCommits); + static DrawFinisher& Get(); + static bool Enabled(); + static bool OnFinisherThread(); + static bool Busy() { return outstanding.load(std::memory_order_acquire) != 0; } + void Enqueue(Job job); + void Enqueue(Job job, Pending held); + void Drain(); + void Settle(); + +private: + DrawFinisher() = default; + void run(); + inline static std::atomic<std::size_t> outstanding{0}; + std::mutex mutex; + std::condition_variable wake; + std::condition_variable idle; + std::deque<Job> jobs; + std::deque<Pending> pending; + std::vector<Job> ran; + std::exception_ptr failure; + bool running = false; + bool sleeping = false; + std::thread thread; +}; + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 0e96bcc002..74ba276280 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -41,8 +41,14 @@ #include <utility> #include <vector> +namespace AgcDriver { +class PerformanceTimer; +} + namespace AgcDriver::DriverDetail { +struct DrawTail; + class Driver { public: static Driver& Get(); @@ -141,7 +147,7 @@ private: static bool verifyDrawRecipe(); static std::size_t drawCacheEntries(); void accountDrawVariant(const DispatchVariant& variant, bool added); - void insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode, std::uint64_t shape = 0); + void insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode, std::uint64_t shape = 0, bool replaceShape = false); bool admitDrawKey(std::uint64_t key, std::uint64_t shape); void eraseDrawEntry(std::uint64_t key, const std::shared_ptr<DrawEntry>& expected); std::shared_ptr<const DrawRecipe> findDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages); @@ -167,11 +173,12 @@ private: void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, const std::shared_ptr<DrawEntry>& dataEntry, DrawDataCandidates& dataCandidates, std::vector<StageDataHit>& stageData); std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics); static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads); - void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey); + void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry = false); static bool drawPrecheck(); std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr); DrawVerdict draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected); + DrawVerdict finishDraw(DrawTail& tail, PerformanceTimer& timing); void addDriverPhases(DispatchClass which, const std::array<double, DriverPhaseCount>& ms, bool hit, bool validated); static PendingDispatchPhases& pendingDispatchPhases(); static std::chrono::steady_clock::time_point& packetStartedAt(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 203940d7c3..059cd7253c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -177,6 +177,7 @@ struct ShaderSnapshot { std::shared_ptr<const RegisteredShaderState> registeredState; std::shared_ptr<std::atomic<std::uint8_t>> workgroupUse = std::make_shared<std::atomic<std::uint8_t>>(0u); std::shared_ptr<std::atomic<std::uint8_t>> positionVectorUse = std::make_shared<std::atomic<std::uint8_t>>(0u); + std::shared_ptr<std::atomic<std::uint8_t>> memoryWrites = std::make_shared<std::atomic<std::uint8_t>>(0u); std::shared_ptr<std::atomic<bool>> rawGraphicsNoted = std::make_shared<std::atomic<bool>>(false); std::optional<std::uint32_t> userDataTailRegister; }; @@ -189,6 +190,7 @@ void NoteRawGraphicsDraw(const ShaderSnapshot& snapshot); bool RawGraphicsDrawn(); bool RawGraphicsDrawnIn(std::uint64_t begin, std::uint64_t end); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); +bool ProgramWritesMemory(const ShaderSnapshot& snapshot, std::size_t codeOffset); // The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program // without pos1..pos3 exports, so its artifact does not depend on them. std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span<const std::uint32_t> code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation = nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 24e0fc7968..b61abd1f07 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -106,6 +106,9 @@ bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std:: // workers bump before every packet as before. CollectEpochBumps counts the bumps ([guestmem] line). void BumpCollectEpoch(); void AdoptLatestCollectEpoch(); +std::uint64_t ProofSerial(); +void BreakProofs(); +void MarkProofThread(); std::uint64_t CollectEpoch(); std::uint64_t CollectEpochBumps(); std::uint64_t ThreadCollectEpoch(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 0b6f1f0eb0..6391881462 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" @@ -107,6 +108,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l if (!allQueues && submission.queue == 0) return; auto& resolved = resolvedAhead(); if (resolved.contains(from) || device.Load() == nullptr) return; + if (submission.queue == 0 && DrawPipeline::Depth() != 0 && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (submission.commands[from] >> 8u) & 0xffu); auto& stats = groupCaptureStats(); const auto begin = std::chrono::steady_clock::now(); ++stats.runs; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp index e4136cbebe..99c7b35426 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp @@ -93,7 +93,7 @@ EntryOutcome Driver::validateVariant(std::uint64_t program, std::uint32_t queue, if (live != nullptr) live->clear(); if (DrawPipeline::Active()) { auto& pipeline = DrawPipeline::Queue0(); - if (pipeline.Busy() && std::any_of(variant.runs.begin(), variant.runs.end(), [&](const auto& run) { return pipeline.Overlaps(run.first, static_cast<std::size_t>(run.second - run.first)); })) pipeline.Drain(DrawPipeline::DrainReason::Capture); + if (pipeline.Busy() && pipeline.OverlapsAny(variant.runs)) pipeline.Drain(DrawPipeline::DrainReason::Capture); } const bool masked = live != nullptr && !variant.dataPositions.empty(); const DataMask mask{variant.dataPositions, live}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 566f251089..a1be0d8910 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -1,6 +1,8 @@ +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" @@ -8,21 +10,201 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include <atomic> +#include <array> #include <algorithm> #include <bit> +#include <chrono> +#include <cstdio> +#include <unordered_set> #include <cstdlib> #include <cstdio> #include <cstring> namespace AgcDriver::DriverDetail { +struct DrawTail { + using GpuLock = std::unique_lock<std::remove_reference_t<decltype(GuestMemory::GpuMutex())>>; + bool profile = false; + std::array<double, DrawDriverPhaseCount> phaseMs{}; + std::uint64_t captures = 0; + std::chrono::steady_clock::time_point phaseLap{}; + DrawPhaseTiming phaseTiming{false, phaseMs, phaseLap}; + Pm4::DrawParameters drawParameters{}; + bool pipelined = false; + bool traceIndirect = false; + std::uint64_t dumpTarget = 0; + std::uint64_t dumpSlot1 = 0; + std::shared_ptr<VulkanDevice> localDevice; + bool registerKey = false; + std::uint64_t drawKey = 0; + std::shared_ptr<DrawEntry> entry; + std::shared_ptr<const DrawDecode> decode; + std::shared_ptr<const DrawDecode> relocatedDecode; + std::shared_ptr<DrawEntry> dataEntry; + std::uint64_t shapeKey = 0; + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + std::vector<DrawProgram> programs; + std::vector<ShaderRecompiler::MemoryRegion> memory; + std::vector<ShaderRecompiler::LinkedProgram> linked; + std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>> vertexInfos; + std::vector<std::vector<Graphics::DecodeRead>> decodeReads; + std::unique_ptr<ShaderMemory> shaderMemory; + std::vector<ShaderRecompiler::RecompileResult> results; + std::vector<std::shared_ptr<const ShaderRecompiler::RecompileResult>> compiledStages; + std::vector<Graphics::CompiledShader> stages; + std::uint32_t pushCursorBytes = 0; + std::vector<const ShaderRecompiler::RecompileResult*> programResults; + std::vector<StageCapture> stageCaptures; + std::vector<std::shared_ptr<DispatchVariant>> matched; + std::vector<std::vector<ShaderRecompiler::MemoryRegion>> matchedRegions; + std::vector<std::shared_ptr<DispatchVariant>> fresh; + std::vector<bool> recompiled; + DrawDataCandidates dataCandidates; + std::vector<bool> reused; + std::vector<StageDataHit> stageData; + bool drawHit = false; + bool verifyHit = false; + bool stageEntries = false; + bool patchedStages = false; + const Submission* submission = nullptr; + std::uint32_t submissionQueue = 0; + std::uint64_t submissionSerial = 0; + std::size_t packetOffset = 0; + std::uint32_t packetOpcode = 0; + std::uint32_t reg207 = 0; + std::uint64_t epoch = 0; + QueueState* queue = nullptr; + std::span<const std::uint32_t> packet; + std::string* rejected = nullptr; + std::string ownRejected; + GpuLock* gpuLock = nullptr; + GpuLock ownLock{GuestMemory::GpuMutex(), std::defer_lock}; +}; + +namespace { + +void noteFinisherHandOff(bool handed) { + static std::atomic<std::uint64_t> handedOff{0}, kept{0}; + static std::atomic<std::int64_t> reportedAt{0}; + (handed ? handedOff : kept).fetch_add(1, std::memory_order_relaxed); + static const bool stats = std::getenv("APS5_PIPELINE_STATS") != nullptr || std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!stats) return; + const auto tenSeconds = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 10; + if (reportedAt.exchange(tenSeconds, std::memory_order_relaxed) == tenSeconds) return; + std::fprintf(stderr, "[finisher] draws (10 s): %llu handed to the draw finisher, %llu finished on the worker%s\n", static_cast<unsigned long long>(handedOff.exchange(0)), static_cast<unsigned long long>(kept.exchange(0)), DrawFinisher::DrainText().c_str()); +} + +} + +namespace { + +enum class HeldReason : std::size_t { Known, NoDecode, Size, NullVertex, NullFragment, NullOther, Bda, Buffer, Storage, Count }; +std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(HeldReason::Count)> heldReasons{}; + +void noteHeld(HeldReason reason) { + heldReasons[static_cast<std::size_t>(reason)].fetch_add(1, std::memory_order_relaxed); + static const bool profile = std::getenv("APS5_PIPELINE_STATS") != nullptr || std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) return; + static std::atomic<std::uint64_t> total{0}; + const auto count = ++total; + if (count % 262144u != 0u && (count > 262144u || (count & (count - 1u)) != 0u)) return; + constexpr std::array<const char*, static_cast<std::size_t>(HeldReason::Count)> names{"targets only", "no decode", "stage count", "no variant (vertex)", "no variant (fragment)", "no variant (other)", "BDA stores", "written buffer", "storage image"}; + std::string text; + for (std::size_t i = 0; i < names.size(); ++i) { + char item[64]; + std::snprintf(item, sizeof(item), "%s %s %llu", i == 0 ? "" : ",", names[i], static_cast<unsigned long long>(count >= 262144u ? heldReasons[i].exchange(0) : heldReasons[i].load())); + text += item; + } + std::fprintf(stderr, "[finisher-held] held draws' writes:%s\n", text.c_str()); +} + +std::optional<HeldReason> writesBeyondTargets(const ShaderRecompiler::RecompileResult& compiled) { + using Role = ShaderRecompiler::DescriptorRole; + for (const auto& binding : compiled.bindings) { + if (binding.role == Role::BdaPagetable && Graphics::ShaderStoresThroughAddresses(compiled.spirv)) return HeldReason::Bda; + if (binding.role == Role::GuestBuffers) { + for (std::uint32_t element = 0; element < binding.count; ++element) { + if (element >= binding.bufferWritten.size() || binding.bufferWritten[element]) return HeldReason::Buffer; + } + } + if (binding.role == Role::GuestImages && binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { + for (std::uint32_t element = 0; element < binding.count; ++element) { + if (element >= binding.imageWritten.size() || binding.imageWritten[element]) return HeldReason::Storage; + } + } + } + return std::nullopt; +} + +DrawFinisher::Pending heldWrites(const DrawTail& tail) { + DrawFinisher::Pending held; + held.key = tail.drawKey; + held.shape = tail.shapeKey; + if (tail.decode == nullptr) { + noteHeld(HeldReason::NoDecode); + return held; + } + if (tail.matched.size() != tail.programs.size()) { + noteHeld(HeldReason::Size); + return held; + } + const auto& roles = tail.decode->roles; + for (std::size_t i = 0; i < tail.matched.size(); ++i) { + if (i < roles.size() && roles[i] == ShaderRecompiler::ProgramRole::GeometryBack) continue; + const auto& variant = tail.matched[i]; + if (variant == nullptr || variant->compiled == nullptr) { + const bool candidates = !tail.drawHit && i < tail.dataCandidates.size() && !tail.dataCandidates[i].empty(); + const auto& program = tail.programs[i]; + if (!candidates && program.snapshot != nullptr && !ProgramWritesMemory(*program.snapshot, program.codeOffset) && (program.tailContinuation == nullptr || !ProgramWritesMemory(*program.tailContinuation, 0))) continue; + if (!candidates) { + const auto stage = tail.programs[i].binary.stage; + noteHeld(stage == ShaderRecompiler::ShaderStage::Fragment ? HeldReason::NullFragment : stage == ShaderRecompiler::ShaderStage::Vertex ? HeldReason::NullVertex : HeldReason::NullOther); + return held; + } + for (const auto& [candidate, regions] : tail.dataCandidates[i]) { + if (candidate == nullptr || candidate->compiled == nullptr) { + noteHeld(HeldReason::NullOther); + return held; + } + if (const auto reason = writesBeyondTargets(*candidate->compiled)) { + noteHeld(*reason); + return held; + } + } + continue; + } + if (const auto reason = writesBeyondTargets(*variant->compiled)) { + noteHeld(*reason); + return held; + } + if (i < tail.stageData.size() && tail.stageData[i].compiled != nullptr) { + if (const auto reason = writesBeyondTargets(*tail.stageData[i].compiled)) { + noteHeld(*reason); + return held; + } + } + } + noteHeld(HeldReason::Known); + held.writes = DrawWriteRanges(tail.decode->state, {}, ExactDrawWrites()); + held.unknown = false; + return held; +} + +} + DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected) { PerformanceTimer timing("Driver.Draw"); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - std::array<double, DrawDriverPhaseCount> phaseMs{}; - std::uint64_t captures = 0; - auto phaseLap = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - DrawPhaseTiming phaseTiming{profile, phaseMs, phaseLap}; + auto tailHolder = std::make_shared<DrawTail>(); + auto& tail = *tailHolder; + auto& phaseMs = tail.phaseMs; + auto& captures = tail.captures; + auto& phaseLap = tail.phaseLap; + phaseLap = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + tail.profile = profile; + tail.phaseTiming.profile = profile; + auto& phaseTiming = tail.phaseTiming; if (profile && packetStartedAt() != std::chrono::steady_clock::time_point{}) phaseMs[DrawRowPrologue] = std::chrono::duration<double, std::milli>(phaseLap - packetStartedAt()).count(); const auto drawn = [&] { @@ -35,8 +217,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe pending.tailAt = phaseLap; return DrawVerdict::Drawn; }; - auto drawParameters = Pm4::ResolveDraw(packet, queue); - const bool pipelined = DrawPipeline::Active(); + auto& drawParameters = tail.drawParameters; + drawParameters = Pm4::ResolveDraw(packet, queue); + tail.pipelined = DrawPipeline::Active(); + const bool pipelined = tail.pipelined; static const bool pipelinedIndirect = std::getenv("APS5_NO_PIPELINED_INDIRECT") == nullptr; if (pipelined && drawParameters.indirect) { const auto& indirect = *drawParameters.indirect; @@ -44,10 +228,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe const bool overlaps = pipeline.Busy() && (pipeline.Overlaps(indirect.arguments, static_cast<std::size_t>(std::max<std::uint64_t>(indirect.RangeBytes(), indirect.recordBytes))) || (indirect.countIndirect && pipeline.Overlaps(indirect.countAddress, 4))); if (!pipelinedIndirect || overlaps) pipeline.Drain(DrawPipeline::DrainReason::Indirect); } - const auto drainBeforeInline = [&] { - if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect); - }; - bool traceIndirect = false; + auto& traceIndirect = tail.traceIndirect; if (const auto verdict = precheckDraw(queue, submission, packet, drawParameters, rejected, traceIndirect)) return *verdict; phaseTiming.Phase(DrawRowPrecheck); using Stage = ShaderRecompiler::ShaderStage; @@ -59,7 +240,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe static const bool lockedPrepare = std::getenv("APS5_LOCKED_DRAW_PREPARE") != nullptr; std::unique_lock gpuLock(GuestMemory::GpuMutex(), std::defer_lock); - std::shared_ptr<VulkanDevice> localDevice; + auto& localDevice = tail.localDevice; if (lockedPrepare) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); @@ -81,13 +262,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe phaseTiming.Phase(DrawRowVectors); const bool useDrawEntries = drawEntries() && !ShaderRecompiler::DebugProbeActive() && dumpTarget == 0 && dumpSlot1 == 0; - const bool registerKey = useDrawEntries && registerKeyEnabled(); - std::uint64_t drawKey = 0; + tail.registerKey = useDrawEntries && registerKeyEnabled(); + const bool registerKey = tail.registerKey; + auto& drawKey = tail.drawKey; std::array<std::uint64_t, 5> drawKeyComponents{}; - std::shared_ptr<DrawEntry> entry; - std::shared_ptr<const DrawDecode> decode; - std::shared_ptr<DrawEntry> dataEntry; - std::uint64_t shapeKey = 0; + auto& entry = tail.entry; + auto& decode = tail.decode; + auto& dataEntry = tail.dataEntry; + auto& shapeKey = tail.shapeKey; std::uint64_t drawIdentity = 0, drawAlias = 0; std::vector<DrawUserPatch> userPatches; if (registerKey) { @@ -95,6 +277,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe std::shared_ptr<const DrawRelocation> relocation; bool aliasFirst = false; if (drawRelocationEnabled()) { + if (DrawFinisher::Busy() && drawRelocationPending.key != 0) DrawFinisher::Get().Drain(); drawIdentity = drawIdentityKey(queue, *submission.shaders); std::lock_guard cacheLock(drawCacheMutex); learnPendingRelocation(); @@ -107,6 +290,17 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe } } drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr, aliasFirst ? relocation.get() : nullptr, aliasFirst ? &drawAlias : nullptr); + if (DrawFinisher::Busy()) { + bool shapeMatters = false; + if (DrawFinisher::Overlap() && drawDataHits()) { + std::lock_guard peekLock(drawCacheMutex); + const auto shape = drawShapes.find(shapeKey); + shapeMatters = !drawCache.contains(drawKey) && (shape == drawShapes.end() || !drawCache.contains(shape->second)); + } + const bool drain = !DrawFinisher::Overlap() || DrawFinisher::Get().PendingKey(drawKey, shapeMatters ? shapeKey : 0); + if (drain) DrawFinisher::Get().Drain(); + if (DrawFinisher::Overlap()) DrawFinisher::NoteDrain(DrawFinisher::DrainSite::Lookup, drain); + } std::unique_lock cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; ++drawEntryCounters.registerKeyLookups; @@ -186,16 +380,15 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe } resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile); const auto& graphics = decode->state; - auto pixel = decode->pixel; - const bool writesPrograms = graphics.stages.mesh || drawParameters.indirect || !userPatches.empty(); - std::vector<DrawProgram> writablePrograms; - if (writesPrograms) writablePrograms = decode->programs; - for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value; - const std::vector<DrawProgram>& programs = writesPrograms ? writablePrograms : decode->programs; + auto& pixel = tail.pixel; + pixel = decode->pixel; + auto& programs = tail.programs; + programs = decode->programs; + for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; - auto& words = writablePrograms.front().userData; + auto& words = programs.front().userData; require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters); std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); @@ -231,10 +424,15 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe indirect.startInstanceSgpr = sgprOf(indirect.startInstanceLocation); indirect.drawIndexSgpr = sgprOf(indirect.drawIndexLocation); } - std::vector<ShaderRecompiler::MemoryRegion> memory; + if (DrawFinisher::Busy()) { + const bool drain = !DrawFinisher::Overlap(); + if (drain) DrawFinisher::Get().Drain(); + else DrawFinisher::NoteDrain(DrawFinisher::DrainSite::Reads, false); + } + auto& memory = tail.memory; + auto& linked = tail.linked; memory.reserve(2 * programs.size() + 32); AppendProgramMemory(memory, programs); - std::vector<ShaderRecompiler::LinkedProgram> linked; linked.reserve(programs.size()); for (std::size_t i = 0; i < programs.size(); ++i) { const auto& program = programs[i]; @@ -243,8 +441,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe timing.Mark("prepare"); phaseTiming.Phase(DrawRowProgramPrepare); - std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>> vertexInfos(programs.size()); - std::vector<std::vector<Graphics::DecodeRead>> decodeReads(programs.size()); + auto& vertexInfos = tail.vertexInfos; + vertexInfos.resize(programs.size()); + auto& decodeReads = tail.decodeReads; + decodeReads.resize(programs.size()); const auto decodeVertexInfo = [&](std::size_t i) { const auto& program = programs[i]; if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; @@ -259,31 +459,41 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); phaseTiming.Phase(DrawRowDecode); } - auto shaderMemory = std::make_unique<ShaderMemory>(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter(), &QueryPendingSpan); - std::vector<ShaderRecompiler::RecompileResult> results; - std::vector<std::shared_ptr<const ShaderRecompiler::RecompileResult>> compiledStages; + auto& shaderMemory = tail.shaderMemory; + shaderMemory = std::make_unique<ShaderMemory>(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter(), &QueryPendingSpan); + auto& results = tail.results; + auto& compiledStages = tail.compiledStages; compiledStages.reserve(programs.size()); - std::vector<Graphics::CompiledShader> stages; + auto& stages = tail.stages; results.reserve(programs.size() + 2u); stages.reserve(programs.size()); - std::uint32_t pushCursorBytes = 0; + auto& pushCursorBytes = tail.pushCursorBytes; - std::vector<const ShaderRecompiler::RecompileResult*> programResults(programs.size(), nullptr); + auto& programResults = tail.programResults; + programResults.assign(programs.size(), nullptr); - std::vector<StageCapture> stageCaptures(programs.size()); - std::vector<std::shared_ptr<DispatchVariant>> matched(programs.size()); - std::vector<std::vector<ShaderRecompiler::MemoryRegion>> matchedRegions(programs.size()); + auto& stageCaptures = tail.stageCaptures; + stageCaptures.resize(programs.size()); + auto& matched = tail.matched; + matched.resize(programs.size()); + auto& matchedRegions = tail.matchedRegions; + matchedRegions.resize(programs.size()); - std::vector<std::shared_ptr<DispatchVariant>> fresh(programs.size()); + auto& fresh = tail.fresh; + fresh.resize(programs.size()); - std::vector<bool> recompiled(programs.size(), false); - std::vector<std::vector<std::pair<std::shared_ptr<DispatchVariant>, std::vector<ShaderRecompiler::MemoryRegion>>>> dataCandidates(programs.size()); - std::vector<bool> reused(programs.size(), false); - std::vector<StageDataHit> stageData(programs.size()); - std::uint64_t dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; - bool drawHit = false; - bool verifyHit = false; - const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); + auto& recompiled = tail.recompiled; + recompiled.assign(programs.size(), false); + auto& dataCandidates = tail.dataCandidates; + dataCandidates.resize(programs.size()); + auto& reused = tail.reused; + reused.assign(programs.size(), false); + auto& stageData = tail.stageData; + stageData.resize(programs.size()); + auto& drawHit = tail.drawHit; + auto& verifyHit = tail.verifyHit; + tail.stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); + const bool stageEntries = tail.stageEntries; lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData); for (std::size_t i = 0; i < matched.size(); ++i) { if (matched[i] == nullptr || matched[i]->shaderDataPatches.empty()) continue; @@ -304,8 +514,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe for (const auto& patch : userPatches) current->programs[patch.program].userData[patch.index] = patch.value; decode = std::move(current); } - const bool patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; }); - std::uint64_t stageDataVerified = 0; + tail.patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; }); + tail.relocatedDecode = relocatedDecode; if (registerKey) { @@ -330,6 +540,137 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe phaseTiming.Phase(DrawRowDecode); } + tail.submission = &submission; + tail.submissionQueue = submission.queue; + tail.packetOpcode = (packet[0] >> 8u) & 0xffu; + tail.reg207 = readRegister(queue.context, 0x207); + tail.dumpTarget = dumpTarget; + tail.dumpSlot1 = dumpSlot1; + const bool handOff = pipelined && !drawParameters.indirect && !profile && !lockedPrepare && dumpTarget == 0 && dumpSlot1 == 0 && !tracePrograms && !verifyDrawDataHits() && deferredLabels().labels.empty() && DrawFinisher::Enabled(); + if (handOff) { + tail.epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + tail.submissionSerial = submission.serial; + tail.packetOffset = static_cast<std::size_t>(packet.data() - submission.commands.data()); + noteFinisherHandOff(true); + DrawFinisher::Pending held; + if (DrawFinisher::Overlap()) held = heldWrites(tail); + DrawFinisher::Get().Enqueue([this, tailHolder] { + auto& job = *tailHolder; + DrawPipeline::FollowEpoch(job.epoch); + GuestMemory::SetCurrentPacket(job.packetOpcode, job.submissionQueue); + PerformanceTimer finishTiming("Driver.DrawFinish"); + try { + if (finishDraw(job, finishTiming) == DrawVerdict::Rejected) throw std::runtime_error(job.ownRejected); + } catch (const std::exception& error) { + CaptureTrace::Log("draw-error submission=%llu offset=%zu finisher=1 reason=%.256s", static_cast<unsigned long long>(job.submissionSerial), job.packetOffset, error.what()); + throw std::runtime_error("draw at submission " + std::to_string(job.submissionSerial) + " offset " + std::to_string(job.packetOffset) + " (finished on the draw finisher): " + error.what()); + } + }, std::move(held)); + timing.Mark("draw_handed_off"); + return drawn(); + } + noteFinisherHandOff(false); + if (DrawFinisher::Busy()) { + DrawFinisher::Get().Drain(); + if (DrawFinisher::Overlap()) DrawFinisher::NoteDrain(DrawFinisher::DrainSite::Kept, true); + } + tail.queue = &queue; + tail.packet = packet; + tail.rejected = &rejected; + tail.gpuLock = &gpuLock; + return finishDraw(tail, timing); +} + +DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { + using Stage = ShaderRecompiler::ShaderStage; + using Role = ShaderRecompiler::ProgramRole; + const bool profile = tail.profile; + auto& phaseMs = tail.phaseMs; + auto& phaseLap = tail.phaseLap; + auto& captures = tail.captures; + auto& phaseTiming = tail.phaseTiming; + auto& drawParameters = tail.drawParameters; + const bool pipelined = tail.pipelined; + static const bool pipelinedIndirect = std::getenv("APS5_NO_PIPELINED_INDIRECT") == nullptr; + const auto drainBeforeInline = [&] { + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect); + }; + auto& traceIndirect = tail.traceIndirect; + const auto dumpTarget = tail.dumpTarget; + const auto dumpSlot1 = tail.dumpSlot1; + auto& gpuLock = tail.gpuLock != nullptr ? *tail.gpuLock : tail.ownLock; + auto& localDevice = tail.localDevice; + const bool registerKey = tail.registerKey; + auto& drawKey = tail.drawKey; + auto& entry = tail.entry; + auto& decode = tail.decode; + auto& dataEntry = tail.dataEntry; + auto& shapeKey = tail.shapeKey; + const auto& graphics = decode->state; + auto& pixel = tail.pixel; + auto& programs = tail.programs; + const std::vector<Role>& roles = decode->roles; + auto& memory = tail.memory; + auto& linked = tail.linked; + auto& vertexInfos = tail.vertexInfos; + auto& decodeReads = tail.decodeReads; + auto& shaderMemory = tail.shaderMemory; + auto& results = tail.results; + auto& compiledStages = tail.compiledStages; + auto& stages = tail.stages; + auto& pushCursorBytes = tail.pushCursorBytes; + auto& programResults = tail.programResults; + auto& stageCaptures = tail.stageCaptures; + auto& matched = tail.matched; + auto& matchedRegions = tail.matchedRegions; + auto& fresh = tail.fresh; + auto& recompiled = tail.recompiled; + auto& dataCandidates = tail.dataCandidates; + auto& reused = tail.reused; + auto& stageData = tail.stageData; + std::uint64_t dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; + auto& drawHit = tail.drawHit; + auto& verifyHit = tail.verifyHit; + const bool stageEntries = tail.stageEntries; + const bool patchedStages = tail.patchedStages; + std::uint64_t stageDataVerified = 0; + const Submission& submission = *tail.submission; + std::string& rejected = tail.rejected != nullptr ? *tail.rejected : tail.ownRejected; + static const QueueState noQueue{}; + const QueueState& queue = tail.queue != nullptr ? *tail.queue : noQueue; + const auto drawn = [&] { + phaseTiming.Phase(DrawRowVectors); + if (!profile) return DrawVerdict::Drawn; + auto& pending = pendingDrawPhases(); + pending.phases = true; + pending.captures = captures; + pending.ms = phaseMs; + pending.tailAt = phaseLap; + return DrawVerdict::Drawn; + }; + const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { + if (!graphics.stages.mesh) return; + auto& words = programs.front().userData; + require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); + const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters); + std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); + }; + const auto locate = [&](std::uint32_t location) -> std::optional<std::pair<std::size_t, std::size_t>> { + if (location == 0x280u) return std::nullopt; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; + const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); + if (word < programs[i].userData.size()) return std::make_pair(i, static_cast<std::size_t>(word)); + } + return std::nullopt; + }; + const auto decodeVertexInfo = [&](std::size_t i) { + const auto& program = programs[i]; + if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; + decodeReads[i].clear(); + vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); + if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, tail.reg207, program.tailContinuation.get()) | graphics.layerExports; + }; const auto fold = [&](const ShaderRecompiler::RecompileResult& main, Pm4::DrawParameters& parameters) { FoldDrawOffsets(main, programs.front(), parameters); }; const auto freshRuns = [&](std::size_t i, std::vector<std::pair<std::uint64_t, std::uint64_t>>& runs, std::vector<std::uint32_t>& words) { @@ -433,15 +774,36 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe drawEntryCounters.dataStagesCompiled += dataStagesCompiled; drawEntryCounters.dataVerified += dataVerified; } - cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey); + const bool shapeRefresh = Graphics::DrawPipelineEnabled(); + const auto refreshSeenTwice = [&] { + std::uint64_t data = shapeKey; + bool stored = false; + const auto mix = [&data](std::uint64_t value) { + data ^= value + 0x9e3779b97f4a7c15ull + (data << 6u) + (data >> 2u); + }; + for (std::size_t i = 0; i < programs.size(); ++i) { + const auto& capture = stageCaptures[i]; + if (reused[i] || capture.compiled == nullptr || !CacheableResult(*capture.compiled)) continue; + stored = true; + const auto& words = programs[i].userData; + for (std::size_t w = 0; w < words.size(); ++w) { + if (w >= 64u || (capture.userDataMask >> w & 1u) != 0) mix((static_cast<std::uint64_t>(i) << 40u) ^ (static_cast<std::uint64_t>(w) << 32u) ^ words[w]); + } + } + if (!stored) return false; + static std::array<std::atomic<std::uint64_t>, 4096> lastData{}; + return lastData[shapeKey % lastData.size()].exchange(data, std::memory_order_relaxed) == data; + }; + const bool shapeNeedsEntry = shapeRefresh && registerKey && shapeKey != 0 && drawDataHits() && !drawParameters.indirect && dataEntry != nullptr && dataStagesCompiled != 0 && refreshSeenTwice(); + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey, shapeNeedsEntry); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { for (const auto& read : reads) memory.push_back({read.address, read.Bytes()}); } - if (pipelined && !deferredLabels().labels.empty()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Labels); - if (recordQueuedLabelsAfterCapture(submission.queue, memory)) return draw(queue, packet, submission, rejected); + if (tail.queue != nullptr && pipelined && !deferredLabels().labels.empty()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Labels); + if (tail.queue != nullptr && recordQueuedLabelsAfterCapture(submission.queue, memory)) return draw(*tail.queue, tail.packet, submission, rejected); bool rectListBuilt = false; @@ -504,6 +866,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe snapshot(); timing.Mark("post_compile_prepare"); const auto lockForDraw = [&] { + require(tail.queue != nullptr, "the draw finisher reached a draw it cannot commit through the pipeline"); if (gpuLock.owns_lock()) return; phaseTiming.Phase(DrawRowVectors); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); @@ -553,7 +916,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe std::set<std::size_t> patched; const auto patch = [&](const std::vector<std::pair<std::size_t, std::size_t>>& words, std::uint32_t value) { for (const auto& [program, word] : words) { - writablePrograms[program].userData[word] = value; + programs[program].userData[word] = value; patched.insert(program); } }; @@ -598,7 +961,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe phaseTiming.Phase(DrawRowVectors); auto writes = drawWriteRanges(graphics, stages); const auto commitQueue = submission.queue; - const auto commitPacket = (packet[0] >> 8u) & 0xffu; + const auto commitPacket = tail.packetOpcode; const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { DrawPipeline::FollowEpoch(epoch); @@ -639,20 +1002,20 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe return DrawVerdict::Rejected; } } - if (pipelined && !drawParameters.indirect && deferredLabels().labels.empty()) { + if (pipelined && !drawParameters.indirect && (tail.queue == nullptr || deferredLabels().labels.empty())) { phaseTiming.Phase(DrawRowVectors); auto writes = drawWriteRanges(graphics, stages); const auto commitQueue = submission.queue; - const auto commitPacket = (packet[0] >> 8u) & 0xffu; - const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + const auto commitPacket = tail.packetOpcode; + const auto epoch = DrawFinisher::OnFinisherThread() ? tail.epoch : DrawPipeline::EpochToken().load(std::memory_order_relaxed); auto reads = QueuedDrawReadRanges(drawParameters, stages); - auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(commitPacket, commitQueue); commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey); }; auto held = std::make_shared<decltype(commit)>(std::move(commit)); - DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads), true); phaseTiming.Phase(DrawRowLockWait); timing.Mark("draw_enqueued"); return drawn(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 1d76a6a338..8d05f4693f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -123,10 +123,24 @@ bool Driver::admitDrawKey(std::uint64_t key, std::uint64_t shape) { return drawShapeSightings->SeenBefore(shape); } -void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode, std::uint64_t shape) { +void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode, std::uint64_t shape, bool replaceShape) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; - ++counters.inserts; + if (replaceShape && shape != 0) { + ++counters.shapeUpdates; + if (const auto shapeOf = drawShapes.find(shape); shapeOf != drawShapes.end() && shapeOf->second != key) { + if (const auto previous = drawCache.find(shapeOf->second); previous != drawCache.end()) { + for (const auto& variants : previous->second->stages) { + for (const auto& variant : variants) accountDrawVariant(*variant, false); + } + drawOrder.erase(previous->second->order); + drawCache.erase(previous); + } + drawShapes.erase(shapeOf); + } + } else { + ++counters.inserts; + } const auto found = drawCache.find(key); auto replacement = std::make_shared<DrawEntry>(); replacement->stages.resize(fresh.size()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp index 3e1094adfb..b1f3dc2a02 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp @@ -62,7 +62,7 @@ std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) { std::snprintf(item, sizeof(item), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank])); ranks += item; } - append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast<double>(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast<double>(counters.variantsCompared) / static_cast<double>(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast<double>(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); + append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu shape updates, %llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast<double>(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast<double>(counters.variantsCompared) / static_cast<double>(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.shapeUpdates), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast<double>(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); append("[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast<double>(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches)); append("[draw-cache] stage data hits (10 s): %llu stages (%llu words refreshed), verified %llu; stage inserts with data positions %llu (%.1f positions each)\n", count(counters.stageDataHits), count(counters.stageDataWords), count(counters.stageDataVerified), count(counters.stageDataInserts), counters.stageDataInserts != 0 ? static_cast<double>(counters.stageDataPositions) / static_cast<double>(counters.stageDataInserts) : 0.0); return text; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index c13d57f4a8..a76fd8e08d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -152,8 +152,11 @@ void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderReco variant.dataSlots.resize(kept); } -void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey) { - if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey, registerKey && drawDataHits() ? shapeKey : 0))) { +void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry) { + const bool cacheable = useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu); + const bool admitted = cacheable && (verifyHit || admitDrawKey(drawKey, registerKey && drawDataHits() ? shapeKey : 0)); + const bool replaceShape = cacheable && !admitted && shapeNeedsEntry; + if (admitted || replaceShape) { phaseTiming.Phase(DrawRowVectors); std::uint64_t unstable = 0, mismatches = 0, differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0; for (std::size_t i = 0; i < programs.size(); ++i) { @@ -222,7 +225,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP } fresh[i] = std::move(variant); } - insertDrawEntry(drawKey, fresh, registerKey ? decode : nullptr, registerKey && drawDataHits() ? shapeKey : 0); + insertDrawEntry(drawKey, fresh, registerKey ? decode : nullptr, registerKey && drawDataHits() ? shapeKey : 0, replaceShape); std::uint64_t dataInserts = 0, dataPositions = 0; for (std::size_t i = 0; i < programs.size(); ++i) { if (fresh[i] == nullptr || reused[i] || fresh[i]->dataPositions.empty()) continue; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 9ba7b4a1db..cbd02537d9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -5,6 +5,9 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include <algorithm> +#include <array> +#include <limits> +#include <string> #include <cstdio> #include <cstdlib> #include <cstring> @@ -29,6 +32,188 @@ std::atomic<std::uint64_t> heldReadHits{0}; } +namespace { +thread_local bool finisherThread = false; +thread_local bool finisherProducer = false; +} + +DrawFinisher& DrawFinisher::Get() { + static DrawFinisher* finisher = new DrawFinisher(); + return *finisher; +} + +bool DrawFinisher::Enabled() { + return Graphics::DrawPipelineEnabled(); +} + +bool DrawPipeline::MetadataPassesPipelined() { + return Graphics::DrawPipelineEnabled(); +} + +namespace { +std::atomic<std::uint64_t> pipelinedMetadataPasses{0}; +} + +std::uint64_t DrawPipeline::PipelinedMetadataPasses() { + return pipelinedMetadataPasses.load(std::memory_order_relaxed); +} + +void DrawPipeline::CountPipelinedMetadataPass() { + pipelinedMetadataPasses.fetch_add(1, std::memory_order_relaxed); +} + +bool DrawFinisher::Overlap() { + return Graphics::DrawPipelineEnabled(); +} + +bool DrawFinisher::PendingOverlaps(std::uint64_t begin, std::uint64_t end) { + if (!Busy() || !finisherProducer) return false; + std::lock_guard lock(mutex); + for (const auto& held : pending) { + if (held.unknown) return true; + for (const auto& [first, last] : held.writes) { + if (begin < last && first < end) return true; + } + } + return false; +} + +bool DrawFinisher::PendingOverlapsAny(std::span<const Range> ranges) { + if (!Busy() || !finisherProducer) return false; + std::lock_guard lock(mutex); + for (const auto& held : pending) { + if (held.unknown) return true; + for (const auto& [first, last] : held.writes) { + for (const auto& [begin, end] : ranges) { + if (begin < last && first < end) return true; + } + } + } + return false; +} + +bool DrawFinisher::PendingKey(std::uint64_t key, std::uint64_t shape) { + if (!Busy() || !finisherProducer) return false; + std::lock_guard lock(mutex); + return std::any_of(pending.begin(), pending.end(), [&](const Pending& held) { return held.key == key || (shape != 0 && held.shape == shape); }); +} + +namespace { +std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(DrawFinisher::DrainSite::Count)> finisherDrains{}, finisherSkips{}; +} + +void DrawFinisher::NoteDrain(DrainSite site, bool drained) { + (drained ? finisherDrains : finisherSkips)[static_cast<std::size_t>(site)].fetch_add(1, std::memory_order_relaxed); +} + +namespace { +std::array<std::atomic<std::uint64_t>, 257> drainedOpcodes{}, drainedBeside{}; +} + +void DrawFinisher::NoteDrainedOpcode(std::uint32_t opcode, bool besideCommits) { + drainedOpcodes[std::min<std::uint32_t>(opcode, 256u)].fetch_add(1, std::memory_order_relaxed); + if (besideCommits) drainedBeside[std::min<std::uint32_t>(opcode, 256u)].fetch_add(1, std::memory_order_relaxed); +} + +std::string DrawFinisher::DrainText() { + constexpr std::array<const char*, static_cast<std::size_t>(DrainSite::Count)> names{"lookup", "reads", "packet", "kept draw"}; + std::string text = "; worker drains of the finisher (drained/skipped):"; + for (std::size_t i = 0; i < names.size(); ++i) { + char item[64]; + std::snprintf(item, sizeof(item), " %s %llu/%llu", names[i], static_cast<unsigned long long>(finisherDrains[i].exchange(0)), static_cast<unsigned long long>(finisherSkips[i].exchange(0))); + text += item; + } + text += "; drained by opcode (of them beside commits):"; + for (int shown = 0; shown < 6; ++shown) { + std::size_t top = 0; + for (std::size_t i = 1; i < drainedOpcodes.size(); ++i) { + if (drainedOpcodes[i].load() > drainedOpcodes[top].load()) top = i; + } + const auto count = drainedOpcodes[top].exchange(0); + if (count == 0) break; + char item[48]; + std::snprintf(item, sizeof(item), " 0x%02zx x%llu (%llu)", top, static_cast<unsigned long long>(count), static_cast<unsigned long long>(drainedBeside[top].exchange(0))); + text += item; + } + return text; +} + +bool DrawFinisher::OnFinisherThread() { + return finisherThread; +} + +void DrawFinisher::Enqueue(Job job) { + Enqueue(std::move(job), Pending{}); +} + +void DrawFinisher::Enqueue(Job job, Pending held) { + finisherProducer = true; + constexpr std::size_t depth = 16; + thread_local std::vector<Job> reclaim; + reclaim.clear(); + std::unique_lock lock(mutex); + if (!ran.empty()) std::swap(ran, reclaim); + if (failure != nullptr) std::rethrow_exception(std::exchange(failure, nullptr)); + if (jobs.size() >= depth && GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw finisher full while the enqueuing thread holds the GPU lock"); + idle.wait(lock, [&] { return jobs.size() < depth; }); + jobs.push_back(std::move(job)); + pending.push_back(std::move(held)); + outstanding.fetch_add(1, std::memory_order_release); + if (!running) { + running = true; + thread = std::thread([this] { + finisherThread = true; + run(); + }); + } + if (sleeping) wake.notify_one(); +} + +void DrawFinisher::run() { + for (;;) { + Job* job = nullptr; + bool failed = false; + SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); + { + std::unique_lock lock(mutex); + sleeping = true; + wake.wait(lock, [&] { return !jobs.empty(); }); + sleeping = false; + job = &jobs.front(); + failed = failure != nullptr; + } + std::exception_ptr error; + try { + if (!failed) (*job)(); + } catch (...) { + error = std::current_exception(); + } + std::lock_guard lock(mutex); + if (error != nullptr && failure == nullptr) failure = error; + ran.push_back(std::move(jobs.front())); + jobs.pop_front(); + pending.pop_front(); + outstanding.fetch_sub(1, std::memory_order_release); + idle.notify_all(); + } +} + +void DrawFinisher::Settle() { + if (!Busy() || OnFinisherThread()) return; + if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw finisher settle under the GPU lock"); + std::unique_lock lock(mutex); + idle.wait(lock, [&] { return jobs.empty(); }); +} + +void DrawFinisher::Drain() { + if (OnFinisherThread()) return; + if (Busy() && GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw finisher drain under the GPU lock"); + SpinUntil(HandoffSpinWindow() * 2, [&] { return outstanding.load(std::memory_order_acquire) == 0; }); + std::unique_lock lock(mutex); + if (!jobs.empty()) idle.wait(lock, [&] { return jobs.empty(); }); + if (failure != nullptr) std::rethrow_exception(std::exchange(failure, nullptr)); +} + DrawPipeline& DrawPipeline::Queue0() { static DrawPipeline* pipeline = new DrawPipeline(); return *pipeline; @@ -78,9 +263,15 @@ void DrawPipeline::rethrowFailure() { std::rethrow_exception(error); } -void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress, std::vector<std::byte> labelBytes, std::vector<Range> reads) { +void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress, std::vector<std::byte> labelBytes, std::vector<Range> reads, bool draw) { + if (DrawFinisher::Busy() && !DrawFinisher::OnFinisherThread()) DrawFinisher::Get().Drain(); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + thread_local std::vector<Item> reclaim; + reclaim.clear(); + const auto lockStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; std::unique_lock lock(mutex); + if (!retired.empty()) std::swap(retired, reclaim); + if (profile) lockWaitNs += elapsedNs(lockStart); rethrowFailure(); if (closing) { lock.unlock(); @@ -90,23 +281,33 @@ void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64 if (items.size() >= Depth()) { if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline full while the enqueuing thread holds the GPU lock"); const auto start = std::chrono::steady_clock::now(); + resumeAt = Depth() - 1; ++idleWaiters; - idle.wait(lock, [&] { return items.size() < Depth(); }); + idle.wait(lock, [&] { return items.size() <= resumeAt; }); --idleWaiters; + resumeAt = 0; fullWaitNs += elapsedNs(start); rethrowFailure(); } depthSum += items.size(); ++enqueued; heldReadRanges.fetch_add(reads.size(), std::memory_order_relaxed); - items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes), std::move(reads)}); + countWrites(writes, true); + items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes), std::move(reads), draw}); outstanding.fetch_add(1, std::memory_order_release); if (!thread.joinable()) { - thread = std::thread([this] { run(); }); + thread = std::thread([this] { + GuestMemory::MarkProofThread(); + run(); + }); runningCommitters.fetch_add(1, std::memory_order_acq_rel); } - if (committerWaiting) wake.notify_one(); - if (!profile) return; + if (committerWaiting) { + ++wakes; + wake.notify_one(); + } + static const bool stats = std::getenv("APS5_PIPELINE_STATS") != nullptr; + if (!profile && !stats) return; const auto now = std::chrono::steady_clock::now(); if (now - lastReport >= std::chrono::seconds(10)) report(now); } @@ -135,17 +336,19 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { char reads[64]; std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed))); drainText += reads; - std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, reads, drainText.c_str()); - commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = 0; + std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast<double>(lockWaitNs) / 1e3 / static_cast<double>(enqueued), static_cast<unsigned long long>(wakes), reads, drainText.c_str()); + commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = wakes = lockWaitNs = 0; drains = {}; drainWaitNs = {}; } + void DrawPipeline::run() { PerformanceTrace::BindThread("draw committer"); StartCommitterSampler(); for (;;) { Commit* commit = nullptr; + bool isDraw = false; SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); { std::unique_lock lock(mutex); @@ -154,6 +357,7 @@ void DrawPipeline::run() { committerWaiting = false; if (items.empty()) break; commit = stopped ? nullptr : &items.front().commit; + isDraw = items.front().draw; } const auto start = std::chrono::steady_clock::now(); std::exception_ptr fatal; @@ -161,6 +365,7 @@ void DrawPipeline::run() { bool shutdown = false; try { if (commit != nullptr) (*commit)(); + if (!isDraw) GuestMemory::BreakProofs(); } catch (const std::exception&) { failed = true; fatal = std::current_exception(); @@ -176,13 +381,15 @@ void DrawPipeline::run() { if (!shutdown) Driver::Get().ReportFailure(fatal); } std::lock_guard lock(mutex); + countWrites(items.front().writes, false); + retired.push_back(std::move(items.front())); items.pop_front(); ++commits; commitNs += spent; if (failed) ++commitErrors; if (fatal != nullptr && failure == nullptr) failure = fatal; outstanding.fetch_sub(1, std::memory_order_release); - if (idleWaiters != 0) idle.notify_all(); + if (idleWaiters != 0 && (items.empty() || items.size() <= resumeAt)) idle.notify_all(); } runningCommitters.fetch_sub(1, std::memory_order_acq_rel); } @@ -202,8 +409,10 @@ void DrawPipeline::Stop() { void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { drainRequests[std::min<std::uint32_t>(opcode, 0x100)].fetch_add(1, std::memory_order_relaxed); + const bool finisherBusy = DrawFinisher::Busy() && !DrawFinisher::OnFinisherThread(); + if ((finisherBusy || Busy()) && GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast<unsigned>(reason)) + ", opcode " + std::to_string(opcode) + ")"); + if (finisherBusy) DrawFinisher::Get().Drain(); if (!Busy()) return; - if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast<unsigned>(reason)) + ", opcode " + std::to_string(opcode) + ")"); const auto start = std::chrono::steady_clock::now(); SpinUntil(HandoffSpinWindow() * 2, [&] { return !Busy(); }); std::unique_lock lock(mutex); @@ -257,15 +466,42 @@ std::optional<std::uint64_t> DrawPipeline::PendingLabel(std::uint64_t address, s return std::nullopt; } -bool DrawPipeline::Overlaps(std::uint64_t address, std::size_t bytes) { - const auto end = address + bytes; +void DrawPipeline::countWrites(const std::vector<Range>& writes, bool added) { + for (const auto& range : writes) { + const auto found = std::find_if(queuedWrites.begin(), queuedWrites.end(), [&](const auto& entry) { return entry.first == range; }); + if (added) { + if (found != queuedWrites.end()) ++found->second; + else queuedWrites.emplace_back(range, 1u); + } else if (found != queuedWrites.end() && --found->second == 0) { + *found = queuedWrites.back(); + queuedWrites.pop_back(); + } + } +} + +bool DrawPipeline::Busy() const { + return outstanding.load(std::memory_order_acquire) != 0 || (finisherProducer && DrawFinisher::Busy() && DrawFinisher::Overlap()); +} + +bool DrawPipeline::OverlapsAny(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) { + if (DrawFinisher::Overlap() && DrawFinisher::Get().PendingOverlapsAny(ranges)) return true; std::lock_guard lock(mutex); - for (const auto& item : items) { - for (const auto& [begin, limit] : item.writes) { - if (address < limit && begin < end) return true; + for (const auto& [range, count] : queuedWrites) { + for (const auto& [address, end] : ranges) { + if (address < range.second && range.first < end) return true; } } return false; } +bool DrawPipeline::Overlaps(std::uint64_t address, std::size_t bytes) { + const auto end = address + bytes; + if (DrawFinisher::Overlap() && DrawFinisher::Get().PendingOverlaps(address, end)) return true; + std::lock_guard lock(mutex); + for (const auto& [range, count] : queuedWrites) { + if (address < range.second && range.first < end) return true; + } + return false; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index 6311d6461f..b6749edc50 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include <cstdlib> namespace AgcDriver::DriverDetail { @@ -16,6 +17,25 @@ std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const S if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing; if (const auto pass = Graphics::DecodeColorMetadataPass(queue)) { require(!drawParameters.indirect, "indirect CB metadata passes are unsupported"); + if (DrawPipeline::Active() && DrawPipeline::MetadataPassesPipelined() && deferredLabels().labels.empty()) { + auto writes = Graphics::PipelinedColorMetadataPassWrites(*pass); + const auto localDevice = device.Load(); + if (!writes.empty() && localDevice != nullptr) { + const auto queueId = submission.queue; + const auto opcode = (packet[0] >> 8u) & 0xffu; + DrawPipeline::Queue0().Enqueue([this, localDevice, queueId, opcode, pass = *pass] { + GuestMemory::SetCurrentPacket(opcode, queueId); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + auto current = device.Load(); + const auto& target = current != nullptr ? current : localDevice; + recordLabelsForPacket(target.get(), queueId); + target->ColorMetadataPass(pass); + }, std::move(writes)); + DrawPipeline::CountPipelinedMetadataPass(); + return DrawVerdict::Drawn; + } + } if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (packet[0] >> 8u) & 0xffu); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); std::lock_guard gpuLock(GuestMemory::GpuMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index ce9a656cb7..93202ca2a8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -34,6 +34,16 @@ bool pipelineDma() { return enabled; } +bool keepsDrawFinisher(std::uint32_t header, std::uint32_t opcode) { + if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; + if (Pm4::DrawOpcode(opcode)) return true; + switch (opcode) { + case 0x13: case 0x26: case 0x2a: case 0x2f: case 0x69: case 0x76: case 0x79: return true; + case 0x10: return ((header >> 2u) & 0x3fu) == 0; + default: return false; + } +} + bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode) { if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; @@ -108,13 +118,17 @@ void Driver::deliverFlipEopInterrupt(std::uint32_t queue) { template <typename TWork> void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { + static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!report) { + work(); + return; + } static thread_local WorkerProfile profile; const auto begin = std::chrono::steady_clock::now(); work(); const auto end = std::chrono::steady_clock::now(); profile.*bucket += std::chrono::duration<double, std::milli>(end - begin).count(); - static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (report && end - profile.reported > std::chrono::seconds(10)) { + if (end - profile.reported > std::chrono::seconds(10)) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration<double>(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); } @@ -226,6 +240,12 @@ void Driver::execute(const Submission& submission) { struct ClearAhead { ~ClearAhead() { clearResolvedAhead(); } } clearAhead; + struct SettleFinisher { + bool queue0; + ~SettleFinisher() { + if (queue0) DrawFinisher::Get().Settle(); + } + } settleFinisher{submission.queue == 0}; for (std::size_t cursor = 0; cursor < submission.commands.size();) { if (packetEpoch()) bumpEpoch(&EpochBumps::packets); CheckFailure(); @@ -235,6 +255,14 @@ void Driver::execute(const Submission& submission) { const auto packet = std::span(submission.commands).subspan(cursor, count); const auto opcode = (header >> 8u) & 0xffu; auto nextCursor = cursor + count; + if (submission.queue == 0 && DrawFinisher::Busy() && !keepsDrawFinisher(header, opcode)) { + const bool beside = DrawFinisher::Overlap() && opcode != 0x15 && keepsDrawPipeline(packet, header, opcode); + if (!beside) DrawFinisher::Get().Drain(); + if (DrawFinisher::Overlap()) { + DrawFinisher::NoteDrain(DrawFinisher::DrainSite::Packet, !beside); + if (!beside) DrawFinisher::NoteDrainedOpcode(header == FlipPacketHeader ? 0xffu : opcode, keepsDrawPipeline(packet, header, opcode)); + } + } if (APS5_ENABLE_TIMING_LOG && submission.queue == 0 && pendingFrameTiming == nullptr) includeTimingSubmission(submission, false); PerformanceContext timingContext(submission.queue == 0 ? pendingFrameTiming.get() : FrameTiming::Async()); PerformanceTimer timing("Driver.Packet"); @@ -289,7 +317,7 @@ void Driver::execute(const Submission& submission) { const auto flushStart = profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; flushBetweenPackets(submission.queue, header, opcode == 0x49 || opcode == 0x37); timing.Mark("flush_between_packets"); - PacketTimer packetTimer{profilePackets, header == FlipPacketHeader ? 0xffffu : opcode, submission.queue, packetProfile, std::chrono::steady_clock::now()}; + PacketTimer packetTimer{profilePackets, header == FlipPacketHeader ? 0xffffu : opcode, submission.queue, packetProfile, profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}}; if (profilePackets) { packetStartedAt() = packetTimer.start; @@ -519,6 +547,7 @@ void Driver::execute(const Submission& submission) { cursor = nextCursor; } + if (submission.queue == 0) DrawFinisher::Get().Drain(); PerformanceContext endContext(submission.queue == 0 ? frameTiming() : FrameTiming::Async()); PerformanceTimer endTiming("Driver.EndSubmission"); static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr; 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 8b9b39afe0..a23bb4057a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -236,6 +236,216 @@ bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset return used; } +namespace { + +bool readOnlyMemoryOp(ShaderRecompiler::RdnaOpcode op) { + using Op = ShaderRecompiler::RdnaOpcode; + switch (op) { + case Op::SLoadDword: + case Op::SLoadDwordx2: + case Op::SLoadDwordx4: + case Op::SLoadDwordx8: + case Op::SLoadDwordx16: + case Op::BufferLoadFormatX: + case Op::BufferLoadFormatXy: + case Op::BufferLoadFormatXyz: + case Op::BufferLoadFormatXyzw: + case Op::BufferLoadDword: + case Op::BufferLoadDwordx2: + case Op::BufferLoadDwordx4: + case Op::ImageSample: + case Op::ImageSampleLz: + case Op::ImageSampleBCl: + case Op::ImageSampleBClO: + case Op::ImageSampleBO: + case Op::ImageSampleC: + case Op::ImageSampleCB: + case Op::ImageSampleCBCl: + case Op::ImageSampleCBClO: + case Op::ImageSampleCBO: + case Op::ImageSampleCCl: + case Op::ImageSampleCClO: + case Op::ImageSampleCD: + case Op::ImageSampleCDCl: + case Op::ImageSampleCDClO: + case Op::ImageSampleCDO: + case Op::ImageSampleCL: + case Op::ImageSampleCLzO: + case Op::ImageSampleCLO: + case Op::ImageSampleCO: + case Op::ImageSampleCl: + case Op::ImageSampleClO: + case Op::ImageSampleD: + case Op::ImageSampleDCl: + case Op::ImageSampleDO: + case Op::ImageSampleLzO: + case Op::ImageSampleO: + case Op::ImageSampleCd: + case Op::ImageSampleCdCl: + case Op::ImageSampleCCd: + case Op::ImageSampleCCdCl: + case Op::ImageSampleCdO: + case Op::ImageSampleCdClO: + case Op::ImageSampleCCdO: + case Op::ImageSampleCCdClO: + case Op::ImageSampleDG16: + case Op::ImageSampleDClG16: + case Op::ImageSampleCDG16: + case Op::ImageSampleCDClG16: + case Op::ImageSampleDOG16: + case Op::ImageSampleDClOG16: + case Op::ImageSampleCDOG16: + case Op::ImageSampleCDClOG16: + case Op::ImageSampleCdG16: + case Op::ImageSampleCdClG16: + case Op::ImageSampleCCdG16: + case Op::ImageSampleCCdClG16: + case Op::ImageSampleCdOG16: + case Op::ImageSampleCdClOG16: + case Op::ImageSampleCCdOG16: + case Op::ImageSampleCCdClOG16: + case Op::ImageLoad: + case Op::ImageGetResinfo: + case Op::SBufferLoadDword: + case Op::SBufferLoadDwordx2: + case Op::SBufferLoadDwordx4: + case Op::SBufferLoadDwordx8: + case Op::SBufferLoadDwordx16: + case Op::SScratchLoadDword: + case Op::SScratchLoadDwordx2: + case Op::SScratchLoadDwordx4: + case Op::BufferLoadUbyte: + case Op::BufferLoadSbyte: + case Op::BufferLoadUshort: + case Op::BufferLoadSshort: + case Op::BufferLoadDwordx3: + case Op::BufferLoadUbyteD16: + case Op::BufferLoadUbyteD16Hi: + case Op::BufferLoadSbyteD16: + case Op::BufferLoadSbyteD16Hi: + case Op::BufferLoadShortD16: + case Op::BufferLoadShortD16Hi: + case Op::TbufferLoadFormatX: + case Op::TbufferLoadFormatXy: + case Op::TbufferLoadFormatXyz: + case Op::TbufferLoadFormatXyzw: + case Op::TbufferLoadFormatD16X: + case Op::TbufferLoadFormatD16Xy: + case Op::TbufferLoadFormatD16Xyz: + case Op::TbufferLoadFormatD16Xyzw: + case Op::BufferLoadFormatD16X: + case Op::BufferLoadFormatD16Xy: + case Op::BufferLoadFormatD16Xyz: + case Op::BufferLoadFormatD16Xyzw: + case Op::BufferLoadFormatD16HiX: + case Op::FlatLoadUbyte: + case Op::FlatLoadSbyte: + case Op::FlatLoadUshort: + case Op::FlatLoadSshort: + case Op::FlatLoadDword: + case Op::FlatLoadDwordx2: + case Op::FlatLoadDwordx3: + case Op::FlatLoadDwordx4: + case Op::GlobalLoadDwordAddtid: + case Op::FlatLoadUbyteD16: + case Op::FlatLoadUbyteD16Hi: + case Op::FlatLoadSbyteD16: + case Op::FlatLoadSbyteD16Hi: + case Op::FlatLoadShortD16: + case Op::FlatLoadShortD16Hi: + case Op::ImageGetLod: + case Op::ImageLoadMip: + case Op::ImageLoadPck: + case Op::ImageLoadPckSgn: + case Op::ImageLoadMipPck: + case Op::ImageLoadMipPckSgn: + case Op::ImageLoadBy2: + case Op::ImageLoadBy4: + case Op::ImageLoadMipBy2: + case Op::ImageLoadMipBy4: + case Op::ImageLoadPck2: + case Op::ImageLoadPck4: + case Op::ImageLoadMipPck2: + case Op::ImageLoadMipPck4: + case Op::ImageMsaaLoad: + case Op::ImageGather4Lz: + case Op::ImageGather4C: + case Op::ImageGather4CLz: + case Op::ImageGather4LzO: + case Op::ImageGather4CO: + case Op::ImageGather4CLzO: + case Op::ImageGather4h: + case Op::ImageGather4hPck: + case Op::ImageGather4: + case Op::ImageGather4B: + case Op::ImageGather4BCl: + case Op::ImageGather4BClO: + case Op::ImageGather4BO: + case Op::ImageGather4Cl: + case Op::ImageGather4ClO: + case Op::ImageGather4CB: + case Op::ImageGather4CBCl: + case Op::ImageGather4CBClO: + case Op::ImageGather4CBO: + case Op::ImageGather4CCl: + case Op::ImageGather4CClO: + case Op::ImageGather4CL: + case Op::ImageGather4CLO: + case Op::ImageGather4L: + case Op::ImageGather4LO: + case Op::ImageGather4O: + case Op::ImageBvhIntersectRay: + case Op::SGl1Inv: + case Op::SDcacheInv: + case Op::SDcacheWb: + case Op::SMemtime: + case Op::SMemrealtime: + case Op::SDcacheDiscard: + case Op::SDcacheDiscardX2: + case Op::BufferGl0Inv: + case Op::BufferGl1Inv: + return true; + default: + return false; + } +} + +bool decodedWritesMemory(std::span<const std::uint32_t> code) { + using Family = ShaderRecompiler::RdnaInstructionFamily; + using Op = ShaderRecompiler::RdnaOpcode; + try { + const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + return std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { + switch (instruction.family) { + case Family::MUBUF: case Family::MTBUF: case Family::FLAT: case Family::MIMG: case Family::SMEM: + return !readOnlyMemoryOp(instruction.op); + case Family::DS: + return instruction.gds || instruction.op == Op::DsGwsInit || instruction.op == Op::DsGwsBarrier || instruction.op == Op::DsGwsSemaV || instruction.op == Op::DsGwsSemaP || instruction.op == Op::DsGwsSemaBr || instruction.op == Op::DsGwsSemaReleaseAll || instruction.op == Op::DsOrderedCount || instruction.op == Op::DsAppend || instruction.op == Op::DsConsume; + case Family::Unknown: + return true; + default: + return instruction.op == Op::SSetpcB64 || instruction.op == Op::SSwappcB64 || instruction.op == Op::SRfeB64; + } + }); + } catch (const std::exception&) { + return true; + } +} + +} + +bool ProgramWritesMemory(const ShaderSnapshot& snapshot, std::size_t codeOffset) { + constexpr std::uint8_t Unknown = 0, Reads = 1, Writes = 2; + if (codeOffset == 0) { + const auto cached = snapshot.memoryWrites->load(std::memory_order_acquire); + if (cached != Unknown) return cached == Writes; + } + if (codeOffset > snapshot.code.size()) return true; + const bool writes = decodedWritesMemory(std::span(snapshot.code).subspan(codeOffset)); + if (codeOffset == 0) snapshot.memoryWrites->store(writes ? Writes : Reads, std::memory_order_release); + return writes; +} + std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span<const std::uint32_t> code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation) { constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; const auto bits = paClVsOutCntl & Graphics::PositionExportLayoutBits; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 313498d4ca..28fe51847d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1485,6 +1485,23 @@ std::uint64_t CollectEpochBumps() { return collectEpochBumps.load(std::memory_order_relaxed); } +namespace { +std::atomic<std::uint64_t> proofSerial{1}; +thread_local bool proofThread = false; +} + +std::uint64_t ProofSerial() { + return proofSerial.load(std::memory_order_acquire); +} + +void BreakProofs() { + proofSerial.fetch_add(1, std::memory_order_acq_rel); +} + +void MarkProofThread() { + proofThread = true; +} + std::uint64_t CollectEpoch() { return collectMemoEnabled() ? threadCollectEpoch : 0; } @@ -1692,6 +1709,7 @@ bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint6 std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function<std::pair<std::uint64_t, std::uint64_t>()>& store) { if (bytes == 0) return 0; + BreakProofs(); const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t { if (stored.second <= stored.first) return 0; ++tracker.generation; @@ -1736,6 +1754,7 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span<const std::byte> so } static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool complete) { + BreakProofs(); auto& tracker = Tracker(); auto lock = lockTracker(tracker); tracker.initialize(); @@ -2099,7 +2118,10 @@ std::atomic<std::uint64_t> gpuHoldSerial{0}; void GpuMutexType::acquired() { owner.store(ThreadToken(), std::memory_order_relaxed); - if (++depth == 1) gpuHoldSerial.fetch_add(1, std::memory_order_relaxed); + if (++depth == 1) { + gpuHoldSerial.fetch_add(1, std::memory_order_relaxed); + if (!proofThread) BreakProofs(); + } } bool GpuMutexType::try_lock() { @@ -2454,6 +2476,7 @@ void WriteChangedCommitted(std::uint64_t address, std::span<const std::byte> cur void WriteChanged(std::uint64_t address, std::span<const std::byte> current, std::span<const std::byte> original) { require(current.size() == original.size(), "write-back snapshot sizes differ"); + BreakProofs(); if (current.empty()) return; // A store: the hook sync it makes orders this write after a pending GPU write, whatever a read // site above it was doing (the [hooksync] line counts stores apart). diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index 253d832b7c..f0e0b777f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -18,6 +18,8 @@ #include <memory> #include <stdexcept> #include <vector> +#include <mutex> +#include <unordered_map> namespace AgcDriver { namespace { @@ -130,6 +132,23 @@ std::vector<std::unique_ptr<TPage>>& pagePool() { constexpr std::size_t PagePoolLimit = 64; +bool sharedPagePools() { + static const bool enabled = std::getenv("APS5_DRAW_PIPELINE") != nullptr; + return enabled; +} + +template<typename TPage> +struct SharedPagePool { + std::mutex mutex; + std::vector<std::unique_ptr<TPage>> pages; +}; + +template<typename TPage> +SharedPagePool<TPage>& sharedPagePool() { + static auto* pool = new SharedPagePool<TPage>(); + return *pool; +} + } void ShaderMemory::SetWaitedMsProvider(WaitedMsProvider provider) { @@ -144,6 +163,15 @@ ShaderMemory::ShaderMemory(std::span<const ShaderRecompiler::MemoryRegion> regio } ShaderMemory::~ShaderMemory() { + if (sharedPagePools() && !pages.empty()) { + auto& shared = sharedPagePool<Page>(); + std::lock_guard lock(shared.mutex); + for (const auto& [base, page] : pages) { + if (shared.pages.size() < 1024) shared.pages.emplace_back(page); + else delete page; + } + return; + } auto& pool = pagePool<Page>(); for (const auto& [base, page] : pages) { if (pool.size() < PagePoolLimit) pool.emplace_back(page); @@ -191,6 +219,14 @@ std::size_t ShaderMemory::pageAtOrAfter(std::uint64_t base) const { ShaderMemory::Page& ShaderMemory::addPage(std::size_t position, std::uint64_t base) { auto& pool = pagePool<Page>(); + if (pool.empty() && sharedPagePools()) { + auto& shared = sharedPagePool<Page>(); + std::lock_guard lock(shared.mutex); + for (std::size_t i = 0; i < 32 && !shared.pages.empty(); ++i) { + pool.push_back(std::move(shared.pages.back())); + shared.pages.pop_back(); + } + } std::unique_ptr<Page> owned; if (pool.empty()) { owned = std::make_unique<Page>(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index b79528bf72..06f3c528b6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -128,6 +128,9 @@ std::shared_ptr<std::vector<std::shared_ptr<ShaderResources>>> DrawCopiedWriters void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass); void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass); +std::vector<std::pair<std::uint64_t, std::uint64_t>> PipelinedColorMetadataPassWrites(const ColorMetadataPass& pass); +bool DrawPipelineEnabled(); +bool CommitTrims(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5321104605..eb48888af9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -48,6 +48,7 @@ std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span< bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second); bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words); +bool ShaderStoresThroughAddresses(const ShaderRecompiler::SharedSpirv& spirv); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. @@ -292,6 +293,7 @@ public: ProofFailure failure = ProofFailure::None; }; bool Revalidate(std::span<const CompiledShader> shaders, ProofReport* report = nullptr); + static bool EpochProofs(); bool Revalidate(const CompiledShader& shader, ProofReport* report = nullptr) { return Revalidate(std::span<const CompiledShader>(&shader, 1), report); } // The proof a recipe hit runs on its template (design_cpu_final M4): Revalidate by that name. bool ProveCurrent(std::span<const CompiledShader> shaders, ProofReport* report = nullptr) { return Revalidate(shaders, report); } @@ -632,6 +634,10 @@ private: // no image other than the object's own sources has results pending over its surfaces and // regions, so those checks are skipped (see fastRevalidate). std::uint64_t pendingSerialSeen = 0; + std::uint64_t provedEpoch = 0; + std::uint64_t provedProof = 0; + std::uint64_t provedPending = 0; + bool revalidateFull(std::span<const CompiledShader> shaders, ProofReport* report); // The import table's identity when the direct regions' serials were last proved. HostImportsProof importsProof; // FNV-1a offset basis: the hash of no data buffers (DataWordsHash). @@ -721,8 +727,25 @@ private: void noteUse(const Key& key); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { - std::uint64_t hash = 14695981039346656037ull; - for (const auto word : key) hash = (hash ^ word) * 1099511628211ull; + constexpr std::uint64_t Prime = 0x9e3779b97f4a7c15ull; + std::uint64_t lanes[4] = {0x243f6a8885a308d3ull, 0x13198a2e03707344ull, 0xa4093822299f31d0ull, 0x082efa98ec4e6c89ull}; + const auto* words = key.data(); + const auto count = key.size(); + std::size_t at = 0; + for (; at + 8u <= count; at += 8u) { + for (std::size_t lane = 0; lane < 4u; ++lane) { + const auto value = static_cast<std::uint64_t>(words[at + lane * 2u]) | static_cast<std::uint64_t>(words[at + lane * 2u + 1u]) << 32u; + lanes[lane] = (lanes[lane] ^ value) * Prime; + lanes[lane] ^= lanes[lane] >> 29u; + } + } + std::uint64_t hash = lanes[0] ^ (lanes[1] << 17u | lanes[1] >> 47u) ^ (lanes[2] << 31u | lanes[2] >> 33u) ^ (lanes[3] << 47u | lanes[3] >> 17u) ^ count; + for (; at < count; ++at) hash = (hash ^ words[at]) * Prime; + hash ^= hash >> 33u; + hash *= 0xff51afd7ed558ccdull; + hash ^= hash >> 33u; + hash *= 0xc4ceb9fe1a85ec53ull; + hash ^= hash >> 33u; return static_cast<std::size_t>(hash); } }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 87a9416e87..c9706d4528 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -370,6 +370,7 @@ public: // Keeps the image current with guest memory (see GuestMemory::CollectWrites). bool Refresh(); bool RefreshLayers(std::uint32_t first, std::uint32_t count); + inline static std::atomic<std::uint64_t> epochRefreshes{0}; static std::uint64_t SinglePassMoves(); static std::uint64_t SlabRetiles(); static std::uint64_t RefreshesProved(); @@ -383,6 +384,9 @@ public: VkDeviceSize AllocationBytes() const { return memoryBytes; } private: + std::atomic<std::uint64_t> provedEpoch{0}; + std::atomic<std::uint64_t> provedProof{0}; + std::atomic<std::uint64_t> provedPending{0}; StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); // The regions of every array layer, or of the tracked layers `layers` selects. std::vector<VkBufferImageCopy> CopyRegions(const std::vector<bool>* layers = nullptr) const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7926925ff3..dd6bef67f1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -723,11 +723,25 @@ std::uint32_t CachedFragmentOutputs(const Context& context, std::span<const Comp auto& key = validationKeyScratch(); const bool keyed = ValidationKey(context, shaders, state, key); static std::map<std::vector<std::uint64_t>, std::uint32_t> memo; + thread_local std::vector<std::uint64_t> lastKey; + thread_local std::uint32_t lastOutputs = 0; + thread_local bool lastValid = false; + const bool trims = keyed && CommitTrims(); + if (trims && lastValid && key == lastKey) { + memoized = true; + hit = true; + return lastOutputs; + } if (keyed) { memoized = true; std::lock_guard lock(validationMutex()); if (const auto found = memo.find(key); found != memo.end()) { hit = true; + if (trims) { + lastKey = key; + lastOutputs = found->second; + lastValid = true; + } return found->second; } } @@ -782,10 +796,16 @@ ResourceCache::Key DrawResourceKey(const Context& context, std::span<const Compi append64(reinterpret_cast<std::uint64_t>(context.device)); key.push_back(static_cast<std::uint32_t>(shaders.size())); for (const auto& shader : shaders) { - const auto at = key.size(); - key.push_back(0); - ShaderResources::AppendContentKey(shader, true, MovableBuffers(), key); - key[at] = static_cast<std::uint32_t>(key.size() - at - 1); + const auto sizeAt = key.size(); + key.push_back(0u); + auto& memo = shader.program->drawKeyMemo.words; + if (auto words = memo.load(std::memory_order_acquire); words != nullptr) { + key.insert(key.end(), words->begin(), words->end()); + } else { + ShaderResources::AppendContentKey(shader, true, MovableBuffers(), key); + memo.store(std::make_shared<const std::vector<std::uint32_t>>(key.begin() + static_cast<std::ptrdiff_t>(sizeAt + 1u), key.end()), std::memory_order_release); + } + key[sizeAt] = static_cast<std::uint32_t>(key.size() - sizeAt - 1u); } key.push_back(colorAttachments); if (ranges) { @@ -1694,6 +1714,7 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources> resources->WriteBackBuffers(); }); writers->push_back(resources); + GuestMemory::BreakProofs(); } else if (completion) { if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); @@ -3081,6 +3102,26 @@ void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass) { CopyDepthSample(context, pass.depthAddress, pass.samples, pass.sample, *resident, resident->StorageView(0, false), pass.extent); } +std::vector<std::pair<std::uint64_t, std::uint64_t>> PipelinedColorMetadataPassWrites(const ColorMetadataPass& pass) { + std::vector<std::pair<std::uint64_t, std::uint64_t>> writes; + if (pass.mode == ColorMetadataPass::Mode::Resolve) { + if (!pass.source.has_value() || pass.targets.size() != 1) return writes; + const auto& destination = pass.targets.front(); + if (destination.samples != 1u || destination.bytes == 0 || destination.cmaskAddress != 0) return writes; + writes.emplace_back(destination.address, destination.address + destination.bytes); + if (destination.dccAddress != 0) writes.emplace_back(destination.dccAddress, destination.dccAddress + std::max<std::size_t>(colorKeyCount(destination, destination.bytes), 1u)); + return writes; + } + if (pass.mode != ColorMetadataPass::Mode::EliminateFastClear && pass.mode != ColorMetadataPass::Mode::DccDecompress) return writes; + for (const auto& color : pass.targets) { + if (color.samples != 1u || color.bytes == 0) return {}; + writes.emplace_back(color.address, color.address + color.bytes); + if (color.cmaskAddress != 0) writes.emplace_back(color.cmaskAddress, color.cmaskAddress + std::max<std::size_t>(color.cmaskBytes, 1u)); + if (color.dccAddress != 0) writes.emplace_back(color.dccAddress, color.dccAddress + std::max<std::size_t>(colorKeyCount(color, color.bytes), 1u)); + } + return writes; +} + void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) { if (pass.mode == ColorMetadataPass::Mode::Resolve) { Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination"); @@ -3125,4 +3166,13 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) } } +bool DrawPipelineEnabled() { + static const bool enabled = std::getenv("APS5_DRAW_PIPELINE") != nullptr; + return enabled; +} + +bool CommitTrims() { + return DrawPipelineEnabled(); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9f03343a67..30fb7b1889 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3451,6 +3451,7 @@ void Recorder::AfterCompletions(std::uint64_t address, std::span<const std::byte } else { ++batch.completionLabelCount; completionLabels.fetch_add(1, std::memory_order_acq_rel); + GuestMemory::BreakProofs(); } // The table entry before the write note: the note bumps the generation a poller watches, and a // poller that sees the bump then finds the label without the GPU mutex. @@ -3467,6 +3468,7 @@ bool Recorder::AfterRecordedWork(std::function<void()> action) { batch.completions.push_back(std::move(action)); ++batch.completionLabelCount; completionLabels.fetch_add(1, std::memory_order_acq_rel); + GuestMemory::BreakProofs(); if (&batch == open.get() && activeRecorder == this && pendingLabelSince.load(std::memory_order_relaxed) == NoPendingLabel) { pendingLabelSince.store(std::chrono::steady_clock::now().time_since_epoch().count(), std::memory_order_release); } @@ -3494,6 +3496,7 @@ void Recorder::NoteWrittenBack(std::uint64_t address, std::size_t bytes) { label.counted = true; ++batch.completionLabelCount; completionLabels.fetch_add(1, std::memory_order_acq_rel); + GuestMemory::BreakProofs(); completionLabelsCountedLate.fetch_add(1, std::memory_order_relaxed); recorder->markBehindCompletion(batch, label.begin, label.end); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 2915b76a5a..10d27703c7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -93,6 +93,10 @@ bool storesThroughAddressesCached(const ShaderRecompiler::SharedSpirv& spirv) { } +bool ShaderStoresThroughAddresses(const ShaderRecompiler::SharedSpirv& spirv) { + return storesThroughAddressesCached(spirv); +} + bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words) { if (words.size() < 5 || words[0] != spv::MagicNumber) return true; std::unordered_set<std::uint32_t> pointerTypes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 3859fc17d0..3b038b1925 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -244,6 +244,46 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st read.size = static_cast<std::uint32_t>(bytes.size()); }; if (reads != nullptr) reads->reserve(reads->size() + 2u * shader.num_input_semantics); + std::array<std::uint32_t, ShaderRecompiler::ShaderVertexStageInfo::MaxResources> attribWords{}; + std::array<std::array<std::uint32_t, 4>, ShaderRecompiler::ShaderVertexStageInfo::MaxResources> sharpWords{}; + if (!staticAbi) { + std::uint32_t firstSemantic = ~0u, lastSemantic = 0; + for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { + firstSemantic = std::min<std::uint32_t>(firstSemantic, semantics[i].semantic); + lastSemantic = std::max<std::uint32_t>(lastSemantic, semantics[i].semantic); + } + const auto readWords = [](std::uint64_t base, std::uint32_t stride, std::uint32_t first, std::uint32_t last, std::vector<std::uint32_t>& words, auto&& wanted) { + const auto wordsPer = stride / 4u; + words.assign(static_cast<std::size_t>(last - first + 1u) * wordsPer, 0u); + const auto begin = base + static_cast<std::uint64_t>(first) * stride; + if (AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(begin), words.size() * 4u)) { + AgcDriver::GuestMemory::Read(begin, std::as_writable_bytes(std::span(words)), 4); + return; + } + for (std::uint32_t entry = first; entry <= last; ++entry) { + if (!wanted(entry)) continue; + AgcDriver::GuestMemory::Read(base + static_cast<std::uint64_t>(entry) * stride, std::as_writable_bytes(std::span(words).subspan(static_cast<std::size_t>(entry - first) * wordsPer, wordsPer)), 4); + } + }; + thread_local std::vector<std::uint32_t> table; + readWords(attribTableAddr, 4u, firstSemantic, lastSemantic, table, [&](std::uint32_t semantic) { return std::any_of(semantics.begin(), semantics.begin() + shader.num_input_semantics, [&](const ShaderSemantic& candidate) { return candidate.semantic == semantic; }); }); + std::uint32_t firstIndex = ~0u, lastIndex = 0; + for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { + attribWords[i] = table[semantics[i].semantic - firstSemantic]; + const auto index = attribWords[i] & 0x1fu; + if (index >= ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex buffer index exceeds the supported domain"); + firstIndex = std::min(firstIndex, index); + lastIndex = std::max(lastIndex, index); + } + if (shader.num_input_semantics != 0) { + thread_local std::vector<std::uint32_t> sharps; + readWords(bufferTableAddr, 16u, firstIndex, lastIndex, sharps, [&](std::uint32_t index) { return std::any_of(attribWords.begin(), attribWords.begin() + shader.num_input_semantics, [&](std::uint32_t word) { return (word & 0x1fu) == index; }); }); + for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { + const auto index = attribWords[i] & 0x1fu; + std::copy_n(sharps.begin() + static_cast<std::ptrdiff_t>((index - firstIndex) * 4u), 4, sharpWords[i].begin()); + } + } + } for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { const auto& semantic = semantics[i]; if (semantic.static_vb_index == 1 || semantic.static_attribute == 1) throw std::runtime_error("AGC graphics: statically bound vertex attributes are not implemented"); @@ -256,10 +296,9 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st } std::array<std::byte, 4> attribWordBytes{}; const auto attribWordAddress = attribTableAddr + static_cast<std::uint64_t>(semantic.semantic) * 4u; - AgcDriver::GuestMemory::Read(attribWordAddress, attribWordBytes, 4); + std::memcpy(attribWordBytes.data(), &attribWords[i], 4); record(attribWordAddress, attribWordBytes); - std::uint32_t attribWord; - std::memcpy(&attribWord, attribWordBytes.data(), 4); + const std::uint32_t attribWord = attribWords[i]; const auto index = attribWord & 0x1fu; const auto format = (attribWord >> 5u) & 0x1ffu; const auto offset = (attribWord >> 14u) & 0xfffu; @@ -267,7 +306,7 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st if (index >= ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex buffer index exceeds the supported domain"); std::array<std::byte, 16> sharpBytes{}; const auto sharpAddress = bufferTableAddr + static_cast<std::uint64_t>(index) * 16u; - AgcDriver::GuestMemory::Read(sharpAddress, sharpBytes, 4); + std::memcpy(sharpBytes.data(), sharpWords[i].data(), 16); record(sharpAddress, sharpBytes); std::array<std::uint32_t, 4> sharp{}; std::memcpy(sharp.data(), sharpBytes.data(), 16); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d8a6109735..80b305f975 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3281,7 +3281,54 @@ const ShaderRecompiler::DescriptorBinding* ShaderResources::tableImages(std::spa return nullptr; } +bool ShaderResources::EpochProofs() { + return DrawPipelineEnabled(); +} + +namespace { +void noteEpochProof(bool reused, bool verified, bool failed) { + static std::atomic<std::uint64_t> reuses{0}, proofs{0}, checks{0}, failures{0}; + if (verified) checks.fetch_add(1, std::memory_order_relaxed); + if (failed) failures.fetch_add(1, std::memory_order_relaxed); + const auto count = (reused ? reuses : proofs).fetch_add(1, std::memory_order_relaxed) + 1u; + static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr || std::getenv("APS5_VERIFY_DRAW_PIPELINE") != nullptr; + if (!report) return; + if (count % 262144u == 0u || (count & (count - 1u)) == 0u) std::fprintf(stderr, "[epoch-proofs] resource proofs: %llu reused the epoch's, %llu proved; verify: %llu checked, %llu failed; storage refreshes reused %llu\n", static_cast<unsigned long long>(reuses.load()), static_cast<unsigned long long>(proofs.load()), static_cast<unsigned long long>(checks.load()), static_cast<unsigned long long>(failures.load()), static_cast<unsigned long long>(StorageTexture::epochRefreshes.load())); +} +} + bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofReport* report) { + if (EpochProofs()) { + const auto epoch = GuestMemory::CollectEpoch(); + const auto proof = GuestMemory::ProofSerial(); + const auto pending = StorageTexture::PendingSerial(); + if (epoch != 0 && provedEpoch == epoch && provedProof == proof && provedPending == pending) { + static const bool verify = std::getenv("APS5_VERIFY_DRAW_PIPELINE") != nullptr; + thread_local std::uint64_t reuses = 0; + if (verify && ++reuses % 64u == 0u) { + const bool ok = revalidateFull(shaders, report); + noteEpochProof(true, true, !ok); + return ok; + } + if (report != nullptr) *report = {ProofPath::Full, ProofFailure::None}; + noteEpochProof(true, false, false); + return true; + } + const bool ok = revalidateFull(shaders, report); + noteEpochProof(false, false, false); + if (ok) { + provedEpoch = epoch; + provedProof = proof; + provedPending = pending; + } else { + provedEpoch = 0; + } + return ok; + } + return revalidateFull(shaders, report); +} + +bool ShaderResources::revalidateFull(std::span<const CompiledShader> shaders, ProofReport* report) { if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -5595,6 +5642,7 @@ void ShaderResources::WriteBack() { } catch (const BdaUnalignedAccess&) { unaligned = std::current_exception(); } + if (!guestMemory.Writes().empty() || std::find(storageWritten.begin(), storageWritten.end(), true) != storageWritten.end() || (usesBda && !bdaReadOnly)) GuestMemory::BreakProofs(); if (!SkipWriteBack()) { for (std::size_t index = 0; index < storageTextures.size(); ++index) { if (!storageWritten[index] || storageTextures[index] == nullptr) continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1e10ed8d97..ef8c717fd3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" // For the declaration of PendingStorageOverlaps, defined below. #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -2057,10 +2058,25 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) const bool profile = LookupOutcomes::Profiled(); auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const bool scoped = firstLayer != 0 || lastLayer != trackedLayers; + const bool epochProofs = ShaderResources::EpochProofs(); + const auto epoch = epochProofs ? GuestMemory::CollectEpoch() : 0; + if (epoch != 0 && provedEpoch.load(std::memory_order_relaxed) == epoch && provedProof.load(std::memory_order_relaxed) == GuestMemory::ProofSerial() && provedPending.load(std::memory_order_relaxed) == PendingSerial()) { + NoteProved(); + refreshesProved.fetch_add(1, std::memory_order_relaxed); + epochRefreshes.fetch_add(1, std::memory_order_relaxed); + return true; + } + const auto proofBefore = GuestMemory::ProofSerial(); + const auto pendingBefore = PendingSerial(); CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)); if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); if (refreshProved()) { layerBehind.clear(); + if (epoch != 0) { + provedEpoch.store(epoch, std::memory_order_relaxed); + provedProof.store(proofBefore, std::memory_order_relaxed); + provedPending.store(pendingBefore, std::memory_order_relaxed); + } NoteProved(); refreshesProved.fetch_add(1, std::memory_order_relaxed); if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start); @@ -3730,11 +3746,19 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast<unsigned long long>(descriptor.baseAddress), first, count, static_cast<unsigned long long>(generation)); ++writes; static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; - std::vector<bool> unsaved(trackedLayers, false); + std::vector<bool> unsaved; bool anyUnsaved = false; - for (std::uint32_t layer = first; layer < first + count; ++layer) { - unsaved[layer] = !layerPending[layer] && !layerOriginal[layer] && layerGeneration[layer] == 0; - anyUnsaved = anyUnsaved || unsaved[layer]; + const bool trims = CommitTrims(); + if (trims) { + for (std::uint32_t layer = first; layer < first + count && !anyUnsaved; ++layer) anyUnsaved = !layerPending[layer] && !layerOriginal[layer] && layerGeneration[layer] == 0; + } + if (!trims || anyUnsaved) { + unsaved.assign(trackedLayers, false); + anyUnsaved = false; + for (std::uint32_t layer = first; layer < first + count; ++layer) { + unsaved[layer] = !layerPending[layer] && !layerOriginal[layer] && layerGeneration[layer] == 0; + anyUnsaved = anyUnsaved || unsaved[layer]; + } } if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) captureGuestBytes(&unsaved); for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; @@ -4252,8 +4276,10 @@ bool StorageTexture::AnyPendingOverlaps(std::span<const std::pair<std::uint64_t, } return false; }; - for (const auto* texture : pending.textures) { - if (overlapsAny(texture)) return true; + if (std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.second > range.first && pending.textures.MayOverlap(range.first, static_cast<std::size_t>(range.second - range.first)); })) { + for (const auto* texture : pending.textures) { + if (overlapsAny(texture)) return true; + } } for (const auto* texture : pending.flushing) { if (overlapsAny(texture)) return true; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index cd2998c06b..e33a4d0c7a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -492,11 +492,11 @@ std::size_t PublishShadow(std::uint64_t address, std::size_t bytes, PublishScope return units; } -bool AnyShadowedOverlaps(std::uint64_t address, std::size_t bytes) { - if (!UnitShadowEnabled() || bytes == 0) return false; +namespace { + +bool anyShadowedLocked(const Shadows& registry, std::uint64_t address, std::size_t bytes) { + if (bytes == 0) return false; const auto end = bytes > std::numeric_limits<std::uint64_t>::max() - address ? std::numeric_limits<std::uint64_t>::max() : address + bytes; - auto& registry = Registry(); - std::lock_guard lock(registry.mutex); return anyOverlapping(registry, address, end, [&](const std::shared_ptr<UnitShadow>& shadow) { if (shadow->liveUnits == 0) return false; const auto begin = std::max(address, shadow->importBase); @@ -508,10 +508,21 @@ bool AnyShadowedOverlaps(std::uint64_t address, std::size_t bytes) { }); } +} + +bool AnyShadowedOverlaps(std::uint64_t address, std::size_t bytes) { + if (!UnitShadowEnabled() || bytes == 0) return false; + auto& registry = Registry(); + std::lock_guard lock(registry.mutex); + return anyShadowedLocked(registry, address, bytes); +} + bool AnyShadowedOverlaps(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) { - if (!UnitShadowEnabled()) return false; + if (!UnitShadowEnabled() || ranges.empty()) return false; + auto& registry = Registry(); + std::lock_guard lock(registry.mutex); for (const auto& [begin, end] : ranges) { - if (end > begin && AnyShadowedOverlaps(begin, static_cast<std::size_t>(end - begin))) return true; + if (end > begin && anyShadowedLocked(registry, begin, static_cast<std::size_t>(end - begin))) return true; } return false; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 8a0b4d0ddc..569be1e0ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4642,6 +4642,9 @@ bool unitShadowTests(const Device& device, Recorder& recorder) { retile(*half, unit3, unit / 2, 0x22, true); Require(AnyShadowedOverlaps(unit3, 1) && AnyShadowedOverlaps(unit4 - 1, 1) && AnyShadowedOverlaps(unit3 + 1000, 64), "a fresh unit is not seen as shadowed"); Require(!AnyShadowedOverlaps(unit3 - 1, 1) && !AnyShadowedOverlaps(unit4, unit) && !AnyShadowedOverlaps(address, 3 * unit), "a range outside the unit is seen as shadowed"); + const std::array<std::pair<std::uint64_t, std::uint64_t>, 2> outside{{{unit3 - 8, unit3}, {unit4, unit4 + unit}}}; + const std::array<std::pair<std::uint64_t, std::uint64_t>, 2> reaching{{{unit3 - 8, unit3}, {unit4 - 4, unit4}}}; + Require(!AnyShadowedOverlaps(outside) && AnyShadowedOverlaps(reaching), "a set of ranges around the fresh unit was answered wrongly"); { // An upload's runs over units 2..4 split at unit 3's freshness; the fresh piece reads the slab. const std::pair<std::uint64_t, std::uint64_t> run{2 * unit, 5 * unit}; @@ -6420,6 +6423,9 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); image->MarkDirty(); + const std::array<std::pair<std::uint64_t, std::uint64_t>, 2> beside{{{address - 64, address - 16}, {address + surfaceBytes, address + surfaceBytes + 64}}}; + const std::array<std::pair<std::uint64_t, std::uint64_t>, 2> touching{{{address - 64, address - 16}, {address + surfaceBytes - 4, address + surfaceBytes}}}; + Require(!StorageTexture::AnyPendingOverlaps(beside) && StorageTexture::AnyPendingOverlaps(touching), "(c) " + what + ": a set of ranges beside the pending image or touching its last word was answered wrongly"); for (std::size_t i = 0; i < 4096; ++i) memory[offset + i] = pattern(offset + i); if (changed) memory[offset + 16] = static_cast<std::uint8_t>(~pattern(offset + 16)); AgcDriver::GuestMemory::CollectWritesUncached(blockAddress, bytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp index 13d67731a9..2da8fc562c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -177,6 +177,21 @@ void UnreadUserWordsKeepTheStage(bool verify) { Require(counters.dataHits == moved.dataHits + 1, "a draw that pointed the pixel stage at another buffer descriptor was not a data-only hit: " + Counts(counters)); Require(counters.dataStagesCompiled - moved.dataStagesCompiled >= 1, "a draw that pointed the pixel stage at another buffer descriptor reused the pixel stage: " + Counts(counters)); if (!verify) Require(counters.dataStagesReused - moved.dataStagesReused == 1, "a draw that changed only the pixel stage's descriptor pointer did not reuse the vertex stage: " + Counts(counters)); + + const bool refresh = std::getenv("APS5_DRAW_PIPELINE") != nullptr; + for (const std::uint32_t unread : {0x33u, 0x44u}) { + commands.clear(); + PixelUserData(commands, second, unread); + Append(commands, 0x2d, {3, 2}); + const auto last = counters; + counters = Submit(std::move(commands)); + Require(counters.dataHits == last.dataHits + 1 && counters.inserts == last.inserts, "a draw at the moved pointer with a new unread word: " + Counts(counters)); + if (verify) continue; + const auto compiled = counters.dataStagesCompiled - last.dataStagesCompiled; + const bool expectReuse = refresh && unread == 0x44u; + Require(compiled == (expectReuse ? 0u : 1u), std::string(expectReuse ? "after a refresh, the draw at the moved pointer compiled its pixel stage again: " : "the draw at the moved pointer did not compile its pixel stage: ") + Counts(counters)); + Require(counters.shapeUpdates - last.shapeUpdates == (refresh && unread == 0x33u ? 1u : 0u), "shape refreshes: " + std::to_string(counters.shapeUpdates - last.shapeUpdates) + " (" + Counts(counters) + ")"); + } } unsigned BrightestColorByte() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp index c0a50d0476..61a0e679ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp @@ -6,6 +6,7 @@ #include <bit> #include <cstdint> #include <cstdio> +#include <cstdlib> #include <exception> #include <initializer_list> #include <iostream> @@ -92,6 +93,7 @@ void NamesShaderAndDescriptor() { Require(message.find("buffer descriptor uses an unsupported type") != std::string::npos, "draw failed for another reason: " + message); Require(message.find(shader) != std::string::npos, std::string("draw error does not name ") + shader + ": " + message); Require(message.find(words) != std::string::npos, std::string("draw error does not name ") + words + ": " + message); + if (std::getenv("APS5_DRAW_PIPELINE") != nullptr) Require(message.find("draw at submission ") != std::string::npos && message.find("(finished on the draw finisher)") != std::string::npos, "a draw finished on the draw finisher does not name its submission and offset: " + message); return; } throw std::runtime_error("a pixel shader read a buffer through a descriptor of type 2"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp new file mode 100644 index 0000000000..7fa9eb1f07 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include <algorithm> +#include <array> +#include <atomic> +#include <chrono> +#include <thread> +#include <bit> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <mutex> +#include <span> +#include <stdexcept> +#include <string> +#include <utility> +#include <vector> +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif + +namespace { + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 4; +constexpr std::uint32_t Stale = 0x5a5a5a5au; +constexpr std::uint32_t Clear = 0x11223344u; + +constexpr std::size_t MemoryBytes = 65536; + +std::byte* allocateMemory() { +#ifdef _WIN32 + auto* memory = static_cast<std::byte*>(VirtualAlloc(nullptr, MemoryBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* memory = static_cast<std::byte*>(std::aligned_alloc(MemoryBytes, MemoryBytes)); +#endif + if (memory == nullptr) throw std::runtime_error("pipelined metadata pass: cannot allocate the surface memory"); + std::memset(memory, 0, MemoryBytes); + return memory; +} + +std::byte* const Memory = allocateMemory(); +const std::span<std::uint32_t, Width * Height> Surface{reinterpret_cast<std::uint32_t*>(Memory), Width * Height}; +const std::span<std::uint8_t, 4096> Cmask{reinterpret_cast<std::uint8_t*>(Memory + 4096), 4096}; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined metadata pass: " + what); +} + +void setContext(std::vector<std::uint32_t>& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0016900u, offset, value}); +} + +void dmaFill(std::vector<std::uint32_t>& words, std::uint32_t data, const void* destination, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(destination); + words.insert(words.end(), {0xc0055000u, 2u << 29u, data, 0u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), bytes}); +} + +void eliminate(std::vector<std::uint32_t>& words) { + const auto surface = reinterpret_cast<std::uintptr_t>(Surface.data()); + const auto cmask = reinterpret_cast<std::uintptr_t>(Cmask.data()); + words.insert(words.end(), {0xc0017900u, 0x242u, 4u}); + words.insert(words.end(), {0xc0017600u, 0x008u, 0x100u, 0xc0017600u, 0x009u, 0u}); + const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{ + {0x2d5, 0x2000}, {0x1b6, 0}, {0x1b8, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0020}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028u | 0x2000u}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000u | (0x18u << 19u) | 0x4000000u}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast<std::uint32_t>(surface >> 8u)}, {0x390, static_cast<std::uint32_t>(surface >> 40u)}, + {0x31f, static_cast<std::uint32_t>(cmask >> 8u)}, {0x398, static_cast<std::uint32_t>(cmask >> 40u)}, + {0x323, Clear}, {0x324, 0}, + {0x0, 0}, {0xb4, 0}, + {0x10f, std::bit_cast<std::uint32_t>(32.0f)}, {0x110, std::bit_cast<std::uint32_t>(32.0f)}, + {0x111, std::bit_cast<std::uint32_t>(-2.0f)}, {0x112, std::bit_cast<std::uint32_t>(2.0f)}, + {0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)}, + }; + for (const auto& [offset, value] : context) setContext(words, offset, value); + words.insert(words.end(), {0xc0002f00u, 1u}); + words.insert(words.end(), {0xc0012d00u, 3u, 2u}); +} + +void submit(std::vector<std::uint32_t>& words) { + Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void expectEliminated(const std::string& what) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(Surface.data()), Surface.size_bytes(), nullptr, "test"); + } + for (std::size_t i = 0; i < Surface.size(); ++i) check(Surface[i] == Clear, what + ": texel " + std::to_string(i) + " holds " + std::to_string(Surface[i])); + const auto cmaskBytes = AgcDriver::Graphics::CmaskLayout(Width, Height).Bytes(); + for (std::size_t i = 0; i < cmaskBytes; ++i) check(Cmask[i] == 0xffu, what + ": the CMASK was not left expanded"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); + _putenv_s("APS5_DRAW_PIPELINE", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); + setenv("APS5_DRAW_PIPELINE", "1", 1); +#endif + try { + { + GuestAllocations::Mutation mutation; + mutation.Add(Surface.data(), Surface.size_bytes(), true, true, true); + mutation.Add(Cmask.data(), Cmask.size(), true, true, true); + } + using AgcDriver::DriverDetail::DrawPipeline; + std::fill(Surface.begin(), Surface.end(), Stale); + std::fill(Cmask.begin(), Cmask.end(), std::uint8_t{0}); + std::vector<std::uint32_t> first; + eliminate(first); + submit(first); + expectEliminated("the first eliminate"); + const auto before = DrawPipeline::PipelinedMetadataPasses(); + std::vector<std::uint32_t> second; + std::atomic<bool> release{false}; + DrawPipeline::Queue0().Enqueue([&release] { + while (!release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {}); + std::thread releaser([&release] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + release.store(true, std::memory_order_release); + }); + dmaFill(second, Stale, Surface.data(), static_cast<std::uint32_t>(Surface.size_bytes())); + dmaFill(second, 0, Cmask.data(), static_cast<std::uint32_t>(Cmask.size())); + eliminate(second); + submit(second); + releaser.join(); + check(DrawPipeline::PipelinedMetadataPasses() == before + 1u, "the second eliminate was not ordered with the commits (" + std::to_string(DrawPipeline::PipelinedMetadataPasses() - before) + " pipelined)"); + expectEliminated("the eliminate after the queued stores"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(Surface.data()); + mutation.Remove(Cmask.data()); + } + LibcRunShutdown_nid_postfix(); + std::puts("pipelined metadata pass tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ProgramWritesMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProgramWritesMemory.cpp new file mode 100644 index 0000000000..9efc1296de --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProgramWritesMemory.cpp @@ -0,0 +1,48 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include <cstdint> +#include <cstdio> +#include <initializer_list> +#include <iostream> +#include <stdexcept> +#include <string> +#include <vector> + +namespace { + +constexpr std::uint32_t EndProgram = 0xbf810000u; + +bool writes(std::initializer_list<std::uint32_t> words) { + AgcDriver::DriverDetail::ShaderSnapshot snapshot{}; + snapshot.code.assign(words.begin(), words.end()); + return AgcDriver::DriverDetail::ProgramWritesMemory(snapshot, 0); +} + +void expect(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("program memory writes: " + what); +} + +} + +int main() { + try { + expect(!writes({0xf800000fu, 0x00000000u, EndProgram}), "an export-only program writes memory"); + expect(!writes({0xe0300000u, 0x80000100u, EndProgram}), "a buffer load writes memory"); + expect(writes({0xe0700000u, 0x80000100u, EndProgram}), "a buffer store does not write memory"); + expect(writes({0xf0200f00u, 0x00000000u, EndProgram}), "an image store does not write memory"); + expect(writes({0xd8360000u, 0x00000000u, EndProgram}), "a GDS write does not write memory"); + expect(writes({0xbe802000u, EndProgram}), "an indirect jump is treated as reading only"); + expect(writes({0x7e000280u}), "code that never ends decodes as reading only"); + AgcDriver::DriverDetail::ShaderSnapshot store{}; + store.code = {0xe0700000u, 0x80000100u, EndProgram}; + expect(AgcDriver::DriverDetail::ProgramWritesMemory(store, 0) && AgcDriver::DriverDetail::ProgramWritesMemory(store, 0), "the cached answer differs"); + expect(!AgcDriver::DriverDetail::ProgramWritesMemory(store, 2), "the program from an offset past the store writes memory"); + LibcRunShutdown_nid_postfix(); + std::puts("program memory writes tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 2362ce871a..8d4d50baff 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -87,7 +87,7 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 280, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 216, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 528, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 544, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 32e2509783..8676f9acff 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -130,6 +130,7 @@ flowchart LR - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=<n>` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. +- `APS5_DRAW_PIPELINE=1` (off by default, meant with `APS5_PIPELINED_DRAWS=32 APS5_PIPELINE_DISPATCH=1`) takes more of a draw off the worker. A `DrawFinisher` thread runs the second half of a direct pipelined draw (stage materialization, the draw cache insert and the hand-off to the committer) while the worker reads the next packets; the worker drains it before the next draw's cache lookup, before packets other than register writes and draws, and before guest reads, and finishes in place a draw whose stages may write memory (`ProgramWritesMemory`) or write through the BDA page table. The committer skips the fragment-output validation and storage-image layer scans whose inputs match its last commit (`CommitTrims`), and a resource or storage image proved current keeps its proof for the rest of the collect epoch until `GuestMemory::BreakProofs` (a CPU write, a write-back, a recorded write) moves `ProofSerial`. Color metadata passes go through the pipeline with their write ranges (`PipelinedColorMetadataPassWrites`) instead of draining it. A data-only hit whose shape entry belongs to an older draw replaces that entry in place at the second sight of the same read user words. On Linux the main image's pages get a page table entry, and capture pages are pooled across threads. `APS5_VERIFY_DRAW_PIPELINE=1` runs a full check next to 1 in 64 reused proofs and prints `[epoch-proofs]` counts. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. - `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. Explicit ABI resolution still prepares its requested stages. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index dd75c7f25f..e56ecbaa7f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -119,6 +119,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DRAW_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | treats draw packets as drains, waiting for them instead of recording ahead | | `APS5_DRAW_INPUTS_IN_PLACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | binds draw vertex and index ranges in place from host imports also with NVIDIA's Windows driver, which copies them by default | | `APS5_DRAW_KEY_ALL_USER_WORDS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | includes all 32 user words in a draw's register key instead of the used count | +| `APS5_DRAW_PIPELINE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | runs the second half of a pipelined draw on a draw finisher thread and keeps proofs for the collect epoch | | `APS5_DRAW_SNAPSHOT_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 64 | upper bound in MiB on the snapshot bytes a draw's bindings may take | | `APS5_DRAW_TRANSITIONS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps the per-draw upload and download barriers and layout transitions instead of the lean recorded path | | `APS5_DRAW_UPLOAD_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 4096 | largest draw snapshot in bytes copied into the per-recorder upload chunk; 0 turns the chunk off | @@ -432,6 +433,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_VERIFY_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | recomputes a dispatch data hit and aborts on a disagreement | | `APS5_VERIFY_DRAW_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | recomputes a draw data hit and aborts on a disagreement | | `APS5_VERIFY_DRAW_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes every draw-cache hit through the full path and checks it | +| `APS5_VERIFY_DRAW_PIPELINE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | runs a full check next to 1 in 64 reused proofs and prints `[epoch-proofs]` counts | | `APS5_VERIFY_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes a draw recipe instead of using the cache and checks the result | | `APS5_VERIFY_DRAW_SNAPSHOTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | re-reads a reused draw snapshot and counts the ones that differ | | `APS5_VERIFY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | serves known values through the hook too and counts mismatches | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ed15035dc9..67b24f696c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -236,3 +236,7 @@ - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. - [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws + +### Opt-in draw paths + +- [Draw finisher](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp) (`APS5_DRAW_PIPELINE`) - opt-in. The Linux main-image page table it adds matches what Windows already does by default. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index eadd1e54ec..988659c9ae 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -293,6 +293,10 @@ Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` d Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. +### Draw pipeline + +`APS5_DRAW_PIPELINE=1` (off by default) moves more of each draw off the thread that reads the title's command buffers: a second thread finishes each draw while the next one is read, the thread that records draws skips work whose inputs did not change, and checks that a draw's resources are still current are reused until the title or the GPU writes memory. It is meant with `APS5_PIPELINED_DRAWS=32 APS5_PIPELINE_DISPATCH=1`. The picture does not change; if it does, that is a bug, and a run without the switch shows whether it is the cause. `APS5_VERIFY_DRAW_PIPELINE=1` runs the full checks next to the reused ones, at a cost in speed, and prints `[epoch-proofs]` lines with how many differed (any nonzero count is a bug). `APS5_PROFILE_DRAW=1` finishes every draw on the reading thread, so a run with it does not measure this switch. To see the hand-off without that, set `APS5_PIPELINE_STATS` (any value; `1` also turns on Vulkan pipeline statistics, which cost time): every 10 s a `[finisher]` line counts the draws handed to the second thread and those finished on the reading thread, and `[finisher-held]` lines count the draws held back by why their writes are not known. + ### Texture cache budget Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. From 1aacde9a68312a2295d1d5adcbbe91ab89211771 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:32:12 -0700 Subject: [PATCH 1103/1360] fix(agc): keep the draw pipeline's thread-local scratch in HostThreadLocal On Windows a thread_local destructor in a module runs after emulated TLS has freed the object, and prx_thread_local_destructors refuses libSceAgcDriver.prx for it. The draw pipeline of upstream PR 6946 (Raghav314, f8192a64b3) added such scratch objects: DrawFinisher::Enqueue's and DrawPipeline::Enqueue's reclaim lists, CachedFragmentOutputs' last key and outputs, DecodeVertexStageInfo's attribute and buffer tables, and PrepareDrawBindings' pool size counts. They now live in HostThreadLocal, as the driver's other per-thread scratch does; on other hosts that is the same thread_local. --- .../Execution/src/Driver/Draw/DrawPipeline.cpp | 7 +++++-- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +++- .../prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp | 7 +++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index cbd02537d9..cd6c6f9da1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include <algorithm> #include <array> #include <limits> @@ -149,7 +150,8 @@ void DrawFinisher::Enqueue(Job job) { void DrawFinisher::Enqueue(Job job, Pending held) { finisherProducer = true; constexpr std::size_t depth = 16; - thread_local std::vector<Job> reclaim; + struct ReclaimStorage {}; + auto& reclaim = HostThreadLocal<std::vector<Job>, ReclaimStorage>(); reclaim.clear(); std::unique_lock lock(mutex); if (!ran.empty()) std::swap(ran, reclaim); @@ -266,7 +268,8 @@ void DrawPipeline::rethrowFailure() { void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress, std::vector<std::byte> labelBytes, std::vector<Range> reads, bool draw) { if (DrawFinisher::Busy() && !DrawFinisher::OnFinisherThread()) DrawFinisher::Get().Drain(); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - thread_local std::vector<Item> reclaim; + struct ReclaimStorage {}; + auto& reclaim = HostThreadLocal<std::vector<Item>, ReclaimStorage>(); reclaim.clear(); const auto lockStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; std::unique_lock lock(mutex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index dd6bef67f1..43b52cbfe6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -20,6 +20,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" #include "ThreadOwned.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include <algorithm> #include <array> #include <atomic> @@ -723,7 +724,8 @@ std::uint32_t CachedFragmentOutputs(const Context& context, std::span<const Comp auto& key = validationKeyScratch(); const bool keyed = ValidationKey(context, shaders, state, key); static std::map<std::vector<std::uint64_t>, std::uint32_t> memo; - thread_local std::vector<std::uint64_t> lastKey; + struct LastKeyStorage {}; + auto& lastKey = HostThreadLocal<std::vector<std::uint64_t>, LastKeyStorage>(); thread_local std::uint32_t lastOutputs = 0; thread_local bool lastValid = false; const bool trims = keyed && CommitTrims(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 3b038b1925..30ea580803 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include <algorithm> #include <array> #include <cstddef> @@ -265,7 +266,8 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st AgcDriver::GuestMemory::Read(base + static_cast<std::uint64_t>(entry) * stride, std::as_writable_bytes(std::span(words).subspan(static_cast<std::size_t>(entry - first) * wordsPer, wordsPer)), 4); } }; - thread_local std::vector<std::uint32_t> table; + struct TableStorage {}; + auto& table = HostThreadLocal<std::vector<std::uint32_t>, TableStorage>(); readWords(attribTableAddr, 4u, firstSemantic, lastSemantic, table, [&](std::uint32_t semantic) { return std::any_of(semantics.begin(), semantics.begin() + shader.num_input_semantics, [&](const ShaderSemantic& candidate) { return candidate.semantic == semantic; }); }); std::uint32_t firstIndex = ~0u, lastIndex = 0; for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { @@ -276,7 +278,8 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st lastIndex = std::max(lastIndex, index); } if (shader.num_input_semantics != 0) { - thread_local std::vector<std::uint32_t> sharps; + struct SharpsStorage {}; + auto& sharps = HostThreadLocal<std::vector<std::uint32_t>, SharpsStorage>(); readWords(bufferTableAddr, 16u, firstIndex, lastIndex, sharps, [&](std::uint32_t index) { return std::any_of(attribWords.begin(), attribWords.begin() + shader.num_input_semantics, [&](std::uint32_t word) { return (word & 0x1fu) == index; }); }); for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { const auto index = attribWords[i] & 0x1fu; From a220da9f5763469d08c27af49e89e13dc6d91fd3 Mon Sep 17 00:00:00 2001 From: raghav314 <samraghav2@gmail.com> Date: Sat, 17 Oct 2026 20:01:13 +0530 Subject: [PATCH 1104/1360] fix(agc): drain the draw finisher before the committer stops at exit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 0557a7af52..e81b06092c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -161,6 +161,10 @@ void Driver::stop() { for (auto& [queue, worker] : workers) { if (worker.thread.joinable()) worker.thread.join(); } + try { + DrawFinisher::Get().Drain(); + } catch (...) { + } DrawPipeline::Queue0().Stop(); StopWorkerSampler(); CloseRegistrationPreparation(); From 3ded7d326976ff5f31f1cc8b269575763a520303 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:22:19 -0700 Subject: [PATCH 1105/1360] fix(recompiler): hold the result-key memo in AtomicSharedPtr so libc++ builds ResultKeyMemo declared std::atomic<std::shared_ptr<...>>. libc++ has no atomic<shared_ptr> specialization, so with Apple clang the recompiler headers stop at std::atomic's trivially-copyable static assertion (request_serializer_tests in the macOS relinker build). The memo now uses AtomicSharedPtr from libc, which is that specialization where the library provides it and a locked shared_ptr with the same load and store otherwise. The header joins the shader disk cache key's sources, and request_serializer_tests gets core/libs on its include path. --- CMakeLists.txt | 1 + core/shader/recompiler/CMakeLists.txt | 4 +++- core/shader/recompiler/Recompiler.hpp | 12 ++++++++++++ tools/tests/test_shader_cache_runtime_sources.py | 3 +++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eaa66c8db..b4bb291af5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp) target_include_directories(request_serializer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/libs ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include) add_test(NAME request_serializer COMMAND request_serializer_tests) diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 652d255115..0f83ea58a8 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -189,7 +189,9 @@ function(add_shader_recompiler target runtimeTarget) get_target_property(shaderCacheCompiledSources ${target} SOURCES) list(APPEND shaderCacheRecompilerSources ${shaderCacheCompiledSources}) list(REMOVE_DUPLICATES shaderCacheRecompilerSources) - set(shaderCacheKeySources "${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/HostThreadLocal.hpp") + set(shaderCacheKeySources + "${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/HostThreadLocal.hpp" + "${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp") foreach(source IN LISTS shaderCacheKeySources) if(NOT EXISTS "${source}") message(FATAL_ERROR "Shader disk cache key source does not exist: ${source}") diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 23c4658d37..94c26d25c0 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -3,6 +3,7 @@ #include "RuntimeAbi.hpp" #include "PipelineSpecialization.hpp" +#include "prx/libc/include/general/AtomicSharedPtr.hpp" #include <algorithm> #include <array> #include <atomic> @@ -704,7 +705,18 @@ struct PipelineIdentity { auto operator<=>(const PipelineIdentity&) const = default; }; +struct ResultKeyMemo { + mutable AtomicSharedPtr<const std::vector<std::uint32_t>> words; + ResultKeyMemo() = default; + ResultKeyMemo(const ResultKeyMemo&) {} + ResultKeyMemo& operator=(const ResultKeyMemo&) { + words.store(nullptr, std::memory_order_relaxed); + return *this; + } +}; + struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { + ResultKeyMemo drawKeyMemo; bool cacheHit = false; std::uint32_t workgroupMemoryDwords = 0; std::vector<std::uint32_t> shaderDataUserWords; diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index d4d43946e7..6aebbb7529 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -34,6 +34,9 @@ class Fixture: header = root / 'core/libs/prx/libc/include/HostThreadLocal.hpp' header.parent.mkdir(parents=True) header.write_text('struct FixtureThreadLocal {};\n', encoding='utf-8') + shared = root / 'core/libs/prx/libc/include/general/AtomicSharedPtr.hpp' + shared.parent.mkdir(parents=True) + shared.write_text('struct FixtureAtomicSharedPtr {};\n', encoding='utf-8') self.guard = root / 'driver.cpp' self.guard.write_text('int fixture_driver;\n', encoding='utf-8') compiler = root / ('compiler.cmd' if os.name == 'nt' else 'compiler') From 989860014580fd9da04c0fd0b6f52a9a8f06ca9f Mon Sep 17 00:00:00 2001 From: Scramblrrrr <joey.purkis@gmail.com> Date: Mon, 19 Oct 2026 00:05:40 -0300 Subject: [PATCH 1106/1360] fix(agc): include draw finisher work in CPU traces --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 ++++++- .../src/Driver/Draw/DrawPipeline.cpp | 1 + .../tests/execution/DrawPipelineTrace.cpp | 31 +++++++++++++------ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 94f8ca3aa1..8e54b1ff89 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2340,8 +2340,17 @@ if(BUILD_TESTING) --unset=APS5_TRACE_FRAMES --unset=APS5_TRACE_SECONDS --unset=APS5_TRACE_SUMMARY_EVERY $<TARGET_FILE:agc_driver_draw_pipeline_trace_tests> disabled) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT "APS5_PIPELINED_DRAWS=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_draw_finisher_trace COMMAND agc_driver_draw_pipeline_trace_tests finisher) + set_tests_properties(agc_driver_draw_finisher_trace PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30) + set_property(TEST agc_driver_draw_finisher_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") + add_test(NAME agc_driver_draw_finisher_trace_disabled COMMAND ${CMAKE_COMMAND} -E env + --unset=APS5_TRACE_FRAMES --unset=APS5_TRACE_SECONDS --unset=APS5_TRACE_SUMMARY_EVERY + $<TARGET_FILE:agc_driver_draw_pipeline_trace_tests> finisher-disabled) + set_tests_properties(agc_driver_draw_finisher_trace_disabled PROPERTIES ENVIRONMENT "APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=8" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_pipeline_trace_disabled agc_driver_draw_finisher_trace agc_driver_draw_finisher_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index cd6c6f9da1..da39a3c3db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -172,6 +172,7 @@ void DrawFinisher::Enqueue(Job job, Pending held) { } void DrawFinisher::run() { + PerformanceTrace::BindThread("draw finisher"); for (;;) { Job* job = nullptr; bool failed = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawPipelineTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawPipelineTrace.cpp index 31456ae51f..e3a7f4cc80 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawPipelineTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawPipelineTrace.cpp @@ -11,35 +11,46 @@ int main(int argc, char** argv) { try { using namespace AgcDriver; using DriverDetail::DrawPipeline; + using DriverDetail::DrawFinisher; if (DrawPipeline::Depth() == 0) return 77; - const bool disabled = argc > 1 && std::string_view(argv[1]) == "disabled"; + const auto mode = argc > 1 ? std::string_view(argv[1]) : std::string_view{}; + const bool disabled = mode == "disabled" || mode == "finisher-disabled"; + const bool finisher = mode.starts_with("finisher"); + const char* worker = finisher ? "draw finisher" : "draw committer"; + if (finisher && !DrawFinisher::Enabled()) throw std::runtime_error("draw finisher is disabled"); if (PerformanceTrace::Configured() == disabled) throw std::runtime_error("unexpected trace configuration"); if (!disabled) PerformanceTrace::Present(1, 1, nullptr); bool checked = false; const char* failure = nullptr; - DrawPipeline::Queue0().Enqueue([&] { + const auto checkWorker = [&] { auto* trace = PerformanceTrace::Thread(); if (disabled) { - if (trace != nullptr) { failure = "unconfigured draw committer allocated a trace buffer"; return; } + if (trace != nullptr) { failure = "unconfigured draw worker allocated a trace buffer"; return; } checked = true; return; } - if (trace == nullptr) { failure = "draw committer CPU spans are absent from the trace"; return; } + if (trace == nullptr) { failure = "draw worker CPU spans are absent from the trace"; return; } { PerformanceTimer timer("DrawPipeline.TraceRegression"); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } const auto snapshot = trace->Collect(); - if (snapshot.name != "draw committer" || snapshot.dropped) { failure = "draw committer trace identity or capacity is incorrect"; return; } + if (snapshot.name != worker || snapshot.dropped) { failure = "draw worker trace identity or capacity is incorrect"; return; } const auto found = [](const auto& event) { return event.scope != nullptr && std::string_view(event.scope) == "DrawPipeline.TraceRegression" && event.stage == nullptr && event.length > std::chrono::steady_clock::duration::zero(); }; if (std::none_of(snapshot.slices.begin(), snapshot.slices.end(), found) || std::none_of(snapshot.totals.begin(), snapshot.totals.end(), found)) - { failure = "committed work was missing from trace slices or totals"; return; } + { failure = "draw work was missing from trace slices or totals"; return; } checked = true; - }, {}); - DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + }; + if (finisher) { + DrawFinisher::Get().Enqueue(checkWorker); + DrawFinisher::Get().Drain(); + } else { + DrawPipeline::Queue0().Enqueue(checkWorker, {}); + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + } if (failure != nullptr) throw std::runtime_error(failure); - if (!checked) throw std::runtime_error("draw committer did not run the trace check"); - std::cout << "Draw committer tracing checked\n"; + if (!checked) throw std::runtime_error("draw worker did not run the trace check"); + std::cout << worker << " tracing checked\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 89ebfe1f94fd29440449b3e4be00aa52d74e1ede Mon Sep 17 00:00:00 2001 From: raghav314 <samraghav2@gmail.com> Date: Tue, 20 Oct 2026 03:27:29 +0530 Subject: [PATCH 1107/1360] perf(agc): queue labels behind finisher draws instead of draining the finisher A WRITE_DATA or RELEASE_MEM label the worker queues for the committer drained the draw finisher first, so that it could not reach the committer before the draws the finisher still held. In Hades II nearly every drain of the finisher was one (81,762 of 82,467 per 10 s were WRITE_DATA). While the finisher holds draws, the label's enqueue now runs as a finisher job, so it reaches the committer after them, and its write range and value are a pending write of the finisher until then. A WAIT_REG_MEM that a pending label satisfies, in the finisher or behind it, stays beside the finisher as it did beside the committer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../include/Driver/Draw/DrawPipeline.hpp | 4 + .../Execution/include/Driver/Driver.hpp | 1 + .../src/Driver/Draw/DrawPipeline.cpp | 22 +++ .../src/Driver/Packets/PacketExecution.cpp | 14 +- .../src/Driver/Packets/PacketMemory.cpp | 29 +++- .../tests/execution/FinisherLabelOrder.cpp | 153 ++++++++++++++++++ 7 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/FinisherLabelOrder.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8e54b1ff89..422030db81 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2354,7 +2354,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) - set(agcDrawPipelineTests agc_driver_draw_write_ranges agc_driver_draw_unsupported_buffer_type agc_driver_bda_span_reads agc_driver_copied_writer_copy agc_driver_image_table_reuse agc_driver_depth_storage_reuse agc_driver_staged_clear_sampled_read agc_driver_storage_write_back_overhang agc_driver_write_data_written_ahead agc_driver_gated_dispatch_capture agc_driver_buffer_load_store agc_driver_pipelined_dma agc_driver_pipelined_fill agc_driver_pipelined_predication_read agc_driver_pipelined_predicated_label_order agc_driver_pipelined_metadata_pass agc_driver_draw_cache_reuse agc_driver_draw_data_hit_user_words) + set(agcDrawPipelineTests agc_driver_draw_write_ranges agc_driver_draw_unsupported_buffer_type agc_driver_bda_span_reads agc_driver_copied_writer_copy agc_driver_image_table_reuse agc_driver_depth_storage_reuse agc_driver_staged_clear_sampled_read agc_driver_storage_write_back_overhang agc_driver_write_data_written_ahead agc_driver_gated_dispatch_capture agc_driver_buffer_load_store agc_driver_pipelined_dma agc_driver_pipelined_fill agc_driver_pipelined_predication_read agc_driver_pipelined_predicated_label_order agc_driver_pipelined_metadata_pass agc_driver_draw_cache_reuse agc_driver_draw_data_hit_user_words agc_driver_finisher_label_order agc_driver_same_queue_label_wait agc_driver_unimported_label_order) foreach(agcTestName IN LISTS agcDrawPipelineTests) add_test(NAME ${agcTestName}_draw_pipeline COMMAND ${agcTestName}_tests) set_tests_properties(${agcTestName}_draw_pipeline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=32;APS5_PIPELINE_DISPATCH=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index 98199f8560..0a03c052fe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -102,9 +102,13 @@ public: std::uint64_t shape = 0; bool unknown = true; std::vector<Range> writes; + std::uint64_t labelAddress = 0; + std::vector<std::byte> labelBytes; }; + enum class LabelLookup : std::uint8_t { NotHeld, Value, Unknown }; static bool Overlap(); bool PendingOverlaps(std::uint64_t begin, std::uint64_t end); + LabelLookup PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t& value); bool PendingOverlapsAny(std::span<const Range> ranges); bool PendingKey(std::uint64_t key, std::uint64_t shape); enum class DrainSite : std::uint32_t { Lookup, Reads, Packet, Kept, Count }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 74ba276280..1bba99967e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -251,6 +251,7 @@ private: void noteDrawWriters(std::span<const Graphics::CompiledShader> stages, std::uint32_t queue); static std::vector<std::pair<std::uint64_t, std::uint64_t>> drawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages); void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey); + bool labelPacketQueues(std::span<const std::uint32_t> packet, std::uint32_t opcode); bool enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue); bool enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state); void commitDmaStore(std::uint32_t queue, std::uint64_t address, std::span<const std::byte> bytes); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index da39a3c3db..a2646ea366 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -79,6 +79,22 @@ bool DrawFinisher::PendingOverlaps(std::uint64_t begin, std::uint64_t end) { return false; } +DrawFinisher::LabelLookup DrawFinisher::PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t& value) { + if (!Busy() || !finisherProducer) return LabelLookup::NotHeld; + const auto end = address + bytes; + std::lock_guard lock(mutex); + for (auto held = pending.rbegin(); held != pending.rend(); ++held) { + if (held->unknown) return LabelLookup::Unknown; + const bool overlaps = std::any_of(held->writes.begin(), held->writes.end(), [&](const Range& range) { return address < range.second && range.first < end; }); + if (!overlaps) continue; + if (held->labelBytes.empty() || address < held->labelAddress || end > held->labelAddress + held->labelBytes.size()) return LabelLookup::Unknown; + value = 0; + std::memcpy(&value, held->labelBytes.data() + (address - held->labelAddress), bytes); + return LabelLookup::Value; + } + return LabelLookup::NotHeld; +} + bool DrawFinisher::PendingOverlapsAny(std::span<const Range> ranges) { if (!Busy() || !finisherProducer) return false; std::lock_guard lock(mutex); @@ -457,6 +473,12 @@ void DrawPipeline::ReleaseRunningReads() { std::optional<std::uint64_t> DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > 8) return std::nullopt; + if (DrawFinisher::Overlap()) { + std::uint64_t held = 0; + const auto lookup = DrawFinisher::Get().PendingLabel(address, bytes, held); + if (lookup == DrawFinisher::LabelLookup::Value) return held; + if (lookup == DrawFinisher::LabelLookup::Unknown) return std::nullopt; + } const auto end = address + bytes; std::lock_guard lock(mutex); for (auto item = items.rbegin(); item != items.rend(); ++item) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 93202ca2a8..a4c16c2410 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -44,6 +44,14 @@ bool keepsDrawFinisher(std::uint32_t header, std::uint32_t opcode) { } } +bool waitTakesPendingLabel(std::span<const std::uint32_t> packet) { + if (packet.size() < 5) return false; + const auto awaitedBytes = Pm4::WaitAwaitedBytes(packet); + const std::uint64_t awaited = (packet[2] & (awaitedBytes == 8 ? ~7u : ~3u)) | (static_cast<std::uint64_t>(packet[3]) << 32u); + const auto value = DrawPipeline::Queue0().PendingLabel(awaited, awaitedBytes); + return value.has_value() && Pm4::WaitComparesValue(packet, *value); +} + bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode) { if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; @@ -255,8 +263,11 @@ void Driver::execute(const Submission& submission) { const auto packet = std::span(submission.commands).subspan(cursor, count); const auto opcode = (header >> 8u) & 0xffu; auto nextCursor = cursor + count; + const bool predicated = Pm4::Predicated(header) && queue.predication.operation != 0; if (submission.queue == 0 && DrawFinisher::Busy() && !keepsDrawFinisher(header, opcode)) { - const bool beside = DrawFinisher::Overlap() && opcode != 0x15 && keepsDrawPipeline(packet, header, opcode); + const bool queuesLabel = pipelined && !predicated && header != FlipPacketHeader && header != RenderingWaitPacketHeader && !submission.flips.contains(cursor) && labelPacketQueues(packet, opcode); + const bool heldLabelWait = pipelined && !predicated && orderedWaits && (opcode == 0x3c || opcode == 0x93) && header != RenderingWaitPacketHeader && waitTakesPendingLabel(packet); + const bool beside = DrawFinisher::Overlap() && opcode != 0x15 && (queuesLabel || heldLabelWait || keepsDrawPipeline(packet, header, opcode)); if (!beside) DrawFinisher::Get().Drain(); if (DrawFinisher::Overlap()) { DrawFinisher::NoteDrain(DrawFinisher::DrainSite::Packet, !beside); @@ -266,7 +277,6 @@ void Driver::execute(const Submission& submission) { if (APS5_ENABLE_TIMING_LOG && submission.queue == 0 && pendingFrameTiming == nullptr) includeTimingSubmission(submission, false); PerformanceContext timingContext(submission.queue == 0 ? pendingFrameTiming.get() : FrameTiming::Async()); PerformanceTimer timing("Driver.Packet"); - const bool predicated = Pm4::Predicated(header) && queue.predication.operation != 0; if (pipelined && !predicated && header != FlipPacketHeader && header != RenderingWaitPacketHeader && !submission.flips.contains(cursor) && (enqueueLabelPacket(packet, opcode, submission.queue) || enqueueDmaPacket(packet, opcode, submission.queue, queue))) { traceLabel(packet, submission.queue); recent.Record(cursor); 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 d65d4bf795..e9c4bdd918 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -380,9 +380,15 @@ void Driver::commitDmaStore(std::uint32_t queue, std::uint64_t address, std::spa Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); } -bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue) { +bool Driver::labelPacketQueues(std::span<const std::uint32_t> packet, std::uint32_t opcode) { static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; if (!ordered || !DeferLabels() || (opcode != 0x49 && opcode != 0x37) || !deferredLabels().labels.empty()) return false; + if (Pm4::DecodeLabelWrite(packet).has_value()) return true; + return opcode == 0x49 && ((packet[2] >> 29u) == 0 || (packet[3] | (static_cast<std::uint64_t>(packet[4]) << 32u)) == 0); +} + +bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue) { + if (!labelPacketQueues(packet, opcode)) return false; const auto interruptSelect = opcode == 0x49 ? (packet[2] >> 24u) & 7u : 0u; const bool endOfPipeInterrupt = interruptSelect != 0 && interruptSelect != 3; const auto label = Pm4::DecodeLabelWrite(packet); @@ -406,10 +412,23 @@ bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint } if (endOfPipeInterrupt) bumpEpoch(&EpochBumps::drains); auto stored = bytes; - DrawPipeline::Queue0().Enqueue([this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt] { - GuestMemory::SetCurrentPacket(opcode, queue); - commitLabel(queue, address, bytes, endOfPipeInterrupt); - }, std::move(writes), address, std::move(stored)); + auto labelBytes = bytes; + auto enqueue =[this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt, writes, stored = std::move(stored)]() mutable { + DrawPipeline::Queue0().Enqueue([this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt] { + GuestMemory::SetCurrentPacket(opcode, queue); + commitLabel(queue, address, bytes, endOfPipeInterrupt); + }, std::move(writes), address, std::move(stored)); + }; + if (queue == 0 && DrawFinisher::Overlap() && DrawFinisher::Busy()) { + DrawFinisher::Pending held; + held.unknown = false; + held.writes = std::move(writes); + held.labelAddress = address; + held.labelBytes = std::move(labelBytes); + DrawFinisher::Get().Enqueue(std::move(enqueue), std::move(held)); + return true; + } + enqueue(); return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FinisherLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FinisherLabelOrder.cpp new file mode 100644 index 0000000000..80e9c72dba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FinisherLabelOrder.cpp @@ -0,0 +1,153 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <bit> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <exception> +#include <initializer_list> +#include <iostream> +#include <string> +#include <utility> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array<std::uint32_t, 16> VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array<std::uint32_t, 10> PixelCode{0xf4000100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0x06000002u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array<float, 4> PixelConstant{}; +alignas(256) std::array<std::byte, 1024> ColorMemory{}; +constexpr std::uint32_t Labels = 8; +alignas(256) std::array<std::uint32_t, Labels> LabelMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast<std::uint32_t>(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) { + commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector<std::uint32_t> Setup() { + const auto color = reinterpret_cast<std::uintptr_t>(ColorMemory.data()); + const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data()); + const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data()); + const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast<std::uint32_t>(color >> 8u)}, {0x390, static_cast<std::uint32_t>(color >> 40u)}, + {0x10f, std::bit_cast<std::uint32_t>(32.0f)}, {0x110, std::bit_cast<std::uint32_t>(32.0f)}, + {0x111, std::bit_cast<std::uint32_t>(-2.0f)}, {0x112, std::bit_cast<std::uint32_t>(2.0f)}, + {0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{ + {0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 0}, {0x8c, 0}, + {0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 3u << 1u}, + }; + std::vector<std::uint32_t> commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto table = reinterpret_cast<std::uintptr_t>(PixelConstant.data()); + Append(commands, 0x76, {0x00c, static_cast<std::uint32_t>(table)}); + Append(commands, 0x76, {0x00d, static_cast<std::uint32_t>(table >> 32u)}); + Append(commands, 0x76, {0x00e, std::bit_cast<std::uint32_t>(0.25f)}); + return commands; +} + +void WriteLabel(std::vector<std::uint32_t>& commands, std::uint32_t index, std::uint32_t value) { + const auto address = reinterpret_cast<std::uintptr_t>(&LabelMemory[index]); + commands.insert(commands.end(), {0xc0033700u, 0x00100200u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), value}); +} + +void WaitLabel(std::vector<std::uint32_t>& commands, std::uint32_t index, std::uint32_t value) { + const auto address = reinterpret_cast<std::uintptr_t>(&LabelMemory[index]); + commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), value, 0xffffffffu, 0x19u}); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + using AgcDriver::DriverDetail::DrawFinisher; + const bool finisher = DrawFinisher::Enabled() && DrawFinisher::Overlap(); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + PixelConstant[0] = 0.25f; + static_cast<void>(DrawFinisher::DrainText()); + auto commands = Setup(); + for (std::uint32_t index = 0; index < Labels; ++index) { + Append(commands, 0x2d, {3, 2}); + WriteLabel(commands, index, 0x100u + index); + WaitLabel(commands, index, 0x100u + index); + } + Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto drains = DrawFinisher::DrainText(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(LabelMemory.data()), sizeof(LabelMemory)); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(ColorMemory.data()), ColorMemory.size()); + for (std::uint32_t index = 0; index < Labels; ++index) { + Require(LabelMemory[index] == 0x100u + index, "label " + std::to_string(index) + " is " + std::to_string(LabelMemory[index]) + " after the draws before it, expected " + std::to_string(0x100u + index)); + } + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast<unsigned>(std::to_integer<std::uint8_t>(value))); + Require(brightest + 2 >= 128 && brightest <= 130, "the draws before the labels wrote " + std::to_string(brightest) + ", expected 128"); + if (finisher) Require(drains.find(" 0x37 x") == std::string::npos, "a WRITE_DATA label after a draw handed to the finisher drained the finisher:" + drains); + if (finisher) Require(drains.find(" 0x3c x") == std::string::npos, "a WAIT_REG_MEM on a label the finisher holds drained the finisher:" + drains); + AgcDriverShutdown_nid_postfix(); + std::cout << "labels after finisher draws " << (finisher ? "queued through the finisher" : "written in order") << "\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 635c1ff656b962758bbb2b9aeb7f6f23075442e2 Mon Sep 17 00:00:00 2001 From: raghav314 <samraghav2@gmail.com> Date: Thu, 22 Oct 2026 07:56:00 +0530 Subject: [PATCH 1108/1360] fix(agc): release finished draw pipeline items at shutdown so the device is destroyed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Execution/include/Driver/Draw/DrawPipeline.hpp | 2 ++ .../Execution/src/Driver/Draw/DrawPipeline.cpp | 12 ++++++++++++ .../Execution/src/Driver/Lifecycle.cpp | 2 ++ 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index 0a03c052fe..05cb49ba12 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -37,6 +37,7 @@ public: void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {}, std::vector<Range> reads = {}, bool draw = false); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); void Stop(); + void ReleaseRetired(); static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode); bool Busy() const; std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } @@ -123,6 +124,7 @@ public: void Enqueue(Job job, Pending held); void Drain(); void Settle(); + void ReleaseRan(); private: DrawFinisher() = default; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index a2646ea366..98555544f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -233,6 +233,12 @@ void DrawFinisher::Drain() { if (failure != nullptr) std::rethrow_exception(std::exchange(failure, nullptr)); } +void DrawFinisher::ReleaseRan() { + std::vector<Job> done; + std::lock_guard lock(mutex); + std::swap(done, ran); +} + DrawPipeline& DrawPipeline::Queue0() { static DrawPipeline* pipeline = new DrawPipeline(); return *pipeline; @@ -427,6 +433,12 @@ void DrawPipeline::Stop() { else committer.join(); } +void DrawPipeline::ReleaseRetired() { + std::vector<Item> done; + std::lock_guard lock(mutex); + std::swap(done, retired); +} + void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { drainRequests[std::min<std::uint32_t>(opcode, 0x100)].fetch_add(1, std::memory_order_relaxed); const bool finisherBusy = DrawFinisher::Busy() && !DrawFinisher::OnFinisherThread(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index e81b06092c..75637f94d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -166,6 +166,8 @@ void Driver::stop() { } catch (...) { } DrawPipeline::Queue0().Stop(); + DrawPipeline::Queue0().ReleaseRetired(); + DrawFinisher::Get().ReleaseRan(); StopWorkerSampler(); CloseRegistrationPreparation(); Graphics::StopPipelineLibraryWorker(); From 5595ad7c7b111b0d4415f7338ea2815a89e0f370 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:38:19 -0700 Subject: [PATCH 1109/1360] test(agc): count the draw finisher trace test as skipped when the build leaves it out --- 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 422030db81..a3834458f1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2342,7 +2342,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT "APS5_PIPELINED_DRAWS=8" SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_driver_draw_finisher_trace COMMAND agc_driver_draw_pipeline_trace_tests finisher) set_tests_properties(agc_driver_draw_finisher_trace PROPERTIES - ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30) + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" SKIP_RETURN_CODE 77 TIMEOUT 30) set_property(TEST agc_driver_draw_finisher_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") add_test(NAME agc_driver_draw_finisher_trace_disabled COMMAND ${CMAKE_COMMAND} -E env From a9e21414311c62f8c8c7b75356c3f429a8014077 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:38:20 -0700 Subject: [PATCH 1110/1360] test(agc): run the draw cache relocation test on the draw pipeline --- 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 a3834458f1..808b99ef80 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2354,7 +2354,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) - set(agcDrawPipelineTests agc_driver_draw_write_ranges agc_driver_draw_unsupported_buffer_type agc_driver_bda_span_reads agc_driver_copied_writer_copy agc_driver_image_table_reuse agc_driver_depth_storage_reuse agc_driver_staged_clear_sampled_read agc_driver_storage_write_back_overhang agc_driver_write_data_written_ahead agc_driver_gated_dispatch_capture agc_driver_buffer_load_store agc_driver_pipelined_dma agc_driver_pipelined_fill agc_driver_pipelined_predication_read agc_driver_pipelined_predicated_label_order agc_driver_pipelined_metadata_pass agc_driver_draw_cache_reuse agc_driver_draw_data_hit_user_words agc_driver_finisher_label_order agc_driver_same_queue_label_wait agc_driver_unimported_label_order) + set(agcDrawPipelineTests agc_driver_draw_write_ranges agc_driver_draw_unsupported_buffer_type agc_driver_bda_span_reads agc_driver_copied_writer_copy agc_driver_image_table_reuse agc_driver_depth_storage_reuse agc_driver_staged_clear_sampled_read agc_driver_storage_write_back_overhang agc_driver_write_data_written_ahead agc_driver_gated_dispatch_capture agc_driver_buffer_load_store agc_driver_pipelined_dma agc_driver_pipelined_fill agc_driver_pipelined_predication_read agc_driver_pipelined_predicated_label_order agc_driver_pipelined_metadata_pass agc_driver_draw_cache_reuse agc_driver_draw_cache_relocation agc_driver_draw_data_hit_user_words agc_driver_finisher_label_order agc_driver_same_queue_label_wait agc_driver_unimported_label_order) foreach(agcTestName IN LISTS agcDrawPipelineTests) add_test(NAME ${agcTestName}_draw_pipeline COMMAND ${agcTestName}_tests) set_tests_properties(${agcTestName}_draw_pipeline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_PIPELINE=1;APS5_PIPELINED_DRAWS=32;APS5_PIPELINE_DISPATCH=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") From 83debaa12fe0a382e8d9aad54f145e98696bbd5b Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 00:26:06 +0200 Subject: [PATCH 1111/1360] docs(debt): record the two-user special port measurement With two users logged in on a PS4 Pro (firmware 10.71) each user's special port handle, ALREADY_OPENED on its second open and close are independent of the other user's, as the branch already models. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- docs/dev/debt/libScePad.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index fea6b7fe4b..65a1d270e1 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -12,7 +12,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists -- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: two users at once, the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, one user, no special controller) did not cover; it measured open, `ALREADY_OPENED` on a second open, `scePadGetHandle`, both reads, both informations, vibration, light bar, motion sensor and orientation setters, and close. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED` on a second open, `scePadGetHandle`, both reads, both informations, vibration, light bar, motion sensor and orientation setters, close, and with two users logged in that each user's handle, `ALREADY_OPENED` and close are independent. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect From fa5b090230e38d364afbe9d641f66d48ae0c34f8 Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 00:26:50 +0200 Subject: [PATCH 1112/1360] docs(debt): keep the special port entry under the length limit Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- docs/dev/debt/libScePad.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 65a1d270e1..3eee2c921f 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -12,7 +12,7 @@ - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists -- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED` on a second open, `scePadGetHandle`, both reads, both informations, vibration, light bar, motion sensor and orientation setters, close, and with two users logged in that each user's handle, `ALREADY_OPENED` and close are independent. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 passes a 16-byte param naming the device and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect From 1d0c48d1ee1324e9edb1dbea3c1ec29a668ceccc Mon Sep 17 00:00:00 2001 From: valenciana-nc <valenciana-nc@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:03:01 -0600 Subject: [PATCH 1113/1360] fix(libc): preserve exception flags for zero-argument float Bessel calls --- core/libs/prx/libc/src/Math.cpp | 1 - core/libs/tests/GuestBessel.cpp | 28 ++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..39c9c405cc 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -226,7 +226,6 @@ T GuestFmax(T x, T y) { } float GuestBesselSecondKindZero() { - std::feraiseexcept(FE_DIVBYZERO); return -std::numeric_limits<float>::infinity(); } diff --git a/core/libs/tests/GuestBessel.cpp b/core/libs/tests/GuestBessel.cpp index 75c94ec4ac..6650201428 100644 --- a/core/libs/tests/GuestBessel.cpp +++ b/core/libs/tests/GuestBessel.cpp @@ -17,6 +17,9 @@ double APS5_VABI jn_nid_postfix(int, double); double APS5_VABI y0_nid_postfix(double); double APS5_VABI y1_nid_postfix(double); double APS5_VABI yn_nid_postfix(int, double); +float APS5_VABI y0f_nid_postfix(float); +float APS5_VABI y1f_nid_postfix(float); +float APS5_VABI ynf_nid_postfix(int, float); int* APS5_VABI __error_nid_postfix(); } @@ -100,6 +103,30 @@ static void CheckSecondKind() { } } +static void CheckFloatSecondKindZero() { + const char* names[] = {"y0f", "y1f", "ynf"}; + for (int function = 0; function < 3; ++function) { + for (const int order : {INT_MIN, -3, -2, -1, 0, 1, 2, 3, INT_MAX}) { + for (const float input : {0.0f, -0.0f}) { + for (const int flags : {0, FE_INEXACT, FE_DIVBYZERO}) { + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(flags); + const int expectedFlags = std::fetestexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 12345; + const float result = function == 0 ? y0f_nid_postfix(input) : function == 1 ? y1f_nid_postfix(input) : ynf_nid_postfix(order, input); + const int actualFlags = std::fetestexcept(FE_ALL_EXCEPT); + if (std::bit_cast<std::uint32_t>(result) != 0xff800000u || actualFlags != expectedFlags || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s(%d, %08x): result %08x, flags %x, expected %x, errno %d\n", names[function], order, + std::bit_cast<std::uint32_t>(input), std::bit_cast<std::uint32_t>(result), actualFlags, expectedFlags, *__error_nid_postfix()); + std::abort(); + } + } + } + } + } + std::feclearexcept(FE_ALL_EXCEPT); +} + static void CheckIntegerOrders() { const OrderCase first[] = { {2, 0x3ddb7cdfd9d7bdbbull, 0x3b979ca10c924224ull}, {5, 0xbddb7cdfd9d7bdbbull, 0xb4cfed4f3ec0c466ull}, {33, 0x3e00000000000000ull, 0x0000000000000000ull}, @@ -181,6 +208,7 @@ static void CheckValues() { int main() { CheckFirstKind(); CheckSecondKind(); + CheckFloatSecondKindZero(); CheckIntegerOrders(); CheckValues(); return 0; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..8092690d8d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -89,7 +89,7 @@ - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed -- [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The console's `y0f`, `y1f` and `ynf` raise none either, unlike [ours](../../../core/libs/prx/libc/src/Math.cpp), which raise `FE_DIVBYZERO` at ±0. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 +- [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured - [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From 35b22f0ef38f07133818a72ef9b236fcbbbb110a Mon Sep 17 00:00:00 2001 From: valenciana-nc <valenciana-nc@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:28:00 -0600 Subject: [PATCH 1114/1360] fix(relinker): preserve icon and title metadata inputs --- core/relinker/main.cpp | 9 +- .../relinker/tests/test_output_collisions.py | 90 ++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 98bca27396..91c5b09ee7 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -337,10 +337,17 @@ int Run(const int argc, char* argv[]) { if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase); const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; - const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); + const auto metadataRoot = Io::NativePath(args.inputPath).parent_path() / "sce_sys"; + const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(metadataRoot, Io::NativePath(absPath)); + std::vector<std::filesystem::path> metadataInputs; + if (args.toWindows || desktopEntry) metadataInputs.push_back(metadataRoot / "icon0.png"); + if (desktopEntry) metadataInputs.push_back(metadataRoot / "param.json"); const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { if (!std::filesystem::exists(path)) return; + for (const auto& input : metadataInputs) + if (std::filesystem::exists(input) && SameOutputFile(input, path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite input metadata: " + Io::Utf8Path(path)); for (const auto& executable : executables) if (SameOutputFile(Io::NativePath(executable.InputPath), path)) throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py index 4213674c46..0361c30544 100644 --- a/core/relinker/relinker/tests/test_output_collisions.py +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -50,6 +50,93 @@ def case_sensitive(directory): return sensitive +def metadata_case(case, windows): + command, outputs, inputs = prepare(case, windows) + metadata = case / "sce_sys" + metadata.mkdir(exist_ok=True) + icon = metadata / "icon0.png" + param = metadata / "param.json" + icon.write_bytes(png(1, 1)) + param.write_text(json.dumps({"titleId": "PPSA00000", "defaultLanguage": "en-US", + "localizedParameters": {"en-US": {"titleName": "Sample Title"}}}), encoding="utf-8") + for path in outputs.values(): + path.write_bytes(b"previous output") + inputs.update({icon: icon.read_bytes(), param: param.read_bytes()}) + return command, outputs, inputs + + +def tree_snapshot(root): + return {path.relative_to(root): ("link", os.readlink(path)) if path.is_symlink() + else ("directory", None) if path.is_dir() else ("file", path.read_bytes()) + for path in root.rglob("*")} + + +def metadata_inputs(relinker, work): + checked = 0 + + def rejected(case, command): + before = tree_snapshot(case) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert result.returncode == 2 and "output would overwrite input metadata" in result.stderr, ( + case.name, result.returncode, result.stdout, result.stderr) + assert tree_snapshot(case) == before, case + + for windows in (False, True): + for name in (("icon0.png",) if windows else ("icon0.png", "param.json")): + case = work / f"metadata-direct-{windows}-{name}" + command, _, _ = metadata_case(case, windows) + rejected(case, [*command[:-1], str(case / "sce_sys" / name)]) + checked += 1 + outputs = ("executable", "module", "registry", "module-registry", "needed", "module-needed") + outputs += ("diagnostics",) if windows else ("icon", "desktop") + for alias in ("hardlink", "symlink"): + for output in outputs: + case = work / f"metadata-{windows}-{name}-{alias}-{output}" + command, paths, _ = metadata_case(case, windows) + destination = paths[output] + destination.unlink() + metadata = case / "sce_sys" / name + if alias == "hardlink": + os.link(metadata, destination) + else: + try: + destination.symlink_to(metadata) + except (OSError, NotImplementedError) as error: + print(f"Metadata symlink case skipped: {error}") + continue + rejected(case, command) + checked += 1 + case = work / f"metadata-distinct-{windows}" + command, paths, inputs = metadata_case(case, windows) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert paths["executable"].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + + for format, name in (("windows", "param.json"), ("macos", "icon0.png"), ("linux-no-icon", "param.json")): + case = work / f"metadata-unused-{format}" + command, _, inputs = metadata_case(case, format == "windows") + if format == "macos": + command.insert(0, "--macos") + if format == "linux-no-icon": + icon = case / "sce_sys/icon0.png" + icon.unlink() + inputs.pop(icon) + output = case / "sce_sys" / name + result = subprocess.run([str(relinker), *command[:-1], str(output)], cwd=case, + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert result.returncode == 0, (format, result.stdout, result.stderr) + magic = b"MZ" if format == "windows" else b"\xcf\xfa\xed\xfe" if format == "macos" else b"\x7fELF" + assert output.read_bytes().startswith(magic), output + for path, original in inputs.items(): + if path != output: + assert path.read_bytes() == original, path + return checked + + def filesystem_aliases(relinker, work): if os.name == "nt": return @@ -91,6 +178,7 @@ def main(): checked = 0 with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: work = Path(directory) + metadata_checked = metadata_inputs(relinker, work) filesystem_aliases(relinker, work) for windows, existing in itertools.product((False, True), repeat=2): case = work / f"needed-secondary-{windows}-{existing}" @@ -213,7 +301,7 @@ def main(): assert not list(case.rglob(".anyps5-output-identity-*")) finally: output.chmod(stat.S_IWRITE) - print(f"Output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + print(f"Output collision tests passed: {checked} hard-link pairs, {metadata_checked} metadata protections, direct paths, and identity controls") if __name__ == "__main__": From 59b2192ef980001548eda5d1ee1323b95d9cd5a6 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Thu, 22 Oct 2026 19:39:28 -0300 Subject: [PATCH 1115/1360] docs(libSceHttp): record that a PS4 takes no bracketed URI host Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceHttp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 94d946efb1..1c61e6029b 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -31,7 +31,7 @@ ### Functional -- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - bytes 0x80 to 0xFF were measured only as 0x80, 0xC3 and 0xFF in a hostname; the others, and any of them in a scheme or userinfo, are taken as the measured ASCII bytes outside those sets are. ASCII classification and scheme case folding are independent of the host locale. +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - bytes 0x80 to 0xFF were measured only as 0x80, 0xC3 and 0xFF in a hostname; the others, and any of them in a scheme or userinfo, are taken as the measured ASCII bytes outside those sets are. ASCII classification and scheme case folding are independent of the host locale. A bracketed host such as `[::1]` is parsed, merged and built without its brackets, as a PS5 (firmware 13.60) does; a PS4 (firmware 10.71) takes no bracketed host (`http://[::1]x/` has an empty host and the path `[::1]x/`) and keeps `[::1]:80` in a merge, as measured in #9058, so this follows the PS5 only. - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. From f878b14cd21e7001f493b0d4d19783ff70ad3ae3 Mon Sep 17 00:00:00 2001 From: Joacoorandom <62301477+Joacoorandom@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:41:54 -0300 Subject: [PATCH 1116/1360] test(tools): check interior repeated pad timestamps --- tools/tests/test_pad_timeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py index 887310714f..b7a91f3d96 100644 --- a/tools/tests/test_pad_timeline.py +++ b/tools/tests/test_pad_timeline.py @@ -79,6 +79,9 @@ class TimelineTests(unittest.TestCase): self.assertEqual(rows[0], "APS5_PAD_TIMELINE 1 42 4096") self.assertEqual(len(rows) - 1, 4096) self.assertEqual(rows[1:3], ["200 100 18432", "500 200 64"]) + self.assertEqual(rows[2047:2051], [ + "716300 100 18432", "716600 200 64", "717000 100 18432", "717300 200 64", + ]) self.assertEqual(rows[-2:], ["1433100 100 18432", "1433400 200 64"]) with self.assertRaisesRegex(ValueError, "1 to 4096 events after repetition"): pad_timeline.compile_timeline(self.document, 42, 2049, 0) From 1a8ee4d99944c0381aa2690e08f2c013f1e809bb Mon Sep 17 00:00:00 2001 From: VakzOs <eric.prevost2@gmail.com> Date: Wed, 21 Oct 2026 10:44:38 +0000 Subject: [PATCH 1117/1360] feat(libSceLibreSsl): implement DES_set_key, DES_ecb_encrypt and DES_set_odd_parity The DES block cipher and the parity helper come from mbedtls, which is already a submodule; DES_set_key follows LibreSSL with DES_check_key off and stores the key in the schedule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzhWRMauD3ouc8kSxT2NnT --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 11 +++ core/libs/prx/libSceLibreSsl/Export.cpp | 39 ++++++++--- core/libs/tests/GuestLibreSslDes.cpp | 75 +++++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 4 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 core/libs/tests/GuestLibreSslDes.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index d9d266270c..0ac9f475d7 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) if(BUILD_TESTING) @@ -13,4 +14,14 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") endif() + + add_test_executable(guest_libressl_des_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDes.cpp) + target_include_directories(guest_libressl_des_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_des_tests PRIVATE libSceLibreSsl) + add_test(NAME guest_libressl_des COMMAND guest_libressl_des_tests) + set_tests_properties(guest_libressl_des PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libressl_des PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") + endif() endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 670bb8911e..084f0bcf59 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1,7 +1,17 @@ #include <cstddef> #include <cstdint> +#include <cstring> +#include <stdexcept> +#include <string> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "mbedtls/des.h" + +namespace { + +constexpr std::size_t DesScheduleBytes = 128; + +} extern "C" { @@ -301,19 +311,28 @@ int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void) { return 0; } -int APS5_VABI DES_ecb_encrypt_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); +void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char* input, unsigned char* output, const unsigned char* schedule, int enc) { + if (input == nullptr || output == nullptr || schedule == nullptr) APS5_INVALID_ARG_EX; + mbedtls_des_context context; + mbedtls_des_init(&context); + unsigned char block[MBEDTLS_DES_KEY_SIZE]; + std::memcpy(block, input, sizeof(block)); + int status = enc ? mbedtls_des_setkey_enc(&context, schedule) : mbedtls_des_setkey_dec(&context, schedule); + if (status == 0) status = mbedtls_des_crypt_ecb(&context, block, output); + mbedtls_des_free(&context); + if (status != 0) throw std::runtime_error("DES_ecb_encrypt: mbedtls failed"); +} + +int APS5_VABI DES_set_key_nid_postfix(const unsigned char* key, unsigned char* schedule) { + if (key == nullptr || schedule == nullptr) APS5_INVALID_ARG_EX; + std::memset(schedule, 0, DesScheduleBytes); + std::memcpy(schedule, key, MBEDTLS_DES_KEY_SIZE); return 0; } -int APS5_VABI DES_set_key_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI DES_set_odd_parity_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI DES_set_odd_parity_nid_postfix(unsigned char* key) { + if (key == nullptr) APS5_INVALID_ARG_EX; + mbedtls_des_key_set_parity(key); } int APS5_VABI DTLSv1_client_method_nid_postfix(void) { diff --git a/core/libs/tests/GuestLibreSslDes.cpp b/core/libs/tests/GuestLibreSslDes.cpp new file mode 100644 index 0000000000..630f2601ed --- /dev/null +++ b/core/libs/tests/GuestLibreSslDes.cpp @@ -0,0 +1,75 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <algorithm> +#include <array> +#include <bit> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <stdexcept> + +using DesBlock = std::array<unsigned char, 8>; +using DesSchedule = std::array<unsigned char, 128>; + +extern "C" { +void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char*, unsigned char*, const unsigned char*, int); +int APS5_VABI DES_set_key_nid_postfix(const unsigned char*, unsigned char*); +void APS5_VABI DES_set_odd_parity_nid_postfix(unsigned char*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template<typename TAction> +static void RequireThrows(TAction action) { + try { action(); } catch (const std::invalid_argument&) { return; } + std::abort(); +} + +static DesBlock Block(std::uint64_t value) { + DesBlock block{}; + for (int index = 0; index < 8; ++index) block[index] = static_cast<unsigned char>(value >> (56 - 8 * index)); + return block; +} + +static void CheckVector(std::uint64_t key, std::uint64_t plain, std::uint64_t cipher) { + const auto keyBlock = Block(key); + DesSchedule schedule{}; + schedule.fill(0xaa); + Require(DES_set_key_nid_postfix(keyBlock.data(), schedule.data()) == 0); + const auto plainBlock = Block(plain); + DesBlock output{}; + DES_ecb_encrypt_nid_postfix(plainBlock.data(), output.data(), schedule.data(), 1); + Require(output == Block(cipher)); + DES_ecb_encrypt_nid_postfix(output.data(), output.data(), schedule.data(), 0); + Require(output == plainBlock); + DesBlock again{}; + DES_ecb_encrypt_nid_postfix(plainBlock.data(), again.data(), schedule.data(), 7); + Require(again == Block(cipher)); +} + +static void CheckParity() { + DesBlock key{0x00, 0x01, 0x02, 0x03, 0x12, 0x13, 0xfe, 0xff}; + DES_set_odd_parity_nid_postfix(key.data()); + Require(key == (DesBlock{0x01, 0x01, 0x02, 0x02, 0x13, 0x13, 0xfe, 0xfe})); + for (int value = 0; value < 256; ++value) { + DesBlock single{}; + single.fill(static_cast<unsigned char>(value)); + DES_set_odd_parity_nid_postfix(single.data()); + Require((single[0] & 0xfe) == (value & 0xfe) && std::popcount(single[0]) % 2 == 1); + } +} + +int main() { + CheckVector(0x133457799bbcdff1, 0x0123456789abcdef, 0x85e813540f0ab405); + CheckVector(0x0e329232ea6d0d73, 0x8787878787878787, 0x0000000000000000); + CheckVector(0x0101010101010101, 0x8000000000000000, 0x95f8a5e5dd31d900); + CheckParity(); + DesSchedule schedule{}; + const auto weak = Block(0x0101010101010101); + Require(DES_set_key_nid_postfix(weak.data(), schedule.data()) == 0); + const auto badParity = Block(0x0000000000000000); + Require(DES_set_key_nid_postfix(badParity.data(), schedule.data()) == 0); + RequireThrows([&] { DES_set_key_nid_postfix(nullptr, schedule.data()); }); + RequireThrows([&] { DES_ecb_encrypt_nid_postfix(weak.data(), schedule.data(), nullptr, 1); }); + RequireThrows([] { DES_set_odd_parity_nid_postfix(nullptr); }); + return 0; +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index bcaab509a1..434eef416e 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,5 +2,6 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 377 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 374 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw +- [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults From 7eaf7b0fd5f5bb539cde758811346cdb97bc254a Mon Sep 17 00:00:00 2001 From: THEGABOALE <garciaga@uamv.edu.ni> Date: Thu, 22 Oct 2026 17:31:19 -0600 Subject: [PATCH 1118/1360] fix(tools): detect multiline environment variable reads --- tools/env_reference.py | 8 +++--- tools/tests/test_env_reference.py | 45 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index 683b1e9168..b7277df3bb 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -92,10 +92,12 @@ def reads(roots, prefixes=PREFIXES): continue lines = text.splitlines() for number, line in enumerate(lines, 1): - if not any(prefix in line for prefix in prefixes): - continue window = "\n".join(lines[number - 1:number + 2]) - for reader, match in line_reads(line, prefixes): + if not any(prefix in window for prefix in prefixes): + continue + for reader, match in line_reads(window, prefixes): + if match.start() >= len(line): + continue found.setdefault(match.group("name"), []).append( (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index c414e314c0..f56a933b14 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -66,6 +66,51 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["APS5_WIDE_PRESENT"][0][2:], ("presence", "")) self.assertEqual(found["APS5_WIDE_INT"][0][2:], ("integer", "")) + def test_reads_environmental_variable_with_multiline_getenv(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text( + 'static const char* value = std::getenv(\n' + ' "APS5_MULTILINE");\n', + encoding="utf-8" + ) + + found = env_reference.reads([root / "core"]) + self.assertIn("APS5_MULTILINE", found) + + def test_multiline_reads_keep_line_numbers_and_do_not_duplicate(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text( + 'auto a = std::getenv("APS5_SINGLE");\n' + 'auto b = std::getenv(\n' + ' "APS5_MULTI");\n' + 'auto c = EnvInt(\n' + '\n' + ' "APS5_INT", 42);\n' + 'auto d = std::getenv("APS5_SINGLE");\n', + encoding="utf-8" + ) + + found = env_reference.reads([root / "core"]) + + self.assertEqual( + set(found), + {"APS5_SINGLE", "APS5_MULTI", "APS5_INT"} + ) + self.assertEqual( + [site[1] for site in found["APS5_SINGLE"]], [1, 7] + ) + self.assertEqual( + [site[1] for site in found["APS5_MULTI"]], [2] + ) + self.assertEqual( + [site[1] for site in found["APS5_INT"]], [4] + ) + self.assertEqual( + found["APS5_INT"][0][2:], ("integer", "42") + ) + def test_the_default_prefixes_cover_both(self): root = self.fixture() found = env_reference.reads([root / "core"]) From 957b65fa2f35ee0fbd42b93ed34534752f0bb652 Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:06:51 -0400 Subject: [PATCH 1119/1360] fix(tools): compare explicit APS5_EXPORT NIDs with named exports in pr_overlap --- tools/pr_overlap.py | 20 +++++++++++++++++++- tools/tests/test_pr_overlap_exports.py | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index c828d2d9ed..29008a4d9d 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -9,6 +9,8 @@ from itertools import combinations from pathlib import Path from urllib.parse import quote, urlencode +from nid_names import compute_nid, source_aliases + PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") NID_DISAMBIG = re.compile(r"_nid_disambig[0-9]+$") @@ -122,6 +124,21 @@ def exports(diff): return found +def aliases(diff): + added = "\n".join(line[1:] for line in diff.splitlines() if line.startswith("+") and not line.startswith("+++")) + return {nid for nid, _ in source_aliases(added)} + + +def export_ids(pr): + return {compute_nid(name) for name in pr["exports"]} | pr.get("aliases", set()) + + +def same_exports(pa, pb, shared): + common = (export_ids(pa) & export_ids(pb)).difference(*(export_ids(pr) for pr in shared)) + named = {name for name in pa["exports"] | pb["exports"] if compute_nid(name) in common} + return named | common.difference(compute_nid(name) for name in named) + + def scan(pr): tree, conflicted = merge("refs/pr/base", pr["ref"]) pr["tree"] = None if conflicted else commit(tree) @@ -134,6 +151,7 @@ def scan(pr): pr["added"].add(path) diff = git("diff", "-U0", "--no-color", old, new, "--", "core/libs/prx/*.cpp") pr["exports"] = exports(diff) + pr["aliases"] = aliases(diff) def scan_all(prs, jobs): @@ -188,7 +206,7 @@ def overlaps(prs, a, b): found = {"added": (pa["added"] & pb["added"]).difference(*(pr["added"] for pr in shared))} if pa["author"] != pb["author"]: found["conflicts"] = conflicts(pa, pb) - found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in shared)) + found["exports"] = same_exports(pa, pb, shared) if not (pa["tree"] and pb["tree"]): uncompared = pa.get("files", set()) & pb.get("files", set()) if uncompared: diff --git a/tools/tests/test_pr_overlap_exports.py b/tools/tests/test_pr_overlap_exports.py index d326656d8b..d109c06966 100644 --- a/tools/tests/test_pr_overlap_exports.py +++ b/tools/tests/test_pr_overlap_exports.py @@ -53,5 +53,23 @@ class ExportTests(unittest.TestCase): self.assertNotIn("Helper_nid_no_patch", found) + def test_explicit_export_aliases_are_collected(self): + diff = ('+int APS5_VABI placeholder_nid_no_patch() {\n' + '+ NotImplemented_nid_no_patch(__func__);\n' + '+}\n' + '+APS5_EXPORT("Pz262plj20w", placeholder_nid_no_patch);\n' + '-APS5_EXPORT("sKneJjUXWkQ", removed_nid_no_patch);\n') + self.assertEqual(pr_overlap.exports(diff), set()) + self.assertEqual(pr_overlap.aliases(diff), {"Pz262plj20w"}) + self.assertEqual(pr_overlap.aliases(diff.replace('+APS5_EXPORT("Pz262plj20w", placeholder_nid_no_patch);\n', "")), set()) + + def test_explicit_alias_overlaps_named_export_with_the_same_nid(self): + named = {"author": "a", "exports": {"FT_Outline_GetInsideBorder"}, "aliases": set()} + explicit = {"author": "b", "exports": set(), "aliases": {"Pz262plj20w"}} + self.assertEqual(pr_overlap.same_exports(named, explicit, []), {"FT_Outline_GetInsideBorder"}) + self.assertEqual(pr_overlap.same_exports(explicit, dict(explicit, author="c"), []), {"Pz262plj20w"}) + self.assertEqual(pr_overlap.same_exports(named, explicit, [explicit]), set()) + + if __name__ == "__main__": unittest.main() From 3fcafeb4205f730c0c9f966b947b5f6a6e3802ca Mon Sep 17 00:00:00 2001 From: tkcm01 <75453983+tkcm01@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:18:30 -0400 Subject: [PATCH 1120/1360] fix(libkernel): keep the thread stack lock alive through static destruction #9272 keeps liveStacks and protectedStackPages alive through static destruction so that a late mprotect from an atexit handler can still look up guest thread stacks. The mutex guarding them is still a static std::mutex. libstdc++'s std::mutex has a trivial destructor, so it keeps working on Linux, but libc++ destroys the pthread mutex, and a later lock throws "mutex lock failed: Invalid argument": on macOS the exit probe of guest_memory_lifetime fails at its mapping protection stage. The lock is now allocated and never destroyed, like the maps. The same change is part of #7201 and #8658. Co-Authored-By: marion909 <103169397+marion909@users.noreply.github.com> Co-Authored-By: iamthehimansh <67262056+iamthehimansh@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Pthread/src/Thread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index e3371f9802..aa76de407d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -74,7 +74,7 @@ static GuestThreadState& CurrentGuestState() { return state; #endif } -static std::mutex stackLock; +static std::mutex& stackLock = *new std::mutex(); static auto& liveStacks = *new std::map<PthreadPrivate*, std::pair<std::uintptr_t, std::uintptr_t>>(); static auto& protectedStackPages = *new std::map<std::uintptr_t, std::size_t>(); #if defined(__linux__) From 2c92b0f662369df6c0ec88eccdceea7f9a6e045a Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Thu, 22 Oct 2026 21:26:24 -0300 Subject: [PATCH 1121/1360] fix(libkernel): refuse closing the standard descriptors with EPERM as measured on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/File/src/Open.cpp | 2 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestHostStreams.cpp | 10 ++++++++-- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 417c79bd96..2cb36ef96c 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -298,7 +298,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { - if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; + if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); #ifdef _WIN32 const auto positioned = File::LockPositionedTransfer(d); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fa57cf492d..eef3f0b642 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -711,7 +711,7 @@ int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int } int APS5_VABI close_nid_postfix(int d) { - if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); + if (File::IsHostStream(d)) return PosixFailure(GUEST_EPERM); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; diff --git a/core/libs/tests/GuestHostStreams.cpp b/core/libs/tests/GuestHostStreams.cpp index e31c1dad51..83a39b5768 100644 --- a/core/libs/tests/GuestHostStreams.cpp +++ b/core/libs/tests/GuestHostStreams.cpp @@ -11,7 +11,9 @@ int* APS5_VABI __error_nid_postfix(); static void Require(bool value) { if (!value) std::abort(); } +constexpr int GuestEperm = 1; constexpr int GuestEbadf = 9; +constexpr int SceKernelErrorEperm = static_cast<int>(0x80020001); constexpr int SceKernelErrorEbadf = static_cast<int>(0x80020009); static bool Open(int fd) { @@ -26,13 +28,17 @@ int main() { for (int fd = 0; fd < 3; ++fd) open[fd] = Open(fd); for (int fd = 0; fd < 3; ++fd) { if (!open[fd]) continue; - Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEbadf); - Require(sceKernelClose(fd) == SceKernelErrorEbadf); + Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEperm); + Require(sceKernelClose(fd) == SceKernelErrorEperm); Require(Open(fd)); } int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); for (const int fd : pipe) Require(fd >= 3 || !open[fd]); Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(pipe[0]) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(pipe[1]) == SceKernelErrorEbadf); + Require(close_nid_postfix(-1) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(-1) == SceKernelErrorEbadf); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c9f6ad57e9..847205b362 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -113,7 +113,7 @@ - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` -- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed +- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EPERM` (`sceKernelClose`: `0x80020001`) and leaves it open, as closing the open descriptors 0 to 2 of a payload process did on a PS5 (firmware 13.60); whether a title process can close them is unverified. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws ### Functional From 4d1e7d8b60bb20b67b6120dcbb37d4e3e1b8a4de Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Fri, 23 Oct 2026 13:32:50 +1300 Subject: [PATCH 1122/1360] fix(libc): set EILSEQ in btowc for bytes that cannot start a character Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SNqSvnFwRBz3D76n476yw6 --- core/libs/prx/libc/src/RuneLocale.cpp | 6 +++++- core/libs/tests/GuestRuneLocale.cpp | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp index 4f3b67a702..da1e4ace53 100644 --- a/core/libs/prx/libc/src/RuneLocale.cpp +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include <cerrno> #include <cstddef> #include <cstdint> #include <cstring> @@ -148,7 +149,10 @@ std::uint32_t APS5_VABI towupper_nid_postfix(std::uint32_t c) { std::uint32_t APS5_VABI btowc_nid_postfix(int c) { const auto byte = static_cast<unsigned char>(c); - return c == -1 || byte >= 0x80 ? GuestRune::Weof : byte; + if (c == -1) return GuestRune::Weof; + if (byte < 0x80) return byte; + if (byte < 0xc0 || byte >= 0xf0) errno = 86; + return GuestRune::Weof; } int APS5_VABI wctob_nid_postfix(std::uint32_t c) { diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp index 5d801f27ac..ff51720da5 100644 --- a/core/libs/tests/GuestRuneLocale.cpp +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -149,5 +149,15 @@ int main() { } errno = 0; Require(towctrans_nid_postfix('A', 0) == 'A' && towctrans_nid_postfix('a', 3) == 'a' && errno == 0); + for (int c = -300; c < 0x1200; ++c) { + const auto byte = static_cast<unsigned char>(c); + errno = 0; + const std::uint32_t wide = btowc_nid_postfix(c); + const bool invalidLead = c != -1 && ((byte >= 0x80 && byte < 0xc0) || byte >= 0xf0); + Require(wide == (c != -1 && byte < 0x80 ? byte : 0xffffffffu) && errno == (invalidLead ? 86 : 0)); + } + errno = 7; + Require(btowc_nid_postfix(0xc3) == 0xffffffff && btowc_nid_postfix(0xef) == 0xffffffff && btowc_nid_postfix(-1) == 0xffffffff && errno == 7); + Require(btowc_nid_postfix(0x41) == 'A' && errno == 7); return 0; } From 1f206b59c4da66d15a1c00dfff0896ca61b27722 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 20:34:21 -0400 Subject: [PATCH 1123/1360] fix(libc): accept any setlocale category and only the C locale, as titles' libc.prx does setlocale returned a null pointer for a category outside 0 to 6 and accepted "POSIX" as a name for "C". The libc.prx that titles ship ignores the category, so setlocale(-1, NULL) and setlocale(7, "C") return "C", and returns a null pointer for "POSIX" as for any name other than "C" or the empty string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/StdioExtras.cpp | 5 ++--- core/libs/tests/GuestCharacter.cpp | 13 +++++++++---- docs/dev/debt/libc.md | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index 28b5f7b33b..fbbdd1a972 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -83,9 +83,8 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma } // Only the "C" locale exists. -const char* APS5_VABI setlocale_nid_postfix(int category, const char* locale) { - if (category < 0 || category > 6) return nullptr; - if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0 || std::strcmp(locale, "POSIX") == 0) return "C"; +const char* APS5_VABI setlocale_nid_postfix(int, const char* locale) { + if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0) return "C"; return nullptr; } diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 7601716b77..94fa6665a1 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include <array> #include <cctype> #include <clocale> #include <cstdio> @@ -29,16 +30,20 @@ int main() { for (int category = 0; category <= 6; ++category) { const char* result = setlocale_nid_postfix(category, "C"); Require(result != nullptr && std::strcmp(result, "C") == 0); - result = setlocale_nid_postfix(category, "POSIX"); - Require(result != nullptr && std::strcmp(result, "C") == 0); + Require(setlocale_nid_postfix(category, "POSIX") == nullptr); result = setlocale_nid_postfix(category, nullptr); Require(result != nullptr && std::strcmp(result, "C") == 0); result = setlocale_nid_postfix(category, ""); Require(result != nullptr && std::strcmp(result, "C") == 0); Require(setlocale_nid_postfix(category, "en_US.UTF-8") == nullptr); } - Require(setlocale_nid_postfix(-1, "C") == nullptr); - Require(setlocale_nid_postfix(7, "C") == nullptr); + for (const int category : std::array<int, 4>{-1, 7, 100, 1 << 30}) { + const char* result = setlocale_nid_postfix(category, "C"); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, nullptr); + Require(result != nullptr && std::strcmp(result, "C") == 0); + Require(setlocale_nid_postfix(category, "POSIX") == nullptr && setlocale_nid_postfix(category, "x") == nullptr); + } for (int c = EOF; c <= 255; ++c) { Require(bool(isupper_nid_postfix(c)) == bool(std::isupper(c))); Require(bool(islower_nid_postfix(c)) == bool(std::islower(c))); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..de5c794e5e 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -45,6 +45,7 @@ - [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. - [snprintf_s and vsnprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - follow C11 K.3.5.3.5 and K.3.5.3.12 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`, as `snwprintf_s` does: a null buffer or format, `size` of 0 or above `RSIZE_MAX`, any `%n` and a null `%s`, `%ls` or `%S` argument return a negative value and set `buffer[0]` to zero when the buffer and size allow it; otherwise the output is truncated to the buffer and the full length is returned. A violation only returns: the constraint handler is not invoked, and the console's error value is unverified. Not confirmed on a PS5 title - [vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Not confirmed on a PS5 title +- [setlocale](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) knows only the "C" locale, as the `libc.prx` that titles ship does: it ignores the category, returns "C" for a null, empty or "C" name and a null pointer for any other, "POSIX" included, without setting `errno`. Checked by running the copies bundled with PPSA07923 and PPSA13934 side by side for categories -40 to 79 and 2^30, not measured on a console. - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. Within a module every `_s` function calls the installed handler, with the guest's calling convention. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified - [qsort_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) sorts as `qsort` does and passes its context as the comparator's third argument, as FreeBSD 13's [`qsort_s`](https://github.com/freebsd/freebsd-src/blob/release/13.0.0/lib/libc/stdlib/qsort.c#L97-L127) (C11 Annex K) does. It returns `EINVAL` after calling the constraint handler when `n` or `es` exceeds `RSIZE_MAX`, or when `n` is not 0 and `a` or `cmp` is null; FreeBSD 11, which the rest of libc follows, has no `qsort_s`. An element size of 0 sorts nothing, where FreeBSD loops over the same address. The console's behaviour is unverified; the Quake homebrew (#6698) imports it from its `libOSMesa.so.8` From 791990ad3ec78e48faa5c5cc996fcd69cd9d1b02 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Thu, 22 Oct 2026 21:37:07 -0300 Subject: [PATCH 1124/1360] fix(libkernel): check the event flag name before the output as measured on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp | 5 ++++- core/libs/tests/GuestKernelErrors.cpp | 7 +++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 4d2e1f7d1d..698b8ceb0e 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -25,6 +25,7 @@ static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_EFAULT = 0x8002000E; static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = 0x8002003F; static constexpr int SCE_KERNEL_ERROR_ECANCELED = 0x80020055; @@ -178,7 +179,8 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui } int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { - if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; + if (!name) return SCE_KERNEL_ERROR_EFAULT; + if (std::strlen(name) >= EVF_NAME_CAPACITY) return SCE_KERNEL_ERROR_ENAMETOOLONG; try { std::unique_lock lock(eventFlagRegistryMutex); std::shared_ptr<KernelEventFlagPrivate> target; @@ -190,6 +192,7 @@ int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { break; } if (!target) return SCE_KERNEL_ERROR_ESRCH; + if (!ef) return SCE_KERNEL_ERROR_EFAULT; *ef = RegisterEventFlag(std::make_shared<EventFlagHandle>(EventFlagHandle{std::move(target), true})); } catch (const std::bad_alloc&) { return SCE_KERNEL_ERROR_ENOMEM; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index a635a36c7f..8989624672 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -224,8 +224,10 @@ int main() { } KernelEventFlag opened = nullptr; - Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EFAULT && opened == nullptr); + Require(sceKernelOpenEventFlag(&opened, "") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); + Require(sceKernelOpenEventFlag(&opened, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn") == SCE_KERNEL_ERROR_ENAMETOOLONG && opened == nullptr); Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); Require(sceKernelCloseEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); @@ -242,6 +244,7 @@ int main() { KernelEventFlag sharedTwin = nullptr; Require(sceKernelCreateEventFlag(&sharedTwin, "test_open", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EPERM && sharedTwin == nullptr); Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened != nullptr && opened != ef && opened != plain); + Require(sceKernelOpenEventFlag(nullptr, "test_open") == SCE_KERNEL_ERROR_EFAULT); KernelEventFlag openedTwice = nullptr; Require(sceKernelOpenEventFlag(&openedTwice, "test_open") == SCE_OK && openedTwice != opened && openedTwice != ef); Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4fb32ed53b..dce2237b6e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,7 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, where the console faulted; with a name it cannot open, both return `SCE_KERNEL_ERROR_ESRCH`. - [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked From c0e6c9b14f091eacf98e2263b26f39d1d8e03178 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:38:41 +0100 Subject: [PATCH 1125/1360] test(agc): add driver DLL path for packet test --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index cba05219e5..e62364794c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -464,6 +464,10 @@ if(BUILD_TESTING) target_link_libraries(agc_driver_nop_data_packet_tests PRIVATE libSceAgcDriver) configure_windows_unwind(agc_driver_nop_data_packet_tests) add_test(NAME agc_driver_nop_data_packet COMMAND agc_driver_nop_data_packet_tests) + if(WIN32) + set_tests_properties(agc_driver_nop_data_packet PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp) target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR}) From 1b1cea24a5963e9c64f85cd345d58dafbeae1f82 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:19:07 +0200 Subject: [PATCH 1126/1360] docs(tools): name ANYPS5_NID_DB in the nid_names help and test it Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- tools/nid_names.py | 2 +- tools/tests/test_nid_names.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 1818f593c9..86610f409c 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -184,7 +184,7 @@ def main(argv=None): parser = argparse.ArgumentParser( description="Suggest real names for Unknown NID stubs") parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"), - help="NID database CSV (downloaded to a cache dir by default)") + help="NID database CSV; defaults to $ANYPS5_NID_DB, then to a copy downloaded to a cache dir") parser.add_argument("--nid", action="append", default=None, help="resolve only these NIDs instead of scanning the tree") parser.add_argument("--name", "--symbol", dest="name", action="append", default=None, diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 8445352ecd..171bea2c94 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -30,6 +30,21 @@ class DatabaseCacheTests(unittest.TestCase): else: self.assertEqual(result.stdout.split(), ["NAME", "NID"]) + def test_environment_variable_selects_the_database_and_help_names_it(self): + script = str(Path(nid_names.__file__)) + helped = subprocess.run([sys.executable, script, "--help"], capture_output=True, text=True, timeout=60) + self.assertEqual(helped.returncode, 0, helped.stderr) + self.assertIn("ANYPS5_NID_DB", " ".join(helped.stdout.split())) + with tempfile.TemporaryDirectory() as directory: + database = Path(directory) / "nid database.csv" + nid = nid_names.compute_nid("sceExample") + database.write_text(f"{nid} sceExample\n", encoding="utf-8") + result = subprocess.run([sys.executable, script, "--nid", nid, "--json"], capture_output=True, text=True, + env={**os.environ, "ANYPS5_NID_DB": str(database)}, timeout=120) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("downloading", result.stderr) + self.assertIn("sceExample", result.stdout) + def test_interrupted_download_does_not_publish_a_partial_cache(self): with tempfile.TemporaryDirectory() as directory: cache = Path(directory) / "cache" / "aerolib.csv" From b26688591b36fcc084becbb628dfaf0a436c3e13 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:20:55 +0200 Subject: [PATCH 1127/1360] test(tools): keep the ANYPS5_NID_DB test off the shared cache and under 30 seconds Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- tools/tests/test_nid_names.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 171bea2c94..e317357de5 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -39,8 +39,12 @@ class DatabaseCacheTests(unittest.TestCase): database = Path(directory) / "nid database.csv" nid = nid_names.compute_nid("sceExample") database.write_text(f"{nid} sceExample\n", encoding="utf-8") + scratch = str(Path(directory) / "scratch") + Path(scratch).mkdir() + environment = {**os.environ, "ANYPS5_NID_DB": str(database), "TEMP": scratch, "TMP": scratch, "TMPDIR": scratch} result = subprocess.run([sys.executable, script, "--nid", nid, "--json"], capture_output=True, text=True, - env={**os.environ, "ANYPS5_NID_DB": str(database)}, timeout=120) + env=environment, timeout=20) + self.assertEqual(list(Path(scratch).iterdir()), []) self.assertEqual(result.returncode, 0, result.stderr) self.assertNotIn("downloading", result.stderr) self.assertIn("sceExample", result.stdout) From e9229a1f8ea5357801d6bbd4fb8b067286237716 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:15:57 +0200 Subject: [PATCH 1128/1360] docs(debt): note that a title call of sceAjmBatchJobControl agrees with the INITIALIZE flag and sizes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAjm.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index c7d4e74ca1..c51bdb33e6 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -12,7 +12,7 @@ - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`, and PPSA02225's one call agrees with that layout (`rdi`, `esi`, `xmm0` and `xmm1` as two single floats, `edx`, `rcx`) without showing `flags` or the float roles. With a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. -- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw +- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (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); the one call in PPSA25936 passes the flags 0x4000 (bit 14 only), an 8-byte input and an 8-byte output, which agrees with INITIALIZE alone and the 8-byte parameters, without showing the codec or the meaning of the words; 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 and M4AAC, 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 From d2927e0ec12d75ac5bfbc928f652effd39a8889a Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Fri, 23 Oct 2026 14:19:46 +1300 Subject: [PATCH 1129/1360] fix(libc): follow libc.prx for strtof signs and hexadecimal round-up ERANGE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SNqSvnFwRBz3D76n476yw6 --- core/libs/prx/libc/src/Strings.cpp | 9 ++++++--- core/libs/tests/GuestMath.cpp | 24 +++++++++++++++++++----- core/libs/tests/GuestWideStrings.cpp | 6 +++++- docs/dev/debt/libc.md | 2 +- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index cb3ab774ae..5d88f8c55e 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -153,7 +153,7 @@ TFloat NanValue(const NanSubject& subject, const char*& end) { return std::bit_cast<double>((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); } else if constexpr (std::is_same_v<TFloat, float>) { end = NanSequenceEnd(subject.tail); - return std::bit_cast<float>(0x7fc00000u); + return std::bit_cast<float>((subject.negative ? 0x80000000u : 0) | 0x7fc00000u); } else { end = NanSequenceEnd(subject.tail); TFloat value{}; @@ -190,8 +190,10 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha const char* subject = text; while (std::isspace(static_cast<unsigned char>(*subject))) ++subject; if (parsedEnd == text) { + if (subject[0] != '-') return TFloat{0}; + if constexpr (std::is_same_v<TFloat, float>) return subject[1] == '\0' ? TFloat{0} : -TFloat{0}; const bool special = std::strchr("iInN", subject[1]) != nullptr && subject[1] != '\0'; - return !std::is_same_v<TFloat, double> && subject[0] == '-' && !special ? -TFloat{0} : TFloat{0}; + return std::is_same_v<TFloat, long double> && !special ? -TFloat{0} : TFloat{0}; } if (*subject == '+' || *subject == '-') ++subject; const bool hexadecimal = subject[0] == '0' && (subject[1] == 'x' || subject[1] == 'X') && parsedEnd > subject + 2; @@ -199,7 +201,8 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha const float magnitude = value < 0 ? -value : value; if (value != 0 && magnitude < std::numeric_limits<float>::min()) { const long double exact = std::strtold(text, nullptr); - errno = !hexadecimal && static_cast<long double>(value) != exact ? ERANGE : saved; + const bool roundedUp = magnitude == std::numeric_limits<float>::denorm_min() && (exact < 0 ? -exact : exact) < magnitude; + errno = hexadecimal ? (roundedUp ? ERANGE : saved) : (static_cast<long double>(value) != exact ? ERANGE : saved); } return value; } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index c66c945324..86b42f45d2 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -437,8 +437,9 @@ static void CheckNanConversions() { std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } + const std::uint32_t expectedSingle = (test.bits >> 63 != 0 ? 0x80000000u : 0) | 0x7fc00000u; const auto single = std::bit_cast<std::uint32_t>(strtof_nid_postfix(test.text, &end)); - if (single != 0x7fc00000u || end != test.text + test.sequenceUsed) { + if (single != expectedSingle || end != test.text + test.sequenceUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast<unsigned>(single)); correct = false; } @@ -461,7 +462,7 @@ static void CheckNanConversions() { } if (test.wideOtherMeasured) { const auto wideSingle = std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wide, &wideEnd)); - const bool singleOk = wideSingle == 0x7fc00000u && wideEnd == wide + test.sequenceUsed; + const bool singleOk = wideSingle == expectedSingle && wideEnd == wide + test.sequenceUsed; const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); @@ -509,6 +510,12 @@ static void CheckFloatUnderflowErrno() { Require(strtof_nid_postfix("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", &end) == 0x1p-149f && strtof_nid_postfix("1.17549435e-38", &end) == 0x1p-126f && errno == 7); Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && errno == ERANGE); errno = 7; + Require(strtof_nid_postfix("0x1.8p-150", &end) == 0x1p-149f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("-0x0.0000018p-126", &end) == -0x1p-149f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("0x1.4p-149", &end) == 0x1p-149f && strtof_nid_postfix("0x1.cp-149", &end) == 0x1p-148f && errno == 7); + errno = 7; Require(strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == ERANGE); errno = 7; Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == ERANGE); @@ -1259,13 +1266,14 @@ static void CheckLongDoubleRounding(long double value, std::int64_t expected, bo } static void CheckNoConversionZero() { - for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { + for (const char* text : {"", " ", "-", " -", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; const double parsed = strtod_nid_postfix(text, &end); Require(parsed == 0. && !std::signbit(parsed) && end == text); const bool negative = std::strchr(text, '-') != nullptr; + const bool loneSign = negative && text[std::strlen(text) - 1] == '-'; const float parsedFloat = strtof_nid_postfix(text, &end); - Require(parsedFloat == 0.f && std::signbit(parsedFloat) == negative && end == text); + Require(parsedFloat == 0.f && std::signbit(parsedFloat) == (negative && !loneSign) && end == text); const long double parsedLong = strtold_nid_postfix(text, &end); Require(parsedLong == 0.L && std::signbit(parsedLong) == negative && end == text); Require(!std::signbit(atof_nid_postfix(text))); @@ -1273,9 +1281,15 @@ static void CheckNoConversionZero() { for (const char* text : {"-i", "-in", "-N", "-nax"}) { char* end = nullptr; const float parsedFloat = strtof_nid_postfix(text, &end); - Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); + Require(parsedFloat == 0.f && std::signbit(parsedFloat) && end == text); Require(!std::signbit(strtold_nid_postfix(text, nullptr))); } + for (const char* text : {"-nan", "-NAN", " -nan(5)"}) { + char* end = nullptr; + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(std::bit_cast<std::uint32_t>(parsedFloat) == 0xffc00000u && end == text + std::strlen(text)); + } + Require(std::bit_cast<std::uint32_t>(strtof_nid_postfix("nan", nullptr)) == 0x7fc00000u); char* end = nullptr; const char negativeZero[] = "-0.0x"; const double parsed = strtod_nid_postfix(negativeZero, &end); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 268d223c37..36e299c22f 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -365,11 +365,15 @@ int main() { require(wcstof_nid_postfix(u"1e-45", nullptr) == 0x1p-149f && *__error_nid_postfix() == ERANGE); *__error_nid_postfix() = 0; require(wcstof_nid_postfix(u"0x1p-149", nullptr) == 0x1p-149f && *__error_nid_postfix() == 0); + require(wcstof_nid_postfix(u"0x1.8p-150", nullptr) == 0x1p-149f && *__error_nid_postfix() == ERANGE); + const char16_t* loneSign = u" -"; + const float loneSignValue = wcstof_nid_postfix(loneSign, &end); + require(loneSignValue == 0.f && !std::signbit(loneSignValue) && end == loneSign && std::signbit(wcstold_nid_postfix(loneSign, &end))); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; require(std::bit_cast<std::uint64_t>(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); - require(std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wideNan, &end)) == 0x7fc00000u && end == wideNan + 12); + require(std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e9cd7991bc..9471b1b5a8 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -62,7 +62,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced -- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one never does, and underflow to zero and overflow set it. Both builds also read a `NAN(...)` sequence in `strtof` as their `strtod` does, keep the sign and take a payload from its leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); that is not reproduced. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one only when it lies below the smallest subnormal and rounds up to it, and underflow to zero and overflow set it. Both builds also take a `NAN(...)` payload in `strtof` from the sequence's leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); the sign is kept here, the payload is not. `libSceLibcInternal`'s `_Stof` differs: no `ERANGE` for hexadecimal input, a positive `nan`, and after a minus sign a failed parse is `-0.0` for a lone sign and `+0.0` before an `inf`/`nan` spelling, the reverse of `libc.prx`. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. From 4d05702cdd4654152318ec9149ef733d3098467d Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:15:25 +0200 Subject: [PATCH 1130/1360] docs(debt): note that a title call of sceAjmBatchJobGetCodecInfo agrees with the result size Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAjm.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 65ee52f866..7797276a0f 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -8,7 +8,7 @@ - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. - [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: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, 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 +- [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 (the one call in PPSA04354 passes a 24-byte result buffer, the 8-byte result plus the 16-byte ATRAC9 codec info written here; the call does not show the codec or the fields); a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`, and PPSA02225's one call agrees with that layout (`rdi`, `esi`, `xmm0` and `xmm1` as two single floats, `edx`, `rcx`) without showing `flags` or the float roles. With a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. From 71932c0a56447615c74c5edca1e1272c5ec8e870 Mon Sep 17 00:00:00 2001 From: sivasdeadknight <sivasdeadknight@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:55:01 +0200 Subject: [PATCH 1131/1360] fix(libSceNpManager): validate request ids for abort, delete and poll sceNpAbortRequest and sceNpDeleteRequest return SCE_NP_ERROR_REQUEST_NOT_FOUND for an id that was never created or was deleted; sceNpPollAsync does too and additionally rejects a null result pointer with SCE_NP_ERROR_INVALID_ARGUMENT before reading the id, without writing the caller's result. A live request keeps its signed-out completion. This pins the validation half of shadPS4's np_manager model that issue #9277 reports; the kind, completion state and WaitAsync publication half stays open, documented in the debt entry, and the sceNpWaitAsync export itself stays with PR #9282. Tests: guest_np_manager tests the four absent/deleted probes (0, -1, 0x7fffffff, a deleted id) for all three calls, output preservation, null-result precedence and the live-id paths; guest_np_reachability unchanged. Both pass on Linux; the new asserts failed on unpatched exports first. --- core/libs/prx/libSceNpManager/Export.cpp | 11 +++++---- core/libs/tests/GuestNpManager.cpp | 30 ++++++++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 5 ++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 15fd862b97..c50ab5b01f 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -673,8 +673,8 @@ int APS5_VABI sceNpManagerIntLoginGetAccessToken(void) { } int APS5_VABI sceNpAbortRequest(int req_id) { - (void)req_id; - return 0; + std::lock_guard lock(requestMutex); + return liveRequests.count(req_id) ? 0 : SCE_NP_ERROR_REQUEST_NOT_FOUND; } int APS5_VABI sceNpCheckCallback(void) { @@ -724,6 +724,7 @@ int APS5_VABI sceNpCreateRequest(void) { int APS5_VABI sceNpDeleteRequest(int req_id) { std::lock_guard lock(requestMutex); + if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND; liveRequests.erase(req_id); return 0; } @@ -834,8 +835,10 @@ int APS5_VABI sceNpHasSignedUp(int user_id, bool* has_signed_up) { } int APS5_VABI sceNpPollAsync(int req_id, int* result) { - (void)req_id; - if (result) *result = SCE_NP_ERROR_SIGNED_OUT; + if (!result) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(requestMutex); + if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND; + *result = SCE_NP_ERROR_SIGNED_OUT; return NP_POLL_ASYNC_FINISHED; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 67e63ff3b0..ef285cab3a 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -33,6 +33,8 @@ extern "C" { int APS5_VABI sceNpCreateRequest(void); int APS5_VABI sceNpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param); int APS5_VABI sceNpDeleteRequest(int reqId); +int APS5_VABI sceNpAbortRequest(int reqId); +int APS5_VABI sceNpPollAsync(int reqId, int* result); int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); } @@ -238,6 +240,33 @@ void CheckRequestTimeouts() { Require(sceNpSetTimeout(asyncId, 2, 0, 0, 0, 0) == RequestNotFound, "deleted async request"); } +void CheckAsyncRequestLifecycle() { + NpCreateAsyncRequestParameter param{}; + const int unusedAsyncResult = sceNpCreateAsyncRequest(¶m); + Require(unusedAsyncResult > 0, "sceNpCreateAsyncRequest must return a positive request id"); + const int deletedId = sceNpCreateAsyncRequest(¶m); + Require(sceNpDeleteRequest(deletedId) == 0, "initial delete"); + + constexpr int resultSentinel = 0x5a5a5a5a; + const int unknownIds[] = {0, -1, 0x7fffffff, deletedId}; + for (const int id : unknownIds) { + int result = resultSentinel; + Require(sceNpPollAsync(id, &result) == RequestNotFound, "poll must reject an absent or deleted id"); + Require(result == resultSentinel, "poll must not touch the result for an absent or deleted id"); + Require(sceNpPollAsync(id, nullptr) == InvalidArgument, + "poll must reject a null result before the id"); + Require(sceNpAbortRequest(id) == RequestNotFound, "abort must reject an absent or deleted id"); + Require(sceNpDeleteRequest(id) == RequestNotFound, "delete must reject an absent or deleted id"); + } + + int result = resultSentinel; + Require(sceNpPollAsync(unusedAsyncResult, &result) == 0, "poll on a live async request"); + Require(result == SignedOut, "poll must report the signed-out result on a live async request"); + Require(sceNpAbortRequest(unusedAsyncResult) == 0, "abort on a live request"); + Require(sceNpDeleteRequest(unusedAsyncResult) == 0, "delete after abort"); + Require(sceNpDeleteRequest(unusedAsyncResult) == RequestNotFound, "second delete"); +} + void CheckOnlineIdQueries() { NpOnlineId onlineId{}; std::memcpy(onlineId.data, "player", 6); @@ -394,6 +423,7 @@ int main() { CheckStateCallbacksA(); CheckLegacyStateCallback(); CheckRequestTimeouts(); + CheckAsyncRequestLifecycle(); CheckPlusAndContentRestriction(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index a484f93b3a..0595fc92c4 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -29,3 +29,8 @@ - [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 - [sceNpCheckCallbackForLib](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a PS5: it delivers the pending state callbacks like `sceNpCheckCallback`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp) does with its pending state events; shadPS4's `sceNpCheckCallback` also runs other registered NP callbacks, which this library does not keep + +- [sceNpAbortRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` and a live request returns 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpAbortRequest`; shadPS4 there also moves the request into an `Aborted` state that its later requests read as `ABORTED`, and whether aborting on the console changes a live request's completion is not measured on a PS5 ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)), so nothing in this library reads an abort state and the id stays live +- [sceNpDeleteRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND`, and a live request is removed and returns 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpDeleteRequest` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)) +- [sceNpPollAsync](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a null result pointer returns `SCE_NP_ERROR_INVALID_ARGUMENT` before the id is read, a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` without writing the caller's result, and a live request keeps its signed-out completion `0` with `SCE_NP_ERROR_SIGNED_OUT`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpPollAsync` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)). The parts of that model beyond the validation are not implemented: shadPS4 keeps a per-request state machine decorated by `sceNpCheckNpAvailability` and neighbors and refuses `InvalidId` for a ready async request in wait/poll, whose completion publication, poll-versus-wait precedence and already-complete errors remain unattributable to the PS5 +- [sceNpCreateAsyncRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the parameter is not read ([NpCreateAsyncRequestParameter](../../../core/libs/SceTypes.hpp) is a 0x40-byte opaque blob, so shadPS4's nonnull check and `size == sizeof` reject ([81be949167d0060b08f26dc3e055a0b640d129c8](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp)) cannot be applied without inventing a layout) and no async kind or completion state is stored until the console's contract for them is evidenced ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)); `sceNpWaitAsync` (`jyi5p9XWUSs`) has no export here yet and stays with [PR #9282](https://github.com/boykopovar/AnyPS5/pull/9282) From e4d512bbbd8af3133afe7a9bf11198e8b7c82483 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 00:45:47 -0400 Subject: [PATCH 1132/1360] feat(libSceJson2): implement sce::Json::Array::pop_front Graven (PPSA09614) imports sce::Json::Array::pop_front() (pPs3qZTZQvQ) from one call site of its executable, and no built library exported it, so tools/import_audit.py lists it as absent and the loader stops before the title starts. pop_front removes the first element and destroys it, like clear does for every element. On an empty array it throws std::out_of_range, as front and back do here; the console's result there is unverified and listed in the debt file. guest_json pops both elements of a two-element copy, checks the remaining element and the empty array, and expects the throw on a third pop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceJson2/Export.cpp | 5 +++++ core/libs/tests/GuestJson.cpp | 14 ++++++++++++++ docs/dev/debt/libSceJson2.md | 1 + 3 files changed, 20 insertions(+) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 51c4bb6730..a84b073cd9 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -763,6 +763,11 @@ const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { if (self->items->empty()) throw std::out_of_range("sce::Json::Array::back: empty array"); return &self->items->back(); } +void APS5_VABI _ZN3sce4Json5Array9pop_frontEv(Array* self) { + if (self->items->empty()) throw std::out_of_range("sce::Json::Array::pop_front: empty array"); + Destroy(self->items->front()); + self->items->pop_front(); +} void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { delete self->state; self->state = nullptr; } ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { if (self->state && self->state->position != self->state->end) ++self->state->position; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 7c59af4225..b2951558b6 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -60,6 +60,8 @@ Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); Iterator* APS5_VABI _ZNK3sce4Json5Array3endEv(Iterator*, const Array*); std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array*); const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array*); +void APS5_VABI _ZN3sce4Json5Array9pop_frontEv(Array*); +bool APS5_VABI _ZNK3sce4Json5Array5emptyEv(const Array*); void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(Iterator*); Iterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(Iterator*); Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const Iterator*); @@ -360,6 +362,18 @@ static void ObjectsAndArrays() { Require(_ZNK3sce4Json5Array4sizeEv(©) == 2); Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(©))); Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array4backEv(©)) == 2.5); + _ZN3sce4Json5Array9pop_frontEv(©); + Require(_ZNK3sce4Json5Array4sizeEv(©) == 1); + Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array5frontEv(©)) == 2.5); + _ZN3sce4Json5Array9pop_frontEv(©); + Require(_ZNK3sce4Json5Array5emptyEv(©)); + threw = false; + try { + _ZN3sce4Json5Array9pop_frontEv(©); + } catch (const std::out_of_range&) { + threw = true; + } + Require(threw && _ZNK3sce4Json5Array5emptyEv(©)); _ZN3sce4Json5ArrayD1Ev(©); _ZN3sce4Json5ArrayD1Ev(&array); } diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 706f8c4594..9c411149b3 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -3,6 +3,7 @@ ### Unknown function info - [JSON real-number conversion](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) uses locale-independent `to_chars` with 17 significant digits and `from_chars`, following JSON's decimal-point grammar in RFC 8259 section 6. Out-of-range input falls back to a classic-locale stream so underflow retains the prior signed-zero behavior and overflow throws. Console rounding and formatting are unverified. +- [sce::Json::Array::pop_front](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - removes and destroys the first element; on an empty array it throws `std::out_of_range`, as `front` and `back` do here. The console's result for an empty array is unverified. - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. From 6ba488116707f4b2e28fa4687d0050fe408f5a89 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:01:50 +0200 Subject: [PATCH 1133/1360] test(recompiler): keep the shader cache source test at its 180 s CTest timeout CONTRIBUTING asks to split or shrink a test that gets close instead of raising its timeout; the subprocess timeouts of the previous commit stay. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7dd610b9f5..9eaa66c8db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,7 +135,7 @@ if(BUILD_TESTING) add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND}) - set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 600) + set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} From e118657d4d2c7c52524e2041a348c28bf12d8fd7 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Fri, 23 Oct 2026 02:34:13 +0000 Subject: [PATCH 1134/1360] fix(agc): share a registered shader's decoded state with its other headers Every registration decodes the header's registers into a RegisteredShaderState: three dense register sets, about 10 KiB, kept by the snapshot for the life of the registry. A title that registers the same code with the same registers at another header address got a copy each time. Ghost of Tsushima does that for 85 % of its registrations, about 400 a second during its opening, so by 200 s 68,000 of its 80,000 snapshots held a copy of a state another snapshot already had. A new snapshot now takes the state of a published snapshot at the same code address whose type, code and decoded registers are equal. The state is immutable once published, and its users only copy it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../include/Driver/Shaders/ShaderRegistry.hpp | 2 ++ .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 12 ++++++++++++ .../tests/execution/PreparedShaders.cpp | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 203940d7c3..d24d81c3eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -165,6 +165,8 @@ struct RegisteredShaderState { Registers shader; Registers context; Registers userConfig; + + bool operator==(const RegisteredShaderState&) const = default; }; struct ShaderSnapshot { 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 5f93b08490..be8fb88cff 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1854,6 +1854,18 @@ void Driver::RegisterShader(const Shader* shader) { localDevice = device; } snapshot.registeredState = std::make_shared<const RegisteredShaderState>(DecodeRegisteredState(snapshot)); + { + std::lock_guard lock(mutex); + if (shaders != nullptr) { + if (const auto found = shaders->find(snapshot.codeAddress); found != shaders->end()) { + for (const auto& current : found->second) { + if (current->registeredState == nullptr || current->type != snapshot.type || current->code != snapshot.code || *current->registeredState != *snapshot.registeredState) continue; + snapshot.registeredState = current->registeredState; + break; + } + } + } + } QueueState registered{}; registered.shader = snapshot.registeredState->shader; registered.context = snapshot.registeredState->context; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 734a7c6825..8d0c2eab31 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -437,6 +437,15 @@ void SharedCodeHeaders() { AgcDriverRegisterShader_nid_postfix(&mesh.shader); AgcDriverResolveShaderAbi_nid_postfix(&vertex.shader, {}, primitive); AgcDriverRegisterShader_nid_postfix(&mesh.shader); + const auto snapshots = [&] { + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "shared-code headers published no shader registry"); + auto pair = std::pair{AgcDriver::DriverDetail::RegisteredHeader(*registry, &vertex.shader), AgcDriver::DriverDetail::RegisteredHeader(*registry, &mesh.shader)}; + Require(pair.first != nullptr && pair.second != nullptr && pair.first != pair.second, "the shared-code headers were not registered apart"); + return pair; + }; + const auto [vertexSnapshot, meshSnapshot] = snapshots(); + Require(vertexSnapshot->registeredState != meshSnapshot->registeredState, "a header with other registers shared another header's registered state"); Shader copy = vertex.shader; copy.user_data = nullptr; AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); @@ -461,6 +470,8 @@ void SharedCodeHeaders() { mesh.shader.cx_registers = mesh.context.data(); mesh.shader.num_cx_registers = mesh.context.size(); AgcDriverRegisterShader_nid_postfix(&mesh.shader); + const auto [sameVertex, sameMesh] = snapshots(); + Require(sameVertex->registeredState == sameMesh->registeredState && sameMesh->registeredState->context.at(0x2d5) == 0x2000u, "two headers with the same code and registers decoded their registered state twice"); AgcDriverResolveShaderAbi_nid_postfix(&mesh.shader, {}, primitive); AgcDriverResolveShaderAbi_nid_postfix(&vertex.shader, {}, primitive); Require(sceAgcDriverSubmitDcb(&packet) == 0, "same-type registered header draw failed"); From 45bc4321632af092d7b16ba61970007bc4483909 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 <best.dv1996+github@gmail.com> Date: Fri, 23 Oct 2026 08:06:11 +0530 Subject: [PATCH 1135/1360] fix(libSceNpCppWebApi): copy sources in IntrusivePtr helpers and use the vector count offset --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 974a7f0403..c8eec1ea1a 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -2412,7 +2412,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, 0x00, __func__); + ReleaseIntrusivePtr(self, 0x30, __func__); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { @@ -2436,8 +2436,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessio return VectorPushBackIntrusivePtr(self, static_cast<const IntrusivePtr*>(element)); } -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEC1ERS7_(IntrusivePtr* self) { - *self = {}; +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { @@ -2457,8 +2457,8 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 return self->object; } -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V160PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEC1ERS7_(IntrusivePtr* self) { - *self = {}; +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V160PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); } void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V120PlayerSessionForReadEEEEEEptEv(const IntrusivePtr* self) { @@ -2694,12 +2694,12 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 *self = {}; } -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V143PutPlayerSessionsSessionIdLeaderRequestBodyEEC1ERS7_(IntrusivePtr* self) { - *self = {}; +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V143PutPlayerSessionsSessionIdLeaderRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); } -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V139PatchPlayerSessionsSessionIdRequestBodyEEC1ERS7_(IntrusivePtr* self) { - *self = {}; +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V139PatchPlayerSessionsSessionIdRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); } void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEptEv(const IntrusivePtr* self) { @@ -2720,12 +2720,12 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { - ReleaseIntrusivePtr(self, 0x00, __func__); - CopyIntrusivePtr(self, source, 0x00); + ReleaseIntrusivePtr(self, 0x30, __func__); + CopyIntrusivePtr(self, source, 0x30); } -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEEC1ERS7_(IntrusivePtr* self) { - *self = {}; +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); } void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEE3getEv(const IntrusivePtr* self) { @@ -2923,7 +2923,7 @@ std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrIN } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V120PlayerSessionForReadEEEEEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, 0x00, __func__); + ReleaseIntrusivePtr(self, 0x30, __func__); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEEE5beginEv() { @@ -3132,8 +3132,8 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InG return 0; } -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1ERKSA_(IntrusivePtr* self) { - *self = {}; +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1ERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x30); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1ERKSE_(void* self) { @@ -3176,8 +3176,8 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGam } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { - ReleaseIntrusivePtr(self, 0x00, __func__); - CopyIntrusivePtr(self, source, 0x00); + ReleaseIntrusivePtr(self, 0x30, __func__); + CopyIntrusivePtr(self, source, 0x30); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv() { @@ -3226,7 +3226,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv() { } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, const char* other) { - return self->buffer != nullptr && other != nullptr && std::strcmp(self->buffer, other) == 0; + return other != nullptr && std::strcmp(self->buffer != nullptr ? self->buffer : "", other) == 0; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv() { From 7d8bbff86da8faf88b572fae553f39eb05ffc6e5 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:13:47 +0800 Subject: [PATCH 1136/1360] feat(libScePad): implement Lotus IR key toggles and virtual-device settings scePadSetLotusEnableIrKey and scePadSetLotusDisableIrKey validate the pad handle, returning PAD_ERROR_INVALID_HANDLE (0x80920002) for any invalid handle and 0 on open pads as silent stubs. scePadVirtualDeviceDisableButtonRemapping acts as a silent stub returning 0. scePadVirtualDeviceGetRemoteSetting recovers the 64-byte remote setting layout, rejecting null buffers and non-positive device IDs with PAD_ERROR_INVALID_ARG (0x80920001) and returning PAD_ERROR_DEVICE_NOT_CONNECTED (0x80920007). --- core/libs/prx/libScePad/Export.cpp | 20 +++++++++++--------- core/libs/tests/GuestPad.cpp | 15 +++++++++++++++ docs/dev/debt/libScePad.md | 3 +++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 057d434611..dd6c2394f3 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -22,7 +22,10 @@ struct PadUsbDetectionState; struct PadVrUsbConnectionStatus; struct PadDeviceInfo; struct PadLicenseControllerInformation; -struct PadRemoteSetting; +struct PadRemoteSetting { + std::uint8_t data[64]; +}; +static_assert(sizeof(PadRemoteSetting) == 64); struct PadVirtualTriggerSetting; struct PadProcessFocusParameter; struct PadDataExtended; @@ -663,13 +666,13 @@ int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* r } int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + return PAD_OK; } int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + return PAD_OK; } int APS5_VABI scePadGetUsbDetectionState(std::int32_t deviceId, PadUsbDetectionState* state) { @@ -1018,8 +1021,8 @@ int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(std::int32_t deviceI } int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t deviceId, PadRemoteSetting* setting) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (setting == nullptr || deviceId <= 0) return PAD_ERROR_INVALID_ARG; + return PAD_ERROR_DEVICE_NOT_CONNECTED; } int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, const PadVirtualTriggerSetting* setting) { @@ -1028,8 +1031,7 @@ int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, co } int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool disabled) { - NotImplemented_nid_no_patch(__func__); - return 0; + return PAD_OK; } int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 6dfa8c9a9c..2783ffce72 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -38,6 +38,10 @@ int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); int APS5_VABI scePadSetMotionSensorState(int, bool); +int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t); +int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t); +int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool); +int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -502,4 +506,15 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetLotusEnableIrKey(handle) == PAD_OK); + Require(scePadSetLotusEnableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetLotusDisableIrKey(handle) == PAD_OK); + Require(scePadSetLotusDisableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadVirtualDeviceDisableButtonRemapping(true) == PAD_OK); + Require(scePadVirtualDeviceDisableButtonRemapping(false) == PAD_OK); + std::array<std::uint8_t, 64> remoteSetting{}; + Require(scePadVirtualDeviceGetRemoteSetting(1, remoteSetting.data()) == static_cast<int>(0x80920007)); + Require(scePadVirtualDeviceGetRemoteSetting(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadVirtualDeviceGetRemoteSetting(0, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); + Require(scePadVirtualDeviceGetRemoteSetting(-1, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index c8fa44cf9d..e740790bfe 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -3,6 +3,8 @@ ### Silent stubs - [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing extension-port state. The boolean argument is recovered; connected-device behavior is not modeled. +- [scePadSetLotusEnableIrKey and scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) return success on valid open handle without hardware Lotus IR key dispatching. +- [scePadVirtualDeviceDisableButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing virtual-device remapping tables. ### Unknown function info @@ -20,6 +22,7 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown +- [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - checks the output pointer and non-zero device id, returning `PAD_ERROR_DEVICE_NOT_CONNECTED` (0x80920007) when no virtual device is connected. Remote setting payload uses a 64-byte record whose internal field meanings remain unverified on hardware. - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. From 6eb461aa4dd9ef86ebbda66e103b28f71c921d1e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:13:56 +0200 Subject: [PATCH 1137/1360] fix(tools): report an output directory that progress.py cannot write progress.py <output> raised an uncaught OSError with a traceback when the output path was a file or sat in a directory it could not create. Report it as a usage error that names the path, like the other bad arguments. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- tools/progress.py | 5 ++++- tools/tests/test_progress.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/progress.py b/tools/progress.py index 944ba4170b..153975e4d5 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -526,7 +526,10 @@ def main(argv=None): except ValueError as error: parser.error(str(error)) if args.output: - write_site(args.output, libraries, shaders) + try: + write_site(args.output, libraries, shaders) + except OSError as error: + parser.error(f"{args.output}: {error.strerror or error}") if args.json: print(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2)) else: diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 1db231c27f..f018237531 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -497,6 +497,17 @@ class ProgressTests(unittest.TestCase): self.assertEqual(raised.exception.code, 2) self.assertIn(str(path), stderr.getvalue()) + def test_output_directory_that_cannot_be_written_is_named(self): + with tempfile.TemporaryDirectory() as directory: + blocked = Path(directory) / "blocked output" + blocked.write_text("a file, not a directory", encoding="utf-8") + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main([str(blocked)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn(str(blocked), stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + def test_root_must_be_a_source_tree(self): with tempfile.TemporaryDirectory() as directory: empty = Path(directory) / "empty tree" From c0024158db26ef721643a3f76947480a118c1ac2 Mon Sep 17 00:00:00 2001 From: x <x@x> Date: Thu, 22 Oct 2026 21:51:15 -0500 Subject: [PATCH 1138/1360] fix(libSceFont): measure glyphs through a zero-extent render surface Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLz9JYSXCPssYHgRW1CxJH --- core/libs/prx/libSceFont/src/Layout.cpp | 3 ++- core/libs/tests/GuestSystemFont.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceFont.md | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index a59a3b5f2f..523350a7f2 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -120,7 +120,8 @@ void FillImageMetrics(FontRenderOutput* result, const FontGlyphMetrics* metrics, int RenderGlyphIndexToSurface(FontObj& obj, std::uint32_t glyphIndex, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { ClearRenderOutputs(metrics, result); - if (!surface->buffer || surface->width <= 0 || surface->height <= 0 || surface->widthByte <= 0 || surface->pixelSizeByte <= 0) return SCE_FONT_ERROR_NO_SUPPORT_SURFACE; + const bool emptyExtent = surface->width == 0 || surface->height == 0; + if (!surface->buffer || surface->width < 0 || surface->height < 0 || surface->widthByte < 0 || (!emptyExtent && surface->widthByte == 0) || surface->pixelSizeByte <= 0) return SCE_FONT_ERROR_NO_SUPPORT_SURFACE; const int bytesPerPixel = surface->pixelSizeByte; if (bytesPerPixel != 1 && bytesPerPixel != 4) return SCE_FONT_ERROR_NO_SUPPORT_SURFACE; const auto face = static_cast<FT_Face>(obj.ft_face); diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 392d3ad044..f11c62d607 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -338,6 +338,24 @@ int main() { Require(RenderedPixels(mixed, 0x4E00, advance) > 0 && advance == 63.0f); Require(RenderedPixels(mixed, 0x0627, advance) > 0 && advance == 66.0f); Require(RenderedPixels(mixed, 0xAC00, advance) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + std::vector<std::uint8_t> untouched(128 * 128); + FontRenderSurface measuring{}; + sceFontRenderSurfaceInit(&measuring, untouched.data(), 0, 1, 0, 31); + FontGlyphMetrics measured{}; + FontRenderOutput measuredResult{}; + Require(sceFontRenderCharGlyphImageHorizontal(mixed, 'A', &measuring, 10.0f, 100.0f, &measured, &measuredResult) == SCE_FONT_OK); + Require(measured.Horizontal.advance == 60.0f && measured.width > 0.0f && measuredResult.ImageMetrics.advance > 0.0f); + Require(measuredResult.UpdateRect.w == 0 && measuredResult.UpdateRect.h == 0); + for (const std::uint8_t pixel : untouched) Require(pixel == 0); + sceFontRenderSurfaceInit(&measuring, untouched.data(), 128, 1, 128, 0); + Require(sceFontRenderCharGlyphImageHorizontal(mixed, 'A', &measuring, 10.0f, 100.0f, &measured, &measuredResult) == SCE_FONT_OK && measured.Horizontal.advance == 60.0f); + sceFontRenderSurfaceInit(&measuring, untouched.data(), 128, 1, 128, 128); + measuring.width = -1; + Require(sceFontRenderCharGlyphImageHorizontal(mixed, 'A', &measuring, 10.0f, 100.0f, &measured, &measuredResult) == SCE_FONT_ERROR_NO_SUPPORT_SURFACE); + sceFontRenderSurfaceInit(&measuring, untouched.data(), 0, 1, 128, 128); + Require(sceFontRenderCharGlyphImageHorizontal(mixed, 'A', &measuring, 10.0f, 100.0f, &measured, &measuredResult) == SCE_FONT_ERROR_NO_SUPPORT_SURFACE); + sceFontRenderSurfaceInit(&measuring, nullptr, 0, 1, 0, 31); + Require(sceFontRenderCharGlyphImageHorizontal(mixed, 'A', &measuring, 10.0f, 100.0f, &measured, &measuredResult) == SCE_FONT_ERROR_NO_SUPPORT_SURFACE); Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index b1b5e56398..3158492e1f 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceFontRenderCharGlyphImage, sceFontRenderCharGlyphImageHorizontal, sceFontRenderCharGlyphImageVertical](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) accept a surface whose width or height is 0, with any row stride, and return the glyph metrics and an empty update rectangle without writing to the surface. A negative width, height or stride, a zero stride on a surface with pixels, a null buffer and a pixel size other than 1 or 4 bytes still return `SCE_FONT_ERROR_NO_SUPPORT_SURFACE`; those cases and the exact console result for a zero-extent surface are not measured. The extent-0 behaviour is inferred from the Tekken Dark Resurrection PSP emulator which, to measure a string, initialises the surface with width 0 and height 31 and an unbound stride, renders each character and sums the returned advances. + - [Typographic design](../../../core/libs/prx/libSceFont/src/Style.cpp): native selector, feature, font-set eligibility and output-clearing rules are recovered. Driver typography selection and queries, feature-cache updates and glyph substitution remain unimplemented and throw instead of storing arbitrary selector/value pairs. - [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [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 From 12a14ac1a517863335603d6d5762cec1ff1ac4f1 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:03:49 +0100 Subject: [PATCH 1139/1360] feat(libSceAgcVsh): declare 150 pending exports --- core/libs/prx/libSceAgcVsh/Export.cpp | 900 +++++++++++++++++++++++++- docs/dev/debt/libSceAgcVsh.md | 5 + 2 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceAgcVsh.md diff --git a/core/libs/prx/libSceAgcVsh/Export.cpp b/core/libs/prx/libSceAgcVsh/Export.cpp index fb34206c21..0b65c5fa98 100644 --- a/core/libs/prx/libSceAgcVsh/Export.cpp +++ b/core/libs/prx/libSceAgcVsh/Export.cpp @@ -5,6 +5,904 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAgcDcbCopyData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1rZSWUv1IRc", sceAgcDcbCopyData_nid_no_patch); + +int APS5_VABI sceAgcBranchPatchSetElseTarget_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("QmfvaYpsOcI", sceAgcBranchPatchSetElseTarget_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegistersDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("UZbQjYAwwXM", sceAgcCbSetShRegistersDirect_nid_no_patch); + +int APS5_VABI sceAgcCreateShader_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("f3dg2CSgRKY", sceAgcCreateShader_nid_no_patch); + +int APS5_VABI sceAgcDcbSetNumInstancesGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("6DFuRKT4C9w", sceAgcDcbSetNumInstancesGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetUcRegisterDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("aP1Ki9G3++4", sceAgcDcbSetUcRegisterDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirectMultiGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("r98I08t+LOg", sceAgcDcbDrawIndexIndirectMultiGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicGdsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1tB0xkLNjcw", sceAgcDcbAtomicGdsGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbCopyDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("CbQh3DKMSno", sceAgcAcbCopyDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbWaitUntilSafeForRendering_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GPbUp9jXQa8", sceAgcAcbWaitUntilSafeForRendering_nid_no_patch); + +int APS5_VABI sceAgcAsyncCondExecPatchSetEnd_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("k-JpyR2dYAM", sceAgcAsyncCondExecPatchSetEnd_nid_no_patch); + +int APS5_VABI sceAgcDcbDmaDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("2ccJz9LQI+w", sceAgcDcbDmaDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetUcRegistersIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hvUfkUIQcOE", sceAgcDcbSetUcRegistersIndirect_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaults2_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("2JtWUUiYBXs", sceAgcGetRegisterDefaults2_nid_no_patch); + +int APS5_VABI sceAgcCbReleaseMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("wr23dPKyWc0", sceAgcCbReleaseMem_nid_no_patch); + +int APS5_VABI sceAgcCondExecPatchSetEnd_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ORWsxIbk4TE", sceAgcCondExecPatchSetEnd_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicGds_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("pH3-dfRpfA0", sceAgcDcbAtomicGds_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegistersIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GBCh3zCihoU", sceAgcDcbSetCxRegistersIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbSetUcRegisterRangeDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MDLD5Ly94Xk", sceAgcCbSetUcRegisterRangeDirect_nid_no_patch); + +int APS5_VABI sceAgcDcbEventWrite_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("aJf+j5yntiU", sceAgcDcbEventWrite_nid_no_patch); + +int APS5_VABI sceAgcDcbWriteData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("i1jyy49AjXU", sceAgcDcbWriteData_nid_no_patch); + +int APS5_VABI sceAgcSetUcRegIndirectPatchSetAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("6lNcCp+fxi4", sceAgcSetUcRegIndirectPatchSetAddress_nid_no_patch); + +int APS5_VABI sceAgcCbCondWriteGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("FuVbkyKlf+s", sceAgcCbCondWriteGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("zg6u-N6Otxs", sceAgcDcbQueueEndOfShaderActionGetSize_nid_no_patch); + +int APS5_VABI sceAgcGetPacketSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Lkf86B98qPc", sceAgcGetPacketSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAcquireMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("KT-hTp-Ch14", sceAgcAcbAcquireMem_nid_no_patch); + +int APS5_VABI sceAgcAcbEventWriteGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Y-5vneiBtzk", sceAgcAcbEventWriteGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexIndirectArgs_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("0o3VDdtA6nM", sceAgcDcbSetIndexIndirectArgs_nid_no_patch); + +int APS5_VABI sceAgcAcbDmaData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("-RnpfpxIhec", sceAgcAcbDmaData_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("t1vNu082-jM", sceAgcDcbDrawIndexIndirect_nid_no_patch); + +int APS5_VABI sceAgcDcbPrimeUtcl2GetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("KjPeVduz6jU", sceAgcDcbPrimeUtcl2GetSize_nid_no_patch); + +int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("3ZWa3AoyWZQ", sceAgcAsyncCondExecPatchSetCommandAddress_nid_no_patch); + +int APS5_VABI sceAgcAcbAcquireMemGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ewobAQeMo5k", sceAgcAcbAcquireMemGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("AFIh8SQkYlQ", sceAgcDcbSetIndexIndirectArgsGetSize_nid_no_patch); + +int APS5_VABI sceAgcCreateInterpolantMapping_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("pdEV7bI6COI", sceAgcCreateInterpolantMapping_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirectMulti_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ypVBz4uPKcQ", sceAgcDcbDrawIndexIndirectMulti_nid_no_patch); + +int APS5_VABI sceAgcDcbEventWriteGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("C4l9fB17t8w", sceAgcDcbEventWriteGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetUcRegisterDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w4-d0n60hdo", sceAgcDcbSetUcRegisterDirect_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("0fWWK5uG9rQ", sceAgcQueueEndOfPipeActionPatchAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndirectMultiGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("pYoKs3lPy88", sceAgcDcbDrawIndirectMultiGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetBaseDrawIndirectArgsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MMlmJAL7N5w", sceAgcDcbSetBaseDrawIndirectArgsGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetNumInstances_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("tSBxhAPyytQ", sceAgcDcbSetNumInstances_nid_no_patch); + +int APS5_VABI sceAgcDcbWaitOnAddressGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("43WJ08sSugE", sceAgcDcbWaitOnAddressGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicGds_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("cduV1f0dcGQ", sceAgcAcbAtomicGds_nid_no_patch); + +int APS5_VABI sceAgcDcbSetBoolPredicationEnableGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("yheJGN-ay+A", sceAgcDcbSetBoolPredicationEnableGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicMemGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("da1Sm8-QDoU", sceAgcAcbAtomicMemGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCfRegisterRangeDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("BVFg3CWU6Eo", sceAgcDcbSetCfRegisterRangeDirect_nid_no_patch); + +int APS5_VABI sceAgcDcbCondExecGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ou16V5hh5sg", sceAgcDcbCondExecGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexMultiInstanced_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Rlx+bykm0r0", sceAgcDcbDrawIndexMultiInstanced_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegistersIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ZvwO9euwYzc", sceAgcDcbSetCxRegistersIndirect_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegistersDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("yUBESvCCJ4I", sceAgcCbSetShRegistersDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCondExecPatchSetCommandAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("YWTKOju587o", sceAgcCondExecPatchSetCommandAddress_nid_no_patch); + +int APS5_VABI sceAgcRewindPatchSetRewindState_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ziVA3whp3p4", sceAgcRewindPatchSetRewindState_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchGcrCntl_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("J8YCgfKAMQs", sceAgcQueueEndOfPipeActionPatchGcrCntl_nid_no_patch); + +int APS5_VABI sceAgcWaitRegMemPatchReference_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("7nOoijNPvEU", sceAgcWaitRegMemPatchReference_nid_no_patch); + +int APS5_VABI sceAgcAcbRewind_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("DwICrVxerkY", sceAgcAcbRewind_nid_no_patch); + +int APS5_VABI sceAgcAcbCondExecGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ozKzBP4aki4", sceAgcAcbCondExecGetSize_nid_no_patch); + +int APS5_VABI sceAgcSuspendPointAndCheckStatus_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("b+fis+WZ3Ig", sceAgcSuspendPointAndCheckStatus_nid_no_patch); + +int APS5_VABI sceAgcAcbWaitRegMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("htn36gPnBk4", sceAgcAcbWaitRegMem_nid_no_patch); + +int APS5_VABI sceAgcDcbAcquireMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("57labkp+rSQ", sceAgcDcbAcquireMem_nid_no_patch); + +int APS5_VABI sceAgcSetCxRegIndirectPatchSetNumRegisters_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("whb1RL7K4Ss", sceAgcSetCxRegIndirectPatchSetNumRegisters_nid_no_patch); + +int APS5_VABI sceAgcDcbContextStateOp_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("HabmgqPwPw0", sceAgcDcbContextStateOp_nid_no_patch); + +int APS5_VABI sceAgcAcbWriteData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("eZ4+17OQz4Q", sceAgcAcbWriteData_nid_no_patch); + +int APS5_VABI sceAgcAcbDmaDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("M0ttm8h7SKA", sceAgcAcbDmaDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegisterRangeDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("bxGoVxpdSPQ", sceAgcCbSetShRegisterRangeDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbNop_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("LtTouSCZjHM", sceAgcCbNop_nid_no_patch); + +int APS5_VABI sceAgcDcbResetQueue_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("TRO721eVt4g", sceAgcDcbResetQueue_nid_no_patch); + +int APS5_VABI sceAgcDcbDmaData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("WmAc2MEj6Io", sceAgcDcbDmaData_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("6ee9Hd3EWXQ", sceAgcDcbDrawIndexGetSize_nid_no_patch); + +int APS5_VABI sceAgcDmaDataPatchSetDstAddressOrOffset_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("IxYiarKlXxM", sceAgcDmaDataPatchSetDstAddressOrOffset_nid_no_patch); + +int APS5_VABI sceAgcCbQueueEndOfPipeActionGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hL7C0IRpWZI", sceAgcCbQueueEndOfPipeActionGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetFlip_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("YUeqkyT7mEQ", sceAgcDcbSetFlip_nid_no_patch); + +int APS5_VABI sceAgcDcbSetMarker_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("QhCbS4X9Rl8", sceAgcDcbSetMarker_nid_no_patch); + +int APS5_VABI sceAgcDcbSetPredication_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("bbFueFP+J4k", sceAgcDcbSetPredication_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1-gUn1PI4Sw", sceAgcDcbAtomicMem_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaults2Internal_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("wRbq6ZjNop4", sceAgcGetRegisterDefaults2Internal_nid_no_patch); + +int APS5_VABI sceAgcSetShRegIndirectPatchAddRegisters_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("z2duB-hHQSM", sceAgcSetShRegIndirectPatchAddRegisters_nid_no_patch); + +int APS5_VABI sceAgcDcbPrimeUtcl2_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("jt3pl7EN17o", sceAgcDcbPrimeUtcl2_nid_no_patch); + +int APS5_VABI sceAgcAcbRewindGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("0ZOG0jc9nRg", sceAgcAcbRewindGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetZPassPredicationEnableGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("XN+Iuu7XsM8", sceAgcDcbSetZPassPredicationEnableGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbWaitUntilSafeForRendering_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MWiElSNE8j8", sceAgcDcbWaitUntilSafeForRendering_nid_no_patch); + +int APS5_VABI sceAgcBranchPatchSetThenTarget_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("xb8VgcXQhvI", sceAgcBranchPatchSetThenTarget_nid_no_patch); + +int APS5_VABI sceAgcSetCxRegIndirectPatchSetAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vcmNN+AAXnY", sceAgcSetCxRegIndirectPatchSetAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicMemGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("oz6zQq1JwCE", sceAgcDcbAtomicMemGetSize_nid_no_patch); + +int APS5_VABI sceAgcUpdatePrimState_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Y3ymLfZ1384", sceAgcUpdatePrimState_nid_no_patch); + +int APS5_VABI sceAgcCbNopGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("t7PlZ9nt5Lc", sceAgcCbNopGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbGetLodStats_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vuSXe69VILM", sceAgcDcbGetLodStats_nid_no_patch); + +int APS5_VABI sceAgcWaitRegMemPatchAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("3KDcnM3lrcU", sceAgcWaitRegMemPatchAddress_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MlEw1feXcjg", sceAgcQueueEndOfPipeActionPatchData_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaultsInternal_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("uIwxsqDlHRc", sceAgcGetRegisterDefaultsInternal_nid_no_patch); + +int APS5_VABI sceAgcInit_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("kW3GLb7QfPg", sceAgcInit_nid_no_patch); + +int APS5_VABI sceAgcDcbDispatchIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w8HVkEeXPv8", sceAgcDcbDispatchIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbCondExec_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("BIPexNBSGog", sceAgcDcbCondExec_nid_no_patch); + +int APS5_VABI sceAgcAcbSetMarker_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("xAeBOa0A3kk", sceAgcAcbSetMarker_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexOffsetGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("qMlfB1ZhMDc", sceAgcDcbDrawIndexOffsetGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbJump_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("e1DFTg+Sd8U", sceAgcAcbJump_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexCount_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("8N2tmT3jmC8", sceAgcDcbSetIndexCount_nid_no_patch); + +int APS5_VABI sceAgcDcbSetShRegistersIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("nNlUtdDDvZ0", sceAgcDcbSetShRegistersIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbStallCommandBufferParserGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("+u6dKSLWM2o", sceAgcDcbStallCommandBufferParserGetSize_nid_no_patch); + +int APS5_VABI sceAgcSuspendPoint_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("h9z6+0hEydk", sceAgcSuspendPoint_nid_no_patch); + +int APS5_VABI sceAgcWaitRegMemPatchMask_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hXAnLgDHCoI", sceAgcWaitRegMemPatchMask_nid_no_patch); + +int APS5_VABI sceAgcAcbDispatchIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("PxKWV2fVAps", sceAgcAcbDispatchIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicGdsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hcIxS8pmXF4", sceAgcAcbAtomicGdsGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbCondWrite_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("7toV+elXqNM", sceAgcCbCondWrite_nid_no_patch); + +int APS5_VABI sceAgcDcbEndOcclusionQueryGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("P1CugZ99Uzc", sceAgcDcbEndOcclusionQueryGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetPredicationDisableGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vLrBL8DQiz8", sceAgcDcbSetPredicationDisableGetSize_nid_no_patch); + +int APS5_VABI sceAgcLinkShaders_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MqAdbRMdNz4", sceAgcLinkShaders_nid_no_patch); + +int APS5_VABI sceAgcDcbWaitRegMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("VmW0Tdpy420", sceAgcDcbWaitRegMem_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaults_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Wi82ArQtAwg", sceAgcGetRegisterDefaults_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegisterRangeDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("n2fD4A+pb+g", sceAgcCbSetShRegisterRangeDirect_nid_no_patch); + +int APS5_VABI sceAgcSetCxRegIndirectPatchAddRegisters_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("d-6uF9sZDIU", sceAgcSetCxRegIndirectPatchAddRegisters_nid_no_patch); + +int APS5_VABI sceAgcDcbDispatchIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("CtB+A9-VxO0", sceAgcDcbDispatchIndirect_nid_no_patch); + +int APS5_VABI sceAgcCbMemSemaphore_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vHX9guneRBY", sceAgcCbMemSemaphore_nid_no_patch); + +int APS5_VABI sceAgcCbSetUcRegisterRangeDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("JOWmDrl+j20", sceAgcCbSetUcRegisterRangeDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbDispatch_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("k3GhuSNmBLU", sceAgcCbDispatch_nid_no_patch); + +int APS5_VABI sceAgcDcbCopyDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("b5u0Jzm8TF8", sceAgcDcbCopyDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexMultiInstancedGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("mR9j7+SfM34", sceAgcDcbDrawIndexMultiInstancedGetSize_nid_no_patch); + +int APS5_VABI sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("cdDRpqcFGbU", sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate_nid_no_patch); + +int APS5_VABI sceAgcSetShRegIndirectPatchSetAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Qrj4c+61z4A", sceAgcSetShRegIndirectPatchSetAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbJump_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("xSAR0LTcRKM", sceAgcDcbJump_nid_no_patch); + +int APS5_VABI sceAgcDcbJumpGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("VEGu4dixjUg", sceAgcDcbJumpGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbPrimeUtcl2_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("szG7hz2yEhA", sceAgcAcbPrimeUtcl2_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegisterDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1DeUNpRIDDA", sceAgcDcbSetCxRegisterDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchType_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("T9fjQIINoeE", sceAgcQueueEndOfPipeActionPatchType_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegisterDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("LHFXRrlTPD8", sceAgcDcbSetCxRegisterDirect_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndex_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("q88lQ+GP5Yk", sceAgcDcbDrawIndex_nid_no_patch); + +int APS5_VABI sceAgcCbDispatchGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Abendgtz+3o", sceAgcCbDispatchGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbGetLodStatsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("rUuVjyR+Rd4", sceAgcDcbGetLodStatsGetSize_nid_no_patch); + +int APS5_VABI sceAgcGetDataPacketPayloadAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("CQsSq6l6+kA", sceAgcGetDataPacketPayloadAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1q1titRBL6o", sceAgcDcbDrawIndirect_nid_no_patch); + +int APS5_VABI sceAgcAcbSetFlip_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ebixW91gpPw", sceAgcAcbSetFlip_nid_no_patch); + +int APS5_VABI sceAgcGetFusedShaderSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("nQT5kYLv0cg", sceAgcGetFusedShaderSize_nid_no_patch); + +int APS5_VABI sceAgcSetRangePredication_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("n8vgpaQg6dA", sceAgcSetRangePredication_nid_no_patch); + +int APS5_VABI sceAgcDcbSetShRegisterDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("QhPDD513V0w", sceAgcDcbSetShRegisterDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GIIW2J37e70", sceAgcDcbSetIndexSize_nid_no_patch); + +int APS5_VABI sceAgcSetPacketPredication_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w6Dj1VJt5qY", sceAgcSetPacketPredication_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexOffset_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("B+aG9DUnTKA", sceAgcDcbDrawIndexOffset_nid_no_patch); + +int APS5_VABI sceAgcAsyncRewindPatchSetRewindState_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("eWaWyFegzgQ", sceAgcAsyncRewindPatchSetRewindState_nid_no_patch); + +int APS5_VABI sceAgcJumpPatchSetTarget_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("2BS4EtAaF28", sceAgcJumpPatchSetTarget_nid_no_patch); + +int APS5_VABI sceAgcBranchPatchSetCompareAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GXBlM-ekzrI", sceAgcBranchPatchSetCompareAddress_nid_no_patch); + +int APS5_VABI sceAgcAcbJumpGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("b-oySn+G2tE", sceAgcAcbJumpGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbBranch_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w1KFAHVqpaU", sceAgcCbBranch_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("mStuvI0zOtc", sceAgcDcbDrawIndexIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbCopyData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("qzMN2XKGA4k", sceAgcAcbCopyData_nid_no_patch); + +int APS5_VABI sceAgcDcbWriteDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("p9tI+yTvx68", sceAgcDcbWriteDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("XKKuA6VkSRc", sceAgcAcbAtomicMem_nid_no_patch); + +int APS5_VABI sceAgcAcbPrimeUtcl2GetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("eCjKaqeeQ5s", sceAgcAcbPrimeUtcl2GetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbResetQueue_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("JrtiDtKeS38", sceAgcAcbResetQueue_nid_no_patch); + +int APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("F8NLhWvFemI", sceAgcAcbQueueEndOfShaderActionGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ms1xVoZ-Vwc", sceAgcDcbBeginOcclusionQueryGetSize_nid_no_patch); } diff --git a/docs/dev/debt/libSceAgcVsh.md b/docs/dev/debt/libSceAgcVsh.md new file mode 100644 index 0000000000..c2fb6a18d8 --- /dev/null +++ b/docs/dev/debt/libSceAgcVsh.md @@ -0,0 +1,5 @@ +# libSceAgcVsh technical debt + +### Unknown function info + +- The 150 declarations in [Export.cpp](../../../core/libs/prx/libSceAgcVsh/Export.cpp) are present in the PS5 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgcVsh.c) and the local import-stub table. Their signatures and behavior are unverified; the parameterless declarations are for import resolution only and throw through NotImplemented_nid_no_patch. From 5ad029659f6b0e01d79a8e47c15776aa9b5e01de Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:56:22 +0200 Subject: [PATCH 1140/1360] feat(libSceFreeType): implement guest list operations --- core/libs/prx/libSceFreeType/CMakeLists.txt | 14 + .../prx/libSceFreeType/include/GuestList.hpp | 33 +++ core/libs/prx/libSceFreeType/src/List.cpp | 101 +++++++ core/libs/tests/GuestFreeTypeList.cpp | 261 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 411 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/include/GuestList.hpp create mode 100644 core/libs/prx/libSceFreeType/src/List.cpp create mode 100644 core/libs/tests/GuestFreeTypeList.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 2292f26da8..6e479c5a37 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL + src/List.cpp Export.cpp src/GuestObjects.cpp src/Names.cpp @@ -24,6 +25,19 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() configure_windows_unwind(libSceFreeType) +if(BUILD_TESTING) + add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp) + target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_list_tests) + add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests) + set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") + endif() +endif() + if(BUILD_TESTING) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFreeType/include/GuestList.hpp b/core/libs/prx/libSceFreeType/include/GuestList.hpp new file mode 100644 index 0000000000..67a8396b87 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestList.hpp @@ -0,0 +1,33 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTLIST_HPP +#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTLIST_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include <ft2build.h> +#include FT_LIST_H + +#include <cstddef> +#include <cstdint> + +namespace FreeType { + +using GuestListRec = FT_ListRec; +using GuestListNodeRec = FT_ListNodeRec; + +struct GuestMemoryRec { + void* user; + void* (APS5_VABI *alloc)(GuestMemoryRec*, std::int64_t); + void (APS5_VABI *free)(GuestMemoryRec*, void*); + void* (APS5_VABI *realloc)(GuestMemoryRec*, std::int64_t, std::int64_t, void*); +}; + +using GuestListIterator = int (APS5_VABI *)(GuestListNodeRec*, void*); +using GuestListDestructor = void (APS5_VABI *)(GuestMemoryRec*, void*, void*); + +static_assert(sizeof(GuestListNodeRec) == 24 && offsetof(GuestListNodeRec, prev) == 0 && offsetof(GuestListNodeRec, next) == 8 && offsetof(GuestListNodeRec, data) == 16); +static_assert(sizeof(GuestListRec) == 16 && offsetof(GuestListRec, head) == 0 && offsetof(GuestListRec, tail) == 8); +static_assert(sizeof(GuestMemoryRec) == 32 && offsetof(GuestMemoryRec, alloc) == 8 && offsetof(GuestMemoryRec, free) == 16 && offsetof(GuestMemoryRec, realloc) == 24); + +} + +#endif diff --git a/core/libs/prx/libSceFreeType/src/List.cpp b/core/libs/prx/libSceFreeType/src/List.cpp new file mode 100644 index 0000000000..ff3deb8272 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/List.cpp @@ -0,0 +1,101 @@ +#include "prx/libSceFreeType/include/GuestList.hpp" +#include "prx/libc/include/GuestArena.hpp" + +#include <array> +#include <stdexcept> + +using namespace FreeType; + +namespace { + +class ListWrite { +public: + ListWrite(GuestListRec* list, std::array<GuestListNodeRec*, 4> nodes = {}) + : writes{GuestArena::HostWrite(list, sizeof(*list)), + GuestArena::HostWrite(nodes[0], nodes[0] ? sizeof(*nodes[0]) : 0), + GuestArena::HostWrite(nodes[1], nodes[1] ? sizeof(*nodes[1]) : 0), + GuestArena::HostWrite(nodes[2], nodes[2] ? sizeof(*nodes[2]) : 0), + GuestArena::HostWrite(nodes[3], nodes[3] ? sizeof(*nodes[3]) : 0)} { + for (const auto& write : writes) { + if (!write.Open()) throw std::runtime_error("FT_List: a destination is not writable"); + } + } + +private: + std::array<GuestArena::HostWrite, 5> writes; +}; + +struct Iteration { + GuestListIterator iterator; + void* user; +}; + +int Iterate(FT_ListNode node, void* user) { + auto& call = *static_cast<Iteration*>(user); + return call.iterator(node, call.user); +} + +struct Finalization { + GuestMemoryRec* memory; + GuestListDestructor destroy; + void* user; +}; + +void Destroy(FT_Memory memory, void* data, void*) { + auto& call = *static_cast<Finalization*>(memory->user); + call.destroy(call.memory, data, call.user); +} + +void FreeNode(FT_Memory memory, void* block) { + auto& call = *static_cast<Finalization*>(memory->user); + call.memory->free(call.memory, block); +} + +} + +extern "C" { + +GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec* list, void* data) { + return FT_List_Find(list, data); +} + +void APS5_VABI FT_List_Add_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node) return; + const ListWrite write(list, {node, list->tail}); + FT_List_Add(list, node); +} + +void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node) return; + const ListWrite write(list, {node, list->head}); + FT_List_Insert(list, node); +} + +void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node) return; + const ListWrite write(list, {node->prev, node->next}); + FT_List_Remove(list, node); +} + +void APS5_VABI FT_List_Up_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node || !node->prev) return; + const ListWrite write(list, {node, node->prev, node->next, list->head}); + FT_List_Up(list, node); +} + +int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec* list, GuestListIterator iterator, void* user) { + Iteration call{iterator, user}; + return FT_List_Iterate(list, iterator ? Iterate : nullptr, &call); +} + +void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec* list, GuestListDestructor destroy, GuestMemoryRec* memory, void* user) { + if (!list || !memory) return; + const ListWrite write(list); + Finalization call{memory, destroy, user}; + FT_MemoryRec_ native{}; + native.user = &call; + native.free = FreeNode; + FT_List_Finalize(list, destroy ? Destroy : nullptr, &native, user); +} + +} diff --git a/core/libs/tests/GuestFreeTypeList.cpp b/core/libs/tests/GuestFreeTypeList.cpp new file mode 100644 index 0000000000..ba2a805e48 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeList.cpp @@ -0,0 +1,261 @@ +#include "prx/libSceFreeType/include/GuestList.hpp" + +#include <array> +#include <cstdio> +#include <cstdlib> +#include <map> +#include <random> +#include <vector> + +using namespace FreeType; + +extern "C" { +GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec*, void*); +void APS5_VABI FT_List_Add_nid_postfix(GuestListRec*, GuestListNodeRec*); +void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec*, GuestListNodeRec*); +void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec*, GuestListNodeRec*); +void APS5_VABI FT_List_Up_nid_postfix(GuestListRec*, GuestListNodeRec*); +int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec*, GuestListIterator, void*); +void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec*, GuestListDestructor, GuestMemoryRec*, void*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType list check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +constexpr std::size_t Count = 16; +using Nodes = std::array<FT_ListNodeRec, Count>; + +static int Index(FT_ListNode node, const Nodes& nodes) { + if (!node) return -1; + for (std::size_t i = 0; i < nodes.size(); ++i) if (node == &nodes[i]) return static_cast<int>(i); + Require(false); + return -2; +} + +struct Lists { + FT_ListRec guest{}; + FT_ListRec native{}; + Nodes actual{}; + Nodes expected{}; + std::array<int, 4> values{11, 22, 33, 44}; + + Lists() { + for (std::size_t i = 0; i < Count; ++i) actual[i].data = expected[i].data = i % 5 == 0 ? nullptr : &values[i % values.size()]; + } + + void Match() const { + Require(Index(guest.head, actual) == Index(native.head, expected)); + Require(Index(guest.tail, actual) == Index(native.tail, expected)); + for (std::size_t i = 0; i < Count; ++i) { + Require(Index(actual[i].prev, actual) == Index(expected[i].prev, expected)); + Require(Index(actual[i].next, actual) == Index(expected[i].next, expected)); + Require(actual[i].data == expected[i].data); + } + } + + void Fill() { + for (std::size_t i = 0; i < Count; ++i) { + FT_List_Add_nid_postfix(&guest, &actual[i]); + FT_List_Add(&native, &expected[i]); + } + Match(); + } +}; + +static void Mutations() { + Lists lists; + std::array<bool, Count> active{}; + std::array<unsigned, 4> operations{}; + std::mt19937 random(0x7157); + for (unsigned step = 0; step < 2048; ++step) { + const auto i = random() % Count; + const bool choice = (random() & 1u) != 0; + if (active[i]) { + if (choice) { + FT_List_Up_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Up(&lists.native, &lists.expected[i]); + ++operations[3]; + } else { + FT_List_Remove_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Remove(&lists.native, &lists.expected[i]); + active[i] = false; + ++operations[2]; + } + } else { + if (choice) { + FT_List_Insert_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Insert(&lists.native, &lists.expected[i]); + ++operations[1]; + } else { + FT_List_Add_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Add(&lists.native, &lists.expected[i]); + ++operations[0]; + } + active[i] = true; + } + lists.Match(); + for (auto& value : lists.values) { + Require(Index(FT_List_Find_nid_postfix(&lists.guest, &value), lists.actual) == Index(FT_List_Find(&lists.native, &value), lists.expected)); + } + Require(Index(FT_List_Find_nid_postfix(&lists.guest, nullptr), lists.actual) == Index(FT_List_Find(&lists.native, nullptr), lists.expected)); + int absent = 123; + Require(FT_List_Find_nid_postfix(&lists.guest, &absent) == nullptr); + } + for (const auto count : operations) Require(count != 0); + for (auto operation : {FT_List_Add_nid_postfix, FT_List_Insert_nid_postfix, FT_List_Remove_nid_postfix, FT_List_Up_nid_postfix}) { + operation(nullptr, &lists.actual[0]); + operation(&lists.guest, nullptr); + operation(nullptr, nullptr); + lists.Match(); + } + Require(FT_List_Find_nid_postfix(nullptr, nullptr) == nullptr); + std::printf("list mutations: 2048 states compared with native FreeType\n"); +} + +struct Walk { + FT_List list; + Nodes* nodes; + bool guest; + bool remove; + std::size_t stop; + int error; + std::vector<int> visits; +}; + +static int Visit(FT_ListNode node, void* user, bool guest) { + auto& state = *static_cast<Walk*>(user); + Require(state.guest == guest); + state.visits.push_back(Index(node, *state.nodes)); + if (state.remove) { + if (guest) FT_List_Remove_nid_postfix(state.list, node); + else FT_List_Remove(state.list, node); + node->prev = nullptr; + node->next = nullptr; + } + return state.visits.size() == state.stop ? state.error : 0; +} + +static int NativeIterator(FT_ListNode node, void* user) { return Visit(node, user, false); } +static int APS5_VABI GuestIterator(GuestListNodeRec* node, void* user) { return Visit(node, user, true); } + +static void IterationCases() { + for (const bool remove : {false, true}) { + for (const std::size_t stop : {0u, 1u, 3u, 16u}) { + Lists lists; + lists.Fill(); + const int error = stop == 3 ? -29 : 73; + Walk guest{&lists.guest, &lists.actual, true, remove, stop, error, {}}; + Walk native{&lists.native, &lists.expected, false, remove, stop, error, {}}; + const int result = FT_List_Iterate_nid_postfix(&lists.guest, GuestIterator, &guest); + Require(result == FT_List_Iterate(&lists.native, NativeIterator, &native)); + Require(result == (stop == 0 ? 0 : error)); + Require(guest.visits == native.visits && guest.visits.size() == (stop == 0 ? Count : stop)); + lists.Match(); + } + } + Lists empty; + Walk guest{&empty.guest, &empty.actual, true, false, 0, 0, {}}; + Walk native{&empty.native, &empty.expected, false, false, 0, 0, {}}; + Require(FT_List_Iterate_nid_postfix(&empty.guest, GuestIterator, &guest) == 0 && guest.visits.empty()); + Require(FT_List_Iterate_nid_postfix(nullptr, GuestIterator, &guest) == FT_List_Iterate(nullptr, NativeIterator, &native)); + Require(FT_List_Iterate_nid_postfix(&empty.guest, nullptr, &guest) == FT_List_Iterate(&empty.native, nullptr, &native)); + Require(FT_List_Iterate_nid_postfix(nullptr, nullptr, nullptr) == FT_List_Iterate(nullptr, nullptr, nullptr)); + std::printf("list iteration: 8 callback/order/error/removal cases compared\n"); +} + +struct Event { + int kind; + int id; + bool cleared; + bool operator==(const Event&) const = default; +}; + +struct Disposal { + FT_List list; + FT_Memory native; + GuestMemoryRec* guest; + void* user; + std::map<void*, int> live; + std::vector<Event> events; +}; + +static void Release(Disposal& state, void* block) { + const auto found = state.live.find(block); + Require(found != state.live.end()); + state.events.push_back({2, found->second, state.list->head == nullptr || state.list->tail == nullptr}); + state.live.erase(found); + delete static_cast<FT_ListNode>(block); +} + +static void NativeFree(FT_Memory memory, void* block) { + auto& state = *static_cast<Disposal*>(memory->user); + Require(state.native == memory); + Release(state, block); +} + +static void APS5_VABI GuestFree(GuestMemoryRec* memory, void* block) { + auto& state = *static_cast<Disposal*>(memory->user); + Require(state.guest == memory); + Release(state, block); +} + +static void NativeDestroy(FT_Memory memory, void* data, void* user) { + auto& state = *static_cast<Disposal*>(memory->user); + Require(state.native == memory && state.user == user); + state.events.push_back({1, data ? *static_cast<int*>(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); +} + +static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* user) { + auto& state = *static_cast<Disposal*>(memory->user); + Require(state.guest == memory && state.user == user); + state.events.push_back({1, data ? *static_cast<int*>(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); +} + +static void FinalizationCases() { + for (const bool destroy : {false, true}) { + for (const unsigned count : {0u, 1u, 8u}) { + FT_ListRec guestList{}, nativeList{}; + std::array<int, 8> ids{0, 1, 2, 3, 4, 5, 6, 7}; + int token = 9; + Disposal guest{&guestList, nullptr, nullptr, &token, {}, {}}; + Disposal native{&nativeList, nullptr, nullptr, &token, {}, {}}; + GuestMemoryRec guestMemory{&guest, nullptr, GuestFree, nullptr}; + FT_MemoryRec_ nativeMemory{&native, nullptr, NativeFree, nullptr}; + guest.guest = &guestMemory; + native.native = &nativeMemory; + for (unsigned i = 0; i < count; ++i) { + auto* a = new FT_ListNodeRec{}; + auto* b = new FT_ListNodeRec{}; + a->data = b->data = i == 0 ? nullptr : &ids[i]; + guest.live.emplace(a, i); + native.live.emplace(b, i); + FT_List_Add_nid_postfix(&guestList, a); + FT_List_Add(&nativeList, b); + } + const auto savedHead = guestList.head; + FT_List_Finalize_nid_postfix(&guestList, GuestDestroy, nullptr, &token); + Require(guestList.head == savedHead && guest.events.empty()); + FT_List_Finalize_nid_postfix(nullptr, GuestDestroy, &guestMemory, &token); + Require(guest.events.empty()); + FT_List_Finalize_nid_postfix(&guestList, destroy ? GuestDestroy : nullptr, &guestMemory, &token); + FT_List_Finalize(&nativeList, destroy ? NativeDestroy : nullptr, &nativeMemory, &token); + Require(guestList.head == nullptr && guestList.tail == nullptr && nativeList.head == nullptr && nativeList.tail == nullptr); + Require(guest.live.empty() && native.live.empty()); + Require(guest.events == native.events && guest.events.size() == count * (destroy ? 2u : 1u)); + for (const auto& event : guest.events) Require(!event.cleared); + } + } + std::printf("list finalization: 6 allocator/destructor/order cases compared\n"); +} + +int main() { + Mutations(); + IterationCases(); + FinalizationCases(); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index b2a706294a..24d6d994ff 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -16,6 +16,8 @@ - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. +- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated. + - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read From 217df9a44cde9640fca4c6b6d622178e534fee7c Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:59:47 +0200 Subject: [PATCH 1141/1360] build(libSceFreeType): keep list registration independent of pending features --- core/libs/prx/libSceFreeType/CMakeLists.txt | 28 ++++++++++----------- docs/dev/debt/libSceFreeType.md | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 6e479c5a37..060d3f7830 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL - src/List.cpp Export.cpp src/GuestObjects.cpp src/Names.cpp @@ -20,24 +19,12 @@ if(BUILD_TESTING) endif() endif() +target_sources(libSceFreeType PRIVATE src/List.cpp) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceFreeType PRIVATE LINKER:--exclude-libs,ALL) endif() configure_windows_unwind(libSceFreeType) -if(BUILD_TESTING) - add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp) - target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc) - configure_windows_unwind(guest_freetype_list_tests) - add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests) - set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20) - if(WIN32) - set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") - endif() -endif() - if(BUILD_TESTING) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -70,3 +57,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp) + target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_list_tests) + add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests) + set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") + endif() +endif() diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 24d6d994ff..62890d99b5 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -16,10 +16,10 @@ - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. -- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated. - - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. +- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated. + - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read - [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. From f8f1a4a19341f0932818169d2e53dfd505ba7ae2 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:45:47 +0200 Subject: [PATCH 1142/1360] docs(debt): add call-site evidence for the libSceNpAuth V3 parameter layouts Three disassembled titles show the parameter layouts and how titles drive sceNpAuthGetAuthorizationCodeV3 and sceNpAuthGetIdTokenV3 (static reading only). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceNpAuth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 98b7bde5c9..e918766151 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified +- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Call sites in three disassembled titles (PPSA02225, PPSA15246, PPSA30601; static reading only) show the V3 parameter layouts: the code request parameter is 0x20 bytes (size, a 32-bit value that the titles take from their user object, a pointer to a client-ID-like string, a pointer to a constant string) and the call is `(reqId, param, code[128], int* issuerId)`; the ID token request parameter is 0x28 bytes (size, the same kind of 32-bit value, a pointer to a client-ID-like string, a pointer to a second buffer (256 bytes in one title) and a pointer to a constant string) and the call is `(reqId, param, token)`. The client-ID-like string is 36 characters plus a terminator in one title and a 128-byte buffer in two. In two titles the calls follow `sceNpAuthCreateAsyncRequest` (whose parameter holds only a size of 0x18) and are polled with `sceNpAuthPollAsync` (one title once per frame, one in a blocking loop of up to 40 polls 100 ms apart); the third title is a synchronous caller that takes the request ID from an argument-less call. All three call `sceNpAuthDeleteRequest` once the request has finished or failed (immediately after the call in the synchronous title, after a poll return of 0 in the per-frame title, on every path in the blocking one). The titles treat a poll return of 0 as finished and any negative result as a failure without looking at the specific code; one title, after 40 polls that still return non-zero, treats a return of exactly 1 as a timeout and calls `sceNpAuthAbortRequest`, and reports 0x80550301 and 0x80550306 from Poll as an invalid result pointer and an unknown request ID. The size fields are not checked here; the console's size checks and the behaviour of the calls on a console are unverified - [sceNpAuthPollAsync and sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - PS5 abort-before-start behavior is unmeasured. Poll/Wait keep returning `ORBIS_NP_AUTH_ERROR_INVALID_ID` without changing output after a pre-start abort, including subsequent rejected getter calls, following [shadPS4 StartRequest and FindAsyncRequest](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp). From 1487e952341a0884e69b8619d8cfffcef5677165 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:51:24 +0200 Subject: [PATCH 1143/1360] docs(debt): add call-site evidence for sceAppContentDownloadDataGetAvailableSpaceKb One disassembled title ignores the return value and multiplies the output by 1024 (static reading only). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAppContent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 9976365de2..1780d0bde0 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB +- [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB. One disassembled title (PPSA15246) calls it with the `/download0` mount point and a zeroed 64-bit output, ignores the return value and multiplies the output by 1024, so for that title an error that leaves the output untouched and an OK with 0 KB look the same - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified From f51eafc9c4f09a26bf72531112436a909df91311 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Fri, 23 Oct 2026 05:25:18 +0200 Subject: [PATCH 1144/1360] fix(libSceNgs2.native): use main's option size and channel error constants --- core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 2c5520d64e..28eb907597 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,9 +9,7 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast<int>(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast<int>(0x804A8011); -static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast<int>(0x804A8013); static constexpr int SCE_NGS2_ERROR_INVALID_SAMPLE_RATE = static_cast<int>(0x804A8051); -static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast<int>(0x804A8052); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast<int>(0x804A8201); static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast<int>(0x804A8202); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast<int>(0x804A8305); From 0f35e7b1760378415d4c106ddb222609b397e8e1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:35:47 +0000 Subject: [PATCH 1145/1360] fix(libc): follow msun for coshf --- core/libs/prx/libc/src/Math.cpp | 24 +++++++++++++++++++++++- docs/dev/debt/libc.md | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..b8fdda2b22 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -487,7 +487,29 @@ double APS5_VABI sin_nid_postfix(double x) { return std::sin(x); } double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } double APS5_VABI sinh_nid_postfix(double x) { return std::sinh(x); } double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } -float APS5_VABI coshf_nid_postfix(float x) { return std::cosh(x); } +float APS5_VABI coshf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast<std::uint32_t>(x) & 0x7fffffffu; + if (magnitude >= 0x7f800000u) return x * x; + const float absolute = std::fabs(x); + if (magnitude < 0x3eb17218u) { + const float t = SinhfExpm1(absolute); + const float w = 1.0f + t; + if (magnitude < 0x39800000u) return 1.0f; + return 1.0f + (t * t) / (w + w); + } + if (magnitude < 0x41100000u) { + const float t = SinhfExp(absolute); + return 0.5f * t + 0.5f / t; + } + if (magnitude < 0x42b17217u) return 0.5f * SinhfExp(absolute); + if (magnitude <= 0x42b2d4fcu) { + const float w = SinhfExp(0.5f * absolute); + const float t = 0.5f * w; + return t * w; + } + volatile float huge = 1.0e30f; + return huge * huge; +} float APS5_VABI sinhf_nid_postfix(float x) { const std::uint32_t magnitude = std::bit_cast<std::uint32_t>(x) & 0x7fffffffu; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..1d28b3280a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -85,6 +85,7 @@ - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` +- [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code From af343b1601385e2466e621f2c90dff6636bd8a04 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:35:47 +0000 Subject: [PATCH 1146/1360] test(libc): cover coshf range boundaries --- core/libs/tests/GuestMath.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..4591f77277 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -868,6 +868,37 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckCoshfMsunBoundaries() { + static constexpr struct { + std::uint32_t input; + std::uint32_t output; + } cases[] = { + {0x00000000u, 0x3f800000u}, + {0x80000000u, 0x3f800000u}, + {0x00000001u, 0x3f800000u}, + {0x397fffffu, 0x3f800000u}, + {0x39800000u, 0x3f800000u}, + {0x3eb17217u, 0x3f87c3b6u}, + {0x3eb17218u, 0x3f87c3b6u}, + {0x3f800000u, 0x3fc583aau}, + {0xbf800000u, 0x3fc583aau}, + {0x410fffffu, 0x457d389cu}, + {0x41100000u, 0x457d38acu}, + {0x42b17216u, 0x7effff04u}, + {0x42b17217u, 0x7effff84u}, + {0x42b17218u, 0x7f000002u}, + {0x42b2d4fbu, 0x7f7fff6cu}, + {0x42b2d4fcu, 0x7f7fffecu}, + {0xc2b2d4fcu, 0x7f7fffecu}, + }; + for (const auto& test : cases) + Require(std::bit_cast<std::uint32_t>(coshf_nid_postfix(std::bit_cast<float>(test.input))) == test.output); + Require(std::bit_cast<std::uint32_t>(coshf_nid_postfix(std::bit_cast<float>(0x42b2d4fdu))) == 0x7f800000u); + Require(std::bit_cast<std::uint32_t>(coshf_nid_postfix(INFINITY)) == 0x7f800000u); + Require(std::bit_cast<std::uint32_t>(coshf_nid_postfix(-INFINITY)) == 0x7f800000u); + Require(std::bit_cast<std::uint32_t>(coshf_nid_postfix(std::bit_cast<float>(0x7fc12345u))) == 0x7fc12345u); +} + static void CheckScaledHyperbolic() { const int rounding = std::fegetround(); Require(std::fesetround(FE_TONEAREST) == 0); @@ -1586,6 +1617,7 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckCoshfMsunBoundaries(); CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); From 5dfa258d9f75206e7a88ec7cc4af42f2bae3d303 Mon Sep 17 00:00:00 2001 From: Arnab Ghosh <arnab.ghosh@g.bracu.ac.bd> Date: Wed, 21 Oct 2026 16:39:16 +0600 Subject: [PATCH 1147/1360] feat(libSceNet): implement sceNetIoctl --- core/libs/prx/libSceNet/Export.cpp | 31 ++++++++++++++++- core/libs/tests/GuestSceNet.cpp | 53 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + docs/dev/debt/libkernel.md | 1 + 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 450868e186..e2e4286b3e 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -10,6 +10,7 @@ #include <netdb.h> #include <netinet/in.h> #include <netinet/tcp.h> +#include <sys/ioctl.h> #include <sys/socket.h> #include <poll.h> #include <unistd.h> @@ -232,6 +233,7 @@ constexpr int NET_EBADF = 9; constexpr int NET_EACCES = 13; constexpr int NET_EFAULT = 14; constexpr int NET_EINVAL = 22; +constexpr int NET_ENOTTY = 25; constexpr int NET_ENOSPC = 28; constexpr int NET_EDOM = 33; constexpr int NET_EAGAIN = 35; @@ -2467,8 +2469,35 @@ int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { return 1; } +extern "C" int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument); + int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { - NotImplemented_nid_no_patch(__func__); + constexpr std::uint32_t nonblockingRequest = 0x8004667e; + constexpr std::uint32_t readableRequest = 0x4004667f; + std::shared_ptr<NativeSocketHandle> native; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(socketId); + if (it != g_socks.end()) native = it->second.native; + } + if (!native) { + if (!GuestSocketIsOpen_nid_no_patch(socketId)) return fail(NET_EBADF); + return ioctl_nid_postfix(socketId, command, argument) == 0 ? 0 : fail(*__error_nid_postfix()); + } + if (command != nonblockingRequest && command != readableRequest) return fail(NET_ENOTTY); + if (!argument) return fail(NET_EFAULT); + if (command == nonblockingRequest) { + const int enabled = *static_cast<const int*>(argument) != 0; + return sceNetSetsockopt(socketId, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)); + } +#ifdef _WIN32 + u_long available = 0; + if (ioctlsocket(native->value, FIONREAD, &available) != 0) return fail(native_error()); +#else + int available = 0; + if (::ioctl(native->value, FIONREAD, &available) != 0) return fail(native_error()); +#endif + *static_cast<int*>(argument) = static_cast<int>(available); return 0; } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 60298ce61c..9e6b36aadd 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -55,6 +55,7 @@ int APS5_VABI sceNetSocketAbort(int, int); int APS5_VABI sceNetShutdown(int, int); int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int APS5_VABI sceNetIoctl(int, std::uint32_t, void*); int* APS5_VABI sceNetErrnoLoc(void); int APS5_VABI sceNetEtherNtostr(const NetEtherAddr*, char*, std::size_t); int APS5_VABI sceNetEtherStrton(const char*, NetEtherAddr*); @@ -812,6 +813,57 @@ static void CheckKernelTimeouts() { Require(sceNetSocketClose(descriptor) == 0); } +static void CheckIoctl() { + const int socket = sceNetSocket("ioctl", 2, 2, 17); + Require(socket >= 0); + std::array<std::uint8_t, 16> address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(socket, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && size == address.size()); + for (const int enabled : {1, 0, 7}) { + int value = enabled; + Require(sceNetIoctl(socket, 0x8004667e, &value) == 0); + int nonblocking = -1; + std::uint32_t length = sizeof(nonblocking); + Require(sceNetGetsockopt(socket, 0xffff, 0x1200, &nonblocking, &length) == 0 && nonblocking == (enabled != 0 ? 1 : 0)); + Require(fcntl_nid_postfix(socket, 3) == (enabled != 0 ? 2 | 4 : 2)); + } + int disabled = 0; + int enabled = 1; + Require(sceNetIoctl(socket, 0x8004667e, &enabled) == 0); + char buffer[8]{}; + Require(Failed(sceNetRecv(socket, buffer, sizeof(buffer), 0), 35)); + int readable = -1; + Require(sceNetIoctl(socket, 0x4004667f, &readable) == 0 && readable == 0); + const char payload[5] = {'i', 'o', 'c', 't', 'l'}; + Require(sceNetSendto(socket, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + GuestPollEntry entry{socket, 1, 0}; + Require(poll_nid_postfix(&entry, 1, 1000) == 1 && entry.revents == 1); + Require(sceNetIoctl(socket, 0x4004667f, &readable) == 0 && readable == sizeof(payload)); + Require(sceNetRecv(socket, buffer, sizeof(buffer), 0) == sizeof(payload)); + Require(sceNetIoctl(socket, 0x4004667f, &readable) == 0 && readable == 0); + Require(Failed(sceNetIoctl(socket, 0x8004667e, nullptr), 14)); + Require(Failed(sceNetIoctl(socket, 0x4004667f, nullptr), 14)); + Require(Failed(sceNetIoctl(socket, 123, &readable), 25)); + Require(Failed(sceNetIoctl(socket, 123, nullptr), 25)); + Require(sceNetSocketClose(socket) == 0); + Require(Failed(sceNetIoctl(socket, 0x8004667e, &enabled), 9)); + Require(Failed(sceNetIoctl(-1, 0x4004667f, &readable), 9)); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(random >= 0); + Require(Failed(sceNetIoctl(random, 0x4004667f, &readable), 9)); + Require(close_nid_postfix(random) == 0); + + const int kernel = socket_nid_postfix(2, 2, 0); + Require(kernel >= 0); + std::array<std::uint8_t, 16> kernelAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(kernel, kernelAddress.data(), kernelAddress.size()) == 0); + Require(sceNetIoctl(kernel, 0x8004667e, &enabled) == 0 && fcntl_nid_postfix(kernel, 3) == (2 | 4)); + Require(sceNetIoctl(kernel, 0x8004667e, &disabled) == 0 && fcntl_nid_postfix(kernel, 3) == 2); + Require(sceNetIoctl(kernel, 0x4004667f, &readable) == 0 && readable == 0); + Require(close_nid_postfix(kernel) == 0); +} + static void CheckKernelStatusFlags() { const int descriptor = sceNetSocket("kernel-status", 2, 1, 6); Require(descriptor >= 0); @@ -1662,6 +1714,7 @@ int main() { CheckKernelTimeouts(); CheckSocketError(); CheckKernelStatusFlags(); + CheckIoctl(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f9f697287e..7f36c7c4c5 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -27,6 +27,7 @@ ### Functional - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) prints nothing for a negative count, where the console prints `[LIBNET] usleep <count>` to standard output before failing with `EINVAL`. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) +- [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - on a `sceNetSocket` socket only `FIONBIO` and `FIONREAD` are handled and other requests fail with `ENOTTY`, as in libkernel's `ioctl`, though FreeBSD 11.4's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L156-L262) handles more; not measured on a console. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/net/net.cpp#L1839) (PS4) returns `EINVAL` for other requests. `FIONBIO` sets `SO_NBIO`, assumed to be `soo_ioctl`'s `SS_NBIO`. `FIONREAD` asks the host, which on Linux counts only the next datagram where `soo_ioctl` counts all. Libkernel's own sockets go to its `ioctl`; other descriptors get `EBADF`. - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d4f5cfd338..8646080c24 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -224,6 +224,7 @@ - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) do not pass a `sceNetSocket` socket to libSceNet as `fcntl` does. On Linux its number duplicates the host socket, so `FIONBIO` switches the host `O_NONBLOCK` but not the library's `SO_NBIO` state; on Windows it is a `NUL` placeholder, so `FIONREAD` and enabling `FIONBIO` throw as on a character device. `sceNetIoctl` handles both requests on such a socket. - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it From e9bbebeef2cd609539736314064fe1c147122e48 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:13:57 -0300 Subject: [PATCH 1148/1360] test(libc): cover host stream buffering isolation --- core/libs/prx/libkernel/CMakeLists.txt | 3 +- .../tests/GuestHostStandardErrorWindows.cpp | 49 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1b9c5d5348..1f673fc89f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -357,7 +357,8 @@ if(BUILD_TESTING) target_link_libraries(guest_host_standard_error_windows_tests PRIVATE libkernel libc) configure_windows_unwind(guest_host_standard_error_windows_tests) add_test(NAME guest_host_standard_error_windows COMMAND guest_host_standard_error_windows_tests) - set_tests_properties(guest_host_standard_error_windows PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION + add_test(NAME guest_host_standard_buffering_windows COMMAND guest_host_standard_error_windows_tests --buffering) + set_tests_properties(guest_host_standard_error_windows guest_host_standard_buffering_windows PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp index f91c92d09e..c571983957 100644 --- a/core/libs/tests/GuestHostStandardErrorWindows.cpp +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -4,6 +4,7 @@ #include <cstdint> #include <cstdio> #include <cstdlib> +#include <cstring> #include <string> #ifdef _WIN32 #include <windows.h> @@ -19,6 +20,8 @@ int APS5_VABI printf_nid_postfix(const char*, ...); int APS5_VABI puts_nid_postfix(const char*); int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI fflush_nid_postfix(FileStream*); +int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); +void APS5_VABI setbuf_nid_postfix(FileStream*, char*); void APS5_VABI perror_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); extern FileStream _Stdout_nid_postfix; @@ -96,7 +99,51 @@ static void GuestText(int standard, const std::string& text) { ::_close(guest[1]); } -int main() { +static void Buffering(int standard) { + int capture[2]{-1, -1}; + Require(::_pipe(capture, 65536, _O_BINARY) == 0); + const int original = ::_dup(standard); + Require(original >= 0 && ::_dup2(capture[1], standard) == 0); + auto* guestStream = standard == 1 ? &_Stdout_nid_postfix : &_Stderr_nid_postfix; + char buffer[4096]{}; + if (standard == 1) setbuf_nid_postfix(guestStream, buffer); + else Require(setvbuf_nid_postfix(guestStream, buffer, 0, sizeof(buffer)) == 0); + Require(fprintf_nid_postfix(guestStream, "guest-buffered") == 14); + Require(std::fputs("host-immediate", standard == 1 ? stdout : stderr) >= 0); + char bytes[64]{}; + DWORD read = 0; + Require(Available(capture[0], bytes, sizeof(bytes), read) == 14); + Require(read == 14 && std::string(bytes, read) == "host-immediate"); + Require(::_read(capture[0], bytes, 14) == 14); + Require(Available(capture[0], bytes, sizeof(bytes), read) == 0); + Require(fflush_nid_postfix(guestStream) == 0); + Require(Available(capture[0], bytes, sizeof(bytes), read) == 14); + Require(read == 14 && std::string(bytes, read) == "guest-buffered"); + Require(::_read(capture[0], bytes, 14) == 14); + + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0 && dup2_nid_postfix(guest[1], standard) == standard); + Require(fprintf_nid_postfix(guestStream, "guest-redirected") == 16 && fflush_nid_postfix(guestStream) == 0); + Require(std::fputs("host-original", standard == 1 ? stdout : stderr) >= 0); + Require(Available(capture[0], bytes, sizeof(bytes), read) == 13); + Require(read == 13 && std::string(bytes, read) == "host-original"); + Require(Available(guest[0], bytes, sizeof(bytes), read) == 16); + Require(read == 16 && std::string(bytes, read) == "guest-redirected"); + Require(setvbuf_nid_postfix(guestStream, nullptr, 2, 0) == 0); + Require(::_dup2(original, standard) == 0); + ::_close(original); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--buffering") == 0) { + Buffering(2); + Buffering(1); + return 0; + } Split(2); Split(1); GuestText(1, "guest-printf\nguest-puts\n"); From 6f401fe667a6bc60aa5ccf044bb90be90d73fb09 Mon Sep 17 00:00:00 2001 From: Felipe Arce <felipearce.2004@gmail.com> Date: Fri, 23 Oct 2026 01:18:59 -0300 Subject: [PATCH 1149/1360] feat: run the common-dialog state machine in the Facebook and game custom data dialogs --- .../libSceGameCustomDataDialog/CMakeLists.txt | 14 ++++ .../prx/libSceGameCustomDataDialog/Export.cpp | 76 +++++++++++++++---- .../libSceNpSnsFacebookDialog/CMakeLists.txt | 14 ++++ .../prx/libSceNpSnsFacebookDialog/Export.cpp | 74 ++++++++++++------ core/libs/tests/GuestGameCustomDataDialog.cpp | 72 ++++++++++++++++++ core/libs/tests/GuestNpSnsFacebookDialog.cpp | 72 ++++++++++++++++++ docs/dev/debt/libSceGameCustomDataDialog.md | 9 +++ docs/dev/debt/libSceNpSnsFacebookDialog.md | 4 +- 8 files changed, 297 insertions(+), 38 deletions(-) create mode 100644 core/libs/prx/libSceGameCustomDataDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpSnsFacebookDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestGameCustomDataDialog.cpp create mode 100644 core/libs/tests/GuestNpSnsFacebookDialog.cpp create mode 100644 docs/dev/debt/libSceGameCustomDataDialog.md diff --git a/core/libs/prx/libSceGameCustomDataDialog/CMakeLists.txt b/core/libs/prx/libSceGameCustomDataDialog/CMakeLists.txt new file mode 100644 index 0000000000..6f19d7891e --- /dev/null +++ b/core/libs/prx/libSceGameCustomDataDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceGameCustomDataDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceGameCustomDataDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceGameCustomDataDialog) + +if(BUILD_TESTING) + add_test_executable(guest_game_custom_data_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestGameCustomDataDialog.cpp) + target_include_directories(guest_game_custom_data_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_game_custom_data_dialog_tests PRIVATE libSceGameCustomDataDialog) + add_test(NAME guest_game_custom_data_dialog COMMAND guest_game_custom_data_dialog_tests) + if(WIN32) + set_tests_properties(guest_game_custom_data_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGameCustomDataDialog>") + endif() +endif() diff --git a/core/libs/prx/libSceGameCustomDataDialog/Export.cpp b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp index 5b647ad000..594d4adfac 100644 --- a/core/libs/prx/libSceGameCustomDataDialog/Export.cpp +++ b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp @@ -1,14 +1,74 @@ +#include <atomic> +#include <cstdint> +#include <cstddef> +#include <cstring> #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<int>(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceGameCustomDataDialogGetResult() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameCustomDataDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceGameCustomDataDialogInitialize() { +int APS5_VABI sceGameCustomDataDialogOpenA(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceGameCustomDataDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceGameCustomDataDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceGameCustomDataDialogGetResultA(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceGameCustomDataDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceGameCustomDataDialogGetResult() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -18,14 +78,4 @@ int APS5_VABI sceGameCustomDataDialogOpen() { return 0; } -int APS5_VABI sceGameCustomDataDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceGameCustomDataDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceNpSnsFacebookDialog/CMakeLists.txt b/core/libs/prx/libSceNpSnsFacebookDialog/CMakeLists.txt new file mode 100644 index 0000000000..5445d0dddd --- /dev/null +++ b/core/libs/prx/libSceNpSnsFacebookDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpSnsFacebookDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpSnsFacebookDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpSnsFacebookDialog) + +if(BUILD_TESTING) + add_test_executable(guest_np_sns_facebook_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpSnsFacebookDialog.cpp) + target_include_directories(guest_np_sns_facebook_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_sns_facebook_dialog_tests PRIVATE libSceNpSnsFacebookDialog) + add_test(NAME guest_np_sns_facebook_dialog COMMAND guest_np_sns_facebook_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_sns_facebook_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpSnsFacebookDialog>") + endif() +endif() diff --git a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp index 9a4fe0e8cd..e61b490597 100644 --- a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -1,42 +1,70 @@ -#include <cstddef> +#include <atomic> #include <cstdint> +#include <cstddef> +#include <cstring> #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<int>(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceNpSnsFacebookDialogClose(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSnsFacebookDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } +int APS5_VABI sceNpSnsFacebookDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogUpdateStatus(void) { + return g_status.load(); +} + int APS5_VABI sceNpSnsFacebookDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); + return g_status.load(); +} + +int APS5_VABI sceNpSnsFacebookDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); return 0; } -int APS5_VABI sceNpSnsFacebookDialogGetResult() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSnsFacebookDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } -int APS5_VABI sceNpSnsFacebookDialogInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpSnsFacebookDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpSnsFacebookDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpSnsFacebookDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpSnsFacebookDialogTerminate(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/GuestGameCustomDataDialog.cpp b/core/libs/tests/GuestGameCustomDataDialog.cpp new file mode 100644 index 0000000000..f410bc031b --- /dev/null +++ b/core/libs/tests/GuestGameCustomDataDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdlib> + +extern "C" { +int APS5_VABI sceGameCustomDataDialogInitialize(void); +int APS5_VABI sceGameCustomDataDialogOpenA(const void* param); +int APS5_VABI sceGameCustomDataDialogUpdateStatus(void); +int APS5_VABI sceGameCustomDataDialogGetStatus(void); +int APS5_VABI sceGameCustomDataDialogGetResultA(void* result); +int APS5_VABI sceGameCustomDataDialogClose(void); +int APS5_VABI sceGameCustomDataDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast<int>(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast<int>(0x80B80004); +constexpr int kErrNotFinished = static_cast<int>(0x80B80005); +constexpr int kErrArgNull = static_cast<int>(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceGameCustomDataDialogTerminate() == kErrNotInitialized); + Require(sceGameCustomDataDialogOpenA(¶m) == kErrNotInitialized); + Require(sceGameCustomDataDialogClose() == kErrNotInitialized); + Require(sceGameCustomDataDialogGetResultA(result) == kErrNotInitialized); + Require(sceGameCustomDataDialogGetStatus() == kStatusNone); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusNone); + + Require(sceGameCustomDataDialogInitialize() == 0); + Require(sceGameCustomDataDialogInitialize() == kErrAlreadyInitialized); + Require(sceGameCustomDataDialogGetStatus() == kStatusInitialized); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusInitialized); + + Require(sceGameCustomDataDialogOpenA(nullptr) == kErrArgNull); + Require(sceGameCustomDataDialogGetResultA(result) == kErrNotFinished); + + Require(sceGameCustomDataDialogOpenA(¶m) == 0); + Require(sceGameCustomDataDialogGetStatus() == kStatusFinished); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusFinished); + + Require(sceGameCustomDataDialogGetResultA(nullptr) == kErrArgNull); + Require(sceGameCustomDataDialogGetResultA(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceGameCustomDataDialogClose() == 0); + Require(sceGameCustomDataDialogTerminate() == 0); + Require(sceGameCustomDataDialogTerminate() == kErrNotInitialized); + Require(sceGameCustomDataDialogGetStatus() == kStatusNone); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusNone); + + Require(sceGameCustomDataDialogInitialize() == 0); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusInitialized); + Require(sceGameCustomDataDialogClose() == 0); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceGameCustomDataDialogGetResultA(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceGameCustomDataDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestNpSnsFacebookDialog.cpp b/core/libs/tests/GuestNpSnsFacebookDialog.cpp new file mode 100644 index 0000000000..2b78b0007c --- /dev/null +++ b/core/libs/tests/GuestNpSnsFacebookDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdlib> + +extern "C" { +int APS5_VABI sceNpSnsFacebookDialogInitialize(void); +int APS5_VABI sceNpSnsFacebookDialogOpen(const void* param); +int APS5_VABI sceNpSnsFacebookDialogUpdateStatus(void); +int APS5_VABI sceNpSnsFacebookDialogGetStatus(void); +int APS5_VABI sceNpSnsFacebookDialogGetResult(void* result); +int APS5_VABI sceNpSnsFacebookDialogClose(void); +int APS5_VABI sceNpSnsFacebookDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast<int>(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast<int>(0x80B80004); +constexpr int kErrNotFinished = static_cast<int>(0x80B80005); +constexpr int kErrArgNull = static_cast<int>(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceNpSnsFacebookDialogTerminate() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogClose() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(result) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetStatus() == kStatusNone); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusNone); + + Require(sceNpSnsFacebookDialogInitialize() == 0); + Require(sceNpSnsFacebookDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpSnsFacebookDialogGetStatus() == kStatusInitialized); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpSnsFacebookDialogOpen(nullptr) == kErrArgNull); + Require(sceNpSnsFacebookDialogGetResult(result) == kErrNotFinished); + + Require(sceNpSnsFacebookDialogOpen(¶m) == 0); + Require(sceNpSnsFacebookDialogGetStatus() == kStatusFinished); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusFinished); + + Require(sceNpSnsFacebookDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpSnsFacebookDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceNpSnsFacebookDialogClose() == 0); + Require(sceNpSnsFacebookDialogTerminate() == 0); + Require(sceNpSnsFacebookDialogTerminate() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetStatus() == kStatusNone); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusNone); + + Require(sceNpSnsFacebookDialogInitialize() == 0); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusInitialized); + Require(sceNpSnsFacebookDialogClose() == 0); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceNpSnsFacebookDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceNpSnsFacebookDialogTerminate() == 0); +} diff --git a/docs/dev/debt/libSceGameCustomDataDialog.md b/docs/dev/debt/libSceGameCustomDataDialog.md new file mode 100644 index 0000000000..09743a0781 --- /dev/null +++ b/docs/dev/debt/libSceGameCustomDataDialog.md @@ -0,0 +1,9 @@ +# libSceGameCustomDataDialog technical debt + +### Silent stubs + +- [libSceGameCustomDataDialog](../../../core/libs/prx/libSceGameCustomDataDialog/Export.cpp) - the game custom data dialog (`sceGameCustomDataDialogOpenA` and `sceGameCustomDataDialogGetResultA`) runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN game custom data. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. + +### Unknown function info + +- [sceGameCustomDataDialogGetResult and sceGameCustomDataDialogOpen](../../../core/libs/prx/libSceGameCustomDataDialog/Export.cpp) (libSceGameCustomDataDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGameCustomDataDialog.c), but Orbital has prototypes only for the `A` variants and no credible prototypes were found for these. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpSnsFacebookDialog.md b/docs/dev/debt/libSceNpSnsFacebookDialog.md index 3009c1d373..5ec4954b21 100644 --- a/docs/dev/debt/libSceNpSnsFacebookDialog.md +++ b/docs/dev/debt/libSceNpSnsFacebookDialog.md @@ -1,5 +1,5 @@ # libSceNpSnsFacebookDialog technical debt -### Unknown function info +### Silent stubs -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpSnsFacebookDialogClose`](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp), [`sceNpSnsFacebookDialogGetStatus`](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp). +- [libSceNpSnsFacebookDialog](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp) - the Facebook dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no Facebook account link. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. From 7358bec9ff7dd3db53547411369160771460d18f Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:17:39 -0300 Subject: [PATCH 1150/1360] fix(libc): isolate host stream buffering --- core/libs/prx/libc/include/General.hpp | 4 +-- core/libs/prx/libc/src/General.cpp | 30 +++++++++++++++++++ core/libs/prx/libc/src/StandardStreams.cpp | 8 ++++- core/libs/prx/libkernel/File/src/Stdio.cpp | 19 +----------- .../tests/GuestHostStandardErrorWindows.cpp | 4 +++ 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index f22c2e8556..bfdf1c9645 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -16,10 +16,10 @@ extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName); extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle); extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); extern "C" bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError); -#ifndef _WIN32 extern "C" int KeepHostStandardError_nid_no_patch(); -extern "C" int HostStandardError_nid_no_patch(); extern "C" int KeepHostStandardOutput_nid_no_patch(); +#ifndef _WIN32 +extern "C" int HostStandardError_nid_no_patch(); #else extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved); extern "C" std::FILE* HostStandardOutputFile_nid_no_patch(); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 26c1935c5a..a02ce8b108 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -557,6 +557,36 @@ extern "C" std::FILE* HostStandardErrorFile_nid_no_patch() { if (std::FILE* kept = g_hostStandardErrorFile.load(std::memory_order_acquire)) return kept; return __acrt_iob_func(2); } + +namespace { +std::mutex g_hostStandardStreamMutex; + +int keepHostStandardStream(int standard) { + auto& slot = standard == 1 ? g_hostStandardOutputFile : g_hostStandardErrorFile; + if (auto* stream = slot.load(std::memory_order_acquire)) return ::_fileno(stream); + const std::unique_ptr<std::FILE, decltype(&::_unlock_file)> unlock(__acrt_iob_func(standard), ::_unlock_file); + ::_lock_file(unlock.get()); + const std::lock_guard lock(g_hostStandardStreamMutex); + if (auto* stream = slot.load(std::memory_order_acquire)) return ::_fileno(stream); + const int saved = DuplicateHostDescriptor_nid_no_patch(standard); + if (saved < 0) return -1; + if (!AdoptHostStandardStream_nid_no_patch(standard, saved)) { + const int error = errno; + ::_close(saved); + errno = error; + return -1; + } + return saved; +} +} + +extern "C" int KeepHostStandardError_nid_no_patch() { + return keepHostStandardStream(2); +} + +extern "C" int KeepHostStandardOutput_nid_no_patch() { + return keepHostStandardStream(1); +} #endif extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 2cd656f01f..aeecad396a 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include <cerrno> #include <cstddef> @@ -194,7 +195,12 @@ int APS5_VABI setvbuf_nid_postfix(FileStream* stream, char* buffer, int mode, st buffer = nullptr; size = BUFSIZ; } - return std::setvbuf(GetStreamHandle(stream), buffer, native, size); + auto* handle = GetStreamHandle(stream); +#ifdef _WIN32 + if (handle == __acrt_iob_func(1) && KeepHostStandardOutput_nid_no_patch() < 0) return -1; + if (handle == __acrt_iob_func(2) && KeepHostStandardError_nid_no_patch() < 0) return -1; +#endif + return std::setvbuf(handle, buffer, native, size); } void APS5_VABI setbuf_nid_postfix(FileStream* stream, char* buffer) { setvbuf_nid_postfix(stream, buffer, buffer ? 0 : 2, buffer ? 1024 : 0); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index af3a52401b..53a4e4844f 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -757,26 +757,9 @@ std::atomic<int> g_hostStandardOutput{-1}; void PreserveHostStream(int standard, std::atomic<int>& slot) { if (slot.load() >= 0) return; -#ifdef _WIN32 - const int saved = WithoutParameterHandler([standard] { return ::_dup(standard); }); -#else const int saved = standard == 1 ? KeepHostStandardOutput_nid_no_patch() : KeepHostStandardError_nid_no_patch(); -#endif if (saved < 0) throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); - int expected = -1; - if (!slot.compare_exchange_strong(expected, saved)) { -#ifdef _WIN32 - _close(saved); -#endif - return; - } -#ifdef _WIN32 - if (!AdoptHostStandardStream_nid_no_patch(standard, saved)) { - slot.store(-1); - _close(saved); - throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); - } -#endif + slot.store(saved); } void PreserveHostStandardError() { PreserveHostStream(2, g_hostStandardError); } diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp index c571983957..559b90d3f2 100644 --- a/core/libs/tests/GuestHostStandardErrorWindows.cpp +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -22,6 +22,8 @@ int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI fflush_nid_postfix(FileStream*); int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); void APS5_VABI setbuf_nid_postfix(FileStream*, char*); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); void APS5_VABI perror_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); extern FileStream _Stdout_nid_postfix; @@ -106,8 +108,10 @@ static void Buffering(int standard) { Require(original >= 0 && ::_dup2(capture[1], standard) == 0); auto* guestStream = standard == 1 ? &_Stdout_nid_postfix : &_Stderr_nid_postfix; char buffer[4096]{}; + flockfile_nid_postfix(guestStream); if (standard == 1) setbuf_nid_postfix(guestStream, buffer); else Require(setvbuf_nid_postfix(guestStream, buffer, 0, sizeof(buffer)) == 0); + funlockfile_nid_postfix(guestStream); Require(fprintf_nid_postfix(guestStream, "guest-buffered") == 14); Require(std::fputs("host-immediate", standard == 1 ? stdout : stderr) >= 0); char bytes[64]{}; From 99018acee743642ccc67d76480ba0f4c9d85486c Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 <slopggobblercobbler81@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:07:00 +0200 Subject: [PATCH 1151/1360] fix(recompiler): normalize shader cache paths across platform separators --- .../recompiler/ShaderCacheVersion.cmake | 10 ++++--- .../tests/test_shader_cache_native_paths.py | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index 847a513496..e137778069 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -26,6 +26,7 @@ endforeach() list(SORT sources) list(SORT guard) get_filename_component(rootReal "${ROOT}" REALPATH) +get_filename_component(recompilerReal "${RECOMPILER_DIR}" REALPATH) set(sourcesReal) foreach(source IN LISTS sources) get_filename_component(real "${source}" REALPATH) @@ -39,8 +40,9 @@ foreach(source IN LISTS sources) if(NOT EXISTS "${source}") continue() endif() + get_filename_component(sourceReal "${source}" REALPATH) file(SHA256 "${source}" digest) - file(RELATIVE_PATH relative "${ROOT}" "${source}") + file(RELATIVE_PATH relative "${rootReal}" "${sourceReal}") string(APPEND manifest "${relative} ${digest}\n") if(NOT source MATCHES "\\.(cpp|hpp|h)$") continue() @@ -50,11 +52,11 @@ foreach(source IN LISTS sources) string(REGEX MATCHALL "\"APS5_[A-Z0-9_]+\"" names "${line}") list(APPEND switches ${names}) endforeach() - string(FIND "${source}" "${RECOMPILER_DIR}/" inRecompiler) + string(FIND "${sourceReal}" "${recompilerReal}/" inRecompiler) if(NOT inRecompiler EQUAL 0) continue() endif() - get_filename_component(sourceDirectory "${source}" DIRECTORY) + get_filename_component(sourceDirectory "${sourceReal}" DIRECTORY) file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") foreach(line IN LISTS lines) string(REGEX REPLACE "^[ \t]*#[ \t]*include[ \t]*\"([^\"]+)\".*$" "\\1" included "${line}") @@ -65,7 +67,7 @@ foreach(source IN LISTS sources) break() endif() endforeach() - file(RELATIVE_PATH relativeSource "${ROOT}" "${source}") + file(RELATIVE_PATH relativeSource "${rootReal}" "${sourceReal}") if(NOT resolved) continue() endif() diff --git a/core/shader/recompiler/tests/test_shader_cache_native_paths.py b/core/shader/recompiler/tests/test_shader_cache_native_paths.py index b2f1e8c57f..1c1ad63ae2 100644 --- a/core/shader/recompiler/tests/test_shader_cache_native_paths.py +++ b/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -74,5 +74,32 @@ class ShaderCachePathTests(unittest.TestCase): self.assertIn('does not hash', result.stdout + result.stderr) self.assertFalse(output.exists()) + def test_windows_path_separators(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / 'windows separators' + source, listing, output = self.fixture(root) + windows_path = str(source).replace('/', '\\') + listing.write_text(f'sources {windows_path}\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + windows_version = output.read_bytes() + listing.write_text(f'sources {source.as_posix()}\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(windows_version, output.read_bytes()) + + def test_windows_path_separators_reject_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / 'windows separators' + source, listing, output = self.fixture(root) + windows_path = str(source).replace('/', '\\') + listing.write_text(f'sources {windows_path}\n', encoding='utf-8') + (root / 'shared.hpp').write_text('int example();\n', encoding='utf-8') + source.write_text('#include "../shared.hpp"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + unittest.main() From f6826208fb6408b4e42c5979baf1a4f4a3eb0120 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:29:25 +0000 Subject: [PATCH 1152/1360] fix(shader): normalize offset perspective barycentrics --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 3 ++ .../IrMetadata/ShaderInfo.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 3 ++ core/shader/recompiler/Recompiler.cpp | 2 +- core/shader/recompiler/Recompiler.hpp | 2 ++ core/shader/recompiler/ShaderDiskCache.cpp | 6 +++- .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/BarycentricGeometryShader.cpp | 32 ++++++++++++++++--- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 7 +++- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 19 ++++++++++- 10 files changed, 67 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4b05f827f4..790bf35cba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7969,6 +7969,9 @@ void barycentricEmulationTests() { const std::array<FragmentParameter, 3> parameters{{{0u, 0u, false, false}, {1u, 1u, false, true}, {2u, 2u, true, false}}}; const auto layout = LayoutBarycentricEmulation(parameters, {true, true, true}); Require(layout.smoothLocation == 1u && layout.linearLocation == 3u && layout.perVertexLocations == std::vector<std::pair<std::uint32_t, std::uint32_t>>{{1u, 4u}}, "emulated barycentrics did not take the free locations in order"); + const auto offsetLayout = LayoutBarycentricEmulation(parameters, {true, true, false, true}); + Require(offsetLayout.smoothLocation == 1u && offsetLayout.reciprocalWLocation == 3u && offsetLayout.perVertexLocations == std::vector<std::pair<std::uint32_t, std::uint32_t>>{{1u, 4u}}, "perspective offset emulation did not reserve reciprocal-w inputs"); + Require(LayoutBarycentricEmulation(parameters, {true, true, false}).reciprocalWLocation == BarycentricEmulationLayout::NoLocation, "non-offset perspective barycentrics reserved reciprocal-w inputs"); Require(LayoutBarycentricEmulation(parameters, {}).perVertexLocations.empty() && LayoutBarycentricEmulation(parameters, {}).smoothLocation == BarycentricEmulationLayout::NoLocation, "an inactive emulation was given locations"); std::vector<FragmentParameter> crowded; for (std::uint32_t location = 0; location < 11u; ++location) crowded.push_back({location, location, false, true}); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 1440effcb2..76c0af830f 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -37,6 +37,7 @@ struct ShaderInfo { bool dispatchThreadLimit = false; bool rasterPrimitive = false; bool layerExported = false; + bool smoothOffsetBarycentrics = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index a111de6891..d9c6fbcc3d 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -423,8 +423,11 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { AddInput(info, kind, 0, 1, "gl_SampleMaskIn"); break; case StageInputKind::BaryCoordSmooth: + AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR"); + break; case StageInputKind::BaryCoordSmoothCenter: case StageInputKind::BaryCoordSmoothSample: + info.smoothOffsetBarycentrics = true; AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR"); if (kind == StageInputKind::BaryCoordSmoothSample && program.Metadata().pixelIteratesSamples) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); break; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index dae9b85256..dab956cb91 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -686,7 +686,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.fragmentParameters = DescribeFragmentParameters(program, inputInfo); const auto& inputs = program.Info().inputs; const auto reads = [&](StageInputKind kind) { return std::any_of(inputs.begin(), inputs.end(), [&](const auto& input) { return input.kind == kind; }); }; - if (program.Metadata().barycentricEmulation) result.barycentricEmulation = {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}; + if (program.Metadata().barycentricEmulation) result.barycentricEmulation = {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective), program.Info().smoothOffsetBarycentrics}; } if (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local) { if (inputInfo.vertex == nullptr) throw std::runtime_error("vertex input metadata is missing"); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 4cb5a69c8d..98f343bacf 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -389,6 +389,7 @@ struct BarycentricEmulation { bool active = false; bool smooth = false; bool linear = false; + bool smoothOffset = false; bool operator==(const BarycentricEmulation&) const = default; }; @@ -513,6 +514,7 @@ struct BarycentricEmulationLayout { std::uint32_t smoothLocation = NoLocation; std::uint32_t linearLocation = NoLocation; std::uint32_t positionsLocation = NoLocation; + std::uint32_t reciprocalWLocation = NoLocation; std::vector<std::pair<std::uint32_t, std::uint32_t>> perVertexLocations; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 2362ce871a..ab479b1043 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -139,7 +139,7 @@ constexpr std::size_t fieldCount() { } } -static_assert(fieldCount<ShaderInfo>() == 23, "ShaderInfo changed: update the info encoder"); +static_assert(fieldCount<ShaderInfo>() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount<BufferResource>() == 12, "BufferResource changed: update the info encoder"); static_assert(fieldCount<ImageResource>() == 41, "ImageResource changed: update the info encoder"); static_assert(fieldCount<SamplerResource>() == 8, "SamplerResource changed: update the info encoder"); @@ -509,6 +509,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); writer.Value(result.barycentricEmulation.linear); + writer.Value(result.barycentricEmulation.smoothOffset); writer.Value(result.meshBarycentrics.has_value()); if (result.meshBarycentrics) { writer.Value(result.meshBarycentrics->emulation.active); @@ -568,6 +569,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); reader.Value(result.barycentricEmulation.linear); + reader.Value(result.barycentricEmulation.smoothOffset); bool hasMeshBarycentrics = false; reader.Value(hasMeshBarycentrics); result.meshBarycentrics.reset(); @@ -801,6 +803,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { writer.Value(info.dispatchThreadLimit); writer.Value(info.rasterPrimitive); writer.Value(info.layerExported); + writer.Value(info.smoothOffsetBarycentrics); } void decodeShaderInfo(Reader& reader, ShaderInfo& info) { @@ -908,6 +911,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { reader.Value(info.dispatchThreadLimit); reader.Value(info.rasterPrimitive); reader.Value(info.layerExported); + reader.Value(info.smoothOffsetBarycentrics); } void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 92ea828d77..1d668cdad8 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -227,6 +227,7 @@ struct SpirvEmitterState { std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t subgroupIdVariable = 0; std::uint32_t perVertexVariable = 0; + std::uint32_t barycentricReciprocalWVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; std::optional<std::uint32_t> zeroPositionClipIndex; diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index 6f86243daf..00cc69e109 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -77,6 +77,8 @@ public: }; const auto smooth = barycentricOutput(layout.smoothLocation); const auto linear = barycentricOutput(layout.linearLocation); + const auto reciprocalW = barycentricOutput(layout.reciprocalWLocation); + if (reciprocalW != 0u) builder.AddAnnotation(spv::OpDecorate, reciprocalW, spv::DecorationNoPerspective); std::vector<std::uint32_t> perVertexOutputs; for (const auto& parameter : perVertex) { perVertexOutputs.push_back(addInterface(spv::StorageClassOutput, vertexArrayType)); @@ -94,11 +96,18 @@ public: for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) values[location][vertex] = load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[location], constant(intType, vertex))); } for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) { - store(position, load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, constant(intType, vertex), constant(intType, 0u)))); + const auto clipPosition = load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, constant(intType, vertex), constant(intType, 0u))); + store(position, clipPosition); for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], values[passed[i]][vertex]); const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, vertex == 0u ? one : zero, vertex == 1u ? one : zero, vertex == 2u ? one : zero); if (smooth != 0u) store(smooth, weights); if (linear != 0u) store(linear, weights); + if (reciprocalW != 0u) { + const auto clipW = result(spv::OpCompositeExtract, floatType, clipPosition, 3u); + const auto reciprocal = result(spv::OpFDiv, floatType, one, clipW); + const auto reciprocalWeights = result(spv::OpCompositeConstruct, vec3FloatType, vertex == 0u ? reciprocal : zero, vertex == 1u ? reciprocal : zero, vertex == 2u ? reciprocal : zero); + store(reciprocalW, reciprocalWeights); + } for (std::size_t i = 0; i < perVertex.size(); ++i) { for (std::uint32_t source = 0; source < 3u; ++source) store(result(spv::OpAccessChain, ptrOutputVec4, perVertexOutputs[i], constant(intType, source)), values[perVertex[i].sourceLocation][source]); } @@ -251,6 +260,8 @@ public: }; const auto smooth = barycentricOutput(layout.smoothLocation); const auto linear = barycentricOutput(layout.linearLocation); + const auto reciprocalW = barycentricOutput(layout.reciprocalWLocation); + if (reciprocalW != 0u) builder.AddAnnotation(spv::OpDecorate, reciprocalW, spv::DecorationNoPerspective); std::vector<std::uint32_t> perVertexOutputs; for (const auto& parameter : perVertex) { perVertexOutputs.push_back(addInterface(spv::StorageClassOutput, vertexArrayType)); @@ -275,6 +286,16 @@ public: for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, provokingVertex) : index))); if (smooth != 0u) store(smooth, weights); if (linear != 0u) store(linear, weights); + if (reciprocalW != 0u) { + std::array<std::uint32_t, 3> reciprocalWeights{}; + for (std::uint32_t source = 0; source < 3u; ++source) { + const auto clipPosition = load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, constant(intType, source), constant(intType, 0u))); + const auto clipW = result(spv::OpCompositeExtract, floatType, clipPosition, 3u); + const auto inverseW = result(spv::OpFDiv, floatType, one, clipW); + reciprocalWeights[source] = result(spv::OpFMul, floatType, weight(source), inverseW); + } + store(reciprocalW, result(spv::OpCompositeConstruct, vec3FloatType, reciprocalWeights[0], reciprocalWeights[1], reciprocalWeights[2])); + } for (std::size_t i = 0; i < perVertex.size(); ++i) { for (std::uint32_t source = 0; source < 3u; ++source) store(result(spv::OpAccessChain, ptrOutputVec4, perVertexOutputs[i], constant(intType, source)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[perVertex[i].sourceLocation], constant(intType, source)))); } @@ -370,6 +391,7 @@ BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentPa if (emulation.smooth) layout.smoothLocation = take(1u); if (emulation.linear) layout.linearLocation = take(1u); if (meshPositions) layout.positionsLocation = take(3u); + if (emulation.smoothOffset) layout.reciprocalWLocation = take(1u); for (const auto& parameter : parameters) { if (parameter.perVertex) layout.perVertexLocations.emplace_back(parameter.location, take(3u)); } @@ -404,9 +426,9 @@ RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, co const auto emulated = CollectEmulatedInterface(vertex, fragment); const auto passed = static_cast<std::uint32_t>(emulated.passed.size()); const auto perVertex = static_cast<std::uint32_t>(emulated.perVertex.size()); - const auto outputComponents = 4u + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; + const auto outputComponents = 4u + 4u * passed + 3u * (emulated.barycentrics + (emulated.layout.reciprocalWLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u)) + 12u * perVertex; const auto inputComponents = 4u + 4u * emulated.sources; - const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex + (emulated.layout.reciprocalWLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u)); require(limits->maxGeometryOutputVertices >= 3u && outputComponents <= limits->maxGeometryOutputComponents && 3u * outputComponents <= limits->maxGeometryTotalOutputComponents && inputComponents <= limits->maxGeometryInputComponents && fragmentComponents <= limits->maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); BarycentricGeometryEmitter emitter(target.spirvVersion); RecompileResult result; @@ -429,8 +451,8 @@ BarycentricTessellationShaders BuildBarycentricTessellationShaders(const Recompi const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); const auto controlComponents = 4u + clipDistances + 4u * emulated.sources; - const auto evaluationOutputComponents = 4u + clipDistances + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; - const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); + const auto evaluationOutputComponents = 4u + clipDistances + 4u * passed + 3u * (emulated.barycentrics + (emulated.layout.reciprocalWLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u)) + 12u * perVertex; + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex + (emulated.layout.reciprocalWLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u)); const auto& limits = *target.tessellation; require(limits.maxPatchSize >= 3u && controlComponents <= limits.maxControlPerVertexInputComponents && controlComponents <= limits.maxControlPerVertexOutputComponents && 3u * controlComponents + 4u <= limits.maxControlTotalOutputComponents && limits.maxControlPerPatchOutputComponents >= 4u && controlComponents <= limits.maxEvaluationInputComponents && evaluationOutputComponents <= limits.maxEvaluationOutputComponents && fragmentComponents <= maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); std::set<std::uint32_t> sourceSet(emulated.passed.begin(), emulated.passed.end()); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index a8068f0415..accb81d256 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -218,7 +218,7 @@ void DefineInputs(SpirvEmitterState& state) { const auto reads = [&](StageInputKind kind) { return std::any_of(state.inputs.begin(), state.inputs.end(), [&](const SpirvInputBinding& input) { return input.kind == kind; }); }; - emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}, state.meshBarycentricPixel); + emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective), state.program.Info().smoothOffsetBarycentrics}, state.meshBarycentricPixel); if (state.meshBarycentricPixel && emulation.positionsLocation != BarycentricEmulationLayout::NoLocation) { state.pixelFlatPositions = DefineInterfaceVariable(state, state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)), spv::StorageClassInput, "meshBarycentricPositions"); state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationLocation, emulation.positionsLocation); @@ -226,6 +226,11 @@ void DefineInputs(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityMeshShadingEXT); state.module.EmitExtension("SPV_EXT_mesh_shader"); } + if (emulation.reciprocalWLocation != BarycentricEmulationLayout::NoLocation) { + state.barycentricReciprocalWVariable = DefineInterfaceVariable(state, TypeF32Vector(state, 3u), spv::StorageClassInput, "gl_BarycentricReciprocalW"); + state.module.AddAnnotation(spv::OpDecorate, state.barycentricReciprocalWVariable, spv::DecorationNoPerspective); + state.module.AddAnnotation(spv::OpDecorate, state.barycentricReciprocalWVariable, spv::DecorationLocation, emulation.reciprocalWLocation); + } } for (auto& input : state.inputs) { if (pixelStage && input.kind == StageInputKind::Parameter) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 57eb96f9ce..4f558e43a1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -142,7 +142,10 @@ std::uint32_t EmitSampleOffset(SpirvEmitterState& state) { } std::uint32_t EmitOffsetBarycentricU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component) { - const auto variable = InputVariableForKind(state, BarycentricInputOf(kind)); + const bool smoothOffset = kind == StageInputKind::BaryCoordSmoothCenter || kind == StageInputKind::BaryCoordSmoothSample; + const auto variable = smoothOffset && state.barycentricReciprocalWVariable != 0u + ? state.barycentricReciprocalWVariable + : InputVariableForKind(state, BarycentricInputOf(kind)); if (variable == 0u) { throw std::runtime_error("an offset barycentric read lacks its barycentric input"); } @@ -156,6 +159,20 @@ std::uint32_t EmitOffsetBarycentricU32(SpirvEmitterState& state, StageInputKind state.module.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3u), weights, GlslStd450(state), GLSLstd450InterpolateAtOffset, variable, offset); const auto weight = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), weight, weights, component + 1u); + if (smoothOffset && state.barycentricReciprocalWVariable != 0u) { + std::array<std::uint32_t, 3> components{}; + for (std::uint32_t index = 0; index < components.size(); ++index) { + components[index] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), components[index], weights, index); + } + const auto firstSum = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, TypeF32(state), firstSum, components[0], components[1]); + const auto sum = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, TypeF32(state), sum, firstSum, components[2]); + const auto normalized = state.module.AllocateId(); + state.module.AddFunction(spv::OpFDiv, TypeF32(state), normalized, weight, sum); + return Unary(state, spv::OpBitcast, TypeU32(state), normalized); + } return Unary(state, spv::OpBitcast, TypeU32(state), weight); } From 62bd4638d2338be17a39cd7e6fe5e07c4e81d5f8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:30:43 +0000 Subject: [PATCH 1153/1360] fix(shader): bump cache format for barycentric metadata --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index e5acfa71f9..abd799b205 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 44; +inline constexpr std::uint32_t FormatVersion = 45; enum class LoadStatus { Loaded, From e58b8f5c221dc772e2b7372f5fd5c3bc5bb9d89c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:33:14 +0000 Subject: [PATCH 1154/1360] test: drop obsolete HTTP cookie stub expectations --- core/libs/tests/GuestOfflineImports.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 54acdb9e0c..351c78faa7 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -6,14 +6,12 @@ #include <cstdlib> #include <cstring> #include <stdexcept> -#include <string_view> extern "C" { int APS5_VABI sceVideoRecordingGetInfo(std::int32_t info, void* data, std::size_t size); int APS5_VABI sceVideoRecordingSetInfo(std::int32_t info, const void* data, std::size_t size); int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t); int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t); -int APS5_VABI sceHttpSetCookieEnabled(int, int); int APS5_VABI sceHttpSendRequest(int, const void*, std::size_t); int APS5_VABI sceNpEntitlementAccessGetEntitlementKey( std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessEntitlementKey*); @@ -76,20 +74,8 @@ int main() { Require(abortFailed && deleteFailed); } - constexpr int invalidValue = static_cast<int>(0x804311FE); constexpr int network = static_cast<int>(0x80431063); - Require(sceHttpSetCookieEnabled(1, 0) == 0); Require(sceHttpSendRequest(1, nullptr, 0) == network); - bool cookieUnsupported = false; - try { - sceHttpSetCookieEnabled(1, 1); - } catch (const std::runtime_error& error) { - cookieUnsupported = std::string_view(error.what()) == "sceHttpSetCookieEnabled not implemented"; - } - Require(cookieUnsupported); - for (int enabled : {-1, 2, 0x100}) { - Require(sceHttpSetCookieEnabled(1, enabled) == invalidValue); - } constexpr int parameter = static_cast<int>(0x817D0002); constexpr int noEntitlement = static_cast<int>(0x817D0007); From f73c71563a1ac30f926032deaa2dba548889a2a5 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:37:00 +0200 Subject: [PATCH 1155/1360] docs(debt): note what a title call of sceKeyboardGetKey2Char shows about the output buffer Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceKeyboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md index 60edc36b24..ba3dcb499f 100644 --- a/docs/dev/debt/libSceKeyboard.md +++ b/docs/dev/debt/libSceKeyboard.md @@ -2,7 +2,7 @@ ### Functional -- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp): the original validates HID usages 4 through 231 before the handle and preserves unconverted HID usages in the output with `processed == 0`. The host instead accepts out-of-range usages and writes zero for unconverted keys. The original ignores the Kana LED, while the host throws for it. This partially implemented function contains `NotImplemented_nid_no_patch(__func__)`; its body is preserved under the requested restriction. +- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp): the original validates HID usages 4 through 231 before the handle and preserves unconverted HID usages in the output with `processed == 0`. The host instead accepts out-of-range usages and writes zero for unconverted keys. The original ignores the Kana LED, while the host throws for it. This partially implemented function contains `NotImplemented_nid_no_patch(__func__)`; its body is preserved under the requested restriction. One title (PPSA15246) disassembled for this entry calls it once with an output buffer cleared to 24 bytes, uses a character only when the result is 0 and the first output byte is nonzero, and then reads the 16-bit value at offset 8, which agrees with `processed` and `char_code` as laid out here. Which other output fields it reads and what its other arguments mean are not shown. ### Unknown function info From 0a3ea213e16a691724efcb8ae8a12d083f081b28 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Wed, 21 Oct 2026 01:30:13 +0700 Subject: [PATCH 1156/1360] fix(shader): read a 2D MSAA T# through DIM 2D as its sample 0 A DIM 2D image_load or image_sample of a 2D MSAA T# returns sample 0 on RDNA2 (RX 6700 XT, measured through HIP; bilinear blends sample-0 texels). The recompiler now reads such a direct sampled access as a 2D image, and the driver binds a one-sample copy of the target's sample 0, refreshed by a fragment pass before each binding. Peace Walker (PPSA18216) stops on this read at the end of its opening movie. Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 21 +- .../Graphics/include/FirstSampleCopy.hpp | 40 +++ .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/include/Recorder.hpp | 2 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/shaders/FirstSample.frag | 16 ++ .../Graphics/src/FirstSampleCopy.cpp | 252 ++++++++++++++++++ .../Graphics/src/MultisampleTarget.cpp | 41 +++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 24 +- .../Graphics/src/ShaderResources.cpp | 12 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 95 +++++-- .../libSceAgcDriver/tests/ShaderMemory.cpp | 32 +++ .../execution/SampleMultisampleTarget.cpp | 128 ++++++++- .../Optimization/src/ResourceMaterializer.cpp | 5 + docs/dev/debt/libSceAgcDriver.md | 1 + docs/dev/debt/shader.md | 1 + 16 files changed, 629 insertions(+), 43 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7902207afd..cccc8f8739 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -225,6 +225,22 @@ foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;- ) list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader}) endforeach() +foreach(agcFirstSampleClass IN ITEMS Float Uint Sint) + string(TOUPPER "${agcFirstSampleClass}" agcFirstSampleSymbol) + set(agcFirstSampleShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/FirstSample.frag) + set(agcFirstSampleShaderSpv ${agcTextureDetileGeneratedDir}/FirstSample${agcFirstSampleClass}.spv) + set(agcFirstSampleShaderHeader ${agcTextureDetileGeneratedDir}/FirstSample${agcFirstSampleClass}_spv.h) + add_custom_command( + OUTPUT "${agcFirstSampleShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -D${agcFirstSampleSymbol}_SAMPLES=1 -o "${agcFirstSampleShaderSpv}" "${agcFirstSampleShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcFirstSampleShaderSpv} -DOUTPUT=${agcFirstSampleShaderHeader} + -DSYMBOL=FIRST_SAMPLE_${agcFirstSampleSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcFirstSampleShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcFirstSampleShaderHeader}) +endforeach() foreach(agcOverlayStage IN ITEMS Vert Frag) string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) @@ -323,6 +339,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/FirstSampleCopy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -1657,10 +1674,12 @@ if(BUILD_TESTING) set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only) set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_uncounted_passes COMMAND agc_driver_recorder_tests --uncounted-passes-only) + set_tests_properties(agc_uncounted_passes PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only) set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) - set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_uncounted_passes agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp new file mode 100644 index 0000000000..4f973ff75e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp @@ -0,0 +1,40 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_FIRSTSAMPLECOPY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_FIRSTSAMPLECOPY_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Graphics { + +class Recorder; + +class FirstSampleCopy { +public: + FirstSampleCopy(const Context& context, VkFormat format, VkExtent2D extent); + ~FirstSampleCopy(); + FirstSampleCopy(const FirstSampleCopy&) = delete; + FirstSampleCopy& operator=(const FirstSampleCopy&) = delete; + + void Record(Recorder& recorder, VkImageView source, VkImageLayout layout); + VkImage Image() const { return image; } + +private: + void release() noexcept; + + Context context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkSampler sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkRenderPass renderPass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index b9c18eb7d1..68eb4d07db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -67,6 +67,7 @@ bool TakeMultisampledSingleClear(std::span<const std::uint32_t> code, std::span< bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target); std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); +std::shared_ptr<Texture> MultisampleTargetFirstSample(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image = nullptr); void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr<void>& owner); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 6ef4b5cf08..2a569afda2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -220,6 +220,7 @@ public: bool QueuesSampleDumps() const; void PrepareSampleSlot(); void EndPassSamples(); + void RecordUncounted(const std::function<void(VkCommandBuffer)>& record); struct SampleDumpStatistics { std::uint64_t queued; std::uint64_t queuedInPass; @@ -578,6 +579,7 @@ private: VkQueryPool queries = VK_NULL_HANDLE; VkQueryPool samples = VK_NULL_HANDLE; std::shared_ptr<void> samplePool; + std::vector<VkQueryPool> endedSamples; bool sampleActive = false; bool samplesDrawn = false; VkQueryPool segments = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5321104605..fb6569cf46 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -472,6 +472,7 @@ private: std::array<std::uint32_t, 8> words{}; bool depthCompare = false; VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D; + bool firstSample = false; std::shared_ptr<Texture> texture; }; PreviousSampled previousSampled; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag new file mode 100644 index 0000000000..287527d25a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag @@ -0,0 +1,16 @@ +#version 450 + +#if defined(UINT_SAMPLES) +layout(set = 0, binding = 0) uniform usampler2DMS source; +layout(location = 0) out uvec4 color; +#elif defined(SINT_SAMPLES) +layout(set = 0, binding = 0) uniform isampler2DMS source; +layout(location = 0) out ivec4 color; +#else +layout(set = 0, binding = 0) uniform sampler2DMS source; +layout(location = 0) out vec4 color; +#endif + +void main() { + color = texelFetch(source, ivec2(gl_FragCoord.xy), 0); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp new file mode 100644 index 0000000000..3a33f8bc0d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp @@ -0,0 +1,252 @@ +#include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/FirstSampleFloat_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/FirstSampleSint_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/FirstSampleUint_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassVert_spv.h" +#include <memory> +#include <span> + +namespace AgcDriver::Graphics { +namespace { + +bool sintFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_SINT: + case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_SINT: + case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32A32_SINT: + return true; + default: return false; + } +} + +bool uintFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_B8G8R8A8_UINT: + case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_R16_UINT: + case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32B32A32_UINT: + return true; + default: return false; + } +} + +std::span<const std::uint32_t> fragmentCode(VkFormat format) { + if (sintFormat(format)) return FIRST_SAMPLE_SINT_SPV; + if (uintFormat(format)) return FIRST_SAMPLE_UINT_SPV; + return FIRST_SAMPLE_FLOAT_SPV; +} + +struct DescriptorPool { + DescriptorPool(const Context& context, VkDescriptorPool pool) : context(context), pool(pool) {} + ~DescriptorPool() { context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr); } + DescriptorPool(const DescriptorPool&) = delete; + DescriptorPool& operator=(const DescriptorPool&) = delete; + Context context; + VkDescriptorPool pool; +}; + +} + +FirstSampleCopy::FirstSampleCopy(const Context& context, VkFormat format, VkExtent2D extent) : context(context), extent(extent) { + try { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = format; + imageInfo.extent = {extent.width, extent.height, 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage first sample"); + VkMemoryRequirements requirements{}; + context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory first sample"); + Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory first sample"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView first sample"); + + VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + Check(context.Function<PFN_vkCreateSampler>("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler first sample"); + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + binding.pImmutableSamplers = &sampler; + VkDescriptorSetLayoutCreateInfo descriptorInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + descriptorInfo.bindingCount = 1; + descriptorInfo.pBindings = &binding; + Check(context.Function<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout first sample"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &descriptorLayout; + Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout first sample"); + + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0}, + }; + VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + renderPassInfo.attachmentCount = 1; + renderPassInfo.pAttachments = &attachment; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = 2; + renderPassInfo.pDependencies = dependencies; + Check(context.Function<PFN_vkCreateRenderPass>("vkCreateRenderPass")(context.device, &renderPassInfo, nullptr, &renderPass), "vkCreateRenderPass first sample"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1; + Check(context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer first sample"); + + vertex = CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule first sample vertex"); + const auto code = fragmentCode(format); + fragment = CreateShaderModule(context, code.data(), code.size_bytes(), "vkCreateShaderModule first sample fragment"); + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewportRect{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.pViewports = &viewportRect; + viewport.scissorCount = 1; + viewport.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewport; + pipelineInfo.pRasterizationState = &rasterization; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pColorBlendState = &blend; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.renderPass = renderPass; + Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines first sample"); + } catch (...) { + release(); + throw; + } +} + +FirstSampleCopy::~FirstSampleCopy() { + release(); +} + +void FirstSampleCopy::release() noexcept { + if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (fragment) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (framebuffer) context.Function<PFN_vkDestroyFramebuffer>("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (renderPass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, renderPass, nullptr); + if (pipelineLayout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (descriptorLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); + if (sampler) context.Function<PFN_vkDestroySampler>("vkDestroySampler")(context.device, sampler, nullptr); + if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr); + pipeline = VK_NULL_HANDLE; + fragment = vertex = VK_NULL_HANDLE; + framebuffer = VK_NULL_HANDLE; + renderPass = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; + descriptorLayout = VK_NULL_HANDLE; + sampler = VK_NULL_HANDLE; + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; +} + +void FirstSampleCopy::Record(Recorder& recorder, VkImageView source, VkImageLayout layout) { + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &size; + VkDescriptorPool created = VK_NULL_HANDLE; + Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &created), "vkCreateDescriptorPool first sample"); + const auto pool = std::make_shared<DescriptorPool>(context, created); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = pool->pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &descriptorLayout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function<PFN_vkAllocateDescriptorSets>("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets first sample"); + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, source, layout}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imageInfo; + context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); + recorder.Keep(pool); + recorder.RecordUncounted([&](VkCommandBuffer commands) { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = renderPass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Function<PFN_vkCmdBindDescriptorSets>("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &set, 0, nullptr); + context.Function<PFN_vkCmdDraw>("vkCmdDraw")(commands, 3, 1, 0, 0); + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 809e680d42..b2fa994818 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" @@ -426,6 +427,19 @@ public: return {shared_from_this(), texture.get()}; } + std::shared_ptr<Texture> FirstSample(VkComponentMapping components) { + const auto source = Sampled({}); + if (firstSampleCopy == nullptr) firstSampleCopy = std::make_unique<FirstSampleCopy>(context, target.format, target.extent); + firstSampleCopy->Record(*Recorder::Active(), source->View(), source->Layout()); + const std::array<std::uint32_t, 4> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a)}; + auto& texture = firstSampleTextures[key]; + if (texture == nullptr) { + texture = std::make_shared<Texture>(context, firstSampleCopy->Image(), target.format, VK_IMAGE_ASPECT_COLOR_BIT, components); + texture->MarkRefreshedPerUse(); + } + return {shared_from_this(), texture.get()}; + } + VkImageView View(VkFormat format) { if (format == target.format) return view; Require(twinFormat != VK_FORMAT_UNDEFINED && format == twinFormat, "a multisampled color target is only viewed in its own format or its sRGB twin"); @@ -516,6 +530,8 @@ private: std::map<std::array<std::uint32_t, 5>, std::unique_ptr<Texture>> fmaskTextures; std::map<std::array<std::uint32_t, 5>, std::shared_ptr<Texture>> textures; std::vector<std::shared_ptr<Texture>> retiredTextures; + std::unique_ptr<FirstSampleCopy> firstSampleCopy; + std::map<std::array<std::uint32_t, 4>, std::shared_ptr<Texture>> firstSampleTextures; std::vector<GuestMemory::UnchangedQuery> dccRanges; std::optional<std::uint32_t> dccFill; std::optional<std::uint32_t> cmaskFill; @@ -626,6 +642,8 @@ private: fmaskTextures.clear(); textures.clear(); retiredTextures.clear(); + firstSampleTextures.clear(); + firstSampleCopy.reset(); for (const auto& [format, resolvePass] : resolvePasses) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr); if (orderView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, orderView, nullptr); if (twinView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, twinView, nullptr); @@ -1159,6 +1177,29 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const throw std::runtime_error(text); } +std::shared_ptr<Texture> MultisampleTargetFirstSample(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + const bool sameShape = rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0; + if (sameShape && rendered.format == format) return target->FirstSample(components); + if (sameShape && redBlueSwapped(rendered.format, format)) return target->FirstSample(swapRedBlue(components)); + other = target.get(); + } + if (other == nullptr) { + Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); + return createFilled(context, resource, format, "sampling").FirstSample(components); + } + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: reading sample 0 of multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast<unsigned long long>(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast<int>(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast<int>(format)); + throw std::runtime_error(text); +} + void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); acquire(context, target); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9f03343a67..4f1fca0558 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2282,6 +2282,20 @@ void Recorder::EndPassSamples() { if (open != nullptr && open->segmentActive) endSegment(*open); } +void Recorder::RecordUncounted(const std::function<void(VkCommandBuffer)>& record) { + const auto commands = Commands(); + if (open->segmentActive) endSegment(*open); + const bool counted = open->sampleActive; + if (counted) { + open->endedSamples.reserve(open->endedSamples.size() + 1); + endSamples(*open); + open->endedSamples.push_back(open->samples); + open->samples = VK_NULL_HANDLE; + } + record(commands); + if (counted) beginSamples(*open); +} + void Recorder::takeSegmentPool(Batch& batch) { auto cache = std::static_pointer_cast<SamplePoolCache>(segmentPools); if (cache == nullptr) { @@ -2572,9 +2586,15 @@ void Recorder::beginSamples(Batch& batch) { void Recorder::readSamples(Batch& batch) { if (sampleCounterState > 0) return; if (batch.samples == VK_NULL_HANDLE || !batch.submitted) return; + std::uint64_t total = 0; + for (const auto pool : batch.endedSamples) { + std::uint64_t samples = 0; + Check(context.Function<PFN_vkGetQueryPoolResults>("vkGetQueryPoolResults")(context.device, pool, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); + total += samples; + } std::uint64_t samples = 0; Check(context.Function<PFN_vkGetQueryPoolResults>("vkGetQueryPoolResults")(context.device, batch.samples, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); - samplesPassed.fetch_add(samples, std::memory_order_acq_rel); + samplesPassed.fetch_add(total + samples, std::memory_order_acq_rel); } void Recorder::readGpuTiming(Batch& batch) { @@ -4181,6 +4201,8 @@ void Recorder::release(Batch& batch) noexcept { if (batch.samples != VK_NULL_HANDLE && batch.samplePool == nullptr) context.Function<PFN_vkDestroyQueryPool>("vkDestroyQueryPool")(context.device, batch.samples, nullptr); batch.samples = VK_NULL_HANDLE; batch.samplePool.reset(); + for (const auto pool : batch.endedSamples) context.Function<PFN_vkDestroyQueryPool>("vkDestroyQueryPool")(context.device, pool, nullptr); + batch.endedSamples.clear(); batch.segments = VK_NULL_HANDLE; batch.segmentPool.reset(); // A completed (or never submitted) batch's objects are kept for reuse: the fence is signaled or diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d8a6109735..ff27f8accb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4658,18 +4658,23 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); - if (!firstLayer && !depth1D && !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<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")"); + const bool firstSample = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DMsaa; + if (!firstLayer && !depth1D && !firstSample && !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<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr<Texture> texture; const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; - if (texture == nullptr && record != nullptr && record->texture != nullptr) { + if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && previousSampled.firstSample == firstSample && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; + if (texture == nullptr && !firstSample && record != nullptr && record->texture != nullptr) { texture = fastTexture(*record); (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } + if (texture == nullptr && firstSample) { + Require(!depthCompare && !DepthSurfaceAt(resource.baseAddress), "reading sample 0 of a multisampled depth surface through a 2D image is not implemented"); + texture = MultisampleTargetFirstSample(context, resource, components); + } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare, multisampleViewType); if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; @@ -4697,6 +4702,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind std::copy(words.begin(), words.end(), previousSampled.words.begin()); previousSampled.depthCompare = depthCompare; previousSampled.multisampleViewType = multisampleViewType; + previousSampled.firstSample = firstSample; previousSampled.texture = texture; } textures.push_back(std::move(texture)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 8a0b4d0ddc..21270e8d0e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7032,35 +7032,7 @@ public: const auto commands = start.commands; if (!continued) { recorder.PrepareSampleSlot(); - if (context.graphicsPipelineLibrary) { - if (!transitioned) { - transitioned = true; - VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - barrier.image = target.Image(); - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - } - VkRenderingAttachmentInfoKHR attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; - attachment.imageView = target.View(); - attachment.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; - rendering.renderArea = {{0, 0}, {8, 8}}; - rendering.layerCount = 1; - rendering.colorAttachmentCount = 1; - rendering.pColorAttachments = &attachment; - context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); - } else { - VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = renderPass; - begin.framebuffer = framebuffer; - begin.renderArea = {{0, 0}, {8, 8}}; - context.Function<PFN_vkCmdBeginRenderPass>("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); - } + begin(commands); } context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); recorder.NoteSampledDraw(commands); @@ -7069,10 +7041,48 @@ public: return continued; } + void Record(VkCommandBuffer commands) const { + begin(commands); + context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Function<PFN_vkCmdDraw>("vkCmdDraw")(commands, 3, 1, 0, 0); + EndRenderPass(context, commands); + } + static constexpr std::uint64_t Samples = 64; private: static constexpr std::uint64_t PassKey = 0xc0de; + void begin(VkCommandBuffer commands) const { + if (context.graphicsPipelineLibrary) { + if (!transitioned) { + transitioned = true; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.image = target.Image(); + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + VkRenderingAttachmentInfoKHR attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; + attachment.imageView = target.View(); + attachment.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; + rendering.renderArea = {{0, 0}, {8, 8}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); + } else { + VkRenderPassBeginInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + info.renderPass = renderPass; + info.framebuffer = framebuffer; + info.renderArea = {{0, 0}, {8, 8}}; + context.Function<PFN_vkCmdBeginRenderPass>("vkCmdBeginRenderPass")(commands, &info, VK_SUBPASS_CONTENTS_INLINE); + } + } void release() noexcept { const auto device = context.device; if (pipeline != VK_NULL_HANDLE) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(device, pipeline, nullptr); @@ -7420,6 +7430,24 @@ void sampleDumpTests(const Device& device, Recorder& recorder) { inPassSampleDumpTests(recorder, context, cover, words, address, target); } +void uncountedPassTests(const Device& device) { + auto context = device.GetContext(); + context.bufferDeviceAddress = false; + Recorder recorder(context); + CoverPass cover(context); + recorder.CountSamples(); + Require(!recorder.QueuesSampleDumps(), "a recorder without buffer device addresses counted samples in segments"); + const auto base = recorder.SamplesTotal(); + cover.Draw(recorder); + recorder.RecordUncounted([&](VkCommandBuffer commands) { cover.Record(commands); }); + cover.Draw(recorder); + recorder.Submit(); + recorder.Sync(); + const auto counted = recorder.SamplesTotal() - base; + Require(counted != 0, "the draws around an uncounted pass were not counted"); + if (context.occlusionQueryPrecise) Require(counted == 2 * CoverPass::Samples, "the batch-wide occlusion query counted " + std::to_string(counted) + " samples, not the " + std::to_string(2 * CoverPass::Samples) + " of the two draws around an uncounted pass"); +} + void cmaskPassTests(const Device& device, Recorder& recorder) { namespace GuestMemory = AgcDriver::GuestMemory; auto context = device.GetContext(); @@ -10405,6 +10433,15 @@ int main(int argc, char** argv) { std::cout << "Occlusion counter dump tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--uncounted-passes-only") { + if (!device.Graphics()) { + std::cout << "skipped, the test queue has no graphics\n"; + return 77; + } + uncountedPassTests(device); + std::cout << "Uncounted pass tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--shadowed-key-stores-only") { if (!shadowedKeyStoreTests(device, recorder)) { std::cout << "skipped, the device imports no watched host memory under a unit shadow\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 8d8b12086e..a96499a786 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7544,6 +7544,37 @@ void verifyVolumeArrayLoadMode() { expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); } +void verifyMultisamplePlaneReadMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x02754d00u, 0xc3800000u, 0x00cbc167u, 0xe1b20facu, 0u, 0x00700020u, 0u, 0u}, 8u}; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(modes[selected].dimension == RdnaImageDimension::Dim2D && !modes[selected].cube && modes[selected].numericClass == IrTextureNumericClass::Float, "a 2D read of a 2D MSAA T# (PPSA18216's) did not select a 2D runtime mode"); + for (const auto& [word1, word3, word5, numericClass] : std::array<std::tuple<std::uint32_t, std::uint32_t, std::uint32_t, IrTextureNumericClass>, 4>{{{0xc0100000u, 0xe1b20facu, 0x00700020u, IrTextureNumericClass::Float}, {0xc0500000u, 0xe1810facu, 0x00700010u, IrTextureNumericClass::Uint}, {0xc0c00000u, 0xe1b20facu, 0x00700020u, IrTextureNumericClass::Sint}, {0xc0d00000u, 0xe1830facu, 0x00700030u, IrTextureNumericClass::Float}}}) { + const DescriptorValue narrow{{0x02754d00u, word1, 0x00cbc167u, word3, 0u, word5, 0u, 0u}, 8u}; + const auto mode = ResourceMaterializer::RuntimeImageMode(image, narrow, modes); + require(modes[mode].dimension == RdnaImageDimension::Dim2D && !modes[mode].cube && modes[mode].numericClass == numericClass, "a 2D read of a 2D MSAA T# with 8-bit or 16-bit texels did not select a 2D runtime mode"); + } + const DescriptorValue depthBits{{0x02754d00u, 0xc0b00000u, 0x00cbc167u, 0xe1820facu, 0u, 0x00700020u, 0u, 0u}, 8u}; + expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(image, depthBits, modes)); }, "incompatible with the static runtime image interface", "a 2D read of a 16_UINT SW_64KB_Z_X 2D MSAA T#, read as depth bits, was accepted"); + const auto refused = [&](const ImageResource& access, const char* what) { + expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(access, descriptor, ResourceMaterializer::RuntimeImageModes(access))); }, "incompatible with the static runtime image interface", what); + }; + auto other = image; + other.cubeFaceCompatible = false; + refused(other, "a 2D read of a 2D MSAA T# through a mip, gather, bias, gradient or offset access was accepted"); + other = image; + other.depthCompare = true; + refused(other, "a 2D depth comparison of a 2D MSAA T# was accepted"); + other = image; + other.resourceClass = ImageResourceClass::Storage; + other.written = true; + refused(other, "a 2D storage access to a 2D MSAA T# was accepted"); +} + void verifyVolumeBiasTableRejection() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -7799,6 +7830,7 @@ int main(int argc, char** argv) { verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); verifyArrayVolumeStoreMode(); + verifyMultisamplePlaneReadMode(); verifyVolumeBiasTableRejection(); verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index abe81b5503..69392bd173 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -62,9 +62,13 @@ constexpr std::uint32_t Float16x4Info = 0x0730u; constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Uint16AbgrExport = 7; constexpr std::uint32_t Format16161616Float = 71; constexpr std::uint32_t Unorm8x4SwappedInfo = 0x8828u; +constexpr std::uint32_t Uint8x4Info = 0x8428u; constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t Format8888Uint = 60; +constexpr std::uint32_t Format8888Sint = 61; constexpr std::uint32_t Type2DMsaa = 14; constexpr std::uint32_t TileRenderTarget = 0x1b; constexpr std::uint32_t TileZ64KBX = 0x18; @@ -79,6 +83,7 @@ constexpr std::uint32_t FillOnes = 0x3c003c00u; constexpr std::array<float, 4> Ones{1.0f, 1.0f, 1.0f, 1.0f}; constexpr std::array<float, 4> Written{0.75f, 0.25f, 0.5f, 1.0f}; constexpr std::uint32_t StoredSample = 1; +constexpr std::array<std::uint32_t, 4> PointMipSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -91,6 +96,12 @@ constexpr std::array<float, 4> DrawnUnorm8{64.0f / 255.0f, 128.0f / 255.0f, 32.0 alignas(256) constexpr std::array<std::uint32_t, 11> Unorm8PixelCode{ 0x7e0802ff, std::bit_cast<std::uint32_t>(DrawnUnorm8[0]), 0x7e0a02ff, std::bit_cast<std::uint32_t>(DrawnUnorm8[1]), 0x7e0c02ff, std::bit_cast<std::uint32_t>(DrawnUnorm8[2]), 0x7e0e02ff, std::bit_cast<std::uint32_t>(DrawnUnorm8[3]), 0xf800180f, 0x07060504, 0xbf810000, }; +constexpr std::array<std::uint32_t, 4> DrawnUint8{0x12u, 0xbeu, 0x42u, 0x81u}; +alignas(256) constexpr std::array<std::uint32_t, 7> Uint8PixelCode{ + 0x7e0802ff, DrawnUint8[0] | (DrawnUint8[1] << 16u), 0x7e0a02ff, DrawnUint8[2] | (DrawnUint8[3] << 16u), 0xf8001c0f, 0x00000504, 0xbf810000, +}; +constexpr std::uint32_t FilledSint8 = 0x81beff12u; +constexpr std::array<std::uint32_t, 4> FilledSint8Texel{0x12u, 0xffffffffu, 0xffffffbeu, 0xffffff81u}; alignas(256) constexpr std::array<std::uint32_t, 26> LoadCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, @@ -106,6 +117,15 @@ alignas(256) constexpr std::array<std::uint32_t, 26> ArrayLoadCode{ 0x80001301, 0xbf810000, }; +alignas(256) constexpr std::array<std::uint32_t, 44> PlaneCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xbf8c3f70, 0xf0001f08, 0x00011005, 0xf09c0f08, 0x00611402, + 0xf0900f08, 0x00611802, 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xe0701040, 0x80001401, 0xe0701044, 0x80001501, 0xe0701048, + 0x80001601, 0xe070104c, 0x80001701, 0xe0701050, 0x80001801, 0xe0701054, 0x80001901, 0xe0701058, + 0x80001a01, 0xe070105c, 0x80001b01, 0xbf810000, +}; + alignas(256) constexpr std::array<std::uint32_t, 19> StoreCode{ 0x34020087, 0xe030100c, 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xe0301018, 0x80001001, 0xe030101c, 0x80001101, 0xe0301020, 0x80001201, 0xe0301024, 0x80001301, 0xbf8c3f70, @@ -210,6 +230,21 @@ AgcDriver::Graphics::ColorTarget DecodeSwappedTarget(const Block& block) { return color; } +AgcDriver::Graphics::ColorTarget DecodeUintTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Uint8x4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R8G8B8A8_UINT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the 8_8_8_8_UINT target did not decode as a 16x16 RGBA8 UINT surface of 4 samples"); + color.uintExport = true; + return color; +} + void CheckSwappedSamples(const std::vector<std::array<float, 4>>& loaded) { const std::array<float, 4> swapped{DrawnUnorm8[2], DrawnUnorm8[1], DrawnUnorm8[0], DrawnUnorm8[3]}; const auto codes = [](const std::array<float, 4>& texel) { @@ -229,6 +264,19 @@ void CheckSwappedSamples(const std::vector<std::array<float, 4>>& loaded) { Require(drawn != 0, "no sample of the BGRA8 target holds the drawn color"); } +void CheckUintSamples(const std::vector<std::array<float, 4>>& loaded) { + std::size_t drawn = 0; + for (const auto& texel : loaded) { + const auto words = std::bit_cast<std::array<std::uint32_t, 4>>(texel); + if (words == std::array<std::uint32_t, 4>{}) continue; + char text[128]; + std::snprintf(text, sizeof(text), "a sample of the 8_8_8_8_UINT target reads %08x %08x %08x %08x instead of the drawn color", words[0], words[1], words[2], words[3]); + Require(words == DrawnUint8, text); + ++drawn; + } + Require(drawn != 0, "no sample of the 8_8_8_8_UINT target holds the drawn color"); +} + AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; @@ -269,7 +317,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size()); const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, state.color.uintExport ? Uint16AbgrExport : Float32Export}}; const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, @@ -307,12 +355,13 @@ std::array<std::uint32_t, 8> TargetDescriptor(const Block& block, std::uint32_t }; } -ShaderRecompiler::RecompileResult CompileProgram(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::array<std::uint32_t, 8>& texture, std::vector<std::uint32_t>& userData) { +ShaderRecompiler::RecompileResult CompileProgram(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::array<std::uint32_t, 8>& texture, std::vector<std::uint32_t>& userData, const std::array<std::uint32_t, 4>& sampler = {}) { const auto address = reinterpret_cast<std::uintptr_t>(Buffer.data()); const std::array<std::uint32_t, 4> buffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), static_cast<std::uint32_t>(Buffer.size() * 4u), 0x31016facu}; userData.assign(16, 0u); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -417,6 +466,37 @@ std::vector<std::array<float, 4>> LoadSamples(AgcDriver::VulkanDevice& device, c return LoadSamples(device, TargetDescriptor(block, SampleShift, format, tileMode)); } +void CheckFirstSampleReads(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, const std::vector<std::array<float, 4>>& samples, bool distinct, const std::string& what) { + std::vector<std::uint32_t> userData; + const auto program = CompileProgram(device, PlaneCode, texture, userData, PointMipSampler); + const auto codeAddress = reinterpret_cast<std::uintptr_t>(PlaneCode.data()); + const auto bits = [&](std::size_t index) { return std::bit_cast<std::array<std::uint32_t, 4>>(samples[index]); }; + bool differs = false; + for (std::uint32_t first = 0; first < Side * Side; first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto pixel = first + tid; + const std::array<std::uint32_t, 5> words{std::bit_cast<std::uint32_t>((pixel % Side + 0.5f) / Side), std::bit_cast<std::uint32_t>((pixel / Side + 0.5f) / Side), std::bit_cast<std::uint32_t>(1.0f), pixel % Side, pixel / Side}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, codeAddress); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto pixel = first + tid; + const auto sampleZero = bits(pixel * Samples); + for (std::uint32_t sample = 1; sample < Samples; ++sample) differs = differs || bits(pixel * Samples + sample) != sampleZero; + for (std::uint32_t read = 0; read < 3u; ++read) { + std::array<std::uint32_t, 4> texel{}; + std::copy_n(&Buffer[tid * Words + LoadResult + read * 4u], texel.size(), texel.begin()); + char where[200]; + std::snprintf(where, sizeof(where), "%s of pixel (%u, %u) read %08x %08x %08x %08x, sample 0 is %08x %08x %08x %08x", read == 0u ? "a DIM 2D image_load" : read == 1u ? "a DIM 2D image_sample_lz" : "a DIM 2D image_sample_l at LOD 1", pixel % Side, pixel / Side, texel[0], texel[1], texel[2], texel[3], sampleZero[0], sampleZero[1], sampleZero[2], sampleZero[3]); + Require(texel == sampleZero, std::string(where) + " " + what); + } + } + } + Require(!distinct || differs, "no pixel's sample 0 differs from its other samples " + what); +} + std::array<std::uint32_t, 8> CompressedDescriptor(const Block& block, std::uint32_t tileMode, std::uint64_t htile) { auto texture = TargetDescriptor(block, SampleShift, Format16161616Float, tileMode); const auto meta = htile >> 8u; @@ -885,12 +965,46 @@ int main(int argc, char** argv) { Block other(BlockBytes); const auto color = DecodeTarget(block); const auto state = TargetState(color); + Draw(*device, state); + const auto drawn = LoadSamples(*device, block); + CheckSamples(state, drawn, {}); + CheckFirstSampleReads(*device, TargetDescriptor(block, SampleShift), drawn, true, "from the drawn 4x target"); + Draw(*device, state, 0, 2u, Unorm8PixelCode); + const auto redrawn = LoadSamples(*device, block); + Require(redrawn != drawn, "drawing the 4x target again with another color left its samples unchanged"); + CheckFirstSampleReads(*device, TargetDescriptor(block, SampleShift), redrawn, true, "from the 4x target drawn again with another color after a first read"); + Block zeroed(BlockBytes); + CheckFirstSampleReads(*device, TargetDescriptor(zeroed, SampleShift), std::vector<std::array<float, 4>>(Side * Side * Samples), false, "from zeroed color memory no draw rendered to"); + Block aliased(4u * BlockBytes); + Draw(*device, TargetState(DecodeSwappedTarget(aliased)), 0, 2u, Unorm8PixelCode); + Draw(*device, TargetState(DecodeTarget(aliased))); + const auto aliasedSamples = LoadSamples(*device, aliased); + CheckSamples(state, aliasedSamples, {}); + CheckFirstSampleReads(*device, TargetDescriptor(aliased, SampleShift), aliasedSamples, true, "from a 4x RGBA16F target drawn after a BGRA8 target at the same address"); + std::puts("DIM 2D image_load, image_sample_lz and image_sample_l reads of a 4x color target return each pixel's sample 0"); + Block swapped(4u * BlockBytes); + Draw(*device, TargetState(DecodeSwappedTarget(swapped)), 0, 2u, Unorm8PixelCode); + const auto swappedSamples = LoadSamples(*device, swapped, Format8888Unorm); + CheckSwappedSamples(swappedSamples); + CheckFirstSampleReads(*device, TargetDescriptor(swapped, SampleShift, Format8888Unorm), swappedSamples, true, "from a 4x BGRA8 target read through an RGBA8 descriptor"); + std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); + Block integer(4u * BlockBytes); + Block signedFill(BlockBytes); + if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).limits.sampledImageIntegerSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::puts("DIM 2D reads of 4x 8_8_8_8_UINT and 8_8_8_8_SINT targets not tested: sampledImageIntegerSampleCounts lacks VK_SAMPLE_COUNT_4_BIT"); + } else { + Draw(*device, TargetState(DecodeUintTarget(integer)), 0, 2u, Uint8PixelCode); + const auto integerSamples = LoadSamples(*device, integer, Format8888Uint); + CheckUintSamples(integerSamples); + CheckFirstSampleReads(*device, TargetDescriptor(integer, SampleShift, Format8888Uint), integerSamples, true, "from a drawn 4x 8_8_8_8_UINT target"); + StoreWords(signedFill, BlockBytes, FilledSint8); + CheckFirstSampleReads(*device, TargetDescriptor(signedFill, SampleShift, Format8888Sint), std::vector<std::array<float, 4>>(Side * Side * Samples, std::bit_cast<std::array<float, 4>>(FilledSint8Texel)), false, "from 8_8_8_8_SINT color memory no draw rendered to"); + std::puts("DIM 2D reads of a drawn 4x 8_8_8_8_UINT target and of 8_8_8_8_SINT color memory no draw rendered to return each pixel's sample 0 bit for bit"); + } if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { - std::puts("multisampled target sampling not tested: storage image multisampling unavailable"); + std::puts("multisampled target fills, stores and swizzle modes not tested: storage image multisampling unavailable"); return VulkanTestSkipped; } - Draw(*device, state); - CheckSamples(state, LoadSamples(*device, block), {}); const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast<std::size_t>(colorBytes), FillOnes); @@ -953,10 +1067,6 @@ int main(int argc, char** argv) { StoreWords(standard, colorBytes, FillOnes); RequireRefused(*device, TargetDescriptor(standard, SampleShift, Format16161616Float, TileS64KBX), "has no modeled multisampled color footprint", "loading an S swizzle multisampled texture no draw rendered to"); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); - Block swapped(4u * BlockBytes); - Draw(*device, TargetState(DecodeSwappedTarget(swapped)), 0, 2u, Unorm8PixelCode); - CheckSwappedSamples(LoadSamples(*device, swapped, Format8888Unorm)); - std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); Block destination(BlockBytes); struct ReleaseIndexDevice { std::unique_ptr<AgcDriver::VulkanDevice>& device; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 45b3a7ea1e..10c4dd5100 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -128,6 +128,10 @@ std::uint32_t imageCoordinates(RdnaImageDimension dimension) { } } +bool multisamplePlaneRead(const ImageResource& image) { + return image.dimension == RdnaImageDimension::Dim2D && image.resourceClass == ImageResourceClass::Sampled && image.table == NoTable && !image.depthCompare && !image.r128 && image.cubeFaceCompatible; +} + bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { const auto type = rawImageType(descriptor); const auto format = rawImageFormat(descriptor); @@ -214,6 +218,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); + if (decoded.dimension == RdnaImageDimension::Dim2DMsaa && rawImageType(descriptor) == ImageType::Color2DMsaa && multisamplePlaneRead(base) && !IsFmaskTextureFormat(format) && !IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) decoded.dimension = RdnaImageDimension::Dim2D; if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { return rejectImage(PoisonReason::InvalidFormat, "64-bit atomic image descriptor uses an unsupported format " + std::to_string(static_cast<std::uint32_t>(format))); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d795f5d53f..81d74bddf9 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,6 +163,7 @@ - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. +- [Sample 0 of a multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp) (libSceAgcDriver) - a 2D image read of a multisampled color target returns its float, UNORM, SNORM or sRGB sample 0 exactly only where the driver keeps those values through a shader and a color attachment, which Vulkan does not require, and one of a multisampled depth surface throws. The read binds a one-sample copy that a fragment pass (`texelFetch` of sample 0) draws before every such binding; integer formats copy exactly. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2b6ace99db..69f49a712c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -47,6 +47,7 @@ - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. +- A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. From 1f1ddc9e3a011d14ee04b6a4fdc5c2cdb4e4619f Mon Sep 17 00:00:00 2001 From: Arnab Ghosh <arnab.ghosh@g.bracu.ac.bd> Date: Fri, 23 Oct 2026 10:43:29 +0600 Subject: [PATCH 1157/1360] feat(libSceNgs2): implement sceNgs2VoiceGetMatrixInfo and sceNgs2CustomRackGetModuleInfo --- .../libSceNgs2.native/include/Ngs2Types.hpp | 18 ++++++ .../libs/prx/libSceNgs2.native/src/Custom.cpp | 3 + .../libSceNgs2.native/src/Ngs2Internal.hpp | 1 + core/libs/prx/libSceNgs2.native/src/Rack.cpp | 11 ++++ core/libs/prx/libSceNgs2.native/src/Voice.cpp | 14 +++++ .../libSceNgs2.native/tests/Ngs2Custom.cpp | 57 +++++++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 2 + .../tests/Ngs2VoiceHandles.cpp | 49 ++++++++++++++++ core/libs/prx/libSceNgs2/PendingExports.cpp | 8 --- docs/dev/debt/libSceNgs2.md | 2 +- docs/dev/debt/libSceNgs2.native.md | 2 + 11 files changed, 158 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index f2d40adab9..78533b44db 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -293,6 +293,18 @@ struct Ngs2CustomRackModuleInfo { }; static_assert(sizeof(Ngs2CustomRackModuleInfo) == 40); +struct Ngs2CustomModuleInfo { + 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(Ngs2CustomModuleInfo) == 32); + struct Ngs2CustomRackPortInfo { std::uint32_t source_buffer_id; std::uint32_t reserved; @@ -635,6 +647,12 @@ struct Ngs2VoicePortInfo { }; static_assert(sizeof(Ngs2VoicePortInfo) == 24); +struct Ngs2VoiceMatrixInfo { + std::uint32_t num_levels; + float levels[64]; +}; +static_assert(sizeof(Ngs2VoiceMatrixInfo) == 260); + struct Ngs2VoiceState { std::uint32_t state_flags; std::int32_t error_code; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index aaad9fdb18..de39824e53 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -91,6 +91,7 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { rack.customModuleIds.resize(option.num_modules); + rack.customModules.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); for (auto& voice : rack.voices) { @@ -100,6 +101,8 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { } for (std::uint32_t m = 0; m < option.num_modules; m++) { rack.customModuleIds[m] = option.module[m].module_id; + const auto& info = option.module[m]; + rack.customModules[m] = {info.module_id, info.source_buffer_id, info.extra_buffer_id, info.dest_buffer_id, info.state_offset, info.state_size, 0, 0}; if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue; if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { const auto& module = *reinterpret_cast<const Ngs2CustomUserFx2ModuleOption*>(option.module[m].option); diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index a0e886d230..ac5ec0321a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -172,6 +172,7 @@ struct Ngs2Rack { std::vector<Ngs2CustomUserFx2ModuleOption> userFx; std::vector<std::vector<std::uint8_t>> userFxCommon; std::vector<std::uint32_t> customModuleIds; + std::vector<Ngs2CustomModuleInfo> customModules; std::thread::id lockOwner{}; std::uint32_t lockDepth = 0; }; diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index f5c85495eb..21d0cb17ba 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -264,6 +264,17 @@ int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size return SCE_NGS2_OK; } +int APS5_VABI sceNgs2CustomRackGetModuleInfo(uintptr_t rack_handle, uint32_t module_index, Ngs2CustomModuleInfo* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + const Ngs2Lock lock; + const auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (module_index >= rack->customModules.size() || info_size < sizeof(Ngs2CustomModuleInfo)) APS5_INVALID_ARG_EX; + if (info_size != sizeof(Ngs2CustomModuleInfo)) throw std::runtime_error("NGS2: custom module info of " + std::to_string(info_size) + " bytes is not implemented"); + *info = rack->customModules[module_index]; + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2RackSetUserData(uintptr_t rack_handle, uintptr_t user_data) { std::lock_guard lock(Ngs2Mutex()); auto* rack = Ngs2FindRack(rack_handle); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index bc19ab0a0a..8f9c3fabc4 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -526,6 +526,20 @@ int APS5_VABI sceNgs2VoiceGetOwner(uintptr_t voice_handle, uintptr_t* owner_rack return SCE_NGS2_OK; } +int APS5_VABI sceNgs2VoiceGetMatrixInfo(uintptr_t voice_handle, uint32_t matrix_id, Ngs2VoiceMatrixInfo* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (info_size != sizeof(Ngs2VoiceMatrixInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + const Ngs2Lock lock; + const auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + if (matrix_id >= found->matrices.size()) APS5_INVALID_ARG_EX; + const auto& levels = found->matrices[matrix_id]; + *info = {}; + info->num_levels = static_cast<std::uint32_t>(levels.size()); + std::copy(levels.begin(), levels.end(), info->levels); + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { return Ngs2PanInit(work, speaker_angles, unit_angle, num_speakers); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 5b6acd7ad2..a4f8a347b3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -663,6 +663,62 @@ static void TestCustomGain() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +template <typename TException> +static bool ModuleInfoThrows(uintptr_t rack, std::uint32_t moduleIndex, std::size_t infoSize) { + Ngs2CustomModuleInfo info; + std::memset(&info, 0xa5, sizeof(info)); + bool threw = false; + try { + sceNgs2CustomRackGetModuleInfo(rack, moduleIndex, &info, infoSize); + } catch (const TException&) { + threw = true; + } + const auto* bytes = reinterpret_cast<const unsigned char*>(&info); + for (std::size_t i = 0; i < sizeof(info); i++) Require(bytes[i] == 0xa5); + return threw; +} + +static void TestCustomModuleInfo() { + const auto system = CreateSystem(); + const Ngs2CustomUserFx2ModuleOption userFx2{{sizeof(Ngs2CustomUserFx2ModuleOption)}, nullptr, nullptr, nullptr, Gain, sizeof(float), sizeof(float), 8, 0x77}; + const Ngs2CustomUserFx2ModuleOption modules[2] = {userFx2, userFx2}; + auto option = RackOption(modules, 2); + auto& first = option.custom_rack_option.module[0]; + first.state_offset = 0x40; + first.reserved = 0x11; + first.reserved2 = 0x22; + auto& filter = option.custom_rack_option.module[1]; + filter.option = nullptr; + filter.module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + filter.state_size = 0; + const auto rack = CustomRack(system, option); + + Ngs2CustomModuleInfo info; + std::memset(&info, 0xa5, sizeof(info)); + Require(sceNgs2CustomRackGetModuleInfo(rack, 0, &info, sizeof(info)) == SCE_NGS2_OK); + Require(info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 && info.source_buffer_id == 0 && info.extra_buffer_id == 0 && info.dest_buffer_id == 0); + Require(info.state_offset == 0x40 && info.state_size == 16 && info.reserved == 0 && info.reserved2 == 0); + std::memset(&info, 0xa5, sizeof(info)); + Require(sceNgs2CustomRackGetModuleInfo(rack, 1, &info, sizeof(info)) == SCE_NGS2_OK); + Require(info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER && info.state_offset == 0 && info.state_size == 0 && info.reserved == 0 && info.reserved2 == 0); + + Require(sceNgs2CustomRackGetModuleInfo(rack, 0, nullptr, sizeof(info)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2CustomRackGetModuleInfo(0x1234, 0, nullptr, sizeof(info)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + std::memset(&info, 0xa5, sizeof(info)); + Require(sceNgs2CustomRackGetModuleInfo(0x1234, 0, &info, sizeof(info)) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2CustomRackGetModuleInfo(system, 0, &info, sizeof(info)) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + const auto* bytes = reinterpret_cast<const unsigned char*>(&info); + for (std::size_t i = 0; i < sizeof(info); i++) Require(bytes[i] == 0xa5); + Require(ModuleInfoThrows<std::invalid_argument>(rack, 2, sizeof(Ngs2CustomModuleInfo))); + Require(ModuleInfoThrows<std::invalid_argument>(rack, 0, sizeof(Ngs2CustomModuleInfo) - 1)); + Require(ModuleInfoThrows<std::runtime_error>(rack, 0, sizeof(Ngs2CustomModuleInfo) + 8)); + Require(ModuleInfoThrows<std::invalid_argument>(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER), 0, sizeof(Ngs2CustomModuleInfo))); + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2CustomRackGetModuleInfo(rack, 0, &info, sizeof(info)) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestUserFxChain(); TestRackOptions(); @@ -671,5 +727,6 @@ int main() { TestCustomFilter(); TestCustomLegacyUserFx(); TestCustomGain(); + TestCustomModuleInfo(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 4768cc7cb3..e1c5907df4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -44,6 +44,8 @@ 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 sceNgs2VoiceGetOwner(uintptr_t, uintptr_t*, uint32_t*); +int APS5_VABI sceNgs2VoiceGetMatrixInfo(uintptr_t, uint32_t, Ngs2VoiceMatrixInfo*, size_t); +int APS5_VABI sceNgs2CustomRackGetModuleInfo(uintptr_t, uint32_t, Ngs2CustomModuleInfo*, size_t); int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp index c5fed18835..043e02c96d 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp @@ -35,6 +35,54 @@ static void Invalid(std::string_view api, uintptr_t handle) { Require(std::all_of(output.begin(), output.end(), [](auto byte) { return byte == 0xa5; })); } +template <typename TException> +static bool MatrixInfoThrows(uintptr_t voice, std::uint32_t matrixId) { + Ngs2VoiceMatrixInfo info; + std::memset(&info, 0xa5, sizeof(info)); + bool threw = false; + try { + sceNgs2VoiceGetMatrixInfo(voice, matrixId, &info, sizeof(info)); + } catch (const TException&) { + threw = true; + } + const auto* bytes = reinterpret_cast<const unsigned char*>(&info); + Require(std::all_of(bytes, bytes + sizeof(info), [](auto byte) { return byte == 0xa5; })); + return threw; +} + +static void MatrixInfo() { + Ngs2VoiceMatrixInfo info; + std::memset(&info, 0xa5, sizeof(info)); + Status(sceNgs2VoiceGetMatrixInfo(0x1234, 0, &info, sizeof(info)), InvalidVoice, "GetMatrixInfo accepted an invalid voice"); + const auto* bytes = reinterpret_cast<const unsigned char*>(&info); + Require(std::all_of(bytes, bytes + sizeof(info), [](auto byte) { return byte == 0xa5; })); + Status(sceNgs2VoiceGetMatrixInfo(0x1234, 0, nullptr, sizeof(info)), SCE_NGS2_ERROR_INVALID_OUT_ADDRESS, "GetMatrixInfo checked the voice before null output"); + Status(sceNgs2VoiceGetMatrixInfo(0x1234, 0, &info, sizeof(info) - 1), SCE_NGS2_ERROR_INVALID_OUT_SIZE, "GetMatrixInfo checked the voice before the output size"); + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = Voice(rack); + Status(sceNgs2VoiceGetMatrixInfo(voice, 0, &info, sizeof(info)), SCE_NGS2_OK, "GetMatrixInfo failed for a matrix without levels"); + Require(info.num_levels == 0 && std::all_of(std::begin(info.levels), std::end(info.levels), [](float level) { return level == 0.0f; })); + const float levels[4] = {1.0f, 0.5f, -5.0f, 6.0f}; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 4, levels}); + std::memset(&info, 0xa5, sizeof(info)); + Status(sceNgs2VoiceGetMatrixInfo(voice, 0, &info, sizeof(info)), SCE_NGS2_OK, "GetMatrixInfo failed for a matrix with levels"); + Require(info.num_levels == 4 && info.levels[0] == 1.0f && info.levels[1] == 0.5f && info.levels[2] == -4.0f && info.levels[3] == 4.0f); + Require(std::all_of(std::begin(info.levels) + 4, std::end(info.levels), [](float level) { return level == 0.0f; })); + std::array<float, 64> full{}; + for (std::size_t i = 0; i < full.size(); i++) full[i] = static_cast<float>(i) / 64.0f; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 64, full.data()}); + Status(sceNgs2VoiceGetMatrixInfo(voice, 0, &info, sizeof(info)), SCE_NGS2_OK, "GetMatrixInfo failed for 64 levels"); + Require(info.num_levels == 64 && std::equal(full.begin(), full.end(), info.levels)); + Status(sceNgs2VoiceGetMatrixInfo(voice, 0, &info, sizeof(info) + 1), SCE_NGS2_ERROR_INVALID_OUT_SIZE, "GetMatrixInfo accepted a larger output"); + Require(MatrixInfoThrows<std::invalid_argument>(voice, 1)); + const auto master = Voice(CreateRack(system, SCE_NGS2_RACK_ID_MASTERING)); + Require(MatrixInfoThrows<std::invalid_argument>(master, 0)); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Status(sceNgs2VoiceGetMatrixInfo(voice, 0, &info, sizeof(info)), InvalidVoice, "GetMatrixInfo accepted a voice of a destroyed rack"); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main(int argc, char** argv) { try { if (argc > 2 || (argc == 2 && std::find(Names.begin(), Names.end(), std::string_view(argv[1])) == Names.end())) throw std::runtime_error("unknown voice API test selection"); @@ -80,6 +128,7 @@ int main(int argc, char** argv) { Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); for (auto name : Names) if (selected(name)) Invalid(name, voice); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + if (argc == 1) MatrixInfo(); std::cout << "NGS2 invalid voice errors, outputs and precedence checked\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceNgs2/PendingExports.cpp b/core/libs/prx/libSceNgs2/PendingExports.cpp index d930fb00c1..ec4b3cad95 100644 --- a/core/libs/prx/libSceNgs2/PendingExports.cpp +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -5,10 +5,6 @@ extern "C" { -int APS5_VABI sceNgs2CustomRackGetModuleInfo(std::uintptr_t rackHandle, std::uint32_t moduleIndex, void* outInfo, std::size_t infoSize) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNgs2FftInit() { NotImplemented_nid_no_patch(__func__); return 0; @@ -33,9 +29,5 @@ int APS5_VABI sceNgs2SystemEnumRackHandles(std::uintptr_t systemHandle, std::uin NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNgs2VoiceGetMatrixInfo(std::uintptr_t voiceHandle, std::uint32_t matrixId, void* outInfo, std::size_t infoSize) { - NotImplemented_nid_no_patch(__func__); - return 0; -} } diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md index a3bd2a6776..f25b3c941d 100644 --- a/docs/dev/debt/libSceNgs2.md +++ b/docs/dev/debt/libSceNgs2.md @@ -3,4 +3,4 @@ ### Unknown function info - [sceNgs2FftInit, sceNgs2FftProcess, sceNgs2FftQuerySize](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. -- [sceNgs2CustomRackGetModuleInfo, sceNgs2ParseWaveformUser, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. +- [sceNgs2ParseWaveformUser, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3071c0931d..b42e0d9a34 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -33,6 +33,8 @@ - [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 - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other +- [sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a matrix id past the rack's `max_matrices` throws, as the PS5 code for shadPS4's PS4 [`INVALID_MATRIX_INDEX`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L92) is unknown. A matrix without levels reports 0 levels, though it mixes through the default channel map. Signature from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L442), layout (a count and 64 levels) from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.h#L263-L266); levels are the stored, clamped ones. Unmeasured: a null info answers `INVALID_OUT_ADDRESS` and a size other than 260 `INVALID_OUT_SIZE` before the voice is checked, as in `sceNgs2VoiceGetPortInfo` +- [sceNgs2CustomRackGetModuleInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - a size over 32 bytes throws, as the longer per-module info in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_custom.h#L419-L442) is not kept. A size under 32 and an index past the rack's modules (any on a non-custom rack) also throw: the PS5 codes for shadPS4's PS4 [`INVALID_MODULE_INDEX` and `INVALID_MODULE_INFO_SIZE`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L117-L118) are unknown. It repeats the ids, state offset and size from rack creation, reserved words 0, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L323-L341) lays it out. Unmeasured: a null info gives `INVALID_OUT_ADDRESS`, then a bad handle `INVALID_RACK_HANDLE` - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown From e256e6d2d53d30ff343a05483b3a778abf1f4443 Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 <slopggobblercobbler81@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:10:41 +0200 Subject: [PATCH 1158/1360] test(recompiler): gate Windows separator test cases to Windows platform --- core/shader/recompiler/tests/test_shader_cache_native_paths.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/shader/recompiler/tests/test_shader_cache_native_paths.py b/core/shader/recompiler/tests/test_shader_cache_native_paths.py index 1c1ad63ae2..fe6d0f0922 100644 --- a/core/shader/recompiler/tests/test_shader_cache_native_paths.py +++ b/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -74,6 +74,7 @@ class ShaderCachePathTests(unittest.TestCase): self.assertIn('does not hash', result.stdout + result.stderr) self.assertFalse(output.exists()) + @unittest.skipUnless(sys.platform == 'win32', 'Windows path separator semantics require Windows host') def test_windows_path_separators(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) / 'windows separators' @@ -88,6 +89,7 @@ class ShaderCachePathTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(windows_version, output.read_bytes()) + @unittest.skipUnless(sys.platform == 'win32', 'Windows path separator semantics require Windows host') def test_windows_path_separators_reject_unhashed_include(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) / 'windows separators' From f73a4e81335cf26258881c2a46b4fb676abf2b11 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Wed, 21 Oct 2026 15:18:19 +0200 Subject: [PATCH 1159/1360] feat(libkernel_sys): implement the scePthread scope, atfork and setconcurrency forms --- core/libs/CMakeLists.txt | 7 +- .../prx/libkernel_sys/Export.Discovery.cpp | 24 +++---- core/libs/tests/GuestKernelSysPthread.cpp | 66 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 4 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysPthread.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 16cc1b019b..ac235edc51 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -321,12 +321,17 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_sys_barrier_tests) add_test(NAME guest_kernel_sys_barrier COMMAND guest_kernel_sys_barrier_tests) set_tests_properties(guest_kernel_sys_barrier PROPERTIES TIMEOUT 20) + add_test_executable(guest_kernel_sys_pthread_tests tests/GuestKernelSysPthread.cpp) + target_include_directories(guest_kernel_sys_pthread_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_pthread_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_pthread_tests) + add_test(NAME guest_kernel_sys_pthread COMMAND guest_kernel_sys_pthread_tests) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>) endif() if(WIN32) - set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_barrier PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_barrier guest_kernel_sys_pthread PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") endif() if(APPLE) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index c07286436a..4fd2ea6eaa 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -14,6 +14,10 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(void* const* attr, int* int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(void** attr, int pshared); int APS5_VABI pthread_getcpuclockid_nid_postfix(void* thread, int* clockId); int APS5_VABI pthread_getconcurrency_nid_postfix(); +int APS5_VABI pthread_setconcurrency_nid_postfix(int level); +int APS5_VABI pthread_attr_getscope_nid_postfix(void* const* attr, int* scope); +int APS5_VABI pthread_attr_setscope_nid_postfix(void** attr, int scope); +int APS5_VABI pthread_atfork_nid_postfix(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)()); int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags); } @@ -2306,14 +2310,12 @@ int APS5_VABI sceLibcMspaceCreateForMonoMutex(void) { return 0; } -int APS5_VABI scePthreadAtfork(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAtfork(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)()) { + return SceResult(pthread_atfork_nid_postfix(prepare, parent, child)); } -int APS5_VABI scePthreadAttrGetscope(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAttrGetscope(void* const* attr, int* scope) { + return SceResult(pthread_attr_getscope_nid_postfix(attr, scope)); } int APS5_VABI scePthreadAttrSetcreatesuspend(void) { @@ -2321,9 +2323,8 @@ int APS5_VABI scePthreadAttrSetcreatesuspend(void) { return 0; } -int APS5_VABI scePthreadAttrSetscope(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAttrSetscope(void** attr, int scope) { + return SceResult(pthread_attr_setscope_nid_postfix(attr, scope)); } int APS5_VABI scePthreadBarrierDestroy(void** barrier) { @@ -2439,9 +2440,8 @@ int APS5_VABI scePthreadSetBesteffort(void) { return 0; } -int APS5_VABI scePthreadSetconcurrency(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadSetconcurrency(int level) { + return SceResult(pthread_setconcurrency_nid_postfix(level)); } int APS5_VABI scePthreadSingle(void) { diff --git a/core/libs/tests/GuestKernelSysPthread.cpp b/core/libs/tests/GuestKernelSysPthread.cpp new file mode 100644 index 0000000000..9d352aa66c --- /dev/null +++ b/core/libs/tests/GuestKernelSysPthread.cpp @@ -0,0 +1,66 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Atfork.hpp" +#include <cstdlib> + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope); +int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope); +int APS5_VABI scePthreadAtfork(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child); +int APS5_VABI scePthreadSetconcurrency(int level); +int APS5_VABI scePthreadGetconcurrency(); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016); +static constexpr int SCOPE_PROCESS = 0; +static constexpr int SCOPE_SYSTEM = 2; + +static void Require(bool value) { if (!value) std::abort(); } + +static void APS5_VABI Prepare() {} +static void APS5_VABI Parent() {} +static void APS5_VABI Child() {} + +static void TestScope() { + PthreadAttr attr{}; + Require(scePthreadAttrInit(&attr) == 0); + int scope = -1; + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrSetscope(&attr, SCOPE_PROCESS) == 0); + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + for (const int invalid : {1, 3, -1}) { + Require(scePthreadAttrSetscope(&attr, invalid) == SCE_KERNEL_ERROR_EINVAL); + scope = -1; + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + } + Require(scePthreadAttrSetscope(&attr, SCOPE_SYSTEM) == 0); + Require(scePthreadAttrGetscope(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrGetscope(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetscope(nullptr, &scope) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetscope(nullptr, SCOPE_PROCESS) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrDestroy(&attr) == 0); +} + +static void TestConcurrency() { + Require(scePthreadSetconcurrency(3) == 0 && scePthreadGetconcurrency() == 3); + Require(scePthreadSetconcurrency(-1) == SCE_KERNEL_ERROR_EINVAL && scePthreadGetconcurrency() == 3); + Require(scePthreadSetconcurrency(0) == 0 && scePthreadGetconcurrency() == 0); +} + +static void TestAtfork() { + const auto before = PthreadAtforkHandlers_nid_no_patch().size(); + Require(scePthreadAtfork(Prepare, Parent, Child) == 0); + Require(scePthreadAtfork(nullptr, nullptr, nullptr) == 0); + const auto handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == before + 2); + Require(handlers[before].prepare == Prepare && handlers[before].parent == Parent && handlers[before].child == Child); + Require(handlers[before + 1].prepare == nullptr && handlers[before + 1].parent == nullptr && handlers[before + 1].child == nullptr); +} + +int main() { + TestScope(); + TestConcurrency(); + TestAtfork(); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 4c333c4b78..af2ddf76fd 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -23,3 +23,4 @@ ### Unknown function info - [scePthreadBarrier*, scePthreadBarrierattr*, scePthreadGetcpuclockid, scePthreadGetconcurrency and sceKernelMsync](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_barrier*`, `pthread_getcpuclockid`, `pthread_getconcurrency` and `msync` and turn a POSIX error into `0x80020000 | errno`; that the console's `sce` forms do the same is not measured. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); `scePthreadBarrierInit` ignores its name, and `scePthreadBarrierWait` returns -1 to the serial thread, both as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/barrier.cpp) (PS4). `scePthreadGetconcurrency` returns the level unchanged +- [scePthreadAttrGetscope, scePthreadAttrSetscope, scePthreadAtfork and scePthreadSetconcurrency](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_attr_getscope`, `pthread_attr_setscope`, `pthread_atfork` and `pthread_setconcurrency` and turn a POSIX error into `0x80020000 | errno`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/kernel/threads/pthread_attr.cpp#L392) exports `scePthreadAttrGetscope` as `ORBIS(posix_pthread_attr_getscope)`; shadPS4 has no export for the other three, whose names and NIDs are in the [symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and whose signatures follow the same POSIX forms. Not measured on a console. As in libkernel, the scope is 0 (process) or 2 (system), and `scePthreadAtfork` handlers are only recorded, as no process forks From 6660dd080dec6c3f9106d0dfe28fbd099e1e85f7 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Fri, 23 Oct 2026 07:18:26 +0200 Subject: [PATCH 1160/1360] fix(libSceNgs2.native): throw for ATRAC9 loops inside the decoder delay and blocks past the data chunk --- core/libs/prx/libSceNgs2.native/src/Atrac9.cpp | 7 +++++-- core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp | 12 +++++++++++- docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 171e79004c..4fedf5e215 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -306,10 +306,12 @@ static int ReadChunks(std::size_t size, TRead&& read, RiffChunks& chunks) { } static void AddAtrac9Block(Ngs2WaveformInfo& info, std::uint32_t begin, std::uint32_t samples, std::uint32_t repeats) { - auto& block = info.block[info.num_blocks++]; - block = Atrac9Block(info.format.config_data, begin, samples); + auto block = Atrac9Block(info.format.config_data, begin, samples); + if (block.data_offset > info.data_size || block.data_size > info.data_size - block.data_offset) + throw std::runtime_error("NGS2: an ATRAC9 loop block reaching past the data chunk is not implemented"); block.data_offset += info.data_offset; block.num_repeats = repeats; + info.block[info.num_blocks++] = block; } static int ParseAtrac9Loop(const RiffChunks& chunks, Ngs2WaveformInfo& info) { @@ -317,6 +319,7 @@ static int ParseAtrac9Loop(const RiffChunks& chunks, Ngs2WaveformInfo& info) { if (chunks.samplerSize < 32) throw std::runtime_error("NGS2: a smpl chunk too short for its loop count is not implemented"); if (ReadLe32(chunks.sampler + 28) == 0) return SCE_NGS2_OK; if (chunks.samplerSize < 52) throw std::runtime_error("NGS2: a smpl chunk too short for its first loop is not implemented"); + if (ReadLe32(chunks.sampler + 44) < info.num_delay_samples) throw std::runtime_error("NGS2: an ATRAC9 loop starting inside the decoder delay is not implemented"); const std::uint32_t begin = ReadLe32(chunks.sampler + 44) - info.num_delay_samples; const std::uint32_t end = ReadLe32(chunks.sampler + 48) - info.num_delay_samples + 1u; info.loop_begin_position = begin; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 39736292e8..6b22d603bc 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -385,7 +385,7 @@ static void TestMalformedLoops() { Require(info.loop_begin_position == 100 && info.loop_end_position == 700 && info.num_blocks == 3); for (std::uint32_t block = 0; block < 3; ++block) Require(std::memcmp(&info.block[block], &expected.block[block], sizeof(Ngs2WaveformBlock)) == 0); } - for (const auto [start, last, begin, end] : {std::tuple<std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t>{0, 955, 0xffffff00u, 700}, {356, 255, 100, 0}, {956, 955, 700, 700}, {956, 700, 700, 445}}) { + for (const auto [start, last, begin, end] : {std::tuple<std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t>{256, 255, 0, 0}, {356, 255, 100, 0}, {956, 955, 700, 700}, {956, 700, 700, 445}}) { auto file = original; Set32(file, 144, start); Set32(file, 148, last); @@ -399,7 +399,17 @@ static void TestMalformedLoops() { Set32(file, 148, last); Require(ParseThrows(file)); } + for (const std::uint32_t start : {0u, 255u}) { + auto file = original; + Set32(file, 144, start); + Require(ParseThrows(file)); + } Ngs2WaveformInfo info{}; + auto pastData = original; + Set32(pastData, 80, 4 * SuperframeSamples - 255); + Require(ParseThrows(pastData)); + Set32(pastData, 80, 4 * SuperframeSamples - 256); + Require(sceNgs2ParseWaveformData(pastData.data(), pastData.size(), &info) == SCE_NGS2_OK && info.num_blocks == 3); for (std::size_t size = 92; size < 168; ++size) Require(sceNgs2ParseWaveformData(original.data(), size, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); for (const std::uint32_t size : {28u, 36u, 50u}) Require(ParseThrows(ShortSampler(original, size))); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index fd60051d79..634ee66a1e 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -7,7 +7,7 @@ - [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. -- [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - a loop ending past the last sample (the module's tail count wraps), a `smpl` chunk too short for its count or first loop, and a looped file at a rate the module has no frame size for throw; unmeasured on a console. As [prosper reads the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L249-L257), only the first loop's start and inclusive end are read, less the one-frame decoder delay; type, fraction, play count and other loops are ignored. As in [its block list](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L297-L346), an end not after the start keeps one block, else an intro, the loop repeating forever and a tail. PPSA03747's chunk (start 256, inclusive end 299224, 298969 samples) gives one block repeating forever +- [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - unmeasured on a console. A loop starting inside the decoder delay or ending past the last sample, a loop block past the `data` chunk, a `smpl` chunk too short for its first loop and a looped file at a rate with no module frame size throw: the module wraps or does not bound these, and what it then decodes is unknown. As [prosper reads the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L249-L346), only the first loop's start and inclusive end are used, less the one-frame decoder delay, and type, fraction, play count and other loops are ignored; an end not after the start keeps one block, else an intro, the loop repeating forever and a tail. PPSA03747 (start 256, inclusive end 299224, 298969 samples) gets one block repeating forever - [NGS2 ATRAC9 parsed delay](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reports one codec frame as `num_delay_samples` whatever the `fact` chunk's delay word holds, as [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) sets it for ATRAC9 (its [test](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/tests/hle/test_ngs2_waveform.cpp#L326-L342) expects 256 for a `fact` of 900 and 0). Not measured on a console. The block skip still comes from the `fact` chunk's third word, where prosper uses the same one-frame delay - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. From f648ad61e28a92283189024e85e1bd67a0847b66 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 10:41:05 +1300 Subject: [PATCH 1161/1360] fix(libSceConvertKeycode): map the user's keyboard type to the IME keyboard type --- .../prx/libSceConvertKeycode/CMakeLists.txt | 3 +- core/libs/prx/libSceConvertKeycode/Export.cpp | 20 ++- .../libSceConvertKeycode/ImeKeyboardType.hpp | 66 ++++++++++ core/libs/prx/libSceUserService/Export.cpp | 26 ++++ core/libs/tests/GuestConvertKeycode.cpp | 114 ++++++++++++++++-- core/libs/tests/GuestUserService.cpp | 12 ++ docs/dev/debt/libSceConvertKeycode.md | 2 +- docs/dev/debt/libSceUserService.md | 1 + 8 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp diff --git a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt index d4f82eb5f7..d04045b5c6 100644 --- a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt +++ b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceConvertKeycode SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceConvertKeycode PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceConvertKeycode PRIVATE libSceUserService) configure_windows_unwind(libSceConvertKeycode) if(BUILD_TESTING) add_test_executable(guest_convert_keycode_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycode.cpp) target_include_directories(guest_convert_keycode_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libc) + target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libSceUserService libc) configure_windows_unwind(guest_convert_keycode_tests) add_test(NAME guest_convert_keycode COMMAND guest_convert_keycode_tests) if(WIN32) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index a38cb0293c..24e61537bb 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,12 +1,21 @@ #include <array> +#include <cstddef> #include <cstdint> +#include <cstring> #include <stdexcept> #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceConvertKeycode/ImeKeyboardType.hpp" + +extern "C" { +int APS5_VABI sceUserServiceGetKeyboardType(int32_t userId, int32_t* type); +int APS5_VABI sceUserServiceGetJapaneseInputType(int32_t userId, int32_t* type); +int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int32_t userId, int32_t* type); +} namespace { constexpr int kErrInvalidAddress = static_cast<int>(0x80BC0031u); -constexpr int kErrInvalidUserId = static_cast<int>(0x80BC0010u); constexpr std::array<std::uint16_t, 256> BaseVirtualKeys = { 255, 255, 255, 255, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 49, 50, @@ -2398,9 +2407,12 @@ char16_t ConvertCharacter(std::uint16_t keycode, std::uint32_t state, std::uint3 extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(int32_t userId, uint32_t* type) { - if (type == nullptr) return kErrInvalidAddress; - if (userId < 0) return kErrInvalidUserId; - throw std::runtime_error("sceConvertKeycodeGetImeKeyboardType: user keyboard language and variant are unavailable"); + // The PS4/PS5 client rule: AnyPS5 only runs PS5 processes (sceKernelIsPs4Process is unimplemented and reports 0). + constexpr bool ps4Client = false; + const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 16, sizeof(sdkVersion)); + return ImeKeyboardType::Resolve(userId, type, sdkVersion, ps4Client, sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType); } int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uint32_t keyboardType, std::uint16_t* virtualKeycode) { if (virtualKeycode == nullptr) return kErrInvalidAddress; diff --git a/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp b/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp new file mode 100644 index 0000000000..fcf9ccbe16 --- /dev/null +++ b/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp @@ -0,0 +1,66 @@ +#ifndef CORE_LIBS_PRX_LIBSCECONVERTKEYCODE_IMEKEYBOARDTYPE_HPP +#define CORE_LIBS_PRX_LIBSCECONVERTKEYCODE_IMEKEYBOARDTYPE_HPP + +#include <cstdint> + +// sceConvertKeycodeGetImeKeyboardType: the UserService keyboard type (plus the Japanese and Traditional Chinese +// input variants) to IME keyboard type mapping and the SDK compatibility rules, as specified by the repository +// owner (see docs/dev/debt/libSceConvertKeycode.md). Header-only so the guest test drives every row and error path +// with fake UserService getters. +namespace ImeKeyboardType { + +constexpr int kErrInvalidAddress = static_cast<int>(0x80BC0031u); +constexpr int kErrInvalidUserId = static_cast<int>(0x80BC0010u); +constexpr int kErrGeneric = static_cast<int>(0x80BC00FFu); +constexpr int kUserServiceUnknownUser = static_cast<int>(0x80960105u); + +constexpr std::int32_t kJapaneseKeyboard = 37; +constexpr std::int32_t kTraditionalChineseKeyboard = 39; + +// Index = UserService keyboard type - 16. Types 37 and 39 are resolved through their variants; 0 = no mapping. +constexpr std::int32_t kFirstMappedType = 16; +constexpr std::uint32_t kImeTypeByKeyboardType[] = { + 1, 2, 3, 5, 4, 6, 7, 8, 10, 11, 9, 12, 13, 14, 15, 16, 17, 18, 19, 20, // 16..35 + 21, 0, 24, 0, 25, 30, 31, 33, 32, 34, 35, 36, 37, 38, // 36..49 (37 and 39 are variants) +}; +constexpr std::int32_t kMappedTypeCount = static_cast<std::int32_t>(sizeof(kImeTypeByKeyboardType) / sizeof(kImeTypeByKeyboardType[0])); + +constexpr std::uint32_t LookUp(std::int32_t keyboardType, std::int32_t japaneseVariant, std::int32_t traditionalChineseVariant) { + if (keyboardType == kJapaneseKeyboard) { + constexpr std::uint32_t japanese[] = {22, 23}; + return japaneseVariant >= 0 && japaneseVariant < 2 ? japanese[japaneseVariant] : 0; + } + if (keyboardType == kTraditionalChineseKeyboard) { + constexpr std::uint32_t traditionalChinese[] = {26, 27, 28, 29, 39}; + return traditionalChineseVariant >= 0 && traditionalChineseVariant < 5 ? traditionalChinese[traditionalChineseVariant] : 0; + } + const std::int32_t index = keyboardType - kFirstMappedType; + return index >= 0 && index < kMappedTypeCount ? kImeTypeByKeyboardType[index] : 0; +} + +// Applied after the lookup. 38 -> 4 below PS5 SDK 0x06000000 / PS4 SDK 0x10000000; 39 -> 29 below PS5 SDK 0x08000000 +// and for every PS4 client. +constexpr std::uint32_t ApplySdkCompatibility(std::uint32_t imeType, std::uint32_t sdkVersion, bool ps4Client) { + if (imeType == 38 && sdkVersion < (ps4Client ? 0x10000000u : 0x06000000u)) return 4; + if (imeType == 39 && (ps4Client || sdkVersion < 0x08000000u)) return 29; + return imeType; +} + +template <typename GetKeyboardType, typename GetJapaneseInputType, typename GetTraditionalChineseInputType> +int Resolve(std::int32_t userId, std::uint32_t* type, std::uint32_t sdkVersion, bool ps4Client, GetKeyboardType getKeyboardType, GetJapaneseInputType getJapaneseInputType, GetTraditionalChineseInputType getTraditionalChineseInputType) { + if (type == nullptr) return kErrInvalidAddress; + std::int32_t keyboardType = 0; + const int result = getKeyboardType(userId, &keyboardType); + if (result == kUserServiceUnknownUser) return kErrInvalidUserId; + if (result != 0) return kErrGeneric; + std::int32_t japaneseVariant = 0; + std::int32_t traditionalChineseVariant = 0; + if (keyboardType == kJapaneseKeyboard && getJapaneseInputType(userId, &japaneseVariant) != 0) japaneseVariant = 0; + if (keyboardType == kTraditionalChineseKeyboard && getTraditionalChineseInputType(userId, &traditionalChineseVariant) != 0) traditionalChineseVariant = 0; + *type = ApplySdkCompatibility(LookUp(keyboardType, japaneseVariant, traditionalChineseVariant), sdkVersion, ps4Client); + return 0; +} + +} // namespace ImeKeyboardType + +#endif diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index a12d5c3263..25c97875cf 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -20,6 +20,20 @@ static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t val return USER_SERVICE_OK; } + +// Per-user keyboard settings. Nothing in AnyPS5 stores a keyboard language or input variant yet, so every known user +// reports 0 (see docs/dev/debt/libSceUserService.md). +static int ReportKeyboardSetting(int user_id, int32_t* type) { + if (type == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_UNKNOWN_USER; + } + *type = 0; + return USER_SERVICE_OK; +} + extern "C" { int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32_t* chat_transcription) { @@ -218,6 +232,18 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) { return USER_SERVICE_OK; } +int APS5_VABI sceUserServiceGetKeyboardType(int user_id, int32_t* type) { + return ReportKeyboardSetting(user_id, type); +} + +int APS5_VABI sceUserServiceGetJapaneseInputType(int user_id, int32_t* type) { + return ReportKeyboardSetting(user_id, type); +} + +int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, int32_t* type) { + return ReportKeyboardSetting(user_id, type); +} + 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; diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index cce08ee498..d10852c231 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -1,9 +1,15 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" +#include "prx/libSceConvertKeycode/ImeKeyboardType.hpp" +#include <array> +#include <cstddef> +#include <cstring> #include <cstdint> #include <cstdlib> #include <initializer_list> #include <stdexcept> +#include <utility> extern "C" { int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t, std::uint32_t, std::uint16_t*); @@ -13,7 +19,103 @@ int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::u static void Require(bool value) { if (!value) std::abort(); } +namespace { + +struct FakeUser { + int keyboardResult = 0; + std::int32_t keyboardType = 0; + int japaneseResult = 0; + std::int32_t japaneseVariant = 0; + int chineseResult = 0; + std::int32_t chineseVariant = 0; + int calls = 0; +}; + +int Resolve(const FakeUser& user, std::uint32_t* type, std::uint32_t sdkVersion = 0xFFFFFFFFu, bool ps4Client = false) { + return ImeKeyboardType::Resolve( + 7, type, sdkVersion, ps4Client, + [&](std::int32_t id, std::int32_t* value) { Require(id == 7); *value = user.keyboardType; return user.keyboardResult; }, + [&](std::int32_t id, std::int32_t* value) { Require(id == 7); *value = user.japaneseVariant; return user.japaneseResult; }, + [&](std::int32_t id, std::int32_t* value) { Require(id == 7); *value = user.chineseVariant; return user.chineseResult; }); +} + +std::uint32_t Map(std::int32_t keyboardType, std::int32_t japanese = 0, std::int32_t chinese = 0, std::uint32_t sdkVersion = 0xFFFFFFFFu, bool ps4Client = false) { + FakeUser user; + user.keyboardType = keyboardType; + user.japaneseVariant = japanese; + user.chineseVariant = chinese; + std::uint32_t type = 0xFFFFFFFFu; + Require(Resolve(user, &type, sdkVersion, ps4Client) == 0); + return type; +} + +void RequireImeKeyboardTypeMapping() { + // The owner's table: UserService keyboard type -> IME keyboard type. + constexpr std::pair<int, int> table[] = { + {16, 1}, {17, 2}, {18, 3}, {19, 5}, {20, 4}, {21, 6}, {22, 7}, {23, 8}, {24, 10}, {25, 11}, {26, 9}, {27, 12}, + {28, 13}, {29, 14}, {30, 15}, {31, 16}, {32, 17}, {33, 18}, {34, 19}, {35, 20}, {36, 21}, {38, 24}, {40, 25}, + {41, 30}, {42, 31}, {43, 33}, {44, 32}, {45, 34}, {46, 35}, {47, 36}, {48, 37}, {49, 38}}; + for (const auto& [keyboard, ime] : table) { + // 49 -> 38 is compatibility-sensitive; the table row is checked on a new SDK, the rule separately below. + Require(Map(keyboard) == static_cast<std::uint32_t>(ime)); + } + // Unmatched keyboard types succeed with IME type 0 (including 0, the gaps, and out-of-range values). + for (int keyboard : {-1, 0, 1, 15, 50, 51, 1000, 0x7FFFFFFF, static_cast<int>(0x80000000u)}) Require(Map(keyboard) == 0); + + // Type 37: Japanese input variant; anything else is unmatched. + Require(Map(37, 0) == 22 && Map(37, 1) == 23); + Require(Map(37, 2) == 0 && Map(37, -1) == 0); + Require(Map(37, 0, 4) == 22); // the Traditional Chinese variant is ignored for 37 + // Type 39: Traditional Chinese input variant. + Require(Map(39, 0, 0) == 26 && Map(39, 0, 1) == 27 && Map(39, 0, 2) == 28 && Map(39, 0, 3) == 29 && Map(39, 0, 4) == 39); + Require(Map(39, 0, 5) == 0 && Map(39, 0, -1) == 0); + Require(Map(39, 1, 0) == 26); // the Japanese variant is ignored for 39 + + // Variant query failure falls back to variant 0 and still succeeds; other types never query the variants. + FakeUser user; + std::uint32_t type = 0xFFFFFFFFu; + user.keyboardType = 37; user.japaneseResult = static_cast<int>(0x80960001u); user.japaneseVariant = 1; + Require(Resolve(user, &type) == 0 && type == 22); + user = {}; + type = 0xFFFFFFFFu; + user.keyboardType = 39; user.chineseResult = static_cast<int>(0x80960001u); user.chineseVariant = 4; + Require(Resolve(user, &type) == 0 && type == 26); + user = {}; + type = 0xFFFFFFFFu; + user.keyboardType = 20; user.japaneseResult = 1; user.chineseResult = 1; + Require(Resolve(user, &type) == 0 && type == 4); + + // SDK compatibility, applied after the lookup. IME 38 (from 49): PS5 SDK < 0x06000000, PS4 SDK < 0x10000000 -> 4. + Require(Map(49, 0, 0, 0x05FFFFFFu, false) == 4); + Require(Map(49, 0, 0, 0x06000000u, false) == 38); + Require(Map(49, 0, 0, 0x0FFFFFFFu, true) == 4); + Require(Map(49, 0, 0, 0x10000000u, true) == 38); + // IME 39 (from 39 variant 4): PS5 SDK < 0x08000000 and every PS4 client -> 29. + Require(Map(39, 0, 4, 0x07FFFFFFu, false) == 29); + Require(Map(39, 0, 4, 0x08000000u, false) == 39); + Require(Map(39, 0, 4, 0xFFFFFFFFu, true) == 29); + // Other IME types are never rewritten. + Require(Map(20, 0, 0, 0, false) == 4 && Map(36, 0, 0, 0, true) == 21 && Map(39, 0, 3, 0, true) == 29); + + // Errors leave the output untouched. + type = 0x1234u; + user = {}; + Require(ImeKeyboardType::Resolve(7, nullptr, 0, false, [](std::int32_t, std::int32_t*) { Require(false); return 0; }, [](std::int32_t, std::int32_t*) { return 0; }, [](std::int32_t, std::int32_t*) { return 0; }) == static_cast<int>(0x80BC0031u)); + user.keyboardResult = static_cast<int>(0x80960105u); + Require(Resolve(user, &type) == static_cast<int>(0x80BC0010u) && type == 0x1234u); + for (int other : {1, -1, static_cast<int>(0x80960002u), static_cast<int>(0x80960005u), static_cast<int>(0x80BC0010u)}) { + user.keyboardResult = other; + Require(Resolve(user, &type) == static_cast<int>(0x80BC00FFu) && type == 0x1234u); + } +} + +} // namespace + int main() { + static std::array<std::byte, 64> process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + constexpr std::uint32_t sdkVersion = 0x09000000u; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); constexpr int invalidAddress = static_cast<int>(0x80BC0031u); constexpr int invalidUserId = static_cast<int>(0x80BC0010u); @@ -41,18 +143,14 @@ int main() { Require(rejected && virtualKey == 0xABCD); } + // The exported function with the real UserService: unknown users fail, the initial user has no keyboard setting yet (0). std::uint32_t type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); + Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == invalidUserId); Require(type == 0xFFFFFFFFu); + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 0); - bool rejected = false; - try { - sceConvertKeycodeGetImeKeyboardType(0, &type); - } catch (const std::runtime_error&) { - rejected = true; - } - Require(rejected && type == 0xFFFFFFFFu); - + RequireImeKeyboardTypeMapping(); Require(sceConvertKeycodeGetCharacter(4, 0, 0, nullptr) == invalidAddress); char16_t character = u'?'; Require(sceConvertKeycodeGetCharacter(4, 0, 0, &character) == static_cast<int>(0x80BC001Du)); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 3e6d7559da..7ccd08f4af 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -10,6 +10,9 @@ int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* valu 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 sceUserServiceGetKeyboardType(int user_id, std::int32_t* type); +int APS5_VABI sceUserServiceGetJapaneseInputType(int user_id, std::int32_t* type); +int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, std::int32_t* type); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value); @@ -57,6 +60,15 @@ int main() { 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); + + using KeyboardSetting = int (*)(int, std::int32_t*); + for (KeyboardSetting setting : {sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType}) { + std::int32_t value = 0x5A5A; + Require(setting(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(setting(123, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A); + Require(setting(USER_SERVICE_USER_ID_INVALID, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A); + Require(setting(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == 0); + } Require(color == 0); std::uint64_t account_id = 0xdeadbeefu; diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 5c01a3cabc..4b2be47cdd 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -3,4 +3,4 @@ ### Functional - [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. -- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): user keyboard language and Japanese/Chinese keyboard variants are not exposed by the current UserService implementation. Valid-user queries throw instead of returning a fabricated keyboard type. The confirmed language/variant mapping and SDK compatibility rules still need integration with those settings. +- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule assumes a PS5 process because `sceKernelIsPs4Process` is unimplemented and AnyPS5 runs PS5 processes. UserService has no stored keyboard setting yet, so every user reports keyboard type 0 and the function returns IME type 0 until one is stored (see libSceUserService.md). diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index e320add914..551d6a0049 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,3 +8,4 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports 0 for all three and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The repository owner allows initialising the keyboard type from the system language through an explicit conversion to UserService keyboard-type ids; that table is not recovered yet, so no value is guessed. `sceConvertKeycodeGetImeKeyboardType` consumes these values. From 0f3d4bc9ee4847b9b704d31d9c8c783982538d19 Mon Sep 17 00:00:00 2001 From: Retr0 <gurau.eduard@ichb.ro> Date: Wed, 21 Oct 2026 15:27:26 +0300 Subject: [PATCH 1162/1360] fix(libc): fail with ENOTDIR for a file named with a trailing slash on Windows FreeBSD 11.4's lookup() (sys/kern/vfs_lookup.c) fails with ENOTDIR when a path with a trailing slash ends at something that is not a directory. On Windows a mount alias dropped the slash, and the host calls do not report ENOTDIR for "file\", so unlink, rename and open acted on the file or failed with EINVAL. Guest path resolution now rejects an existing non-directory named with a trailing slash with ENOTDIR on every host, and a mount alias keeps the slash on Windows as it does on Linux and macOS. --- core/libs/prx/libc/src/General.cpp | 17 +++++++++++++---- core/libs/tests/GuestFilesystem.cpp | 12 ++++++++++-- docs/dev/debt/libc.md | 1 - docs/dev/debt/libkernel.md | 1 + 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 26c1935c5a..9e4f93d42b 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -255,13 +255,11 @@ std::optional<std::filesystem::path> ResolveAlias(const std::string& guestPath, const auto result = std::filesystem::path(host).make_preferred(); return module ? PathOrRelinkedModule(result) : result; } -#ifndef _WIN32 if (guestPath.back() == '/') { - auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); + auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))).make_preferred(); result /= ""; return module ? PathOrRelinkedModule(result) : result; } -#endif return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; @@ -318,6 +316,14 @@ void RequireHostDirectory(WorkingDirectory& state, const std::vector<std::string #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } +void RejectNonDirectory(const std::filesystem::path& host) { + auto target = host; + while (!target.has_filename() && target.has_relative_path()) target = target.parent_path(); + std::error_code error; + const auto status = std::filesystem::status(target, error); + if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) + throw std::filesystem::filesystem_error("Guest path with a trailing slash is not a directory", host, std::make_error_code(std::errc::not_a_directory)); +} std::vector<std::string> WithTrailingSeparator(std::vector<std::string> names, bool trailing) { if (trailing && !names.empty()) names.emplace_back(); return names; @@ -360,7 +366,10 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool mo if (!trailing) names.push_back(name); } if (endsWithDot && !names.empty() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, names); - return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); + const bool directory = trailing && !names.empty(); + auto resolved = HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); + if (directory) RejectNonDirectory(resolved); + return resolved; } #ifdef _WIN32 using WriteMark = FILETIME; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index d759f1051d..72f247a695 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -283,19 +283,27 @@ static void TestParentTraversal() { Require(rmdir_nid_postfix("/parent-test-mount/missing/..") == -1 && *__error_nid_postfix() == 2); Require(std::filesystem::is_directory(root / "dot-victim") && std::filesystem::is_regular_file(root / "present.txt")); std::filesystem::remove(root / "dot-victim"); -#ifndef _WIN32 for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); Require(remove_nid_postfix("/parent-test-mount/slash-remove/") == -1 && *__error_nid_postfix() == 20); Require(rename_nid_postfix("/parent-test-mount/slash-rename/", "/parent-test-mount/slash-renamed") == -1 && *__error_nid_postfix() == 20); Require(open_nid_postfix("/parent-test-mount/slash-open/", 0, 0) == -1 && *__error_nid_postfix() == 20); Require(sceKernelUnlink("/parent-test-mount/slash-kernel/") == static_cast<int>(0x80020014u)); + Require(rename_nid_postfix("/parent-test-mount/slash-remove", "/parent-test-mount/slash-open/") == -1 && *__error_nid_postfix() == 20); + Require(stat_nid_postfix("/parent-test-mount/slash-open/", &status) == -1 && *__error_nid_postfix() == 20); + Require(access_nid_postfix("/parent-test-mount/slash-open/", 0) == -1 && *__error_nid_postfix() == 20); + Require(chmod_nid_postfix("/parent-test-mount/slash-open/", 0644) == -1 && *__error_nid_postfix() == 20); + Require(mkdir_nid_postfix("/parent-test-mount/slash-open/", 0755) == -1 && *__error_nid_postfix() == 20); + Require(rmdir_nid_postfix("/parent-test-mount/slash-open/") == -1 && *__error_nid_postfix() == 20); + Require(sceKernelOpen("/parent-test-mount/slash-open/", 0x601, 0600) == static_cast<int>(0x80020014u)); + Require(unlink_nid_postfix((root.generic_string() + "/slash-kernel/").c_str()) == -1 && *__error_nid_postfix() == 20); for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) Require(std::filesystem::is_regular_file(root / name)); Require(!std::filesystem::exists(root / "slash-renamed")); Require(sceKernelStat("/parent-test-mount/Existing/", &status) == 0 && (status.st_mode & 0170000) == 0040000); + const int listed = sceKernelOpen("/parent-test-mount/Existing/", 0x20000, 0); + Require(listed >= 0 && sceKernelClose(listed) == 0); Require(mkdir_nid_postfix("/parent-test-mount/slash-directory/", 0755) == 0 && std::filesystem::is_directory(root / "slash-directory")); Require(rmdir_nid_postfix("/parent-test-mount/slash-directory/") == 0 && !std::filesystem::exists(root / "slash-directory")); -#endif RemovePathAlias_nid_no_patch("parent-test-mount"); std::error_code linkError; std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..a6a47913b0 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -122,7 +122,6 @@ - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `<name>.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `<name>.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/<name>.prx` and `Media/Plugins/<name>.prx`, and the relinker names the converted module after the ELF it was built from, `<name>.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known -- [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d4f5cfd338..25c18e783c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -221,6 +221,7 @@ - [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console +- [Creating through a trailing slash](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `open` and `sceKernelOpen` with `O_CREAT` on a missing name followed by a slash (`new/`) fail with `EINVAL` on Windows and with the host's `EISDIR` on Linux, where FreeBSD 11.4's `lookup()` (sys/kern/vfs_lookup.c) fails with `ENOENT` because only a directory may be created through a trailing slash - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests From 767e188339af8f956d7f42ecb43ff86b4a7e3ec1 Mon Sep 17 00:00:00 2001 From: Federico Marras <f.marras92@studenti.unica.it> Date: Fri, 23 Oct 2026 07:21:17 +0200 Subject: [PATCH 1163/1360] test(libSceNgs2.native): expect the module's ATRAC9 block in the frame info test --- core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp | 7 ++----- docs/dev/debt/libSceNgs2.native.md | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 1edb905892..d3e7dffe07 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -230,17 +230,14 @@ static void TestFrameInfo() { }; for (const auto& format : supported) { Ngs2WaveformBlock block{}; - Require(sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes); + Require(sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block) == SCE_NGS2_OK && block.data_size == 2 * SuperframeBytes); std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_OK); Require(values[0] == SuperframeBytes && values[1] == SuperframeSamples && values[2] == 4 && values[3] == SuperframeSamples / 4); } for (const auto& format : rejected) { - Ngs2WaveformBlock block{}; - const int calc = sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block); - Require(calc == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; - Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == calc); + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); } std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 4ef0c6e970..56a27af9e7 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -28,7 +28,7 @@ - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before -- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console; shadPS4 only stubs it, and prosper does not cover the call. The outputs are taken to be the frame fields of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes and samples, the frames per superframe and a one-frame delay, whatever the `fact` chunk says. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad format `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified +- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console; shadPS4 only stubs it, and prosper does not cover the call. The outputs are taken to be the frame fields of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes and samples, the frames per superframe and a one-frame delay, whatever the `fact` chunk says. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and an ATRAC9 format the codec does not accept `INVALID_WAVEFORM_FORMAT`; the codes are unverified - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [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 From 5dbfbc9382a4248859c448be4adb0ecbdac78f6e Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:44:04 +0100 Subject: [PATCH 1164/1360] feat(libSceIpmi): declare 49 PS5 pending exports --- core/libs/prx/libSceIpmi/CMakeLists.txt | 3 + core/libs/prx/libSceIpmi/PendingExports.cpp | 205 ++++++++++++++++++++ docs/dev/debt/libSceIpmi.md | 2 + 3 files changed, 210 insertions(+) create mode 100644 core/libs/prx/libSceIpmi/CMakeLists.txt create mode 100644 core/libs/prx/libSceIpmi/PendingExports.cpp diff --git a/core/libs/prx/libSceIpmi/CMakeLists.txt b/core/libs/prx/libSceIpmi/CMakeLists.txt new file mode 100644 index 0000000000..f549ae49f1 --- /dev/null +++ b/core/libs/prx/libSceIpmi/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(libSceIpmi SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp) +target_include_directories(libSceIpmi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceIpmi) diff --git a/core/libs/prx/libSceIpmi/PendingExports.cpp b/core/libs/prx/libSceIpmi/PendingExports.cpp new file mode 100644 index 0000000000..62a504bc35 --- /dev/null +++ b/core/libs/prx/libSceIpmi/PendingExports.cpp @@ -0,0 +1,205 @@ +#include <cstddef> +#include <cstdint> +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI _ZN4IPMI4impl11SessionImplC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config14MsgQueueConfig20estimateMsgQueueSizeEmj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandler21onAsyncMethodDispatchEPNS_7SessionEjjPvmS4_m(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config26SyncMethodInvocationConfigC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandler20onSyncMethodDispatchEPNS_7SessionEjPvmmS4_m(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI7SessionD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl9tryGetMsgEjPvPmm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandler20onSyncMethodDispatchEPNS_7SessionEjPKNS_8DataInfoEjPNS_10BufferInfoEj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config14MsgQueueConfigC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ServerD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ServerD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config24estimateClientMemorySizeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server6createEPPS0_PKNS0_6ConfigEPvS6_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6ConfigC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandler15onSessionKilledEPNS_7SessionE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImplD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl27respondToAsyncMethodRequestEjiPKvm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config27AsyncMethodInvocationConfigC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandler21onAsyncMethodDispatchEPNS_7SessionEjjPKNS_8DataInfoEj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ClientD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl7sendMsgEjPKvmPj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ClientC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI7SessionD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZNK4IPMI4impl11SessionImpl16isPeerPrivilegedEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI7SessionC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImplD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ClientD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandlerD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI7Session6Config25estimateSessionMemorySizeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZNK4IPMI6Server6Config29estimateTempWorkingMemorySizeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImplD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl7destroyEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config27AsyncMethodInvocationConfigC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl6getMsgEjPvPmmPj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl27respondToAsyncMethodRequestEjjiPKNS_8DataInfoEj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandlerD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ClientD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6createEPPS0_PKNS0_6ConfigEPvS6_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ServerC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImplC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI7SessionD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6ServerD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server6ConfigC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server12EventHandlerD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI4impl11SessionImpl9getServerEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6ConfigC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config26SyncMethodInvocationConfigC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Client6Config14MsgQueueConfigC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI _ZN4IPMI6Server6ConfigC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/docs/dev/debt/libSceIpmi.md b/docs/dev/debt/libSceIpmi.md index 09625b0ec9..e1d0461397 100644 --- a/docs/dev/debt/libSceIpmi.md +++ b/docs/dev/debt/libSceIpmi.md @@ -2,4 +2,6 @@ ### Unknown function info +- [libSceIpmi pending exports](../../../core/libs/prx/libSceIpmi/PendingExports.cpp) - 49 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIpmi.c). Their signatures and behavior are unverified; the no-parameter declarations exist for import resolution and throw through `NotImplemented_nid_no_patch`. + - [libSceIpmi pending exports](../../../core/libs/prx/libSceIpmi/Export.cpp) - 5 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIpmi.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From b13deef0db01704fd485bbf6e261f18641b2dbbd Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:19:46 +0200 Subject: [PATCH 1165/1360] feat(libSceFreeType): implement FT_Get_Gasp --- core/libs/prx/libSceFreeType/Export.cpp | 7 +++ core/libs/tests/FreeTypeFontFixture.hpp | 15 ++++- core/libs/tests/GuestFreeType.cpp | 74 +++++++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 9eaae33cae..825bc9b1bb 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -3,6 +3,8 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" +#include FT_GASP_H + #include <algorithm> #include <bit> #include <cstdint> @@ -186,6 +188,11 @@ int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec* face, std::uint32_t glyph_ return error; } +int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec* face, std::uint32_t ppem) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Gasp(owner ? owner->host : nullptr, ppem); +} + int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec* face, std::uint64_t charCode, std::int32_t loadFlags) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Load_Char(owner ? owner->host : nullptr, static_cast<FT_ULong>(static_cast<std::uint32_t>(charCode)), loadFlags); diff --git a/core/libs/tests/FreeTypeFontFixture.hpp b/core/libs/tests/FreeTypeFontFixture.hpp index 9458d09a81..b876a19139 100644 --- a/core/libs/tests/FreeTypeFontFixture.hpp +++ b/core/libs/tests/FreeTypeFontFixture.hpp @@ -20,7 +20,8 @@ static void PutAll16(std::vector<unsigned char>& out, std::initializer_list<int> for (const int value : values) Put16(out, static_cast<std::uint32_t>(static_cast<std::uint16_t>(value))); } -static std::vector<unsigned char> SquareGlyphFont(std::uint32_t codepoint, int width, bool symbolMap = false) { +static std::vector<unsigned char> SquareGlyphFont(std::uint32_t codepoint, int width, bool symbolMap = false, + const std::vector<unsigned char>* gasp = nullptr) { std::vector<unsigned char> head; Put32(head, 0x00010000u); Put32(head, 0x00010000u); @@ -53,11 +54,19 @@ static std::vector<unsigned char> SquareGlyphFont(std::uint32_t codepoint, int w const int symbol = static_cast<int>(codepoint + 0xF000); PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, symbol, 0xFFFF, 0, symbol, 0xFFFF, 1 - symbol, 1, 0, 0}); } - const std::vector<std::pair<const char*, const std::vector<unsigned char>*>> tables = { + std::vector<std::pair<const char*, const std::vector<unsigned char>*>> tables = { {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + if (gasp) tables.insert(tables.begin() + 1, {"gasp", gasp}); + unsigned power = 1; + unsigned selector = 0; + while (power <= tables.size() / 2) { + power *= 2; + ++selector; + } std::vector<unsigned char> font; Put32(font, 0x00010000u); - PutAll16(font, {static_cast<int>(tables.size()), 64, 2, static_cast<int>(tables.size()) * 16 - 64}); + PutAll16(font, {static_cast<int>(tables.size()), static_cast<int>(power * 16), static_cast<int>(selector), + static_cast<int>(tables.size() * 16 - power * 16)}); std::uint32_t offset = 12 + static_cast<std::uint32_t>(tables.size()) * 16; for (const auto& [tag, data] : tables) { font.insert(font.end(), tag, tag + 4); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index bccc9a51c2..a68e64258f 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -4,6 +4,7 @@ #include <ft2build.h> #include FT_FREETYPE_H +#include FT_GASP_H #include FT_OUTLINE_H #include <algorithm> @@ -28,6 +29,7 @@ extern "C" { void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec*, const GuestMatrix*, const GuestVector*); int APS5_VABI FT_Init_FreeType_nid_postfix(void**); int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec*, std::uint32_t); int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); @@ -47,6 +49,77 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static void CheckGaspTable(const std::vector<unsigned char>* table, const std::vector<std::pair<unsigned, int>>& ranges, + bool versionZero, bool render) { + const auto font = SquareGlyphFont('A', 700, false, table); + void* library = nullptr; + GuestFaceRec* face = nullptr; + FT_Library nativeLibrary = nullptr; + FT_Face native = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face(nativeLibrary, font.data(), static_cast<FT_Long>(font.size()), 0, &native) == 0); + auto* slot = face->glyph; + const auto glyphIndex = slot->glyph_index; + const auto bitmap = slot->bitmap.buffer; + const auto points = slot->outline.points; + for (std::uint32_t ppem = 0; ppem <= 65536; ++ppem) { + int expected = FT_GASP_NO_TABLE; + for (const auto& [maximum, flags] : ranges) { + if (ppem > maximum) continue; + expected = versionZero ? flags & 3 : flags; + break; + } + Require(FT_Get_Gasp_nid_postfix(face, ppem) == expected); + Require(FT_Get_Gasp(native, ppem) == expected); + } + Require(FT_Get_Gasp_nid_postfix(face, UINT32_MAX) == FT_GASP_NO_TABLE); + Require(face->glyph == slot && slot->glyph_index == glyphIndex && slot->bitmap.buffer == bitmap && slot->outline.points == points); + if (render) { + for (const unsigned ppem : {7u, 8u, 15u, 16u, 31u, 32u, 64u}) { + const int flags = FT_Get_Gasp_nid_postfix(face, ppem); + const int load = flags & FT_GASP_DO_GRIDFIT ? FT_LOAD_DEFAULT : FT_LOAD_NO_HINTING; + const auto mode = flags & FT_GASP_DO_GRAY ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO; + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, ppem) == 0 && FT_Set_Pixel_Sizes(native, 0, ppem) == 0); + Require(FT_Load_Char_nid_postfix(face, 'A', load) == 0 && FT_Load_Char(native, 'A', load) == 0); + Require(FT_Render_Glyph_nid_postfix(face->glyph, mode) == 0 && FT_Render_Glyph(native->glyph, mode) == 0); + const auto& actual = face->glyph->bitmap; + const auto& expected = native->glyph->bitmap; + Require(actual.width > 0 && actual.rows > 0); + Require(actual.width == expected.width && actual.rows == expected.rows && actual.pitch == expected.pitch); + Require(actual.pixel_mode == expected.pixel_mode && actual.num_grays == expected.num_grays); + Require(face->glyph->bitmap_left == native->glyph->bitmap_left && face->glyph->bitmap_top == native->glyph->bitmap_top); + Require(std::memcmp(actual.buffer, expected.buffer, static_cast<std::size_t>(actual.rows) * std::abs(actual.pitch)) == 0); + } + } + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} + +static void CheckGasp() { + Require(FT_Get_Gasp_nid_postfix(nullptr, 0) == FT_GASP_NO_TABLE); + Require(FT_Get_Gasp_nid_postfix(nullptr, UINT32_MAX) == FT_GASP_NO_TABLE); + CheckGaspTable(nullptr, {}, false, false); + const std::vector<std::pair<unsigned, int>> ranges{{0, 0x8003}, {7, 2}, {15, 1}, {31, 0}, {65535, 0xFFFF}}; + for (const unsigned version : {0u, 1u}) { + std::vector<unsigned char> table; + Put16(table, version); + Put16(table, ranges.size()); + for (const auto& [maximum, flags] : ranges) { + Put16(table, maximum); + Put16(table, flags); + } + CheckGaspTable(&table, ranges, version == 0, true); + } + std::vector<unsigned char> shortRange; + PutAll16(shortRange, {1, 1, 48, FT_GASP_DO_GRAY}); + CheckGaspTable(&shortRange, {{48, FT_GASP_DO_GRAY}}, false, false); + for (const std::vector<unsigned char>& table : {std::vector<unsigned char>{}, {0, 1}, {0, 1, 0, 0}, {0, 2, 0, 1, 0, 10, 0, 3}}) { + CheckGaspTable(&table, {}, false, false); + } + std::puts("FreeType gasp: 8 fonts, 524304 queries, null faces and 14 rendered glyphs passed"); +} + template <typename TCall> static bool ThrowsWith(TCall call, const std::string& prefix) { try { call(); @@ -361,6 +434,7 @@ static void CheckTransforms(const std::vector<unsigned char>& font) { int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); + CheckGasp(); const std::vector<unsigned char> font = SquareGlyphFont('A', 700); { std::ofstream file(root / "app0" / "font.ttf", std::ios::binary); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index d56614d548..fc27092fbb 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -22,3 +22,5 @@ - [FT_Outline_Get_Orientation](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI/version and large-outline equivalence are unverified. The adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with the existing unsigned guest counts and an LP64 area accumulator, including on Windows. - [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. + +- [FT_Get_Gasp](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console FreeType version and query-result compatibility are unverified. The guest face adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftgasp.c) and its existing LP64 face bridge. From ac06b871a9299345e55f64f7fa6e632bf02262ef Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:22:31 +0200 Subject: [PATCH 1166/1360] test(libSceFreeType): isolate the gasp query declaration --- core/libs/tests/GuestFreeType.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index a68e64258f..ff611d19b4 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -29,7 +29,6 @@ extern "C" { void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec*, const GuestMatrix*, const GuestVector*); int APS5_VABI FT_Init_FreeType_nid_postfix(void**); int APS5_VABI FT_Done_FreeType_nid_postfix(void*); -int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec*, std::uint32_t); int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); @@ -49,6 +48,8 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +extern "C" int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec*, std::uint32_t); + static void CheckGaspTable(const std::vector<unsigned char>* table, const std::vector<std::pair<unsigned, int>>& ranges, bool versionZero, bool render) { const auto font = SquareGlyphFont('A', 700, false, table); From 29554673bbff163c7ec7e2e92cbc4c765b7fb1d6 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:31:24 +0200 Subject: [PATCH 1167/1360] perf(agc): tell a mid-decode vertex table store by the retired-item count When queue 0 was busy, DecodeOrderedVertexStageInfo reread every recorded attribute word and V# after the decode to catch a store that finished while it ran, a second read of the vertex tables for every draw issued behind queued work. Queue 0 now counts retired items. Before the decode the helper notes that count and the write ranges queued then; afterwards it drains and decodes again only when a read overlaps a write still queued, or when the count moved and a read overlaps a range that was queued at the start (as CnRJay proposed). Writes still held by the draw finisher count as queued, as in Overlaps. Decode-caused drains are counted on the [draw] pipelined line. agc_driver_vertex_table_read_order also checks that a table a finished item wrote, and a store elsewhere retiring mid-decode, drain nothing. AI-assisted: yes --- .../include/Driver/Draw/DrawPipeline.hpp | 13 +++++++++ .../Execution/src/Driver/Draw/DrawDecode.cpp | 17 +++++------ .../src/Driver/Draw/DrawPipeline.cpp | 29 +++++++++++++++++-- .../tests/execution/VertexTableReadOrder.cpp | 29 ++++++++++++++++++- 4 files changed, 75 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index 05cb49ba12..50844922e8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -24,6 +24,11 @@ class DrawPipeline { public: using Commit = std::function<void()>; using Range = std::pair<std::uint64_t, std::uint64_t>; + struct Watch { + std::uint64_t retired = 0; + std::size_t items = 0; + std::vector<Range> writes; + }; enum class DrainReason : std::uint8_t { Packet, Flush, Labels, Capture, Indirect, Submission, Count }; static DrawPipeline& Queue0(); @@ -43,6 +48,11 @@ public: std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); bool OverlapsAny(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges); + Watch WatchWrites(); + bool WrittenSince(const Watch& watch, const std::vector<Range>& reads); + std::uint64_t Retired() const { return retiredItems.load(std::memory_order_acquire); } + void NoteDecodeDrain() { decodeDrains.fetch_add(1, std::memory_order_relaxed); } + std::uint64_t DecodeDrains() const { return decodeDrains.load(std::memory_order_relaxed); } std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes); static bool HeldReads(std::uint64_t address, std::size_t bytes); void ReleaseRunningReads(); @@ -73,6 +83,9 @@ private: std::vector<Item> retired; std::exception_ptr failure; std::atomic<std::size_t> outstanding{0}; + std::atomic<std::uint64_t> retiredItems{0}; + std::atomic<std::uint64_t> decodeDrains{0}; + std::uint64_t reportedDecodeDrains = 0; std::uint32_t idleWaiters = 0; std::size_t resumeAt = 0; bool committerWaiting = false; 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 fc48f5263a..4b8ea5e15b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -4,7 +4,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include <algorithm> -#include <array> #include <spirv/unified1/spirv.hpp> namespace AgcDriver::DriverDetail { @@ -39,16 +38,14 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span<c }; if (!DrawPipeline::Active()) return decode(); auto& queue = DrawPipeline::Queue0(); - const bool queued = queue.Busy(); + const auto watch = queue.WatchWrites(); auto info = decode(); - if (!queued && !queue.Busy()) return info; - const auto pending = [&queue](const Graphics::DecodeRead& read) { return queue.Overlaps(read.address, read.size); }; - const auto unchanged = [](const Graphics::DecodeRead& read) { - std::array<std::byte, 16> now{}; - GuestMemory::Read(read.address, std::span(now.data(), read.size), 4); - return std::equal(read.data.begin(), read.data.begin() + read.size, now.begin()); - }; - if (std::none_of(reads.begin(), reads.end(), pending) && std::all_of(reads.begin(), reads.end(), unchanged)) return info; + if (watch.items == 0 && !queue.Busy() && queue.Retired() == watch.retired) return info; + std::vector<DrawPipeline::Range> ranges; + ranges.reserve(reads.size()); + for (const auto& read : reads) ranges.emplace_back(read.address, read.address + read.size); + if (!queue.WrittenSince(watch, ranges)) return info; + queue.NoteDecodeDrain(); queue.Drain(DrawPipeline::DrainReason::Capture); return decode(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 98555544f2..c207db4cc2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -359,8 +359,10 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { const auto perCommit = commits == 0 ? 0.0 : static_cast<double>(commitNs) / 1e3 / static_cast<double>(commits); const auto perDraw = enqueued == 0 ? 0.0 : static_cast<double>(fullWaitNs) / 1e3 / static_cast<double>(enqueued); const auto queued = enqueued == 0 ? 0.0 : static_cast<double>(depthSum) / static_cast<double>(enqueued); - char reads[64]; - std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed))); + const auto decodeDrainCount = decodeDrains.load(std::memory_order_relaxed); + char reads[96]; + std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held, %llu decode drains", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(decodeDrainCount - reportedDecodeDrains)); + reportedDecodeDrains = decodeDrainCount; drainText += reads; std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast<double>(lockWaitNs) / 1e3 / static_cast<double>(enqueued), static_cast<unsigned long long>(wakes), reads, drainText.c_str()); commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = wakes = lockWaitNs = 0; @@ -410,6 +412,7 @@ void DrawPipeline::run() { countWrites(items.front().writes, false); retired.push_back(std::move(items.front())); items.pop_front(); + retiredItems.fetch_add(1, std::memory_order_release); ++commits; commitNs += spent; if (failed) ++commitErrors; @@ -517,6 +520,28 @@ void DrawPipeline::countWrites(const std::vector<Range>& writes, bool added) { } } +DrawPipeline::Watch DrawPipeline::WatchWrites() { + if (!Busy()) return {Retired(), 0, {}}; + std::lock_guard lock(mutex); + Watch watch{retiredItems.load(std::memory_order_relaxed), items.size(), {}}; + for (const auto& item : items) watch.writes.insert(watch.writes.end(), item.writes.begin(), item.writes.end()); + return watch; +} + +bool DrawPipeline::WrittenSince(const Watch& watch, const std::vector<Range>& reads) { + const auto overlaps = [&reads](const std::vector<Range>& writes) { + return std::any_of(reads.begin(), reads.end(), [&writes](const Range& read) { + return std::any_of(writes.begin(), writes.end(), [&read](const Range& write) { return read.first < write.second && write.first < read.second; }); + }); + }; + if (DrawFinisher::Overlap() && DrawFinisher::Get().PendingOverlapsAny(reads)) return true; + std::lock_guard lock(mutex); + if (std::any_of(items.begin(), items.end(), [&](const Item& item) { return overlaps(item.writes); })) return true; + const auto retiredNow = retiredItems.load(std::memory_order_relaxed); + if (retiredNow == watch.retired) return false; + return retiredNow - watch.retired > watch.items || overlaps(watch.writes); +} + bool DrawPipeline::Busy() const { return outstanding.load(std::memory_order_acquire) != 0 || (finisherProducer && DrawFinisher::Busy() && DrawFinisher::Overlap()); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp index 5d66117892..62786e6108 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexTableReadOrder.cpp @@ -103,9 +103,10 @@ void WaitEmpty() { } std::shared_ptr<Gate> completeAtBufferRead; +const void* completeAtRead = BufferTable.data(); void CompleteQueuedStoreAtBufferRead(std::uint64_t address, std::size_t) { - if (address != Address(BufferTable.data()) || completeAtBufferRead == nullptr) return; + if (address != Address(completeAtRead) || completeAtBufferRead == nullptr) return; std::exchange(completeAtBufferRead, nullptr)->release.store(true, std::memory_order_release); WaitEmpty(); } @@ -161,11 +162,37 @@ int main() { completeAtBufferRead = Hold(AttribTable.data(), 4, [] { AttribTable[0] = (0x2cu << 5u) | 1u; }); AgcDriver::GuestMemory::SetReadGuard(&CompleteQueuedStoreAtBufferRead); + auto drains = DrawPipeline::Queue0().DecodeDrains(); info = Decode(reads); AgcDriver::GuestMemory::SetReadGuard(nullptr); check(completeAtBufferRead == nullptr, "the queued attribute store did not complete during the decode"); check(info.resources[0].fields[0] == 0x9000u && info.resources[0].fields[2] == 128u, "the decode kept the vertex buffer of an attribute word that a queued store replaced during the decode"); check(reads.size() == 2 && reads[1].address == Address(BufferTable.data() + 4), "the recorded reads are not the stored attribute word and the V# it names"); + check(DrawPipeline::Queue0().DecodeDrains() == drains + 1, "the drain the decode caused was not counted"); + + WaitEmpty(); + auto finished = Hold(BufferTable.data() + 4, 16, [] { + BufferTable[4] = 0xb000u; + BufferTable[6] = 256u; + }); + finished->release.store(true, std::memory_order_release); + WaitEmpty(); + gate = Hold(Elsewhere.data(), sizeof(Elsewhere), [] {}); + drains = DrawPipeline::Queue0().DecodeDrains(); + info = Decode(reads); + check(info.resources[0].fields[0] == 0xb000u && info.resources[0].fields[2] == 256u, "the decode did not read the V# a finished item stored"); + check(DrawPipeline::QueuedItems() == 1 && DrawPipeline::Queue0().DecodeDrains() == drains, "a decode of a table that a finished item wrote drained the pipeline"); + gate->release.store(true, std::memory_order_release); + WaitEmpty(); + + completeAtBufferRead = Hold(Elsewhere.data(), sizeof(Elsewhere), [] { Elsewhere[0] = 1u; }); + completeAtRead = BufferTable.data() + 4; + AgcDriver::GuestMemory::SetReadGuard(&CompleteQueuedStoreAtBufferRead); + drains = DrawPipeline::Queue0().DecodeDrains(); + info = Decode(reads); + AgcDriver::GuestMemory::SetReadGuard(nullptr); + check(completeAtBufferRead == nullptr, "the unrelated queued store did not complete during the decode"); + check(info.resources[0].fields[0] == 0xb000u && DrawPipeline::Queue0().DecodeDrains() == drains, "a store elsewhere that retired during the decode made the decode drain"); StopQueue(); std::puts("vertex table read order tests passed"); From d377ed07b9b78a73d9e1ceb37dccbe88130dfbbb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:55:48 +0000 Subject: [PATCH 1168/1360] fix(libkernel_sys): use guest timespec for sched interval --- core/libs/prx/libkernel_sys/Export.cpp | 2 +- core/libs/tests/GuestSchedPriority.cpp | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index e094365558..dba700ab2a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -547,7 +547,7 @@ int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { return 0; } -int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, struct timespec* interval) { +int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, KernelTimespec* interval) { constexpr int GuestEsrch = 3; const int self = getpid_nid_postfix(); if (pid != 0 && pid != self) return static_cast<int>(Fail(GuestEsrch)); diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 59ff1c29e6..0b15d2becd 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -17,7 +17,11 @@ struct GuestRtPrio { int APS5_VABI rtprio_thread_nid_postfix(int function, int lwpid, GuestRtPrio* priority); int APS5_VABI pthread_setconcurrency_nid_postfix(int newLevel); int APS5_VABI pthread_getconcurrency_nid_postfix(void); -int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, struct timespec* interval); +struct GuestTimespec { + std::int64_t tv_sec; + std::int64_t tv_nsec; +}; +int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, GuestTimespec* interval); int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } @@ -50,9 +54,12 @@ static void RequireSetError(int which, int who, int nice, int error) { int main() { Require(Rejects(-1) && Rejects(2) && Rejects(5)); const int self = getpid_nid_postfix(); - struct timespec interval{}; + GuestTimespec interval{}; *__error_nid_postfix() = 13; Require(sched_rr_get_interval_nid_postfix(0, &interval) == 0 && interval.tv_sec == 0 && interval.tv_nsec == 94000000 && *__error_nid_postfix() == 13); + interval.tv_sec = -1; + interval.tv_nsec = -1; + Require(sched_rr_get_interval_nid_postfix(0, &interval) == 0 && interval.tv_sec == 0 && interval.tv_nsec == 94000000); interval = {}; Require(sched_rr_get_interval_nid_postfix(self, &interval) == 0 && interval.tv_sec == 0 && interval.tv_nsec == 94000000); *__error_nid_postfix() = 0; From df421d52bcf64605f9a99c003de55deb0975a130 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 20 Oct 2026 07:28:18 -0400 Subject: [PATCH 1169/1360] fix(agc): verify refreshed draw stage data --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++++ .../Execution/include/Driver/Driver.hpp | 2 +- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 22 +++++++++------- .../tests/execution/DrawCacheReuse.cpp | 26 ++++++++++++++++--- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 48689954f3..452bbae9b6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2780,6 +2780,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_draw_cache_reuse_verify COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_verify PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_draw_cache_reuse_entry_verify COMMAND agc_driver_draw_cache_reuse_tests) + set_tests_properties(agc_driver_draw_cache_reuse_entry_verify PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_ENTRIES=1" SKIP_RETURN_CODE 77 FAIL_REGULAR_EXPRESSION "FATAL:") + if(WIN32) + set_tests_properties(agc_driver_draw_cache_reuse_entry_verify PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_driver_draw_cache_reuse_inline COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 1bba99967e..1d22afe65a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -173,7 +173,7 @@ private: void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, const std::shared_ptr<DrawEntry>& dataEntry, DrawDataCandidates& dataCandidates, std::vector<StageDataHit>& stageData); std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics); static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads); - void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry = false); + void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<StageDataHit>& stageData, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry = false); static bool drawPrecheck(); std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 28c9800bb3..5527b71fec 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -796,7 +796,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { return lastData[shapeKey % lastData.size()].exchange(data, std::memory_order_relaxed) == data; }; const bool shapeNeedsEntry = shapeRefresh && registerKey && shapeKey != 0 && drawDataHits() && !drawParameters.indirect && dataEntry != nullptr && dataStagesCompiled != 0 && refreshSeenTwice(); - cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey, shapeNeedsEntry); + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, stageData, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey, shapeNeedsEntry); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index a76fd8e08d..288ca494b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -152,7 +152,7 @@ void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderReco variant.dataSlots.resize(kept); } -void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry) { +void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<StageDataHit>& stageData, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry) { const bool cacheable = useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu); const bool admitted = cacheable && (verifyHit || admitDrawKey(drawKey, registerKey && drawDataHits() ? shapeKey : 0)); const bool replaceShape = cacheable && !admitted && shapeNeedsEntry; @@ -192,17 +192,17 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP ++differingRunsChanged; } } - const auto sameContent = [](const DispatchVariant& a, const DispatchVariant& b) { - const auto flatten = [](const DispatchVariant& source) { + const auto sameContent = [](const DispatchVariant& a, const std::vector<std::uint32_t>& words, const DispatchVariant& b) { + const auto flatten = [](const DispatchVariant& source, const std::vector<std::uint32_t>& values) { std::vector<std::pair<std::uint64_t, std::uint32_t>> flat; std::size_t word = 0; for (const auto& [begin, end] : source.runs) { - for (auto address = begin; address < end && word < source.words.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, source.words[word++]); + for (auto address = begin; address < end && word < values.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, values[word++]); } std::sort(flat.begin(), flat.end()); return flat; }; - return (a.runs == b.runs && a.words == b.words) || flatten(a) == flatten(b); + return (a.runs == b.runs && words == b.words) || flatten(a, words) == flatten(b, b.words); }; const auto sameForwardedWords = [](const ShaderRecompiler::RecompileResult& a, const ShaderRecompiler::RecompileResult& b, std::uint32_t hidden) { if (a.pushConstants.size() != b.pushConstants.size() || a.shaderDataUserWords != b.shaderDataUserWords) return false; @@ -211,10 +211,14 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP } return true; }; - if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || !sameForwardedWords(*matched[i]->compiled, *variant->compiled, programs[i].hiddenUserWords))) { - ++mismatches; - static std::atomic<std::uint64_t> reports{0}; - if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast<unsigned long long>(programs[i].binary.codeAddress), matched[i]->runs.size(), matched[i]->words.size(), variant->runs.size(), variant->words.size()); + if (verifyHit && matched[i] != nullptr) { + const auto& words = stageData[i].compiled != nullptr ? stageData[i].words : matched[i]->words; + const auto& compiled = stageData[i].compiled != nullptr ? *stageData[i].compiled : *matched[i]->compiled; + if (!sameContent(*matched[i], words, *variant) || !sameForwardedWords(compiled, *variant->compiled, programs[i].hiddenUserWords)) { + ++mismatches; + static std::atomic<std::uint64_t> reports{0}; + if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast<unsigned long long>(programs[i].binary.codeAddress), matched[i]->runs.size(), words.size(), variant->runs.size(), variant->words.size()); + } } if (insertCompare()) { const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::DrawCache); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index ab9818c6ba..41ff2d9a50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -183,6 +183,20 @@ void StageDataHitsRefreshTheirWords(bool libraries) { Require(BrightestColorByte() == 255, mode + ": the draw of constant 1.0 wrote " + std::to_string(BrightestColorByte()) + ", expected 255"); } +void RefreshedStageDataVerifies() { + Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ConstantPixelHeader, ConstantPixelCode.data(), sizeof(ConstantPixelCode), 1); + ColorMemory.fill(std::byte{0}); + PixelConstant[0] = 0.25f; + const auto cached = SubmitConstantDraws(3); + Require(BrightestColorByte() == 64, "draw-entry verification: the initial constant did not render"); + PixelConstant[0] = 1.0f; + const auto refreshed = SubmitConstantDraws(1); + Require(refreshed.verifyHits - cached.verifyHits == 1 && refreshed.stageDataHits - cached.stageDataHits == 1, "draw-entry verification: the refreshed constant did not exercise a verified stage data hit"); + Require(refreshed.verifyMismatches == cached.verifyMismatches, "draw-entry verification: refreshed stage data differed from a fresh capture"); + Require(BrightestColorByte() == 255, "draw-entry verification: the refreshed constant did not render"); +} + void RetainedPixelCacheTransitions() { auto commands = Draws(32.0f, VertexCode.data(), 1, PixelCode.data(), 1, {}); Append(commands, 0x76, {0x00c, std::bit_cast<std::uint32_t>(1.0f)}); @@ -239,10 +253,14 @@ int main() { libraries = device->GraphicsPipelineLibraries(); } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; - ThirdDrawHitsTheCache(libraries); - UserDataDrawsReuseTheirShape(libraries); - StageDataHitsRefreshTheirWords(libraries); - RetainedPixelCacheTransitions(); + if (std::getenv("APS5_VERIFY_DRAW_ENTRIES") != nullptr) { + RefreshedStageDataVerifies(); + } else { + ThirdDrawHitsTheCache(libraries); + UserDataDrawsReuseTheirShape(libraries); + StageDataHitsRefreshTheirWords(libraries); + RetainedPixelCacheTransitions(); + } AgcDriverShutdown_nid_postfix(); Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "the driver's shutdown left the pipeline library worker running"); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; From 1d153c7be658bd258e5b33ff9a57d18b3bb70dd9 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Fri, 23 Oct 2026 01:03:02 +0600 Subject: [PATCH 1170/1360] fix(ci): honor initial newlines in CMake bracket arguments --- tools/check_test_registration.py | 2 +- tools/tests/test_test_registration.py | 31 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index bbc80508d8..7903e7262c 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -81,7 +81,7 @@ def commands(content): argument = re.sub(r"\\(.)", lambda match: {"n": "\n", "r": "\r", "t": "\t"}.get(match[1], match[1]), argument[1:-1], flags=re.S) elif token.group("equals") is not None: width = len(token.group("equals")) + 2 - argument = argument[width:-width] + argument = argument[width:-width].removeprefix("\n") arguments.append(argument) position = token.end() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index de64bbec73..c6bc83ae2b 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -15,6 +15,32 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + def test_bracket_arguments_ignore_only_one_initial_newline(self): + values = (("path", "path"), ("\npath", "path"), ("\n\npath", "\npath"), + ("\n path", " path"), (" \npath", " \npath"), ("\\npath", "\\npath"), + ("\n", ""), ("\npath\n", "path\n")) + for equals in ("", "=", "==="): + for content, expected in values: + with self.subTest(equals=equals, content=content): + command = f"subdirs([{equals}[{content}]{equals}])\n" + self.assertEqual(list(check_test_registration.commands(command)), [("subdirs", [expected])]) + self.assertEqual(list(check_test_registration.commands('subdirs("\npath")\n')), + [("subdirs", ["\npath"])]) + + def test_bracket_initial_newlines_do_not_hide_disabled_tests(self): + content = ('add_test([=[\nbounds]=] "/build/tests/buffer_bounds_tests")\n' + 'set_tests_properties(bounds PROPERTIES DISABLED [=[\nTRUE]=])\n') + self.assertNotIn("buffer_bounds_tests", self.registered(content)) + + def test_bracket_initial_newline_in_subdirectory_is_ignored(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "nested").mkdir() + (build / "CTestTestfile.cmake").write_text('subdirs([=[\nnested]=])\n', encoding="utf-8") + (build / "nested/CTestTestfile.cmake").write_text( + 'add_test(bounds "/build/tests/buffer_bounds_tests")\n', encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"buffer_bounds_tests"}) + def test_literal_prefixes_cannot_expose_commands_inside_arguments(self): prefixes = (r"\#", r"\(", '"prefix" ', '"" ', '"line\nprefix" ', "[[prefix]] ", "[=[prefix]=] ", "[===[line\nprefix]===] ") @@ -311,6 +337,11 @@ class ScriptRegistrationTests(unittest.TestCase): "core/relinker/CMakeLists.txt": "if(WIN32)\n add_test(NAME a COMMAND ${Python3_EXECUTABLE}\n ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_a.py)\nendif()\n", "core/relinker/relinker/tests/test_a.py": ""}), []) + def test_script_in_bracket_argument_with_initial_newline_is_registered(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND python [=[\ntools/tests/test_a.py]=])\n", + "tools/tests/test_a.py": ""}), []) + def test_unregistered_script_is_reported(self): self.assertEqual(self.unrun({ "CMakeLists.txt": "add_test(NAME a COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_a.py)\n", From 1bc835b2fa64c3fe2f0083d7294e9978489328e5 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 19:12:12 +1300 Subject: [PATCH 1171/1360] test(libSceUserService): take the keyboard getter type from its declaration The function-pointer alias dropped APS5_VABI, which does not convert from the declared getters on Windows. Use decltype of the declaration instead. --- core/libs/tests/GuestUserService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 7ccd08f4af..9d5eebdd50 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -61,7 +61,7 @@ int main() { Require(sceUserServiceGetUserColor(123, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK); - using KeyboardSetting = int (*)(int, std::int32_t*); + using KeyboardSetting = decltype(&sceUserServiceGetKeyboardType); for (KeyboardSetting setting : {sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType}) { std::int32_t value = 0x5A5A; Require(setting(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); From 46ff145113d75bfb5c179287fbc8cd3b85fde550 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Wed, 21 Oct 2026 21:52:49 +0600 Subject: [PATCH 1172/1360] fix(tools): flag qualified imports exported only under a NID --- docs/user/USAGE.md | 2 ++ tools/import_audit.py | 10 +++++-- tools/tests/test_import_audit.py | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 988659c9ae..6cd3e2ad5f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -387,6 +387,8 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `absent` | No built library exports the NID: the loader fails. | | `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | +An import that names a library requires the literal export name. If its implementation is found only by hashing the import name, it still counts as `implemented` or `stub`, but the report lists the name mismatch, sets `symbol_mismatch` on the JSON import record and counts it in the summary, and exits with code 1. A strong import with a name mismatch is a current stop. Imports without a library retain the audit's name-to-NID fallback. + Module registries mark ELF weak imports with `"weak": true`, allowing the audit's current-stop selection to skip them. Strong imports omit the field. Older module registries do not preserve weak bindings; regenerate them to distinguish optional imports. ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, PE/SELF containers and non-ELF files with module extensions remain errors. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. diff --git a/tools/import_audit.py b/tools/import_audit.py index 6ee58ad673..2f445230b9 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -594,6 +594,7 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None + record["symbol_mismatch"] = kind in ("implemented", "stub") and resolved_nid != nid and bool(library) ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) @@ -618,7 +619,7 @@ def current_stop(imports, records): if weak: continue record = classes.get((nid, library)) - if record is not None and record["class"] == "absent": + if record is not None and (record["class"] == "absent" or record["symbol_mismatch"]): return record return None @@ -677,6 +678,7 @@ def summarize(records, references, missing, missing_hosts=()): "missing_libraries": missing, "missing_host_dependencies": [{"importer": entry["importer"], "chain": list(entry["chain"])} for entry in missing_hosts], "library_mismatch": sum(1 for record in records if record["library_mismatch"]), + "symbol_mismatch": sum(1 for record in records if record["symbol_mismatch"]), } @@ -722,6 +724,10 @@ def render(summary, records, missing, stops, missing_hosts=(), located=None): current = record["library"] lines.append(f" [{current or 'library not named'}]") lines.append(f" {label(record)}") + mismatched = [record for record in records if record["symbol_mismatch"]] + if mismatched: + lines.extend(["", f"Qualified import names exported only as their NID ({len(mismatched)}): the loader cannot resolve these names"]) + lines.extend(f" {label(record)} -> {nid_names.compute_nid(record['nid'])}" for record in mismatched) if stops: lines.append("") lines.append("Current stop (first unresolved import, in registry order)") @@ -800,7 +806,7 @@ def main(argv=None): return 2 located = locate_missing(missing, {path.resolve().parent for path in args.registries}) if missing else None print(render(summary, records, missing, stops, missing_hosts, located)) - return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 + return 1 if summary["unique_by_class"]["absent"] or summary["symbol_mismatch"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 if __name__ == "__main__": diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index a29775c13b..8d5d3844b3 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -290,6 +290,57 @@ class ImportAuditTests(unittest.TestCase): self.assertTrue(records["missing_function"]["weak"]) self.assertIsNone(data["stops"][0]["nid"]) + def test_nid_only_exports_do_not_resolve_literal_import_names(self): + work = self.work + name, nid = "strlen", nid_names.compute_nid("strlen") + registry, report = work.root / "sdk.registry.json", work.root / "audit.json" + for image in (exporting(nid), build_pe([nid])): + (work.libs / "libc.prx").write_bytes(image) + for stub in (False, True): + write_source(work.source, "libc", (STUB_SOURCE if stub else DONE_SOURCE).format(name=name)) + for weak in (False, True): + with self.subTest(pe=image.startswith(b"MZ"), stub=stub, weak=weak): + write_registry(registry, [(name, "libc.prx", weak)]) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + record, = data["imports"] + self.assertEqual(record["class"], "stub" if stub else "implemented") + self.assertEqual(record["providers"], ["libc.prx"]) + self.assertTrue(record["symbol_mismatch"]) + self.assertEqual(data["symbol_mismatch"], 1) + self.assertEqual(data["stops"][0]["nid"], None if weak else name) + self.assertIn("Qualified import names exported only as their NID", out) + self.assertIn(f"-> {nid}", out) + + def test_literal_exports_resolve_without_a_name_mismatch(self): + work = self.work + name, nid = "strlen", nid_names.compute_nid("strlen") + registry, report = work.root / "sdk.registry.json", work.root / "audit.json" + for image in (exporting(name, nid), build_pe(sorted([name, nid]))): + with self.subTest(pe=image.startswith(b"MZ")): + (work.libs / "libc.prx").write_bytes(image) + write_registry(registry, [(name, "libc.prx"), (nid, "libc.prx")]) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["symbol_mismatch"], 0) + self.assertTrue(all(not record["symbol_mismatch"] for record in data["imports"])) + self.assertIsNone(data["stops"][0]["nid"]) + self.assertNotIn("Qualified import names exported only as their NID", out) + + def test_unscoped_windows_imports_keep_the_loaders_nid_fallback(self): + work = self.work + name, nid = "strlen", nid_names.compute_nid("strlen") + registry, report = work.root / "sdk.registry.json", work.root / "audit.json" + write_registry(registry, [(name, "")]) + (work.libs / "libc.prx").write_bytes(build_pe([nid])) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["symbol_mismatch"], 0) + self.assertIsNone(data["stops"][0]["nid"]) + def test_named_nid_fallback_preserves_stub_and_library_classification(self): nid = nid_names.compute_nid(MESSAGE_NAME) imports = [(MESSAGE_NAME, "libSceOther.prx", False)] From b7a6e9d245e190a0fc2ba1d26f7fc9a84c482b98 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Fri, 23 Oct 2026 12:23:51 +0600 Subject: [PATCH 1173/1360] fix(tools): decouple symbol mismatches from stub classification --- tools/import_audit.py | 3 ++- tools/tests/test_import_audit.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 2f445230b9..a074a6105a 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -584,9 +584,11 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu if not library: providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) resolved_nid = nid + record["symbol_mismatch"] = False if not providers: resolved_nid = nid_names.compute_nid(nid) providers = exports.get(resolved_nid, []) + record["symbol_mismatch"] = bool(library) and library not in modules and bool(providers) and resolved_nid != nid if not library: providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) kind = classify(resolved_nid, library, providers, stubs, modules) @@ -594,7 +596,6 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None - record["symbol_mismatch"] = kind in ("implemented", "stub") and resolved_nid != nid and bool(library) ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 8d5d3844b3..b57fdf0b49 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -313,6 +313,17 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("Qualified import names exported only as their NID", out) self.assertIn(f"-> {nid}", out) + def test_name_mismatch_requires_a_resolved_host_fallback(self): + name, library = "strlen", "libc.prx" + exports = {nid_names.compute_nid(name): [library]} + for providers, modules, kind, mismatch in (({}, set(), "absent", False), + (exports, {library}, "module", False), + (exports, set(), "implemented", True)): + with self.subTest(kind=kind): + records, _ = import_audit.audit([(name, library, False)], providers, {}, {library}, modules) + self.assertEqual(records[0]["class"], kind) + self.assertEqual(records[0]["symbol_mismatch"], mismatch) + def test_literal_exports_resolve_without_a_name_mismatch(self): work = self.work name, nid = "strlen", nid_names.compute_nid("strlen") From 705f6e1580202075f5a80fb6fe01193ba07b000b Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Fri, 23 Oct 2026 06:33:36 +0000 Subject: [PATCH 1174/1360] feat(libkernel_sys): implement chown, lchown, fchownat and linkat All four exports threw. The ownership calls follow fchown: owner and group 0 and the unchanged value are accepted and any other value throws. The path is looked up first with stat, lstat or fstatat, as FreeBSD 11's kern_fchownat does, so a lookup error is returned before the owner is checked. fchownat rejects flags other than AT_SYMLINK_NOFOLLOW with EINVAL. linkat passes AT_FDCWD and absolute paths to link and rejects flags other than AT_SYMLINK_FOLLOW with EINVAL; a relative path with a directory descriptor and a symbolic link source without AT_SYMLINK_FOLLOW throw. On Windows a symbolic link source is detected with the file attributes, since libstdc++ there reports it as a regular file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- .../prx/libkernel_sys/Export.Discovery.cpp | 20 ----- core/libs/prx/libkernel_sys/Export.cpp | 51 ++++++++++++ core/libs/tests/GuestKernelSys.cpp | 81 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 2 + 4 files changed, 134 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index e60ae139a5..e2a7678d17 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -92,11 +92,6 @@ int APS5_VABI blockpool_unmap_nid_postfix(void) { return 0; } -int APS5_VABI chown_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI cpuset_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -117,11 +112,6 @@ int APS5_VABI dynlib_get_obj_member_nid_postfix(void) { return 0; } -int APS5_VABI fchownat_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI futimesat_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -332,16 +322,6 @@ int APS5_VABI lchflags_nid_postfix(void) { return 0; } -int APS5_VABI lchown_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI linkat_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI mdbg_call_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 53ab5bc867..0ff29a6c8a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -101,6 +101,7 @@ constexpr int PcAclPathMax = 60; constexpr int PcMacPresent = 63; constexpr int PcAclNfs4 = 64; constexpr int GuestAtSymlinkNofollow = 0x0200; +constexpr int GuestAtSymlinkFollow = 0x0400; int GuestFileError(int error) { switch (error) { @@ -152,6 +153,22 @@ bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } +void RequireGuestOwner(const char* call, std::uint32_t uid, std::uint32_t gid) { + constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; + if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": an owner other than the guest user").c_str()); + if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": a group other than the guest group").c_str()); +} + +bool IsSymbolicLink(const std::filesystem::path& native) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code error; + return std::filesystem::is_symlink(std::filesystem::symlink_status(native, error)); +#endif +} + std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); @@ -416,6 +433,17 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { return 0; } +int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char* path2, int flag) { + if (flag & ~GuestAtSymlinkFollow) return static_cast<int>(Fail(GuestEinval)); + if (path1 == nullptr || path2 == nullptr) return static_cast<int>(Fail(GuestEfault)); + if ((fd1 != GuestAtFdcwd && *path1 != '/') || (fd2 != GuestAtFdcwd && *path2 != '/')) + NotImplemented_nid_no_patch("linkat: a relative path with a directory descriptor"); + std::filesystem::path source; + if (flag == 0 && *path1 != '\0' && ResolvePathError_nid_no_patch(path1, source) == 0 && IsSymbolicLink(source)) + NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); + return link_nid_postfix(path1, path2); +} + int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); @@ -627,6 +655,29 @@ int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag); + +int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { + FileStat status{}; + if (stat_nid_postfix(path, &status) != 0) return -1; + RequireGuestOwner("chown", uid, gid); + return 0; +} + +int APS5_VABI lchown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { + FileStat status{}; + if (lstat_nid_postfix(path, &status) != 0) return -1; + RequireGuestOwner("lchown", uid, gid); + return 0; +} + +int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, std::uint32_t gid, int flag) { + if (flag & ~GuestAtSymlinkNofollow) return static_cast<int>(Fail(GuestEinval)); + FileStat status{}; + if (fstatat_nid_postfix(fd, path, &status, flag) != 0) return -1; + RequireGuestOwner("fchownat", uid, gid); + return 0; +} int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { if (flag & ~GuestAtSymlinkNofollow) return static_cast<int>(Fail(GuestEinval)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index b481f508fe..e080ec6745 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -63,6 +63,10 @@ int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); int APS5_VABI lchmod_nid_postfix(const char*, std::uint16_t); int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); +int APS5_VABI chown_nid_postfix(const char*, std::uint32_t, std::uint32_t); +int APS5_VABI lchown_nid_postfix(const char*, std::uint32_t, std::uint32_t); +int APS5_VABI fchownat_nid_postfix(int, const char*, std::uint32_t, std::uint32_t, int); +int APS5_VABI linkat_nid_postfix(int, const char*, int, const char*, int); int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); @@ -98,6 +102,82 @@ bool ChrootThrows(const std::string& path) { return false; } +template <typename Call> +bool ThrowsNaming(Call call, const char* text) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), text) != nullptr; + } + return false; +} + +void OwnershipAndLinkTests(const std::filesystem::path& root) { + constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; + const auto file = (root / "owned.txt").string(); + std::ofstream(file).put('x'); + const auto missing = (root / "missing.txt").string(); + const auto throughFile = file + "/inner"; + const auto linked = (root / "linked.txt").string(); + const auto other = (root / "other.txt").string(); + Require(chown_nid_postfix(file.c_str(), 0, 0) == 0); + Require(chown_nid_postfix(file.c_str(), Unchanged, Unchanged) == 0); + Require(lchown_nid_postfix(file.c_str(), 0, Unchanged) == 0); + Require(fchownat_nid_postfix(-100, file.c_str(), Unchanged, 0, 0) == 0); + Require(fchownat_nid_postfix(-100, file.c_str(), 0, 0, 0x200) == 0); + for (const auto call : {chown_nid_postfix, lchown_nid_postfix}) { + Require(call(missing.c_str(), 0, 0) == -1 && Error() == 2); + Require(call(throughFile.c_str(), 0, 0) == -1 && Error() == 20); + Require(call("", 0, 0) == -1 && Error() == 2); + Require(call(nullptr, 0, 0) == -1 && Error() == 14); + } + Require(fchownat_nid_postfix(-100, missing.c_str(), 0, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(-100, file.c_str(), 0, 0, 0x400) == -1 && Error() == 22); + Require(chown_nid_postfix(missing.c_str(), 1000, 0) == -1 && Error() == 2); + Require(chown_nid_postfix(nullptr, 1000, 0) == -1 && Error() == 14); + Require(lchown_nid_postfix(throughFile.c_str(), 0, 1000) == -1 && Error() == 20); + Require(fchownat_nid_postfix(-100, missing.c_str(), 1000, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(4321, "owned.txt", 1000, 0, 0) == -1 && Error() == 9); + Require(fchownat_nid_postfix(-100, file.c_str(), 1000, 0, 0x400) == -1 && Error() == 22); + Require(ThrowsNaming([&] { chown_nid_postfix(file.c_str(), 1000, 0); }, "chown: an owner")); + Require(ThrowsNaming([&] { lchown_nid_postfix(file.c_str(), 0, 1000); }, "lchown: a group")); + Require(ThrowsNaming([&] { fchownat_nid_postfix(-100, file.c_str(), 5, Unchanged, 0); }, "fchownat: an owner")); + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + Require(fchownat_nid_postfix(directory, "owned.txt", 0, 0, 0) == 0); + Require(fchownat_nid_postfix(directory, "missing.txt", 0, 0, 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0) == 0); + Require(std::filesystem::hard_link_count(file) == 2); + Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0x400) == -1 && Error() == 17); + Require(linkat_nid_postfix(-100, missing.c_str(), -100, other.c_str(), 0x400) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, file.c_str(), -100, other.c_str(), 0x200) == -1 && Error() == 22); + Require(linkat_nid_postfix(-100, nullptr, -100, other.c_str(), 0) == -1 && Error() == 14); + Require(linkat_nid_postfix(-100, "", -100, other.c_str(), 0) == -1 && Error() == 2); + Require(ThrowsNaming([&] { linkat_nid_postfix(directory, "owned.txt", -100, other.c_str(), 0); }, "linkat: a relative path")); + Require(ThrowsNaming([&] { linkat_nid_postfix(-100, file.c_str(), directory, "other.txt", 0); }, "linkat: a relative path")); + const auto symbolic = (root / "owned.link").string(); + const auto dangling = (root / "dangling.link").string(); + if (symlink_nid_postfix("owned.txt", symbolic.c_str()) == 0) { + Require(symlink_nid_postfix("missing.txt", dangling.c_str()) == 0); + Require(chown_nid_postfix(symbolic.c_str(), 0, 0) == 0 && lchown_nid_postfix(symbolic.c_str(), 0, 0) == 0); + Require(chown_nid_postfix(dangling.c_str(), 0, 0) == -1 && Error() == 2); + Require(lchown_nid_postfix(dangling.c_str(), 0, 0) == 0); + Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0x200) == 0); + Require(ThrowsNaming([&] { linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0); }, "symbolic link itself")); + Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0x400) == 0); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(other).c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else + Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(other))); +#endif + Require(std::filesystem::hard_link_count(file) == 3); + Require(std::filesystem::remove(other) && std::filesystem::remove(symbolic) && std::filesystem::remove(dangling)); + } + Require(sceKernelClose(directory) == 0); + Require(std::filesystem::remove(linked) && std::filesystem::remove(file)); +} + bool SymlinkTargetThrows(const char* target, const std::string& created) { try { symlink_nid_postfix(target, created.c_str()); @@ -277,6 +357,7 @@ int main() { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); DirectoryRenameTests(root); + OwnershipAndLinkTests(root); const auto file = (root / "file.txt").string(); std::ofstream(file).put('x'); CheckPathconf(root, file); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8a7b58c9c3..22df35e8d7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -25,6 +25,8 @@ - [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it +- [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified +- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `link`, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. A relative path with a directory descriptor throws, and so does a symbolic link as the source without `AT_SYMLINK_FOLLOW`, where FreeBSD links the symbolic link itself. Console behaviour is unverified - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 ### Unknown function info From 78983daa1ceb738b3131a7c774b823d426d5daa2 Mon Sep 17 00:00:00 2001 From: Maximilian Fleckenstein <166870690+maximilianfv@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:27:14 -0300 Subject: [PATCH 1175/1360] fix(libSceNet): wake a receive on an unbound datagram socket at shutdown on macOS On macOS, shutdown(SHUT_RD) on a UDP socket that was never bound does not wake a thread blocked in recv on it, so sceNetShutdown left the receiver hanging. Use the receive wait that Windows already has: block on the socket table until the socket gets a local port or is shut, then poll. On macOS an unbound socket reports port 0 from getsockname, so the check looks at the port instead of the Winsock error. Ported from #8658. Co-authored-by: Himansh Raj <iamthehimansh@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceNet/Export.cpp | 30 ++++++++++++++++++++++++------ docs/dev/debt/libSceNet.md | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 450868e186..25b19c1442 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -685,12 +685,23 @@ int native_message_flags(int flags) { return native; } -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) +#ifdef __APPLE__ +bool has_local_address(NativeSocket native) { + sockaddr_storage local{}; + NativeLength local_length = sizeof(local); + if (::getsockname(native, reinterpret_cast<sockaddr*>(&local), &local_length) != 0) return true; + if (local.ss_family == AF_INET) return reinterpret_cast<const sockaddr_in&>(local).sin_port != 0; + if (local.ss_family == AF_INET6) return reinterpret_cast<const sockaddr_in6&>(local).sin6_port != 0; + return true; +} +#else bool has_local_address(NativeSocket native) { sockaddr_storage local{}; NativeLength local_length = sizeof(local); return ::getsockname(native, reinterpret_cast<sockaddr*>(&local), &local_length) == 0 || WSAGetLastError() != WSAEINVAL; } +#endif void note_local_address(int s, const Sock& socket) { if (socket.type == NET_SOCK_STREAM || socket.bound || !has_local_address(socket.native->value)) return; @@ -707,10 +718,17 @@ int wait_for_datagram(const Sock& socket, std::chrono::steady_clock::time_point const auto now = std::chrono::steady_clock::now(); if (now >= deadline) return NET_EAGAIN; const auto remaining = std::chrono::ceil<std::chrono::milliseconds>(deadline - now).count(); +#ifdef _WIN32 WSAPOLLFD descriptor{}; descriptor.fd = socket.native->value; descriptor.events = POLLRDNORM; return WSAPoll(&descriptor, 1, static_cast<INT>(std::min<long long>(remaining, INT_MAX))) == 0 ? NET_EAGAIN : 0; +#else + pollfd descriptor{}; + descriptor.fd = socket.native->value; + descriptor.events = POLLIN; + return ::poll(&descriptor, 1, static_cast<int>(std::min<long long>(remaining, INT_MAX))) == 0 ? NET_EAGAIN : 0; +#endif } int wait_for_local_address(int s, const Sock& socket, bool empty, int flags) { @@ -1211,7 +1229,7 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { #endif if (native.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); if (::connect(socket.native->value, reinterpret_cast<const sockaddr*>(&native), native_length) == 0) { -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) note_local_address(s, socket); #endif return 0; @@ -1247,7 +1265,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) if (const int wait = wait_for_local_address(s, socket, len == 0, flags)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); @@ -1276,7 +1294,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* } const GuestArena::HostWrite lengthOutput(from == nullptr ? nullptr : fromlen, from == nullptr ? 0 : sizeof(*fromlen)); if (!lengthOutput.Open()) return fail(NET_EFAULT); -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) if (const int wait = wait_for_local_address(s, socket, len == 0, flags)) { if (wait != RECEIVE_NOTHING) return fail(wait); if (from) *fromlen = 0; @@ -1350,7 +1368,7 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co }, true, static_cast<int>(len)); } if (result < 0) return fail(native_error()); -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) note_local_address(s, socket); #endif return result; @@ -1394,7 +1412,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (!destinations.emplace_back(entry.base, static_cast<std::size_t>(entry.length)).Open()) return fail(NET_EFAULT); } const bool datagram = socket.type != NET_SOCK_STREAM; -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) if (const int wait = wait_for_local_address(s, socket, total == 0, flags)) { if (wait != RECEIVE_NOTHING) return fail(wait); msg->flags = 0; diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f9f697287e..f31549f076 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -41,7 +41,7 @@ - [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 - [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. -- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before +- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. - [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ends an IPv4 address at the first space, tab, LF, VT, FF or CR and ignores the rest (`"1.2.3.4 x"` converts), leaves the destination and `sceNetErrnoLoc` unchanged when a conversion fails, as PS4 firmware 10.71 does; not yet confirmed on PS5. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and IPv6 text with whitespace fail. From 1e869157d025c1ed9691a8ac5ad1bc0bf1abc32f Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 19:39:38 +1300 Subject: [PATCH 1176/1360] fix(libSceUserService): report keyboard type 20 (US English) by default The initial user reported keyboard type 0, which the converter maps to IME type 0 and then rejects, so a title reading the layout still could not use it. Report 20 for sceUserServiceGetKeyboardType, which the owner's table maps to IME keyboard type 4 (English, US); the Japanese and Traditional Chinese input variants stay 0. Tests and both debt entries updated. --- core/libs/prx/libSceUserService/Export.cpp | 13 +++++++------ core/libs/tests/GuestConvertKeycode.cpp | 4 ++-- core/libs/tests/GuestUserService.cpp | 7 +++++-- docs/dev/debt/libSceConvertKeycode.md | 2 +- docs/dev/debt/libSceUserService.md | 2 +- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 25c97875cf..6bd5434acd 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -22,15 +22,16 @@ static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t val // Per-user keyboard settings. Nothing in AnyPS5 stores a keyboard language or input variant yet, so every known user -// reports 0 (see docs/dev/debt/libSceUserService.md). -static int ReportKeyboardSetting(int user_id, int32_t* type) { +// reports the same default: 20 (US English) for the keyboard type, 0 for the input variants (see +// docs/dev/debt/libSceUserService.md). +static int ReportKeyboardSetting(int user_id, int32_t* type, int32_t value) { if (type == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } if (user_id != USER_SERVICE_INITIAL_USER_ID) { return USER_SERVICE_ERROR_UNKNOWN_USER; } - *type = 0; + *type = value; return USER_SERVICE_OK; } @@ -233,15 +234,15 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) { } int APS5_VABI sceUserServiceGetKeyboardType(int user_id, int32_t* type) { - return ReportKeyboardSetting(user_id, type); + return ReportKeyboardSetting(user_id, type, 20); } int APS5_VABI sceUserServiceGetJapaneseInputType(int user_id, int32_t* type) { - return ReportKeyboardSetting(user_id, type); + return ReportKeyboardSetting(user_id, type, 0); } int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, int32_t* type) { - return ReportKeyboardSetting(user_id, type); + return ReportKeyboardSetting(user_id, type, 0); } int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) { diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index d10852c231..60721d3937 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -143,12 +143,12 @@ int main() { Require(rejected && virtualKey == 0xABCD); } - // The exported function with the real UserService: unknown users fail, the initial user has no keyboard setting yet (0). + // The exported function with the real UserService: unknown users fail, the initial user reports the US-English default (keyboard type 20 -> IME 4). std::uint32_t type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == invalidUserId); Require(type == 0xFFFFFFFFu); - Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 0); + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4); RequireImeKeyboardTypeMapping(); Require(sceConvertKeycodeGetCharacter(4, 0, 0, nullptr) == invalidAddress); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 9d5eebdd50..b40b52e694 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -3,6 +3,7 @@ #include <cstdint> #include <cstdlib> #include <stdexcept> +#include <utility> extern "C" { int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number); @@ -62,12 +63,14 @@ int main() { Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK); using KeyboardSetting = decltype(&sceUserServiceGetKeyboardType); - for (KeyboardSetting setting : {sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType}) { + const std::pair<KeyboardSetting, std::int32_t> keyboardSettings[] = { + {sceUserServiceGetKeyboardType, 20}, {sceUserServiceGetJapaneseInputType, 0}, {sceUserServiceGetTraditionalChineseInputType, 0}}; + for (const auto& [setting, expected] : keyboardSettings) { std::int32_t value = 0x5A5A; Require(setting(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(setting(123, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A); Require(setting(USER_SERVICE_USER_ID_INVALID, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A); - Require(setting(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == 0); + Require(setting(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == expected); } Require(color == 0); diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 4b2be47cdd..01812a5b99 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -3,4 +3,4 @@ ### Functional - [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. -- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule assumes a PS5 process because `sceKernelIsPs4Process` is unimplemented and AnyPS5 runs PS5 processes. UserService has no stored keyboard setting yet, so every user reports keyboard type 0 and the function returns IME type 0 until one is stored (see libSceUserService.md). +- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule assumes a PS5 process because `sceKernelIsPs4Process` is unimplemented and AnyPS5 runs PS5 processes. UserService has no stored keyboard setting yet, so every user reports the US-English default keyboard type 20 and the function returns IME type 4 until one is stored (see libSceUserService.md). diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 551d6a0049..65ca188348 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,4 +8,4 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output -- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports 0 for all three and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The repository owner allows initialising the keyboard type from the system language through an explicit conversion to UserService keyboard-type ids; that table is not recovered yet, so no value is guessed. `sceConvertKeycodeGetImeKeyboardType` consumes these values. +- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the US-English default 20 for the keyboard type (the owner's table maps it to IME keyboard type 4) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The repository owner allows initialising the keyboard type from the system language through an explicit conversion to UserService keyboard-type ids; that table is not recovered yet, so 20 stays the fixed default until it exists and no other value is guessed. `sceConvertKeycodeGetImeKeyboardType` consumes these values. From 407489e2954019388079ab685675eb26d0ebcb06 Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Tue, 20 Oct 2026 18:19:13 +0200 Subject: [PATCH 1177/1360] feat(libSceFreeType): implement FT_Get_Kerning --- core/libs/prx/libSceFreeType/Export.cpp | 9 ++ core/libs/tests/GuestFreeType.cpp | 105 ++++++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 3 files changed, 116 insertions(+) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 9eaae33cae..d03b7bab55 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -179,6 +179,15 @@ std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec* face, std::u return FT_Get_Char_Index(owner ? owner->host : nullptr, static_cast<FT_ULong>(static_cast<std::uint32_t>(charcode))); } +int APS5_VABI FT_Get_Kerning_nid_postfix(GuestFaceRec* face, std::uint32_t leftGlyph, std::uint32_t rightGlyph, std::uint32_t kernMode, GuestVector* kerning) { + const GuestFace* owner = FaceOf(face); + if (!owner || !kerning) return FT_Get_Kerning(owner ? owner->host : nullptr, leftGlyph, rightGlyph, kernMode, nullptr); + FT_Vector result{}; + const FT_Error error = FT_Get_Kerning(owner->host, leftGlyph, rightGlyph, kernMode, &result); + *kerning = {result.x, result.y}; + return error; +} + int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec* face, std::uint32_t glyph_index, std::int32_t load_flags) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Load_Glyph(owner ? owner->host : nullptr, glyph_index, load_flags); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index bccc9a51c2..0ba8c05be9 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -37,6 +37,7 @@ int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32 int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::int32_t); int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_t); int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); +int APS5_VABI FT_Get_Kerning_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t, std::uint32_t, GuestVector*); } static void Check(bool value, int line) { @@ -56,6 +57,69 @@ template <typename TCall> static bool ThrowsWith(TCall call, const std::string& return false; } +static void Put16(std::vector<unsigned char>& out, std::uint32_t value) { + out.push_back(static_cast<unsigned char>(value >> 8)); + out.push_back(static_cast<unsigned char>(value)); +} + +static void Put32(std::vector<unsigned char>& out, std::uint32_t value) { + Put16(out, value >> 16); + Put16(out, value & 0xFFFFu); +} + +static void PutAll16(std::vector<unsigned char>& out, std::initializer_list<int> values) { + for (const int value : values) Put16(out, static_cast<std::uint32_t>(static_cast<std::uint16_t>(value))); +} + +static std::vector<unsigned char> SquareGlyphFont(std::uint32_t codepoint, int width, bool withKerning = false) { + std::vector<unsigned char> head; + Put32(head, 0x00010000u); + Put32(head, 0x00010000u); + Put32(head, 0); + Put32(head, 0x5F0F3CF5u); + PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); + std::vector<unsigned char> hhea; + Put32(hhea, 0x00010000u); + PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); + std::vector<unsigned char> maxp; + Put32(maxp, 0x00010000u); + PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + std::vector<unsigned char> hmtx; + PutAll16(hmtx, {width, 0, width, 100}); + std::vector<unsigned char> glyf; + PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); + glyf.insert(glyf.end(), {1, 1, 1, 1}); + PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); + std::vector<unsigned char> loca; + PutAll16(loca, {0, 0, static_cast<int>(glyf.size() / 2)}); + std::vector<unsigned char> cmap; + PutAll16(cmap, {0, 1, 3, 1}); + Put32(cmap, 12); + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast<int>(codepoint), 0xFFFF, 0, static_cast<int>(codepoint), 0xFFFF, 1 - static_cast<int>(codepoint), 1, 0, 0}); + std::vector<unsigned char> kern; + PutAll16(kern, {0, 1, 0, 20, 1, 1, 6, 0, 0, 1, 1, -73}); + std::vector<std::pair<const char*, const std::vector<unsigned char>*>> tables = { + {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + if (withKerning) tables.insert(tables.begin() + 5, {"kern", &kern}); + std::vector<unsigned char> font; + Put32(font, 0x00010000u); + const int searchRange = withKerning ? 128 : 64; + PutAll16(font, {static_cast<int>(tables.size()), searchRange, withKerning ? 3 : 2, static_cast<int>(tables.size()) * 16 - searchRange}); + std::uint32_t offset = 12 + static_cast<std::uint32_t>(tables.size()) * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag, tag + 4); + Put32(font, 0); + Put32(font, offset); + Put32(font, static_cast<std::uint32_t>(data->size())); + offset += (static_cast<std::uint32_t>(data->size()) + 3u) & ~3u; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data->begin(), data->end()); + font.resize((font.size() + 3u) & ~std::size_t{3}); + } + return font; +} + static void FaceMatches(const GuestFaceRec* face, FT_Face host, const void* library) { Require(face->num_faces == host->num_faces && face->face_index == host->face_index && face->face_flags == host->face_flags); Require(face->style_flags == host->style_flags && face->num_glyphs == host->num_glyphs && face->num_glyphs == 2); @@ -579,6 +643,47 @@ int main() { Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); } + GuestVector kerning{123, 456}; + Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, &kerning) == FT_Err_Invalid_Face_Handle); + Require(kerning.x == 123 && kerning.y == 456); + Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, nullptr) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Kerning_nid_postfix(face, 1, 1, FT_KERNING_DEFAULT, nullptr) == FT_Err_Invalid_Argument); + Require(FT_Get_Kerning_nid_postfix(face, 1, 1, FT_KERNING_DEFAULT, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + + const auto kernFont = SquareGlyphFont('A', 700, true); + { + std::ofstream file(root / "app0" / "kern.ttf", std::ios::binary); + file.write(reinterpret_cast<const char*>(kernFont.data()), static_cast<std::streamsize>(kernFont.size())); + } + FT_Face kernHost = nullptr; + GuestFaceRec* kernFace = nullptr; + Require(FT_New_Memory_Face(hostLibrary, kernFont.data(), static_cast<FT_Long>(kernFont.size()), 0, &kernHost) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/kern.ttf", 0, &kernFace) == 0); + Require(FT_HAS_KERNING(kernHost)); + const unsigned modes[] = {FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, FT_KERNING_UNSCALED, 99}; + for (const unsigned size : {100u, 12u}) { + Require(FT_Set_Pixel_Sizes_nid_postfix(kernFace, 0, size) == 0 && FT_Set_Pixel_Sizes(kernHost, 0, size) == 0); + for (const unsigned mode : modes) { + FT_Vector expected{}; + Require(FT_Get_Kerning(kernHost, 1, 1, mode, &expected) == 0); + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, 1, 1, mode, &kerning) == 0); + Require(kerning.x == expected.x && kerning.y == expected.y && kerning.y == 0); + if (mode == FT_KERNING_UNSCALED) Require(kerning.x == -73); + if (size == 100 && mode == FT_KERNING_DEFAULT) Require(kerning.x == -448); + if (size == 100 && mode == FT_KERNING_UNFITTED) Require(kerning.x == -467); + } + } + for (const unsigned glyph : {0u, 2u, 0xFFFFFFFFu}) { + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, glyph, 1, FT_KERNING_UNSCALED, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, 1, glyph, FT_KERNING_UNSCALED, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + } + FT_Done_Face(kernHost); Finalizers nativeDone; Finalizers guestDone; diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index cf8dbe3f78..8f7ebb151d 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -6,6 +6,8 @@ - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive +- [FT_Get_Kerning](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the name and NID `LPtlB241JSg` occur in the [PS5 3.20 libSceFreeTypeOt inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c), but the console signature and kerning results are unverified. The adapter assumes the existing LP64 guest layout and follows bundled FreeType 2.13.3 for scaling, rounding and errors + - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. From f200fcf468ec202575e728794d073695fb49e84a Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Tue, 20 Oct 2026 18:24:57 +0200 Subject: [PATCH 1178/1360] test(libSceFreeType): isolate kerning fixture changes --- core/libs/tests/GuestFreeType.cpp | 165 +++++++++++------------------- docs/dev/debt/libSceFreeType.md | 3 +- 2 files changed, 62 insertions(+), 106 deletions(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 0ba8c05be9..2ff57898e8 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -57,69 +57,6 @@ template <typename TCall> static bool ThrowsWith(TCall call, const std::string& return false; } -static void Put16(std::vector<unsigned char>& out, std::uint32_t value) { - out.push_back(static_cast<unsigned char>(value >> 8)); - out.push_back(static_cast<unsigned char>(value)); -} - -static void Put32(std::vector<unsigned char>& out, std::uint32_t value) { - Put16(out, value >> 16); - Put16(out, value & 0xFFFFu); -} - -static void PutAll16(std::vector<unsigned char>& out, std::initializer_list<int> values) { - for (const int value : values) Put16(out, static_cast<std::uint32_t>(static_cast<std::uint16_t>(value))); -} - -static std::vector<unsigned char> SquareGlyphFont(std::uint32_t codepoint, int width, bool withKerning = false) { - std::vector<unsigned char> head; - Put32(head, 0x00010000u); - Put32(head, 0x00010000u); - Put32(head, 0); - Put32(head, 0x5F0F3CF5u); - PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); - std::vector<unsigned char> hhea; - Put32(hhea, 0x00010000u); - PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); - std::vector<unsigned char> maxp; - Put32(maxp, 0x00010000u); - PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); - std::vector<unsigned char> hmtx; - PutAll16(hmtx, {width, 0, width, 100}); - std::vector<unsigned char> glyf; - PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); - glyf.insert(glyf.end(), {1, 1, 1, 1}); - PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); - std::vector<unsigned char> loca; - PutAll16(loca, {0, 0, static_cast<int>(glyf.size() / 2)}); - std::vector<unsigned char> cmap; - PutAll16(cmap, {0, 1, 3, 1}); - Put32(cmap, 12); - PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast<int>(codepoint), 0xFFFF, 0, static_cast<int>(codepoint), 0xFFFF, 1 - static_cast<int>(codepoint), 1, 0, 0}); - std::vector<unsigned char> kern; - PutAll16(kern, {0, 1, 0, 20, 1, 1, 6, 0, 0, 1, 1, -73}); - std::vector<std::pair<const char*, const std::vector<unsigned char>*>> tables = { - {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; - if (withKerning) tables.insert(tables.begin() + 5, {"kern", &kern}); - std::vector<unsigned char> font; - Put32(font, 0x00010000u); - const int searchRange = withKerning ? 128 : 64; - PutAll16(font, {static_cast<int>(tables.size()), searchRange, withKerning ? 3 : 2, static_cast<int>(tables.size()) * 16 - searchRange}); - std::uint32_t offset = 12 + static_cast<std::uint32_t>(tables.size()) * 16; - for (const auto& [tag, data] : tables) { - font.insert(font.end(), tag, tag + 4); - Put32(font, 0); - Put32(font, offset); - Put32(font, static_cast<std::uint32_t>(data->size())); - offset += (static_cast<std::uint32_t>(data->size()) + 3u) & ~3u; - } - for (const auto& [tag, data] : tables) { - font.insert(font.end(), data->begin(), data->end()); - font.resize((font.size() + 3u) & ~std::size_t{3}); - } - return font; -} - static void FaceMatches(const GuestFaceRec* face, FT_Face host, const void* library) { Require(face->num_faces == host->num_faces && face->face_index == host->face_index && face->face_flags == host->face_flags); Require(face->style_flags == host->style_flags && face->num_glyphs == host->num_glyphs && face->num_glyphs == 2); @@ -511,6 +448,67 @@ int main() { SlotMatches(memoryFace->glyph, host->glyph); Require(face->size->metrics.y_ppem == 0); + GuestVector kerning{123, 456}; + Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, &kerning) == FT_Err_Invalid_Face_Handle); + Require(kerning.x == 123 && kerning.y == 456); + Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, nullptr) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Kerning_nid_postfix(face, 1, 1, FT_KERNING_DEFAULT, nullptr) == FT_Err_Invalid_Argument); + Require(FT_Get_Kerning_nid_postfix(face, 1, 1, FT_KERNING_DEFAULT, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + + auto kernFont = font; + const std::uint32_t kernOffset = static_cast<std::uint32_t>(kernFont.size()) + 16; + kernFont[5] = 8; + kernFont[7] = 128; + kernFont[9] = 3; + kernFont[11] = 0; + for (std::size_t entry = 12; entry < 12 + 7 * 16; entry += 16) { + const std::uint32_t offset = (static_cast<std::uint32_t>(kernFont[entry + 8]) << 24) | + (static_cast<std::uint32_t>(kernFont[entry + 9]) << 16) | + (static_cast<std::uint32_t>(kernFont[entry + 10]) << 8) | kernFont[entry + 11]; + std::vector<unsigned char> adjusted; + Put32(adjusted, offset + 16); + std::memcpy(kernFont.data() + entry + 8, adjusted.data(), 4); + } + std::vector<unsigned char> kernEntry{'k', 'e', 'r', 'n'}; + Put32(kernEntry, 0); + Put32(kernEntry, kernOffset); + Put32(kernEntry, 24); + kernFont.insert(kernFont.begin() + 12 + 5 * 16, kernEntry.begin(), kernEntry.end()); + PutAll16(kernFont, {0, 1, 0, 20, 1, 1, 6, 0, 0, 1, 1, -73}); + { + std::ofstream file(root / "app0" / "kern.ttf", std::ios::binary); + file.write(reinterpret_cast<const char*>(kernFont.data()), static_cast<std::streamsize>(kernFont.size())); + } + FT_Face kernHost = nullptr; + GuestFaceRec* kernFace = nullptr; + Require(FT_New_Memory_Face(hostLibrary, kernFont.data(), static_cast<FT_Long>(kernFont.size()), 0, &kernHost) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/kern.ttf", 0, &kernFace) == 0); + Require(FT_HAS_KERNING(kernHost)); + const unsigned modes[] = {FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, FT_KERNING_UNSCALED, 99}; + for (const unsigned size : {100u, 12u}) { + Require(FT_Set_Pixel_Sizes_nid_postfix(kernFace, 0, size) == 0 && FT_Set_Pixel_Sizes(kernHost, 0, size) == 0); + for (const unsigned mode : modes) { + FT_Vector expected{}; + Require(FT_Get_Kerning(kernHost, 1, 1, mode, &expected) == 0); + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, 1, 1, mode, &kerning) == 0); + Require(kerning.x == expected.x && kerning.y == expected.y && kerning.y == 0); + if (mode == FT_KERNING_UNSCALED) Require(kerning.x == -73); + if (size == 100 && mode == FT_KERNING_DEFAULT) Require(kerning.x == -448); + if (size == 100 && mode == FT_KERNING_UNFITTED) Require(kerning.x == -467); + } + } + for (const unsigned glyph : {0u, 2u, 0xFFFFFFFFu}) { + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, glyph, 1, FT_KERNING_UNSCALED, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, 1, glyph, FT_KERNING_UNSCALED, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + } + FT_Done_Face(kernHost); + Require(FT_Set_Pixel_Sizes_nid_postfix(nullptr, 0, 100) == FT_Err_Invalid_Face_Handle); Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); const auto& metrics = host->size->metrics; @@ -643,47 +641,6 @@ int main() { Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); } - GuestVector kerning{123, 456}; - Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, &kerning) == FT_Err_Invalid_Face_Handle); - Require(kerning.x == 123 && kerning.y == 456); - Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, nullptr) == FT_Err_Invalid_Face_Handle); - Require(FT_Get_Kerning_nid_postfix(face, 1, 1, FT_KERNING_DEFAULT, nullptr) == FT_Err_Invalid_Argument); - Require(FT_Get_Kerning_nid_postfix(face, 1, 1, FT_KERNING_DEFAULT, &kerning) == 0); - Require(kerning.x == 0 && kerning.y == 0); - - const auto kernFont = SquareGlyphFont('A', 700, true); - { - std::ofstream file(root / "app0" / "kern.ttf", std::ios::binary); - file.write(reinterpret_cast<const char*>(kernFont.data()), static_cast<std::streamsize>(kernFont.size())); - } - FT_Face kernHost = nullptr; - GuestFaceRec* kernFace = nullptr; - Require(FT_New_Memory_Face(hostLibrary, kernFont.data(), static_cast<FT_Long>(kernFont.size()), 0, &kernHost) == 0); - Require(FT_New_Face_nid_postfix(library, "/app0/kern.ttf", 0, &kernFace) == 0); - Require(FT_HAS_KERNING(kernHost)); - const unsigned modes[] = {FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, FT_KERNING_UNSCALED, 99}; - for (const unsigned size : {100u, 12u}) { - Require(FT_Set_Pixel_Sizes_nid_postfix(kernFace, 0, size) == 0 && FT_Set_Pixel_Sizes(kernHost, 0, size) == 0); - for (const unsigned mode : modes) { - FT_Vector expected{}; - Require(FT_Get_Kerning(kernHost, 1, 1, mode, &expected) == 0); - kerning = {123, 456}; - Require(FT_Get_Kerning_nid_postfix(kernFace, 1, 1, mode, &kerning) == 0); - Require(kerning.x == expected.x && kerning.y == expected.y && kerning.y == 0); - if (mode == FT_KERNING_UNSCALED) Require(kerning.x == -73); - if (size == 100 && mode == FT_KERNING_DEFAULT) Require(kerning.x == -448); - if (size == 100 && mode == FT_KERNING_UNFITTED) Require(kerning.x == -467); - } - } - for (const unsigned glyph : {0u, 2u, 0xFFFFFFFFu}) { - kerning = {123, 456}; - Require(FT_Get_Kerning_nid_postfix(kernFace, glyph, 1, FT_KERNING_UNSCALED, &kerning) == 0); - Require(kerning.x == 0 && kerning.y == 0); - kerning = {123, 456}; - Require(FT_Get_Kerning_nid_postfix(kernFace, 1, glyph, FT_KERNING_UNSCALED, &kerning) == 0); - Require(kerning.x == 0 && kerning.y == 0); - } - FT_Done_Face(kernHost); Finalizers nativeDone; Finalizers guestDone; diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 8f7ebb151d..77ce9c768c 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -6,8 +6,6 @@ - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive -- [FT_Get_Kerning](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the name and NID `LPtlB241JSg` occur in the [PS5 3.20 libSceFreeTypeOt inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c), but the console signature and kerning results are unverified. The adapter assumes the existing LP64 guest layout and follows bundled FreeType 2.13.3 for scaling, rounding and errors - - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. @@ -21,6 +19,7 @@ - [FT_Outline_Reverse, FT_Outline_Translate](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and overflow behavior are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with LP64 coordinates. Translation uses 64-bit modular addition to match that build's `ADD_LONG`; reversal preserves each contour's first point and toggles the reverse-fill flag. Coordinates outside the host `FT_Pos` range still throw if the modified outline is later rendered on Windows - [FT_Get_X11_Font_Format, FT_Get_FSType_Flags](../../../core/libs/prx/libSceFreeType/src/FaceInfo.cpp) (libSceFreeType) - console signatures, format labels and embedding-flag behavior are unverified; the adapter follows bundled FreeType 2.13.3's [format](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfntfmt.c) and [FSType](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfstype.c) services. Format strings borrow native driver storage; the console's string lifetime has not been measured +- [FT_Get_Kerning](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the name and NID `LPtlB241JSg` occur in the [PS5 3.20 libSceFreeTypeOt inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c), but the console signature and kerning results are unverified. The adapter assumes the existing LP64 guest layout and follows bundled FreeType 2.13.3 for scaling, rounding and errors ### Functional From 70a666db7eaef931586d4214ef5f4fb9cbf69468 Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Tue, 20 Oct 2026 18:26:05 +0200 Subject: [PATCH 1179/1360] test(libSceFreeType): keep kerning checks clear of face lifecycle tests --- core/libs/tests/GuestFreeType.cpp | 22 +++++++++++----------- docs/dev/debt/libSceFreeType.md | 2 ++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 2ff57898e8..f952ce3485 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -448,6 +448,17 @@ int main() { SlotMatches(memoryFace->glyph, host->glyph); Require(face->size->metrics.y_ppem == 0); + Require(FT_Set_Pixel_Sizes_nid_postfix(nullptr, 0, 100) == FT_Err_Invalid_Face_Handle); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + const auto& metrics = host->size->metrics; + const auto& guestMetrics = face->size->metrics; + Require(guestMetrics.x_ppem == 100 && guestMetrics.y_ppem == 100 && guestMetrics.x_scale == metrics.x_scale && guestMetrics.y_scale == metrics.y_scale); + Require(guestMetrics.ascender == metrics.ascender && guestMetrics.descender == metrics.descender && guestMetrics.height == metrics.height && guestMetrics.max_advance == metrics.max_advance); + + Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); + Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); + Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); + GuestVector kerning{123, 456}; Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, &kerning) == FT_Err_Invalid_Face_Handle); Require(kerning.x == 123 && kerning.y == 456); @@ -509,17 +520,6 @@ int main() { } FT_Done_Face(kernHost); - Require(FT_Set_Pixel_Sizes_nid_postfix(nullptr, 0, 100) == FT_Err_Invalid_Face_Handle); - Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); - const auto& metrics = host->size->metrics; - const auto& guestMetrics = face->size->metrics; - Require(guestMetrics.x_ppem == 100 && guestMetrics.y_ppem == 100 && guestMetrics.x_scale == metrics.x_scale && guestMetrics.y_scale == metrics.y_scale); - Require(guestMetrics.ascender == metrics.ascender && guestMetrics.descender == metrics.descender && guestMetrics.height == metrics.height && guestMetrics.max_advance == metrics.max_advance); - - Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); - Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); - Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); - Require(FT_Load_Char_nid_postfix(nullptr, 'A', FT_LOAD_DEFAULT) == FT_Err_Invalid_Face_Handle); for (const std::uint64_t code : std::array<std::uint64_t, 4>{'A', 'B', 0x100000041ull, 0xFFFFFFFFFFFFFFFFull}) { for (const std::int32_t flags : std::array<std::int32_t, 4>{FT_LOAD_DEFAULT, FT_LOAD_NO_SCALE, FT_LOAD_NO_HINTING, FT_LOAD_RENDER}) { diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 77ce9c768c..4cd1b71734 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -21,6 +21,8 @@ - [FT_Get_X11_Font_Format, FT_Get_FSType_Flags](../../../core/libs/prx/libSceFreeType/src/FaceInfo.cpp) (libSceFreeType) - console signatures, format labels and embedding-flag behavior are unverified; the adapter follows bundled FreeType 2.13.3's [format](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfntfmt.c) and [FSType](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfstype.c) services. Format strings borrow native driver storage; the console's string lifetime has not been measured - [FT_Get_Kerning](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the name and NID `LPtlB241JSg` occur in the [PS5 3.20 libSceFreeTypeOt inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c), but the console signature and kerning results are unverified. The adapter assumes the existing LP64 guest layout and follows bundled FreeType 2.13.3 for scaling, rounding and errors +- [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions + ### Functional - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. From d4d20364ad39505f5c3dad8e877a7466f8d913f7 Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Tue, 20 Oct 2026 18:28:14 +0200 Subject: [PATCH 1180/1360] test(libSceFreeType): avoid neighboring export test conflicts --- core/libs/tests/GuestFreeType.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index f952ce3485..6ed72c7127 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -32,12 +32,12 @@ int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFac int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t); +int APS5_VABI FT_Get_Kerning_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t, std::uint32_t, GuestVector*); std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64_t); int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::int32_t); int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_t); int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); -int APS5_VABI FT_Get_Kerning_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t, std::uint32_t, GuestVector*); } static void Check(bool value, int line) { @@ -455,10 +455,6 @@ int main() { Require(guestMetrics.x_ppem == 100 && guestMetrics.y_ppem == 100 && guestMetrics.x_scale == metrics.x_scale && guestMetrics.y_scale == metrics.y_scale); Require(guestMetrics.ascender == metrics.ascender && guestMetrics.descender == metrics.descender && guestMetrics.height == metrics.height && guestMetrics.max_advance == metrics.max_advance); - Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); - Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); - Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); - GuestVector kerning{123, 456}; Require(FT_Get_Kerning_nid_postfix(nullptr, 1, 1, FT_KERNING_DEFAULT, &kerning) == FT_Err_Invalid_Face_Handle); Require(kerning.x == 123 && kerning.y == 456); @@ -520,6 +516,10 @@ int main() { } FT_Done_Face(kernHost); + Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); + Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); + Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); + Require(FT_Load_Char_nid_postfix(nullptr, 'A', FT_LOAD_DEFAULT) == FT_Err_Invalid_Face_Handle); for (const std::uint64_t code : std::array<std::uint64_t, 4>{'A', 'B', 0x100000041ull, 0xFFFFFFFFFFFFFFFFull}) { for (const std::int32_t flags : std::array<std::int32_t, 4>{FT_LOAD_DEFAULT, FT_LOAD_NO_SCALE, FT_LOAD_NO_HINTING, FT_LOAD_RENDER}) { From 69850c0a294f38967f332968730577a8df32981e Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Tue, 20 Oct 2026 21:07:18 +0200 Subject: [PATCH 1181/1360] fix(libSceFreeType): preserve LP64 kerning for large left indices --- core/libs/prx/libSceFreeType/Export.cpp | 4 ++++ core/libs/tests/GuestFreeType.cpp | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index d03b7bab55..1675df6a53 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -182,6 +182,10 @@ std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec* face, std::u int APS5_VABI FT_Get_Kerning_nid_postfix(GuestFaceRec* face, std::uint32_t leftGlyph, std::uint32_t rightGlyph, std::uint32_t kernMode, GuestVector* kerning) { const GuestFace* owner = FaceOf(face); if (!owner || !kerning) return FT_Get_Kerning(owner ? owner->host : nullptr, leftGlyph, rightGlyph, kernMode, nullptr); + if (leftGlyph > std::numeric_limits<std::uint16_t>::max() && FT_IS_SFNT(owner->host)) { + *kerning = {0, 0}; + return 0; + } FT_Vector result{}; const FT_Error error = FT_Get_Kerning(owner->host, leftGlyph, rightGlyph, kernMode, &result); *kerning = {result.x, result.y}; diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 6ed72c7127..73139c73d1 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -504,6 +504,14 @@ int main() { if (mode == FT_KERNING_UNSCALED) Require(kerning.x == -73); if (size == 100 && mode == FT_KERNING_DEFAULT) Require(kerning.x == -448); if (size == 100 && mode == FT_KERNING_UNFITTED) Require(kerning.x == -467); + for (const unsigned leftGlyph : {0x10001u, 0x7FFF0001u}) { + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, leftGlyph, 1, mode, &kerning) == 0); + Require(kerning.x == 0 && kerning.y == 0); + } + kerning = {123, 456}; + Require(FT_Get_Kerning_nid_postfix(kernFace, 1, 0x10001u, mode, &kerning) == 0); + Require(kerning.x == expected.x && kerning.y == expected.y); } } for (const unsigned glyph : {0u, 2u, 0xFFFFFFFFu}) { From cf3218b7029da54808cb2009597909781eae23f1 Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Wed, 21 Oct 2026 23:54:11 +0200 Subject: [PATCH 1182/1360] docs(libSceFreeType): remove duplicate debt after rebase --- docs/dev/debt/libSceFreeType.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 4cd1b71734..77ce9c768c 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -21,8 +21,6 @@ - [FT_Get_X11_Font_Format, FT_Get_FSType_Flags](../../../core/libs/prx/libSceFreeType/src/FaceInfo.cpp) (libSceFreeType) - console signatures, format labels and embedding-flag behavior are unverified; the adapter follows bundled FreeType 2.13.3's [format](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfntfmt.c) and [FSType](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfstype.c) services. Format strings borrow native driver storage; the console's string lifetime has not been measured - [FT_Get_Kerning](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the name and NID `LPtlB241JSg` occur in the [PS5 3.20 libSceFreeTypeOt inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c), but the console signature and kerning results are unverified. The adapter assumes the existing LP64 guest layout and follows bundled FreeType 2.13.3 for scaling, rounding and errors -- [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - ### Functional - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. From b23a74f7191a5199f5bb6e56072541bcbc47ccc0 Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Thu, 22 Oct 2026 00:03:26 +0200 Subject: [PATCH 1183/1360] ci: retrigger PR checks From 6ab7bd2ff5a9152154db9c7da472f6c20a7e0a15 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:04:45 +0200 Subject: [PATCH 1184/1360] feat(shader): take shader disk cache format 46 for the 4_4_4_4 target merge main took format 45 for its barycentric metadata, so this branch takes the next number. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index abd799b205..d007fc126a 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 45; +inline constexpr std::uint32_t FormatVersion = 46; enum class LoadStatus { Loaded, From 6093c4647f4671a674ad19ab4316d281d9e3c9e1 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Fri, 23 Oct 2026 13:04:50 +0600 Subject: [PATCH 1185/1360] fix(ci): reject non-JUnit skip reports --- tools/check_skips.py | 5 ++++- tools/tests/test_check_skips.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index c837fb1096..e44304872d 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -27,7 +27,10 @@ def high_refresh_limitation(output): def skips(path): - for case in ET.parse(path).getroot().iter("testcase"): + root = ET.parse(path).getroot() + if root.tag not in ("testsuite", "testsuites"): + raise ValueError(f"{path}: expected a JUnit testsuite or testsuites root, got {root.tag!r}") + for case in root.iter("testcase"): if case.get("status") == "disabled": raise ValueError(f"{path}: testcase {case.get('name')!r} is disabled and was not run") skipped = case.find("skipped") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index ea82a476d1..d0b03c0742 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -36,6 +36,21 @@ OTHER = [ class CheckSkipsTests(unittest.TestCase): + def test_non_junit_xml_fails_in_every_cli_mode(self): + reports = ('<config/>', '<Site><Testing><Test Status="notrun"><Name>coverage</Name></Test></Testing></Site>') + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "ctest.xml" + for report in reports: + junit.write_text(report, encoding="utf-8") + for arguments in (("--job", "linux"), ("--job", "windows"), ("--list",)): + with self.subTest(report=report, arguments=arguments): + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), *arguments], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("expected a JUnit", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertNotIn("all expected", result.stdout) + def test_missing_high_refresh_display_is_expected_in_both_jobs(self): reason = "current display does not provide 120 Hz" reports = ( @@ -213,6 +228,18 @@ class SkipReportTests(unittest.TestCase): path.write_text(content, encoding="utf-8") return list(check_skips.skips(path)) + def test_non_junit_roots_are_rejected_even_with_testcases(self): + for content in ('<report/>', '<testcase name="passed"/>', + '<report><testcase name="passed"/></report>', + '<report><testcase name="gpu"><skipped message="no display"/></testcase></report>'): + with self.subTest(content=content), self.assertRaisesRegex(ValueError, "expected a JUnit"): + self.skipped(content) + + def test_empty_junit_suites_remain_valid(self): + for content in ('<testsuite/>', '<testsuites/>', '<testsuites><testsuite/></testsuites>'): + with self.subTest(content=content): + self.assertEqual(self.skipped(content), []) + def test_ctest_disabled_status_is_not_silently_ignored(self): with self.assertRaisesRegex(ValueError, "disabled.*not run"): self.skipped('<testsuite><testcase name="coverage" status="disabled"/></testsuite>') From f5519f4fcdac9026cf55f7e76dc424000ecb468f Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 09:13:47 +0200 Subject: [PATCH 1186/1360] fix(libSceJpegEnc): return the JPEG size from sceJpegEncEncode On a PS4 Pro with firmware 10.71 a successful sceJpegEncEncode returns the size of the JPEG it wrote, the same value it stores in the output info, and it does so when the output info pointer is null too. AnyPS5 returned 0, so a title that takes the size from the return value got an empty image. The guest test now checks the returned size against the output info. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceJpegEnc/Export.cpp | 2 +- core/libs/tests/GuestJpegEnc.cpp | 14 +++++++------- docs/dev/debt/libSceJpegEnc.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index 4ebcc09712..a5801fcbeb 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -204,7 +204,7 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param outputInfo->size = static_cast<std::uint32_t>(jpeg.size()); outputInfo->height = param->image_height; } - return 0; + return static_cast<std::int32_t>(jpeg.size()); } int32_t APS5_VABI sceJpegEncQueryMemorySize(const JpegEncCreateParam* param) { diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 8206423e5f..5475dde235 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -209,7 +209,7 @@ int main() { rgba.image_pitch = pitch; rgba.compression_ratio = 0; info = {}; - Require(sceJpegEncEncode(handle, &rgba, &info) == 0); + Require(sceJpegEncEncode(handle, &rgba, &info) == static_cast<std::int32_t>(info.size) && info.size > 0); Require(info.height == 16); Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); @@ -225,7 +225,7 @@ int main() { JpegEncEncodeParam bgra = rgba; bgra.pixel_format = 1; info = {}; - Require(sceJpegEncEncode(handle, &bgra, &info) == 0); + Require(sceJpegEncEncode(handle, &bgra, &info) == static_cast<std::int32_t>(info.size) && info.size > 0); Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); @@ -242,7 +242,7 @@ int main() { yuv.image_pitch = 16 * 2; yuv.pixel_format = 10; info = {}; - Require(sceJpegEncEncode(handle, &yuv, &info) == 0); + Require(sceJpegEncEncode(handle, &yuv, &info) == static_cast<std::int32_t>(info.size) && info.size > 0); Require(AverageError(std::vector<std::uint8_t>(16 * 16 * 3, 100), DecodeOutput(info)) < 3); static unsigned char gray[16 * 16]; @@ -257,7 +257,7 @@ int main() { y8.color_space = 2; y8.sampling_type = 0; info = {}; - Require(sceJpegEncEncode(handle, &y8, &info) == 0); + Require(sceJpegEncEncode(handle, &y8, &info) == static_cast<std::int32_t>(info.size) && info.size > 0); Require(ReadFrame(jpeg, info.size) == std::pair{1, std::uint8_t{0x11}}); const auto decodedGray = DecodeOutput(info, 1); Require(AverageError(std::vector<std::uint8_t>(gray, gray + sizeof(gray)), decodedGray) < 6); @@ -265,11 +265,11 @@ int main() { rgba.sampling_type = 1; rgba.compression_ratio = 0; info = {}; - Require(sceJpegEncEncode(handle, &rgba, &info) == 0); + Require(sceJpegEncEncode(handle, &rgba, &info) == static_cast<std::int32_t>(info.size) && info.size > 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); + Require(sceJpegEncEncode(handle, &rgba, nullptr) == static_cast<std::int32_t>(info.size)); Require(jpeg[0] == 0xFF && jpeg[1] == 0xD8); JpegEncEncodeParam tooSmall = rgba; @@ -303,7 +303,7 @@ int main() { p.jpeg_size = sizeof(restarted); p.restart_interval = restart; JpegEncOutputInfo out{}; - Require(sceJpegEncEncode(handle, &p, &out) == 0); + Require(sceJpegEncEncode(handle, &p, &out) == static_cast<std::int32_t>(out.size)); 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); diff --git a/docs/dev/debt/libSceJpegEnc.md b/docs/dev/debt/libSceJpegEnc.md index d64816c332..e41f12ac23 100644 --- a/docs/dev/debt/libSceJpegEnc.md +++ b/docs/dev/debt/libSceJpegEnc.md @@ -2,4 +2,4 @@ ### Unimplemented functionality -- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - MJPEG mode throws; its encoding path is not implemented. Restart intervals are confirmed by the native validator, MCU counter and DRI writer: 0 disables restart markers, -1 selects one MCU row, and 1 through 65535 specify an MCU count. Other values return the invalid-parameter error. +- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - MJPEG mode throws; its encoding path is not implemented. Restart intervals are confirmed by the native validator, MCU counter and DRI writer: 0 disables restart markers, -1 selects one MCU row, and 1 through 65535 specify an MCU count. Other values return the invalid-parameter error. A successful call returns the JPEG's size, as a PS4 Pro (firmware 10.71) does; the size itself comes from this encoder, so it differs from the console's for the same image (the console 633 bytes, this encoder 650, for an 8x8 RGBA image at 4:2:0). From 6653dffe62a1c923ed18169f3c6c3c18e2d812e7 Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:42:08 +0500 Subject: [PATCH 1187/1360] feat(libkernel_sys): implement scePthreadCondattrGetclock Read the clock of a libkernel condition attribute, returning SCE_KERNEL_ERROR_EINVAL for a null or destroyed attribute as scePthreadCondattrSetclock does, after FreeBSD 11.4 libthr's _pthread_condattr_getclock. A null clock pointer throws. --- .../prx/libkernel_sys/Export.Discovery.cpp | 5 ---- core/libs/prx/libkernel_sys/Export.cpp | 9 +++++++ core/libs/tests/GuestKernelSys.cpp | 26 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 63d4260ea0..b9642b654c 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -2316,11 +2316,6 @@ int APS5_VABI scePthreadBarrierattrSetpshared(void** attr, int pshared) { return SceResult(pthread_barrierattr_setpshared_nid_postfix(attr, pshared)); } -int APS5_VABI scePthreadCondattrGetclock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePthreadGetconcurrency(void) { return pthread_getconcurrency_nid_postfix(); } diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 53ab5bc867..210dc49dd5 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -22,6 +22,8 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" @@ -712,6 +714,13 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { return 0; } +int APS5_VABI scePthreadCondattrGetclock(const PthreadCondattr* attr, KernelClockid* clockId) { + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!clockId) throw std::runtime_error("scePthreadCondattrGetclock: null clock id"); + *clockId = (*attr)->_clockid; + return 0; +} + int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to) { if (from == nullptr || to == nullptr) return static_cast<int>(Fail(GuestEfault)); if (*from == '\0' || *to == '\0') return static_cast<int>(Fail(GuestEnoent)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index b481f508fe..98c4ef58da 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "SceTypes.hpp" #include <array> #include <chrono> @@ -66,6 +67,10 @@ int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); +int APS5_VABI scePthreadCondattrInit(PthreadCondattr*); +int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr*); +int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr*, KernelClockid); +int APS5_VABI scePthreadCondattrGetclock(const PthreadCondattr*, KernelClockid*); int* APS5_VABI __error_nid_postfix(); } @@ -149,6 +154,26 @@ void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { } } +void CheckCondattrClock() { + constexpr KernelClockid Realtime = 0; + constexpr KernelClockid Monotonic = 4; + PthreadCondattr attr = nullptr; + KernelClockid clock = -1; + Require(scePthreadCondattrGetclock(nullptr, &clock) == SCE_KERNEL_ERROR_EINVAL && clock == -1); + Require(scePthreadCondattrGetclock(nullptr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadCondattrGetclock(&attr, &clock) == SCE_KERNEL_ERROR_EINVAL && clock == -1); + Require(scePthreadCondattrInit(&attr) == 0 && attr != nullptr); + Require(scePthreadCondattrGetclock(&attr, &clock) == 0 && clock == Realtime); + Require(scePthreadCondattrSetclock(&attr, Monotonic) == 0); + Require(scePthreadCondattrGetclock(&attr, &clock) == 0 && clock == Monotonic); + bool getclockThrew = false; + try { scePthreadCondattrGetclock(&attr, nullptr); } catch (const std::runtime_error&) { getclockThrew = true; } + Require(getclockThrew); + Require(scePthreadCondattrDestroy(&attr) == 0 && attr == nullptr); + clock = -1; + Require(scePthreadCondattrGetclock(&attr, &clock) == SCE_KERNEL_ERROR_EINVAL && clock == -1); +} + void CheckPathconf(const std::filesystem::path& root, const std::string& file) { const auto directory = root.generic_string(); for (const auto& path : {file, directory}) { @@ -273,6 +298,7 @@ int main() { Require(umask_nid_postfix(0) == 022); Require(umask_nid_postfix(07777) == 0); Require(umask_nid_postfix(022) == 07777); + CheckCondattrClock(); const auto root = std::filesystem::path("anyps5-kernel-sys-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8a7b58c9c3..b2b1559b1a 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -6,6 +6,7 @@ ### Unknown function info - [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - which SDK version the console returns is unmeasured. The [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/crt/kernel.c#L257-L275) declares the process parameters with a 32-bit PS4 SDK version at offset `0x10` and a 32-bit PS5 SDK version at `0x14`; this export returns the field at `0x10`, the one [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reads and the one [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138) (PS4, a single 64-bit `sdk_version` there) reads from the eboot. The `int (int32_t*)` signature and `EINVAL` (`0x80020016`) for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62). Closes when: measured on a PS5 +- [scePthreadCondattrGetclock](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the `int (const PthreadCondattr*, KernelClockid*)` signature and checks follow FreeBSD 11.4 libthr's [`_pthread_condattr_getclock`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_condattr.c#L82-L90) with the error code of libkernel's `scePthreadCondattrSetclock`: a null or destroyed attribute returns `SCE_KERNEL_ERROR_EINVAL`, and a null clock pointer throws, since libthr writes through it unchecked. Console behaviour is unverified ### Functional From 9dd607ee33e2736fe65be32d1a4faaef10bbfaba Mon Sep 17 00:00:00 2001 From: xTRACKGOD <135815363+xTRACKGOD@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:55:36 +0530 Subject: [PATCH 1188/1360] fix(tools): skip unselected executables in fresh builds --- tools/select_tests.py | 35 ++++++++++++ tools/tests/test_select_tests.py | 91 +++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index a69c482e43..80005e556d 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -1,5 +1,6 @@ import argparse import collections +import importlib.util import json import os import re @@ -7,6 +8,10 @@ import subprocess import sys from pathlib import Path +REGISTRATION_SPEC = importlib.util.spec_from_file_location("check_test_registration", Path(__file__).with_name("check_test_registration.py")) +REGISTRATION = importlib.util.module_from_spec(REGISTRATION_SPEC) +REGISTRATION_SPEC.loader.exec_module(REGISTRATION) + FULL = re.compile(r"^CMakeLists\.txt$|^core/CMakeLists\.txt$|^core/libs/CMakeLists\.txt$|^\.github/workflows/build\.yml$" r"|^3rdparty/|^cmake/|^\.gitmodules$|^tools/select_tests\.py$") LOCAL = re.compile(r"(^|/)CMakeLists\.txt$|\.cmake$") @@ -87,8 +92,36 @@ class Build: def tests(self, *arguments): listing = json.loads(subprocess.run(["ctest", "--test-dir", self.path, "--show-only=json-v1", *arguments], capture_output=True, text=True, check=True).stdout) + missing = {test["name"] for test in listing["tests"] if not test.get("command")} + if missing: + commands = self.test_commands(missing) + for test in listing["tests"]: + candidates = commands.get(test["name"], set()) + if test["name"] in missing and len(candidates) == 1: + command = next(iter(candidates)) + if command and os.path.isabs(command[0]): + test["command"] = list(command) return listing, listing["tests"] + def test_commands(self, names): + found, visited, pending = collections.defaultdict(set), set(), [Path(self.path)] + while pending: + directory = pending.pop().resolve() + file = directory / "CTestTestfile.cmake" + if directory in visited or not file.is_file(): + continue + visited.add(directory) + try: + commands = list(REGISTRATION.commands(file.read_text(encoding="utf-8-sig"))) + except (OSError, UnicodeError, ValueError): + continue + for command, arguments in commands: + if command == "subdirs": + pending.extend(directory / argument for argument in arguments) + elif command == "add_test" and arguments and arguments[0] in names: + found[arguments[0]].add(tuple(arguments[1:])) + return found + def output(self, path): absolute = self.absolute(path) if not inside(absolute, self.path): @@ -216,6 +249,8 @@ def plan(build, graph, picked): _, selected = build.tests("-I", "0,0,0," + ",".join(map(str, run))) names = {t["name"] for t in selected} run = [i for i, t in enumerate(listed, 1) if t["name"] in names] + if any(not t.get("command") for t in listed if t["name"] in names): + return run, build.leaves(), len(listed) keep = {build.output(a) for t in listed if t["name"] in names for a in t.get("command") or [] if os.path.isabs(a)} skip = {build.target(t) for t in listed if t["name"] not in names} - keep - {None} return run, [t for t in build.leaves() if t not in skip], len(listed) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 7dbeb88bee..b89ba0904a 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -119,13 +119,61 @@ class SelectTests(unittest.TestCase): self.assertEqual(self.select("alpha/alpha.h", graph=older)[1], {"alpha", "alpha_data", "beta", "beta_setup", "script"}) def test_selected_target_skips_unselected_executables(self): - self.select("alpha/main.c") subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + self.select("alpha/main.c") subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) built = {p.stem for p in self.build.rglob("*_tests*") if p.is_file() and os.access(p, os.X_OK)} subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) self.assertEqual(built, {"alpha_tests"}) + def test_fresh_build_keeps_a_shared_selected_executable(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) / "fresh" + compiler = shutil.which("gcc") or shutil.which("cc") + subprocess.run(["cmake", "-S", str(self.source), "-B", str(build), "-G", "Ninja", f"-DCMAKE_C_COMPILER={compiler}"], + check=True, capture_output=True) + changed = build / "changed.txt" + changed.write_text("data/input.txt\n", encoding="utf-8") + self.tool("select", build, self.graph, changed) + subprocess.run(["ninja", "-C", str(build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + built = {path.stem for path in build.rglob("*_tests*") if path.is_file() and os.access(path, os.X_OK)} + self.assertEqual(built, {"alpha_tests"}) + output = self.tool("ctest", build, "--output-on-failure") + self.assertEqual(set(re.findall(r"Test +#\d+: (\S+)", output)), {"alpha_data", "script"}) + + def test_empty_selection_does_not_build_unselected_executables(self): + cmake = self.source / "CMakeLists.txt" + original = cmake.read_text(encoding="utf-8") + try: + cmake.write_text(original.replace('add_test(NAME script COMMAND ${CMAKE_COMMAND} -E true)\n', ''), encoding="utf-8") + subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) + subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + self.assertEqual(self.select("README.md")[1], set()) + subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + self.assertFalse(any(path.is_file() and os.access(path, os.X_OK) for path in self.build.rglob("*_tests*"))) + finally: + cmake.write_text(original, encoding="utf-8") + subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + + def test_retargeted_test_does_not_prune_its_previous_executable(self): + cmake = self.source / "alpha/CMakeLists.txt" + original = cmake.read_text(encoding="utf-8") + try: + cmake.write_text(original.replace("add_executable(alpha_tests main.c)", + "add_executable(alpha_tests main.c)\nadd_executable(replacement_tests main.c)") + .replace("COMMAND alpha_tests", "COMMAND replacement_tests"), encoding="utf-8") + subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) + subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + self.select("beta/main.c") + subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + built = {path.stem for path in self.build.rglob("*_tests*") if path.is_file() and os.access(path, os.X_OK)} + self.assertEqual(built, {"alpha_tests", "beta_tests", "setup_tests"}) + finally: + cmake.write_text(original, encoding="utf-8") + subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + def test_explain_reports_input_and_fixture_reasons_without_a_build_cache(self): changed = self.build / "explain-changed.txt" cache = self.build / "CMakeCache.txt" @@ -182,8 +230,8 @@ class FixtureBuildTests(unittest.TestCase): marker.unlink(missing_ok=True) listed = self.build / "changed.txt" listed.write_text(changed + "\n", encoding="utf-8") - SelectTests.tool("select", self.build, graph, listed) subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + SelectTests.tool("select", self.build, graph, listed) subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) result = subprocess.run([sys.executable, str(TOOL), "ctest", str(self.build), "--output-on-failure"], capture_output=True, text=True) @@ -292,6 +340,15 @@ class CaseInsensitivePathTests(unittest.TestCase): class PlanTests(unittest.TestCase): + def test_unresolved_selected_command_keeps_unselected_build_targets(self): + build = mock.Mock() + listed = [{"name": "selected"}, {"name": "other", "command": ["/build/tests/shared_tests"]}] + build.tests.side_effect = [({}, listed), ({}, [listed[0]])] + build.leaves.return_value = ["tests/shared_tests", "helper"] + build.target.side_effect = lambda test: "tests/shared_tests" if test.get("command") else None + graph = {"tests": {"selected": {}, "other": {}}} + self.assertEqual(select_tests.plan(build, graph, {"selected"}), ([1], ["tests/shared_tests", "helper"], 2)) + def test_plan_empty_run_avoids_invoking_ctest(self): build = mock.Mock() build.tests.return_value = ({}, [{"name": "alpha", "command": ["/build/alpha_tests"]}]) @@ -305,5 +362,35 @@ class PlanTests(unittest.TestCase): build.tests.assert_called_once_with() +class MissingCommandTests(unittest.TestCase): + def test_only_unambiguous_absolute_commands_in_reachable_files_are_recovered(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + active, stale = root / "active tests", root / "stale" + active.mkdir() + stale.mkdir() + executable = (root / "tests/unit_tests").as_posix() + helper = (root / "tests/helper_tests").as_posix() + (root / "CTestTestfile.cmake").write_text('subdirs("active tests")\n', encoding="utf-8") + (active / "CTestTestfile.cmake").write_text( + f'add_test([=[absolute]=] "{executable}" "{helper}")\n' + 'add_test(relative tests/relative_tests)\n' + 'add_test(path unit_tests)\n' + f'if(CONFIG)\nadd_test(ambiguous "{executable}")\nelse()\nadd_test(ambiguous "{helper}")\nendif()\n' + f'add_test(existing "{executable}")\n' + 'subdirs("..")\n', encoding="utf-8") + (stale / "CTestTestfile.cmake").write_text(f'add_test(stale "{executable}")\n', encoding="utf-8") + names = ("absolute", "relative", "path", "ambiguous", "existing", "stale") + listed = [{"name": name} for name in names] + listed[4]["command"] = [helper] + build = object.__new__(select_tests.Build) + build.path = str(root) + with mock.patch.object(select_tests.subprocess, "run", return_value=mock.Mock(stdout=json.dumps({"tests": listed}))): + _, recovered = build.tests() + commands = {test["name"]: test.get("command") for test in recovered} + self.assertEqual(commands, {"absolute": [executable, helper], "relative": None, "path": None, + "ambiguous": None, "existing": [helper], "stale": None}) + + if __name__ == "__main__": unittest.main() From 616bd98623c933495577525b6a6317bc0dd58088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 09:30:05 +0200 Subject: [PATCH 1189/1360] fix(main): raise conventions shard timeout to 120s Failing run 39915816778 (windows conventions_function_bodies_shard_1 Timeout 30.06s; sibling shards took 27s on the same runner). No culprit merge: the 30s limit sat at the edge of normal runtime. Linux failure in that run was a runner-side submodule shallow.lock (infra). --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fba7c7a18b..25d411aa58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,7 +169,7 @@ if(BUILD_TESTING) endif() add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12) - set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30) + set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 120) endforeach() add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) From fe0383c0d66782315ef5d427c4a5055a2b3611bc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:19:46 +0000 Subject: [PATCH 1190/1360] fix(agc): publish unit-shadow results before an address-based build maps its heaps An address-based build binds the registered heaps in place and does not store the storage results pending in them. With unit shadows on, a write-back that retiled into the shadow reaches the import only through a publish, and nothing published before such a build, so its pointer reads saw the bytes of the last publish. UploadFinish now publishes the fresh units over the heaps the build maps: the address space's base on the cached path, the exempt in-place regions on the per-build path. One AnyShadowedOverlaps check over their span gates it, pending results stay pending, and the copies count under their own publish reason, "address". --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++ .../Graphics/include/UnitShadow.hpp | 2 +- .../Graphics/src/GuestBufferMemory.cpp | 14 +- .../Graphics/src/UnitShadow.cpp | 3 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 136 ++++++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 48689954f3..ddfaa7231d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1612,6 +1612,14 @@ if(BUILD_TESTING) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_idle_publish_wait COMMAND agc_driver_recorder_tests --idle-publish-wait-only) set_tests_properties(agc_idle_publish_wait PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000;APS5_WRITE_WATCH_IMPORTS=watch") + add_test(NAME agc_driver_bda_reads_shadowed_results COMMAND agc_driver_recorder_tests --bda-shadowed-results-only) + add_test(NAME agc_driver_bda_reads_shadowed_results_per_build COMMAND agc_driver_recorder_tests --bda-shadowed-results-only) + set_tests_properties(agc_driver_bda_reads_shadowed_results agc_driver_bda_reads_shadowed_results_per_build PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_driver_bda_reads_shadowed_results_per_build PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_SPACE_CACHE=1") + if(WIN32) + set_tests_properties(agc_driver_bda_reads_shadowed_results agc_driver_bda_reads_shadowed_results_per_build PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() if(WIN32) add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only) set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp index 4c8124f513..978cb22468 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp @@ -48,7 +48,7 @@ bool UnitShadowEnabled(); // byte of the range; PartialUnits: a writer of the range (only units it covers partly are published, // so the neighbours' results in the rest of the unit reach the import before the write stamps it). enum class PublishScope : std::uint8_t { None, Whole, PartialUnits }; -enum class PublishReason : std::uint8_t { Hook, Region, Indirect, CopySource, CopyDestination, Fill, FillClear, Label, Keys, Scanout, Validate, Upload, TailMip, Evict, Retire, Teardown, Count }; +enum class PublishReason : std::uint8_t { Hook, Region, Indirect, CopySource, CopyDestination, Fill, FillClear, Label, Keys, Scanout, Validate, Upload, TailMip, Evict, Retire, Teardown, Address, Count }; // The reason for a FlushPending reason string ("memory access" -> Hook, ...). PublishReason PublishReasonFor(const char* flushReason); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c46494982e..95bd7113e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4529,6 +4529,12 @@ void GuestBufferMemory::UploadFinish(bool addressable) { const auto loopStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; std::uint64_t refreshUs = 0; std::uint64_t copyUs = 0; + std::uint64_t heapsBegin = std::numeric_limits<std::uint64_t>::max(); + std::uint64_t heapsEnd = 0; + if (addressable && space != nullptr && !space->base.empty()) { + heapsBegin = space->base.front().begin; + heapsEnd = space->base.back().end; + } for (auto& region : regions) { if (!region.pending) continue; region.pending = false; @@ -4613,7 +4619,12 @@ void GuestBufferMemory::UploadFinish(bool addressable) { // an address-based shader maps are exempt (they hold every pending image, and such shaders // do not read texture memory through pointers); APS5_BDA_FLUSH=1 flushes for them too. static const bool bdaFlush = std::getenv("APS5_BDA_FLUSH") != nullptr; - if (!(addressable && region.hostBacked) || bdaFlush) StorageTexture::FlushPending(region.begin, static_cast<std::size_t>(bytes), nullptr, "imported buffer region"); + if (!(addressable && region.hostBacked) || bdaFlush) { + StorageTexture::FlushPending(region.begin, static_cast<std::size_t>(bytes), nullptr, "imported buffer region"); + } else { + heapsBegin = std::min(heapsBegin, region.begin); + heapsEnd = std::max(heapsEnd, region.end); + } continue; } if (copyOnGpu) { @@ -4644,6 +4655,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { copyRegion(region, addressable); if (profile) copyUs += microsecondsSince(copyStart); } + if (heapsEnd > heapsBegin) StorageTexture::PublishShadowsOnly(heapsBegin, static_cast<std::size_t>(heapsEnd - heapsBegin), PublishScope::Whole, "address-based heaps"); if (!gpuCopies.empty()) recordGpuCopies(gpuCopies, addressable); takeHeapReferences(); if (!profile) return; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index e33a4d0c7a..09131d5068 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -22,7 +22,7 @@ namespace { constexpr std::uint64_t UnitBytes = 65536; constexpr std::size_t ReasonCount = static_cast<std::size_t>(PublishReason::Count); -constexpr const char* ReasonNames[ReasonCount] = {"hook", "region", "indirect", "copy-src", "copy-dst", "fill", "fill-clear", "label", "keys", "scanout", "validate", "upload", "tail", "evict", "retire", "teardown"}; +constexpr const char* ReasonNames[ReasonCount] = {"hook", "region", "indirect", "copy-src", "copy-dst", "fill", "fill-clear", "label", "keys", "scanout", "validate", "upload", "tail", "evict", "retire", "teardown", "address"}; std::uint64_t BudgetBytes() { static const std::uint64_t budget = [] { @@ -454,6 +454,7 @@ PublishReason PublishReasonFor(const char* flushReason) { if (is("packet store")) return PublishReason::Label; if (is("buffer copy count") || is("dispatch-cache entry")) return PublishReason::Validate; if (is("storage image creation") || is("storage refresh")) return PublishReason::Upload; + if (is("address-based heaps")) return PublishReason::Address; return PublishReason::Hook; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 569be1e0ff..148dfce3c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4988,6 +4988,139 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) recorder.Sync(); } +bool addressShadowTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (!UnitShadowEnabled() || base.hostImportAlignment == 0) { + std::cout << "skipped, unit shadows or host imports are unavailable\n"; + return false; + } + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + SetImportWatch(base, ImportWatch::Watch); +#endif + struct Restore { + const Context& context; + ImportWatch decided; + ~Restore() { SetImportWatch(context, decided); } + } restore{base, decided}; + constexpr std::uint32_t side = 256; + constexpr std::size_t bytes = side * side * 4; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "skipped, no write watching\n"; + return false; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + std::memset(block, 0x55, bytes); + const auto address = reinterpret_cast<std::uint64_t>(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr || !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "skipped, the heap is not imported or not watched\n"; + return false; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == bytes, "the heap's surface has an unexpected size"); + const auto* words = static_cast<const std::uint8_t*>(block); + const auto holds = [&](std::array<std::uint8_t, 4> texel) { + for (std::size_t i = 0; i < bytes; ++i) { + if (words[i] != texel[i % 4]) return false; + } + return true; + }; + const auto settle = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + struct Build { + bool copied; + bool current; + }; + const auto build = [&](std::array<std::uint8_t, 4> texel) { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + Build result{recorder.PendingWriteOverlaps(address, bytes), false}; + recorder.Submit(); + device.WaitQueue(); + result.current = holds(texel); + leased.WriteBack(); + recorder.Sync(); + return result; + }; + const auto none = build({0x55, 0x55, 0x55, 0x55}); + Require(!none.copied && none.current, "an address-based build over a heap with no shadowed units recorded a copy"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + draw({{1.0f, 0.0f, 0.0f, 1.0f}}); + StorageTexture::FlushPending(address, bytes, nullptr, "storage refresh", PublishScope::None); + settle(); + if (!AnyShadowedOverlaps(address, bytes)) { + std::cout << "skipped, the image's write-back did not retile into the unit shadow\n"; + return false; + } + Require(holds({0x55, 0x55, 0x55, 0x55}), "a write-back into the unit shadow reached the import"); + static_cast<void>(ShadowReport()); + const auto shadowed = build({255, 0, 0, 255}); + const auto report = ShadowReport(); + Require(shadowed.current, "an address-based build over a heap left the unit shadow's stored results unpublished: the import held the earlier bytes at its submission"); + Require(shadowed.copied && !AnyShadowedOverlaps(address, bytes), "an address-based build published the heap's shadowed units without noting the copy, or left some unpublished"); + Require(report.find(" address 1/") != std::string::npos, "the address-based build's publish was not counted under its own reason:" + report); + const auto again = build({255, 0, 0, 255}); + Require(!again.copied && again.current, "a second address-based build with nothing newly retiled recorded another copy"); + draw({{0.0f, 1.0f, 0.0f, 1.0f}}); + const auto pending = build({255, 0, 0, 255}); + Require(StorageTexture::FindPending(address, bytes) == image, "an address-based build stored the pending results of an image over its heap"); + Require(!pending.copied && pending.current, "an address-based build changed the heap while an image's results were pending"); + StorageTexture::FlushPending(address, bytes, nullptr, "memory access"); + settle(); + } + recorder.Sync(); + std::cout << "address-based builds read the unit shadow's stored results (address space cache " << (std::getenv("APS5_NO_ADDRESS_SPACE_CACHE") != nullptr ? "off" : "on") << ")\n"; + return true; +} + bool writtenBufferOrderTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -10347,6 +10480,9 @@ int main(int argc, char** argv) { } return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--bda-shadowed-results-only") { + return addressShadowTests(device, recorder) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--split-writeback-only") { if (device.GetContext().hostImportAlignment == 0) { std::cout << "skipped, host imports are unavailable\n"; From 546040adc5ab9412598d5afbdfe48a6d2d795b2a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 03:32:02 +0000 Subject: [PATCH 1191/1360] test(agc): size the address shadow test's heap past the Windows settle floor On Windows only host imports of 1 MiB or more are settled and stay write-watched; a smaller import is compared, so its range leaves the watch and the unit shadow never engages. The address shadow test's heap was 256 KiB, so both of its tests skipped on Windows with "the heap is not imported or not watched", with the fix and without it. The heap is now 2 MiB, filled exactly by a 1024x512 storage image, and its allocation keeps one unregistered 64 KiB block after the range, so no other registered range can continue it into a larger import run. The assertions and Linux's forced watch are unchanged. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 148dfce3c5..8d47574d35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5003,16 +5003,18 @@ bool addressShadowTests(const Device& device, Recorder& recorder) { ImportWatch decided; ~Restore() { SetImportWatch(context, decided); } } restore{base, decided}; - constexpr std::uint32_t side = 256; - constexpr std::size_t bytes = side * side * 4; - void* block = AllocateWatched(bytes, 65536); + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t bytes = width * height * 4; + constexpr std::size_t allocated = bytes + 65536; + void* block = AllocateWatched(allocated, 65536); if (block == nullptr) { std::cout << "skipped, no write watching\n"; return false; } struct Release { void* block; - ~Release() { ReleaseWatched(block, bytes); } + ~Release() { ReleaseWatched(block, allocated); } } release{block}; std::memset(block, 0x55, bytes); const auto address = reinterpret_cast<std::uint64_t>(block); @@ -5041,8 +5043,8 @@ bool addressShadowTests(const Device& device, Recorder& recorder) { context.detiler = &detiler; GuestTextureResource resource{}; resource.baseAddress = address; - resource.width = side; - resource.height = side; + resource.width = width; + resource.height = height; resource.mipCount = 1; resource.tileMode = TextureTileMode::kR64KBX; resource.dimension = TextureDimension::k2D; From 530c6fb0df68502583b8622b7218cc75a1c041a2 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:56:59 +0000 Subject: [PATCH 1192/1360] fix(agc): publish the units a completion store covers partly before it lands A CPU store made inside a completion action (a copied buffer's write-back) reached the flush hook with PublishScope::None, and PublishShadow skipped such stores anyway. Nothing was published before the store landed, and its own stamp then made every unit it touched stale, so the next publish dropped those units whole: the storage results the store did not cover were lost, both those already in the unit shadow and those of a pending image the hook had just stored into it. The hook now publishes the units such a store covers partly, as outside completion actions, and its full Sync inside completion actions lands the copies before the store. PublishShadow, FlushPending and PublishShadowsOnly take a waited flag for this; other publishes made by completion stores are still skipped. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Graphics/include/Texture.hpp | 4 +- .../Graphics/include/UnitShadow.hpp | 2 +- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 10 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 14 +- .../Graphics/src/UnitShadow.cpp | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 123 ++++++++++++++++++ 7 files changed, 145 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ddfaa7231d..21570ee8ab 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1620,6 +1620,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_bda_reads_shadowed_results agc_driver_bda_reads_shadowed_results_per_build PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_unit_shadow_completion_store COMMAND agc_driver_recorder_tests --completion-store-shadow-only) + set_tests_properties(agc_unit_shadow_completion_store PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_unit_shadow_completion_store PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() if(WIN32) add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only) set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index c9706d4528..f61b3836ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -212,7 +212,7 @@ public: // any unit was: a reader of the import must sync for a publish as for a store. Only images // with a pending unit inside the range are stored (APS5_NO_FLUSH_PRETEST=1 lists every // overlapping pending image, as before). - static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); + static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr, bool waited = false); static void FlushAllPending(const char* reason); static bool WaitForFlushing(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr); static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); @@ -277,7 +277,7 @@ public: // FlushPending's publish of the unit shadows over the range without the image stores (what it // does when it lists no image, and all an access AccessKeptByCpu approved needs): whether any // unit was published. - static bool PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason = "memory access"); + static bool PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason = "memory access", bool waited = false); // For the [hooksync] recorded-store line: images a FlushPending stored after a hook skip of // theirs (AccessKeptByCpu), of which by the hook for the same read site as the last skip; 10 s deltas. struct HookSkipCounts { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp index 978cb22468..6d69206ae4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp @@ -90,7 +90,7 @@ struct ShadowedRange { // published, clears stale ones. Returns the units copied. Inside a completion action a store's // publish records nothing (the store's own stamp makes the unit stale; a recorded publish would land // over the store later). -std::size_t PublishShadow(std::uint64_t address, std::size_t bytes, PublishScope scope, PublishReason reason); +std::size_t PublishShadow(std::uint64_t address, std::size_t bytes, PublishScope scope, PublishReason reason, bool waited = false); // No GpuMutex, no tracker: conservative (a unit with a generation and no publish counts), for // hook-free readers and a build's stage A. bool AnyShadowedOverlaps(std::uint64_t address, std::size_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 01071562ad..cfbb3c7b06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1040,11 +1040,9 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { recorder->SyncThrough(address, bytes, true); } } - // A reader takes every shadowed unit of the range; a store only the units it covers partly - // (its own stamp makes the others stale). Inside a completion action a store has no publish - // recorded: the batch is not waited for, and the copy would land over the store later. const auto site = GuestMemory::CurrentReadSite(); - const auto scope = site != GuestMemory::ReadSite::Store ? PublishScope::Whole : completionDepth != 0 ? PublishScope::None : PublishScope::PartialUnits; + const auto scope = site != GuestMemory::ReadSite::Store ? PublishScope::Whole : PublishScope::PartialUnits; + const bool waited = completionDepth != 0; bool published = false; // The classification is made before the store (the store changes what it measures); profile only. StorageTexture::AccessClassification classification{}; @@ -1059,10 +1057,10 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { const bool kept = !HookFlushCpuBlocks() && StorageTexture::AccessKeptByCpu(address, bytes, &images, &evicted); if (kept || (images == 0 && !HookFlushCpuBlocks())) { stored = StorageTexture::WaitForFlushing(address, bytes); - published = StorageTexture::PublishShadowsOnly(address, bytes, scope); + published = StorageTexture::PublishShadowsOnly(address, bytes, scope, "memory access", waited); if (kept && HookSyncProfiled()) CountRecordedStoreSkip(RecordedStoreKey(), evicted); } else { - stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published); + stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published, waited); } if (stored || published || (completionDepth == 0 && SnapshotOverlaps(address, bytes))) { // Stores into imported memory were only recorded; the CPU is about to read them. The wait diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ef8c717fd3..ff2f19ca7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3868,7 +3868,7 @@ void waitForIdlePublish(const char* reason) { } -bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { +bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published, bool waited) { static_cast<void>(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { @@ -3907,12 +3907,12 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons if (!flush.empty()) BumpPendingSerial(); } if (flush.empty()) { - const bool waited = WaitForFlushing(address, bytes, except); + const bool stored = WaitForFlushing(address, bytes, except); // Units shadowed over the range (another image's results retiled into the import's unit // shadow, or an earlier flush's) still reach the import as the scope asks. - const bool units = PublishShadowsOnly(address, bytes, scope, reason); + const bool units = PublishShadowsOnly(address, bytes, scope, reason, waited || stored); if (published != nullptr) *published = units; - return waited; + return stored; } struct Unregister { const std::vector<std::shared_ptr<StorageTexture>>& flush; @@ -3960,7 +3960,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons // The stores above went into the unit shadows where slabs took them: the consumer reads the // import, so they are published under the same hold, after every store. if (scope != PublishScope::None) { - const auto units = PublishShadow(address, bytes, scope, PublishReasonFor(reason)); + const auto units = PublishShadow(address, bytes, scope, PublishReasonFor(reason), waited); if (units != 0) waitForIdlePublish(reason); if (published != nullptr) *published = units != 0; } @@ -4124,10 +4124,10 @@ void StorageTexture::awaitStoredSave(const std::vector<bool>* layers) { if (outstanding()) recorder->Sync(); } -bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) { +bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason, bool waited) { if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); - if (PublishShadow(address, bytes, scope, PublishReasonFor(reason)) == 0) return false; + if (PublishShadow(address, bytes, scope, PublishReasonFor(reason), waited) == 0) return false; waitForIdlePublish(reason); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index 09131d5068..94b0655305 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -473,9 +473,9 @@ ShadowSlabPin::~ShadowSlabPin() { slab->pins.fetch_sub(1, std::memory_order_relaxed); } -std::size_t PublishShadow(std::uint64_t address, std::size_t bytes, PublishScope scope, PublishReason reason) { +std::size_t PublishShadow(std::uint64_t address, std::size_t bytes, PublishScope scope, PublishReason reason, bool waited) { if (!UnitShadowEnabled() || scope == PublishScope::None || bytes == 0) return 0; - if (Recorder::InCompletion() && GuestMemory::CurrentReadSite() == GuestMemory::ReadSite::Store) { + if (!waited && Recorder::InCompletion() && GuestMemory::CurrentReadSite() == GuestMemory::ReadSite::Store) { // The store's own stamp makes the unit stale; a publish recorded now would land over the // store when the batch runs (the completion is not waited for). Stats().skippedInCompletion.fetch_add(1, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 8d47574d35..68a362406c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5123,6 +5123,126 @@ bool addressShadowTests(const Device& device, Recorder& recorder) { return true; } +bool completionStoreShadowTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (!UnitShadowEnabled() || base.hostImportAlignment == 0) { + std::cout << "skipped, unit shadows or host imports are unavailable\n"; + return false; + } + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + SetImportWatch(base, ImportWatch::Watch); +#endif + struct Restore { + const Context& context; + ImportWatch decided; + ~Restore() { SetImportWatch(context, decided); } + } restore{base, decided}; + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t bytes = width * height * 4; + constexpr std::size_t allocated = bytes + 65536; + constexpr std::size_t storedOffset = 16; + void* block = AllocateWatched(allocated, 65536); + if (block == nullptr) { + std::cout << "skipped, no write watching\n"; + return false; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, allocated); } + } release{block}; + std::memset(block, 0x55, bytes); + const auto address = reinterpret_cast<std::uint64_t>(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr || !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "skipped, the heap is not imported or not watched\n"; + return false; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == bytes, "the heap's surface has an unexpected size"); + const auto settle = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + constexpr std::array<std::byte, 4> stored{std::byte{0x77}, std::byte{0x77}, std::byte{0x77}, std::byte{0x77}}; + std::vector<std::byte> read(bytes); + const auto run = [&](bool shadowed) { + std::memset(block, 0x55, bytes); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkClearColorValue red{{1.0f, 0.0f, 0.0f, 1.0f}}; + context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + settle(); + Require(StorageTexture::FindPending(address, bytes) == image, "the cleared image's results are not pending"); + if (shadowed) { + StorageTexture::FlushPending(address, bytes, nullptr, "storage refresh", PublishScope::None); + settle(); + if (!AnyShadowedOverlaps(address, bytes)) return false; + Require(StorageTexture::FindPending(address, bytes) == nullptr, "the image's results stayed pending after their write-back into the unit shadow"); + } + recorder.OnComplete([&] { AgcDriver::GuestMemory::Write(address + storedOffset, stored, 4); }); + settle(); + AgcDriver::GuestMemory::Read(address, read, 4); + settle(); + } + recorder.Sync(); + const std::string over = shadowed ? "a shadowed unit" : "a pending image"; + Require(std::equal(stored.begin(), stored.end(), read.begin() + storedOffset), "a CPU store inside a completion did not reach guest memory over " + over); + std::size_t lost = 0; + for (std::size_t i = 0; i < bytes; ++i) { + if (i >= storedOffset && i < storedOffset + stored.size()) continue; + const auto expected = std::array<std::uint8_t, 4>{255, 0, 0, 255}[i % 4]; + if (static_cast<std::uint8_t>(read[i]) != expected) ++lost; + } + Require(lost == 0, "a CPU store inside a completion over " + over + " lost " + std::to_string(lost) + " bytes of the image's results around it"); + return true; + }; + run(false); + if (!run(true)) { + std::cout << "skipped, the image's write-back did not retile into the unit shadow\n"; + return false; + } + std::cout << "a CPU store inside a completion keeps the results of a pending image and of a shadowed unit around it\n"; + return true; +} + bool writtenBufferOrderTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -10485,6 +10605,9 @@ int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--bda-shadowed-results-only") { return addressShadowTests(device, recorder) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--completion-store-shadow-only") { + return completionStoreShadowTests(device, recorder) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--split-writeback-only") { if (device.GetContext().hostImportAlignment == 0) { std::cout << "skipped, host imports are unavailable\n"; From 7e217f1490cfb844ab332e693dcf47393d265be7 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:39:07 +0200 Subject: [PATCH 1193/1360] test(libkernel): run the guest_kqueue standard input case on a null device The case expects an EVFILT_WRITE change on descriptor 0 to throw because descriptor 0 is neither a socket nor the write end of a pipe. When the test is started with a pipe as standard input the guest cannot tell the ends of a Windows pipe apart, the change is accepted and the test aborts at that check. Point descriptor 0 at the null device first so the result no longer depends on how the test was started. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestKqueue.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 62085d4470..d7c73cad1e 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -15,6 +15,12 @@ #include <aclapi.h> #include <windows.h> #endif +#include <fcntl.h> +#ifdef _WIN32 +#include <io.h> +#else +#include <unistd.h> +#endif extern "C" { int APS5_VABI kqueue_nid_postfix(); @@ -1112,7 +1118,20 @@ static void Timeout() { Require(close_nid_postfix(kq) == 0); } +static void ReplaceStandardInputWithNull() { +#ifdef _WIN32 + const int null = _open("NUL", _O_RDONLY); + Require(null >= 0 && _dup2(null, 0) == 0); + Require(_close(null) == 0); +#else + const int null = ::open("/dev/null", O_RDONLY); + Require(null >= 0 && ::dup2(null, 0) == 0); + Require(::close(null) == 0); +#endif +} + static void StandardInput() { + ReplaceStandardInputWithNull(); const int copy = dup_nid_postfix(0); if (copy < 0) { Require(*__error_nid_postfix() == GuestEbadf); From a142125bd102d14e62214d620d4c676ae714bb78 Mon Sep 17 00:00:00 2001 From: klosixgaming-arch <klosixgaming-arch@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:09:22 +0000 Subject: [PATCH 1194/1360] fix(agc): bind a multisampled sRGB target as a storage image in its UNORM twin MultisampleTargetStorageView required the rendered VkFormat to equal the requested one. A 4x R8G8B8A8_SRGB target asked for as guest format 56 (R8G8B8A8_UNORM) therefore threw "storage image access to multisampled color target ... is not implemented", although the target is created with a UNORM storage view for exactly this access. The lookup now matches when both formats resolve to the same host storage format, which only holds for an sRGB format and its UNORM twin or for identical formats. Unrelated formats of the same size are still refused. AI-assisted: yes. Written with AI assistance and reviewed by me. --- .../Graphics/src/MultisampleTarget.cpp | 8 ++++- .../tests/execution/MultisampleRetirement.cpp | 35 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 809e680d42..da1732e9e2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1134,6 +1134,12 @@ VkComponentMapping swapRedBlue(VkComponentMapping components) { return {swapped(components.r, VK_COMPONENT_SWIZZLE_R), swapped(components.g, VK_COMPONENT_SWIZZLE_G), swapped(components.b, VK_COMPONENT_SWIZZLE_B), swapped(components.a, VK_COMPONENT_SWIZZLE_A)}; } +bool sameStorageForm(const Context& context, VkFormat rendered, VkFormat requested) { + if (rendered == requested) return true; + const auto storage = HostStorageFormat(context, rendered); + return storage != VK_FORMAT_UNDEFINED && storage == HostStorageFormat(context, requested); +} + } std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { @@ -1173,7 +1179,7 @@ VkImageView MultisampleTargetStorageView(const Context& context, const GuestText if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; const auto& rendered = target->target; - if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format)); + if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format)); other = target.get(); } if (other == nullptr) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index 27f7d4ae49..5cc437a819 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -265,11 +265,11 @@ private: bool mapped = false; }; -ColorTarget Target(const GuestBlock& block) { +ColorTarget Target(const GuestBlock& block, VkFormat format = VK_FORMAT_R8G8B8A8_UNORM) { ColorTarget target{}; target.address = target.surfaceAddress = block.Address(); target.extent = target.surfaceExtent = {Side, Side}; - target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.format = format; target.elementBytes = 4; target.samples = 4; target.tileMode = ColorTileMode::RenderTarget; @@ -556,6 +556,36 @@ public: AwaitImages(0, "MSAA targets with twin views were not fully released"); } + void StorageSrgbTwin() { + if (!context.storageImageMultisample || HostStorageFormat(context, VK_FORMAT_R8G8B8A8_SRGB) != VK_FORMAT_R8G8B8A8_UNORM) { + std::puts("the device has no UNORM storage form for a multisampled sRGB target; its storage twin case is skipped"); + return; + } + GuestBlock block; + const auto target = Target(block, VK_FORMAT_R8G8B8A8_SRGB); + auto resource = Resource(block); + VkImageView storageView = VK_NULL_HANDLE; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + MultisampleTargetView(context, target); + storageView = MultisampleTargetStorageView(context, resource); + Require(storageView != VK_NULL_HANDLE, "a UNORM storage access to a multisampled sRGB target has no view"); + Require(MultisampleTargetStorageView(context, resource) == storageView, "a repeated UNORM storage access to a multisampled sRGB target changed its view"); + resource.format = 60; + bool refused = false; + try { + MultisampleTargetStorageView(context, resource); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "a storage access in an unrelated format reached a multisampled sRGB target"); + } + Sync(); + block.Unmap(); + Clear(); + AwaitImages(0, "the sRGB storage twin targets were not fully released"); + } + private: AgcDriver::VulkanDevice& device; Context context; @@ -574,6 +604,7 @@ int main() { run.RecordedRemap(); run.SampledRemap(); run.StorageRemap(); + run.StorageSrgbTwin(); run.OrderRemap(); if (context.storageImageMultisample) run.StorageTwin(); std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes"); From 3f0a1e4222d0fe257d3f72b99b3a7196076de1d8 Mon Sep 17 00:00:00 2001 From: klosixgaming-arch <klosixgaming-arch@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:09:28 +0000 Subject: [PATCH 1195/1360] fix(agc): keep a promoted multisampled target's color view out of storage usage A multisampled sRGB target promoted to storage gained VK_IMAGE_USAGE_STORAGE_BIT on its image, and its color view in the sRGB format inherited that usage. sRGB formats have no storage image feature, so the validation layer reported VUID-VkImageViewCreateInfo-usage-02275 on the first storage access. The view now excludes the storage usage whenever the image has it, which is what it already did for targets created with a sample-order storage form. AI-assisted: yes. Written with AI assistance and reviewed by me. --- .../libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index da1732e9e2..98695ac999 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -227,7 +227,7 @@ public: viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; viewUsage.usage = imageUsage & ~VK_IMAGE_USAGE_STORAGE_BIT; - if (orderFormat != VK_FORMAT_UNDEFINED) viewInfo.pNext = &viewUsage; + if ((imageUsage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) viewInfo.pNext = &viewUsage; Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target"); } catch (...) { destroy(created); From 00d5db19f0ec9ebb588cf38193f6da7559da01c1 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:40:19 +0530 Subject: [PATCH 1196/1360] feat(libSceNpCommerce): implement internal dialog initialization Delegate sceNpCommerceDialogInitializeInternal to sceNpCommerceDialogInitialize, sharing dialog state and returning COMMON_DIALOG_ERROR_ALREADY_INITIALIZED on duplicate initialization, following shadPS4. Add regression tests in GuestNpCommerceDialog covering initial, duplicate, and post-termination initialization, and update technical debt documentation. --- core/libs/prx/libSceNpCommerce/Export.cpp | 3 +-- core/libs/tests/GuestNpCommerceDialog.cpp | 15 +++++++++++++++ docs/dev/debt/libSceNpCommerce.md | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index e224383c46..3a8ee95c0d 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -170,8 +170,7 @@ int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param) { } int APS5_VABI sceNpCommerceDialogInitializeInternal() { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceNpCommerceDialogInitialize(); } } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 9cb602b2b0..3db9b4c3b3 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -13,6 +13,7 @@ extern "C" { int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceNpCommerceDialogInitialize(); +int APS5_VABI sceNpCommerceDialogInitializeInternal(); int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param); int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param); int APS5_VABI sceNpCommerceDialogUpdateStatus(void); @@ -31,6 +32,7 @@ alignas(16) std::array<std::byte, 64> process{}; constexpr int statusNone = 0; constexpr int statusInitialized = 1; constexpr int statusFinished = 3; +constexpr int errorAlreadyInitialized = static_cast<int>(0x80B80004u); constexpr std::int32_t resultUserCanceled = 1; void RequireCanceledResult(std::uint64_t userData, bool clearsReserved) { @@ -72,6 +74,19 @@ int main() { Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); NpCommerceDialogResult result{}; Require(sceNpCommerceDialogGetResult(&result) == static_cast<int>(0x80B80003u)); + + Require(sceNpCommerceDialogInitializeInternal() == 0); + Require(sceNpCommerceDialogGetStatus() == statusInitialized); + Require(sceNpCommerceDialogInitializeInternal() == errorAlreadyInitialized); + Require(sceNpCommerceDialogInitialize() == errorAlreadyInitialized); + Require(sceNpCommerceDialogGetStatus() == statusInitialized); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogInitializeInternal() == errorAlreadyInitialized); + Require(sceNpCommerceDialogGetStatus() == statusInitialized); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); Require(sceNpCommerceDialogInitialize() == 0); Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B8000Bu)); Require(sceNpCommerceDialogGetResult(&result) == static_cast<int>(0x80B80005u)); diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index 02559d2c86..762eed1207 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. +- [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and delegates to `sceNpCommerceDialogInitialize()`, sharing dialog status and returning `COMMON_DIALOG_ERROR_ALREADY_INITIALIZED` (0x80B80004) when already initialized. Any internal options, parameter structs, or console error returns remain unmeasured on hardware. - [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): extended content combinations outside content mode 0 and content mode 1 with dialog mode 1 throw when otherwise valid. The native restriction reads process sysctl field 68; its relation to host process metadata is unresolved. Mode-specific meanings of the option and 64-bit argument remain unresolved. - [sceNpCommerceDialogOpen](../../../core/libs/prx/libSceNpCommerce/Export.cpp): Product copying and service submission remain part of the cancelled UI stub. From 0aa3f3505b71897646d2562c55ea9cfebc15a891 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:47:28 +0100 Subject: [PATCH 1197/1360] feat(libSceCamera): declare PS5 3.20 pending exports --- core/libs/prx/libSceCamera/Export.cpp | 529 ++++++++++++++++++++++++++ docs/dev/debt/libSceCamera.md | 109 ++++++ 2 files changed, 638 insertions(+) diff --git a/core/libs/prx/libSceCamera/Export.cpp b/core/libs/prx/libSceCamera/Export.cpp index 26319c1e6c..2d7757d706 100644 --- a/core/libs/prx/libSceCamera/Export.cpp +++ b/core/libs/prx/libSceCamera/Export.cpp @@ -62,4 +62,533 @@ int APS5_VABI sceCameraIsValidFrameData(void) { return 0; } +int APS5_VABI sceCameraStopByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceIDWithoutOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraInitializeRegistryCalibData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Finalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetConnectedDeviceAndDriverMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2IsAttached(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetTrackerMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibDataForEve(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibDataFromDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraIsConnectedForSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetVideoSyncInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetConfigInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAttributeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetMmapConnectedCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibDataFromDeviceForEve(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetVrCaptureInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetCalibDataForEve(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetLensCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetCalibData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetProcessFocusByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Initialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAccGetData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Stop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetProductInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetLensCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraStartByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Open(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Start(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetLensCorrectionInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraOpenByModuleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioGetData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraWaitForEveReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetRegistryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioGetData2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAppModuleFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetDefectivePixelCancellation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceID(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetForceActivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetFieldOfView(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2IsValidFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetProcessFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceConfigWithoutHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraDeviceOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetUacModeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraRemoveAppModuleFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDefectivePixelCancellation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Close(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetVideoSync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraWaitForLukeReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetDefectivePixelCancellationInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetDebugStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraIsConfigChangeDone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetVideoSync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraCloseByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/debt/libSceCamera.md b/docs/dev/debt/libSceCamera.md index d0346584a7..955b1e157c 100644 --- a/docs/dev/debt/libSceCamera.md +++ b/docs/dev/debt/libSceCamera.md @@ -7,3 +7,112 @@ ### Functional - [sceCameraIsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - no camera is ever attached: index 0 returns 0 (not attached) and any other index returns `SCE_CAMERA_ERROR_PARAM` (0x802E0000), as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera.cpp#L187-L200) does without a configured camera, with the error value from its [camera_error.h](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera_error.h). Not measured on a console. Closes when: a host camera is exposed to guests. + +### Additional PS5 3.20 imports + +- [sceCameraStopByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `+X1Kgnn3bzg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceIDWithoutOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `-H3UwGQvNZI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `0btIPD5hg5A`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraInitializeRegistryCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `0wnf2a60FqI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `1-IJHxzRJGw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Finalize](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `186Jz869gsc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetConnectedDeviceAndDriverMode](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2dBOiluwiMw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2IsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2v21-m4gljU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2zCd8XDOe-Y`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetTrackerMode](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `3VJOpzKoIeM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `3i5MEzrC1pg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibDataForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `4IGO2LPD3h8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibDataFromDevice](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `5Oie5RArfWs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `66IVWcdNHyI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraIsConnectedForSystem](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `68S3GKWhY6o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8MEjogxPrv0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetRegister](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8MjO05qk5hA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetVideoSyncInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8WtmqmE4edw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetConfigInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `9+SNhbctk64`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAttributeInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `96F7zp1Xo+k`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `AfIDd+2ycTs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetMmapConnectedCount](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `B260o9pSzM8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DAycoVmY3Mw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DSOLCrc3Kh8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DhqqFiBU+6g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `FZkD-E6MNTQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibDataFromDeviceForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `H-7tWq8gfKs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `HX5524E5tMY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetVrCaptureInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `IAz2HgZQWzE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetCalibDataForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Jkz3xxMeyIM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `JtD7ZG45rdw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetLensCorrection](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `K7W7H4ZRwbc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `LEMk5cTHKEA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetProcessFocusByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `NVITuK83Z7o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `O5x-G9Rqwx4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ObIste7hqdk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `P-7MVfzvpsM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Initialize](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `PFRl8SmSW+4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `QI8GVJUy2ZY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAccGetData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `QhjrPkRPUZQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Qk6kEJ0RJlM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibrationData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `RHYJ7GKOSMg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `RTDOsWWqdME`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `T8jy0JWa210`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Stop](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `TZWR3p6XxXk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `TzkL-nUWfaQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `U8oNsRef+jI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioClose](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `UFonL7xopFM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetProductInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ULxbwqiYYuU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `WVox2rwGuSc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetLensCorrection](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `XqYRHc4aw3w`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraChangeAppModuleState](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Y0pCDajzkVQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `YlFCMjbyzh8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `a5xFueMZIMs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `bSKEi2PzzXI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `c6Fp9M1EXXc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `c9XZGDF1OcM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraStartByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `cLxF1QtHch0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Open](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `dmLUJh3bVTc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Start](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eGkcUia48ts`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetLensCorrectionInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eHa3vhGu2rQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraOpenByModuleId](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eTywOSWsEiI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetCalibrationData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `emZBXv7cuAI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioGetData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `fkZE7Hup2ro`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `gB+OkFvkSXE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `gJqqsinextg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hHA1frlMxYE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraWaitForEveReady](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hPVradh5wpo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetRegistryInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hawKak+Auw4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioGetData2](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hftC5A1C8OQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAppModuleFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `j5isFVIlZLk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetDefectivePixelCancellation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jMv40y2A23g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceID](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jTJCdyv9GLU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetForceActivate](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jeTpU0MqKU0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `k3zPIcgFNv0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetFieldOfView](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `kVWBITsSB3o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2IsValidFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lQ7rWWlOArI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetProcessFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lS0tM6n+Q5E`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lhEIsHzB8r4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `mLRzKdIDwiU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `n+R7PGJa6MI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceConfigWithoutHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `n+rFeP1XXyM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraDeviceOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `nBH6i2s4Glc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetUacModeInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `nnR7KAIDPv8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `oEi6vM-3E2c`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetRegister](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `olojYZKYiYs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `pfiqU2f6PQY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraRemoveAppModuleFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `py8p6kZcHmA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `qTPRMh4eY60`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDefectivePixelCancellation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `tslCukqFE+E`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Close](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `uBRW3tEoWWM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetVideoSync](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ubD+FitjqhA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraWaitForLukeReady](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `v9XTZHXoJyQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetDefectivePixelCancellationInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vER3cIMBHqI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vPi3gSzw79M`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vS20kteXHdw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetDebugStop](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vejouEusC7g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vx9MQYicBVQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraIsConfigChangeDone](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wQfd7kfRZvo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wgBMXJJA6K4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetVideoSync](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wpeyFwJ+UEI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioReset](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wyU98EXAYxU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `yfSdswDaElo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `zIKL4kZleuc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `zrIUDKZx0iE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraCloseByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ztqH5qNTpTk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` From 88432ab71d537ba857669b92f70cf1cd55181bfd Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 10:02:43 +0200 Subject: [PATCH 1198/1360] docs(libSceJpegEnc): note that the returned size is not confirmed on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceJpegEnc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceJpegEnc.md b/docs/dev/debt/libSceJpegEnc.md index e41f12ac23..28c35b84a2 100644 --- a/docs/dev/debt/libSceJpegEnc.md +++ b/docs/dev/debt/libSceJpegEnc.md @@ -2,4 +2,4 @@ ### Unimplemented functionality -- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - MJPEG mode throws; its encoding path is not implemented. Restart intervals are confirmed by the native validator, MCU counter and DRI writer: 0 disables restart markers, -1 selects one MCU row, and 1 through 65535 specify an MCU count. Other values return the invalid-parameter error. A successful call returns the JPEG's size, as a PS4 Pro (firmware 10.71) does; the size itself comes from this encoder, so it differs from the console's for the same image (the console 633 bytes, this encoder 650, for an 8x8 RGBA image at 4:2:0). +- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - MJPEG mode throws; its encoding path is not implemented. Restart intervals are confirmed by the native validator, MCU counter and DRI writer: 0 disables restart markers, -1 selects one MCU row, and 1 through 65535 specify an MCU count. Other values return the invalid-parameter error. A successful call returns the JPEG's size, as a PS4 Pro (firmware 10.71) does; this is not confirmed on a PS5; the size itself comes from this encoder, so it differs from the console's for the same image (the console 633 bytes, this encoder 650, for an 8x8 RGBA image at 4:2:0). From 5d3c0d741aea0914d41942633557b8d6b74e6a66 Mon Sep 17 00:00:00 2001 From: alfalfa-star <kosher-chive-take@duck.com> Date: Tue, 20 Oct 2026 18:16:19 +0200 Subject: [PATCH 1199/1360] perf(agc): memoize host import run bounds per lease snapshot Every import lookup walked the lease out from the containing range to find the run of continuing registry ranges, which is linear in the run length. That walk depends only on the lease snapshot, the containing range and the import window block, so its first and last index are now memoized per thread under a weak reference to the snapshot, the range index, the window and the window's low end. APS5_NO_RUN_MEMO walks every time. Everything that depends on other state runs on each call as before: the largest size, the budget, the failed blocks, the remembered refusals and the halving block search. importRunBounds returns the chosen indices, so sameRange, runCovers and the whole-run lookup in importAllocation compare against the lease without copying the run's range pointers. importRunFailureShrinkTests records a failed 8 MiB block between two lookups on an unchanged registry and requires the run to shrink to its 4 MiB blocks. importRunGrowthTests and importRunReplacedTests check that a run grows and shrinks when its ranges are registered or replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/GuestBufferMemory.cpp | 78 +++++++++++++---- .../libSceAgcDriver/tests/BdaResources.cpp | 86 +++++++++++++++++++ .../prx/libc/include/GuestAllocations.hpp | 1 + docs/dev/ENVIRONMENT.md | 1 + 4 files changed, 151 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c46494982e..5d08420912 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1255,9 +1255,44 @@ bool stillRefused(const HostImports& state, const ImportRefusal* refusal, std::u return refusal != nullptr && budget != 0 && budget <= refusal->budget && state.liveBytes >= refusal->liveBytes; } -ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address, std::uint64_t largest = std::numeric_limits<std::uint64_t>::max(), std::uint64_t budget = 0) { +struct RunBounds { + std::size_t first = 0; + std::size_t last = 0; + bool found = false; +}; + +std::pair<std::size_t, std::size_t> walkRun(const GuestAllocations::Lease& lease, std::size_t index, std::uint64_t low, std::uint64_t high) { + auto first = index; + while (first > 0 && lease[first - 1]->address >= low && continuesRun(*lease[first - 1], *lease[first])) --first; + auto last = index; + while (last + 1 < lease.size() && lease[last + 1]->address + lease[last + 1]->bytes <= high && continuesRun(*lease[last], *lease[last + 1])) ++last; + return {first, last}; +} + +std::pair<std::size_t, std::size_t> continuedRun(const GuestAllocations::Lease& lease, std::size_t index, std::uint64_t window, std::uint64_t low, std::uint64_t high) { + static const bool memo = std::getenv("APS5_NO_RUN_MEMO") == nullptr; + if (!memo) return walkRun(lease, index, low, high); + struct Memo { + std::weak_ptr<const GuestAllocations::Lease::Ranges> snapshot; + std::size_t index = 0; + std::uint64_t window = 0; + std::uint64_t low = 0; + std::pair<std::size_t, std::size_t> run; + }; + static thread_local std::array<Memo, 1024> memos{}; + const auto& snapshot = lease.Snapshot(); + const auto block = window != 0 ? low / window : 0; + const auto slot = (reinterpret_cast<std::uintptr_t>(snapshot.get()) / 16u ^ index * 0x9e3779b1u ^ block * 0x85ebca6bu) % memos.size(); + auto& entry = memos[slot]; + if (!entry.snapshot.owner_before(snapshot) && !snapshot.owner_before(entry.snapshot) && entry.index == index && entry.window == window && entry.low == low) return entry.run; + entry = Memo{snapshot, index, window, low, walkRun(lease, index, low, high)}; + return entry.run; +} + +RunBounds importRunBounds(const GuestAllocations::Lease& lease, std::uint64_t address, std::uint64_t largest = std::numeric_limits<std::uint64_t>::max(), std::uint64_t budget = 0) { const auto index = leasedIndexContaining(lease, address); if (index == lease.size()) return {}; + const RunBounds single{index, index, true}; const auto& containing = *lease[index]; const auto& state = Imports(); const auto window = importWindow.load(std::memory_order_relaxed); @@ -1266,14 +1301,11 @@ ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address, if (window != 0) { low = address / window * window; high = low + window; - if (containing.address < low || containing.address + containing.bytes > high) return leasedRun(lease, index, index); + if (containing.address < low || containing.address + containing.bytes > high) return single; } - auto first = index; - while (first > 0 && lease[first - 1]->address >= low && continuesRun(*lease[first - 1], *lease[first])) --first; - auto last = index; - while (last + 1 < lease.size() && lease[last + 1]->address + lease[last + 1]->bytes <= high && continuesRun(*lease[last], *lease[last + 1])) ++last; + const auto [first, last] = continuedRun(lease, index, window, low, high); const auto skipped = [&](std::size_t from, std::size_t to) { return rememberedRun(state.failedBlocks, lease, from, to) != nullptr || stillRefused(state, rememberedRun(state.refusals, lease, from, to), budget); }; - if (window == 0) return first == last || lease[last]->address + lease[last]->bytes - lease[first]->address > largest || skipped(first, last) ? leasedRun(lease, index, index) : leasedRun(lease, first, last); + if (window == 0) return first == last || lease[last]->address + lease[last]->bytes - lease[first]->address > largest || skipped(first, last) ? single : RunBounds{first, last, true}; const auto bounding = [&](std::size_t from, std::size_t to, std::uint64_t value, bool start) { const auto found = std::lower_bound(lease.begin() + static_cast<std::ptrdiff_t>(from), lease.begin() + static_cast<std::ptrdiff_t>(to) + 1, value, [start](const auto& range, std::uint64_t wanted) { return (start ? range->address : range->address + range->bytes) < wanted; }); const auto position = static_cast<std::size_t>(found - lease.begin()); @@ -1290,9 +1322,23 @@ ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address, if (blockFirst == lease.size() || blockLast == lease.size()) continue; if (blockFirst == blockLast) break; if (skipped(blockFirst, blockLast)) continue; - return leasedRun(lease, blockFirst, blockLast); + return {blockFirst, blockLast, true}; } - return leasedRun(lease, index, index); + return single; +} + +ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address, std::uint64_t largest = std::numeric_limits<std::uint64_t>::max(), std::uint64_t budget = 0) { + const auto bounds = importRunBounds(lease, address, largest, budget); + return bounds.found ? leasedRun(lease, bounds.first, bounds.last) : ImportRun{}; +} + +template<typename Entry> +bool sameRun(const Entry& entry, const GuestAllocations::Lease& lease, const RunBounds& bounds) { + if (!bounds.found || lease[bounds.first]->address != entry.base || lease[bounds.last]->address + lease[bounds.last]->bytes - entry.base != entry.bytes || bounds.last - bounds.first + 1 != entry.ranges.size()) return false; + for (std::size_t index = 0; index < entry.ranges.size(); ++index) { + if (entry.ranges[index].owner_before(lease[bounds.first + index]) || lease[bounds.first + index].owner_before(entry.ranges[index])) return false; + } + return true; } ImportRun exactRun(const GuestAllocations::Lease& lease, std::uint64_t base, std::uint64_t bytes) { @@ -1307,8 +1353,8 @@ ImportRun exactRun(const GuestAllocations::Lease& lease, std::uint64_t base, std } bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { - const auto run = importRun(lease, begin); - return !run.ranges.empty() && end <= run.base + run.bytes; + const auto bounds = importRunBounds(lease, begin); + return bounds.found && end <= lease[bounds.last]->address + lease[bounds.last]->bytes; } ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { @@ -1363,7 +1409,7 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) { bool sameRange(const ImportFailure& entry, const GuestAllocations::Lease& lease, std::uint64_t budget = 0) { const auto current = entry.ranges.size() == 1 ? leasedRangeOwner(lease, entry.base, entry.base + entry.bytes) : nullptr; if (current != nullptr && entry.bytes < current->bytes) return !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); - return sameRun(entry, importRun(lease, entry.base, std::numeric_limits<std::uint64_t>::max(), budget)); + return sameRun(entry, lease, importRunBounds(lease, entry.base, std::numeric_limits<std::uint64_t>::max(), budget)); } bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease, std::uint64_t budget = 0) { @@ -1371,7 +1417,8 @@ bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease, st const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes); return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); } - return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base, std::numeric_limits<std::uint64_t>::max(), budget)); + if (entry.joined) return sameRun(entry, joinedRun(lease, entry.base, entry.base + entry.bytes)); + return sameRun(entry, lease, importRunBounds(lease, entry.base, std::numeric_limits<std::uint64_t>::max(), budget)); } std::uint64_t effectiveUse(const HostImports& state, const HostImport& entry) { @@ -1586,12 +1633,13 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s ++counts.remembered; return nullptr; } - auto run = importRun(lease, address, largest, budget); + const auto bounds = importRunBounds(lease, address, largest, budget); // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the // first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below. if (fresh && end == 0) { - if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current; + if (const auto* current = findImport(state, address, bounds.found ? lease[bounds.last]->address + lease[bounds.last]->bytes : address + 1)) return current; } + auto run = bounds.found ? leasedRun(lease, bounds.first, bounds.last) : ImportRun{}; if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment); if (run.ranges.empty()) return nullptr; return importRunAt(context, state, std::move(run), false, address, lease, end, evict, requester); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 860ba51020..5d0a0bf828 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -237,6 +237,77 @@ void importSpanTests(const Context& context) { ::operator delete(block, std::align_val_t{piece}); } +void importRunGrowthTests(const Context& context) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(2 * piece, std::align_val_t{piece}); + auto* guest = static_cast<std::uint8_t*>(block); + const auto first = reinterpret_cast<std::uintptr_t>(block); + std::memset(block, 0x3c, 2 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](void* range, std::size_t bytes, bool add) { + GuestAllocations::Mutation mutation; + if (add) mutation.Add(range, bytes, true, false, true); + else mutation.Remove(range); + }; + const auto build = [&](std::size_t pieces, const std::string& step) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(first + 64, 16); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + leased.WriteBack(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + pieces * piece <= range.end; }), "the import of a run does not cover its " + std::to_string(pieces) + " registered pieces " + step); + }; + change(guest, piece, true); + build(1, "before its neighbour was registered"); + change(guest + piece, piece, true); + build(2, "after its neighbour was registered"); + change(guest, 0, false); + change(guest + piece, 0, false); + ::operator delete(block, std::align_val_t{piece}); +} + +void importRunReplacedTests(const Context& context) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(3 * piece, std::align_val_t{piece}); + auto* guest = static_cast<std::uint8_t*>(block); + const auto first = reinterpret_cast<std::uintptr_t>(block); + std::memset(block, 0x3c, 3 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + const auto add = [&](void* range, bool writable) { + GuestAllocations::Mutation mutation; + mutation.Add(range, piece, true, writable, true); + }; + const auto remove = [&](void* range) { + GuestAllocations::Mutation mutation; + mutation.Remove(range); + }; + const auto build = [&](std::size_t pieces, const std::string& step) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(first + 64, 16); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + leased.WriteBack(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + pieces * piece <= range.end; }), "the import of a run does not cover its " + std::to_string(pieces) + " registered pieces " + step); + }; + add(guest, true); + add(guest + piece, false); + add(guest + 2 * piece, true); + build(1, "while the read-only middle piece ends it"); + for (int round = 0; round < 4; ++round) { + remove(guest + piece); + add(guest + piece, round % 2 == 0); + build(round % 2 == 0 ? 3 : 1, "after the middle piece was replaced by one that " + std::string(round % 2 == 0 ? "continues" : "ends") + " the run, with as many ranges registered as before"); + } + remove(guest); + remove(guest + piece); + remove(guest + 2 * piece); + ::operator delete(block, std::align_val_t{piece}); +} + void spaceWritableWritesTests(const Context& context) { if (!AddressSpaceCounters().enabled) return; constexpr std::size_t piece = 65536; @@ -1331,6 +1402,18 @@ void importBlockFailureTests(const Context& context, const BdaTestAccess& access Require(window.Imports(0, PieceWindow::Window) == 1 && window.Block(0, PieceWindow::Window), "a block that failed was not tried again after one of its ranges was mapped again: " + blockText(window, 0, PieceWindow::Window)); } +void importRunFailureShrinkTests(const Context& context, const BdaTestAccess& access) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, PieceWindow::Window); + Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr && window.Block(0, PieceWindow::Window), "a window of registered pieces was not imported as one 8 MiB block: " + blockText(window, 0, PieceWindow::Window)); + ClearHostImports(window.importing.device); + access.limitImports(half); + Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr && window.Block(0, half), "a lookup on an unchanged registry whose 8 MiB block failed after an earlier lookup did not shrink to the 4 MiB block: " + blockText(window, 0, PieceWindow::Window)); + access.limitImports(std::nullopt); + Require(HostImportFor(window.importing, window.Address(half), 16) != nullptr && window.Block(0, half) && window.Block(half, half), "a lookup on an unchanged registry after its 8 MiB block failed imported the failed block again: " + blockText(window, 0, PieceWindow::Window)); +} + void importBlockBudgetTests(const Context& context) { PieceWindow window(context); constexpr std::size_t MiB = std::size_t{1} << 20u; @@ -3482,6 +3565,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { addressTableKeyTests(); snapshotTableTests(context, access); importSpanTests(context); + importRunGrowthTests(context); + importRunReplacedTests(context); failedImportRemapTests(context, access); spaceImportRunTests(context); spaceWritableWritesTests(context); @@ -3513,6 +3598,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importBlockFlagTests(context); importBlockRemapTests(context); importBlockFailureTests(context, access); + importRunFailureShrinkTests(context, access); importBlockBudgetTests(context); importRefusalMemoryTests(context); evictingRefusalMemoryTests(context); diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 7811725142..c94fd68bd1 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -40,6 +40,7 @@ public: const value_type& front() const { return held().front(); } const value_type& back() const { return held().back(); } void clear() { ranges.reset(); } + const std::shared_ptr<const Ranges>& Snapshot() const { return ranges; } private: const Ranges& held() const { diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index e56ecbaa7f..e43c2a2236 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -294,6 +294,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_RESIDENT_PRESENT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | always presents through guest memory instead of the resident image | | `APS5_NO_RESIDENT_TARGETS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | copies every render target in and out again instead of keeping it resident | | `APS5_NO_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the shared resource cache, so every dispatch builds its resources fresh | +| `APS5_NO_RUN_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | walks the lease for a host import's run of ranges on every lookup instead of reusing the bounds found for the same lease snapshot and import window block | | `APS5_NO_SAMPLED_FROM_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps every sampled texture a CPU snapshot instead of viewing the surface's storage image | | `APS5_NO_SAMPLER_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | creates a sampler per binding instead of caching them | | `APS5_NO_SEPARATE_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a queued label enter the recorded table and replace a recorded entry of its dword | From 9f8343519f3d9eb88a5b34b6184e00215993b182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 10:04:41 +0200 Subject: [PATCH 1200/1360] fix(agc): keep the import run memo in host thread-local storage Co-Authored-By: Claude <noreply@anthropic.com> --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 5d08420912..a69a01f640 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -12,6 +12,7 @@ #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libc/include/general/AtomicSharedPtr.hpp" #ifdef _WIN32 #include <windows.h> @@ -1279,7 +1280,8 @@ std::pair<std::size_t, std::size_t> continuedRun(const GuestAllocations::Lease& std::uint64_t low = 0; std::pair<std::size_t, std::size_t> run; }; - static thread_local std::array<Memo, 1024> memos{}; + struct MemoStorage {}; + auto& memos = HostThreadLocal<std::array<Memo, 1024>, MemoStorage>(); const auto& snapshot = lease.Snapshot(); const auto block = window != 0 ? low / window : 0; const auto slot = (reinterpret_cast<std::uintptr_t>(snapshot.get()) / 16u ^ index * 0x9e3779b1u ^ block * 0x85ebca6bu) % memos.size(); From 200d7e18ca64b40b510cd50a367ee46b2f57aaee Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:09:07 +0300 Subject: [PATCH 1201/1360] feat(libSceGnmDriverForNeoMode): write the direct draw and ding-dong marker packets --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 7b715efbb1..ee1c358dbc 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -31,6 +31,21 @@ constexpr int Failure = static_cast<int>(0x8EEE00FFu); constexpr int NoInterface = static_cast<int>(0x80000000u); constexpr int ValidationNotEnabled = static_cast<int>(0x80D13FFFu); +constexpr int CommandFailed = -1; +constexpr std::uint32_t OpNop = 0x10; +constexpr std::uint32_t NeoDrawFlags = 0xe0000000u; +constexpr std::uint32_t AutoIndex = 2; + +constexpr std::uint32_t Header(std::uint32_t opcode, std::uint32_t bodyWords, bool compute = false, bool predicated = false) { + return 0xc0000000u | ((bodyWords - 1) << 16) | (opcode << 8) | (compute ? 2u : 0u) | (predicated ? 1u : 0u); +} + +std::uint32_t* Nop(std::uint32_t* at, std::uint32_t bodyWords) { + at[0] = Header(OpNop, bodyWords); + at[1] = 0; + return at + 1 + bodyWords; +} + } // namespace extern "C" { @@ -88,13 +103,24 @@ int APS5_VABI sceGnmDispatchIndirectOnMec(void) { return 0; } -int APS5_VABI sceGnmDrawIndex(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndex(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t indexCount, std::uint64_t indexAddress, std::uint32_t flags, std::uint32_t) { + if (!cmdbuf || size != 10 || indexAddress == 0 || (indexAddress & 1) != 0 || (flags & 0x1ffffffeu) != 0) return CommandFailed; + cmdbuf[0] = Header(0x27, 5); + cmdbuf[1] = indexCount; + cmdbuf[2] = static_cast<std::uint32_t>(indexAddress); + cmdbuf[3] = static_cast<std::uint32_t>(indexAddress >> 32); + cmdbuf[4] = indexCount; + cmdbuf[5] = flags & NeoDrawFlags; + Nop(cmdbuf + 6, 3); return 0; } -int APS5_VABI sceGnmDrawIndexAuto(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndexAuto(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t indexCount, std::uint32_t flags) { + if (!cmdbuf || size != 7 || (flags & 0x1ffffffeu) != 0) return CommandFailed; + cmdbuf[0] = Header(0x2d, 2); + cmdbuf[1] = indexCount; + cmdbuf[2] = (flags & NeoDrawFlags) | AutoIndex; + Nop(cmdbuf + 3, 3); return 0; } @@ -118,8 +144,14 @@ int APS5_VABI sceGnmDrawIndexMultiInstanced(void) { return 0; } -int APS5_VABI sceGnmDrawIndexOffset(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndexOffset(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t indexOffset, std::uint32_t indexCount, std::uint32_t flags) { + if (!cmdbuf || size != 9) return CommandFailed; + cmdbuf[0] = Header(0x35, 4, false, flags & 1); + cmdbuf[1] = indexCount; + cmdbuf[2] = indexOffset; + cmdbuf[3] = indexCount; + cmdbuf[4] = flags & NeoDrawFlags; + Nop(cmdbuf + 5, 3); return 0; } @@ -321,8 +353,12 @@ int APS5_VABI sceGnmGpuPaDebugLeave(void) { return 0; } -int APS5_VABI sceGnmInsertDingDongMarker(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmInsertDingDongMarker(std::uint32_t* cmdbuf, std::uint32_t size) { + if (!cmdbuf || size != 4) return CommandFailed; + cmdbuf[0] = Header(OpNop, 3); + cmdbuf[1] = 0; + cmdbuf[2] = 0; + cmdbuf[3] = 0; return 0; } From f7bff684dd8b4e9bcea1ab53623e481635c5bfe8 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:09:07 +0300 Subject: [PATCH 1202/1360] feat(libSceGnmDriverForNeoMode): write the dispatch packets --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index ee1c358dbc..1dd06bf3fb 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -88,18 +88,34 @@ int APS5_VABI sceGnmDingDongForWorkload(void) { return 0; } -int APS5_VABI sceGnmDispatchDirect(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDispatchDirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t threadsX, std::uint32_t threadsY, std::uint32_t threadsZ, std::uint32_t flags) { + if (!cmdbuf || size != 9 || static_cast<std::int32_t>(threadsX | threadsY | threadsZ) < 0) return CommandFailed; + cmdbuf[0] = Header(0x15, 4, true, flags & 1); + cmdbuf[1] = threadsX; + cmdbuf[2] = threadsY; + cmdbuf[3] = threadsZ; + cmdbuf[4] = (flags & 0x18) + 1; + Nop(cmdbuf + 5, 3); return 0; } -int APS5_VABI sceGnmDispatchIndirect(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDispatchIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t flags) { + if (!cmdbuf || size != 7) return CommandFailed; + cmdbuf[0] = Header(0x16, 2, true, flags & 1); + cmdbuf[1] = dataOffset; + cmdbuf[2] = (flags & 0x18) + 1; + Nop(cmdbuf + 3, 3); return 0; } -int APS5_VABI sceGnmDispatchIndirectOnMec(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDispatchIndirectOnMec(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t args, std::uint32_t modifier) { + if (!cmdbuf || size != 8 || args == 0 || (args & 3) != 0) return CommandFailed; + cmdbuf[0] = Header(0x16, 3, true, modifier & 1); + cmdbuf[1] = static_cast<std::uint32_t>(args); + cmdbuf[2] = static_cast<std::uint32_t>(args >> 32); + cmdbuf[3] = (modifier & 0x18) | 1; + cmdbuf[4] = Header(OpNop, 3); + cmdbuf[5] = 0; return 0; } From 69c273a101cc7e825fde98e1a5f6334932cd1e0a Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:09:07 +0300 Subject: [PATCH 1203/1360] feat(libSceGnmDriverForNeoMode): write the indirect and multi draw packets --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 96 ++++++++++++++++--- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 1dd06bf3fb..7c2819e779 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -46,6 +46,26 @@ std::uint32_t* Nop(std::uint32_t* at, std::uint32_t bodyWords) { return at + 1 + bodyWords; } +constexpr std::uint32_t ShaderStageCount = 7; +constexpr std::uint32_t StageVs = 2; +constexpr std::uint32_t StageEs = 4; +constexpr std::uint32_t StageLs = 6; +constexpr std::uint32_t IndirectSgprBase[ShaderStageCount] = {0, 0, 0x4c, 0, 0xcc, 0, 0x14c}; +constexpr std::uint32_t DrawIndirectArgsBytes = 0x10; +constexpr std::uint32_t DrawIndexedIndirectArgsBytes = 0x14; + +std::uint32_t IndirectSgpr(std::uint32_t stage, std::uint32_t offset) { + return offset == 0 ? 0 : (offset & 0xffffu) + IndirectSgprBase[stage]; +} + +bool ValidIndirect(std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr) { + return stage < ShaderStageCount && vertexSgpr < 0x10 && instanceSgpr < 0x10; +} + +bool IndexedIndirectStage(std::uint32_t stage) { + return stage == StageVs || stage == StageEs || stage == StageLs; +} + } // namespace extern "C" { @@ -140,18 +160,44 @@ int APS5_VABI sceGnmDrawIndexAuto(std::uint32_t* cmdbuf, std::uint32_t size, std return 0; } -int APS5_VABI sceGnmDrawIndexIndirect(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndexIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) { + if (!cmdbuf || size != 9 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed; + cmdbuf[0] = Header(0x25, 4, false, flags & 1); + cmdbuf[1] = dataOffset; + cmdbuf[2] = IndirectSgpr(stage, vertexSgpr); + cmdbuf[3] = IndirectSgpr(stage, instanceSgpr); + cmdbuf[4] = flags & NeoDrawFlags; + Nop(cmdbuf + 5, 3); return 0; } -int APS5_VABI sceGnmDrawIndexIndirectCountMulti(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndexIndirectCountMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint64_t countAddress, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) { + if (!cmdbuf || size != 0x10 || vertexSgpr >= 0x10 || instanceSgpr >= 0x10 || !IndexedIndirectStage(stage)) return CommandFailed; + auto* at = Nop(cmdbuf, 2); + at[0] = Header(0x9d, 9, false, flags & 1); + at[1] = dataOffset; + at[2] = IndirectSgpr(stage, vertexSgpr); + at[3] = IndirectSgpr(stage, instanceSgpr); + at[4] = countAddress != 0 ? 1u << 30 : 0; + at[5] = maxCount; + at[6] = static_cast<std::uint32_t>(countAddress); + at[7] = static_cast<std::uint32_t>(countAddress >> 32); + at[8] = DrawIndexedIndirectArgsBytes; + at[9] = flags & NeoDrawFlags; + Nop(at + 10, 2); return 0; } -int APS5_VABI sceGnmDrawIndexIndirectMulti(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndexIndirectMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) { + if (!cmdbuf || size != 11 || vertexSgpr >= 0x10 || instanceSgpr >= 0x10 || !IndexedIndirectStage(stage)) return CommandFailed; + cmdbuf[0] = Header(0x38, 6, false, flags & 1); + cmdbuf[1] = dataOffset; + cmdbuf[2] = IndirectSgpr(stage, vertexSgpr); + cmdbuf[3] = IndirectSgpr(stage, instanceSgpr); + cmdbuf[4] = maxCount; + cmdbuf[5] = DrawIndexedIndirectArgsBytes; + cmdbuf[6] = flags & NeoDrawFlags; + Nop(cmdbuf + 7, 3); return 0; } @@ -171,18 +217,44 @@ int APS5_VABI sceGnmDrawIndexOffset(std::uint32_t* cmdbuf, std::uint32_t size, s return 0; } -int APS5_VABI sceGnmDrawIndirect(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) { + if (!cmdbuf || size != 9 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed; + cmdbuf[0] = Header(0x24, 4, false, flags & 1); + cmdbuf[1] = dataOffset; + cmdbuf[2] = IndirectSgpr(stage, vertexSgpr); + cmdbuf[3] = IndirectSgpr(stage, instanceSgpr); + cmdbuf[4] = (flags & NeoDrawFlags) | AutoIndex; + Nop(cmdbuf + 5, 3); return 0; } -int APS5_VABI sceGnmDrawIndirectCountMulti(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndirectCountMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint64_t countAddress, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) { + if (!cmdbuf || size != 0x10 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed; + auto* at = Nop(cmdbuf, 2); + at[0] = Header(0x9c, 9, false, flags & 1); + at[1] = dataOffset; + at[2] = IndirectSgpr(stage, vertexSgpr); + at[3] = IndirectSgpr(stage, instanceSgpr); + at[4] = countAddress != 0 ? 1u << 30 : 0; + at[5] = maxCount; + at[6] = static_cast<std::uint32_t>(countAddress); + at[7] = static_cast<std::uint32_t>(countAddress >> 32); + at[8] = DrawIndirectArgsBytes; + at[9] = (flags & NeoDrawFlags) | AutoIndex; + Nop(at + 10, 2); return 0; } -int APS5_VABI sceGnmDrawIndirectMulti(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmDrawIndirectMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) { + if (!cmdbuf || size != 11 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed; + cmdbuf[0] = Header(0x2c, 6, false, flags & 1); + cmdbuf[1] = dataOffset; + cmdbuf[2] = IndirectSgpr(stage, vertexSgpr); + cmdbuf[3] = IndirectSgpr(stage, instanceSgpr); + cmdbuf[4] = maxCount; + cmdbuf[5] = DrawIndirectArgsBytes; + cmdbuf[6] = (flags & NeoDrawFlags) | AutoIndex; + Nop(cmdbuf + 7, 3); return 0; } From 4a080ff52503de3f52e6d27ab4d6dec9999ecb5d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:09:07 +0300 Subject: [PATCH 1204/1360] feat(libSceGnmDriverForNeoMode): write the compute semaphore and address wait packets --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 7c2819e779..cc389b98fd 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -75,13 +75,28 @@ int APS5_VABI sceGnmBeginWorkload(void) { return 0; } -int APS5_VABI sceGnmComputeWaitOnAddress(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmComputeWaitOnAddress(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t address, std::uint32_t mask, std::uint32_t compare, std::uint32_t reference) { + if (!cmdbuf || size != 14) return CommandFailed; + auto* at = Nop(cmdbuf, 3); + const bool memory = address > 0xffff; + at[0] = Header(0x3c, 6); + at[1] = (memory ? 1u << 4 : 0u) | (compare & 7); + at[2] = static_cast<std::uint32_t>(address & (memory ? 0xfffffffcu : 0xffffu)); + at[3] = static_cast<std::uint32_t>(address >> 32); + at[4] = reference; + at[5] = mask; + at[6] = 10; + Nop(at + 7, 2); return 0; } -int APS5_VABI sceGnmComputeWaitSemaphore(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmComputeWaitSemaphore(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t semaphoreAddress, std::uint32_t flags) { + if (!cmdbuf || size != 10) return CommandFailed; + auto* at = Nop(cmdbuf, 3); + at[0] = Header(0x39, 2); + at[1] = static_cast<std::uint32_t>(semaphoreAddress); + at[2] = (flags == 1 ? 0x100000u : 0u) - 0x20000000u + static_cast<std::uint32_t>(semaphoreAddress >> 32); + Nop(at + 3, 2); return 0; } From 9c618fbc8691f5737bc63c665908c5ae1eb5376d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:09:07 +0300 Subject: [PATCH 1205/1360] feat(libSceGnmDriverForNeoMode): set and reset the VGT control register --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index cc389b98fd..b817e55c45 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -66,6 +66,9 @@ bool IndexedIndirectStage(std::uint32_t stage) { return stage == StageVs || stage == StageEs || stage == StageLs; } +constexpr std::uint32_t OpSetUconfigReg = 0x79; +constexpr std::uint32_t IaMultiVgtParam = 0x40000258u; + } // namespace extern "C" { @@ -535,8 +538,11 @@ int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void) { return 0; } -int APS5_VABI sceGnmResetVgtControl(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmResetVgtControl(std::uint32_t* cmdbuf, std::uint32_t size) { + if (!cmdbuf || size != 3) return CommandFailed; + cmdbuf[0] = Header(OpSetUconfigReg, 2); + cmdbuf[1] = IaMultiVgtParam; + cmdbuf[2] = 0x6d007fu; return 0; } @@ -608,8 +614,16 @@ int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) { return Failure; } -int APS5_VABI sceGnmSetVgtControl(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGnmSetVgtControl(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t primGroupSizeMinusOne, std::uint32_t partialVsWaveMode, std::uint32_t switchOnlyOnEop) { + if (!cmdbuf || size != 3 || primGroupSizeMinusOne >= 0x100 || (switchOnlyOnEop | partialVsWaveMode) >= 2) return CommandFailed; + const std::uint32_t switchOnEop = static_cast<std::uint32_t>(switchOnlyOnEop != 0) << 20; + const std::uint32_t switchOnEoi = static_cast<std::uint32_t>(switchOnlyOnEop == 0) << 19; + const std::uint32_t value = switchOnlyOnEop != 0 + ? ((partialVsWaveMode & 1) << 16) | primGroupSizeMinusOne | switchOnEop | switchOnEoi | 0x40000u + : (primGroupSizeMinusOne & 0x1cffffu) | switchOnEop | switchOnEoi | 0x50000u; + cmdbuf[0] = Header(OpSetUconfigReg, 2); + cmdbuf[1] = IaMultiVgtParam; + cmdbuf[2] = value | 0x600000u; return 0; } From be98265ab04894ac12fb077bcfc8096403c31711 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:10:50 +0300 Subject: [PATCH 1206/1360] test(libSceGnmDriverForNeoMode): check the draw, dispatch, wait and VGT packets word by word --- core/libs/CMakeLists.txt | 10 ++ core/libs/tests/GuestGnmNeoPackets.cpp | 155 +++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 core/libs/tests/GuestGnmNeoPackets.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 241d44a14a..5e198e8f1f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -338,6 +338,16 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_barrier PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") endif() + + add_test_executable(guest_gnm_neo_packets_tests tests/GuestGnmNeoPackets.cpp) + target_include_directories(guest_gnm_neo_packets_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_gnm_neo_packets_tests PRIVATE libSceGnmDriverForNeoMode libc) + configure_windows_unwind(guest_gnm_neo_packets_tests) + add_test(NAME guest_gnm_neo_packets COMMAND guest_gnm_neo_packets_tests) + if(WIN32) + set_tests_properties(guest_gnm_neo_packets PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>") + endif() if(APPLE) add_subdirectory(tests/write_watch) add_subdirectory(tests/guest_arena) diff --git a/core/libs/tests/GuestGnmNeoPackets.cpp b/core/libs/tests/GuestGnmNeoPackets.cpp new file mode 100644 index 0000000000..984bc33dad --- /dev/null +++ b/core/libs/tests/GuestGnmNeoPackets.cpp @@ -0,0 +1,155 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdlib> +#include <initializer_list> + +extern "C" { +int APS5_VABI sceGnmInsertDingDongMarker(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawIndexAuto(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndex(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndexOffset(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDispatchDirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDispatchIndirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDispatchIndirectOnMec(std::uint32_t*, std::uint32_t, std::uint64_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndexIndirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndirectMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndexIndirectMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndirectCountMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmDrawIndexIndirectCountMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmComputeWaitSemaphore(std::uint32_t*, std::uint32_t, std::uint64_t, std::uint32_t); +int APS5_VABI sceGnmComputeWaitOnAddress(std::uint32_t*, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmSetVgtControl(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceGnmResetVgtControl(std::uint32_t*, std::uint32_t); +} + +namespace { + +constexpr std::uint32_t Untouched = 0xa5a5a5a5u; +constexpr std::uint32_t U = Untouched; + +std::uint32_t g_words[16]; + +void Require(bool value) { if (!value) std::abort(); } + +std::uint32_t* Fresh() { + for (auto& word : g_words) word = Untouched; + return g_words; +} + +void RequireWords(std::initializer_list<std::uint32_t> expected) { + std::size_t at = 0; + for (const auto word : expected) Require(g_words[at++] == word); + for (; at < 16; ++at) Require(g_words[at] == Untouched); +} + +void RequireRefused(int result) { + Require(result == -1); + RequireWords({}); +} + +void CheckDirectDraws() { + Require(sceGnmInsertDingDongMarker(Fresh(), 4) == 0); + RequireWords({0xc0021000u, 0, 0, 0}); + RequireRefused(sceGnmInsertDingDongMarker(Fresh(), 3)); + Require(sceGnmInsertDingDongMarker(nullptr, 4) == -1); + + Require(sceGnmDrawIndexAuto(Fresh(), 7, 36, 0xe0000001u) == 0); + RequireWords({0xc0012d00u, 36, 0xe0000002u, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndexAuto(Fresh(), 7, 36, 2)); + RequireRefused(sceGnmDrawIndexAuto(Fresh(), 6, 36, 0)); + + Require(sceGnmDrawIndex(Fresh(), 10, 99, 0x123456789abcull, 0x20000000u, 0) == 0); + RequireWords({0xc0042700u, 99, 0x56789abcu, 0x1234, 99, 0x20000000u, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndex(Fresh(), 10, 99, 0x1001, 0, 0)); + RequireRefused(sceGnmDrawIndex(Fresh(), 10, 99, 0, 0, 0)); + RequireRefused(sceGnmDrawIndex(Fresh(), 10, 99, 0x1000, 0x10, 0)); + + Require(sceGnmDrawIndexOffset(Fresh(), 9, 4, 12, 0x40000001u) == 0); + RequireWords({0xc0033501u, 12, 4, 12, 0x40000000u, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndexOffset(Fresh(), 10, 4, 12, 0)); +} + +void CheckDispatches() { + Require(sceGnmDispatchDirect(Fresh(), 9, 8, 4, 2, 0x19) == 0); + RequireWords({0xc0031503u, 8, 4, 2, 0x19, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDispatchDirect(Fresh(), 9, 0x80000000u, 1, 1, 0)); + RequireRefused(sceGnmDispatchDirect(Fresh(), 8, 1, 1, 1, 0)); + + Require(sceGnmDispatchIndirect(Fresh(), 7, 0x40, 0x8) == 0); + RequireWords({0xc0011602u, 0x40, 9, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDispatchIndirect(Fresh(), 9, 0x40, 0)); + + Require(sceGnmDispatchIndirectOnMec(Fresh(), 8, 0x700001000ull, 0x19) == 0); + RequireWords({0xc0021603u, 0x1000, 7, 0x19, 0xc0021000u, 0}); + RequireRefused(sceGnmDispatchIndirectOnMec(Fresh(), 8, 0x700001002ull, 0)); + RequireRefused(sceGnmDispatchIndirectOnMec(Fresh(), 8, 0, 0)); +} + +void CheckIndirectDraws() { + Require(sceGnmDrawIndirect(Fresh(), 9, 0x20, 2, 3, 5, 0x20000001u) == 0); + RequireWords({0xc0032401u, 0x20, 0x4f, 0x51, 0x20000002u, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndirect(Fresh(), 9, 0x20, 7, 0, 0, 0)); + RequireRefused(sceGnmDrawIndirect(Fresh(), 9, 0x20, 2, 0x10, 0, 0)); + + Require(sceGnmDrawIndexIndirect(Fresh(), 9, 0x60, 4, 1, 0, 0x1) == 0); + RequireWords({0xc0032501u, 0x60, 0xcd, 0, 0, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndexIndirect(Fresh(), 9, 0x60, 4, 0, 0x10, 0)); + + Require(sceGnmDrawIndirectMulti(Fresh(), 11, 0x30, 6, 6, 2, 0, 0x80000000u) == 0); + RequireWords({0xc0052c00u, 0x30, 0x14e, 0, 6, 0x10, 0x80000002u, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndirectMulti(Fresh(), 9, 0x30, 6, 6, 2, 0, 0)); + + Require(sceGnmDrawIndexIndirectMulti(Fresh(), 11, 0x30, 6, 2, 0, 1, 0x60000000u) == 0); + RequireWords({0xc0053800u, 0x30, 0, 0x4d, 6, 0x14, 0x60000000u, 0xc0021000u, 0, U, U}); + RequireRefused(sceGnmDrawIndexIndirectMulti(Fresh(), 11, 0x30, 6, 0, 0, 0, 0)); + RequireRefused(sceGnmDrawIndexIndirectMulti(Fresh(), 11, 0x30, 6, 3, 0, 0, 0)); + + Require(sceGnmDrawIndirectCountMulti(Fresh(), 16, 0, 5, 0x200000040ull, 2, 1, 1, 1) == 0); + RequireWords({0xc0011000u, 0, U, 0xc0089c01u, 0, 0x4d, 0x4d, 0x40000000u, 5, 0x40, 2, 0x10, 2, 0xc0011000u, 0, U}); + RequireRefused(sceGnmDrawIndirectCountMulti(Fresh(), 15, 0, 5, 0, 2, 1, 1, 0)); + + Require(sceGnmDrawIndexIndirectCountMulti(Fresh(), 16, 0x14, 3, 0, 6, 0, 0, 0xe0000000u) == 0); + RequireWords({0xc0011000u, 0, U, 0xc0089d00u, 0x14, 0, 0, 0, 3, 0, 0, 0x14, 0xe0000000u, 0xc0011000u, 0, U}); + RequireRefused(sceGnmDrawIndexIndirectCountMulti(Fresh(), 16, 0, 3, 0, 5, 0, 0, 0)); +} + +void CheckComputeWaits() { + Require(sceGnmComputeWaitSemaphore(Fresh(), 10, 0x123456780ull, 1) == 0); + RequireWords({0xc0021000u, 0, U, U, 0xc0013900u, 0x23456780u, 0xe0100001u, 0xc0011000u, 0, U}); + Require(sceGnmComputeWaitSemaphore(Fresh(), 10, 0x123456780ull, 0) == 0); + Require(g_words[6] == 0xe0000001u); + RequireRefused(sceGnmComputeWaitSemaphore(Fresh(), 9, 0, 0)); + + Require(sceGnmComputeWaitOnAddress(Fresh(), 14, 0x500001003ull, 0xff, 0xb, 7) == 0); + RequireWords({0xc0021000u, 0, U, U, 0xc0053c00u, 0x13, 0x1000, 5, 7, 0xff, 10, 0xc0011000u, 0, U}); + Require(sceGnmComputeWaitOnAddress(Fresh(), 14, 0x1234, 1, 3, 0) == 0); + RequireWords({0xc0021000u, 0, U, U, 0xc0053c00u, 3, 0x1234, 0, 0, 1, 10, 0xc0011000u, 0, U}); + RequireRefused(sceGnmComputeWaitOnAddress(Fresh(), 13, 0x1234, 1, 3, 0)); +} + +void CheckVgtControl() { + Require(sceGnmSetVgtControl(Fresh(), 3, 0x7f, 1, 1) == 0); + RequireWords({0xc0017900u, 0x40000258u, 0x75007fu}); + Require(sceGnmSetVgtControl(Fresh(), 3, 0x10, 1, 0) == 0); + RequireWords({0xc0017900u, 0x40000258u, 0x6d0010u}); + RequireRefused(sceGnmSetVgtControl(Fresh(), 3, 0x100, 0, 0)); + RequireRefused(sceGnmSetVgtControl(Fresh(), 3, 0, 2, 0)); + RequireRefused(sceGnmSetVgtControl(Fresh(), 3, 0, 0, 2)); + RequireRefused(sceGnmSetVgtControl(Fresh(), 4, 0, 0, 0)); + + Require(sceGnmResetVgtControl(Fresh(), 3) == 0); + RequireWords({0xc0017900u, 0x40000258u, 0x6d007fu}); + RequireRefused(sceGnmResetVgtControl(Fresh(), 2)); + Require(sceGnmResetVgtControl(nullptr, 3) == -1); +} + +} // namespace + +int main() { + CheckDirectDraws(); + CheckDispatches(); + CheckIndirectDraws(); + CheckComputeWaits(); + CheckVgtControl(); +} From f0c2cc4879dfca5bbed9bce32e5f3bfd904bdd5d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 18:11:28 +0300 Subject: [PATCH 1207/1360] docs(debt): record the libSceGnmDriverForNeoMode command packets --- docs/dev/debt/libSceGnmDriverForNeoMode.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 44ebce9512..4d80911108 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,9 +2,10 @@ ### Unknown function info -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 65 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 48 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read - [default state packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmDrawInitDefaultHardwareState`, `sceGnmDrawInitDefaultHardwareState200`, `sceGnmDrawInitDefaultHardwareState350`, `sceGnmDrawInitToDefaultContextState` and `sceGnmDrawInitToDefaultContextState400` write the sequences in [InitSequences.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp), taken from [shadPS4's gnmdriver_init.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver_init.h): its Neo sequences, except the plain hardware state, which shadPS4 also writes unchanged in Neo mode. The size checks and returned sizes are shadPS4's. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet +- [draw, dispatch, wait and VGT packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the signatures, size checks, PM4 words and refusals (-1) of the 17 command writers follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) on its Neo-mode path, since this module is the Neo-mode driver: draw initiators keep flag bits 29 to 31, and `sceGnmSetVgtControl` and `sceGnmResetVgtControl` write `IA_MULTI_VGT_PARAM` through `SET_UCONFIG_REG` (shadPS4's `UseNeoCompatSequences` is false). The packets are not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet From 380867d17389c57b5ffe7d0d503dcdee7467da3c Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:35:39 +0300 Subject: [PATCH 1208/1360] feat(libkernel_sys): read the title workaround bits --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 13 +++++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index cf23edd839..137856a12e 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -2101,11 +2101,6 @@ int APS5_VABI sceKernelTerminateSysCore(void) { return 0; } -int APS5_VABI sceKernelTitleWorkaroundIsEnabled(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelTitleWorkdaroundIsEnabled(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 53ab5bc867..dc13eed96e 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -786,4 +786,17 @@ void APS5_VABI scePthreadSetName(void* thread, const char* name) { pthread_set_name_np_nid_postfix(thread, name); } +struct KernelTitleWorkaround { + std::int32_t version; + std::int32_t align; + std::uint64_t ids[2]; +}; + +int APS5_VABI sceKernelTitleWorkaroundIsEnabled(const KernelTitleWorkaround* workaround, std::int32_t bit, std::int32_t* enabled) { + if (!workaround || !enabled) return static_cast<int>(0x8002000E); + if (static_cast<std::uint32_t>(bit) >= 0x3A) return static_cast<int>(0x80020016); + *enabled = static_cast<std::int32_t>((workaround->ids[bit >> 6] >> (bit & 0x3F)) & 1); + return 0; +} + } From c9828c6d3ad61ef47c35b63bc27f998075bc7781 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:35:39 +0300 Subject: [PATCH 1209/1360] feat(libkernel_sys): report the big-application process type for the title's own pid --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 137856a12e..c1bbfba101 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1102,11 +1102,6 @@ int APS5_VABI sceKernelGetProcessName(void) { return 0; } -int APS5_VABI sceKernelGetProcessType(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetProcessTypeOfBudget(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index dc13eed96e..41cb9378a3 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -41,6 +41,7 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getpid_nid_postfix(); namespace { #ifdef _WIN32 @@ -799,4 +800,9 @@ int APS5_VABI sceKernelTitleWorkaroundIsEnabled(const KernelTitleWorkaround* wor return 0; } +int APS5_VABI sceKernelGetProcessType(int pid) { + if (pid != getpid_nid_postfix() && pid != -1 && pid != 0) return static_cast<int>(0x80020003); + return 0; +} + } From 4cbcf1fa23653c2063a34befe7cbb1a8843be601 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:35:39 +0300 Subject: [PATCH 1210/1360] test(libkernel_sys): cover the workaround bit limit and the process type pids --- core/libs/tests/GuestKernelSys.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index b481f508fe..b7808d5641 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -43,6 +43,9 @@ int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); int APS5_VABI chroot_nid_postfix(const char*); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +int APS5_VABI getpid_nid_postfix(); +int APS5_VABI sceKernelTitleWorkaroundIsEnabled(const void*, std::int32_t, std::int32_t*); +int APS5_VABI sceKernelGetProcessType(int); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelRename(const char*, const char*); @@ -263,12 +266,34 @@ void DirectoryRenameTests(const std::filesystem::path& parent) { Require(std::filesystem::remove(original) && std::filesystem::remove(moved) && std::filesystem::remove(fixture)); } +static void CheckProcessQueries() { + struct { + std::int32_t version; + std::int32_t align; + std::uint64_t ids[2]; + } workaround{1, 0, {(1ull << 3) | (1ull << 0x39), ~0ull}}; + std::int32_t enabled = -1; + Require(sceKernelTitleWorkaroundIsEnabled(nullptr, 3, &enabled) == static_cast<int>(0x8002000E)); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 3, nullptr) == static_cast<int>(0x8002000E)); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 0x3A, &enabled) == static_cast<int>(0x80020016) && enabled == -1); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, -1, &enabled) == static_cast<int>(0x80020016) && enabled == -1); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 3, &enabled) == 0 && enabled == 1); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 4, &enabled) == 0 && enabled == 0); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 63, &enabled) == static_cast<int>(0x80020016)); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 0x39, &enabled) == 0 && enabled == 1); + Require(sceKernelTitleWorkaroundIsEnabled(&workaround, 0x38, &enabled) == 0 && enabled == 0); + Require(sceKernelGetProcessType(getpid_nid_postfix()) == 0); + Require(sceKernelGetProcessType(-1) == 0 && sceKernelGetProcessType(0) == 0); + Require(sceKernelGetProcessType(getpid_nid_postfix() + 1) == static_cast<int>(0x80020003)); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); CharUpperBuffW(hostDirectorySpelling.data(), static_cast<DWORD>(hostDirectorySpelling.size())); Require(SetCurrentDirectoryW(hostDirectorySpelling.c_str()) != 0); #endif + CheckProcessQueries(); Require(setsid_nid_postfix() == -1 && Error() == 1); Require(umask_nid_postfix(0) == 022); Require(umask_nid_postfix(07777) == 0); From fadfb4433d85b4b8df96a2857d365fba305da1a1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Wed, 21 Oct 2026 13:35:39 +0300 Subject: [PATCH 1211/1360] docs(debt): note where the workaround and process type rules come from --- docs/dev/debt/libkernel_sys.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8a7b58c9c3..b799d3ce23 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -26,6 +26,8 @@ - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 +- [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids` +- [sceKernelGetProcessType](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; as in shadPS4, the title's own pid, -1 and 0 are type 0 (big application) and any other pid is `ESRCH` ### Unknown function info From 22823182ef8d483a512660be257787994892c138 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar <yadgar360@gmail.com> Date: Thu, 22 Oct 2026 08:21:41 +0300 Subject: [PATCH 1212/1360] docs(debt): note that sceKernelGetAppInfo leaves the workaround block unwritten --- docs/dev/debt/libkernel_sys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index b799d3ce23..453ae547a1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -26,7 +26,7 @@ - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 -- [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids` +- [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids`. Titles usually pass the `title_workaround` block of `sceKernelGetAppInfo`, which does not write that block here, so the bits read are whatever the caller's memory held - [sceKernelGetProcessType](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; as in shadPS4, the title's own pid, -1 and 0 are type 0 (big application) and any other pid is `ESRCH` ### Unknown function info From 40970647ca1d579116c12fbc62b400a283c3f807 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:40:04 +0100 Subject: [PATCH 1213/1360] feat(libSceNpProfileDialog): implement the dialog lifecycle Initialize, Terminate, UpdateStatus, GetStatus, Open, OpenA, Close and GetResult run the common-dialog state machine. There is no PSN profile UI, so Open and OpenA finish at once and GetResult reports result 0, userAction USER_CANCELED and the caller's userData, as shadPS4 does when its window is dismissed. Paths where shadPS4 and the other common dialogs here return different errors throw, and so does a param whose size or mode does not match the shadPS4 layout. The common-dialog codes the library uses are added to libSceCommonDialog/CommonDialog.hpp. --- .../prx/libSceNpProfileDialog/CMakeLists.txt | 16 ++ .../libs/prx/libSceNpProfileDialog/Export.cpp | 175 +++++++++++++++--- core/libs/tests/GuestNpProfileDialog.cpp | 169 +++++++++++++++++ docs/dev/debt/libSceNpProfileDialog.md | 11 +- 4 files changed, 346 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceNpProfileDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestNpProfileDialog.cpp diff --git a/core/libs/prx/libSceNpProfileDialog/CMakeLists.txt b/core/libs/prx/libSceNpProfileDialog/CMakeLists.txt new file mode 100644 index 0000000000..6406591808 --- /dev/null +++ b/core/libs/prx/libSceNpProfileDialog/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpProfileDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpProfileDialog PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNpProfileDialog PRIVATE libc) +configure_windows_unwind(libSceNpProfileDialog) + +if(BUILD_TESTING) + add_test_executable(guest_np_profile_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpProfileDialog.cpp) + target_include_directories(guest_np_profile_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_profile_dialog_tests PRIVATE libSceNpProfileDialog libc) + configure_windows_unwind(guest_np_profile_dialog_tests) + add_test(NAME guest_np_profile_dialog COMMAND guest_np_profile_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_profile_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpProfileDialog>") + endif() +endif() diff --git a/core/libs/prx/libSceNpProfileDialog/Export.cpp b/core/libs/prx/libSceNpProfileDialog/Export.cpp index b0dd450371..a9112db530 100644 --- a/core/libs/prx/libSceNpProfileDialog/Export.cpp +++ b/core/libs/prx/libSceNpProfileDialog/Export.cpp @@ -1,48 +1,175 @@ #include <cstdint> #include <cstddef> +#include <mutex> +#include <stdexcept> +#include <string> #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.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_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +constexpr std::uint32_t NP_PROFILE_DIALOG_MODE_NORMAL = 1; +constexpr std::uint32_t NP_PROFILE_DIALOG_MODE_GRIEF_REPORT = 4; + +struct SceCommonDialogBaseParam { + std::uint64_t size; + std::uint8_t reserved[36]; + std::uint32_t magic; +}; +static_assert(offsetof(SceCommonDialogBaseParam, magic) == 44); +static_assert(sizeof(SceCommonDialogBaseParam) == 48); + +struct SceNpProfileDialogParam { + SceCommonDialogBaseParam baseParam; + std::uint64_t size; + std::uint32_t mode; + std::int32_t userId; + NpOnlineId targetOnlineId; + std::uint8_t padding[4]; + void* userData; + std::uint8_t reserved[32]; +}; +static_assert(offsetof(SceNpProfileDialogParam, size) == 48); +static_assert(offsetof(SceNpProfileDialogParam, mode) == 56); +static_assert(offsetof(SceNpProfileDialogParam, userId) == 60); +static_assert(offsetof(SceNpProfileDialogParam, targetOnlineId) == 64); +static_assert(offsetof(SceNpProfileDialogParam, userData) == 88); +static_assert(sizeof(SceNpProfileDialogParam) == 128); + +struct SceNpProfileDialogParamA { + SceCommonDialogBaseParam baseParam; + std::uint64_t size; + std::uint32_t mode; + std::int32_t userId; + std::uint64_t targetAccountId; + std::uint8_t padding[8]; + void* userData; + std::uint8_t reserved[32]; +}; +static_assert(offsetof(SceNpProfileDialogParamA, size) == 48); +static_assert(offsetof(SceNpProfileDialogParamA, mode) == 56); +static_assert(offsetof(SceNpProfileDialogParamA, userId) == 60); +static_assert(offsetof(SceNpProfileDialogParamA, targetAccountId) == 64); +static_assert(offsetof(SceNpProfileDialogParamA, userData) == 80); +static_assert(sizeof(SceNpProfileDialogParamA) == 120); + +struct SceNpProfileDialogResult { + std::int32_t result; + std::int32_t userAction; + void* userData; + std::uint8_t reserved[32]; +}; +static_assert(offsetof(SceNpProfileDialogResult, userAction) == 4); +static_assert(offsetof(SceNpProfileDialogResult, userData) == 8); +static_assert(offsetof(SceNpProfileDialogResult, reserved) == 16); +static_assert(sizeof(SceNpProfileDialogResult) == 48); + +struct DialogState { + std::mutex mutex; + int status = COMMON_DIALOG_STATUS_NONE; + void* userData = nullptr; +}; + +DialogState& Dialog() { + static DialogState state; + return state; +} + +[[noreturn]] void Unsettled(const char* function, const std::string& what) { + throw std::runtime_error(std::string(function) + ": " + what); +} + +template <typename TParam> +int OpenDialog(const TParam* param, const char* function) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status != COMMON_DIALOG_STATUS_INITIALIZED && dialog.status != COMMON_DIALOG_STATUS_FINISHED) { + Unsettled(function, "status " + std::to_string(dialog.status) + " is neither INITIALIZED nor FINISHED, and the error for it is unsettled"); + } + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (param->size != sizeof(TParam)) { + Unsettled(function, "param size " + std::to_string(param->size) + " is not the " + std::to_string(sizeof(TParam)) + "-byte layout this library reads"); + } + if (param->mode < NP_PROFILE_DIALOG_MODE_NORMAL || param->mode > NP_PROFILE_DIALOG_MODE_GRIEF_REPORT) { + Unsettled(function, "mode " + std::to_string(param->mode) + " is not a known profile dialog mode"); + } + dialog.userData = param->userData; + dialog.status = COMMON_DIALOG_STATUS_FINISHED; + return COMMON_DIALOG_OK; +} + +} + extern "C" { -int APS5_VABI sceNpProfileDialogClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogInitialize(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status != COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + dialog.status = COMMON_DIALOG_STATUS_INITIALIZED; + dialog.userData = nullptr; + return COMMON_DIALOG_OK; } -int APS5_VABI sceNpProfileDialogGetResult() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogTerminate(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + dialog.status = COMMON_DIALOG_STATUS_NONE; + dialog.userData = nullptr; + return COMMON_DIALOG_OK; } -int APS5_VABI sceNpProfileDialogGetStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogUpdateStatus(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + return dialog.status; } -int APS5_VABI sceNpProfileDialogInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogGetStatus(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + return dialog.status; } -int APS5_VABI sceNpProfileDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogOpen(const SceNpProfileDialogParam* param) { + return OpenDialog(param, __func__); } -int APS5_VABI sceNpProfileDialogOpenA() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogOpenA(const SceNpProfileDialogParamA* param) { + return OpenDialog(param, __func__); } -int APS5_VABI sceNpProfileDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogClose(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + Unsettled(__func__, "status " + std::to_string(dialog.status) + " is not RUNNING, and the error for closing a dialog that is not running is unsettled"); } -int APS5_VABI sceNpProfileDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpProfileDialogGetResult(SceNpProfileDialogResult* result) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (dialog.status != COMMON_DIALOG_STATUS_FINISHED) { + if (result == nullptr) Unsettled(__func__, "a null result before the dialog finished has an unsettled error"); + return COMMON_DIALOG_ERROR_NOT_FINISHED; + } + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + *result = SceNpProfileDialogResult{}; + result->result = COMMON_DIALOG_OK; + result->userAction = COMMON_DIALOG_RESULT_USER_CANCELED; + result->userData = dialog.userData; + return COMMON_DIALOG_OK; } } diff --git a/core/libs/tests/GuestNpProfileDialog.cpp b/core/libs/tests/GuestNpProfileDialog.cpp new file mode 100644 index 0000000000..a9d2986bfe --- /dev/null +++ b/core/libs/tests/GuestNpProfileDialog.cpp @@ -0,0 +1,169 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <stdexcept> +#include <string> + +extern "C" { +int APS5_VABI sceNpProfileDialogInitialize(void); +int APS5_VABI sceNpProfileDialogTerminate(void); +int APS5_VABI sceNpProfileDialogUpdateStatus(void); +int APS5_VABI sceNpProfileDialogGetStatus(void); +int APS5_VABI sceNpProfileDialogOpen(const void* param); +int APS5_VABI sceNpProfileDialogOpenA(const void* param); +int APS5_VABI sceNpProfileDialogClose(void); +int APS5_VABI sceNpProfileDialogGetResult(void* result); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast<int>(0x80B80003u); +constexpr int kErrAlreadyInitialized = static_cast<int>(0x80B80004u); +constexpr int kErrNotFinished = static_cast<int>(0x80B80005u); +constexpr int kErrArgNull = static_cast<int>(0x80B8000Du); +constexpr std::int32_t kResultUserCanceled = 1; + +constexpr std::size_t kParamSize = 128; +constexpr std::size_t kParamASize = 120; +constexpr std::size_t kParamUserData = 88; +constexpr std::size_t kParamAUserData = 80; +constexpr std::size_t kResultSize = 48; +constexpr std::uint8_t kSentinel = 0x5a; + +template <typename TValue> +void Store(std::uint8_t* bytes, std::size_t offset, TValue value) { + std::memcpy(bytes + offset, &value, sizeof(value)); +} + +template <typename TValue> +TValue Load(const std::uint8_t* bytes, std::size_t offset) { + TValue value; + std::memcpy(&value, bytes + offset, sizeof(value)); + return value; +} + +void MakeParam(std::uint8_t* bytes, std::size_t capacity, std::uint64_t size, std::uint32_t mode, std::size_t userDataOffset, void* userData) { + std::memset(bytes, 0, capacity); + Store<std::uint64_t>(bytes, 0, 48); + Store<std::uint64_t>(bytes, 48, size); + Store<std::uint32_t>(bytes, 56, mode); + Store<std::int32_t>(bytes, 60, 1); + Store<void*>(bytes, userDataOffset, userData); +} + +template <typename TCall> +void RequireThrows(TCall call) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("not implemented") == std::string::npos; + } + Require(threw); +} + +void RequireCanceled(const std::uint8_t* result, void* userData) { + Require(Load<std::int32_t>(result, 0) == 0); + Require(Load<std::int32_t>(result, 4) == kResultUserCanceled); + Require(Load<void*>(result, 8) == userData); + for (std::size_t i = 16; i < kResultSize; ++i) Require(result[i] == 0); + for (std::size_t i = kResultSize; i < kResultSize + 16; ++i) Require(result[i] == kSentinel); +} + +} + +int main() { + alignas(8) std::uint8_t param[kParamSize + 16]; + alignas(8) std::uint8_t paramA[kParamASize + 16]; + alignas(8) std::uint8_t result[kResultSize + 16]; + int first = 0; + int second = 0; + std::memset(result, kSentinel, sizeof(result)); + MakeParam(param, sizeof(param), kParamSize, 1, kParamUserData, &first); + MakeParam(paramA, sizeof(paramA), kParamASize, 1, kParamAUserData, &second); + + Require(sceNpProfileDialogTerminate() == kErrNotInitialized); + Require(sceNpProfileDialogClose() == kErrNotInitialized); + Require(sceNpProfileDialogGetResult(result) == kErrNotInitialized); + Require(sceNpProfileDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceNpProfileDialogGetStatus() == kStatusNone); + Require(sceNpProfileDialogUpdateStatus() == kStatusNone); + RequireThrows([&] { sceNpProfileDialogOpen(param); }); + RequireThrows([&] { sceNpProfileDialogOpenA(paramA); }); + Require(sceNpProfileDialogGetStatus() == kStatusNone); + Require(result[0] == kSentinel); + + Require(sceNpProfileDialogInitialize() == 0); + Require(sceNpProfileDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpProfileDialogGetStatus() == kStatusInitialized); + Require(sceNpProfileDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpProfileDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kSentinel); + RequireThrows([] { sceNpProfileDialogGetResult(nullptr); }); + RequireThrows([] { sceNpProfileDialogClose(); }); + Require(sceNpProfileDialogGetStatus() == kStatusInitialized); + + Require(sceNpProfileDialogOpen(nullptr) == kErrArgNull); + Require(sceNpProfileDialogOpenA(nullptr) == kErrArgNull); + Require(sceNpProfileDialogGetStatus() == kStatusInitialized); + + alignas(8) std::uint8_t wrong[kParamSize + 16]; + MakeParam(wrong, sizeof(wrong), kParamASize, 1, kParamUserData, &first); + RequireThrows([&] { sceNpProfileDialogOpen(wrong); }); + MakeParam(wrong, sizeof(wrong), kParamSize, 1, kParamAUserData, &second); + RequireThrows([&] { sceNpProfileDialogOpenA(wrong); }); + MakeParam(wrong, sizeof(wrong), kParamSize, 0, kParamUserData, &first); + RequireThrows([&] { sceNpProfileDialogOpen(wrong); }); + MakeParam(wrong, sizeof(wrong), kParamASize, 5, kParamAUserData, &second); + RequireThrows([&] { sceNpProfileDialogOpenA(wrong); }); + Require(sceNpProfileDialogGetStatus() == kStatusInitialized); + Require(sceNpProfileDialogGetResult(result) == kErrNotFinished); + + std::uint8_t paramCopy[sizeof(param)]; + std::memcpy(paramCopy, param, sizeof(param)); + Require(sceNpProfileDialogOpen(param) == 0); + Require(std::memcmp(param, paramCopy, sizeof(param)) == 0); + Require(sceNpProfileDialogGetStatus() == kStatusFinished); + Require(sceNpProfileDialogUpdateStatus() == kStatusFinished); + Require(sceNpProfileDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpProfileDialogGetResult(result) == 0); + RequireCanceled(result, &first); + RequireThrows([] { sceNpProfileDialogClose(); }); + Require(sceNpProfileDialogGetStatus() == kStatusFinished); + + for (std::uint32_t mode = 1; mode <= 4; ++mode) { + Store<std::uint32_t>(paramA, 56, mode); + std::uint8_t paramACopy[sizeof(paramA)]; + std::memcpy(paramACopy, paramA, sizeof(paramA)); + std::memset(result, kSentinel, sizeof(result)); + Require(sceNpProfileDialogOpenA(paramA) == 0); + Require(std::memcmp(paramA, paramACopy, sizeof(paramA)) == 0); + Require(sceNpProfileDialogUpdateStatus() == kStatusFinished); + Require(sceNpProfileDialogGetResult(result) == 0); + RequireCanceled(result, &second); + } + + Require(sceNpProfileDialogTerminate() == 0); + Require(sceNpProfileDialogTerminate() == kErrNotInitialized); + Require(sceNpProfileDialogGetStatus() == kStatusNone); + Require(sceNpProfileDialogGetResult(result) == kErrNotInitialized); + + Require(sceNpProfileDialogInitialize() == 0); + std::memset(result, kSentinel, sizeof(result)); + Require(sceNpProfileDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kSentinel); + Store<std::uint32_t>(param, 56, 4); + Require(sceNpProfileDialogOpen(param) == 0); + Require(sceNpProfileDialogGetResult(result) == 0); + RequireCanceled(result, &first); + Require(sceNpProfileDialogTerminate() == 0); + return 0; +} diff --git a/docs/dev/debt/libSceNpProfileDialog.md b/docs/dev/debt/libSceNpProfileDialog.md index 2755456503..54a1f1b591 100644 --- a/docs/dev/debt/libSceNpProfileDialog.md +++ b/docs/dev/debt/libSceNpProfileDialog.md @@ -1,5 +1,14 @@ # libSceNpProfileDialog technical debt +### Silent stubs + +- [sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogGetResult](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - no profile is shown: `Open` and `OpenA` move the dialog straight to `FINISHED`, and `GetResult` reports `result` 0, `userAction` `USER_CANCELED` (1) and the caller's `userData`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_profile_dialog/np_profile_dialog.cpp#L21-L180) does when its window is dismissed. Not measured on a console. + ### Unknown function info -- [sceNpProfileDialogClose, sceNpProfileDialogGetResult, sceNpProfileDialogGetStatus, sceNpProfileDialogInitialize, sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogTerminate and sceNpProfileDialogUpdateStatus](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpProfileDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogGetResult](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - the layouts come from the [shadPS4 header](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_profile_dialog/np_profile_dialog.h#L31-L66) alone: `SceNpProfileDialogParam` 128 bytes with `userData` at 88, `SceNpProfileDialogParamA` 120 bytes with 4 unnamed bytes after `targetAccountId` and `userData` at 80, the result 48 bytes. A param whose `size` is not that size, or whose `mode` is not 1 to 4, throws. `baseParam` (size, magic), `userId` and the target ID are not checked. +- [sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogClose, sceNpProfileDialogGetResult](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - these paths throw because shadPS4 and the other common dialogs here return different errors: `Open` or `OpenA` before `Initialize` (shadPS4 `INVALID_STATE`, the Signin family `NOT_INITIALIZED`); `Close` on a dialog that is not running, which is every `Close` after `Initialize` here (shadPS4 `NOT_RUNNING`, the Signin family 0 and `FINISHED`); `GetResult` with a null result before the dialog finishes (shadPS4 `ARG_NULL`, Signin and NetCtlAp `NOT_FINISHED`). + +### Functional + +- [sceNpProfileDialogInitialize](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - never returns shadPS4's `NOT_SYSTEM_INITIALIZED` (`sceCommonDialogInitialize` not called) or `BUSY` (another common dialog in use): libSceCommonDialog shares neither state here. From 1c026e5fee87e9e8d27be443d25472903da6bdf3 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Fri, 23 Oct 2026 04:17:45 -0400 Subject: [PATCH 1214/1360] fix(libkernel_sys): report ELOOP from link through a Windows symbolic link loop link took the source's std::filesystem::status first, and MinGW-w64's libstdc++ reports a Windows symbolic link loop as not found (ENOENT), so it returned ENOENT before FollowLinkPath, which maps the loop's ERROR_CANT_RESOLVE_FILENAME to ELOOP, was reached. guest_kernel_sys's loop case failed at that check wherever symbolic links can be created (Windows 11 22H2: "check failed at line 835 (guest errno 2)"). When the status reports the source missing, ask FollowLinkPath first: a loop now returns ELOOP and a dangling link still ENOENT; a missing path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 53ab5bc867..53110cce05 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -398,7 +398,13 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast<int>(Fail(error)); std::error_code error; const auto status = std::filesystem::status(source, error); - if (!std::filesystem::exists(status)) return static_cast<int>(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (!std::filesystem::exists(status)) { +#ifdef _WIN32 + std::filesystem::path target; + if (const int followError = FollowLinkPath(source, target)) return static_cast<int>(Fail(followError)); +#endif + return static_cast<int>(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + } if (std::filesystem::is_directory(status)) return static_cast<int>(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast<int>(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast<int>(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); From c1abb7eb4c2ee43de22d662c488176a3d190b792 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Thu, 22 Oct 2026 00:25:43 +0700 Subject: [PATCH 1215/1360] fix(libc): reject a wcsncat_s copy only when its written and read ranges overlap Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libc/src/Strings.cpp | 4 ++-- core/libs/tests/GuestWideStrings.cpp | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index b3d173267a..1eed911f95 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -863,8 +863,8 @@ int APS5_VABI wcsncat_s_nid_postfix(char16_t* dest, size_t destsz, const char16_ } const auto destination = reinterpret_cast<std::uintptr_t>(dest + used); const auto source = reinterpret_cast<std::uintptr_t>(src); - const auto distance = destination < source ? source - destination : destination - source; - if (length != 0 && distance < (length + 1) * sizeof(char16_t)) { + const auto bytes = length * sizeof(char16_t); + if (destination < source + bytes && source < destination + bytes) { dest[0] = 0; return GuestEinval; } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 9137e19649..cc3bfc0ff5 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -240,6 +240,11 @@ int main() { wmemset_nid_postfix(strBuf, u'x', 4); require(wcsncat_s_nid_postfix(strBuf, 4, u"a", 1) == 34 && strBuf[0] == 0); wcsncpy_s_nid_postfix(strBuf, 16, u"abc", 3); + require(wcsncat_s_nid_postfix(strBuf, 16, strBuf + 1, 2) == 0 && same(strBuf, u"abcbc", 6)); + const char16_t splitSource[] = u"abc\0xyz"; + std::memcpy(strBuf, splitSource, sizeof(splitSource)); + require(wcsncat_s_nid_postfix(strBuf, 16, strBuf + 4, 3) == 22 && strBuf[0] == 0); + wcsncpy_s_nid_postfix(strBuf, 16, u"abc", 3); require(wcscat_s_nid_postfix(strBuf, 16, u"def") == 0 && same(strBuf, u"abcdef", 7)); wcsncpy_s_nid_postfix(strBuf, 8, u"12345", 5); require(wcscat_s_nid_postfix(strBuf, 8, u"6789") == 34 && strBuf[0] == 0); From e53f78214a970deeb6947c25716db7501fa6541a Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Fri, 23 Oct 2026 15:19:08 +0700 Subject: [PATCH 1216/1360] docs(libc): note where the wcsncat_s overlap rule comes from The debt entry says the rule follows a static reading of the console's wcsncat_s in libc.prx, that wcscat_s inherits it unread, and that the narrow strncat_s keeps a wider rule, as asked in review. Co-Authored-By: Claude <noreply@anthropic.com> --- docs/dev/debt/libc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3a606286a0..d9884f836b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -37,6 +37,7 @@ - [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified - [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. +- [wcsncat_s and wcscat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) refuse a copy as overlapping only when the written range `[dest + len1, dest + len1 + len2)` meets the read range `[src, src + len2)`. The rule comes from a static reading of the console's `wcsncat_s` in `libc.prx` (not run on a console); `wcscat_s` gets it through `wcsncat_s`, and its own console code was not read. The narrow `strncat_s` keeps a wider rule, `[dest, dest + used + length + 1)` against `[src, src + length + 1)`, whose console code was not read either, so it refuses some copies whose wide equivalent proceeds. - [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: the handler installed with `set_constraint_handler_s` is not invoked, and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` - [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. From 2659ec333e03dabdb486d48eccdc841654ae529c Mon Sep 17 00:00:00 2001 From: Mehran <hi@mehran.tech> Date: Thu, 15 Oct 2026 12:26:14 +0300 Subject: [PATCH 1217/1360] perf(agc): replace winpthreads locks and TLS on the draw path --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 +- .../libs/prx/libSceAgcDriver/Eq/src/Event.cpp | 3 +- .../Execution/include/Driver/DeviceAccess.hpp | 9 +- .../Execution/include/Driver/Driver.hpp | 17 +- .../Execution/include/GuestMemory.hpp | 5 +- .../Execution/include/Mutex.hpp | 65 ++++++ .../Execution/include/PerformanceTimer.hpp | 3 +- .../Execution/src/Driver/DeviceAccess.cpp | 35 ++- .../src/Driver/Dispatch/IndirectDispatch.cpp | 3 +- .../src/Driver/Memory/BufferCopy.cpp | 3 +- .../src/Driver/Memory/BufferFill.cpp | 3 +- .../src/Driver/Packets/PacketExecution.cpp | 2 +- .../src/Driver/Shaders/ShaderDiagnostics.cpp | 5 +- .../Driver/Synchronization/DeferredLabels.cpp | 10 +- .../Driver/Synchronization/LabelHistory.cpp | 3 +- .../Execution/src/GuestMemory.cpp | 92 +++++--- .../Execution/src/VulkanDevice.cpp | 7 +- .../Execution/src/WorkerSampler.cpp | 3 +- .../Graphics/include/BufferPool.hpp | 5 +- .../Graphics/include/Recorder.hpp | 5 +- .../Graphics/include/Sampler.hpp | 3 +- .../Graphics/include/ShaderResources.hpp | 5 +- .../Graphics/include/SnapshotCopies.hpp | 3 +- .../Graphics/src/BdaResources.cpp | 3 +- .../Graphics/src/DccMetadata.cpp | 5 +- .../Graphics/src/DepthSurface.cpp | 3 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 15 +- .../Graphics/src/GuestBufferMemory.cpp | 21 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 3 +- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 15 +- .../Graphics/src/ShaderResources.cpp | 21 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 13 +- core/libs/prx/libSceAgcDriver/tests/Locks.cpp | 221 ++++++++++++++++++ core/libs/prx/libc/include/HostThreadSlot.hpp | 79 +++++++ 34 files changed, 567 insertions(+), 134 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Mutex.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/Locks.cpp create mode 100644 core/libs/prx/libc/include/HostThreadSlot.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 936cfb1f76..800c8fcd78 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -713,6 +713,17 @@ target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel l set_target_properties(agc_shared_write_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shared_write_tests) +add_test_executable(agc_locks_tests EXCLUDE_FROM_ALL tests/Locks.cpp) +target_include_directories(agc_locks_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_locks_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_locks_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_locks_tests) + add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp @@ -2320,7 +2331,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_locks_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() diff --git a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp index 80774d7216..d7f5463eae 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp +++ b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <algorithm> #include <cstdint> @@ -20,7 +21,7 @@ struct Registration { int id; }; -std::mutex g_mutex; +AgcDriver::Mutex g_mutex; std::vector<Registration> g_registrations; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp index 3bd84a70db..2e33081f07 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp @@ -3,10 +3,9 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include <atomic> -#include <condition_variable> #include <cstddef> +#include <cstdint> #include <memory> -#include <mutex> #include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -33,10 +32,8 @@ public: void unlock(); private: - std::mutex mutex; - std::condition_variable changed; - std::size_t users = 0; - bool replacing = false; + static constexpr std::uint32_t Replacing = 0x80000000u; + std::atomic<std::uint32_t> state{0}; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 1bba99967e..a331ac8de8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -17,6 +17,7 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "prx/libc/include/Shutdown.hpp" #include <array> #include <atomic> @@ -347,7 +348,7 @@ private: void run(std::uint32_t id) noexcept; std::mutex mutex; - std::mutex shutdownMutex; + AgcDriver::Mutex shutdownMutex; std::condition_variable changed; std::map<std::uint32_t, QueueWorker> workers; @@ -370,7 +371,7 @@ private: std::unordered_map<std::uint64_t, std::shared_ptr<DispatchEntry>> dispatchCache; std::list<std::uint64_t> dispatchOrder; - std::mutex dispatchCacheMutex; + AgcDriver::Mutex dispatchCacheMutex; std::uint64_t dispatchCacheHits = 0; std::uint64_t dispatchCacheEvictions = 0; @@ -392,18 +393,18 @@ private: DrawRelocationPending drawRelocationPending; std::unordered_map<std::uint64_t, DrawRelocationTrial> drawRelocationTrials; DrawRelocationCounters drawRelocationCounters; - std::mutex drawCacheMutex; + AgcDriver::Mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; std::optional<DrawKeySightings> drawKeySightings; std::optional<DrawKeySightings> drawShapeSightings; DrawEntryCounters drawEntryCounters; - std::mutex driverPhasesMutex; + AgcDriver::Mutex driverPhasesMutex; std::array<DriverPhaseTotals, DispatchClassCount> driverPhaseTotals{}; std::chrono::steady_clock::time_point driverPhasesReport = std::chrono::steady_clock::now(); - std::mutex drawPhasesMutex; + AgcDriver::Mutex drawPhasesMutex; DrawPhaseTotals drawPhaseTotals; std::map<std::uint32_t, QueueState> queues; @@ -416,7 +417,7 @@ private: std::uint32_t nextEopToken = EopFlipTokenBase; DevicePointer device; - std::mutex labelStoresMutex; + AgcDriver::Mutex labelStoresMutex; std::unordered_map<std::uint64_t, std::array<LabelStore, LabelStoreHistory>> labelStores; std::vector<std::shared_ptr<VulkanDevice>> replacedDevices; @@ -447,7 +448,7 @@ private: std::atomic<std::uint64_t> evidenceValidations{0}; std::atomic<std::uint64_t> dispatchesAdoptedAhead{0}; - std::mutex writtenBuffersMutex; + AgcDriver::Mutex writtenBuffersMutex; std::deque<WrittenBuffer> writtenBuffers; std::uint64_t writtenBufferSerial = 0; std::unordered_map<std::uint64_t, DwordEvidence> dwordEvidence; @@ -457,7 +458,7 @@ private: std::atomic<std::uint64_t> observationsNoWait{0}; std::atomic<std::uint64_t> observationsNotReached{0}; - std::mutex validateMutex; + AgcDriver::Mutex validateMutex; ValidateCounters validateCounters; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index b61abd1f07..0e9c7b751a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_GUESTMEMORY_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_GUESTMEMORY_HPP +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <atomic> #include <cstddef> #include <cstdint> @@ -156,8 +157,10 @@ public: std::uint32_t DepthOnThisThread() const; private: + void enter(); + bool tryEnter(); void acquired(); - std::recursive_mutex mutex; + Mutex mutex; // The holder's thread token and recursion depth, written by the holder only; another thread // reads the owner just to see that it is not itself. std::atomic<const void*> owner{nullptr}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Mutex.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Mutex.hpp new file mode 100644 index 0000000000..182fb1e757 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Mutex.hpp @@ -0,0 +1,65 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_MUTEX_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_MUTEX_HPP + +#include <atomic> +#include <cstdint> + +namespace AgcDriver { + +inline void WaitWhile(const std::atomic<std::uint32_t>& word, std::uint32_t value) noexcept { + word.wait(value, std::memory_order_relaxed); +} + +inline void WakeOne(std::atomic<std::uint32_t>& word) noexcept { + word.notify_one(); +} + +inline void WakeAll(std::atomic<std::uint32_t>& word) noexcept { + word.notify_all(); +} + +class Mutex { +public: + constexpr Mutex() noexcept = default; + Mutex(const Mutex&) = delete; + Mutex& operator=(const Mutex&) = delete; + + void lock() noexcept { + if (!try_lock()) lockContended(); + } + + bool try_lock() noexcept { + std::uint32_t expected = Unlocked; + return state.compare_exchange_strong(expected, Locked, std::memory_order_acquire, std::memory_order_relaxed); + } + + void unlock() noexcept { + if (state.exchange(Unlocked, std::memory_order_release) == Contended) WakeOne(state); + } + +private: + static constexpr std::uint32_t Unlocked = 0; + static constexpr std::uint32_t Locked = 1; + static constexpr std::uint32_t Contended = 2; + + [[gnu::noinline]] void lockContended() noexcept { + while (state.exchange(Contended, std::memory_order_acquire) != Unlocked) WaitWhile(state, Contended); + } + + std::atomic<std::uint32_t> state{Unlocked}; +}; + +inline const void* CurrentThreadToken() noexcept { +#if defined(_WIN32) && defined(__x86_64__) && defined(__GNUC__) + const void* block; + __asm__("movq %%gs:0x30, %0" : "=r"(block)); + return block; +#else + thread_local char token; + return &token; +#endif +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp index 28ccc5c2df..c674c5731e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <algorithm> #include <chrono> #include <cstddef> @@ -206,7 +207,7 @@ private: return std::chrono::duration<double, std::milli>(elapsed).count(); } - std::mutex mutex; + AgcDriver::Mutex mutex; bool localMetrics = false; std::map<std::pair<std::string_view, std::string_view>, Metric> metrics; std::map<std::pair<std::string_view, std::string_view>, Metric> asyncMetrics; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/DeviceAccess.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/DeviceAccess.cpp index a7a14931a5..15b84d7b96 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/DeviceAccess.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/DeviceAccess.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" namespace AgcDriver::DriverDetail { @@ -21,27 +22,37 @@ DevicePointer::operator bool() const { return Load() != nullptr; } bool DevicePointer::operator==(std::nullptr_t) const { return Load() == nullptr; } void DeviceUseGate::lock_shared() { - std::unique_lock lock(mutex); - changed.wait(lock, [&] { return !replacing; }); - ++users; + auto value = state.load(std::memory_order_relaxed); + for (;;) { + if ((value & Replacing) != 0) { + WaitWhile(state, value); + value = state.load(std::memory_order_relaxed); + } else if (state.compare_exchange_weak(value, value + 1, std::memory_order_acquire, std::memory_order_relaxed)) { + return; + } + } } void DeviceUseGate::unlock_shared() { - std::lock_guard lock(mutex); - if (--users == 0) changed.notify_all(); + if (state.fetch_sub(1, std::memory_order_release) == (Replacing | 1)) WakeAll(state); } void DeviceUseGate::lock() { - std::unique_lock lock(mutex); - changed.wait(lock, [&] { return !replacing; }); - replacing = true; - changed.wait(lock, [&] { return users == 0; }); + auto value = state.load(std::memory_order_relaxed); + for (;;) { + if ((value & Replacing) != 0) { + WaitWhile(state, value); + value = state.load(std::memory_order_relaxed); + } else if (state.compare_exchange_weak(value, value | Replacing, std::memory_order_acquire, std::memory_order_relaxed)) { + break; + } + } + for (value = state.load(std::memory_order_acquire); value != Replacing; value = state.load(std::memory_order_acquire)) WaitWhile(state, value); } void DeviceUseGate::unlock() { - std::lock_guard lock(mutex); - replacing = false; - changed.notify_all(); + state.fetch_and(~Replacing, std::memory_order_release); + WakeAll(state); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index 65057babfa..121a1bedde 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <cstdio> #include <cstdlib> @@ -30,7 +31,7 @@ const char* IndirectPathName(int path) { void Driver::countIndirect(int path, double readMs) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile || path < 0 || path >= IndirectPaths) return; - static std::mutex countsMutex; + static AgcDriver::Mutex countsMutex; static std::uint64_t counts[IndirectPaths] = {}; static double readWaitedMs = 0; static auto lastReport = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index 6a76867635..c12a99ce56 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <algorithm> #include <cstdlib> #include <cstring> @@ -58,7 +59,7 @@ void Driver::countCopy(int path, std::size_t bytes, std::chrono::steady_clock::t copyPathCounts[path].fetch_add(1, std::memory_order_relaxed); if (!profile) return; using Outcome = VulkanDevice::CopyOutcome; - static std::mutex countsMutex; + static AgcDriver::Mutex countsMutex; static std::uint64_t counts[CopyPaths] = {}; static std::uint64_t refused[Outcome::Refusals] = {}; static std::uint64_t cpuBytes = 0, gpuBytes = 0, aliasedBytes = 0, syncs = 0, settledBySignal = 0, sourceWaits = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index e410cb88d3..bd1b6112de 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" @@ -186,7 +187,7 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera if (!profile) return; using FillCover = Graphics::StorageTexture::FillCover; static constexpr std::array<const char*, 8> names{"none", "exact", "inside", "around", "straddle", "several", "keys", "layer"}; - static std::mutex countsMutex; + static AgcDriver::Mutex countsMutex; static std::array<std::uint64_t, 8> counts{}, coverBytes{}; static std::map<std::uint32_t, std::uint64_t> coveredFormats; static std::map<std::string, std::uint64_t> refusals; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index a4c16c2410..276a2ad0cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -442,7 +442,7 @@ void Driver::execute(const Submission& submission) { finishDispatchPacket(opcode == 0x16); } else if (opcode == 0x3c || opcode == 0x93) { static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; - const auto waitStart = std::chrono::steady_clock::now(); + const auto waitStart = traceGpu ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; timed(&WorkerProfile::waitMs, [&] { waitMemory(packet, submission.queue, recent, submission.received, submission.heldAtSubmit.contains(cursor)); }); gateOpened() = true; if (traceGpu && std::chrono::steady_clock::now() - waitStart > std::chrono::milliseconds(200)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp index 52f65cc2d0..91b97871d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ControlFlow/RequestSerializer.hpp" #include <cstdlib> #include <functional> @@ -7,7 +8,7 @@ namespace AgcDriver::DriverDetail { void Driver::reportSkip(const char* kind, const std::string& what) { - static std::mutex reportedMutex; + static AgcDriver::Mutex reportedMutex; static std::set<std::size_t> reported; static std::map<std::string, int> requestDumps; @@ -50,7 +51,7 @@ void Driver::reportSkip(const char* kind, const std::string& what) { } std::string Driver::dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix) { - static std::mutex dumpMutex; + static AgcDriver::Mutex dumpMutex; static std::set<std::string> dumped; char hex[32]; std::snprintf(hex, sizeof(hex), "%llx", static_cast<unsigned long long>(address)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp index 858cada5e4..b43a5c49b2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp @@ -2,14 +2,20 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/HostThreadSlot.hpp" #include "ThreadOwned.hpp" #include <cstdlib> namespace AgcDriver::DriverDetail { DeferredLabels& deferredLabels() { - static thread_local DeferredLabels* deferred = nullptr; - return ShaderRecompiler::ThreadOwned(deferred); + struct Slot {}; + auto* deferred = HostThreadSlot<DeferredLabels*, Slot>::Get(); + if (deferred == nullptr) { + ShaderRecompiler::ThreadOwned(deferred); + HostThreadSlot<DeferredLabels*, Slot>::Set(deferred); + } + return *deferred; } bool DeferLabels() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp index 2720b91265..fe15539222 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <cstdlib> #include <cstring> #include <string_view> @@ -60,7 +61,7 @@ void Driver::traceLabel(std::span<const std::uint32_t> packet, std::uint32_t que } if (kind == nullptr) return; if (std::string_view(kind) != "WAIT_REG_MEM") { - static std::mutex historyMutex; + static AgcDriver::Mutex historyMutex; std::lock_guard lock(historyMutex); auto& entry = writeHistory()[writeCursor()++ % writeHistory().size()]; entry = {target, length, queue, opcode}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 28fe51847d..69341f8385 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1,12 +1,14 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/HostThreadSlot.hpp" #include <mutex> #include <array> #include <chrono> @@ -152,13 +154,13 @@ struct MemoryCounters { }; struct MemoryProfile { - std::mutex threadsMutex; + AgcDriver::Mutex threadsMutex; std::vector<const MemoryCounters*> threads; MemoryCounters retired; MemoryCounters shared; std::atomic<std::int64_t> lastReport{0}; // Sampled callers of Read and Write (module offsets), to name what touches guest memory most. - std::mutex callersMutex; + AgcDriver::Mutex callersMutex; std::array<std::pair<unsigned long long, std::uint64_t>, 16> callers{}; std::array<std::pair<unsigned long long, std::uint64_t>, 16> writeCallers{}; }; @@ -222,19 +224,19 @@ void CountCaller(std::array<std::pair<unsigned long long, std::uint64_t>, 16>& c } } -// Attribution tags (see the header): the packet a queue worker executes and the read site the -// driver code named. Plain thread-locals; a store per packet costs nothing measurable. -thread_local PacketTag currentPacket{NoPacket, 0xffffffffu}; -thread_local ReadSite currentReadSite = ReadSite::Unknown; +struct CurrentPacketSlot {}; +struct CurrentReadSiteSlot {}; +constexpr std::uint64_t NoPacketBits = NoPacket | (std::uint64_t{0xffffffffu} << 32u); // Sampled reads by site (every 256th Read/ReadCommitted, like the callers), for the [guestmem] line. std::atomic<std::uint64_t> readSiteSamples[static_cast<std::size_t>(ReadSite::Count)] = {}; void SetCurrentPacket(std::uint32_t opcode, std::uint32_t queue) { - currentPacket = {opcode, queue}; + HostThreadSlot<std::uint64_t, CurrentPacketSlot>::Set((opcode | (std::uint64_t{queue} << 32u)) ^ NoPacketBits); } PacketTag CurrentPacket() { - return currentPacket; + const auto bits = HostThreadSlot<std::uint64_t, CurrentPacketSlot>::Get() ^ NoPacketBits; + return {static_cast<std::uint32_t>(bits), static_cast<std::uint32_t>(bits >> 32u)}; } const char* ReadSiteName(ReadSite site) { @@ -244,11 +246,13 @@ const char* ReadSiteName(ReadSite site) { } ReadSite SetReadSite(ReadSite site) { - return std::exchange(currentReadSite, site); + const auto previous = HostThreadSlot<ReadSite, CurrentReadSiteSlot>::Get(); + HostThreadSlot<ReadSite, CurrentReadSiteSlot>::Set(site); + return previous; } ReadSite CurrentReadSite() { - return currentReadSite; + return HostThreadSlot<ReadSite, CurrentReadSiteSlot>::Get(); } std::size_t CaptureCallerOffsets(std::span<unsigned long long> frames, unsigned skip) { @@ -289,7 +293,7 @@ std::size_t CaptureCallerOffsets(std::span<unsigned long long> frames, unsigned void CountReadCaller(const void* returnAddress) { thread_local std::uint32_t sampled = 0; // Sampled at the same rate as the callers, in step with them (CountCaller advances the counter). - if ((sampled + 1) % 256 == 0) readSiteSamples[static_cast<std::size_t>(currentReadSite)].fetch_add(256, std::memory_order_relaxed); + if ((sampled + 1) % 256 == 0) readSiteSamples[static_cast<std::size_t>(CurrentReadSite())].fetch_add(256, std::memory_order_relaxed); CountCaller(Profile().callers, sampled, 256, returnAddress); } @@ -848,7 +852,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ #endif struct WriteTracker { - std::mutex mutex; + AgcDriver::Mutex mutex; bool initialized = false; bool watched = false; static constexpr std::size_t CoarseBlocks = 64; @@ -1225,7 +1229,7 @@ WriteTracker& Tracker() { // Takes the tracker mutex; under APS5_PROFILE_DRAW an acquisition that found it held is counted // ('tracker waits N / M' in [guestmem]), which says whether walking outside the lock would pay. -std::unique_lock<std::mutex> lockTracker(WriteTracker& tracker) { +std::unique_lock<AgcDriver::Mutex> lockTracker(WriteTracker& tracker) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return std::unique_lock(tracker.mutex); std::unique_lock lock(tracker.mutex, std::try_to_lock); @@ -1260,11 +1264,11 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ // thread that never bumped is given a new epoch for every collect: its walks are never reused (the // presenter, game threads inside the flush hook), as nothing orders the CPU's writes for it. std::atomic<std::uint64_t> nextCollectEpoch{1}; -thread_local std::uint64_t threadCollectEpoch = 0; +struct CollectEpochSlot {}; std::atomic<std::uint64_t> lastBumpedEpoch{0}; std::uint64_t currentCollectEpoch() { - if (threadCollectEpoch != 0) return threadCollectEpoch; + if (const auto epoch = HostThreadSlot<std::uint64_t, CollectEpochSlot>::Get(); epoch != 0) return epoch; return nextCollectEpoch.fetch_add(1, std::memory_order_relaxed) + 1; } @@ -1281,7 +1285,15 @@ struct ThreadCollectMemo { std::array<WriteTracker::Memo, 64> entries{}; std::size_t next = 0; }; -thread_local ThreadCollectMemo threadCollectMemo; +ThreadCollectMemo& threadCollectMemo() { + struct Slot {}; + auto* memo = HostThreadSlot<ThreadCollectMemo*, Slot>::Get(); + if (memo == nullptr) { + ShaderRecompiler::ThreadOwned(memo); + HostThreadSlot<ThreadCollectMemo*, Slot>::Set(memo); + } + return *memo; +} struct EpochCollectMap { std::uint64_t epoch = 0; @@ -1402,7 +1414,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi return tracker.generation.load(std::memory_order_relaxed); } } - for (const auto& entry : threadCollectMemo.entries) { + for (const auto& entry : threadCollectMemo().entries) { if (entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end) { // The current generation, not the memoized one: blocks stamped since (MarkWritten, an // overlapping collect) were written before this caller reads, and an older value would @@ -1426,7 +1438,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi } const TimedAccess timed(CounterCollect, bytes); #ifndef _WIN32 - if (useMemo && threadCollectEpoch != 0 && bytes <= WriteBlockBytes) { + if (useMemo && HostThreadSlot<std::uint64_t, CollectEpochSlot>::Get() != 0 && bytes <= WriteBlockBytes) { const auto begin = first & ~(WriteBlockBytes - 1); const auto end = (stop + WriteBlockBytes - 1) & ~(WriteBlockBytes - 1); if (end > begin && tracker.covers(begin, end - begin)) { @@ -1441,7 +1453,8 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi const auto serial = unwatchSerial.load(std::memory_order_relaxed); if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial, mapping}; else { - threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial, mapping}; + auto& memo = threadCollectMemo(); + memo.entries[memo.next++ % memo.entries.size()] = {first, stop, epoch, serial, mapping}; if (epochCollectMapEnabled()) { auto& entry = epochCollectMap(epoch).ranges[first]; if (entry.unwatched != serial || entry.mapping != mapping || entry.end < stop) entry = {first, stop, epoch, serial, mapping}; @@ -1462,14 +1475,15 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes) { } void BumpCollectEpoch() { - threadCollectEpoch = nextCollectEpoch.fetch_add(1, std::memory_order_relaxed) + 1; + const auto epoch = nextCollectEpoch.fetch_add(1, std::memory_order_relaxed) + 1; + HostThreadSlot<std::uint64_t, CollectEpochSlot>::Set(epoch); auto last = lastBumpedEpoch.load(std::memory_order_relaxed); - while (last < threadCollectEpoch && !lastBumpedEpoch.compare_exchange_weak(last, threadCollectEpoch, std::memory_order_release, std::memory_order_relaxed)) {} + while (last < epoch && !lastBumpedEpoch.compare_exchange_weak(last, epoch, std::memory_order_release, std::memory_order_relaxed)) {} collectEpochBumps.fetch_add(1, std::memory_order_relaxed); } std::uint64_t ThreadCollectEpoch() { - return collectMemoEnabled() ? threadCollectEpoch : 0; + return collectMemoEnabled() ? HostThreadSlot<std::uint64_t, CollectEpochSlot>::Get() : 0; } std::uint64_t UnwatchSerial() { @@ -1478,7 +1492,7 @@ std::uint64_t UnwatchSerial() { void AdoptLatestCollectEpoch() { const auto latest = lastBumpedEpoch.load(std::memory_order_acquire); - if (latest > threadCollectEpoch) threadCollectEpoch = latest; + if (latest > HostThreadSlot<std::uint64_t, CollectEpochSlot>::Get()) HostThreadSlot<std::uint64_t, CollectEpochSlot>::Set(latest); } std::uint64_t CollectEpochBumps() { @@ -1503,7 +1517,7 @@ void MarkProofThread() { } std::uint64_t CollectEpoch() { - return collectMemoEnabled() ? threadCollectEpoch : 0; + return collectMemoEnabled() ? HostThreadSlot<std::uint64_t, CollectEpochSlot>::Get() : 0; } namespace { @@ -2106,18 +2120,20 @@ std::string WaitedBehindReport(const HolderMatrixValues& waited) { } -// The owner token is a thread-local's address: unique per live thread and free to read. namespace { -const void* ThreadToken() { - thread_local char token; - return &token; -} - std::atomic<std::uint64_t> gpuHoldSerial{0}; } +void GpuMutexType::enter() { + if (!HeldByThisThread()) LockSpinning(mutex, HandoffSpinWindow()); +} + +bool GpuMutexType::tryEnter() { + return HeldByThisThread() || mutex.try_lock(); +} + void GpuMutexType::acquired() { - owner.store(ThreadToken(), std::memory_order_relaxed); + owner.store(CurrentThreadToken(), std::memory_order_relaxed); if (++depth == 1) { gpuHoldSerial.fetch_add(1, std::memory_order_relaxed); if (!proofThread) BreakProofs(); @@ -2126,7 +2142,7 @@ void GpuMutexType::acquired() { bool GpuMutexType::try_lock() { if (!GpuLockProfiled()) { - if (!mutex.try_lock()) return false; + if (!tryEnter()) return false; acquired(); return true; } @@ -2135,7 +2151,7 @@ bool GpuMutexType::try_lock() { auto& stats = LockStats(); const auto tagged = std::exchange(stats.nextSite, GpuLockSite::Other); ++stats.tries; - if (!mutex.try_lock()) { + if (!tryEnter()) { ++stats.triesFailed; return false; } @@ -2162,8 +2178,8 @@ void GpuMutexType::unlock() { owner.store(nullptr, std::memory_order_relaxed); // Still the holder here: the hold ends when the mutex is given up, not before. if (GpuHoldProfiled()) EndHold(); + mutex.unlock(); } - mutex.unlock(); // After the release, so what the hook does (destroying a batch's kept objects) is neither part // of the hold nor waited for by the next holder. if (outermost) { @@ -2172,7 +2188,7 @@ void GpuMutexType::unlock() { } bool GpuMutexType::HeldByThisThread() const { - return owner.load(std::memory_order_relaxed) == ThreadToken(); + return owner.load(std::memory_order_relaxed) == CurrentThreadToken(); } std::uint32_t GpuMutexType::DepthOnThisThread() const { @@ -2191,10 +2207,10 @@ void GpuMutexType::lock() { if (!GpuLockProfiled()) { auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr; if (trace == nullptr) { - LockSpinning(mutex, HandoffSpinWindow()); + enter(); } else { const auto site = static_cast<std::size_t>(std::exchange(LockStats().nextSite, GpuLockSite::Other)); - if (!mutex.try_lock()) { + if (!tryEnter()) { const auto start = std::chrono::steady_clock::now(); LockSpinning(mutex, HandoffSpinWindow()); trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now()); @@ -2209,7 +2225,7 @@ void GpuMutexType::lock() { ++stats.siteAcquisitions[site]; // An uncontended (or recursive) acquisition costs no clock reads for the wait; only a wait is // timed. The hold timer (outermost acquisitions only) starts once the mutex is held. - if (!mutex.try_lock()) { + if (!tryEnter()) { const bool holders = GpuHoldProfiled(); HolderMatrixValues before{}; if (holders) Holders().Snapshot(before); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4eb2724ed7..ceb75fd8a7 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/FrameDumpIo.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "BdaAbi.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -426,7 +427,7 @@ struct VulkanDevice::State { // Compute pipeline objects by variant (and push-constant use), under their own mutex: the // dispatch's find-or-insert and the verify switch's lookups touch the map, recipes hold weak // references to its objects. - std::mutex computePipelinesMutex; + AgcDriver::Mutex computePipelinesMutex; std::map<ComputePipelineKey, std::shared_ptr<ComputePipelineObjects>> computePipelines; std::map<ComputePipelineKey, std::shared_future<std::shared_ptr<ComputePipelineObjects>>> computePipelinesBuilding; // The journal replay building this device's pipelines on its own threads; drained first at teardown. @@ -507,7 +508,7 @@ struct VulkanDevice::State { std::uint64_t throughSerial; }; std::deque<RetiredRange> retiredRanges; - std::mutex presentMutex; + AgcDriver::Mutex presentMutex; // The presentation present() submitted and QueuePresent has not handed to the swapchain. bool queuePending = false; std::uint32_t queueIndex = 0; @@ -4155,7 +4156,7 @@ void WatchMemory(std::uint64_t programAddress) { return parsed; }(); if (ranges.empty()) return; - static std::mutex mutex; + static AgcDriver::Mutex mutex; static std::deque<std::pair<std::uint64_t, std::vector<std::uint8_t>>> history; static bool written = false; std::lock_guard lock(mutex); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 7bbca06017..f95ac3a62f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <cstdio> #include <cstdlib> #include <cstring> @@ -43,7 +44,7 @@ struct Sampler { std::uint64_t stackHigh = 0; std::vector<DwarfFrames> modules; std::uint64_t pendingModule = 0; - std::mutex mutex; + AgcDriver::Mutex mutex; std::map<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>> counts; CollapsedStacks stacks; std::uint64_t samples = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 657c5351c4..5dcf54c727 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include <chrono> +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <cstdint> #include <deque> #include <list> @@ -152,7 +153,7 @@ private: VkPhysicalDevice physical = VK_NULL_HANDLE; VkPhysicalDeviceMemoryProperties memoryProperties{}; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; - std::mutex mutex; + AgcDriver::Mutex mutex; // Not `small`/`large`: <rpcndr.h> (via <windows.h>) defines `small` as a macro. Tier smallTier; Tier largeTier; @@ -167,7 +168,7 @@ private: std::uint64_t imageEvictions = 0; std::list<Slot*> imageOrder; std::uint64_t clock = 0; - std::mutex slabMutex; + AgcDriver::Mutex slabMutex; std::unordered_map<std::uint64_t, Slab> slabs; std::unordered_map<VkDeviceMemory, std::unique_ptr<SlabBlock>> slabBlocks; static constexpr VkDeviceSize budget = 512ull * 1024 * 1024; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 6ef4b5cf08..e322257825 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/PassHazards.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <unordered_map> #include <atomic> #include <chrono> @@ -796,7 +797,7 @@ private: // Whether a write-back noted after `sequence` overlapped [begin, end); true when the ring no // longer reaches back to `sequence` (conservative: the store runs as before). bool writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std::uint64_t end); - std::mutex writtenBackMutex; + AgcDriver::Mutex writtenBackMutex; std::deque<std::array<std::uint64_t, 3>> writtenBack; std::uint64_t writtenBackSequence = 0; // PendingLabel without the table mutex (the caller holds it, or the GPU mutex). @@ -844,7 +845,7 @@ private: // frame would otherwise allocate and free their objects each time). std::vector<std::pair<VkCommandBuffer, VkFence>> spare; std::vector<VkQueryPool> sparePools; - mutable std::mutex completedMutex; + mutable AgcDriver::Mutex completedMutex; std::array<Completed, CompletedRingSize> completed; std::uint64_t newestSubmitted = 0; std::chrono::steady_clock::time_point newestSubmittedAt{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index ad2f347b21..7212fb40c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <array> #include <cstdint> #include <map> @@ -59,7 +60,7 @@ private: std::shared_ptr<Sampler> sampler; std::uint64_t lastUse; }; - std::mutex mutex; + AgcDriver::Mutex mutex; std::map<std::array<std::uint32_t, 5>, Entry> entries; std::uint64_t clock = 0; std::size_t capacity; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index eb48888af9..c68ef833c5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "Recompiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include <array> @@ -120,7 +121,7 @@ private: PFN_vkDestroyDescriptorPool destroyPool; PFN_vkResetDescriptorPool resetPool; PFN_vkFreeDescriptorSets freeDescriptorSets = nullptr; - mutable std::mutex mutex; + mutable AgcDriver::Mutex mutex; std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts; struct Pool { VkDescriptorPool handle = VK_NULL_HANDLE; @@ -749,7 +750,7 @@ private: return static_cast<std::size_t>(hash); } }; - mutable std::mutex mutex; + mutable AgcDriver::Mutex mutex; std::list<std::pair<Key, std::shared_ptr<ShaderResources>>> entries; std::unordered_map<Key, decltype(entries)::iterator, KeyHash> index; std::size_t capacity = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp index f60465ac09..3b0b3b94f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SNAPSHOTCOPIES_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SNAPSHOTCOPIES_HPP +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include <cstddef> #include <cstdint> @@ -21,7 +22,7 @@ struct SnapshotCopies { using Key = std::tuple<std::uint64_t, std::size_t, VkBufferUsageFlags>; static constexpr std::size_t Budget = std::size_t{16} << 20u; static constexpr std::size_t MaxEntries = 1024; - std::mutex mutex; + AgcDriver::Mutex mutex; VkDevice device = VK_NULL_HANDLE; struct Entry { std::shared_ptr<Buffer> buffer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 03865c6a92..f9234680ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ImageTableAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include <algorithm> @@ -43,7 +44,7 @@ struct TableEntry { }; struct TableCache { - std::mutex mutex; + AgcDriver::Mutex mutex; VkDevice device = VK_NULL_HANDLE; std::list<TableEntry> entries; std::uint64_t hits = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 5afc90739d..498e656ce0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <algorithm> #include <atomic> #include <chrono> @@ -158,7 +159,7 @@ struct GpuKeyStore { }; struct KeyStoreMemo { - std::mutex mutex; + AgcDriver::Mutex mutex; std::vector<GpuKeyStore> entries; const Recorder* recorder = nullptr; }; @@ -523,7 +524,7 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue 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<std::size_t>(sizeof(probe), BytesPerElement(resource.format))))) { - static std::mutex reportedMutex; + static AgcDriver::Mutex reportedMutex; static std::set<std::pair<std::uint64_t, int>> reported; std::lock_guard lock(reportedMutex); if (reported.size() < 32 && reported.insert({resource.baseAddress, static_cast<int>(keys)}).second) std::fprintf(stderr, "[gpu] texture 0x%llx (format %u) has %s DCC keys at 0x%llx; its texels are read as stored\n", static_cast<unsigned long long>(resource.baseAddress), resource.format, DccKeysName(keys), static_cast<unsigned long long>(resource.dccAddress)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 11f2c659ac..16403dc9df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include <algorithm> #include <array> @@ -1920,7 +1921,7 @@ bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std VkFormat HostDepthFormat(const Context& context, VkFormat format) { if (format != VK_FORMAT_D16_UNORM_S8_UINT) return format; - static std::mutex mutex; + static AgcDriver::Mutex mutex; static std::map<VkPhysicalDevice, VkFormat> chosen; std::lock_guard lock(mutex); if (const auto found = chosen.find(context.physical); found != chosen.end()) return found->second; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 4a0aadae0d..90e159c7eb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -178,8 +179,8 @@ struct CmaskProof { std::uint64_t generation = 0; }; -std::mutex& cmaskProofMutex() { - static std::mutex mutex; +AgcDriver::Mutex& cmaskProofMutex() { + static AgcDriver::Mutex mutex; return mutex; } @@ -405,7 +406,7 @@ enum DrawKind : std::size_t { KindRecipeHit, KindTemplateHit, KindBuild, KindBda constexpr std::array<const char*, KindCount> DrawKindNames{"recipe hit", "template hit", "build", "BDA"}; struct DrawProfile { - std::mutex mutex; + AgcDriver::Mutex mutex; std::array<double, KindCount> kindUs{}; std::array<std::uint64_t, KindCount> kindCounts{}; std::uint64_t fullScissorLookups = 0; @@ -702,8 +703,8 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade return keyed; } -std::mutex& validationMutex() { - static std::mutex mutex; +AgcDriver::Mutex& validationMutex() { + static AgcDriver::Mutex mutex; return mutex; } @@ -1413,7 +1414,7 @@ std::shared_ptr<StorageTexture> refreshResidentTarget(const Context& context, co resident = lookup(); if (resident != nullptr) materializeRegisterClear(context, color, *resident); } catch (const std::exception& error) { - static std::mutex reportMutex; + static AgcDriver::Mutex reportMutex; static std::set<std::uint64_t> reported; std::lock_guard lock(reportMutex); if (reported.insert(color.address).second) std::fprintf(stderr, "[gpu] color target 0x%llx stays non-resident: %s\n", static_cast<unsigned long long>(color.address), error.what()); @@ -2600,7 +2601,7 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara void recordPreparedDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, PreparedDraw& prepared) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; - static std::mutex dumpMutex; + static AgcDriver::Mutex dumpMutex; static std::map<std::uint64_t, int> dumped; auto& timing = *prepared.timing; auto& timer = prepared.timer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index cef2cd2299..8b93e2ac5c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Graphics/include/SnapshotCopies.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" @@ -117,7 +118,7 @@ struct GuestBufferMemory::AddressSpace { // The BDA table entries of `base`, in its order. std::vector<ShaderRecompiler::BdaAbi::Range> ranges; std::shared_ptr<const std::vector<std::pair<std::uint64_t, std::uint64_t>>> directReads; - mutable std::mutex tableMutex; + mutable AgcDriver::Mutex tableMutex; struct Table { std::uint64_t serial; std::shared_ptr<const std::vector<ShaderRecompiler::BdaAbi::Range>> ranges; @@ -228,7 +229,7 @@ struct ImportSettle { // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { - std::mutex mutex; + AgcDriver::Mutex mutex; VkDevice device = VK_NULL_HANDLE; PFN_vkDestroyBuffer destroyBuffer = nullptr; PFN_vkFreeMemory freeMemory = nullptr; @@ -583,7 +584,7 @@ struct ImportChunk { }; struct ImportChunks { - std::mutex mutex; + AgcDriver::Mutex mutex; VkDevice device = VK_NULL_HANDLE; std::map<std::tuple<dev_t, ino_t, std::uint64_t>, std::weak_ptr<ImportChunk>> chunks; }; @@ -1956,7 +1957,7 @@ bool importsStale(const Context& context, const HostImports& state) { // GuestMemory::GpuMutex only (every completion and every stage B holds it). APS5_NO_LEASE_MIRROR=1 // restores copies. struct ImageMirrors { - std::mutex mutex; + AgcDriver::Mutex mutex; VkDevice device = VK_NULL_HANDLE; std::map<std::uint64_t, std::shared_ptr<ImageMirror>> entries; std::set<std::uint64_t> failed; @@ -2002,7 +2003,7 @@ AddressBuildTiming& ThreadAddressTiming() { } struct AddressBuildTotals { - std::mutex mutex; + AgcDriver::Mutex mutex; std::uint64_t builds = 0; AddressBuildTiming sums; double snapshotsUs = 0; @@ -2208,7 +2209,7 @@ private: inline static RefreshPool* instance = nullptr; unsigned helpers = 0; - std::mutex runMutex; + AgcDriver::Mutex runMutex; std::mutex mutex; std::condition_variable_any wake; std::condition_variable done; @@ -2689,7 +2690,7 @@ void reportMirrors() { // waiter yields instead when its own thread holds the device lock (a driver thread mutating the // registry mid-packet, as the plain spin did) and reports whether it finished any work. struct LeaseState { - std::mutex mutex; + AgcDriver::Mutex mutex; LeaseStats stats; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); // Under GuestMemory::GpuMutex: the recorder whose batches hold leases (serials restart with a @@ -4015,7 +4016,7 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { std::snprintf(text, sizeof(text), "guest snapshot differs from registered memory: snapshot 0x%llx+0x%zx (%s region 0x%llx+0x%llx%s%s%s) differs first at +0x%zx", static_cast<unsigned long long>(snapshot.address), snapshot.bytes.size(), live ? "live" : "snapshot-backed", static_cast<unsigned long long>(owner->begin), static_cast<unsigned long long>(owner->end - owner->begin), owner->writable ? ", writable" : "", owner->hostBacked ? ", host-backed" : "", owner->mirror != nullptr ? ", mirrored" : "", first); static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr; if (!live || fatal) throw std::runtime_error(text); - static std::mutex reportMutex; + static AgcDriver::Mutex reportMutex; static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); std::lock_guard lock(reportMutex); const auto mismatches = Snapshots().mismatched.fetch_add(1, std::memory_order_relaxed) + 1; @@ -4187,7 +4188,7 @@ std::shared_ptr<Buffer> stagingBuffer(const Context& context, std::size_t bytes, void traceStaged(std::uint64_t begin, std::uint64_t end, bool atomic) { static const bool trace = std::getenv("APS5_TRACE_STAGING") != nullptr; if (!trace) return; - static std::mutex mutex; + static AgcDriver::Mutex mutex; static std::set<std::pair<std::uint64_t, std::uint64_t>> seen; std::lock_guard lock(mutex); if (!seen.insert({begin, end}).second) return; @@ -4716,7 +4717,7 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { region.snapshot.clear(); if (profile) readUs.fetch_add(microsecondsSince(readStart), std::memory_order_relaxed); if (profile) { - static std::mutex reasonsMutex; + static AgcDriver::Mutex reasonsMutex; static std::uint64_t uploads = 0, noImport = 0, noImportBytes = 0, misaligned = 0, misalignedBytes = 0, snapshotOnly = 0, snapshotBytes = 0; static std::uint64_t logicalBytes = 0, readTotalBytes = 0; static std::map<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>> outside; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index ba31cb17a8..d7a28cebfb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libc/include/HostThreadLocal.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include <algorithm> #include <array> #include <atomic> @@ -982,7 +983,7 @@ struct PipelineStore { std::vector<std::byte> key; std::shared_ptr<Pipeline> pipeline; }; - std::mutex mutex; + AgcDriver::Mutex mutex; // Least recently used first. std::list<Entry> entries; std::unordered_map<std::uint64_t, std::list<Entry>::iterator> index; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 01071562ad..f5702788eb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libkernel/Time/include/SleepWake.hpp" @@ -128,7 +129,7 @@ struct ThreadSyncs { std::array<double, 5> waitedMs; }; std::vector<ThreadSyncs> threadSyncs; -std::mutex threadSyncsMutex; +AgcDriver::Mutex threadSyncsMutex; // This thread's fence and timeline waits in total (Recorder::ThreadWaitedMs): a caller times a span // of its own work and reads the difference to learn how much of it was waiting for the GPU. @@ -176,7 +177,7 @@ std::atomic<int> unlockedWaiters[WaiterSlots]{}; std::atomic<int>& WaitersOf(std::uint64_t id) { return unlockedWaiters[id % WaiterSlots]; } // Recorders alive, by id (own mutex, taken under the GpuMutex or with nothing held): a thread that // released the GpuMutex around a wait learns whether its recorder still exists before touching it. -std::mutex liveRecordersMutex; +AgcDriver::Mutex liveRecordersMutex; std::vector<std::uint64_t> liveRecorders; std::atomic<std::uint64_t> nextRecorderId{1}; std::atomic<std::uint64_t> samplesPassed{0}; @@ -193,7 +194,7 @@ std::atomic<std::uint64_t> dumpsQueued{0}, dumpsQueuedInPass{0}, dumpsAtOnce{0}, struct SamplePoolCache { VkDevice device; PFN_vkDestroyQueryPool destroy; - std::mutex mutex; + AgcDriver::Mutex mutex; std::vector<VkQueryPool> free; bool closed = false; void put(VkQueryPool pool) { @@ -416,7 +417,7 @@ struct ReleaseQueue { // Set by a joiner under `mutex` until the thread was joined: the thread leaves once the queue // is empty, and hand-offs meanwhile destroy inline (nothing may be queued without a taker). bool stop = false; - std::mutex joinMutex; + AgcDriver::Mutex joinMutex; }; ReleaseQueue& ReleaseThreadQueue() { @@ -541,7 +542,7 @@ bool ProcTable() { // instead of queueing behind a dispatch. `labelTableOwner` is the active recorder while it lives, // set and cleared under this mutex, so a lookup never touches a table being destroyed. Nothing // under this mutex takes the GPU mutex (no lock-order cycle). -std::mutex labelTableMutex; +AgcDriver::Mutex labelTableMutex; Recorder* labelTableOwner = nullptr; // The ranges of the calling worker's queued labels (NoteQueuedLabel, cleared by // ForgetQueuedLabels): the flush hook, on the same thread, has them recorded before an access that @@ -898,7 +899,7 @@ struct RecordedStoreTotals { }; struct RecordedStoreStats { - std::mutex mutex; + AgcDriver::Mutex mutex; std::map<HookSyncKey, RecordedStoreTotals> byKey; RecordedStoreTotals totals; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); @@ -1846,7 +1847,7 @@ std::atomic<std::uint64_t> timingPresents{0}; std::atomic<std::uint64_t> presentSerial{0}; std::atomic<std::uint64_t> timingDropped{0}; struct TimingTotals { std::uint64_t count = 0; double ms = 0; std::uint64_t bytes = 0; }; -std::mutex timingMutex; +AgcDriver::Mutex timingMutex; std::map<std::uint64_t, TimingTotals> timingByKey; double timingProgramMs = 0, timingClassMs = 0, timingUnionMs = 0, timingBatchMs = 0; std::uint64_t timingBatches = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 80b305f975..3dd8e92e3f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" @@ -128,7 +129,7 @@ struct CachedTexture { // eviction takes the back. APS5_NO_TEXTURE_HASH=1 finds entries by scanning the list (the index is // still kept), the linear lookup of before. struct TextureCache { - std::mutex mutex; + AgcDriver::Mutex mutex; std::list<CachedTexture> entries; std::unordered_map<TextureKey, std::list<CachedTexture>::iterator, TextureKeyHash> index; std::unordered_multimap<const StorageTexture*, std::list<CachedTexture>::iterator> views; @@ -170,7 +171,7 @@ struct CacheHold { CacheHold& operator=(const CacheHold&) = delete; }; -bool AcquireCache(std::mutex& mutex) { +bool AcquireCache(AgcDriver::Mutex& mutex) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20); do { if (mutex.try_lock()) return true; @@ -180,7 +181,7 @@ bool AcquireCache(std::mutex& mutex) { } struct UncachedImages { - std::mutex mutex; + AgcDriver::Mutex mutex; std::vector<std::weak_ptr<StorageTexture>> images; std::atomic<bool> any{false}; }; @@ -514,7 +515,7 @@ std::uint64_t DepthCopyMinBytes() { // they were when it failed (the allocation generation): pages committed later, or another surface // at the address, get a fresh attempt. struct StorageFailures { - std::mutex mutex; + AgcDriver::Mutex mutex; std::map<std::pair<std::uint64_t, std::uint64_t>, std::uint64_t> generations; }; @@ -576,7 +577,7 @@ std::uint64_t storageBudget(const Context& context) { if (forced != 0) return forced; if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); static struct { - std::mutex mutex; + AgcDriver::Mutex mutex; VkPhysicalDevice device{VK_NULL_HANDLE}; std::chrono::steady_clock::time_point read{}; std::uint64_t budget{0}; @@ -1001,7 +1002,7 @@ struct CachedStorageTexture { // As TextureCache: use order with a hash index by key, plus one by image for StorageImageCached. struct StorageTextureCache { - std::mutex mutex; + AgcDriver::Mutex mutex; std::list<CachedStorageTexture> entries; std::unordered_map<StorageKey, std::list<CachedStorageTexture>::iterator, StorageKeyHash> index; std::unordered_map<const StorageTexture*, std::list<CachedStorageTexture>::iterator> byImage; @@ -1060,7 +1061,7 @@ std::array<std::uint32_t, 8> SurfaceKey(const Context& context, const GuestTextu } struct ExtendedSurfaces { - std::mutex mutex; + AgcDriver::Mutex mutex; std::map<std::array<std::uint32_t, 8>, std::uint32_t> levels; std::atomic<bool> any{false}; }; @@ -1632,7 +1633,7 @@ constexpr std::size_t BuildPhaseCount = static_cast<std::size_t>(ShaderResources constexpr std::array<const char*, BuildPhaseCount> BuildPhaseNames{"bindings", "precollect", "guest memory upload", "descriptors", "stage A", "images", "bda", "stage B"}; struct BuildProfile { - std::mutex mutex; + AgcDriver::Mutex mutex; std::array<double, BuildPhaseCount> ms{}; std::uint64_t builds = 0; }; @@ -2553,7 +2554,7 @@ bool ShaderResources::NeverReusable(std::span<const CompiledShader> shaders) { // APS5_PROFILE_DRAW: the per-device descriptor caches' counters, every 10 s. void ShaderResources::reportDescriptorCaches() const { - static std::mutex reportMutex; + static AgcDriver::Mutex reportMutex; static auto lastReport = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now(); std::lock_guard lock(reportMutex); @@ -3599,7 +3600,7 @@ struct ChurnCounts { // Dispatch keys and draw keys are counted apart: what churns a draw's key decides the draw steps. struct ChurnProfile { - std::mutex mutex; + AgcDriver::Mutex mutex; std::unordered_map<std::uint64_t, ResourceCache::Key> lastByVariant; ChurnCounts dispatch; ChurnCounts draws; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index fc63dfc8cc..14e60f3cac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -118,7 +119,7 @@ std::atomic<std::uint64_t> refreshesProved{0}; std::atomic<std::uint64_t> untrackedCompares{0}; struct StorageTraffic { - std::mutex mutex; + AgcDriver::Mutex mutex; std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> writeBacks; std::array<std::pair<std::uint64_t, std::uint64_t>, 4> uploads{}; std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> uploadReasons; @@ -188,7 +189,7 @@ void countStorageWriteBack(std::uint64_t bytes, bool direct) { // Every storage image alive, for the fill HLE's cover check (StorageTexture::ClassifyFill): the // storage cache indexes surfaces by key, not by address range. Its mutex is a leaf. struct LiveImages { - std::mutex mutex; + AgcDriver::Mutex mutex; std::vector<StorageTexture*> textures; }; @@ -519,7 +520,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex else ++Profile().recordedUploads; if (profile) Profile().gpu += timer.lap(); if (dump) { - static std::mutex dumpMutex; + static AgcDriver::Mutex dumpMutex; static std::map<std::uint64_t, int> dumped; std::lock_guard lock(dumpMutex); auto& count = dumped[descriptor.baseAddress]; @@ -1199,7 +1200,7 @@ VkBufferMemoryBarrier WholeBufferBarrier(VkBuffer buffer, VkAccessFlags from, Vk // remembered as VK_FORMAT_UNDEFINED when the format has no storage form. VkFormat StorageFormatOrUndefined(const Context& context, VkFormat format) { struct Table { - std::mutex mutex; + AgcDriver::Mutex mutex; std::unordered_map<std::uint64_t, VkFormat> formats; }; static Table table; @@ -1328,7 +1329,7 @@ VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED; struct Table { - std::mutex mutex; + AgcDriver::Mutex mutex; std::unordered_map<VkPhysicalDevice, VkFormat> formats; }; static Table table; @@ -1699,7 +1700,7 @@ bool AdjacentGenerationEnabled() { } bool ClearKeepsDenormals(const Context& context, VkFormat format) { - static std::mutex mutex; + static AgcDriver::Mutex mutex; static std::map<std::pair<VkDevice, VkFormat>, bool> known; std::lock_guard lock(mutex); if (const auto found = known.find({context.device, format}); found != known.end()) return found->second; diff --git a/core/libs/prx/libSceAgcDriver/tests/Locks.cpp b/core/libs/prx/libSceAgcDriver/tests/Locks.cpp new file mode 100644 index 0000000000..6c660949ac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Locks.cpp @@ -0,0 +1,221 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" +#include "prx/libc/include/HostThreadSlot.hpp" +#ifdef _WIN32 +#include <windows.h> +#endif +#include <atomic> +#include <chrono> +#include <cstdint> +#include <cstdio> +#include <mutex> +#include <shared_mutex> +#include <thread> +#include <vector> + +namespace { + +int failures = 0; + +void Check(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what); + ++failures; +} + +void MutexCounts(int threads, int iterations, int holdSpins) { + AgcDriver::Mutex mutex; + std::uint64_t counter = 0; + std::vector<std::thread> workers; + for (int t = 0; t < threads; ++t) { + workers.emplace_back([&] { + for (int i = 0; i < iterations; ++i) { + std::lock_guard lock(mutex); + const auto seen = counter; + for (volatile int spin = 0; spin < holdSpins; spin = spin + 1) { + } + counter = seen + 1; + } + }); + } + for (auto& worker : workers) worker.join(); + Check(counter == static_cast<std::uint64_t>(threads) * static_cast<std::uint64_t>(iterations), "every increment under the mutex counted"); +} + +void MutexTryLock() { + AgcDriver::Mutex mutex; + Check(mutex.try_lock(), "try_lock takes a free mutex"); + bool taken = true; + std::thread other([&] { taken = mutex.try_lock(); }); + other.join(); + Check(!taken, "try_lock fails while another thread holds the mutex"); + mutex.unlock(); + std::thread again([&] { + taken = mutex.try_lock(); + if (taken) mutex.unlock(); + }); + again.join(); + Check(taken, "try_lock takes the mutex once released"); +} + +void MutexWakesSleeper() { + AgcDriver::Mutex mutex; + mutex.lock(); + std::atomic<bool> acquired{false}; + std::thread waiter([&] { + std::lock_guard lock(mutex); + acquired = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Check(!acquired, "lock() waits while the mutex is held"); + mutex.unlock(); + waiter.join(); + Check(acquired, "the waiting thread took the mutex after the release"); +} + +void DeviceGate() { + AgcDriver::DriverDetail::DeviceUseGate gate; + std::atomic<int> users{0}; + std::atomic<int> replacements{0}; + std::atomic<bool> overlap{false}; + std::atomic<bool> stop{false}; + std::atomic<std::uint64_t> uses{0}; + std::vector<std::thread> threads; + for (int t = 0; t < 6; ++t) { + threads.emplace_back([&] { + while (!stop.load(std::memory_order_relaxed)) { + std::shared_lock use(gate); + users.fetch_add(1); + if (replacements.load() != 0) overlap = true; + users.fetch_sub(1); + uses.fetch_add(1, std::memory_order_relaxed); + } + }); + } + for (int r = 0; r < 2; ++r) { + threads.emplace_back([&] { + for (int i = 0; i < 300; ++i) { + std::unique_lock replacing(gate); + if (replacements.fetch_add(1) != 0 || users.load() != 0) overlap = true; + std::this_thread::yield(); + if (users.load() != 0) overlap = true; + replacements.fetch_sub(1); + } + }); + } + threads[6].join(); + threads[7].join(); + stop = true; + for (int t = 0; t < 6; ++t) threads[static_cast<std::size_t>(t)].join(); + Check(!overlap, "no user inside the gate during a replacement, one replacement at a time"); + Check(uses.load() != 0, "users got through between replacements"); +} + +void GpuMutexRecursion() { + auto& mutex = AgcDriver::GuestMemory::GpuMutex(); + Check(!mutex.HeldByThisThread(), "the GPU mutex starts free"); + mutex.lock(); + mutex.lock(); + Check(mutex.HeldByThisThread() && mutex.DepthOnThisThread() == 2, "a nested lock counts the depth"); + bool taken = true; + std::thread other([&] { + taken = mutex.try_lock(); + if (taken) mutex.unlock(); + }); + other.join(); + Check(!taken, "another thread cannot take the held GPU mutex"); + Check(mutex.try_lock() && mutex.DepthOnThisThread() == 3, "the holder's try_lock nests"); + mutex.unlock(); + mutex.unlock(); + other = std::thread([&] { + taken = mutex.try_lock(); + if (taken) mutex.unlock(); + }); + other.join(); + Check(!taken, "the GPU mutex stays held until the outermost unlock"); + mutex.unlock(); + Check(!mutex.HeldByThisThread() && mutex.DepthOnThisThread() == 0, "the outermost unlock releases the GPU mutex"); + std::uint64_t counter = 0; + std::vector<std::thread> workers; + for (int t = 0; t < 4; ++t) { + workers.emplace_back([&] { + for (int i = 0; i < 20000; ++i) { + std::lock_guard outer(mutex); + std::lock_guard inner(mutex); + ++counter; + } + }); + } + for (auto& worker : workers) worker.join(); + Check(counter == 80000, "every nested increment under the GPU mutex counted"); +} + +template<typename TTag> +void SlotIsolation(const char* what) { + using Slot = HostThreadSlot<std::uint64_t, TTag>; + bool ok = Slot::Get() == 0; + Slot::Set(0x1234567890abcdefull); + std::uint64_t seen = 1; + std::thread other([&] { + seen = Slot::Get(); + Slot::Set(99); + }); + other.join(); + ok = ok && seen == 0 && Slot::Get() == 0x1234567890abcdefull; + Slot::Set(0); + Check(ok, what); +} + +struct DirectSlot {}; +struct ExpansionSlot {}; +struct PointerSlot {}; + +void ThreadSlots() { + SlotIsolation<DirectSlot>("a thread slot starts at zero on every thread and keeps its own value"); + int object = 0; + HostThreadSlot<int*, PointerSlot>::Set(&object); + Check(HostThreadSlot<int*, PointerSlot>::Get() == &object, "a pointer round-trips through a thread slot"); +#ifdef _WIN32 + std::vector<DWORD> held; + for (DWORD index = TlsAlloc(); index != TLS_OUT_OF_INDEXES; index = TlsAlloc()) { + held.push_back(index); + if (index >= 64) break; + } + SlotIsolation<ExpansionSlot>("an expansion thread slot starts at zero on every thread and keeps its own value"); + for (const auto index : held) TlsFree(index); +#endif + AgcDriver::GuestMemory::PacketTag fresh{}; + AgcDriver::GuestMemory::ReadSite freshSite = AgcDriver::GuestMemory::ReadSite::Count; + std::thread other([&] { + fresh = AgcDriver::GuestMemory::CurrentPacket(); + freshSite = AgcDriver::GuestMemory::CurrentReadSite(); + }); + other.join(); + Check(fresh.opcode == AgcDriver::GuestMemory::NoPacket && fresh.queue == 0xffffffffu, "a thread that ran no packet reads NoPacket"); + Check(freshSite == AgcDriver::GuestMemory::ReadSite::Unknown, "a thread that named no read site reads Unknown"); + AgcDriver::GuestMemory::SetCurrentPacket(0xffffu, 0x2au); + const auto packet = AgcDriver::GuestMemory::CurrentPacket(); + Check(packet.opcode == 0xffffu && packet.queue == 0x2au, "the packet tag reads back what was set"); + const auto previous = AgcDriver::GuestMemory::SetReadSite(AgcDriver::GuestMemory::ReadSite::Scanout); + Check(previous == AgcDriver::GuestMemory::ReadSite::Unknown && AgcDriver::GuestMemory::CurrentReadSite() == AgcDriver::GuestMemory::ReadSite::Scanout, "SetReadSite returns the previous site and sets the new one"); + AgcDriver::GuestMemory::SetReadSite(previous); +} + +} + +int main() { + MutexCounts(8, 100000, 0); + MutexCounts(4, 2000, 20000); + MutexTryLock(); + MutexWakesSleeper(); + DeviceGate(); + GpuMutexRecursion(); + ThreadSlots(); + if (failures != 0) { + std::fprintf(stderr, "%d lock check(s) failed\n", failures); + return 1; + } + std::printf("lock tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libc/include/HostThreadSlot.hpp b/core/libs/prx/libc/include/HostThreadSlot.hpp new file mode 100644 index 0000000000..39e0e0804d --- /dev/null +++ b/core/libs/prx/libc/include/HostThreadSlot.hpp @@ -0,0 +1,79 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADSLOT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADSLOT_HPP + +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <type_traits> + +#if defined(_WIN32) && defined(__x86_64__) && defined(__GNUC__) +#define HOST_THREAD_SLOT_TEB 1 +#include <processthreadsapi.h> +#endif + +template<typename TValue, typename TTag> +class HostThreadSlot { + static_assert(std::is_trivially_copyable_v<TValue> && sizeof(TValue) <= sizeof(std::uintptr_t)); + +public: + static TValue Get() noexcept { + const auto bits = load(); + TValue value; + std::memcpy(&value, &bits, sizeof(TValue)); + return value; + } + + static void Set(TValue value) noexcept { + std::uintptr_t bits = 0; + std::memcpy(&bits, &value, sizeof(TValue)); + store(bits); + } + +private: +#ifdef HOST_THREAD_SLOT_TEB + static constexpr unsigned long DirectSlots = 64; + static constexpr std::uintptr_t DirectSlotsOffset = 0x1480; + static constexpr std::uintptr_t ExpansionSlotsOffset = 0x1780; + + static unsigned long index() noexcept { + static const unsigned long allocated = [] { + const auto value = TlsAlloc(); + if (value == 0xffffffffu) std::abort(); + return value; + }(); + return allocated; + } + + static std::uintptr_t readTeb(std::uintptr_t offset) noexcept { + std::uintptr_t value; + __asm__ __volatile__("movq %%gs:(%1), %0" : "=r"(value) : "r"(offset)); + return value; + } + + static std::uintptr_t load() noexcept { + const auto slot = index(); + if (slot < DirectSlots) return readTeb(DirectSlotsOffset + slot * sizeof(void*)); + const auto* expansion = reinterpret_cast<const std::uintptr_t*>(readTeb(ExpansionSlotsOffset)); + return expansion != nullptr ? expansion[slot - DirectSlots] : 0; + } + + static void store(std::uintptr_t bits) noexcept { + const auto slot = index(); + if (slot < DirectSlots) { + __asm__ __volatile__("movq %0, %%gs:(%1)" : : "r"(bits), "r"(DirectSlotsOffset + slot * sizeof(void*)) : "memory"); + } else if (!TlsSetValue(slot, reinterpret_cast<void*>(bits))) { + std::abort(); + } + } +#else + static std::uintptr_t& bitsOfThread() noexcept { + static thread_local std::uintptr_t bits = 0; + return bits; + } + + static std::uintptr_t load() noexcept { return bitsOfThread(); } + static void store(std::uintptr_t bits) noexcept { bitsOfThread() = bits; } +#endif +}; + +#endif From fcc01c57a6d756f9281295b17fa105e6cae3b052 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Fri, 23 Oct 2026 04:32:33 -0400 Subject: [PATCH 1218/1360] fix(libkernel): give a raise target time to run between host-code samples RaiseOn suspends the target, reads its context and, while it is in host code, resumes it and samples again after SwitchToThread. On a 16-core Ryzen 9 5950X (Windows 11 22H2) that resampled the writer of guest_raise_in_host_code about every 10 us before it could run: in the raise that hung, 4,989 samples over 50 ms all found it in ucrtbase, so the 50 ms host-code limit ran out and the handler was redirected into fwrite, where it deadlocked on the stream lock (the test timed out at 30 s on every run). In a passing raise the samples sat at one RIP for 14,695 of 14,703 samples and the raise took 145 ms. Spin for 20 us before each new host-code sample, so the target gets to leave host code. With that, guest_raise_in_host_code passes 5 of 5 runs, no raise is redirected in host code, and the longest raise is 0.46 to 0.53 ms. Other samples keep SwitchToThread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/System/src/Exception.cpp | 8 +++++++- docs/dev/debt/libkernel.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index e3a768008f..8508d317d0 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -193,6 +193,7 @@ constexpr std::size_t RedZone = 128; constexpr std::size_t HomeArea = 32; constexpr auto RetryLimit = std::chrono::seconds(1); constexpr auto HostCodeLimit = std::chrono::milliseconds(50); +constexpr auto HostCodeResample = std::chrono::microseconds(20); constexpr std::size_t VectorBytes = 16 * 32; struct Delivery { @@ -449,7 +450,12 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { continue; } if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); - SwitchToThread(); + if (running == CodeKind::Host) { + const auto resample = std::chrono::steady_clock::now() + HostCodeResample; + while (std::chrono::steady_clock::now() < resample) YieldProcessor(); + } else { + SwitchToThread(); + } } const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast<DWORD64>(15); if (!StackWritable(slot - HomeArea - 8, delivery.context.Rsp - RedZone)) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 9244e5c018..da692cacbd 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -162,7 +162,7 @@ - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds. The raising thread spins for 20 µs before each new host-code sample: suspended again at once, the target could stay at the same instruction for every sample until the 50 ms below ran out (on a 16-core Ryzen 9 5950X, `guest_raise_in_host_code` then raised into `fwrite` and deadlocked on its stream lock), and the 20 µs is a measured choice, not a console value; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread From 56dfc1327eedb23ff97622b6c59cb2e7c9c024bc Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:36:58 -0400 Subject: [PATCH 1219/1360] fix(libSceSystemService): answer the registry-backed integer parameters with values measured on a PS5 --- core/libs/prx/libSceSystemService/Export.cpp | 23 +++++++++--- .../libs/tests/GuestSystemServiceLanguage.cpp | 37 +++++++++++-------- docs/dev/debt/libSceSystemService.md | 4 +- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 3da3b74835..ad7c9d9937 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,3 +1,4 @@ +#include <algorithm> #include <cerrno> #include <chrono> #include <cstddef> @@ -30,6 +31,17 @@ namespace { +struct MeasuredParameter { + int id; + int value; +}; + +constexpr MeasuredParameter RegistryParameters[] = { + {SYSTEM_SERVICE_PARAM_ID_100, 0}, {101, 1}, {102, 2}, {103, 100}, {104, 0x20202}, {105, 0}, {106, 0}, {107, 100}, {108, 0}, + {109, 100}, {110, 0x20202}, {111, 0}, {200, 0}, {201, 0}, {202, 0}, {203, 0}, {204, 0}, {205, 0}, {206, 0}, {207, 0}, + {208, 0}, {209, 1}, {210, 71}, {211, 0}, {212, 0}, {213, 0}, {300, 0}, +}; + struct ConsoleTimeZone { int minutes; bool summer; @@ -309,12 +321,11 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; - case SYSTEM_SERVICE_PARAM_ID_100: *value = 0; break; - default: - if ((paramId >= 101 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { - NotImplemented_nid_no_patch("sceSystemServiceParamGetInt: registry-backed parameter"); - } - return SYSTEM_SERVICE_ERROR_PARAMETER; + default: { + const auto measured = std::ranges::find(RegistryParameters, paramId, &MeasuredParameter::id); + if (measured == std::ranges::end(RegistryParameters)) return SYSTEM_SERVICE_ERROR_PARAMETER; + *value = measured->value; + } } return SYSTEM_SERVICE_OK; } diff --git a/core/libs/tests/GuestSystemServiceLanguage.cpp b/core/libs/tests/GuestSystemServiceLanguage.cpp index 4c4e769974..2be7c170c6 100644 --- a/core/libs/tests/GuestSystemServiceLanguage.cpp +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -4,6 +4,7 @@ #include <limits> #include <stdexcept> #include <string> +#include <utility> extern "C" int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value); @@ -38,6 +39,11 @@ bool ParameterRejected(int paramId) { return false; } +bool Answers(int paramId) { + return (paramId >= 1 && paramId <= 5) || (paramId >= SYSTEM_SERVICE_PARAM_ID_100 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || + paramId == 300 || paramId == SYSTEM_SERVICE_PARAM_ID_400; +} + bool Rejected(const char* value) { SetLanguage(value); return ParameterRejected(SYSTEM_SERVICE_PARAM_ID_LANG) && ParameterRejected(SYSTEM_SERVICE_PARAM_ID_400); @@ -46,21 +52,11 @@ bool Rejected(const char* value) { } int main() { - for (int paramId = -1; paramId <= 1001; ++paramId) { + for (int paramId = -1; paramId <= 2048; ++paramId) { Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - if ((paramId >= 1 && paramId <= 5) || paramId == SYSTEM_SERVICE_PARAM_ID_100 || paramId == 400) continue; + if (Answers(paramId)) continue; int value = -12345; - if ((paramId >= 101 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { - bool threw = false; - try { - static_cast<void>(sceSystemServiceParamGetInt(paramId, &value)); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); - } else { - Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_ERROR_PARAMETER); - } + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(value == -12345); } for (int paramId : {std::numeric_limits<int>::min(), std::numeric_limits<int>::max()}) { @@ -92,7 +88,16 @@ int main() { Require(unknown == -1); Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); } - int param100 = -1; - Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_100, ¶m100) == SYSTEM_SERVICE_OK); - Require(param100 == 0); + constexpr std::pair<int, int> measured[] = { + {100, 0}, {101, 1}, {102, 2}, {103, 100}, {104, 0x20202}, {105, 0}, {106, 0}, {107, 100}, {108, 0}, {109, 100}, {110, 0x20202}, + {111, 0}, {200, 0}, {201, 0}, {202, 0}, {203, 0}, {204, 0}, {205, 0}, {206, 0}, {207, 0}, {208, 0}, {209, 1}, {210, 71}, + {211, 0}, {212, 0}, {213, 0}, {300, 0}, + }; + for (const auto& [paramId, expected] : measured) { + Require(Answers(paramId)); + int value = -1; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_OK); + Require(value == expected); + Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + } } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 176b34aa6a..c11d910836 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -18,7 +18,7 @@ - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp): the native export takes one URI argument. Null input, the 6015-byte length limit and supported scheme checks are recovered. The host still returns kernel `EOPNOTSUPP` for valid input; a missing browser service's native error is unresolved. - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 and 400 answer; 101 to 111, 200 to 213 and 300 throw (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0, which is returned here too, although what the parameter means is not known. Whether the console writes the value on that error is not measured +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 @@ -150,7 +150,7 @@ - [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. -- [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 101–111, 200–213 and 300 throw because their settings are not modeled; 100 answers the value 0 measured on a PS5. Language results omit the console SDK-version compatibility remapping, and parameter 400 does not reproduce platform-dependent availability. +- [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 100–111, 200–213 and 300 answer the values a PS5 (firmware 13.40, measured 2026-10-23 inside a retail title) returned with its settings at the time: 101 is 1, 102 is 2, 103, 107 and 109 are 100, 104 and 110 are 0x20202, 209 is 1, 210 is 71 and the others are 0. What each parameter means, and which of them follow a user setting, is not known, so every title gets that one console's values. Language results omit the console SDK-version compatibility remapping, and parameter 400 does not reproduce platform-dependent availability. - [Tournament launch parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): occurrence parameters occupy 32 bytes and contain two string pointers and a user ID. The final string semantics are unresolved. Event-details and team-profile launches retain an unused first argument slot of unknown declared type. The daemon blocking-event receive argument is passed unchanged as 64 bits to IPC; its meaning remains unresolved. Protected bodies in [PendingExports.cpp](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) remain unchanged. - [sceSystemServiceIsBgmPlaying, sceShellCoreUtilIsBgmPlaying](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - not measured on a PS5: both return 0. In [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/system/systemservice.cpp) they report whether its libSceMusicPlayerService is playing; there is no libSceMusicPlayerService here, so no background music can be playing From 54a5b24b5f8094fc6280db85911588c9b0143fbb Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 21:44:50 +1300 Subject: [PATCH 1220/1360] fix(agc): share the empty-indirect skip state between draw and finishDraw The draw was split into draw and finishDraw, so the skip flag and the per-program classification cache declared in draw were out of scope where finishDraw records the classification. Indirect draws never hand off to the finisher, so both functions run on the same thread; make the cache and the switch file-scope accessors that both use. --- .../Execution/src/Driver/Draw/Draw.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 22152108e5..d0eb0370f3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -192,6 +192,16 @@ DrawFinisher::Pending heldWrites(const DrawTail& tail) { return held; } +bool EmptyIndirectSkip() { + static const bool enabled = std::getenv("APS5_NO_EMPTY_INDIRECT_SKIP") == nullptr; + return enabled; +} + +std::unordered_map<std::uint64_t, bool>& CpuIndirectPrograms() { + thread_local std::unordered_map<std::uint64_t, bool> programs; + return programs; +} + } DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected) { @@ -387,8 +397,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe programs = decode->programs; for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); - static const bool emptyIndirectSkip = std::getenv("APS5_NO_EMPTY_INDIRECT_SKIP") == nullptr; - thread_local std::unordered_map<std::uint64_t, bool> cpuIndirectPrograms; + const bool emptyIndirectSkip = EmptyIndirectSkip(); + auto& cpuIndirectPrograms = CpuIndirectPrograms(); const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); }; if (emptyIndirectSkip && drawParameters.indirect && !programs.empty()) { const auto known = cpuIndirectPrograms.find(programs.front().binary.codeAddress); @@ -776,7 +786,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { indirectCpu = ClassifyIndirectDraw(result, graphics, programs.front(), localDevice, drawParameters, traceIndirect); - if (emptyIndirectSkip) cpuIndirectPrograms[programs.front().binary.codeAddress] = indirectCpu.has_value(); + if (EmptyIndirectSkip()) CpuIndirectPrograms()[programs.front().binary.codeAddress] = indirectCpu.has_value(); } else if (i == 0) { fold(result, drawParameters); } From ebba43332370e7146a85c16046b76d2a4d1a3f9b Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:18:36 +0530 Subject: [PATCH 1221/1360] fix(libSceSharePlay): return invalid args error for invalid prohibition in sceSharePlayProhibitController Return SCE_SHARE_PLAY_ERROR_INVALID_ARGS (0x810E0001) instead of throwing APS5_INVALID_ARG_EX when an invalid prohibition value is passed to sceSharePlayProhibitController. Update regression test assertions in GuestSharePlay to verify the error code and record technical debt. --- core/libs/prx/libSceSharePlay/Export.cpp | 2 +- core/libs/tests/GuestSharePlay.cpp | 4 ++-- docs/dev/debt/libSceSharePlay.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index a1dcf5eadf..848b32427a 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -41,7 +41,7 @@ int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* in int APS5_VABI sceSharePlayProhibitController(std::uint8_t prohibited) { std::lock_guard lock(g_mutex); if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; - if (prohibited != 0 && prohibited != 1) APS5_INVALID_ARG_EX; + if (prohibited != 0 && prohibited != 1) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; return 0; } diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index 269d05fb4d..4d1b7f1199 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -50,8 +50,8 @@ int main() { Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(0) == 0); - Require(Throws([] { sceSharePlayProhibitController(2); })); - Require(Throws([] { sceSharePlayProhibitController(255); })); + Require(sceSharePlayProhibitController(2) == invalidArgument); + Require(sceSharePlayProhibitController(255) == invalidArgument); Require(sceSharePlayTerminate() == 0); std::memset(&info, 0x5a, sizeof(info)); Require(sceSharePlayGetCurrentConnectionInfoA(&info) == notInitialized); diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md index cd999b7fab..f7255df0d8 100644 --- a/docs/dev/debt/libSceSharePlay.md +++ b/docs/dev/debt/libSceSharePlay.md @@ -9,4 +9,4 @@ - [sceSharePlayInitialize and sceSharePlayTerminate](../../../core/libs/prx/libSceSharePlay/Export.cpp) - native heap validation and initialization state are recovered: a null heap ignores the supplied size, a supplied heap requires at least 0x1800 bytes, repeated initialization returns 0x810E0003 and termination before initialization returns 0x810E0004. The host does not allocate the native internal heap, connect to the Share Play service or reproduce its failures. - [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the native module confirms a 72-byte result, initial zero fields and both user IDs -1. Initialization is checked before the output pointer. The host retains this initial disconnected state; native event draining, cached updates and service reconnection are unimplemented. -- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the argument is one byte, forwarded unchanged to the service. The host checks initialization and accepts 0/1 without a visitor controller; other byte values still throw because their handling belongs to the unimplemented service. +- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the argument is one byte, forwarded unchanged to the service. The host checks initialization, accepts 0/1 without a visitor controller, and returns 0x810E0001 (SCE_SHARE_PLAY_ERROR_INVALID_ARGS) for other values; dynamic service communication and controller state remain unimplemented. From 3fe721780714ddf5b0580b12a4f48a38fb6e2f99 Mon Sep 17 00:00:00 2001 From: VakzOs <eric.prevost2@gmail.com> Date: Fri, 23 Oct 2026 09:20:18 +0000 Subject: [PATCH 1222/1360] test(libSceLibreSsl): set up the DES test for unwinding on Windows The DES test catches std::invalid_argument thrown through the library's sysv_abi frames; like the digest test, it now links libc and calls configure_windows_unwind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzhWRMauD3ouc8kSxT2NnT --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 0ac9f475d7..3e52df3916 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -17,7 +17,8 @@ if(BUILD_TESTING) add_test_executable(guest_libressl_des_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDes.cpp) target_include_directories(guest_libressl_des_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_libressl_des_tests PRIVATE libSceLibreSsl) + target_link_libraries(guest_libressl_des_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_des_tests) add_test(NAME guest_libressl_des COMMAND guest_libressl_des_tests) set_tests_properties(guest_libressl_des PROPERTIES TIMEOUT 20) if(WIN32) From 835c2995ed2efaa8e4208d66890b3fd5cb025e7e Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 22:40:29 +1300 Subject: [PATCH 1223/1360] feat(libSceUserService): report the keyboard type of the system language sceUserServiceGetKeyboardType now returns the keyboard type of the configured system language from the repository owner's table (languages 0 to 29), with 20 (US English) for a language outside it. The input variants stay 0, the only value the table gives for Japanese and Traditional Chinese. sceConvertKeycodeGetImeKeyboardType takes the PS4 client rule from the title ID prefix (CUSA is PS4); a missing or custom ID is treated as PS5. Tests walk the owner's table through both calls and the title ID cases; both debt entries updated. --- .../prx/libSceConvertKeycode/CMakeLists.txt | 2 +- core/libs/prx/libSceConvertKeycode/Export.cpp | 13 ++++++-- .../libSceConvertKeycode/ImeKeyboardType.hpp | 5 +++ .../libs/prx/libSceUserService/CMakeLists.txt | 7 ++-- core/libs/prx/libSceUserService/Export.cpp | 33 +++++++++++++++---- core/libs/tests/GuestConvertKeycode.cpp | 28 ++++++++++++++++ core/libs/tests/GuestUserService.cpp | 25 ++++++++++++++ docs/dev/debt/libSceConvertKeycode.md | 2 +- docs/dev/debt/libSceUserService.md | 2 +- 9 files changed, 102 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt index d04045b5c6..34d023d5ac 100644 --- a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt +++ b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt @@ -6,7 +6,7 @@ configure_windows_unwind(libSceConvertKeycode) if(BUILD_TESTING) add_test_executable(guest_convert_keycode_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycode.cpp) target_include_directories(guest_convert_keycode_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libSceUserService libc) + target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libSceUserService libSceSystemService libc) configure_windows_unwind(guest_convert_keycode_tests) add_test(NAME guest_convert_keycode COMMAND guest_convert_keycode_tests) if(WIN32) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 24e61537bb..11d1fb7f32 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -7,6 +7,8 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceConvertKeycode/ImeKeyboardType.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include <string_view> extern "C" { int APS5_VABI sceUserServiceGetKeyboardType(int32_t userId, int32_t* type); @@ -2402,13 +2404,20 @@ char16_t ConvertCharacter(std::uint16_t keycode, std::uint32_t state, std::uint3 } return character; } +bool Ps4Client() { + try { + const auto titleId = GetAppTitleId_nid_postfix(); + return ImeKeyboardType::IsPs4TitleId(std::string_view(titleId.value, strnlen(titleId.value, sizeof(titleId.value)))); + } catch (const std::runtime_error&) { + return false; + } +} } // namespace extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(int32_t userId, uint32_t* type) { - // The PS4/PS5 client rule: AnyPS5 only runs PS5 processes (sceKernelIsPs4Process is unimplemented and reports 0). - constexpr bool ps4Client = false; + const bool ps4Client = Ps4Client(); const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); std::uint32_t sdkVersion; std::memcpy(&sdkVersion, process + 16, sizeof(sdkVersion)); diff --git a/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp b/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp index fcf9ccbe16..4ace3c252c 100644 --- a/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp +++ b/core/libs/prx/libSceConvertKeycode/ImeKeyboardType.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCECONVERTKEYCODE_IMEKEYBOARDTYPE_HPP #include <cstdint> +#include <string_view> // sceConvertKeycodeGetImeKeyboardType: the UserService keyboard type (plus the Japanese and Traditional Chinese // input variants) to IME keyboard type mapping and the SDK compatibility rules, as specified by the repository @@ -46,6 +47,10 @@ constexpr std::uint32_t ApplySdkCompatibility(std::uint32_t imeType, std::uint32 return imeType; } +constexpr bool IsPs4TitleId(std::string_view titleId) { + return titleId.substr(0, 4) == "CUSA"; +} + template <typename GetKeyboardType, typename GetJapaneseInputType, typename GetTraditionalChineseInputType> int Resolve(std::int32_t userId, std::uint32_t* type, std::uint32_t sdkVersion, bool ps4Client, GetKeyboardType getKeyboardType, GetJapaneseInputType getJapaneseInputType, GetTraditionalChineseInputType getTraditionalChineseInputType) { if (type == nullptr) return kErrInvalidAddress; diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index 6d606a67e8..c78fcbc8d7 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceUserService SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceUserService PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceUserService PRIVATE libSceSystemService) configure_windows_unwind(libSceUserService) 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) + target_link_libraries(guest_user_service_tests PRIVATE libSceUserService libSceSystemService) configure_windows_unwind(guest_user_service_tests) add_test(NAME guest_user_service COMMAND guest_user_service_tests) if(WIN32) @@ -17,7 +18,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_user_service_user_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceUserName.cpp) target_include_directories(guest_user_service_user_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService) + target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService libSceSystemService) add_test(NAME guest_user_service_user_name COMMAND guest_user_service_user_name_tests) if(WIN32) set_tests_properties(guest_user_service_user_name PROPERTIES ENVIRONMENT_MODIFICATION @@ -28,7 +29,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_user_service_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceLifecycle.cpp) target_include_directories(guest_user_service_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_user_service_lifecycle_tests PRIVATE libSceUserService) + target_link_libraries(guest_user_service_lifecycle_tests PRIVATE libSceUserService libSceSystemService) add_test(NAME guest_user_service_lifecycle COMMAND guest_user_service_lifecycle_tests) if(WIN32) set_tests_properties(guest_user_service_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 6bd5434acd..513d813d46 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -21,17 +21,36 @@ static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t val } -// Per-user keyboard settings. Nothing in AnyPS5 stores a keyboard language or input variant yet, so every known user -// reports the same default: 20 (US English) for the keyboard type, 0 for the input variants (see +extern "C" int APS5_VABI sceSystemServiceParamGetInt(int param_id, int* value); + +// Per-user keyboard settings. Nothing in AnyPS5 stores a keyboard type or input variant, so every known user reports +// the keyboard type of the configured system language and 0 for the input variants (see // docs/dev/debt/libSceUserService.md). -static int ReportKeyboardSetting(int user_id, int32_t* type, int32_t value) { +static int32_t KeyboardTypeForSystemLanguage() { + constexpr int param_id_lang = 1; + constexpr int32_t fallback = 20; + constexpr int32_t keyboard_types[] = {37, 20, 26, 21, 17, 28, 29, 33, 34, 38, 39, 40, 23, 35, 16, + 30, 31, 32, 19, 36, 22, 41, 25, 44, 48, 43, 47, 42, 46, 45}; + int language = -1; + if (sceSystemServiceParamGetInt(param_id_lang, &language) != 0 || language < 0 || + language >= static_cast<int>(sizeof(keyboard_types) / sizeof(keyboard_types[0]))) { + return fallback; + } + return keyboard_types[language]; +} + +static int32_t NoInputVariant() { + return 0; +} + +static int ReportKeyboardSetting(int user_id, int32_t* type, int32_t (*value)()) { if (type == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } if (user_id != USER_SERVICE_INITIAL_USER_ID) { return USER_SERVICE_ERROR_UNKNOWN_USER; } - *type = value; + *type = value(); return USER_SERVICE_OK; } @@ -234,15 +253,15 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) { } int APS5_VABI sceUserServiceGetKeyboardType(int user_id, int32_t* type) { - return ReportKeyboardSetting(user_id, type, 20); + return ReportKeyboardSetting(user_id, type, KeyboardTypeForSystemLanguage); } int APS5_VABI sceUserServiceGetJapaneseInputType(int user_id, int32_t* type) { - return ReportKeyboardSetting(user_id, type, 0); + return ReportKeyboardSetting(user_id, type, NoInputVariant); } int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, int32_t* type) { - return ReportKeyboardSetting(user_id, type, 0); + return ReportKeyboardSetting(user_id, type, NoInputVariant); } int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) { diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 60721d3937..3bb2493a66 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -9,6 +9,7 @@ #include <cstdlib> #include <initializer_list> #include <stdexcept> +#include <string> #include <utility> extern "C" { @@ -19,6 +20,14 @@ int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::u static void Require(bool value) { if (!value) std::abort(); } +static void SetLanguage(const char* value) { +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_LANGUAGE", value != nullptr ? value : "") == 0); +#else + Require((value != nullptr ? ::setenv("ANYPS5_LANGUAGE", value, 1) : ::unsetenv("ANYPS5_LANGUAGE")) == 0); +#endif +} + namespace { struct FakeUser { @@ -148,7 +157,26 @@ int main() { Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == invalidUserId); Require(type == 0xFFFFFFFFu); + SetLanguage(nullptr); Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4); + constexpr std::uint32_t imeTypeByLanguage[] = {22, 4, 9, 6, 2, 13, 14, 18, 19, 24, 26, 25, 8, 20, 1, 15, 16, 17, 5, 21, 7, 30, 11, 32, 37, 33, 36, 31, 35, 34}; + for (int language = 0; language < 30; ++language) { + SetLanguage(std::to_string(language).c_str()); + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == imeTypeByLanguage[language]); + } + SetLanguage("30"); + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4); + SetLanguage(nullptr); + + Require(ImeKeyboardType::IsPs4TitleId("CUSA00000")); + Require(ImeKeyboardType::IsPs4TitleId("CUSA")); + Require(!ImeKeyboardType::IsPs4TitleId("PPSA00000")); + Require(!ImeKeyboardType::IsPs4TitleId("")); + Require(!ImeKeyboardType::IsPs4TitleId("CUS")); + Require(!ImeKeyboardType::IsPs4TitleId("cusa00000")); + Require(!ImeKeyboardType::IsPs4TitleId("XCUSA0000")); + Require(!ImeKeyboardType::IsPs4TitleId("HOMEBREW1")); RequireImeKeyboardTypeMapping(); Require(sceConvertKeycodeGetCharacter(4, 0, 0, nullptr) == invalidAddress); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index b40b52e694..b913ced298 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -3,6 +3,7 @@ #include <cstdint> #include <cstdlib> #include <stdexcept> +#include <string> #include <utility> extern "C" { @@ -30,6 +31,14 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +void SetLanguage(const char* value) { +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_LANGUAGE", value != nullptr ? value : "") == 0); +#else + Require((value != nullptr ? ::setenv("ANYPS5_LANGUAGE", value, 1) : ::unsetenv("ANYPS5_LANGUAGE")) == 0); +#endif +} + } int main() { @@ -62,6 +71,7 @@ int main() { Require(sceUserServiceGetUserColor(123, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK); + SetLanguage(nullptr); using KeyboardSetting = decltype(&sceUserServiceGetKeyboardType); const std::pair<KeyboardSetting, std::int32_t> keyboardSettings[] = { {sceUserServiceGetKeyboardType, 20}, {sceUserServiceGetJapaneseInputType, 0}, {sceUserServiceGetTraditionalChineseInputType, 0}}; @@ -72,6 +82,21 @@ int main() { Require(setting(USER_SERVICE_USER_ID_INVALID, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A); Require(setting(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == expected); } + constexpr std::int32_t keyboardTypeByLanguage[] = {37, 20, 26, 21, 17, 28, 29, 33, 34, 38, 39, 40, 23, 35, 16, 30, 31, 32, 19, 36, 22, 41, 25, 44, 48, 43, 47, 42, 46, 45}; + for (int language = 0; language < 30; ++language) { + SetLanguage(std::to_string(language).c_str()); + std::int32_t value = 0x5A5A; + Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == keyboardTypeByLanguage[language]); + Require(sceUserServiceGetKeyboardType(123, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == keyboardTypeByLanguage[language]); + for (const auto variant : {sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType}) { + value = 0x5A5A; + Require(variant(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == 0); + } + } + SetLanguage("30"); + std::int32_t fallback = 0x5A5A; + Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &fallback) == USER_SERVICE_OK && fallback == 20); + SetLanguage(nullptr); Require(color == 0); std::uint64_t account_id = 0xdeadbeefu; diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 01812a5b99..9255ca607c 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -3,4 +3,4 @@ ### Functional - [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. -- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule assumes a PS5 process because `sceKernelIsPs4Process` is unimplemented and AnyPS5 runs PS5 processes. UserService has no stored keyboard setting yet, so every user reports the US-English default keyboard type 20 and the function returns IME type 4 until one is stored (see libSceUserService.md). +- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing or custom ID, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md). diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 65ca188348..bab39a1f05 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,4 +8,4 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output -- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the US-English default 20 for the keyboard type (the owner's table maps it to IME keyboard type 4) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The repository owner allows initialising the keyboard type from the system language through an explicit conversion to UserService keyboard-type ids; that table is not recovered yet, so 20 stays the fixed default until it exists and no other value is guessed. `sceConvertKeycodeGetImeKeyboardType` consumes these values. +- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's and covers languages 0 to 29; a language outside it, such as Ukrainian (30), reports 20 (US English). The input variant is 0 because the owner's table needs one only for Japanese and Traditional Chinese, and gives 0 for both. `sceConvertKeycodeGetImeKeyboardType` consumes these values. From 16686375a9f011d2b178f1804eacb9512a4fe9a2 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 22:57:56 +1300 Subject: [PATCH 1224/1360] fix(agc): keep the empty-indirect classification cache in HostThreadLocal On Windows a thread_local destructor in a module runs after emulated TLS freed the object (prx_thread_local_destructors). Keep the per-thread map in HostThreadLocal like the other per-thread objects in libSceAgcDriver. --- .../prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index d0eb0370f3..c80c0406ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include <atomic> #include <array> #include <algorithm> @@ -197,9 +198,10 @@ bool EmptyIndirectSkip() { return enabled; } +struct CpuIndirectProgramsStorage {}; + std::unordered_map<std::uint64_t, bool>& CpuIndirectPrograms() { - thread_local std::unordered_map<std::uint64_t, bool> programs; - return programs; + return HostThreadLocal<std::unordered_map<std::uint64_t, bool>, CpuIndirectProgramsStorage>(); } } From 7410c37b6cca84034f1352193ee0edb537e5633d Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:47:54 +0200 Subject: [PATCH 1225/1360] perf(shader): emit exact f32 add, subtract and multiply as fma on MoltenVK The recompiler marks every guest f32 add, subtract and multiply NoContraction, because the guest rounds each one separately. SPIRV-Cross turns each of them into a call to a helper that Metal compiles without optimization, which dominates fragment time on MoltenVK, and its multiply helper, fma(a, b, +0.0), turns a -0.0 product into +0.0. On MoltenVK they are now GLSL Fma whose second step is exact, which gives the separately rounded result: a * b is fma(a, b, -0.0), a + b is fma(1.0, a, b) and a - b is fma(-1.0, b, a). The sin, cos and log2 series and the offset image coordinates and gather texel scale use the same forms. The choice is a SpirvTarget flag set for VK_DRIVER_ID_MOLTENVK, part of the shader cache key and of the request serialization (version 44); other drivers are unchanged. agc_driver_f32_separate_rounding checks a multiply feeding an add or a subtract on rows where fusing them changes the result. It, agc_driver_f32_arithmetic_nan and agc_driver_f32_denormal_flush also run the fused forms on devices that do not use them, so lavapipe checks them against the same rows. Co-authored-by: Ycaro Gabriel <ycaro.gabriel@performait.com> --- .../Execution/src/VulkanDevice.cpp | 3 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 72 ++++++- .../tests/execution/F32ArithmeticNan.cpp | 20 +- .../tests/execution/F32DenormalFlush.cpp | 12 +- .../tests/execution/F32SeparateRounding.cpp | 177 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 37 +++- .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 28 ++- .../tests/RequestSerializerTests.cpp | 47 +++-- 15 files changed, 364 insertions(+), 44 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F32SeparateRounding.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 606ab10f61..2c4844ed58 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -321,6 +321,7 @@ struct VulkanDevice::State { bool meshShader = false; bool fragmentShaderBarycentric = false; bool shaderTerminateInvocation = false; + bool fusedExactArithmetic = false; bool geometryShader = false; bool largePoints = false; bool sampleRateShading = false; @@ -1220,6 +1221,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; + state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions); @@ -3739,6 +3741,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); auto target = state->shaderProfile->Target(); target.float32FlushToZero = state->float32FlushToZero; + target.fusedExactArithmetic = state->fusedExactArithmetic; target.float32DenormPreserve = state->float32DenormPreserve; return target; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 8d8b12086e..46feef9192 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -31,6 +31,7 @@ #include "ShaderDiskCache.hpp" #include <spirv/unified1/spirv.hpp> #include <algorithm> +#include <set> #include <array> #include <cstddef> #include <cstdlib> @@ -4003,6 +4004,51 @@ void verifyLanesOutsideHostSubgroup() { require(pixel(permlanex16, 32u) && pixel(permlane16, 8u), "host lanes: a v_permlane16_b32 row that the host subgroup may hold did not build"); } +void verifyFusedExactArithmetic() { + using namespace ShaderRecompiler; + const auto pixel = [](bool fused) { + static const std::vector<std::uint32_t> code{0x7e000200u, 0x10020100u, 0x06040101u, 0x08060302u, 0x7e000303u, 0xf800180fu, 0u, 0xbf810000u}; + ShaderPixelStageInfo info{}; + info.inputAddr = PixelInputBit(PixelInput::PositionX); + info.posX = 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 = 64; + request.target.fusedExactArithmetic = fused; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + struct Counts { + std::uint32_t exactArithmetic = 0; + std::uint32_t fmas = 0; + }; + const auto count = [](std::span<const std::uint32_t> spirv) { + std::set<std::uint32_t> exact; + std::vector<std::uint32_t> arithmetic; + Counts result; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + const auto op = spirv[cursor] & 0xffffu; + const auto words = spirv[cursor] >> 16u; + if (op == spv::OpDecorate && words == 3u && spirv[cursor + 2] == spv::DecorationNoContraction) exact.insert(spirv[cursor + 1]); + if (op == spv::OpFAdd || op == spv::OpFSub || op == spv::OpFMul) arithmetic.push_back(spirv[cursor + 2]); + if (op == spv::OpExtInst && words >= 5u && spirv[cursor + 4] == 50u) ++result.fmas; + } + result.exactArithmetic = static_cast<std::uint32_t>(std::count_if(arithmetic.begin(), arithmetic.end(), [&](std::uint32_t id) { return exact.contains(id); })); + return result; + }; + const auto plain = count(pixel(false)); + const auto fused = count(pixel(true)); + require(plain.exactArithmetic >= 3u, "fused exact arithmetic: the f32 multiply, add and subtract were not exact operations without the flag"); + require(fused.exactArithmetic == 0u && fused.fmas >= plain.fmas + 3u, "fused exact arithmetic: the f32 multiply, add and subtract did not become fused multiply-adds with the flag"); +} + void verifyHalfWaveReduction() { using namespace ShaderRecompiler; static constexpr std::array<std::uint32_t, 5> capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; @@ -4898,7 +4944,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSsAAAA=", "new requests did not use serialization version 43"); + require(requestPrefix(encoded, 8u) == "NVNQQSwAAAA=", "new requests did not use serialization version 44"); const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -5016,7 +5062,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSwAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS0AAAA="}) { expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5110,6 +5156,7 @@ void verifyVertexInfoVersion19() { version20Vertex.pop_back(); version20Vertex.pop_back(); version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -5121,6 +5168,7 @@ void verifyVertexInfoVersion19() { legacy.pop_back(); legacy.pop_back(); legacy.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast<std::uint8_t>(version); @@ -5168,12 +5216,13 @@ void verifyVertexInfoVersion19() { orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); + orderedVersion34.pop_back(); orderedVersion34.erase(orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset - 1u), orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion34[4] = 34u; const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 4u); + orderedVersion30.resize(orderedVersion30.size() - 5u); orderedVersion30.erase(orderedVersion30.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion30.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -5182,6 +5231,7 @@ void verifyVertexInfoVersion19() { orderedSpanned.target.spannedWaves = true; auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); orderedVersion32.pop_back(); + orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); orderedVersion32.pop_back(); orderedVersion32.pop_back(); @@ -5206,6 +5256,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -5214,6 +5265,7 @@ void verifyVertexInfoVersion19() { spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); version32.pop_back(); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5226,9 +5278,11 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = sampledBytes; version36.pop_back(); + version36.pop_back(); version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5241,6 +5295,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -5250,6 +5305,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -5259,6 +5315,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -5309,10 +5366,11 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 7u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 7 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - pointTailBytes, plainBytes.end(), remappedBytes.end() - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags and the point tail of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 8u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 8 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 1 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 1 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail and the fused arithmetic flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5321,6 +5379,7 @@ void verifyDepthRemapRequests() { } auto version35 = remappedBytes; version35.pop_back(); + version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); @@ -5368,8 +5427,8 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 53u + pointTailBytes && legacy[4] == 43u, "a merged hull request did not use serialization version 43"); - legacy.resize(legacy.size() - pointTailBytes); + require(legacy.size() > 54u + pointTailBytes && legacy[4] == 44u, "a merged hull request did not use serialization version 44"); + legacy.resize(legacy.size() - 1u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); @@ -7764,6 +7823,7 @@ int main(int argc, char** argv) { verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); verifyHalfWaveReduction(); + verifyFusedExactArithmetic(); verifyNarrowVertexWave(); verifySingleLaneVertexWave(); verifyMeshConfiguration(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp index c78f91002f..a4b48e2c5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp @@ -23,6 +23,7 @@ constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 16; alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{}; alignas(256) std::array<std::uint32_t, Threads * Results> Output{}; +bool FusedExactArithmetic = false; alignas(256) constexpr std::array<std::uint32_t, 57> Code{ 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, @@ -172,7 +173,7 @@ std::string Hex(std::uint32_t value) { } void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { - Require(actual == expected, std::string("f32 arithmetic nan: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); + Require(actual == expected, std::string("f32 arithmetic nan: ") + (FusedExactArithmetic ? "fused " : "") + "lane " + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); } bool IsNan32(std::uint32_t value) { @@ -202,6 +203,7 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional<ShaderRecompiler:: }; request.useCache = false; request.context.floatMode = floatMode; + request.target.fusedExactArithmetic = FusedExactArithmetic; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); device.WaitIdle(); @@ -223,12 +225,16 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); - Check(ExpectedNoIeee, "no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); - Check(ExpectedNoIeee, "IEEE=0"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); - Check(ExpectedIeee, "IEEE=1"); + for (const bool fused : {false, true}) { + if (!fused && device->Target().fusedExactArithmetic) continue; + FusedExactArithmetic = fused; + Run(*device, std::nullopt); + Check(ExpectedNoIeee, "no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(ExpectedNoIeee, "IEEE=0"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(ExpectedIeee, "IEEE=1"); + } std::puts("f32 arithmetic nan tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index 19beb4db4c..bb4aa369e0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -382,8 +382,10 @@ std::string Hex(std::uint32_t value) { return text; } +bool FusedExactArithmetic = false; + void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { - Require(actual == expected, std::string("f32 denormal flush: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); + Require(actual == expected, std::string("f32 denormal flush: ") + (FusedExactArithmetic ? "fused " : "") + "lane " + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); } bool IsNan(std::uint32_t value) { @@ -434,6 +436,7 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional<ShaderRecompiler:: request.useCache = false; request.context.floatMode = floatMode; request.target.float32FlushToZero = HardwareFlush; + request.target.fusedExactArithmetic = FusedExactArithmetic; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); device.WaitIdle(); @@ -451,7 +454,7 @@ void Check(const char* mode) { if (hostSplits && tid == TinyProductLane && (i == 4u || i == 5u)) continue; const bool floatResult = i < 12u || i >= 15u; if (floatResult && IsNan(Expected[tid][i])) { - Require(IsNan(out[i]), std::string("f32 denormal flush: lane ") + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(out[i]) + ", expected a NaN"); + Require(IsNan(out[i]), std::string("f32 denormal flush: ") + (FusedExactArithmetic ? "fused " : "") + "lane " + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(out[i]) + ", expected a NaN"); continue; } Expect(tid, out[i], Expected[tid][i], (std::string(mode) + " " + Names[i]).c_str()); @@ -516,9 +519,12 @@ int main() { Require(!softwareDeclares && hardwareDeclares == declared, declared ? "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32" : "f32 denormal flush: a module flushed natively declares DenormFlushToZero 32 without the device capability"); Require(hardwareWords < software, "f32 denormal flush: the hardware-flush module is " + std::to_string(hardwareWords) + " words, the emulated one " + std::to_string(software)); } - for (const bool flush : {false, true}) { + for (const std::uint32_t variant : {0u, 1u, 2u, 3u}) { + const bool flush = (variant & 1u) != 0u; if (flush && !hardware) continue; + if ((variant & 2u) == 0u && device->Target().fusedExactArithmetic) continue; HardwareFlush = flush; + FusedExactArithmetic = (variant & 2u) != 0u; Run(*device, std::nullopt); Check("no float mode"); Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32SeparateRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32SeparateRounding.cpp new file mode 100644 index 0000000000..3556b0538b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32SeparateRounding.cpp @@ -0,0 +1,177 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdint> +#include <cstdio> +#include <iostream> +#include <optional> +#include <string> +#include <vector> + +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 = 8; +alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{}; +alignas(256) std::array<std::uint32_t, Threads * Results> Output{}; +bool FusedExactArithmetic = false; + +alignas(256) constexpr std::array<std::uint32_t, 18> Code{ + 0x34020084u, 0x34060085u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x10140b04u, 0x06160d0au, 0x08180d0au, + 0x0a1a0d0au, 0x061c1506u, 0x081e1506u, 0x06200d04u, 0x10220b10u, 0xe0781000u, 0x80010a03u, 0xe0781010u, + 0x80010e03u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[32][4] = { + {0x3f800800u, 0x3f800c00u, 0xbf801401u, 0x00000000u}, + {0x3f801800u, 0x3f801400u, 0xbf802c04u, 0x00000000u}, + {0x3f802800u, 0x3f801c00u, 0xbf804409u, 0x00000000u}, + {0x3f803800u, 0x3f802400u, 0xbf805c10u, 0x00000000u}, + {0x3f804800u, 0x3f802c00u, 0xbf807419u, 0x00000000u}, + {0x3f805800u, 0x3f803400u, 0xbf808c24u, 0x00000000u}, + {0x3f806800u, 0x3f803c00u, 0xbf80a431u, 0x00000000u}, + {0x3f807800u, 0x3f804400u, 0xbf80bc40u, 0x00000000u}, + {0x3f808800u, 0x3f804c00u, 0xbf80d451u, 0x00000000u}, + {0x3f809800u, 0x3f805400u, 0xbf80ec64u, 0x00000000u}, + {0x3f80a800u, 0x3f805c00u, 0xbf810479u, 0x00000000u}, + {0x3f80b800u, 0x3f806400u, 0xbf811c90u, 0x00000000u}, + {0x3d801000u, 0xc1001800u, 0x3f002804u, 0x00000000u}, + {0x3e005000u, 0xc0803800u, 0x3f008822u, 0x00000000u}, + {0x3e809000u, 0xc0005800u, 0x3f00e864u, 0x00000000u}, + {0x3f00d000u, 0xbf807800u, 0x3f0148c2u, 0x00000000u}, + {0x3f811000u, 0xbf009800u, 0x3f01a944u, 0x00000000u}, + {0x40015000u, 0xbe80b800u, 0x3f0209e2u, 0x00000000u}, + {0x40819000u, 0xbe00d800u, 0x3f026aa4u, 0x00000000u}, + {0x4101d000u, 0xbd80f800u, 0x3f02cb82u, 0x00000000u}, + {0x40400000u, 0x3eaaaaabu, 0xbf800000u, 0x00000000u}, + {0x40540000u, 0x3eabaaabu, 0xc0000000u, 0x00000000u}, + {0x40680000u, 0x3eacaaabu, 0xc0400000u, 0x00000000u}, + {0x407c0000u, 0x3eadaaabu, 0xc0800000u, 0x00000000u}, + {0x40880000u, 0x3eaeaaabu, 0xc0a00000u, 0x00000000u}, + {0x40920000u, 0x3eafaaabu, 0xc0c00000u, 0x00000000u}, + {0x00000000u, 0xbf800000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x3f800000u, 0x80000000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0xbf800000u, 0x00000000u}, + {0x4e800001u, 0x4e7ffffeu, 0xdd800000u, 0x00000000u}, + {0x3f800001u, 0x3f7fffffu, 0xbf800000u, 0x00000000u}, + {0xbfc00000u, 0x35800000u, 0x35000000u, 0x00000000u}, +}; +constexpr std::uint32_t Expected[32][8] = { + {0x3f801401u, 0x00000000u, 0x40001401u, 0xc0001401u, 0x00000000u, 0xc0001401u, 0xb9c01000u, 0xb9c02202u}, + {0x3f802c04u, 0x00000000u, 0x40002c04u, 0xc0002c04u, 0x00000000u, 0xc0002c04u, 0xba202000u, 0xba203905u}, + {0x3f804409u, 0x00000000u, 0x40004409u, 0xc0004409u, 0x00000000u, 0xc0004409u, 0xba604800u, 0xba607910u}, + {0x3f805c10u, 0x00000000u, 0x40005c10u, 0xc0005c10u, 0x00000000u, 0xc0005c10u, 0xba904000u, 0xba906892u}, + {0x3f807419u, 0x00000000u, 0x40007419u, 0xc0007419u, 0x00000000u, 0xc0007419u, 0xbab06400u, 0xbab0a0a2u}, + {0x3f808c24u, 0x00000000u, 0x40008c24u, 0xc0008c24u, 0x00000000u, 0xc0008c24u, 0xbad09000u, 0xbad0e4bau}, + {0x3f80a431u, 0x00000000u, 0x4000a431u, 0xc000a431u, 0x00000000u, 0xc000a431u, 0xbaf0c400u, 0xbaf134dcu}, + {0x3f80bc40u, 0x00000000u, 0x4000bc40u, 0xc000bc40u, 0x00000000u, 0xc000bc40u, 0xbb088000u, 0xbb08c884u}, + {0x3f80d451u, 0x00000000u, 0x4000d451u, 0xc000d451u, 0x00000000u, 0xc000d451u, 0xbb18a200u, 0xbb18fca0u}, + {0x3f80ec64u, 0x00000000u, 0x4000ec64u, 0xc000ec64u, 0x00000000u, 0xc000ec64u, 0xbb28c800u, 0xbb2936c3u}, + {0x3f810479u, 0x00000000u, 0x40010479u, 0xc0010479u, 0x00000000u, 0xc0010479u, 0xbb38f200u, 0xbb3976eeu}, + {0x3f811c90u, 0x00000000u, 0x40011c90u, 0xc0011c90u, 0x00000000u, 0xc0011c90u, 0xbb492000u, 0xbb49bd21u}, + {0xbf002803u, 0x33800000u, 0xbf802804u, 0x3f802804u, 0x33800000u, 0x3f802804u, 0x3f102a04u, 0xc090450cu}, + {0xbf008823u, 0xb3800000u, 0xbf808822u, 0x3f808822u, 0xb3800000u, 0x3f808822u, 0x3f209c22u, 0xc020e266u}, + {0xbf00e863u, 0x33800000u, 0xbf80e864u, 0x3f80e864u, 0x33800000u, 0x3f80e864u, 0x3f413064u, 0xbfc1b535u}, + {0xbf0148c3u, 0xb3800000u, 0xbf8148c2u, 0x3f8148c2u, 0xb3800000u, 0x3f8148c2u, 0x3f810c61u, 0xbf81855du}, + {0xbf01a943u, 0x33800000u, 0xbf81a944u, 0x3f81a944u, 0x33800000u, 0x3f81a944u, 0x3fc1e4a2u, 0xbf42cae2u}, + {0xbf0209e3u, 0xb3800000u, 0xbf8209e2u, 0x3f8209e2u, 0xb3800000u, 0x3f8209e2u, 0x4021d278u, 0xbf22bb17u}, + {0xbf026aa3u, 0x33800000u, 0xbf826aa4u, 0x3f826aa4u, 0x33800000u, 0x3f826aa4u, 0x4091dd54u, 0xbf12d379u}, + {0xbf02cb83u, 0xb3800000u, 0xbf82cb82u, 0x3f82cb82u, 0xb3800000u, 0x3f82cb82u, 0x4109fcb8u, 0xbf0b0812u}, + {0x3f800000u, 0x00000000u, 0x40000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x40000000u, 0x3f2aaaabu}, + {0x3f8e2956u, 0xbf63ad54u, 0x404714abu, 0xc04714abu, 0xbf63ad54u, 0xc04714abu, 0x3fa80000u, 0x3ee15000u}, + {0x3f9c7aabu, 0xbfe38555u, 0x40871eabu, 0xc0871eabu, 0xbfe38555u, 0xc0871eabu, 0x3f200000u, 0x3e57d556u}, + {0x3faaf400u, 0xc02a8600u, 0x40aabd00u, 0xc0aabd00u, 0xc02a8600u, 0xc0aabd00u, 0xbd800000u, 0xbcadaaabu}, + {0x3fb99556u, 0xc0633555u, 0x40ce6556u, 0xc0ce6556u, 0xc0633555u, 0xc0ce6556u, 0xbf400000u, 0xbe830000u}, + {0x3fc85eabu, 0xc08de855u, 0x40f217abu, 0xc0f217abu, 0xc08de855u, 0xc0f217abu, 0xbfb80000u, 0xbefc8556u}, + {0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u}, + {0x3f800000u, 0x00000000u, 0x40000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0x00000000u}, + {0x5d800000u, 0x00000000u, 0x5e000000u, 0xde000000u, 0x00000000u, 0xde000000u, 0xdd800000u, 0xec7ffffeu}, + {0x3f800000u, 0x00000000u, 0x40000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x34000000u, 0x33ffffffu}, + {0xb5c00000u, 0xb5800000u, 0xb6000000u, 0x36000000u, 0xb5800000u, 0x36000000u, 0xbfbffffcu, 0xb5bffffcu}, +}; + +constexpr const char* Names[Results] = { + "v_mul_f32 v10, v4, v5", + "v_add_f32 v11, v10, v6", + "v_sub_f32 v12, v10, v6", + "v_subrev_f32 v13, v10, v6", + "v_add_f32 v14, v6, v10", + "v_sub_f32 v15, v6, v10", + "v_add_f32 v16, v4, v6", + "v_mul_f32 v17, v16, v5", +}; + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const std::optional<ShaderRecompiler::ShaderFloatMode>& floatMode) { + 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); + std::vector<std::uint32_t> userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(Code); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + request.target.fusedExactArithmetic = FusedExactArithmetic; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); +} + +void Check(const char* mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < Results; ++i) { + const auto actual = Output[tid * Results + i]; + Require(actual == Expected[tid][i], std::string("f32 separate rounding: ") + (FusedExactArithmetic ? "fused " : "") + "lane " + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(actual) + ", expected " + Hex(Expected[tid][i])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool fused : {false, true}) { + if (!fused && device->Target().fusedExactArithmetic) continue; + FusedExactArithmetic = fused; + Run(*device, std::nullopt); + Check("no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check("IEEE=1"); + } + std::puts("f32 separate rounding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index cf6d2f2fe5..fdcff33207 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -297,6 +297,7 @@ private: append(key, value.float32FlushToZero); append(key, value.waveLdsMemoryBarriers); append(key, value.float32DenormPreserve); + append(key, value.fusedExactArithmetic); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index cf3880a037..96bb58e1a5 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -832,7 +832,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(43u); + writer.WriteU32(44u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -870,6 +870,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(std::bit_cast<std::uint32_t>(request.context.vertex->pointSizeMin)); writer.WriteU32(std::bit_cast<std::uint32_t>(request.context.vertex->pointSizeMax)); } + writer.WriteBool(request.target.fusedExactArithmetic); return base64Encode(buffer); } @@ -878,7 +879,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 > 43u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 44u) 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); @@ -932,6 +933,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const vertex.pointSizeMax = std::bit_cast<float>(reader.ReadU32()); result.vertex = vertex; } + if (version >= 44u) result.request.target.fusedExactArithmetic = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index dab956cb91..d646bac1a9 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -605,6 +605,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; targetOptions.float32FlushToZero = request.target.float32FlushToZero; targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; + targetOptions.fusedExactArithmetic = request.target.fusedExactArithmetic; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 23cdd78d49..1a90745c62 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -298,6 +298,7 @@ struct SpirvTarget { bool float32FlushToZero = false; bool waveLdsMemoryBarriers = false; bool float32DenormPreserve = false; + bool fusedExactArithmetic = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 8c8c56b4ad..74d6b2e281 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -30,6 +30,7 @@ struct SpirvTargetOptions { bool robustBufferAccess2 = false; bool float32FlushToZero = false; bool float32DenormPreserve = false; + bool fusedExactArithmetic = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1d668cdad8..0115fe9f3d 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -113,6 +113,7 @@ struct SpirvEmitterState { bool robustBufferAccess2 = false; bool float32FlushToZero = false; bool float32DenormPreserve = false; + bool fusedExactArithmetic = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 0f4f8ac116..1ce65a7506 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -140,6 +140,20 @@ std::uint32_t EmitExt(SpirvEmitterState& state, std::uint32_t type, std::uint32_ return result; } +namespace { + +std::uint32_t ExactMul(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs) { + if (state.fusedExactArithmetic) return EmitExt(state, TypeF32(state), GLSLstd450Fma, {lhs, rhs, ConstantF32(state, 0x80000000u)}); + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), lhs, rhs)); +} + +std::uint32_t ExactSub(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs) { + if (state.fusedExactArithmetic) return EmitExt(state, TypeF32(state), GLSLstd450Fma, {ConstantF32(state, 0xbf800000u), rhs, lhs}); + return Exact(state, Binary(state, spv::OpFSub, TypeF32(state), lhs, rhs)); +} + +} + std::uint32_t EmitDppWriteCondition(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto flags = inst.Flags<DppMoveFlags>(); @@ -994,13 +1008,13 @@ std::uint32_t Log2SeriesNearOne(SpirvEmitterState& state, std::uint32_t differen for (const std::uint32_t coefficient : {0x3e124925u, 0xbe2aaaabu, 0x3e4ccccdu, 0xbe800000u, 0x3eaaaaabu, 0xbf000000u, 0x3f800000u}) { polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, difference, ConstantF32(state, coefficient)})); } - const auto product = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), difference, polynomial)); - return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), product, ConstantF32(state, 0x3fb8aa3bu))); + const auto product = ExactMul(state, difference, polynomial); + return ExactMul(state, product, ConstantF32(state, 0x3fb8aa3bu)); } std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { const auto value = EmitExt(state, TypeF32(state), GLSLstd450Log2, {arg0}); - const auto difference = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), arg0, ConstantF32(state, 0x3f800000u))); + const auto difference = ExactSub(state, arg0, ConstantF32(state, 0x3f800000u)); const auto series = Log2SeriesNearOne(state, difference); const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); @@ -1011,16 +1025,16 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { - const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + const auto square = ExactMul(state, turns, turns); auto polynomial = ConstantF32(state, 0xc17183a8u); for (const std::uint32_t coefficient : {0x42283c1au, 0xc2996966u, 0x42a335e3u, 0xc2255de7u, 0x40c90fdbu}) { polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); } - return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, polynomial)); + return ExactMul(state, turns, polynomial); } std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { - const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + const auto square = ExactMul(state, turns, turns); auto polynomial = ConstantF32(state, 0x40fce9c5u); for (const std::uint32_t coefficient : {0xc1d368f9u, 0x4270fa83u, 0xc2aae9e4u, 0x4281e0f8u, 0xc19de9e6u, 0x3f800000u}) { polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); @@ -1047,9 +1061,9 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u)); const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u)); - const auto distance = Select(state, TypeF32(state), nearHalf, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)), absolute); + const auto distance = Select(state, TypeF32(state), nearHalf, ExactSub(state, ConstantF32(state, 0x3f000000u), absolute), absolute); const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), distance, ConstantF32(state, 0x3e000000u)); - const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), distance))), SinTurnSeries(state, distance)); + const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, ExactSub(state, ConstantF32(state, 0x3e800000u), distance)), SinTurnSeries(state, distance)); const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u)); const auto result = TrigLinearNearZero(state, value, distance, linearConstant); @@ -1059,10 +1073,10 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, false); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); - const auto distance = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute)); + const auto distance = ExactSub(state, ConstantF32(state, 0x3e800000u), absolute); const auto low = CosTurnSeries(state, absolute); const auto middle = SinTurnSeries(state, distance); - const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)))); + const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, ExactSub(state, ConstantF32(state, 0x3f000000u), absolute))); const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e000000u)); const auto pastThreeEighths = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3ec00000u)); const auto series = Select(state, TypeF32(state), pastThreeEighths, high, Select(state, TypeF32(state), pastEighth, middle, low)); @@ -1467,14 +1481,17 @@ std::uint32_t EmitFPUnordGreaterThanEqual32(SpirvEmitterState& state, std::uint3 } std::uint32_t EmitFPAdd32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + if (state.fusedExactArithmetic) return EmitGlsl<GLSLstd450Fma, IrType::F32>(state, ConstantF32(state, 0x3f800000u), arg0, arg1); return Exact(state, EmitNative<spv::OpFAdd, IrType::F32>(state, arg0, arg1)); } std::uint32_t EmitFPSub32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + if (state.fusedExactArithmetic) return EmitGlsl<GLSLstd450Fma, IrType::F32>(state, ConstantF32(state, 0xbf800000u), arg1, arg0); return Exact(state, EmitNative<spv::OpFSub, IrType::F32>(state, arg0, arg1)); } std::uint32_t EmitFPMul32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + if (state.fusedExactArithmetic) return EmitGlsl<GLSLstd450Fma, IrType::F32>(state, arg0, arg1, ConstantF32(state, 0x80000000u)); return Exact(state, EmitNative<spv::OpFMul, IrType::F32>(state, arg0, arg1)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 17d2db36bf..55aa8b12a7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -279,6 +279,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32FlushToZero = target.float32FlushToZero; state.waveLdsMemoryBarriers = target.waveLdsMemoryBarriers; state.float32DenormPreserve = target.float32DenormPreserve; + state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { state.requirements.ldsLock = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index a903166190..804aeda284 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -493,7 +493,21 @@ std::uint32_t FoldedOffsetCoord(SpirvValueEmitContext& ctx, const ImageEmitAcces auto& state = ctx.state; const auto components = setup.dimensionInfo.spatialComponents; const auto type = components == 1u ? TypeF32(state) : TypeF32Vector(state, components); - const auto sum = Binary(state, spv::OpFAdd, type, setup.coord, Unary(state, spv::OpConvertSToF, type, PackedOffset(ctx, access, setup.layout))); + const auto offset = Unary(state, spv::OpConvertSToF, type, PackedOffset(ctx, access, setup.layout)); + if (state.fusedExactArithmetic) { + auto one = ConstantF32(state, 0x3f800000u); + if (components != 1u) { + const auto ones = state.module.AllocateId(); + std::vector<std::uint32_t> words{spv::OpCompositeConstruct, type, ones}; + words.insert(words.end(), components, one); + state.module.AddFunction(words); + one = ones; + } + const auto fused = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, type, fused, GlslStd450(state), GLSLstd450Fma, one, setup.coord, offset); + return fused; + } + const auto sum = Binary(state, spv::OpFAdd, type, setup.coord, offset); state.module.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction); return sum; } @@ -1201,8 +1215,16 @@ void EmitPackedHorizontalGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAcc return value; }; const auto texelFloor = [&](std::uint32_t address, std::uint32_t length, bool centre) { - auto scaled = Binary(state, spv::OpFMul, TypeF32(state), AddressF32(ctx, access, address), Unary(state, spv::OpConvertSToF, TypeF32(state), length)); - state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); + const auto addressF32 = AddressF32(ctx, access, address); + const auto lengthF32 = Unary(state, spv::OpConvertSToF, TypeF32(state), length); + std::uint32_t scaled = 0; + if (state.fusedExactArithmetic) { + scaled = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), scaled, GlslStd450(state), GLSLstd450Fma, addressF32, lengthF32, ConstantF32(state, 0x80000000u)); + } else { + scaled = Binary(state, spv::OpFMul, TypeF32(state), addressF32, lengthF32); + state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); + } if (centre) { scaled = Binary(state, spv::OpFSub, TypeF32(state), scaled, ConstantF32(state, 0x3f000000u)); } diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index b1db645fe1..eb8d34ad6d 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -219,12 +219,13 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 43u; + upgraded[4] = 44u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); AppendVertexTail(upgraded); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -259,12 +260,13 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 43u; + layered[4] = 44u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); AppendVertexTail(layered); + layered.push_back(0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -285,11 +287,12 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 43u; + bytes[4] = 44u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); AppendVertexTail(bytes); + bytes.push_back(0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -415,11 +418,12 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); auto upgraded = main38; - upgraded[4] = 43u; + upgraded[4] = 44u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); AppendVertexTail(upgraded); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -435,8 +439,11 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version43 = DecodeBase64(serializer.Serialize(pointed)); - Require(version43.size() > 14u && version43[4] == 43u); + auto version44 = DecodeBase64(serializer.Serialize(pointed)); + Require(version44.size() > 15u && version44[4] == 44u); + auto version43 = version44; + version43.pop_back(); + version43[4] = 43u; const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); Require(replay43.request.graphics->tessellation->mergedHull == merged && replay43.request.context.vertex->pointList && replay43.request.context.vertex->pointSize == 3.0f && replay43.request.context.vertex->pointSizeMin == 1.0f && replay43.request.context.vertex->pointSizeMax == 8.0f); auto version42 = version43; @@ -464,7 +471,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 43u; + main25[4] = 44u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -472,6 +479,7 @@ int main() { } if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); AppendVertexTail(main25); + main25.push_back(0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -487,14 +495,15 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 43u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 44u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.pop_back(); + version40.pop_back(); version40[4] = 40u; const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 44u; + future[4] = 45u; bool futureRejected = false; try { static_cast<void>(serializer.Deserialize(EncodeBase64(future))); @@ -540,6 +549,7 @@ int main() { const auto order = std::search(version41.begin(), version41.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) + pixel.sampleLocations.size() + 1; Require(version41.end() - order > static_cast<std::ptrdiff_t>(sizeof(pixel.sampleOrder)) && *order == 0x10u); version41.erase(order, order + sizeof(pixel.sampleOrder)); + version41.pop_back(); version41[4] = 41u; const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); Require(replay41.request.context.pixel.has_value() && replay41.request.context.pixel->sampleLocations == pixel.sampleLocations && replay41.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{}); @@ -557,6 +567,7 @@ int main() { version35.erase(stencilOperationValue, stencilOperationValue + pixelTail); version35[4] = 35u; version35.pop_back(); + version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); Require(replay35.request.target.spannedWaves == spanned && !replay35.request.target.float32FlushToZero); } @@ -574,6 +585,7 @@ int main() { Require(serializer.Serialize(pointReplay.request) == pointEncoded); auto pointBytes = DecodeBase64(pointEncoded); pointBytes.pop_back(); + pointBytes.pop_back(); bool pointRejected = false; try { static_cast<void>(serializer.Deserialize(EncodeBase64(pointBytes))); @@ -593,7 +605,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); + legacy.resize(legacy.size() - (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -605,7 +617,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 43u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); + Require(upgradedLegacy[4] == 44u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); } } @@ -614,14 +626,14 @@ int main() { const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy42.resize(legacy42.size() - 13u); + legacy42.resize(legacy42.size() - 14u); legacy42[4] = 42u; const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 20u); + legacy34.resize(legacy34.size() - 21u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast<std::ptrdiff_t>(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -631,5 +643,14 @@ int main() { Require(replay34.request.context.pixel.has_value() && replay34.request.context.pixel->iteratesSamples && replay34.request.context.pixel->positionLocation == pixel.positionLocation && replay34.request.context.pixel->sampleLocations == pixel.sampleLocations); Require(replay34.request.context.vertex.has_value() && !replay34.request.context.vertex->pointList && replay34.request.context.vertex->pointSize == 1.0f); + for (const bool fused : {false, true}) { + request.target.fusedExactArithmetic = fused; + auto bytes = DecodeBase64(serializer.Serialize(request)); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes.back() == (fused ? 1u : 0u)); + bytes[4] = 43u; + bytes.pop_back(); + Require(!serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic); + } + return 0; } From 9ec09517ee77e37d80ff90dcf2935511f9e1db14 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 23:25:25 +1300 Subject: [PATCH 1226/1360] fix(libSceConvertKeycode): treat oversized app metadata as a PS5 title AppMetadata throws length_error for a title or title ID that does not fit its buffer, which the title ID lookup did not catch, so a homebrew title with a long ID made sceConvertKeycodeGetImeKeyboardType throw instead of using the PS5 fallback. Catch it with the other metadata failures and cover oversized, malformed and missing metadata at the export. sceUserServiceGetKeyboardType also gets a test that a malformed ANYPS5_LANGUAGE is rejected with the output left untouched, and the debt entries say so. --- .../prx/libSceConvertKeycode/CMakeLists.txt | 25 +++++++++++++++++++ core/libs/prx/libSceConvertKeycode/Export.cpp | 2 ++ .../libs/prx/libSceUserService/CMakeLists.txt | 2 +- core/libs/tests/GuestConvertKeycodeTitle.cpp | 25 +++++++++++++++++++ core/libs/tests/GuestUserService.cpp | 15 +++++++++++ docs/dev/debt/libSceConvertKeycode.md | 2 +- docs/dev/debt/libSceUserService.md | 2 +- 7 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestConvertKeycodeTitle.cpp diff --git a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt index 34d023d5ac..341ff705b0 100644 --- a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt +++ b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt @@ -13,4 +13,29 @@ if(BUILD_TESTING) set_tests_properties(guest_convert_keycode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceConvertKeycode>") endif() + add_test_executable(guest_convert_keycode_title_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycodeTitle.cpp) + target_include_directories(guest_convert_keycode_title_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_convert_keycode_title_tests PRIVATE libSceConvertKeycode libSceUserService libSceSystemService libc) + configure_windows_unwind(guest_convert_keycode_title_tests) + string(REPEAT "T" 130 convertKeycodeLongTitle) + foreach(titleCase IN ITEMS oversized-id oversized-title cusa ppsa malformed-json missing) + set(titleRun ${CMAKE_CURRENT_BINARY_DIR}/convert_keycode_title_${titleCase}) + file(REMOVE_RECURSE ${titleRun}) + file(MAKE_DIRECTORY ${titleRun}) + if(titleCase STREQUAL "oversized-id") + file(WRITE ${titleRun}/app0/sce_sys/param.json "{\"titleId\": \"CUSA0123456789012345\", \"localizedParameters\": {\"en-US\": {\"titleName\": \"Fixture\"}}}") + elseif(titleCase STREQUAL "oversized-title") + file(WRITE ${titleRun}/app0/sce_sys/param.json "{\"titleId\": \"PPSA00000\", \"localizedParameters\": {\"en-US\": {\"titleName\": \"${convertKeycodeLongTitle}\"}}}") + elseif(titleCase STREQUAL "cusa" OR titleCase STREQUAL "ppsa") + string(TOUPPER ${titleCase} titlePrefix) + file(WRITE ${titleRun}/app0/sce_sys/param.json "{\"titleId\": \"${titlePrefix}00000\", \"localizedParameters\": {\"en-US\": {\"titleName\": \"Fixture\"}}}") + elseif(titleCase STREQUAL "malformed-json") + file(WRITE ${titleRun}/app0/sce_sys/param.json "{") + endif() + add_test(NAME guest_convert_keycode_title_${titleCase} COMMAND guest_convert_keycode_title_tests WORKING_DIRECTORY ${titleRun}) + if(WIN32) + set_tests_properties(guest_convert_keycode_title_${titleCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceConvertKeycode>") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 11d1fb7f32..0aa1387305 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -2410,6 +2410,8 @@ bool Ps4Client() { return ImeKeyboardType::IsPs4TitleId(std::string_view(titleId.value, strnlen(titleId.value, sizeof(titleId.value)))); } catch (const std::runtime_error&) { return false; + } catch (const std::length_error&) { + return false; } } } // namespace diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index c78fcbc8d7..282c8e98a3 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -6,7 +6,7 @@ configure_windows_unwind(libSceUserService) 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 libSceSystemService) + target_link_libraries(guest_user_service_tests PRIVATE libSceUserService libSceSystemService libc) configure_windows_unwind(guest_user_service_tests) add_test(NAME guest_user_service COMMAND guest_user_service_tests) if(WIN32) diff --git a/core/libs/tests/GuestConvertKeycodeTitle.cpp b/core/libs/tests/GuestConvertKeycodeTitle.cpp new file mode 100644 index 0000000000..6fd7337573 --- /dev/null +++ b/core/libs/tests/GuestConvertKeycodeTitle.cpp @@ -0,0 +1,25 @@ +#include "SceTypes.hpp" +#include "ProcessParametersFixture.hpp" +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <cstring> + +extern "C" { +int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +} + +int main() { +#ifdef _WIN32 + if (_putenv_s("ANYPS5_LANGUAGE", "") != 0) return 2; +#else + if (::unsetenv("ANYPS5_LANGUAGE") != 0) return 2; +#endif + static std::array<std::byte, 64> process{}; + constexpr std::uint32_t sdkVersion = 0x09000000u; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + if (!ProcessParametersFixture::Install(process.data(), process.size())) return 2; + std::uint32_t type = 0xFFFFFFFFu; + return sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4 ? 0 : 1; +} diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index b913ced298..db457f47f3 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -96,6 +96,21 @@ int main() { SetLanguage("30"); std::int32_t fallback = 0x5A5A; Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &fallback) == USER_SERVICE_OK && fallback == 20); + for (const char* malformed : {"arabic", "-1", "31", "1x", ""}) { + SetLanguage(malformed); + std::int32_t untouched = 0x5A5A; + bool rejected = false; + try { + sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &untouched); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("ANYPS5_LANGUAGE") != std::string::npos; + } + const bool configured = malformed[0] != '\0'; + Require(rejected == configured && untouched == (configured ? 0x5A5A : 20)); + untouched = 0x5A5A; + Require(sceUserServiceGetKeyboardType(123, &untouched) == USER_SERVICE_ERROR_UNKNOWN_USER && untouched == 0x5A5A); + Require(sceUserServiceGetJapaneseInputType(USER_SERVICE_INITIAL_USER_ID, &untouched) == USER_SERVICE_OK && untouched == 0); + } SetLanguage(nullptr); Require(color == 0); diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 9255ca607c..ca16750daf 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -3,4 +3,4 @@ ### Functional - [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. -- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing or custom ID, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md). +- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing, malformed or oversized ID or title, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md). diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index bab39a1f05..8401bc7127 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,4 +8,4 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output -- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's and covers languages 0 to 29; a language outside it, such as Ukrainian (30), reports 20 (US English). The input variant is 0 because the owner's table needs one only for Japanese and Traditional Chinese, and gives 0 for both. `sceConvertKeycodeGetImeKeyboardType` consumes these values. +- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's and covers languages 0 to 29; a language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is not replaced by a default: `sceSystemServiceParamGetInt` rejects it with an error naming `ANYPS5_LANGUAGE`, and the keyboard type call throws that error and leaves the output untouched. The input variant is 0 because the owner's table needs one only for Japanese and Traditional Chinese, and gives 0 for both. `sceConvertKeycodeGetImeKeyboardType` consumes these values. From 5b739d2b56f6b30ec0b6a1a7abbf178aba37094c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:33:00 +0200 Subject: [PATCH 1227/1360] fix(libkernel): fail fchflags on a kqueue descriptor with EINVAL FreeBSD resolves the descriptor with getvnode in sys_fchflags (vfs_syscalls.c:2403), which fails with EINVAL for a descriptor that has no vnode. fchflags threw for a kqueue descriptor instead. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 82aa717df1..170e0f5b6b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -706,7 +706,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { } int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { - if (GuestSockets::IsOpen(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchflags"); if (!File::IsVnode(d)) { if (!DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 9e3fe832d1..f76230071e 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -39,6 +39,7 @@ int APS5_VABI fchmod_nid_postfix(int, int); int APS5_VABI chmod_nid_postfix(const char*, int); int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); int APS5_VABI fchflags_nid_postfix(int, std::uint64_t); +int APS5_VABI kqueue_nid_postfix(); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); @@ -708,6 +709,10 @@ int main() { Require(fchflags_nid_postfix(flagged, 0) == -1 && *__error_nid_postfix() == 9); Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 9); Require(fchflags_nid_postfix(-1, 0) == -1 && *__error_nid_postfix() == 9); + const int flaggedKqueue = kqueue_nid_postfix(); + Require(flaggedKqueue >= 0); + Require(fchflags_nid_postfix(flaggedKqueue, 0) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedKqueue) == 0); const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); int flaggedPipe[2]; From f9cbe3f1ac18df47c1892d90a3e802f11c7f3566 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Fri, 23 Oct 2026 23:29:29 +1300 Subject: [PATCH 1228/1360] docs(libSceUserService): keep the keyboard type entry under the length limit --- docs/dev/debt/libSceUserService.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 8401bc7127..cd811dc408 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,4 +8,4 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output -- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's and covers languages 0 to 29; a language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is not replaced by a default: `sceSystemServiceParamGetInt` rejects it with an error naming `ANYPS5_LANGUAGE`, and the keyboard type call throws that error and leaves the output untouched. The input variant is 0 because the owner's table needs one only for Japanese and Traditional Chinese, and gives 0 for both. `sceConvertKeycodeGetImeKeyboardType` consumes these values. +- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's (languages 0 to 29); a valid language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is rejected by `sceSystemServiceParamGetInt` with an error naming `ANYPS5_LANGUAGE`; the call throws it and leaves the output untouched. The input variant is 0 because the owner's table gives 0 for Japanese and Traditional Chinese, the only languages needing one. `sceConvertKeycodeGetImeKeyboardType` consumes these values. From b92291c7aea242520e26cee633767888c3364ef7 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz <eduardo@edubox.org> Date: Fri, 23 Oct 2026 10:05:36 +0200 Subject: [PATCH 1229/1360] feat(libc): export operator delete(void*, const std::nothrow_t&) The console's libc exports _ZdlPvRKSt9nothrow_t (NID McsGnqV6yRE) as a 12-byte tail call to the same delete function that _ZdlPv uses, ignoring the nothrow tag. Forward to _ZdlPv so memory from the nothrow new overloads is released through the registered application heap; a null pointer is ignored. --- core/libs/prx/libc/src/StdioExtras.cpp | 4 ++++ core/libs/tests/GuestNewHandler.cpp | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index 28b5f7b33b..be68fa2638 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -131,6 +131,10 @@ void APS5_VABI _ZdlPv_nid_postfix(void* pointer) { if (pointer != nullptr) ApplicationHeapFree_nid_no_patch(pointer); } +void APS5_VABI _ZdlPvRKSt9nothrow_t_nid_postfix(void* pointer, const void*) noexcept { + _ZdlPv_nid_postfix(pointer); +} + void APS5_VABI _ZdaPv_nid_postfix(void* pointer) { if (pointer != nullptr) ApplicationHeapFree_nid_no_patch(pointer); } diff --git a/core/libs/tests/GuestNewHandler.cpp b/core/libs/tests/GuestNewHandler.cpp index 04a5d9a8a1..6229ada261 100644 --- a/core/libs/tests/GuestNewHandler.cpp +++ b/core/libs/tests/GuestNewHandler.cpp @@ -19,6 +19,7 @@ void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t, std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void APS5_VABI _ZdlPv_nid_postfix(void*); +void APS5_VABI _ZdlPvRKSt9nothrow_t_nid_postfix(void*, const void*) noexcept; void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void*, std::size_t); } @@ -27,6 +28,7 @@ namespace { alignas(16) std::array<std::byte, 64> storage{}; unsigned failuresLeft = 0; unsigned allocations = 0; +unsigned releases = 0; unsigned handlerCalls = 0; std::size_t lastSize = 0; std::size_t lastAlignment = 0; @@ -50,7 +52,7 @@ void* APS5_VABI allocate(std::size_t bytes) { return storage.data(); } -void APS5_VABI release(void*) {} +void APS5_VABI release(void*) { ++releases; } void* APS5_VABI allocateZeroed(std::size_t count, std::size_t bytes) { return allocate(count * bytes); } void* APS5_VABI reallocate(void*, std::size_t bytes) { return allocate(bytes); } void* APS5_VABI align(std::size_t alignment, std::size_t bytes) { @@ -80,6 +82,7 @@ void APS5_VABI allocatingHandler() { void Reset(unsigned failures) { failuresLeft = failures; allocations = 0; + releases = 0; handlerCalls = 0; } @@ -138,6 +141,15 @@ int main(int argc, char** argv) { REQUIRE(_ZSt15get_new_handlerv_nid_postfix() == nullptr); + Reset(0); + void* nothrowPointer = _Znwm_nid_postfix(8); + const int nothrowTag = 0; + REQUIRE(nothrowPointer == storage.data() && releases == 0); + _ZdlPvRKSt9nothrow_t_nid_postfix(nothrowPointer, ¬hrowTag); + REQUIRE(releases == 1); + _ZdlPvRKSt9nothrow_t_nid_postfix(nullptr, ¬hrowTag); + REQUIRE(releases == 1); + Reset(1); REQUIRE(Throws([] { _Znwm_nid_postfix(8); }) && handlerCalls == 0 && allocations == 1); Reset(1); From 30ab85a8db3496c937d4f701bdd28c1c365524df Mon Sep 17 00:00:00 2001 From: atharv-honestly <atharv@askhonestly.ai> Date: Fri, 23 Oct 2026 13:14:22 +0530 Subject: [PATCH 1230/1360] feat(libressl): implement certificate verification error strings --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 11 +++ core/libs/prx/libSceLibreSsl/Export.cpp | 68 ++++++++++++++++- core/libs/tests/GuestLibreSslX509Errors.cpp | 84 +++++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestLibreSslX509Errors.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 3e52df3916..78e8a9adec 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -4,6 +4,17 @@ target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) if(BUILD_TESTING) + add_test_executable(guest_libressl_x509_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslX509Errors.cpp) + target_include_directories(guest_libressl_x509_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_x509_errors_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_x509_errors_tests) + add_test(NAME guest_libressl_x509_errors COMMAND guest_libressl_x509_errors_tests) + set_tests_properties(guest_libressl_x509_errors PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_x509_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") + endif() + add_test_executable(guest_libressl_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDigest.cpp) target_include_directories(guest_libressl_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libressl_digest_tests PRIVATE libSceLibreSsl libc) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 084f0bcf59..1e2c3c9caa 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1,3 +1,5 @@ +#include <cinttypes> +#include <cstdio> #include <cstddef> #include <cstdint> #include <cstring> @@ -1820,9 +1822,69 @@ int APS5_VABI X509_verify_cert_nid_postfix(void) { return 0; } -int APS5_VABI X509_verify_cert_error_string_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +const char* APS5_VABI X509_verify_cert_error_string_nid_postfix(std::int64_t error) { + static constexpr const char* messages[] { + "ok", + nullptr, + "unable to get issuer certificate", + "unable to get certificate CRL", + "unable to decrypt certificate's signature", + "unable to decrypt CRL's signature", + "unable to decode issuer public key", + "certificate signature failure", + "CRL signature failure", + "certificate is not yet valid", + "certificate has expired", + "CRL is not yet valid", + "CRL has expired", + "format error in certificate's notBefore field", + "format error in certificate's notAfter field", + "format error in CRL's lastUpdate field", + "format error in CRL's nextUpdate field", + "out of memory", + "self signed certificate", + "self signed certificate in certificate chain", + "unable to get local issuer certificate", + "unable to verify the first certificate", + "certificate chain too long", + "certificate revoked", + "invalid CA certificate", + "path length constraint exceeded", + "unsupported certificate purpose", + "certificate not trusted", + "certificate rejected", + "subject issuer mismatch", + "authority and subject key identifier mismatch", + "authority and issuer serial number mismatch", + "key usage does not include certificate signing", + "unable to get CRL issuer certificate", + "unhandled critical extension", + "key usage does not include CRL signing", + "unhandled critical CRL extension", + "invalid non-CA certificate (has CA markings)", + "proxy path length constraint exceeded", + "key usage does not include digital signature", + "proxy certificates not allowed, please set the appropriate flag", + "invalid or inconsistent certificate extension", + "invalid or inconsistent certificate policy extension", + "no explicit policy", + "Different CRL scope", + "Unsupported extension feature", + "RFC 3779 resource not subset of parent's resources", + "permitted subtree violation", + "excluded subtree violation", + "name constraints minimum and maximum not supported", + "application verification failure", + "unsupported name constraint type", + "unsupported or invalid name constraint syntax", + "unsupported or invalid name syntax", + "CRL path validation error", + }; + const auto code = static_cast<std::uint32_t>(error); + if (code < sizeof(messages) / sizeof(messages[0]) && messages[code]) return messages[code]; + static char unknown[100]; + std::snprintf(unknown, sizeof(unknown), "error number %" PRId64, error); + return unknown; } int APS5_VABI d2i_OCSP_RESPONSE_nid_postfix(void) { diff --git a/core/libs/tests/GuestLibreSslX509Errors.cpp b/core/libs/tests/GuestLibreSslX509Errors.cpp new file mode 100644 index 0000000000..7a53a5e582 --- /dev/null +++ b/core/libs/tests/GuestLibreSslX509Errors.cpp @@ -0,0 +1,84 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <limits> +#include <string> + +extern "C" const char* APS5_VABI X509_verify_cert_error_string_nid_postfix(std::int64_t); + +static constexpr std::uint64_t ExpectedHashes[] { + 0x8b05d07b5566befULL, 0, 0x708834e4234778b5ULL, 0xefb3526cbf8b4d8fULL, + 0xf9c246e135cdbad1ULL, 0xd42f63e9d35f4aa1ULL, 0xcccbec8974d6af08ULL, 0x8d443856acab19a4ULL, + 0x8d2b572b67e1760ULL, 0xa3b575a1199d9fd1ULL, 0xbed7043d1daf8f09ULL, 0x2a0a7832fb23e29dULL, + 0x265d14ebfd73f005ULL, 0x675282b2d8705cd0ULL, 0xade734fba45db83ULL, 0x2688e741036ec90fULL, + 0xf75897f162ad286eULL, 0x74e217b21ed87663ULL, 0xa33689775feaef0eULL, 0xa3359b0eaed11517ULL, + 0x6925fd54e311eb7cULL, 0xe6f75276f756e2e2ULL, 0xc9122d9145ee8c0fULL, 0xe88507a36fdaa6e6ULL, + 0xe4225a4c8853b86fULL, 0x5ef1c83b11d335a6ULL, 0x9b6dfad0758a0221ULL, 0xfe7f0ff3aadd086ULL, + 0x85a68d2a11031f82ULL, 0x72c1d177130ec956ULL, 0x44d8d6b5ed2f06edULL, 0x738e7dc7a61e12cdULL, + 0xc1fe96db71c85021ULL, 0x26559c0cfe2f2f78ULL, 0xc3bf2a493ebacd94ULL, 0xa4206ad9f25787ddULL, + 0x3f7344fff3aa908bULL, 0x23335a5daca94f1aULL, 0x1d9d2747a36c85e2ULL, 0xf07fa5bffdd43063ULL, + 0x1bad38ca7c7bc1e8ULL, 0x7715922cd76c9dceULL, 0x82fcbf1e3440e64eULL, 0x6d1f21d68462a5d4ULL, + 0x2e572ca0e246a7c9ULL, 0x5e23e31a525a543bULL, 0xa0bd09ebfc8cc89ULL, 0x85288e1dd50bdaceULL, + 0xf659605c19b0997eULL, 0xddad96c31c47ffdeULL, 0x2ccb58faf48aae6aULL, 0x9973a358685fe222ULL, + 0xe154c3e01bf7ba9bULL, 0x1e042e731311c0e8ULL, 0xa9af33b6e407c014ULL, +}; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +static std::uint64_t Hash(const char* text) { + Require(text != nullptr, "error string is non-null"); + std::uint64_t value = 14695981039346656037ULL; + for (; *text; ++text) value = (value ^ static_cast<unsigned char>(*text)) * 1099511628211ULL; + return value; +} + +int main() { + constexpr std::int64_t offsets[] {0, INT64_C(0x100000000), -INT64_C(0x100000000), + std::numeric_limits<std::int64_t>::min(), INT64_C(0x7fffffff00000000)}; + for (std::int64_t code = 0; code < 55; ++code) { + if (code == 1) continue; + const char* stable = X509_verify_cert_error_string_nid_postfix(code); + for (const auto offset : offsets) { + const char* actual = X509_verify_cert_error_string_nid_postfix(code + offset); + Require(Hash(actual) == ExpectedHashes[code], "complete LibreSSL 2.5.4 message mapping"); + Require(actual == stable, "known messages retain static storage across 64-bit aliases"); + } + } + + for (std::int64_t code = -300; code <= 300; ++code) { + if (code == 0 || (code >= 2 && code <= 54)) continue; + const std::string expected = "error number " + std::to_string(code); + const char* actual = X509_verify_cert_error_string_nid_postfix(code); + Require(actual && expected == actual, "unknown and newer codes retain the numeric fallback"); + } + + struct WideCase { std::int64_t error; const char* expected; }; + constexpr WideCase wideCases[] { + {std::numeric_limits<std::int64_t>::max(), "error number 9223372036854775807"}, + {std::numeric_limits<std::int64_t>::min() + 1, "error number -9223372036854775807"}, + {INT64_C(4294967297), "error number 4294967297"}, + {-INT64_C(4294967295), "error number -4294967295"}, + {INT64_C(2147483648), "error number 2147483648"}, + {-INT64_C(2147483648), "error number -2147483648"}, + {INT64_C(4294967358), "error number 4294967358"}, + }; + for (const auto& item : wideCases) { + const char* actual = X509_verify_cert_error_string_nid_postfix(item.error); + Require(actual && std::strcmp(actual, item.expected) == 0, "fallback formats all 64 argument bits"); + } + + const char* shared = X509_verify_cert_error_string_nid_postfix(-123); + const char* known = X509_verify_cert_error_string_nid_postfix(10); + Require(std::strcmp(shared, "error number -123") == 0, "known lookup preserves unknown buffer"); + const char* next = X509_verify_cert_error_string_nid_postfix(62); + Require(shared == next && std::strcmp(shared, "error number 62") == 0, "unknown results share overwritten storage"); + Require(std::strcmp(known, "certificate has expired") == 0, "unknown formatting preserves known message"); + Require(std::strcmp(X509_verify_cert_error_string_nid_postfix(0), "ok") == 0, "success message"); + return 0; +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 434eef416e..f84ef0fbf6 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,6 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 374 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 373 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults +- [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. From 194c03afde5d9eb8c05b13361597fad19036be0d Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Tue, 20 Oct 2026 16:01:31 +0200 Subject: [PATCH 1231/1360] fix(agc): order deferred host-import settles with main-queue accesses --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/src/GuestBufferMemory.cpp | 33 ++----- .../prx/libSceAgcDriver/tests/Recorder.cpp | 94 ++++++++++++++++--- 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8d92618a22..8e0654bc29 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1489,6 +1489,11 @@ if(BUILD_TESTING) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) + add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + SKIP_RETURN_CODE 77 TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 929bd02e0d..2a5f5d4b45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) static_cast<void>(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { @@ -987,7 +987,7 @@ const char* prepareSettleBatch(const Context& context, ImportSettle& settle, Set } if (batch.commands == VK_NULL_HANDLE) { VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - allocate.commandPool = context.settlePool; + allocate.commandPool = context.pool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1; if ((result = context.Function<PFN_vkAllocateCommandBuffers>("vkAllocateCommandBuffers")(context.device, &allocate, &batch.commands)) != VK_SUCCESS) { @@ -1037,9 +1037,7 @@ void submitSettleBatch(const Context& context, HostImports& state) { begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if ((result = context.Function<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(batch.commands, &begin)) != VK_SUCCESS) return fail("vkBeginCommandBuffer (settle batch)"); const auto copy = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); - const bool otherFamily = context.settleFamily != context.queueFamily; - const auto createBuffer = otherFamily ? context.Function<PFN_vkCreateBuffer>("vkCreateBuffer") : nullptr; - const auto bindBufferMemory = otherFamily ? context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory") : nullptr; + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::size_t consumed = 0; for (; consumed < state.settleWanted.size() && batch.bases.size() < settleBatchCopies; ++consumed) { const auto base = state.settleWanted[consumed]; @@ -1050,38 +1048,19 @@ void submitSettleBatch(const Context& context, HostImports& state) { state.pendingSettles.erase(settle); continue; } - VkBuffer source = import->second.buffer; - if (otherFamily) { - VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; - external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; - VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; - aliasInfo.size = std::min<VkDeviceSize>(import->second.bytes, 65536); - aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; - VkBuffer alias = VK_NULL_HANDLE; - if (createBuffer(context.device, &aliasInfo, nullptr, &alias) != VK_SUCCESS) { - state.pendingSettles.erase(settle); - continue; - } - if (bindBufferMemory(context.device, alias, import->second.memory, 0) != VK_SUCCESS) { - destroyBuffer(context.device, alias, nullptr); - state.pendingSettles.erase(settle); - continue; - } - batch.aliases.push_back(alias); - source = alias; - } const VkBufferCopy region{0, (VkDeviceSize{index} * settleBatchCopies + batch.bases.size()) * 4u, 4}; - copy(batch.commands, source, deviceSettle.batchTarget, 1, ®ion); + copy(batch.commands, import->second.buffer, deviceSettle.batchTarget, 1, ®ion); settle->second = PendingSettle{SettlePhase::Submitted, index, context.device}; batch.bases.push_back(base); } state.settleWanted.erase(state.settleWanted.begin(), state.settleWanted.begin() + static_cast<std::ptrdiff_t>(consumed)); + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_WRITE_BIT); if ((result = context.Function<PFN_vkEndCommandBuffer>("vkEndCommandBuffer")(batch.commands)) != VK_SUCCESS) return fail("vkEndCommandBuffer (settle batch)"); if (batch.bases.empty()) return; VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &batch.commands; - if ((result = context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.settleQueue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); + if ((result = context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.queue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); batch.busy = true; state.settlesSubmitted += batch.bases.size(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 68a362406c..63ce16a1e0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2027,7 +2027,10 @@ bool settleFirstUseRaced(const Context& context, int copies) { const auto* import = HostImportFor(context, address, unit); Require(import != nullptr && import->unwatched && !Watched(address, unit), "(f) a 64 KiB import was not compared at first"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + } recorder.Submit(); const auto submittedAt = std::chrono::steady_clock::now(); const auto deadline = submittedAt + std::chrono::seconds(5); @@ -2066,6 +2069,69 @@ bool settleFirstUseRaced(const Context& context, int copies) { return racing; } +bool settleWriteOrderTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: settle write order not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: settle write order not tested\n"; + return false; + } + void* block = AllocateWatched(unit, unit); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, unit); } + } release{block}; + const auto address = reinterpret_cast<std::uint64_t>(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + Recorder recorder(context); + recorder.Activate(); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "the write-order import was not initially compared"); + const auto fill = [&](std::uint32_t value) { + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function<PFN_vkCmdFillBuffer>("vkCmdFillBuffer")(commands, import->buffer, HostImportOffset(*import, address), unit, value); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + recorder.NotePendingWrite(address, unit); + recorder.Submit(); + }; + const auto before = HostImportSettles().submitted; + fill(0x22222222u); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(HostImportFor(context, address, unit) == import, "the write-order import changed while settling"); + } + Require(HostImportSettles().submitted > before, "the write-order import did not submit a settle"); + fill(0x33333333u); + recorder.Sync(); + while (!Watched(address, unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(Watched(address, unit), "the write-order import did not return to the write watch"); + const auto* bytes = static_cast<const std::uint8_t*>(block); + Require(std::all_of(bytes, bytes + unit, [](std::uint8_t value) { return value == 0x33; }), "the GPU store after the settle did not preserve its bytes"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, unit); + std::cout << "settle read between main-queue GPU writes passed\n"; + return true; +} + + bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2080,15 +2146,9 @@ bool settleFirstUseTests(const Device& device) { std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; return false; } - for (int copies = 4096; copies <= 65536; copies *= 4) { - if (settleFirstUseRaced(context, copies)) { - std::cout << "settled import first use tests passed\n"; - return true; - } - std::cout << "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them\n"; - } - std::cout << "(f) the main-queue copies finished first in every round: the first use of a settled import not tested\n"; - return false; + Require(!settleFirstUseRaced(context, 4096), "(f) the import returned to the write watch while its earlier main-queue reads were still in flight"); + std::cout << "settled import first use ordering tests passed\n"; + return true; } bool settleEarlierImportTests(const Device& device) { @@ -2127,7 +2187,10 @@ bool settleEarlierImportTests(const Device& device) { const auto* earlier = HostImportFor(context, address, 16); Require(earlier != nullptr && earlier->base == address && earlier->bytes == unit && earlier->unwatched, "(e) the first window of the range was not imported compared"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + } const auto* whole = HostImportFor(context, address + unit - 16, 32); Require(whole != nullptr && whole->base == address && whole->bytes == 2 * unit && whole->unwatched, "(e) an access across the windows did not replace the first window's import"); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); @@ -10374,7 +10437,7 @@ int main(int argc, char** argv) { } std::unique_ptr<Device> created; try { - created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only")); + created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; @@ -10413,6 +10476,13 @@ int main(int argc, char** argv) { } return deferredSettleTests(device) ? 0 : 77; } + if (argc == 2 && (std::string_view(argv[1]) == "--settle-write-order-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")) { + if (std::string_view(argv[1]) == "--settle-write-order-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return settleWriteOrderTests(device) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From 9d4a0ebfcd9faf6c56151cda430ebda8e2ddd2ba Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Tue, 20 Oct 2026 16:33:21 +0200 Subject: [PATCH 1232/1360] docs(agc): describe deferred settle submission ordering --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 8676f9acff..77bf4fd9af 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -145,7 +145,7 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. - A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7569ec1c0f..0652db2d6e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -113,7 +113,8 @@ - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From a0ecec3d3b5c58c33587bd0a6f2a543ffd7ac545 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Fri, 23 Oct 2026 13:10:52 +0200 Subject: [PATCH 1233/1360] fix(agc): bound deferred settle retirement waits --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/src/GuestBufferMemory.cpp | 2 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 109 ++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8e0654bc29..3ff1462fea 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1491,8 +1491,10 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) - set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + add_test(NAME agc_import_settle_wait_failure COMMAND agc_driver_recorder_tests --settle-wait-failure-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family agc_import_settle_wait_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 2a5f5d4b45..6ceb8a9f43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 60'000'000'000ull), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 63ce16a1e0..86aa5f691a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2131,6 +2131,114 @@ bool settleWriteOrderTests(const Device& device) { return true; } +struct SettleWaitFailure { + PFN_vkGetDeviceProcAddr resolve; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkResult result = VK_SUCCESS; + std::uint64_t timeout = 0; + unsigned waits = 0; + unsigned destroys = 0; + unsigned frees = 0; +}; + +SettleWaitFailure* settleWaitFailure = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL settleWaitForFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) { + auto& failure = *settleWaitFailure; + if (failure.result != VK_SUCCESS) { + failure.timeout = timeout; + ++failure.waits; + return failure.result; + } + return reinterpret_cast<PFN_vkWaitForFences>(failure.resolve(device, "vkWaitForFences"))(device, count, fences, all, timeout); +} + +VKAPI_ATTR void VKAPI_CALL settleDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (buffer == failure.buffer) ++failure.destroys; + reinterpret_cast<PFN_vkDestroyBuffer>(failure.resolve(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL settleFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (memory == failure.memory) ++failure.frees; + reinterpret_cast<PFN_vkFreeMemory>(failure.resolve(device, "vkFreeMemory"))(device, memory, allocator); +} + +PFN_vkVoidFunction VKAPI_CALL settleWaitDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast<PFN_vkVoidFunction>(settleWaitForFences); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(settleDestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(settleFreeMemory); + return settleWaitFailure->resolve(device, name); +} + +bool settleWaitFailureTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + auto context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) return false; + std::lock_guard gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) return false; + ClearHostImports(context.device); + SettleWaitFailure failure{context.deviceProc}; + settleWaitFailure = &failure; + context.deviceProc = settleWaitDeviceProc; + context.functions = nullptr; + void* block = AllocateWatched(unit, unit); + Require(block != nullptr, "no watched block for settle wait failure"); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + struct Release { + Context& context; + SettleWaitFailure& failure; + void* block; + ~Release() { + failure.result = VK_SUCCESS; + ClearHostImports(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, unit); + settleWaitFailure = nullptr; + } + } release{context, failure, block}; + const auto address = reinterpret_cast<std::uint64_t>(block); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "wait failure import was not initially compared"); + failure.buffer = import->buffer; + failure.memory = import->memory; + const auto before = HostImportSettles().submitted; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(HostImportSettles().submitted > before, "wait failure did not submit a settle"); + Check(context.Function<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(context.device), "vkDeviceWaitIdle before settle failure"); + for (const auto result : {VK_TIMEOUT, VK_ERROR_DEVICE_LOST}) { + failure.result = result; + const auto waits = failure.waits; + std::string error; + try { + ClearHostImports(context.device); + } catch (const std::runtime_error& caught) { + error = caught.what(); + } + failure.result = VK_SUCCESS; + Require(failure.waits == waits + 1 && failure.timeout > 0 && failure.timeout <= 60'000'000'000ull, "settle retirement did not use a bounded fence wait"); + Require(error == "AGC graphics: vkWaitForFences (settle batch): Vulkan result " + std::to_string(result), "settle retirement did not propagate its Vulkan failure"); + Require(failure.destroys == 0 && failure.frees == 0 && HostImportCovers(context, address, unit), "failed settle wait released its host import"); + } + ClearHostImports(context.device); + Require(failure.destroys == 1 && failure.frees == 1, "successful settle retirement did not release the retained import exactly once"); + std::cout << "bounded settle wait preserves imports on timeout and device loss; successful retry releases once\n"; + return true; +} bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -10483,6 +10591,7 @@ int main(int argc, char** argv) { } return settleWriteOrderTests(device) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-wait-failure-only") return settleWaitFailureTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From 54747c31fb750037b5792431dd3c744d91b37285 Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:12:08 +0400 Subject: [PATCH 1234/1360] fix(rudp): use precise poll deadlines on Windows --- core/libs/prx/libSceRudp/Export.cpp | 13 ++++++------ core/libs/tests/GuestRudpPoll.cpp | 32 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index dcfb2c25be..ce37e56c0e 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,6 +1,4 @@ #include <atomic> -#include <chrono> -#include <condition_variable> #include <cstdint> #include <cstdio> #include <cstddef> @@ -11,6 +9,7 @@ #include <stdexcept> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" // Offline reliable-UDP: the library initialises and contexts can be created and bound, but no connection can // ever be established because there is no network. @@ -96,7 +95,7 @@ struct RudpPoll { int g_next_poll = 1; std::map<int, RudpPoll> g_polls; -std::condition_variable g_polls_changed; +TimedWait::Condition g_polls_changed; bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; @@ -157,7 +156,7 @@ int APS5_VABI sceRudpEnd(void) { g_inited = false; g_ctx.clear(); g_polls.clear(); - g_polls_changed.notify_all(); + g_polls_changed.NotifyAll(); return 0; } @@ -451,8 +450,8 @@ int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eve } const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; if (timeout > RUDP_POLL_WAIT_MAX_USEC) { - g_polls_changed.wait(lk, closed); - } else if (!g_polls_changed.wait_for(lk, std::chrono::microseconds(timeout), closed)) { + g_polls_changed.Wait(lk, closed); + } else if (!g_polls_changed.WaitUntil(lk, TimedWait::DeadlineNanos(timeout), closed)) { return 0; } return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; @@ -469,7 +468,7 @@ int APS5_VABI sceRudpPollDestroy(int pollId) { if (g_polls.erase(pollId) == 0) { return RUDP_ERROR_INVALID_POLL_ID; } - g_polls_changed.notify_all(); + g_polls_changed.NotifyAll(); return 0; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index 4ff92c482f..a76e793be5 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -1,9 +1,11 @@ #include "SceTypes.hpp" +#include <algorithm> #include <array> #include <chrono> #include <cstddef> #include <cstdint> +#include <cstdio> #include <cstdlib> #include <limits> #include <thread> @@ -92,6 +94,23 @@ int main() { Require(sceRudpPollWait(poll, events.data(), eventCount, 20000) == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(20)); +#ifdef _WIN32 + for (const auto timeout : {100ULL, 1000ULL}) { + std::array<std::chrono::nanoseconds, 31> samples{}; + for (auto& sample : samples) { + const auto before = std::chrono::steady_clock::now(); + Require(sceRudpPollWait(poll, events.data(), eventCount, timeout) == 0); + sample = std::chrono::steady_clock::now() - before; + Require(sample >= std::chrono::microseconds(timeout)); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "sceRudpPollWait(%llu us) median: %lld ns\n", timeout, + static_cast<long long>(median.count())); + Require(median < std::chrono::milliseconds(12)); + } +#endif + Require(sceRudpTerminate(second) == 0); Require(sceRudpPollControl(poll, add, first, read) == 0); Require(sceRudpTerminate(first) == 0); @@ -101,6 +120,19 @@ int main() { Require(sceRudpPollControl(poll, add, third, read) == 0); Require(sceRudpPollControl(other, add, third, read) == 0); + const int finitePoll = sceRudpPollCreate(1); + Require(finitePoll > 0); + int finiteResult = 0; + std::thread finiteWaiter([&] { + finiteResult = sceRudpPollWait(finitePoll, events.data(), eventCount, 1000000); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const auto destroyedAt = std::chrono::steady_clock::now(); + Require(sceRudpPollDestroy(finitePoll) == 0); + finiteWaiter.join(); + Require(finiteResult == invalidPoll); + Require(std::chrono::steady_clock::now() - destroyedAt < std::chrono::milliseconds(500)); + int destroyedResult = 0; std::thread destroyedWaiter([&] { destroyedResult = sceRudpPollWait(poll, events.data(), eventCount, forever); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); From 961493dcc49548bde626220aebd059e748407124 Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:16:41 +0400 Subject: [PATCH 1235/1360] fix(rudp): compile timed waits into the runtime library --- core/libs/prx/libSceRudp/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 core/libs/prx/libSceRudp/CMakeLists.txt diff --git a/core/libs/prx/libSceRudp/CMakeLists.txt b/core/libs/prx/libSceRudp/CMakeLists.txt new file mode 100644 index 0000000000..962bd86e42 --- /dev/null +++ b/core/libs/prx/libSceRudp/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(libSceRudp SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp +) +target_include_directories(libSceRudp PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceRudp) From ac8be901adcac171265288f28e03b802d728dfbe Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Tue, 20 Oct 2026 16:01:31 +0200 Subject: [PATCH 1236/1360] fix(agc): order deferred host-import settles with main-queue accesses --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/src/GuestBufferMemory.cpp | 33 ++----- .../prx/libSceAgcDriver/tests/Recorder.cpp | 94 ++++++++++++++++--- 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..e966e93c15 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1489,6 +1489,11 @@ if(BUILD_TESTING) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) + add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + SKIP_RETURN_CODE 77 TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 0c7f40cd5c..45ea9b5901 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) static_cast<void>(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { @@ -987,7 +987,7 @@ const char* prepareSettleBatch(const Context& context, ImportSettle& settle, Set } if (batch.commands == VK_NULL_HANDLE) { VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - allocate.commandPool = context.settlePool; + allocate.commandPool = context.pool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1; if ((result = context.Function<PFN_vkAllocateCommandBuffers>("vkAllocateCommandBuffers")(context.device, &allocate, &batch.commands)) != VK_SUCCESS) { @@ -1037,9 +1037,7 @@ void submitSettleBatch(const Context& context, HostImports& state) { begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if ((result = context.Function<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(batch.commands, &begin)) != VK_SUCCESS) return fail("vkBeginCommandBuffer (settle batch)"); const auto copy = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); - const bool otherFamily = context.settleFamily != context.queueFamily; - const auto createBuffer = otherFamily ? context.Function<PFN_vkCreateBuffer>("vkCreateBuffer") : nullptr; - const auto bindBufferMemory = otherFamily ? context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory") : nullptr; + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::size_t consumed = 0; for (; consumed < state.settleWanted.size() && batch.bases.size() < settleBatchCopies; ++consumed) { const auto base = state.settleWanted[consumed]; @@ -1050,38 +1048,19 @@ void submitSettleBatch(const Context& context, HostImports& state) { state.pendingSettles.erase(settle); continue; } - VkBuffer source = import->second.buffer; - if (otherFamily) { - VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; - external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; - VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; - aliasInfo.size = std::min<VkDeviceSize>(import->second.bytes, 65536); - aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; - VkBuffer alias = VK_NULL_HANDLE; - if (createBuffer(context.device, &aliasInfo, nullptr, &alias) != VK_SUCCESS) { - state.pendingSettles.erase(settle); - continue; - } - if (bindBufferMemory(context.device, alias, import->second.memory, 0) != VK_SUCCESS) { - destroyBuffer(context.device, alias, nullptr); - state.pendingSettles.erase(settle); - continue; - } - batch.aliases.push_back(alias); - source = alias; - } const VkBufferCopy region{0, (VkDeviceSize{index} * settleBatchCopies + batch.bases.size()) * 4u, 4}; - copy(batch.commands, source, deviceSettle.batchTarget, 1, ®ion); + copy(batch.commands, import->second.buffer, deviceSettle.batchTarget, 1, ®ion); settle->second = PendingSettle{SettlePhase::Submitted, index, context.device}; batch.bases.push_back(base); } state.settleWanted.erase(state.settleWanted.begin(), state.settleWanted.begin() + static_cast<std::ptrdiff_t>(consumed)); + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_WRITE_BIT); if ((result = context.Function<PFN_vkEndCommandBuffer>("vkEndCommandBuffer")(batch.commands)) != VK_SUCCESS) return fail("vkEndCommandBuffer (settle batch)"); if (batch.bases.empty()) return; VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &batch.commands; - if ((result = context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.settleQueue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); + if ((result = context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.queue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); batch.busy = true; state.settlesSubmitted += batch.bases.size(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 851b36e404..667b46c27f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2027,7 +2027,10 @@ bool settleFirstUseRaced(const Context& context, int copies) { const auto* import = HostImportFor(context, address, unit); Require(import != nullptr && import->unwatched && !Watched(address, unit), "(f) a 64 KiB import was not compared at first"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + } recorder.Submit(); const auto submittedAt = std::chrono::steady_clock::now(); const auto deadline = submittedAt + std::chrono::seconds(5); @@ -2066,6 +2069,69 @@ bool settleFirstUseRaced(const Context& context, int copies) { return racing; } +bool settleWriteOrderTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: settle write order not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: settle write order not tested\n"; + return false; + } + void* block = AllocateWatched(unit, unit); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, unit); } + } release{block}; + const auto address = reinterpret_cast<std::uint64_t>(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + Recorder recorder(context); + recorder.Activate(); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "the write-order import was not initially compared"); + const auto fill = [&](std::uint32_t value) { + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function<PFN_vkCmdFillBuffer>("vkCmdFillBuffer")(commands, import->buffer, HostImportOffset(*import, address), unit, value); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + recorder.NotePendingWrite(address, unit); + recorder.Submit(); + }; + const auto before = HostImportSettles().submitted; + fill(0x22222222u); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(HostImportFor(context, address, unit) == import, "the write-order import changed while settling"); + } + Require(HostImportSettles().submitted > before, "the write-order import did not submit a settle"); + fill(0x33333333u); + recorder.Sync(); + while (!Watched(address, unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(Watched(address, unit), "the write-order import did not return to the write watch"); + const auto* bytes = static_cast<const std::uint8_t*>(block); + Require(std::all_of(bytes, bytes + unit, [](std::uint8_t value) { return value == 0x33; }), "the GPU store after the settle did not preserve its bytes"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, unit); + std::cout << "settle read between main-queue GPU writes passed\n"; + return true; +} + + bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2080,15 +2146,9 @@ bool settleFirstUseTests(const Device& device) { std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; return false; } - for (int copies = 4096; copies <= 65536; copies *= 4) { - if (settleFirstUseRaced(context, copies)) { - std::cout << "settled import first use tests passed\n"; - return true; - } - std::cout << "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them\n"; - } - std::cout << "(f) the main-queue copies finished first in every round: the first use of a settled import not tested\n"; - return false; + Require(!settleFirstUseRaced(context, 4096), "(f) the import returned to the write watch while its earlier main-queue reads were still in flight"); + std::cout << "settled import first use ordering tests passed\n"; + return true; } bool settleEarlierImportTests(const Device& device) { @@ -2127,7 +2187,10 @@ bool settleEarlierImportTests(const Device& device) { const auto* earlier = HostImportFor(context, address, 16); Require(earlier != nullptr && earlier->base == address && earlier->bytes == unit && earlier->unwatched, "(e) the first window of the range was not imported compared"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + } const auto* whole = HostImportFor(context, address + unit - 16, 32); Require(whole != nullptr && whole->base == address && whole->bytes == 2 * unit && whole->unwatched, "(e) an access across the windows did not replace the first window's import"); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); @@ -10482,7 +10545,7 @@ int main(int argc, char** argv) { } std::unique_ptr<Device> created; try { - created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only")); + created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; @@ -10521,6 +10584,13 @@ int main(int argc, char** argv) { } return deferredSettleTests(device) ? 0 : 77; } + if (argc == 2 && (std::string_view(argv[1]) == "--settle-write-order-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")) { + if (std::string_view(argv[1]) == "--settle-write-order-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return settleWriteOrderTests(device) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From 894d9f81d43eef187d3c62a9b0119702442ad586 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Tue, 20 Oct 2026 16:33:21 +0200 Subject: [PATCH 1237/1360] docs(agc): describe deferred settle submission ordering --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 8676f9acff..77bf4fd9af 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -145,7 +145,7 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. - A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6de88a50ab..2116ab2ea3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -114,7 +114,8 @@ - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From e6d7e36dcf3baa034536e8d9e0ed8fa31348e213 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Fri, 23 Oct 2026 13:10:52 +0200 Subject: [PATCH 1238/1360] fix(agc): bound deferred settle retirement waits --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/src/GuestBufferMemory.cpp | 2 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 109 ++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e966e93c15..2647cdce37 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1491,8 +1491,10 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) - set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + add_test(NAME agc_import_settle_wait_failure COMMAND agc_driver_recorder_tests --settle-wait-failure-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family agc_import_settle_wait_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 45ea9b5901..d2e39b540e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 60'000'000'000ull), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 667b46c27f..cbffabd1a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2131,6 +2131,114 @@ bool settleWriteOrderTests(const Device& device) { return true; } +struct SettleWaitFailure { + PFN_vkGetDeviceProcAddr resolve; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkResult result = VK_SUCCESS; + std::uint64_t timeout = 0; + unsigned waits = 0; + unsigned destroys = 0; + unsigned frees = 0; +}; + +SettleWaitFailure* settleWaitFailure = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL settleWaitForFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) { + auto& failure = *settleWaitFailure; + if (failure.result != VK_SUCCESS) { + failure.timeout = timeout; + ++failure.waits; + return failure.result; + } + return reinterpret_cast<PFN_vkWaitForFences>(failure.resolve(device, "vkWaitForFences"))(device, count, fences, all, timeout); +} + +VKAPI_ATTR void VKAPI_CALL settleDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (buffer == failure.buffer) ++failure.destroys; + reinterpret_cast<PFN_vkDestroyBuffer>(failure.resolve(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL settleFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (memory == failure.memory) ++failure.frees; + reinterpret_cast<PFN_vkFreeMemory>(failure.resolve(device, "vkFreeMemory"))(device, memory, allocator); +} + +PFN_vkVoidFunction VKAPI_CALL settleWaitDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast<PFN_vkVoidFunction>(settleWaitForFences); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(settleDestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(settleFreeMemory); + return settleWaitFailure->resolve(device, name); +} + +bool settleWaitFailureTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + auto context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) return false; + std::lock_guard gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) return false; + ClearHostImports(context.device); + SettleWaitFailure failure{context.deviceProc}; + settleWaitFailure = &failure; + context.deviceProc = settleWaitDeviceProc; + context.functions = nullptr; + void* block = AllocateWatched(unit, unit); + Require(block != nullptr, "no watched block for settle wait failure"); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + struct Release { + Context& context; + SettleWaitFailure& failure; + void* block; + ~Release() { + failure.result = VK_SUCCESS; + ClearHostImports(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, unit); + settleWaitFailure = nullptr; + } + } release{context, failure, block}; + const auto address = reinterpret_cast<std::uint64_t>(block); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "wait failure import was not initially compared"); + failure.buffer = import->buffer; + failure.memory = import->memory; + const auto before = HostImportSettles().submitted; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(HostImportSettles().submitted > before, "wait failure did not submit a settle"); + Check(context.Function<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(context.device), "vkDeviceWaitIdle before settle failure"); + for (const auto result : {VK_TIMEOUT, VK_ERROR_DEVICE_LOST}) { + failure.result = result; + const auto waits = failure.waits; + std::string error; + try { + ClearHostImports(context.device); + } catch (const std::runtime_error& caught) { + error = caught.what(); + } + failure.result = VK_SUCCESS; + Require(failure.waits == waits + 1 && failure.timeout > 0 && failure.timeout <= 60'000'000'000ull, "settle retirement did not use a bounded fence wait"); + Require(error == "AGC graphics: vkWaitForFences (settle batch): Vulkan result " + std::to_string(result), "settle retirement did not propagate its Vulkan failure"); + Require(failure.destroys == 0 && failure.frees == 0 && HostImportCovers(context, address, unit), "failed settle wait released its host import"); + } + ClearHostImports(context.device); + Require(failure.destroys == 1 && failure.frees == 1, "successful settle retirement did not release the retained import exactly once"); + std::cout << "bounded settle wait preserves imports on timeout and device loss; successful retry releases once\n"; + return true; +} bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -10591,6 +10699,7 @@ int main(int argc, char** argv) { } return settleWriteOrderTests(device) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-wait-failure-only") return settleWaitFailureTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From b996e31d8c11956df72031e42d286e4682ebe1fe Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev <eliasbruno124@gmail.com> Date: Tue, 20 Oct 2026 20:29:11 +0200 Subject: [PATCH 1239/1360] perf(agc): reclaim idle device buffers under storage cache pressure --- .../Graphics/include/BufferPool.hpp | 5 + .../Graphics/src/BufferPool.cpp | 50 +++++++ .../Graphics/src/ShaderResources.cpp | 30 ++-- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 140 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 216 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 5dcf54c727..495f80ee0a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -73,6 +73,7 @@ public: std::size_t RetainedImages(); std::optional<BufferAllocation> Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; + VkDeviceSize ReclaimIdleDevice(VkDeviceSize bytes, std::chrono::steady_clock::time_point now); static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); static VkDeviceSize SlabBlockBytes(std::size_t capacity); SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable); @@ -168,6 +169,10 @@ private: std::uint64_t imageEvictions = 0; std::list<Slot*> imageOrder; std::uint64_t clock = 0; + std::optional<std::chrono::steady_clock::time_point> idleRead; + std::uint64_t idleClock = 0; + std::uint64_t reclaimBefore = 0; + std::uint64_t scannedIdleClock = 0; AgcDriver::Mutex slabMutex; std::unordered_map<std::uint64_t, Slab> slabs; std::unordered_map<VkDeviceMemory, std::unique_ptr<SlabBlock>> slabBlocks; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 128970a898..47d1c1af23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -454,6 +454,56 @@ bool BufferPool::evictOldestImage(std::vector<BufferAllocation>& evicted) { return true; } +VkDeviceSize BufferPool::ReclaimIdleDevice(VkDeviceSize bytes, std::chrono::steady_clock::time_point now) { + if (bytes == 0) return 0; + const auto heaps = std::min<std::uint32_t>(memoryProperties.memoryHeapCount, VK_MAX_MEMORY_HEAPS); + if (std::count_if(memoryProperties.memoryHeaps, memoryProperties.memoryHeaps + heaps, [](const auto& heap) { return (heap.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0; }) != 1) return 0; + std::vector<BufferAllocation> evicted; + { + std::lock_guard lock(mutex); + if (!idleRead) { + idleRead = now; + idleClock = clock; + return 0; + } + if (now < *idleRead) return 0; + if (now - *idleRead >= std::chrono::seconds(1)) { + reclaimBefore = idleClock; + idleClock = clock; + idleRead = now; + } + if (reclaimBefore <= scannedIdleClock) return 0; + VkDeviceSize selected = 0; + while (selected < bytes) { + auto oldest = deviceTier.free.end(); + for (auto it = deviceTier.free.begin(); it != deviceTier.free.end(); ++it) { + if (it->second.empty()) continue; + const auto& slot = it->second.front(); + if (slot.allocation.buffer == VK_NULL_HANDLE || slot.allocation.slab || slot.lastUse > reclaimBefore) continue; + if (oldest == deviceTier.free.end() || slot.lastUse < oldest->second.front().lastUse) oldest = it; + } + if (oldest == deviceTier.free.end()) { + scannedIdleClock = reclaimBefore; + break; + } + if (evicted.empty()) evicted.reserve(deviceTier.slots); + const auto allocation = oldest->second.front().allocation; + evicted.push_back(allocation); + selected += allocation.allocationBytes; + deviceTier.retainedBytes -= allocation.allocationBytes; + oldest->second.pop_front(); + if (oldest->second.empty()) deviceTier.free.erase(oldest); + --deviceTier.slots; + ++deviceTier.evictions; + } + } + VkDeviceSize released = 0; + for (const auto& allocation : evicted) released += destroy(allocation); + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (profile && released != 0) AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] idle device reclaim: %llu bytes released for %llu bytes of storage-cache pressure\n", static_cast<unsigned long long>(released), static_cast<unsigned long long>(bytes)); + return released; +} + std::size_t BufferPool::MaxSlots() { // APS5_BUFFER_POOL_SLOTS=<n> bounds the retained allocations; 64 is the capacity the pool had // before least-recently-used retention, for A/B runs. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 863d6cd9cf..a7177e41f7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" @@ -572,7 +573,7 @@ std::uint64_t forcedCacheBudget() { return forced; } -std::uint64_t storageBudget(const Context& context) { +std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) { const auto forced = forcedCacheBudget(); if (forced != 0) return forced; if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); @@ -581,17 +582,26 @@ std::uint64_t storageBudget(const Context& context) { VkPhysicalDevice device{VK_NULL_HANDLE}; std::chrono::steady_clock::time_point read{}; std::uint64_t budget{0}; + std::uint64_t ceiling{0}; } cached; const auto now = std::chrono::steady_clock::now(); std::lock_guard lock(cached.mutex); - if (cached.device == context.physical && now - cached.read < std::chrono::seconds(1)) return cached.budget; - VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; - VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; - properties.pNext = &reported; - context.memoryProperties2(context.physical, &properties); - cached.device = context.physical; - cached.read = now; - cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); + const auto read = [&] { + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + context.memoryProperties2(context.physical, &properties); + cached.device = context.physical; + cached.read = now; + cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); + cached.ceiling = cached.budget; + if (const auto heap = largestDeviceLocalHeap(context.memory); heap && reported.heapBudget[*heap] != 0) { + cached.ceiling = std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(TextureCacheBudget(context.memory), reported.heapBudget[*heap] / 4u)); + } + }; + if (cached.device != context.physical || now - cached.read >= std::chrono::seconds(1)) read(); + const auto needed = std::min(required, cached.ceiling); + if (needed > cached.budget && context.bufferPool != nullptr && context.bufferPool->ReclaimIdleDevice(needed - cached.budget, now) != 0) read(); return cached.budget; } @@ -1182,7 +1192,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); entry.accounted = heldBytes(*entry.texture); - const auto budget = storageBudget(context); + const auto budget = storageBudget(context, cache.bytes + entry.accounted); while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) evicted.push_back(evictStorage(cache, std::prev(cache.entries.end()))); cache.bytes += entry.accounted; texture = entry.texture; diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index abdf8f0166..9547cfb6c2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -832,6 +832,103 @@ void AllocationFailureReportsHeapBudgets() { Expect(diagnosticBudgetQueries == 0, "missing-extension fallback attempted a memory budget query"); } +Context reclaimContext() { + auto context = mockContext(); + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4096 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {8192 * MiB, 0}; + context.memory.memoryTypes[1].heapIndex = 1; + return context; +} + +void IdleDeviceReclaim() { + mock = MockDevice{}; + { + auto context = reclaimContext(); + VkBuffer cold; + VkBuffer nextCold; + VkBuffer hot; + VkBuffer host; + VkBuffer small; + { DeviceBuffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); cold = buffer.Handle(); } + { DeviceBuffer buffer(context, 3 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); nextCold = buffer.Handle(); } + { DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); hot = buffer.Handle(); } + { Buffer buffer(context, 8 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); host = buffer.Handle(); } + { Buffer buffer(context, 1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); small = buffer.Handle(); } + DeviceBuffer active(context, 16 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.ReclaimIdleDevice(MiB, start) == 0, "the first pressure observation reclaimed a buffer without an idle interval"); + Expect(pool.ReclaimIdleDevice(MiB, start + std::chrono::milliseconds(999)) == 0, "a partial idle interval reclaimed a buffer"); + { + DeviceBuffer reused(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(reused.Handle() == hot, "the warm device buffer was not reused"); + } + const auto next = start + std::chrono::seconds(1); + SetAllocationFailureCountdown(0); + bool threw = false; + try { + pool.ReclaimIdleDevice(MiB, next); + } catch (const std::bad_alloc&) { + threw = true; + } + SetAllocationFailureCountdown(-1); + Expect(threw && mock.liveBuffers.contains(cold) && mock.liveBuffers.contains(nextCold), "a failed reclaim collection lost ownership of an allocation"); + Expect(pool.ReclaimIdleDevice(MiB, next) == 2 * MiB, "reclaim did not release exactly the oldest complete idle allocation"); + Expect(!mock.liveBuffers.contains(cold) && mock.liveBuffers.contains(nextCold), "bounded reclaim released the wrong allocation or exceeded its request"); + Expect(pool.ReclaimIdleDevice(0, next) == 0, "zero pressure released a buffer"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next) == 3 * MiB, "reclaim counted a hot, live, host or slab allocation as idle device memory"); + for (const auto buffer : {hot, host, small, active.Handle()}) Expect(mock.liveBuffers.contains(buffer), "reclaim destroyed a protected buffer"); + SetAllocationFailureCountdown(0); + const auto empty = pool.ReclaimIdleDevice(32 * MiB, next); + SetAllocationFailureCountdown(-1); + Expect(empty == 0, "an unchanged empty eligible set did not stay empty"); + Expect(pool.ReclaimIdleDevice(32 * MiB, start) == 0, "a backwards time observation reclaimed a buffer"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next + std::chrono::seconds(1)) == 4 * MiB, "a device buffer did not become reclaimable after a complete idle interval"); + Expect(mock.liveBuffers.contains(active.Handle()), "reclaim touched an allocation still in use"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next + std::chrono::seconds(2)) == 0, "an empty device tier reported reclaimed memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "idle device reclaim leaked an allocation"); +} + +void IdleReclaimPreservesWorkingSet() { + mock = MockDevice{}; + { + auto context = reclaimContext(); + const auto useWorkingSet = [&] { + std::vector<std::unique_ptr<DeviceBuffer>> buffers; + for (unsigned i = 0; i < 4; ++i) buffers.push_back(std::make_unique<DeviceBuffer>(context, 96 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)); + }; + useWorkingSet(); + const auto allocations = mock.allocations; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + auto& pool = *context.bufferPool; + Expect(pool.ReclaimIdleDevice(1024 * MiB, start) == 0, "initial pressure discarded the working set"); + for (unsigned second = 1; second <= 4; ++second) { + useWorkingSet(); + Expect(pool.ReclaimIdleDevice(1024 * MiB, start + std::chrono::seconds(second)) == 0, "pressure discarded buffers reused during the observation interval"); + Expect(mock.allocations == allocations && mock.liveBytes == 384 * MiB, "a reusable working set larger than 128 MiB was recreated or shrunk"); + } + Expect(pool.ReclaimIdleDevice(1024 * MiB, start + std::chrono::seconds(5)) == 384 * MiB, "the former working set remained retained after becoming idle"); + Expect(mock.liveBytes == 0, "reclaim did not return the idle working set to the device"); + } + Expect(mock.liveBuffers.empty(), "working-set reclaim leaked a buffer"); + for (const auto heaps : {0u, 2u}) { + mock = MockDevice{}; + { + auto context = reclaimContext(); + context.memory.memoryHeapCount = heaps; + context.memory.memoryHeaps[1].flags = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; + { DeviceBuffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.ReclaimIdleDevice(4 * MiB, start) == 0 && pool.ReclaimIdleDevice(4 * MiB, start + std::chrono::seconds(1)) == 0, "reclaim assumed a device heap without unambiguous ownership"); + Expect(mock.liveBytes == 2 * MiB, "reclaim released memory from an unknown or different local heap"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "unsupported-heap reclaim leaked an allocation"); + } +} + void SuccessfulAllocationKeepsPool() { mock = MockDevice{}; { @@ -1001,10 +1098,50 @@ void ImageMemoryReuse() { Expect(mock.frees == 35 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again after a trim"); } +void IdleReclaimPreservesImages() { + mock = MockDevice{}; + { + auto context = reclaimContext(); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 99; + const auto image = AllocateImageMemory(context, allocation, 1u, "test idle image", &type); + ReleaseImageMemory(context, image, 4 * MiB, type); + VkBuffer buffer; + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); buffer = idle.Handle(); } + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.RetainedImages() == 1 && mock.liveBytes == 6 * MiB, "the mixed pool fixture was not retained"); + Expect(pool.ReclaimIdleDevice(MiB, start) == 0, "initial pressure reclaimed mixed pool memory"); + const auto next = start + std::chrono::seconds(1); + const auto released = pool.ReclaimIdleDevice(MiB, next); + Expect(released == 2 * MiB, "idle buffer reclaim selected image memory instead of the oldest buffer"); + if (released != 2 * MiB) return; + Expect(!mock.liveBuffers.contains(buffer) && mock.liveBytes == 4 * MiB && pool.RetainedImages() == 1, "buffer reclaim changed image ownership or accounting"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next) == 0, "an image-only pool was reclaimed as idle buffers"); + SetAllocationFailureCountdown(0); + const auto empty = pool.ReclaimIdleDevice(32 * MiB, next); + SetAllocationFailureCountdown(-1); + Expect(empty == 0, "a repeated image-only scan allocated or reclaimed memory"); + const auto allocations = mock.allocations; + const auto reused = AllocateImageMemory(context, allocation, 1u, "test idle image", &type); + Expect(reused == image && mock.allocations == allocations && pool.RetainedImages() == 0, "the retained image was not reusable after buffer reclaim"); + ReleaseImageMemory(context, reused, 4 * MiB, type); + Expect(pool.RetainedImages() == 1 && mock.liveBytes == 4 * MiB, "the reused image was not retained exactly once"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty() && mock.frees == 2, "mixed pool cleanup leaked or freed an allocation twice"); +} + + int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { SmallDeviceRetention(); + if (std::getenv("APS5_STAGING_POOL_MIB") == nullptr) { + IdleDeviceReclaim(); + IdleReclaimPreservesWorkingSet(); + } return failures == 0 ? 0 : 1; } if (argc == 2) { @@ -1040,6 +1177,9 @@ int main(int argc, char** argv) { AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); AllocationFailureReportsHeapBudgets(); SuccessfulAllocationKeepsPool(); + IdleDeviceReclaim(); + IdleReclaimPreservesWorkingSet(); + IdleReclaimPreservesImages(); PartialSlabDoesNotRetry(); CacheReclaimRecovery(); ImageCacheReclaim(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2116ab2ea3..67b3d2b74f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -102,6 +102,7 @@ - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords, rounded up to an even count so that 64-bit atomics stay 8-byte aligned, per workgroup (the dword after the LDS is the atomic lock on a device without `shaderBufferInt64Atomics`), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. +- [Storage-cache pressure and idle device buffers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp): the one-second idle observation interval is a retention choice, not a console rule; buffers reused less often can be recreated under pressure. Before a storage-cache eviction, independently allocated device-tier buffers unused throughout that interval can be released, oldest first, for the cache's shortfall below its existing ceiling. Live buffers, host tiers and slab allocations are not reclaimed this way. Reclamation requires exactly one device-local heap because retained buffers do not record their heap. The heap budget is read again after an actual release; a delayed driver report can still cause an eviction. This is not a shared budget for all retained GPU resources, and its effect on titles with intermittent large buffer use is unmeasured. - [Host placement after a device-memory refusal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) (libSceAgcDriver): when the driver refuses a device-local allocation with `VK_ERROR_OUT_OF_DEVICE_MEMORY` after the pool's trim and the texture-cache release, device buffers and images (textures, storage textures, block, repacked, depth, depth plane and stencil copies, render, depth and multisampled targets and attachment proxies) are allocated in a host memory type their requirements allow. A host-placed device buffer leaves the pool on release. A host-placed image keeps that placement until it is destroyed and is never moved back to device memory, so every access to it crosses PCIe for its lifetime. The placements are counted in a rate-limited `[bufferpool]` line with the cumulative MiB, not the resident bytes. The 1x1 denormal clear probe still allocates device memory directly and throws on a refusal. - [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only and never compacted, so a module a later run no longer builds pipelines from stays in the file until it is deleted, and the whole file is read into memory while the replay runs (its modules reference that buffer until the last replayed pipeline was built). Only the first `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines recorded are replayed (a record is never rewritten, so a pipeline past the bound stays past it), and the pipeline store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES` (1024 each): a title with more configurations rebuilds the rest at the draw as before. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, which a dispatch that finds it resident checks against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost, an entry whose build fails is skipped, and both are counted in the `[pipeline-journal]` line rather than stopping the run. A replayed pipeline that the title never asks for again costs its build time and its driver memory (its optimized relink is deferred to its first draw, so a run whose first frames bind many replayed pipelines queues their relinks then); nothing ages recipes out of the file. Two runs sharing the file take turns under its lock file, but each only knows the modules it loaded or wrote itself, so a module the other run appended meanwhile may be appended again (the loader keeps the first copy). Records a run appends after another run's load are replayed by that run only after its next start. The file header's format number is the only guard against a change of the recipe codec, `CanonicalPipelineState` or the store key that keeps the structures' sizes (the recompiler's source version does not cover the driver's sources). - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline From e6daabdad83513868cde737087f6dc101154c057 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:42:29 +0200 Subject: [PATCH 1240/1360] test(libkernel): keep descriptor 0 open when guest_kqueue starts with standard input closed When the process starts with descriptor 0 closed, open returns 0, dup2(0, 0) succeeds and closing the temporary descriptor then closes descriptor 0 again, so the standard input case returned early at its EBADF branch. Skip the dup2 and the close when the temporary descriptor already is 0, on both platform branches, and check the replacement for an initially open and an initially closed standard input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestKqueue.cpp | 37 +++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index d7c73cad1e..fd0acdad4e 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1121,15 +1121,43 @@ static void Timeout() { static void ReplaceStandardInputWithNull() { #ifdef _WIN32 const int null = _open("NUL", _O_RDONLY); - Require(null >= 0 && _dup2(null, 0) == 0); - Require(_close(null) == 0); + Require(null >= 0); + if (null != 0) { + Require(_dup2(null, 0) == 0); + Require(_close(null) == 0); + } #else const int null = ::open("/dev/null", O_RDONLY); - Require(null >= 0 && ::dup2(null, 0) == 0); - Require(::close(null) == 0); + Require(null >= 0); + if (null != 0) { + Require(::dup2(null, 0) == 0); + Require(::close(null) == 0); + } #endif } +static bool StandardInputAtEnd() { + char byte = 0; +#ifdef _WIN32 + return _read(0, &byte, 1) == 0; +#else + return ::read(0, &byte, 1) == 0; +#endif +} + +static void StandardInputReplacement() { + ReplaceStandardInputWithNull(); + Require(StandardInputAtEnd()); +#ifdef _WIN32 + Require(_close(0) == 0); +#else + Require(::close(0) == 0); +#endif + ReplaceStandardInputWithNull(); + Require(StandardInputAtEnd()); +} + + static void StandardInput() { ReplaceStandardInputWithNull(); const int copy = dup_nid_postfix(0); @@ -1153,6 +1181,7 @@ static void StandardInput() { } int main() { + StandardInputReplacement(); StandardInput(); CreateAndClose(); Timers(); From 07d3d6447acd38499d2bb619f016576baff71803 Mon Sep 17 00:00:00 2001 From: QuentinRemond <qremond71@gmail.com> Date: Thu, 15 Oct 2026 20:39:32 +0200 Subject: [PATCH 1241/1360] feat(libkernel_sys): implement fpathconf and _fpathconf Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 71 ++++++++++++++++---------- core/libs/tests/GuestKernelSys.cpp | 43 ++++++++++++++++ docs/dev/debt/libkernel_sys.md | 2 +- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 03571bcf29..880b594c50 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -29,6 +29,8 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/KernelErrors.hpp" extern "C" int APS5_VABI getpid_nid_postfix(void); #ifdef _WIN32 #ifndef NOMINMAX @@ -128,6 +130,37 @@ std::int64_t Fail(int error) { return -1; } +std::int64_t PathConfigurationValue(int name, const char* function) { + switch (name) { + case PcLinkMax: return 32767; + case PcNameMax: return static_cast<std::int64_t>(GuestNameMax); + case PcPathMax: return static_cast<std::int64_t>(GuestPathMax); + case PcPipeBuf: return 512; + case PcChownRestricted: return 1; + case PcNoTrunc: return 1; + case PcFileSizeBits: return 64; + case PcRecMaxXferSize: return -1; + case PcSymlinkMax: return static_cast<std::int64_t>(GuestPathMax); + case PcPrioIo: return 0; + case PcSyncIo: return 0; + case PcMaxCanon: + case PcMaxInput: + case PcVdisable: + case PcAllocSizeMin: + case PcRecIncrXferSize: + case PcRecMinXferSize: + case PcRecXferAlign: + case PcMinHoleSize: + case PcAsyncIo: + case PcAclExtended: + case PcAclPathMax: + case PcMacPresent: + case PcAclNfs4: + throw std::runtime_error(std::string(function) + ": name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); + default: return Fail(GuestEinval); + } +} + int GuestError(const std::error_code& error) { const auto condition = error.default_error_condition(); if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; @@ -335,34 +368,16 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { const auto status = std::filesystem::status(native, error); if (error) return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); if (!std::filesystem::exists(status)) return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); - switch (name) { - case PcLinkMax: return 32767; - case PcNameMax: return static_cast<std::int64_t>(GuestNameMax); - case PcPathMax: return static_cast<std::int64_t>(GuestPathMax); - case PcPipeBuf: return 512; - case PcChownRestricted: return 1; - case PcNoTrunc: return 1; - case PcFileSizeBits: return 64; - case PcRecMaxXferSize: return -1; - case PcSymlinkMax: return static_cast<std::int64_t>(GuestPathMax); - case PcPrioIo: return 0; - case PcSyncIo: return 0; - case PcMaxCanon: - case PcMaxInput: - case PcVdisable: - case PcAllocSizeMin: - case PcRecIncrXferSize: - case PcRecMinXferSize: - case PcRecXferAlign: - case PcMinHoleSize: - case PcAsyncIo: - case PcAclExtended: - case PcAclPathMax: - case PcMacPresent: - case PcAclNfs4: - throw std::runtime_error("pathconf: name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); - default: return Fail(GuestEinval); - } + return PathConfigurationValue(name, "pathconf"); +} + +std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { + if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return Fail(GuestEbadf); + return PathConfigurationValue(name, "fpathconf"); +} + +std::int64_t APS5_VABI fpathconf_nid_postfix(int fd, int name) { + return _fpathconf_nid_postfix(fd, name); } #ifdef _WIN32 diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a540b9b666..78bc8e9fdc 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -40,6 +40,8 @@ int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); +std::int64_t APS5_VABI fpathconf_nid_postfix(int, int); +std::int64_t APS5_VABI _fpathconf_nid_postfix(int, int); int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); int APS5_VABI chroot_nid_postfix(const char*); @@ -312,6 +314,38 @@ static void CheckProcessQueries() { Require(sceKernelGetProcessType(getpid_nid_postfix() + 1) == static_cast<int>(0x80020003)); } +template <typename F> +bool DescriptorPathconfThrows(F fn, int descriptor, int name) { + try { + fn(descriptor, name); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +template <typename F> +void CheckDescriptorPathconf(F fn, int descriptor) { + Require(fn(descriptor, 1) == 32767); + Require(fn(descriptor, 4) == 255); + Require(fn(descriptor, 5) == 1024); + Require(fn(descriptor, 6) == 512); + Require(fn(descriptor, 7) == 1); + Require(fn(descriptor, 8) == 1); + Require(fn(descriptor, 12) == 64); + *__error_nid_postfix() = 0; + Require(fn(descriptor, 15) == -1 && Error() == 0); + Require(fn(descriptor, 18) == 1024); + Require(fn(descriptor, 54) == 0); + Require(fn(descriptor, 55) == 0); + for (const int name : {0, 11, 61, 62, 65, -1}) + Require(fn(descriptor, name) == -1 && Error() == 22); + for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) + Require(DescriptorPathconfThrows(fn, descriptor, name)); + Require(fn(-1, 4) == -1 && Error() == 9); + Require(fn(descriptor + 1000, 4) == -1 && Error() == 9); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); @@ -344,10 +378,19 @@ int main() { Require(fstat_nid_postfix(descriptor, &viaFstat) == 0 && _fstat_nid_postfix(descriptor, &viaUnderscore) == 0); Require(std::memcmp(&viaFstat, &viaUnderscore, sizeof(FileStat)) == 0 && viaFstat.st_size == 1); Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); + CheckDescriptorPathconf(fpathconf_nid_postfix, descriptor); + CheckDescriptorPathconf(_fpathconf_nid_postfix, descriptor); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); Require(fstat_nid_postfix(-1, &viaFstat) == -1 && Error() == 9); + Require(fpathconf_nid_postfix(descriptor, 4) == -1 && Error() == 9); + Require(_fpathconf_nid_postfix(descriptor, 4) == -1 && Error() == 9); + const int dirDescriptor = open_nid_postfix(root.generic_string().c_str(), 0, 0); + Require(dirDescriptor >= 0); + CheckDescriptorPathconf(fpathconf_nid_postfix, dirDescriptor); + CheckDescriptorPathconf(_fpathconf_nid_postfix, dirDescriptor); + Require(close_nid_postfix(dirDescriptor) == 0); char buffer[64]; for (const auto& component : {std::string("missing"), std::string("file.txt")}) { const auto prefix = root.generic_string() + "/" + component + "/../"; diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 49e8539bbc..085a67035e 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` From 4e7e4eed8faeda190e3fa96fa76d29998c67e468 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:19:04 +0200 Subject: [PATCH 1242/1360] fix(libkernel_sys): check the fpathconf descriptor with fstat DescriptorIsOpen_nid_no_patch is not extern "C", so the patched libkernel.prx does not export it and libkernel_sys.prx fails to load (patched_libraries: LoadLibraryEx error 127). Check the descriptor like fchown does: GuestFstat on Windows, fstat elsewhere, EBADF on failure. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 880b594c50..63a7c829b4 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -372,7 +372,13 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { } std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { - if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return Fail(GuestEbadf); + if (fd < 0) return Fail(GuestEbadf); +#ifdef _WIN32 + if (GuestFstat(fd) != 0) return Fail(GuestEbadf); +#else + struct stat status{}; + if (::fstat(fd, &status) != 0) return Fail(GuestEbadf); +#endif return PathConfigurationValue(name, "fpathconf"); } From 01655d7219616ed7de71ff7fa488ca7ecf0442b6 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:43:22 +0200 Subject: [PATCH 1243/1360] fix(libkernel_sys): answer fpathconf for pipes and _PC_ASYNC_IO as FreeBSD does kern_fpathconf answers _PC_ASYNC_IO with _POSIX_ASYNCHRONOUS_IO (200112) after the descriptor lookup, and a pipe or socket (no vnode) with PIPE_BUF for _PC_PIPE_BUF and EINVAL for every other name. Drop the unused File.hpp include and the duplicate KernelErrors.hpp include. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 12 ++++++++---- core/libs/tests/GuestKernelSys.cpp | 18 +++++++++++++++++- docs/dev/debt/libkernel_sys.md | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 63a7c829b4..867a3a52bc 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -29,8 +29,6 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" -#include "prx/libkernel/File/include/File.hpp" -#include "prx/libkernel/KernelErrors.hpp" extern "C" int APS5_VABI getpid_nid_postfix(void); #ifdef _WIN32 #ifndef NOMINMAX @@ -52,11 +50,12 @@ namespace { #ifdef _WIN32 extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} -static int GuestFstat(int fd) { +static int GuestFstat(int fd, bool* withoutVnode = nullptr) { const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); struct _stat64 status{}; const int result = ::_fstat64(fd, &status); _set_thread_local_invalid_parameter_handler(previous); + if (result == 0 && withoutVnode != nullptr) *withoutVnode = (status.st_mode & _S_IFMT) == _S_IFIFO; return result; } #endif @@ -100,6 +99,7 @@ constexpr int PcRecXferAlign = 17; constexpr int PcSymlinkMax = 18; constexpr int PcMinHoleSize = 21; constexpr int PcAsyncIo = 53; +constexpr std::int64_t PosixAsynchronousIo = 200112; constexpr int PcPrioIo = 54; constexpr int PcSyncIo = 55; constexpr int PcAclExtended = 59; @@ -373,12 +373,16 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { if (fd < 0) return Fail(GuestEbadf); + bool withoutVnode = false; #ifdef _WIN32 - if (GuestFstat(fd) != 0) return Fail(GuestEbadf); + if (GuestFstat(fd, &withoutVnode) != 0) return Fail(GuestEbadf); #else struct stat status{}; if (::fstat(fd, &status) != 0) return Fail(GuestEbadf); + withoutVnode = S_ISFIFO(status.st_mode) || S_ISSOCK(status.st_mode); #endif + if (name == PcAsyncIo) return PosixAsynchronousIo; + if (withoutVnode) return name == PcPipeBuf ? 512 : Fail(GuestEinval); return PathConfigurationValue(name, "fpathconf"); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 78bc8e9fdc..745c7b0346 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -340,12 +340,26 @@ void CheckDescriptorPathconf(F fn, int descriptor) { Require(fn(descriptor, 55) == 0); for (const int name : {0, 11, 61, 62, 65, -1}) Require(fn(descriptor, name) == -1 && Error() == 22); - for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) + Require(fn(descriptor, 53) == 200112); + for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 59, 60, 63, 64}) Require(DescriptorPathconfThrows(fn, descriptor, name)); Require(fn(-1, 4) == -1 && Error() == 9); Require(fn(descriptor + 1000, 4) == -1 && Error() == 9); } +template <typename F> +void CheckPipePathconf(F fn) { + int ends[2]{-1, -1}; + Require(pipe_nid_postfix(ends) == 0); + for (const int end : ends) { + Require(fn(end, 6) == 512); + Require(fn(end, 53) == 200112); + for (const int name : {0, 1, 4, 5, 7, 8, 12, 18, 54, 55, 61, -1}) + Require(fn(end, name) == -1 && Error() == 22); + } + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); @@ -380,6 +394,8 @@ int main() { Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); CheckDescriptorPathconf(fpathconf_nid_postfix, descriptor); CheckDescriptorPathconf(_fpathconf_nid_postfix, descriptor); + CheckPipePathconf(fpathconf_nid_postfix); + CheckPipePathconf(_fpathconf_nid_postfix); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 085a67035e..5a784a62b7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open, `_PC_ASYNC_IO` 200112 for any open descriptor and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` for every other name, as FreeBSD 11's `kern_fpathconf` does; a guest socket descriptor is not a host descriptor, so the `fstat` check reports it as `EBADF`, as `fchown` does. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and for `pathconf` `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` From 702d85e0bcf82546278cbd624cf5c1c5037ae5e7 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Sat, 24 Oct 2026 00:57:29 +1300 Subject: [PATCH 1244/1360] fix(libSceNpCommon): check for a queued callout before writing it sceNpCalloutStartOnCtx64 cleared and filled the entry before looking for it in the queue, so restarting a queued callout cut off every callout queued after it and changed its deadline while the callout thread could read it. The entry is now written only after the duplicate check under the context mutex. sceNpArchInit and sceNpArchTerm now hold a lock, so a second caller no longer returns before the global heap exists. Both differences from the firmware are recorded in the debt document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- core/libs/prx/libSceNpCommon/Runtime.cpp | 27 ++++++++++++++++-------- core/libs/tests/GuestNpCommonRuntime.cpp | 13 ++++++++++++ docs/dev/debt/libSceNpCommon.md | 2 ++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Runtime.cpp b/core/libs/prx/libSceNpCommon/Runtime.cpp index ef8324be67..965506fa51 100644 --- a/core/libs/prx/libSceNpCommon/Runtime.cpp +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -307,7 +307,8 @@ std::mt19937 g_random {std::random_device {}()}; NpMemoryHeap g_globalHeap {}; NpAllocator g_globalAllocator {}; NpAllocatorEx g_globalAllocatorEx {}; -std::atomic<int> g_archReferences {0}; +int g_archReferences = 0; +std::mutex g_archMutex; std::mutex g_ipcClientMutex; bool g_ipcClientInitialized = false; NpMemoryHeap g_ipcClientHeap {}; @@ -527,14 +528,21 @@ int APS5_VABI sceNpIpc2ClientTerm(void) { } int APS5_VABI sceNpArchInit(void) { - if (g_archReferences.fetch_add(1) != 0) return 0; + std::lock_guard lock(g_archMutex); + if (g_archReferences != 0) { + ++g_archReferences; + return 0; + } const int result = InitMemoryHeap(&g_globalHeap, NP_GLOBAL_HEAP_SIZE, NP_GLOBAL_HEAP_NAME); - if (result < 0) g_archReferences.fetch_sub(1); - return result < 0 ? result : 0; + if (result < 0) return result; + g_archReferences = 1; + return 0; } void APS5_VABI sceNpArchTerm(void) { - if (g_archReferences.fetch_sub(1) == 1) DestroyMemoryHeap(&g_globalHeap); + std::lock_guard lock(g_archMutex); + if (g_archReferences == 0) return; + if (--g_archReferences == 0) DestroyMemoryHeap(&g_globalHeap); } int APS5_VABI sceNpHeapInit(NpHeap* heap, void* base, std::size_t size, const char* name) { @@ -724,10 +732,7 @@ void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context) { int APS5_VABI sceNpCalloutStartOnCtx64(NpCalloutContext* context, NpCallout* callout, std::uint64_t usec, void (APS5_VABI *handler)(void* arg), void* arg) { if (context->initialized == 0) return NP_ERROR_NOT_INITIALIZED; - *callout = {}; - callout->deadline = ClockUsec() + usec; - callout->handler = handler; - callout->arg = arg; + const std::uint64_t deadline = ClockUsec() + usec; sceNpMutexLock(&context->mutex); for (NpCallout* entry = context->pending; entry != nullptr; entry = entry->next) { if (entry == callout) { @@ -735,6 +740,10 @@ int APS5_VABI sceNpCalloutStartOnCtx64(NpCalloutContext* context, NpCallout* cal return NP_ERROR_ALREADY_QUEUED; } } + *callout = {}; + callout->deadline = deadline; + callout->handler = handler; + callout->arg = arg; NpCallout** link = &context->pending; while (*link != nullptr && callout->deadline > (*link)->deadline) link = &(*link)->next; callout->next = *link; diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp index 5056298baf..ff9f9aa995 100644 --- a/core/libs/tests/GuestNpCommonRuntime.cpp +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -294,6 +294,19 @@ void TestCallouts() { Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 1, "a queued callout is removed"); Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 0, "removing twice reports nothing pending"); + NpCallout queuedFirst {}; + NpCallout queuedSecond {}; + Require(sceNpCalloutStartOnCtx(&context, &queuedFirst, 30000000, RecordUnexpected, nullptr) == 0, "an earlier distant callout is queued"); + Require(sceNpCalloutStartOnCtx(&context, &queuedSecond, 60000000, RecordUnexpected, nullptr) == 0, "a later distant callout is queued"); + const std::uint64_t queuedDeadline = queuedFirst.deadline; + Require(sceNpCalloutStartOnCtx(&context, &queuedFirst, 0, RecordFirst, &context) == NpAlreadyQueued, + "the earlier callout cannot be queued again"); + Require(queuedFirst.next == &queuedSecond && queuedFirst.deadline == queuedDeadline && queuedFirst.handler == RecordUnexpected && + queuedFirst.arg == nullptr, "a rejected start leaves the queued callout unchanged"); + Require(sceNpCalloutStopOnCtx(&context, &queuedSecond, &wasPending) == 0 && wasPending == 1, + "the later callout is still pending after the rejected start"); + Require(sceNpCalloutStopOnCtx(&context, &queuedFirst, &wasPending) == 0 && wasPending == 1, "the earlier callout is still pending"); + NpCallout later {}; Require(sceNpCalloutStartOnCtx(&context, &later, 20000, RecordSecond, nullptr) == 0, "a later callout is queued"); Require(sceNpCalloutStartOnCtx(&context, &callout, 1000, RecordFirst, nullptr) == 0, "an earlier callout is queued"); diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 32efbb235e..f18c9e79b3 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -47,6 +47,8 @@ The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. - [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 12.00, read from disassembly and not measured on a console. - [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. +- [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 12.00, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. +- [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 12.00, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. - [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. - [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. From fba61c0da01cf7fd4346cb94a6b3a9d86b68862c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:01:12 +0200 Subject: [PATCH 1245/1360] docs(debt): shorten the pathconf entry under the length limit Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel_sys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 5a784a62b7..68c58a80fb 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open, `_PC_ASYNC_IO` 200112 for any open descriptor and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` for every other name, as FreeBSD 11's `kern_fpathconf` does; a guest socket descriptor is not a host descriptor, so the `fstat` check reports it as `EBADF`, as `fchown` does. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and for `pathconf` `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) report FreeBSD 11's `ufs_pathconf` constants for files and directories alike (`_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0); whether the console's filesystems report the same is not verified. They throw for names whose UFS value depends on the mount, the kernel options or the page size, and where UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and `_PC_ASYNC_IO` in `pathconf`); other names fail with `EINVAL`. `fpathconf` and `_fpathconf` answer `_PC_ASYNC_IO` 200112 and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` otherwise, as FreeBSD 11's `kern_fpathconf` does; a guest socket is not a host descriptor, so the `fstat` check reports it as `EBADF`, as in `fchown` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` From 84ac63232c4250105dbe14ca557f0c40dee74b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= <slegl.jan@gmail.com> Date: Tue, 20 Oct 2026 13:25:26 +0200 Subject: [PATCH 1246/1360] perf(agc): take captureValidation's records in one pass over the lookup log captureValidation gave each texture of a build its last lookup record by scanning the thread's lookup log backwards once per texture. A build keeps all of its own records in the log, so a build over a large image table read on average half of a log at least as long as the table for every texture: about 1.1 million record reads per capture for a 1,500-texture table. LastRecordIndices sorts the build's objects once and walks the log backwards once, stopping when every object has its record: the same records, in O((textures + records) log textures). Its scratch vectors are released like the log's when a large build has grown them past the log's bound. A graphics test compares LastRecordIndices with the backward scan over repeated records and objects, absent and null objects, a 1,500-texture table over its own records, over records looked up twice and over a log missing its first records, and random logs of up to 2,048 records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 53 ++++++++++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 1 + .../libSceAgcDriver/tests/GraphicsTests.hpp | 1 + .../tests/LastRecordIndices.cpp | 64 +++++++++++++++++++ 6 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..ffde30f90e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1903,6 +1903,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp tests/DepthSurfaceReuse.cpp + tests/LastRecordIndices.cpp ) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index c68ef833c5..d090ebd057 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -50,6 +50,7 @@ bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<c bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words); bool ShaderStoresThroughAddresses(const ShaderRecompiler::SharedSpirv& spirv); +void LastRecordIndices(std::span<const void* const> logged, std::span<const void* const> objects, std::vector<std::size_t>& indices); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 863d6cd9cf..4d56d0db54 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -22,6 +22,7 @@ #include <cstdio> #include <cstdlib> #include <exception> +#include <functional> #include <stdexcept> #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -2911,21 +2912,57 @@ bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { } +void LastRecordIndices(std::span<const void* const> logged, std::span<const void* const> objects, std::vector<std::size_t>& indices) { + indices.assign(objects.size(), logged.size()); + thread_local std::vector<std::pair<const void*, std::size_t>>* wantedSlot = nullptr; + auto& wanted = ShaderRecompiler::ThreadOwned(wantedSlot); + wanted.clear(); + for (std::size_t i = 0; i < objects.size(); ++i) wanted.emplace_back(objects[i], i); + const auto byObject = [](const std::pair<const void*, std::size_t>& left, const std::pair<const void*, std::size_t>& right) { return std::less<const void*>{}(left.first, right.first); }; + std::sort(wanted.begin(), wanted.end(), byObject); + std::size_t open = wanted.size(); + for (auto at = logged.size(); at-- > 0 && open != 0;) { + const auto [first, last] = std::equal_range(wanted.begin(), wanted.end(), std::pair<const void*, std::size_t>{logged[at], 0}, byObject); + if (first == last || indices[first->second] != logged.size()) continue; + for (auto it = first; it != last; ++it) indices[it->second] = at; + open -= static_cast<std::size_t>(last - first); + } + if (wanted.capacity() > LookupLogBound + LookupLogBound / 2) { + wanted.clear(); + wanted.shrink_to_fit(); + } +} + // Turns the lookups' records into this object's per-texture validation records (see // ValidatedSurface); the last record of an object is the freshest. void ShaderResources::captureValidation() { - const auto record = [](const void* object) -> const LookupRecord* { - const auto& log = lookupLog(); - for (auto it = log.rbegin(); it != log.rend(); ++it) { - if (it->object == object) return &*it; - } - return nullptr; - }; + const auto& log = lookupLog(); + thread_local std::vector<const void*>* loggedSlot = nullptr; + auto& logged = ShaderRecompiler::ThreadOwned(loggedSlot); + thread_local std::vector<const void*>* objectsSlot = nullptr; + auto& objects = ShaderRecompiler::ThreadOwned(objectsSlot); + thread_local std::vector<std::size_t>* indicesSlot = nullptr; + auto& indices = ShaderRecompiler::ThreadOwned(indicesSlot); + logged.clear(); + for (const auto& entry : log) logged.push_back(entry.object); + objects.clear(); + for (const auto& texture : textures) objects.push_back(texture.get()); + LastRecordIndices(logged, objects, indices); validatedTextures.assign(textures.size(), {}); for (std::size_t i = 0; i < textures.size(); ++i) { - if (const auto* found = record(textures[i].get())) validatedTextures[i] = {found->resource, found->bytes, found->keys, found->generation, 0, found->source, found->depth, true}; + if (indices[i] == log.size()) continue; + const auto& found = log[indices[i]]; + validatedTextures[i] = {found.resource, found.bytes, found.keys, found.generation, 0, found.source, found.depth, true}; } lookupLog().clear(); + const auto trim = [](auto& scratch) { + if (scratch.capacity() <= LookupLogBound + LookupLogBound / 2) return; + scratch.clear(); + scratch.shrink_to_fit(); + }; + trim(logged); + trim(objects); + trim(indices); } std::size_t ShaderResources::LookupLogCapacity() { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7b4e5bda3d..824bc8bc3b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11599,6 +11599,7 @@ int main(int argc, char** argv) { RunGuestTextureResourceTests(); RunDepthSurfaceReuseTests(); RunGuestSamplerResourceTests(); + RunLastRecordIndicesTests(); mock = MockVulkan{}; auto textureDetilerContext = mockContext(); textureDetilerContext.limits.minStorageBufferOffsetAlignment = 16; diff --git a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp index 28a442b75d..95dcc30770 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp @@ -36,6 +36,7 @@ void RunTextureTilingTests(); void RunGuestTextureResourceTests(); void RunDepthSurfaceReuseTests(); void RunGuestSamplerResourceTests(); +void RunLastRecordIndicesTests(); void RunTextureDetilerTests(const AgcDriver::Graphics::Context& context, const TextureDetilerTestAccess& access); #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp new file mode 100644 index 0000000000..75b7a3ee21 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp @@ -0,0 +1,64 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include <array> +#include <cstddef> +#include <random> +#include <string> +#include <vector> + +namespace { + +using namespace AgcDriver::Graphics; + +std::vector<std::size_t> BackwardScan(const std::vector<const void*>& logged, const std::vector<const void*>& objects) { + std::vector<std::size_t> indices(objects.size(), logged.size()); + for (std::size_t i = 0; i < objects.size(); ++i) { + for (auto at = logged.size(); at-- > 0;) { + if (logged[at] == objects[i]) { + indices[i] = at; + break; + } + } + } + return indices; +} + +void CheckIndices(const std::vector<const void*>& logged, const std::vector<const void*>& objects, const std::string& what) { + std::vector<std::size_t> indices; + LastRecordIndices(logged, objects, indices); + Require(indices == BackwardScan(logged, objects), "last record indices, " + what + ": an object did not get the index of its last record"); +} + +} + +void RunLastRecordIndicesTests() { + std::array<int, 4096> pool{}; + const auto object = [&pool](std::size_t index) -> const void* { return &pool[index]; }; + + CheckIndices({object(0), object(1), object(0), object(2)}, {object(0), object(1), object(2), object(3), object(0), nullptr}, "repeated records and objects"); + CheckIndices({}, {object(0), nullptr}, "an empty log"); + CheckIndices({object(0), nullptr, object(1)}, {}, "no objects"); + CheckIndices({nullptr, object(0)}, {nullptr, object(0)}, "a null object"); + + std::vector<const void*> table; + for (std::size_t texture = 0; texture < 1500; ++texture) table.push_back(object(texture)); + CheckIndices(table, table, "a table of 1500 textures over its own records in table order"); + std::vector<const void*> repeated = table; + repeated.insert(repeated.end(), table.begin(), table.begin() + 548); + CheckIndices(repeated, table, "a table of 1500 textures whose first 548 were looked up twice"); + std::vector<const void*> missing(table.begin() + 476, table.end()); + CheckIndices(missing, table, "a table of 1500 textures over a log without its first 476 records"); + + std::mt19937 generator(0x5eed); + for (const std::size_t records : {std::size_t{1}, std::size_t{7}, std::size_t{64}, std::size_t{1023}, std::size_t{1024}, std::size_t{1500}, std::size_t{2048}}) { + for (const std::size_t textures : {std::size_t{1}, std::size_t{5}, std::size_t{300}, std::size_t{1500}}) { + for (const std::size_t distinct : {std::size_t{8}, pool.size()}) { + std::vector<const void*> logged(records); + for (auto& entry : logged) entry = object(generator() % distinct); + std::vector<const void*> objects(textures); + for (auto& entry : objects) entry = object(generator() % distinct); + CheckIndices(logged, objects, std::to_string(records) + " records, " + std::to_string(textures) + " objects of " + std::to_string(distinct)); + } + } + } +} From bcaaf3924173fe27851109ca6af369878c4ac1b9 Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Sat, 24 Oct 2026 01:06:42 +1300 Subject: [PATCH 1247/1360] docs(libSceNpCommon): cite firmware 13.60, not 12.00, for the decrypted modules The firmware version comes from the SDK 0x13600007 recorded in each module's module-param segment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- docs/dev/debt/libSceNpCommon.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index f18c9e79b3..da67369967 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -45,10 +45,10 @@ The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` - [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. - [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. -- [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 12.00, read from disassembly and not measured on a console. +- [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 13.60, read from disassembly and not measured on a console. - [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. -- [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 12.00, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. -- [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 12.00, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. +- [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 13.60, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. +- [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 13.60, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. - [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. - [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. From 5b2955b9cfcfc366e070aa55ca6a67ee399e32ef Mon Sep 17 00:00:00 2001 From: LeandroLP23 <leandroplacidi@gmail.com> Date: Fri, 23 Oct 2026 14:17:45 +0200 Subject: [PATCH 1248/1360] ci(build): let a running main build finish before the next push's build starts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cab59565e5..3dbe96d99d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ permissions: concurrency: group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name != 'push' }} env: CONFIGURE: >- From 12ca5c16f0cc7c9aee25456e323e8c519c90cb9f Mon Sep 17 00:00:00 2001 From: Giuseppe Magliano <giuseppemagliano001@gmail.com> Date: Fri, 23 Oct 2026 12:27:39 +0200 Subject: [PATCH 1249/1360] test(agc): skip the forced-watch shadow tests where imports report pages written agc_driver_bda_reads_shadowed_results, its per-build form and agc_unit_shadow_completion_store force host imports to stay watched on non-Windows hosts. On RADV, importing watched host memory and reading it from the GPU both report the pages written, which is why the driver compares those imports instead of watching them. Under the forced watch those reports read as CPU stores: every shadowed unit is stale-dropped before the address-based publish, and the tests fail on every run. Both tests now run the import probe and skip, like agc_unit_shadow and agc_shadowed_key_stores already do there, when the import or a GPU read reports pages written. Devices whose imports stay clean until a GPU write, such as lavapipe, still run them. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a5bc67e508..ec52550d47 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5106,6 +5106,11 @@ bool addressShadowTests(const Device& device, Recorder& recorder) { } const auto decided = PrepareImportWatch(base); #ifndef _WIN32 + const auto probe = ProbeImportWriteProtection(base); + if (probe.failure != nullptr || probe.writtenAtImport != 0 || probe.writtenAfterSubmit != 0) { + std::cout << "skipped, the device reports watched host memory written when it imports or reads it\n"; + return false; + } SetImportWatch(base, ImportWatch::Watch); #endif struct Restore { @@ -5241,6 +5246,11 @@ bool completionStoreShadowTests(const Device& device, Recorder& recorder) { } const auto decided = PrepareImportWatch(base); #ifndef _WIN32 + const auto probe = ProbeImportWriteProtection(base); + if (probe.failure != nullptr || probe.writtenAtImport != 0 || probe.writtenAfterSubmit != 0) { + std::cout << "skipped, the device reports watched host memory written when it imports or reads it\n"; + return false; + } SetImportWatch(base, ImportWatch::Watch); #endif struct Restore { From eb9491b1949870eee518df39343d4d376a5d1f89 Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:31:01 +0000 Subject: [PATCH 1250/1360] feat(libSceLibreSsl): implement RAND and the CRYPTO allocator functions RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free and CRYPTO_set_mem_functions threw. They now behave as in LibreSSL 2.6.5 through 3.4.3, where these functions did not change: RAND_bytes fills the buffer from host entropy and returns 1, RAND_add ignores its input, RAND_status returns 1, RAND_load_file returns 123456 for -1 and the requested count otherwise, RAND_file_name copies "/dev/urandom" with strlcpy semantics, CRYPTO_malloc and CRYPTO_free use the guest heap, and CRYPTO_set_mem_functions refuses with 0. CRYPTO_malloc of zero or a negative size throws because later LibreSSL versions take a size_t and call malloc instead of returning null. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 13 ++- core/libs/prx/libSceLibreSsl/Export.cpp | 40 --------- core/libs/prx/libSceLibreSsl/src/Runtime.cpp | 56 ++++++++++++ core/libs/tests/GuestLibreSslRuntime.cpp | 94 ++++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/Runtime.cpp create mode 100644 core/libs/tests/GuestLibreSslRuntime.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 78e8a9adec..ad5ccb6a95 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -36,4 +36,15 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_des PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") endif() + + add_test_executable(guest_libressl_runtime_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslRuntime.cpp) + target_include_directories(guest_libressl_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_runtime_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_runtime_tests) + add_test(NAME guest_libressl_runtime COMMAND guest_libressl_runtime_tests) + set_tests_properties(guest_libressl_runtime PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_runtime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") + endif() endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 1e2c3c9caa..c18fb7ef2c 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -283,16 +283,6 @@ int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CRYPTO_malloc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI CRYPTO_num_locks_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -308,11 +298,6 @@ int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char* input, unsigned char* output, const unsigned char* schedule, int enc) { if (input == nullptr || output == nullptr || schedule == nullptr) APS5_INVALID_ARG_EX; mbedtls_des_context context; @@ -1017,31 +1002,6 @@ int APS5_VABI PKCS12_parse_nid_postfix(void) { return 0; } -int APS5_VABI RAND_add_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_bytes_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_file_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_load_file_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_status_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI RSA_flags_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Runtime.cpp b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp new file mode 100644 index 0000000000..d7ce6ed69b --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp @@ -0,0 +1,56 @@ +#include <cstddef> +#include <cstdint> +#include <cstring> +#include <stdexcept> +#include <string> +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t size); +void APS5_VABI free_nid_postfix(void* pointer); + +void APS5_VABI RAND_add_nid_postfix(const void*, int, double) { +} + +int APS5_VABI RAND_status_nid_postfix(void) { + return 1; +} + +int APS5_VABI RAND_bytes_nid_postfix(unsigned char* buffer, int count) { + if (count > 0) { + if (!buffer) throw std::invalid_argument(std::string(__func__) + ": null buffer"); + HostEntropy_nid_no_patch(buffer, static_cast<std::size_t>(count)); + } + return 1; +} + +int APS5_VABI RAND_load_file_nid_postfix(const char*, std::int64_t bytes) { + return bytes == -1 ? 123456 : static_cast<int>(bytes); +} + +const char* APS5_VABI RAND_file_name_nid_postfix(char* buffer, std::size_t size) { + static constexpr char path[] = "/dev/urandom"; + if (size) { + if (!buffer) throw std::invalid_argument(std::string(__func__) + ": null buffer"); + const std::size_t copied = size - 1 < sizeof(path) - 1 ? size - 1 : sizeof(path) - 1; + std::memcpy(buffer, path, copied); + buffer[copied] = '\0'; + } + return sizeof(path) - 1 >= size ? nullptr : buffer; +} + +void* APS5_VABI CRYPTO_malloc_nid_postfix(int count, const char*, int) { + if (count <= 0) NotImplemented_nid_no_patch("CRYPTO_malloc: zero or negative size"); + return malloc_nid_postfix(static_cast<std::size_t>(count)); +} + +void APS5_VABI CRYPTO_free_nid_postfix(void* pointer) { + free_nid_postfix(pointer); +} + +int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void*, void*, void*) { + return 0; +} + +} diff --git a/core/libs/tests/GuestLibreSslRuntime.cpp b/core/libs/tests/GuestLibreSslRuntime.cpp new file mode 100644 index 0000000000..5a46198e17 --- /dev/null +++ b/core/libs/tests/GuestLibreSslRuntime.cpp @@ -0,0 +1,94 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <source_location> +#include <stdexcept> + +extern "C" { +void APS5_VABI RAND_add_nid_postfix(const void*, int, double); +int APS5_VABI RAND_status_nid_postfix(void); +int APS5_VABI RAND_bytes_nid_postfix(unsigned char*, int); +int APS5_VABI RAND_load_file_nid_postfix(const char*, std::int64_t); +const char* APS5_VABI RAND_file_name_nid_postfix(char*, std::size_t); +void* APS5_VABI CRYPTO_malloc_nid_postfix(int, const char*, int); +void APS5_VABI CRYPTO_free_nid_postfix(void*); +int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void*, void*, void*); +void APS5_VABI free_nid_postfix(void*); +void* APS5_VABI malloc_nid_postfix(std::size_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL runtime check failed at line %u\n", location.line()); + std::abort(); +} + +template<typename TAction> static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +static void InitializeHeap() { + void* const api[10] {}; + ApplicationHeapRegister_nid_no_patch(api); +} + +int main() { + InitializeHeap(); + std::array<unsigned char, 4099> first {}; + std::array<unsigned char, 4099> second {}; + first.fill(0xa5); + Require(RAND_bytes_nid_postfix(first.data(), 0) == 1 && RAND_bytes_nid_postfix(first.data(), -5) == 1); + Require(RAND_bytes_nid_postfix(nullptr, 0) == 1); + Require(std::all_of(first.begin(), first.end(), [](unsigned char value) { return value == 0xa5; })); + Require(RAND_bytes_nid_postfix(first.data(), 3) == 1); + Require(std::all_of(first.begin() + 3, first.end(), [](unsigned char value) { return value == 0xa5; })); + Require(RAND_bytes_nid_postfix(first.data(), static_cast<int>(first.size())) == 1); + Require(RAND_bytes_nid_postfix(second.data(), static_cast<int>(second.size())) == 1); + Require(first != second); + std::array<std::size_t, 256> counts {}; + for (const auto value : first) ++counts[value]; + Require(*std::max_element(counts.begin(), counts.end()) < 64); + RequireThrows([] { RAND_bytes_nid_postfix(nullptr, 1); }); + + RAND_add_nid_postfix(first.data(), 16, 8.0); + RAND_add_nid_postfix(nullptr, 0, 0.0); + Require(RAND_status_nid_postfix() == 1); + Require(RAND_load_file_nid_postfix("/nonexistent", -1) == 123456); + Require(RAND_load_file_nid_postfix(nullptr, 1024) == 1024 && RAND_load_file_nid_postfix(nullptr, 0) == 0); + Require(RAND_load_file_nid_postfix(nullptr, 0x100000005) == 5); + + char name[32]; + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, sizeof(name)) == name && !std::strcmp(name, "/dev/urandom")); + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, 13) == name && !std::strcmp(name, "/dev/urandom")); + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, 12) == nullptr && !std::strcmp(name, "/dev/urando")); + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, 0) == nullptr && name[0] == 'x'); + Require(RAND_file_name_nid_postfix(nullptr, 0) == nullptr); + RequireThrows([] { RAND_file_name_nid_postfix(nullptr, 1); }); + + auto* block = static_cast<unsigned char*>(CRYPTO_malloc_nid_postfix(64, "file.c", 10)); + Require(block != nullptr); + std::memset(block, 0x5a, 64); + CRYPTO_free_nid_postfix(block); + block = static_cast<unsigned char*>(CRYPTO_malloc_nid_postfix(32, nullptr, 0)); + Require(block != nullptr); + free_nid_postfix(block); + block = static_cast<unsigned char*>(malloc_nid_postfix(16)); + CRYPTO_free_nid_postfix(block); + CRYPTO_free_nid_postfix(nullptr); + RequireThrows([] { CRYPTO_malloc_nid_postfix(0, nullptr, 0); }); + RequireThrows([] { CRYPTO_malloc_nid_postfix(-1, nullptr, 0); }); + Require(CRYPTO_set_mem_functions_nid_postfix(reinterpret_cast<void*>(&std::malloc), reinterpret_cast<void*>(&std::realloc), reinterpret_cast<void*>(&std::free)) == 0); + Require(CRYPTO_set_mem_functions_nid_postfix(nullptr, nullptr, nullptr) == 0); + std::puts("libSceLibreSsl runtime tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index f84ef0fbf6..43069ab74b 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,8 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 373 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 365 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. +- [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` throws for a size of zero or less, which returns null up to 3.4 and calls `malloc` with a `size_t` size in later versions. A null buffer with a positive length throws From 2df7e993d3aca84c7378aeb2f3fe9082bc82a6bb Mon Sep 17 00:00:00 2001 From: mitul72 <mituldhawan2@gmail.com> Date: Tue, 20 Oct 2026 16:29:18 -0600 Subject: [PATCH 1251/1360] fix(agc): answer a collect memo hit with the generation of the last finished tracker update A per-thread collect memo hit read the tracker generation without the tracker mutex. A walk running on another thread bumps the generation before it stamps the blocks it finds written, so the hit could return a value that walk then stamps blocks with; a caller that recorded it as its verified generation trusts those blocks in every later UnchangedSince and never refreshes them. The tracker now publishes the generation each time an update releases its mutex (TrackerLock), after every stamp of that update is in place, and a memo hit returns the published value. MarkWritten's release around beforeUnwatch does not publish, since it stamps again after relocking. The per-epoch collect map hit returns the published value too. Completed MarkWritten bumps are still seen at once; a value older than the live one only makes a check fail and refresh. agc_collect_memo_generation links GuestMemory against a fake write watch that holds a walk after its generation bump, takes a memo hit on another thread meanwhile, and checks the write the walk then stamps is not trusted against the generation the hit returned. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++ .../Execution/src/GuestMemory.cpp | 62 +++++++-- .../tests/CollectMemoGeneration.cpp | 127 ++++++++++++++++++ 3 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..a1c8a069b1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -748,6 +748,19 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING AND NOT WIN32) + add_test_executable(agc_collect_memo_generation_tests + tests/CollectMemoGeneration.cpp + Execution/src/GuestMemory.cpp + Execution/src/PerformanceTraceThread.cpp + ) + target_include_directories(agc_collect_memo_generation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_collect_memo_generation_tests PRIVATE shader_recompiler_agc_driver libc) + set_target_properties(agc_collect_memo_generation_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME agc_collect_memo_generation COMMAND agc_collect_memo_generation_tests) + set_tests_properties(agc_collect_memo_generation PROPERTIES TIMEOUT 30) +endif() + add_test_executable(agc_thread_priority_tests EXCLUDE_FROM_ALL tests/ThreadPriority.cpp Execution/src/ThreadPriority.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 69341f8385..c89d0cf96b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -878,6 +878,7 @@ struct WriteTracker { // bumps, or an image refreshed after a stamp would keep failing UnchangedSince until the next // walk of its range and re-upload at every use of the epoch. std::atomic<std::uint32_t> generation{1}; + std::atomic<std::uint32_t> stable{1}; std::vector<void*> pages; // Collect memo: page-rounded ranges walked during the current epoch (see BumpCollectEpoch). A // collect fully inside one skips GetWriteWatch, whose cost is proportional to the range's pages, @@ -1229,16 +1230,51 @@ WriteTracker& Tracker() { // Takes the tracker mutex; under APS5_PROFILE_DRAW an acquisition that found it held is counted // ('tracker waits N / M' in [guestmem]), which says whether walking outside the lock would pay. -std::unique_lock<AgcDriver::Mutex> lockTracker(WriteTracker& tracker) { - static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile) return std::unique_lock(tracker.mutex); - std::unique_lock lock(tracker.mutex, std::try_to_lock); - if (!lock.owns_lock()) { - trackerWaits.fetch_add(1, std::memory_order_relaxed); +class TrackerLock { +public: + explicit TrackerLock(WriteTracker& tracker) : tracker(tracker), lock(tracker.mutex, std::defer_lock) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) { + lock.lock(); + return; + } + if (!lock.try_lock()) { + trackerWaits.fetch_add(1, std::memory_order_relaxed); + lock.lock(); + } + trackerAcquisitions.fetch_add(1, std::memory_order_relaxed); + } + ~TrackerLock() { + if (lock.owns_lock()) publish(); + } + TrackerLock(const TrackerLock&) = delete; + TrackerLock& operator=(const TrackerLock&) = delete; + + void Unlock() { + publish(); + lock.unlock(); + } + + void Suspend() { + lock.unlock(); + } + + void Resume() { lock.lock(); } - trackerAcquisitions.fetch_add(1, std::memory_order_relaxed); - return lock; + +private: + void publish() { + const auto current = tracker.generation.load(std::memory_order_relaxed); + if (tracker.stable.load(std::memory_order_relaxed) != current) tracker.stable.store(current, std::memory_order_release); + } + + WriteTracker& tracker; + std::unique_lock<AgcDriver::Mutex> lock; +}; + +TrackerLock lockTracker(WriteTracker& tracker) { + return TrackerLock(tracker); } #ifdef _WIN32 @@ -1411,7 +1447,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi const auto& map = epochCollectMap(epoch); if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && found->second.mapping == mapping && stop <= found->second.end) { collectMemoHits.fetch_add(1, std::memory_order_relaxed); - return tracker.generation.load(std::memory_order_relaxed); + return tracker.stable.load(std::memory_order_acquire); } } for (const auto& entry : threadCollectMemo().entries) { @@ -1420,7 +1456,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // overlapping collect) were written before this caller reads, and an older value would // make every later UnchangedSince fail until the epoch ends. collectMemoHits.fetch_add(1, std::memory_order_relaxed); - return tracker.generation.load(std::memory_order_relaxed); + return tracker.stable.load(std::memory_order_acquire); } } } @@ -1609,7 +1645,7 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function auto lock = lockTracker(tracker); tracker.initialize(); if (!tracker.watched) { - lock.unlock(); + lock.Unlock(); return import(); } constexpr std::uint64_t page = 4096; @@ -1789,9 +1825,9 @@ static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool const bool collected = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast<std::uintptr_t>(blockFirst), static_cast<std::size_t>(blockStop - blockFirst), &stampWrittenRun, &runs); #endif if (!collected) { - lock.unlock(); + lock.Suspend(); beforeUnwatch(blockFirst, static_cast<std::size_t>(blockStop - blockFirst)); - lock.lock(); + lock.Resume(); unwatchLocked(tracker, blockFirst, static_cast<std::size_t>(blockStop - blockFirst)); tracker.noteUnstampedStore(address, address + bytes); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp b/core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp new file mode 100644 index 0000000000..90d5e8a215 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include <algorithm> +#include <chrono> +#include <condition_variable> +#include <cstdint> +#include <iostream> +#include <mutex> +#include <stdexcept> +#include <string> +#include <thread> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint64_t Block = 65536; +constexpr std::uint64_t Page = 4096; +constexpr std::uint64_t Base = std::uint64_t{1} << 32; +constexpr std::chrono::seconds Patience{10}; + +struct FakeWatch { + std::mutex mutex; + std::condition_variable changed; + std::vector<std::pair<std::uintptr_t, std::uintptr_t>> written; + bool holdNextWalk = false; + bool holding = false; + bool released = false; + bool timedOut = false; +}; + +FakeWatch& Fake() { + static FakeWatch fake; + return fake; +} + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +} + +namespace GuestWriteWatch { + +extern "C" { + +bool GuestWriteWatchAvailable_nid_postfix() { + return true; +} + +bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes) { + return address >= Base && bytes <= 2 * Block && address - Base <= 2 * Block - bytes; +} + +bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) { + auto& fake = Fake(); + const auto end = address + bytes; + std::vector<std::pair<std::uintptr_t, std::uintptr_t>> runs; + { + std::unique_lock lock(fake.mutex); + if (fake.holdNextWalk) { + fake.holdNextWalk = false; + fake.holding = true; + fake.changed.notify_all(); + fake.timedOut = !fake.changed.wait_for(lock, Patience, [&] { return fake.released; }); + } + for (auto it = fake.written.begin(); it != fake.written.end();) { + if (it->first < end && address < it->second) { + runs.emplace_back(std::max(it->first, address), std::min(it->second, end)); + it = fake.written.erase(it); + } else ++it; + } + } + for (const auto& [from, to] : runs) written(context, from, to); + return true; +} + +} + +} + +namespace { + +using namespace AgcDriver::GuestMemory; + +void CheckMemoHitDuringWalk() { + auto& fake = Fake(); + BumpCollectEpoch(); + const auto collected = CollectWrites(Base, Block); + Require(collected != 0 && UnchangedSince(Base, Block, collected), "the block was not collected"); + { + const std::lock_guard lock(fake.mutex); + fake.written.emplace_back(Base + Page, Base + 2 * Page); + fake.holdNextWalk = true; + } + std::thread walker([] { CollectWritesUncached(Base, 2 * Block); }); + bool holding = false; + { + std::unique_lock lock(fake.mutex); + holding = fake.changed.wait_for(lock, Patience, [&] { return fake.holding; }); + } + const auto memoized = holding ? CollectWrites(Base, Block) : 0; + { + const std::lock_guard lock(fake.mutex); + fake.released = true; + } + fake.changed.notify_all(); + walker.join(); + Require(holding, "the second walk did not reach the write watch"); + Require(!fake.timedOut, "the memoized collect waited for the walk in progress"); + Require(!UnchangedSince(Base + Page, Page, memoized), "a memoized collect returned the generation a walk in progress then stamped a write with"); + BumpCollectEpoch(); + Require(CollectWrites(Base, Block) > memoized && !UnchangedSince(Base, Block, memoized), "a later epoch trusts the write stamped by the walk in progress"); +} + +} + +int main() { + try { + CheckMemoHitDuringWalk(); + } catch (const std::exception& error) { + std::cerr << "collect memo generation test failed: " << error.what() << "\n"; + return 1; + } + std::cout << "collect memo generation test passed\n"; + return 0; +} From 687e7d637eca5b2576c8f14d921362d0f1f216ef Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Fri, 23 Oct 2026 13:15:04 +0000 Subject: [PATCH 1252/1360] fix(libSceLibreSsl): return null from CRYPTO_malloc for sizes of zero or less CRYPTO_malloc threw for a size of zero or less. LibreSSL 2.5.4, 3.4.1 and 3.4.3 all take an int size and return null there, so it now does the same, and the test checks for null instead of a throw. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- core/libs/prx/libSceLibreSsl/src/Runtime.cpp | 2 +- core/libs/tests/GuestLibreSslRuntime.cpp | 4 ++-- docs/dev/debt/libSceLibreSsl.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Runtime.cpp b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp index d7ce6ed69b..14f1cb5572 100644 --- a/core/libs/prx/libSceLibreSsl/src/Runtime.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp @@ -41,7 +41,7 @@ const char* APS5_VABI RAND_file_name_nid_postfix(char* buffer, std::size_t size) } void* APS5_VABI CRYPTO_malloc_nid_postfix(int count, const char*, int) { - if (count <= 0) NotImplemented_nid_no_patch("CRYPTO_malloc: zero or negative size"); + if (count <= 0) return nullptr; return malloc_nid_postfix(static_cast<std::size_t>(count)); } diff --git a/core/libs/tests/GuestLibreSslRuntime.cpp b/core/libs/tests/GuestLibreSslRuntime.cpp index 5a46198e17..937036d5fd 100644 --- a/core/libs/tests/GuestLibreSslRuntime.cpp +++ b/core/libs/tests/GuestLibreSslRuntime.cpp @@ -86,8 +86,8 @@ int main() { block = static_cast<unsigned char*>(malloc_nid_postfix(16)); CRYPTO_free_nid_postfix(block); CRYPTO_free_nid_postfix(nullptr); - RequireThrows([] { CRYPTO_malloc_nid_postfix(0, nullptr, 0); }); - RequireThrows([] { CRYPTO_malloc_nid_postfix(-1, nullptr, 0); }); + Require(CRYPTO_malloc_nid_postfix(0, nullptr, 0) == nullptr); + Require(CRYPTO_malloc_nid_postfix(-1, nullptr, 0) == nullptr); Require(CRYPTO_set_mem_functions_nid_postfix(reinterpret_cast<void*>(&std::malloc), reinterpret_cast<void*>(&std::realloc), reinterpret_cast<void*>(&std::free)) == 0); Require(CRYPTO_set_mem_functions_nid_postfix(nullptr, nullptr, nullptr) == 0); std::puts("libSceLibreSsl runtime tests passed"); diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 43069ab74b..29a95f22e3 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -6,4 +6,4 @@ - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. -- [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` throws for a size of zero or less, which returns null up to 3.4 and calls `malloc` with a `size_t` size in later versions. A null buffer with a positive length throws +- [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws From 2fd0610bffa21401a7ebd534a0b0d667bf82749f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 15:21:46 +0200 Subject: [PATCH 1253/1360] fix(agc): draw a last provoking vertex that nothing observes without the extension Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 64 +++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 107 +++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 174 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..054830b47f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3170,12 +3170,15 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback) set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_unobserved_provoking_vertex COMMAND agc_driver_graphics_tests --unobserved-provoking-vertex) + set_tests_properties(agc_driver_unobserved_provoking_vertex PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order) set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + set_tests_properties(agc_driver_unobserved_provoking_vertex PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d7a28cebfb..4edd91fd5c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -124,6 +124,70 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } return; } + struct Decorations { + std::unordered_map<std::uint64_t, std::uint32_t> builtIns; + std::vector<std::uint64_t> perVertexValues; + std::vector<std::uint64_t> perPrimitiveValues; + }; + const auto readDecorations = [](const ShaderRecompiler::RecompileResult& program, Decorations& decorations) { + const auto words = program.spirv.Words(); + if (words.size() < 5) return false; + constexpr std::uint32_t NoMember = 0xffffffffu; + const auto key = [](std::uint32_t target, std::uint32_t member) { return (static_cast<std::uint64_t>(target) << 32u) | member; }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if (count == 0 || count > words.size() - cursor) return false; + const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu); + const std::size_t decoration = op == spv::OpDecorate ? 2 : op == spv::OpMemberDecorate ? 3 : 0; + if (decoration != 0 && count > decoration) { + const auto target = key(words[cursor + 1], op == spv::OpMemberDecorate ? words[cursor + 2] : NoMember); + if (words[cursor + decoration] == spv::DecorationFlat || words[cursor + decoration] == spv::DecorationPerVertexKHR) decorations.perVertexValues.push_back(target); + if (words[cursor + decoration] == spv::DecorationPerPrimitiveEXT) decorations.perPrimitiveValues.push_back(target); + if (words[cursor + decoration] == spv::DecorationBuiltIn && count > decoration + 1) decorations.builtIns[target] = words[cursor + decoration + 1]; + } + cursor += count; + } + return true; + }; + const auto readsProvokingVertex = [&](const ShaderRecompiler::RecompileResult& program) { + if (std::any_of(program.fragmentParameters.begin(), program.fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { + return parameter.flat || parameter.perVertex || parameter.custom || parameter.flatP10; + })) return true; + Decorations decorations; + if (!readDecorations(program, decorations)) return true; + const auto& builtIns = decorations.builtIns; + return std::any_of(decorations.perVertexValues.begin(), decorations.perVertexValues.end(), [&](std::uint64_t target) { + const auto builtIn = builtIns.find(target); + if (builtIn == builtIns.end()) return true; + switch (builtIn->second) { + case spv::BuiltInFragCoord: + case spv::BuiltInPointCoord: + case spv::BuiltInFrontFacing: + case spv::BuiltInSampleId: + case spv::BuiltInSamplePosition: + case spv::BuiltInSampleMask: + case spv::BuiltInHelperInvocation: + case spv::BuiltInSubgroupSize: + case spv::BuiltInSubgroupLocalInvocationId: + return false; + default: + return true; + } + }); + }; + const auto writesProvokingVertexBuiltIn = [&](const ShaderRecompiler::RecompileResult& program) { + Decorations decorations; + if (!readDecorations(program, decorations)) return true; + return std::any_of(decorations.builtIns.begin(), decorations.builtIns.end(), [&](const auto& builtIn) { + return (builtIn.second == spv::BuiltInLayer || builtIn.second == spv::BuiltInViewportIndex) && + std::find(decorations.perPrimitiveValues.begin(), decorations.perPrimitiveValues.end(), builtIn.first) == decorations.perPrimitiveValues.end(); + }); + }; + const bool observed = std::any_of(shaders.begin(), shaders.end(), [&](const CompiledShader& shader) { + if (shader.stage == ShaderRecompiler::ShaderStage::Fragment) return shader.program == nullptr || readsProvokingVertex(*shader.program); + return shader.program != nullptr && writesProvokingVertexBuiltIn(*shader.program); + }); + if (!context.provokingVertexLast && !observed) return; Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7b4e5bda3d..144d5a5d79 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9354,7 +9354,7 @@ void ProvokingVertexTests() { ValidateProvokingVertex(unsupported, first); auto last = first; last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; - expectFailure([&] { ValidateProvokingVertex(unsupported, last); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + ValidateProvokingVertex(unsupported, last); auto supported = unsupported; supported.provokingVertexLast = true; ValidateProvokingVertex(supported, last); @@ -9477,6 +9477,105 @@ void ProvokingVertexTests() { expectFailure([&] { ValidateProvokingVertex(supported, last); }, "invalid provoking vertex mode"); } +void UnobservedProvokingVertexTests() { + using namespace AgcDriver::Graphics; + auto last = DecodeState(makeState()); + last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + const Context unsupported{}; + const auto module = [](std::initializer_list<std::uint32_t> instructions) { + std::vector<std::uint32_t> words{spv::MagicNumber, 0x10300u, 0u, 4u, 0u}; + words.insert(words.end(), instructions); + return words; + }; + ShaderRecompiler::RecompileResult fragment{}; + const std::array<CompiledShader, 1> shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const ShaderRecompiler::FragmentParameter smooth{0, 0, false, false}; + const auto refused = [&](const State& state) { + expectFailure([&] { ValidateProvokingVertex(unsupported, state, shaders); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + }; + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { + auto state = last; + state.topology = topology; + ValidateProvokingVertex(unsupported, state); + fragment.fragmentParameters.clear(); + fragment.spirv = module({}); + ValidateProvokingVertex(unsupported, state, shaders); + fragment.fragmentParameters = {smooth}; + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationLocation, 0u}); + ValidateProvokingVertex(unsupported, state, shaders); + for (std::uint32_t read = 0; read < 4; ++read) { + const ShaderRecompiler::FragmentParameter observed{1, 1, read == 0, read == 1, read == 2, read == 3, false}; + fragment.fragmentParameters = {smooth, observed}; + refused(state); + } + fragment.fragmentParameters = {smooth}; + for (const std::uint32_t decoration : {static_cast<std::uint32_t>(spv::DecorationFlat), static_cast<std::uint32_t>(spv::DecorationPerVertexKHR)}) { + fragment.spirv = module({(3u << 16u) | spv::OpDecorate, 1u, decoration}); + refused(state); + fragment.spirv = module({(4u << 16u) | spv::OpMemberDecorate, 1u, 0u, decoration}); + refused(state); + } + fragment.spirv = module({(5u << 16u) | spv::OpDecorate, 1u, spv::DecorationLocation}); + refused(state); + fragment.spirv = std::vector<std::uint32_t>{}; + refused(state); + } + fragment.fragmentParameters = {smooth}; + for (const std::uint32_t generated : {static_cast<std::uint32_t>(spv::BuiltInSampleId), static_cast<std::uint32_t>(spv::BuiltInFrontFacing), static_cast<std::uint32_t>(spv::BuiltInSubgroupLocalInvocationId)}) { + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, generated, (3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat}); + ValidateProvokingVertex(unsupported, last, shaders); + fragment.spirv = module({(3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat, (4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, generated}); + ValidateProvokingVertex(unsupported, last, shaders); + fragment.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, generated, (4u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationFlat}); + ValidateProvokingVertex(unsupported, last, shaders); + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, generated, (3u << 16u) | spv::OpDecorate, 2u, spv::DecorationFlat}); + refused(last); + fragment.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, generated, (4u << 16u) | spv::OpMemberDecorate, 1u, 1u, spv::DecorationFlat}); + refused(last); + } + for (const std::uint32_t passed : {static_cast<std::uint32_t>(spv::BuiltInLayer), static_cast<std::uint32_t>(spv::BuiltInViewportIndex), static_cast<std::uint32_t>(spv::BuiltInPrimitiveId)}) { + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, passed, (3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat}); + refused(last); + fragment.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, passed, (4u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationFlat}); + refused(last); + } + ShaderRecompiler::RecompileResult vertex{}; + vertex.fragmentParameters = {{0, 0, true, true, true, true, true}}; + vertex.spirv = module({(3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat}); + fragment.fragmentParameters = {smooth}; + fragment.spirv = module({}); + const std::array<CompiledShader, 2> stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + ValidateProvokingVertex(unsupported, last, stages); + Context supported{}; + supported.provokingVertexLast = true; + for (const auto stage : {ShaderRecompiler::ShaderStage::Vertex, ShaderRecompiler::ShaderStage::TessellationEvaluation, ShaderRecompiler::ShaderStage::Geometry, ShaderRecompiler::ShaderStage::Mesh}) { + ShaderRecompiler::RecompileResult writer{}; + const std::array<CompiledShader, 2> written{{{stage, &writer, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const auto refusedWrite = [&] { + expectFailure([&] { ValidateProvokingVertex(unsupported, last, written); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + ValidateProvokingVertex(supported, last, written); + }; + for (const std::uint32_t selected : {static_cast<std::uint32_t>(spv::BuiltInLayer), static_cast<std::uint32_t>(spv::BuiltInViewportIndex)}) { + writer.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, selected}); + refusedWrite(); + writer.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, selected}); + refusedWrite(); + writer.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, selected, (4u << 16u) | spv::OpMemberDecorate, 1u, 1u, spv::DecorationPerPrimitiveEXT}); + refusedWrite(); + writer.spirv = module({(4u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationPerPrimitiveEXT, (5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, selected}); + ValidateProvokingVertex(unsupported, last, written); + writer.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, selected, (3u << 16u) | spv::OpDecorate, 1u, spv::DecorationPerPrimitiveEXT}); + ValidateProvokingVertex(unsupported, last, written); + } + writer.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, spv::BuiltInPosition, (5u << 16u) | spv::OpMemberDecorate, 2u, 0u, spv::DecorationBuiltIn, spv::BuiltInPointSize}); + ValidateProvokingVertex(unsupported, last, written); + writer.spirv = std::vector<std::uint32_t>{}; + refusedWrite(); + } + const std::array<CompiledShader, 1> missing{{{ShaderRecompiler::ShaderStage::Fragment, nullptr, 0}}}; + expectFailure([&] { ValidateProvokingVertex(unsupported, last, missing); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); +} + void ProvokingVertexRenderPassTests() { using namespace AgcDriver::Graphics; const auto first = DecodeState(makeState()); @@ -11397,6 +11496,12 @@ int main(int argc, char** argv) { std::cerr << "descriptor pool reset failure did not terminate\n"; return 1; } + if (argc == 2 && std::string_view(argv[1]) == "--unobserved-provoking-vertex") { + UnobservedProvokingVertexTests(); + ProvokingVertexTests(); + std::cout << "Unobserved provoking vertex checks passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") { multisampleTests(); std::cout << "Covered-centroid and multisample state checks passed\n"; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 475aa11fb0..a9e5829f8c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -142,7 +142,7 @@ - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the standard (COMP_SWAP 0) and reversed (COMP_SWAP 2) component orders are accepted, putting red and blue in the 10-bit low field. That follows from the format's layout and from the standard swap Mesa pairs with COLOR_10_11_11 for R11G11B10_FLOAT (ac_formats.c); it was not measured on the console. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Without `VK_EXT_provoking_vertex`, last mode throws only when a pixel shader input could show which vertex provokes (a flat input, a P10 flat read, a per-vertex or custom per-vertex input, or a `Flat` or `PerVertexKHR` decoration in its SPIR-V, ignoring built-ins the pixel stage generates) or another stage's SPIR-V decorates a `Layer` or `ViewportIndex` built-in that is not `PerPrimitiveEXT`; otherwise the draw uses first mode, since the provoking vertex then selects nothing it reads or writes. With the extension, last mode throws for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). - [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format.<format>.states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). From fd459d96f795c8cefd90a035bf9db483fffb8ea6 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Fri, 23 Oct 2026 08:33:11 -0300 Subject: [PATCH 1254/1360] fix(libSceJobManager): honor worker spin and persistence policies --- core/libs/prx/libSceJobManager/CMakeLists.txt | 13 +- core/libs/prx/libSceJobManager/Export.cpp | 44 ++++- core/libs/tests/GuestJobManager.cpp | 150 +++++++++++++++++- core/libs/tests/JobManagerReference.cpp | 79 +++++++++ docs/dev/ENVIRONMENT.md | 2 + docs/dev/debt/libSceJobManager.md | 6 +- tools/test_job_manager_reference.py | 135 ++++++++++++++++ 7 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/JobManagerReference.cpp create mode 100644 tools/test_job_manager_reference.py diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index 8539bbd0c2..fd14b28c4c 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -8,7 +8,18 @@ if(BUILD_TESTING) target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc) configure_windows_unwind(guest_job_manager_tests) - foreach(testCase IN ITEMS memory validation dispatch ordering nested limits priority shutdown) + if(WIN32) + add_library(job_manager_reference SHARED EXCLUDE_FROM_ALL + ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp + ${LIBS_INCLUDE_DIR}/tests/JobManagerReference.cpp) + target_include_directories(job_manager_reference PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(job_manager_reference PRIVATE ANYPS5_JOB_MANAGER_REFERENCE) + target_link_libraries(job_manager_reference PRIVATE libkernel libc) + set_target_properties(job_manager_reference PROPERTIES PREFIX "" SUFFIX .prx + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) + configure_windows_unwind(job_manager_reference) + endif() + foreach(testCase IN ITEMS memory validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_count) add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase}) set_tests_properties(guest_job_manager_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 155a95220d..18a578a6c7 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -6,6 +6,7 @@ #include <cstdint> #include <cstring> #include <deque> +#include <immintrin.h> #include <mutex> #include <stdexcept> #include <string> @@ -23,6 +24,8 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSiz int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** result); +std::uint64_t APS5_VABI sceKernelReadTsc(); +std::uint64_t APS5_VABI sceKernelGetTscFrequency(); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); @@ -226,6 +229,11 @@ struct Manager { std::uint32_t freeNodes = 0; std::uint32_t freeSequences = 0; std::uint32_t persistentThreads = 0; + std::uint32_t workerCount = 0; + std::uint32_t sleepingWorkers = 0; + std::uint32_t wakeTokens = 0; + std::uint64_t outstandingInvocations = 0; + std::uint64_t spinTicks = 0; std::array<std::deque<Item*>, 2> runQueues; std::vector<Sequence> sequences; std::deque<Sequence> computeSequences; @@ -238,6 +246,14 @@ struct Manager { std::vector<Pthread> workers; }; +thread_local Manager* currentWorker = nullptr; + +void WakeWorkers(Manager& manager, std::uint32_t invocations) { + const std::uint32_t available = invocations - (currentWorker == &manager ? 1u : 0u); + manager.wakeTokens += std::min(available, manager.sleepingWorkers - manager.wakeTokens); + if (manager.wakeTokens != 0) manager.work.notify_all(); +} + Manager*& Implementation(void* self) { return *static_cast<Manager**>(self); } @@ -285,7 +301,7 @@ void Sequence::Advance() { ++outstanding; ++cursor; manager->runQueues[priority].push_back(&item); - manager->work.notify_all(); + WakeWorkers(*manager, item.count); continue; } if (item.type == ItemType::Compute || item.type == ItemType::ComputeIndirect) { @@ -320,6 +336,7 @@ void Sequence::Advance() { } void Sequence::DispatchCompleted(std::uint32_t invocations) { + manager->outstandingInvocations -= invocations; completed.invocations += invocations; ++completed.dispatches; --outstanding; @@ -338,6 +355,7 @@ int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, cons if (before.invocations + count > limits.invocations || before.dispatches >= limits.dispatches) return Latch(SequenceLimitExceeded); if (!TakeNode(*manager)) return Latch(SequenceNoNodes); items.push_back({ItemType::Dispatch, this, entry, argument, count, 0, 0, nullptr}); + manager->outstandingInvocations += count; Advance(); return 0; } @@ -551,11 +569,27 @@ bool RunOne(Manager& manager, std::unique_lock<std::mutex>& guard) { void* APS5_VABI WorkerEntry(void* argument) { Manager& manager = *static_cast<Manager*>(argument); + currentWorker = &manager; + std::uint64_t lastWork = sceKernelReadTsc(); std::unique_lock guard(manager.lock); while (manager.running) { - if (RunOne(manager, guard)) continue; - manager.work.wait(guard, [&manager] { return !manager.running || !manager.runQueues[0].empty() || !manager.runQueues[1].empty(); }); + if (RunOne(manager, guard)) { + lastWork = sceKernelReadTsc(); + continue; + } + const bool spinning = manager.outstandingInvocations != 0 && sceKernelReadTsc() - lastWork < manager.spinTicks; + if (spinning || manager.sleepingWorkers >= manager.workerCount - manager.persistentThreads) { + guard.unlock(); + _mm_pause(); + guard.lock(); + continue; + } + ++manager.sleepingWorkers; + manager.work.wait(guard, [&manager] { return !manager.running || manager.wakeTokens != 0; }); + if (manager.wakeTokens != 0) --manager.wakeTokens; + --manager.sleepingWorkers; } + currentWorker = nullptr; return nullptr; } @@ -658,6 +692,8 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument; auto* manager = new Manager; manager->running = true; + manager->workerCount = workers; + manager->spinTicks = sceKernelGetTscFrequency() / 1000000 * settings.spinMicroseconds; manager->freeNodes = nodes - 1; manager->freeSequences = sequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << sequences) - 1; manager->sequences = std::vector<Sequence>(sequences); @@ -781,7 +817,7 @@ ISequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryIn int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void* self, std::uint32_t count) { Manager* manager = Implementation(self); if (manager == nullptr) return ManagerNotInitialized; - if (count > manager->workers.size()) return ManagerInvalidArgument; + if (count > manager->workerCount) return ManagerInvalidArgument; const std::lock_guard guard(manager->lock); manager->persistentThreads = count; return 0; diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index 7258ae6694..9616edabf4 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include <array> #include <atomic> #include <chrono> #include <cstdint> @@ -54,6 +55,9 @@ struct SequenceFactory { }; extern "C" { +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI pthread_getcpuclockid_nid_postfix(Pthread thread, int* clockId); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* time); void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*); void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*); std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*); @@ -348,6 +352,15 @@ static void LimitsCase() { Require(factory->Create(&sequence, 4, 2, 1, 1, "waits") == 0); Require(sequence->Wait() == 0 && sequence->Wait() == 0); Require(factory->Destroy(sequence) == 0); + Require(Shutdown(manager.storage) == 0); +} + +static void NodePoolCase() { + Block block(SizeFor(2, 2, 2, 1, 1)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, nullptr) == 0); + SequenceFactory* factory = Factories(manager.storage, 0); + Sequence* sequence = nullptr; Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0); int result = 0; int accepted = 0; @@ -453,16 +466,149 @@ static void ShutdownCase() { } } -int main(int argc, char** argv) { - const std::string name = argc > 1 ? argv[1] : ""; +static void APS5_VABI CaptureWorkerClock(void* argument, std::uint32_t) { + Require(pthread_getcpuclockid_nid_postfix(scePthreadSelf(), static_cast<int*>(argument)) == 0); +} + +static std::int64_t CpuNanos(int clock) { + KernelTimespec time{}; + Require(sceKernelClockGettime(clock, &time) == 0); + return time.tv_sec * 1000000000LL + time.tv_nsec; +} + +static void RequireParked(int clock) { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + const auto start = CpuNanos(clock); + std::this_thread::sleep_for(std::chrono::milliseconds(80)); + Require(CpuNanos(clock) - start < 20000000); +} + +static void WorkerPolicyCase() { + for (const std::uint32_t mode : {0x10000u, 0x20000u}) { + for (const std::uint32_t spin : {0u, 50u}) { + Block block(SizeFor(1, 1, 8, 2, 2)); + ManagerObject manager; + const ConfigParams config{0, mode | 0x1fff, 0x10000, 0x2bc, "WorkerPolicy", spin, 0, 0, 0}; + Require(Initialize(manager.storage, block.memory, block.bytes, 1, 1, &config) == 0); + auto* factory = Factories(manager.storage, 0); + Sequence* sequence = nullptr; + Require(factory->Create(&sequence, 8, 8, 2, 2, "worker policy") == 0); + int clock = 0; + Require(sequence->Dispatch(CaptureWorkerClock, &clock, 1, nullptr) == 0 && sequence->Wait() == 0); + RequireParked(clock); + for (int round = 0; round < 2; ++round) { + Require(SetPersistentThreadCount(manager.storage, 1) == 0); + RequireParked(clock); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0 && sequence->Wait() == 0); + const auto start = CpuNanos(clock); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (CpuNanos(clock) - start < 30000000 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(CpuNanos(clock) - start >= 30000000); + Require(SetPersistentThreadCount(manager.storage, 0) == 0); + RequireParked(clock); + } + Require(SetPersistentThreadCount(manager.storage, 1) == 0); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0 && sequence->Wait() == 0); + Require(Shutdown(manager.storage) == 0); + } + } +} + +static void WorkerWakeCase() { + Block block(SizeFor(4, 1, 8, 2, 2)); + ManagerObject manager; + for (const std::uint32_t mode : {0x10000u, 0x20000u}) { + const ConfigParams config{0, mode | 0x1fff, 0x10000, 0x2bc, "WorkerWake", 50, 0, 0, 0}; + Require(Initialize(manager.storage, block.memory, block.bytes, 4, 1, &config) == 0); + Sequence* sequence = nullptr; + auto* factory = Factories(manager.storage, 0); + Require(factory->Create(&sequence, 1024, 8, 2, 2, "worker wake") == 0); + for (std::uint32_t round = 0; round < 60; ++round) { + Require(SetPersistentThreadCount(manager.storage, round % 5) == 0); + Tally tally(257); + Require(sequence->Dispatch(CountJob, &tally, 257, nullptr) == 0); + Require(sequence->SyncPoint() == 0); + Require(sequence->Dispatch(CountJob, &tally, 257, nullptr) == 0); + Require(sequence->Wait() == 0 && tally.calls == 514); + for (const auto& count : tally.seen) Require(count == 2); + } + Require(factory->Destroy(sequence) == 0); + Require(Shutdown(manager.storage) == 0); + } +} + +struct WorkerClocks { + std::array<int, 4> clocks{}; + std::atomic<unsigned int> arrived{0}; + std::atomic<bool> release{false}; +}; + +static void APS5_VABI CaptureWorkerClocks(void* argument, std::uint32_t index) { + auto& workers = *static_cast<WorkerClocks*>(argument); + CaptureWorkerClock(&workers.clocks[index], 0); + ++workers.arrived; + while (!workers.release) std::this_thread::yield(); +} + +static void WorkerCountCase() { + Block block(SizeFor(4, 1, 8, 2, 2)); + ManagerObject manager; + const ConfigParams config{0, 0x1000f, 0x10000, 0x2bc, "WorkerCount", 0, 0, 0, 0}; + Require(Initialize(manager.storage, block.memory, block.bytes, 4, 1, &config) == 0); + Sequence* sequence = nullptr; + Require(Factories(manager.storage, 0)->Create(&sequence, 8, 8, 2, 2, "worker count") == 0); + WorkerClocks workers; + Require(sequence->Dispatch(CaptureWorkerClocks, &workers, 4, nullptr) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (workers.arrived != 4 && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(workers.arrived == 4); + workers.release = true; + Require(sequence->Wait() == 0); + for (const std::uint32_t persistent : {2u, 4u}) { + Require(SetPersistentThreadCount(manager.storage, 0) == 0); + for (const int clock : workers.clocks) RequireParked(clock); + Require(SetPersistentThreadCount(manager.storage, persistent) == 0); + for (std::uint32_t job = 0; job < persistent; ++job) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0 && sequence->Wait() == 0); + } + std::array<std::int64_t, 4> starts{}; + for (std::size_t index = 0; index < starts.size(); ++index) starts[index] = CpuNanos(workers.clocks[index]); + const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(2); + unsigned int active = 0; + do { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + active = 0; + for (std::size_t index = 0; index < starts.size(); ++index) { + if (CpuNanos(workers.clocks[index]) - starts[index] >= 30000000) ++active; + } + } while (active < persistent && std::chrono::steady_clock::now() < until); + Require(active == persistent); + } + Require(Shutdown(manager.storage) == 0); +} + +int RunJobManagerCase(const char* caseName) { + const std::string name = caseName; if (name == "memory") MemoryCase(); else if (name == "validation") ValidationCase(); else if (name == "dispatch") DispatchCase(); else if (name == "ordering") OrderingCase(); else if (name == "limits") LimitsCase(); + else if (name == "node_pool") NodePoolCase(); else if (name == "nested") NestedCase(); else if (name == "priority") PriorityCase(); else if (name == "shutdown") ShutdownCase(); + else if (name == "worker_policy") WorkerPolicyCase(); + else if (name == "worker_wake") WorkerWakeCase(); + else if (name == "worker_count") WorkerCountCase(); else return 2; return 0; } + +#ifndef ANYPS5_JOB_MANAGER_REFERENCE +int main(int argc, char** argv) { + return RunJobManagerCase(argc > 1 ? argv[1] : ""); +} +#endif diff --git a/core/libs/tests/JobManagerReference.cpp b/core/libs/tests/JobManagerReference.cpp new file mode 100644 index 0000000000..b808c60f2f --- /dev/null +++ b/core/libs/tests/JobManagerReference.cpp @@ -0,0 +1,79 @@ +#include "SceTypes.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <windows.h> + +struct MemorySizeQueryParams; +struct ConfigParams; +struct SequenceFactory; + +int RunJobManagerCase(const char* name); + +template <typename T> +static T Resolve(const char* nid) { + const bool host = std::getenv("ANYPS5_JOB_MANAGER_HOST") != nullptr; + const char* library = host ? "libSceJobManager.prx" : "libSceJobManager.prx.guest.prx"; + HMODULE module = GetModuleHandleA(library); + if (module == nullptr && host) module = LoadLibraryA("libs\\libSceJobManager.prx"); + const FARPROC symbol = module != nullptr ? GetProcAddress(module, nid) : nullptr; + if (symbol == nullptr) { + std::fprintf(stderr, "JobManager reference: missing %s in %s (Windows error %lu)\n", nid, library, GetLastError()); + std::exit(3); + } + return reinterpret_cast<T>(symbol); +} + +extern "C" { + +void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void* self) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManagerC1Ev)>("cwMzzdesLlY"); + call(self); +} + +void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void* self) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManagerD1Ev)>("4g1ee34TMH8"); + call(self); +} + +std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* query) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE)>("Nm2BhAwYNFk"); + return call(query); +} + +int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void* self, void* memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t mask, const ConfigParams* config) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE)>("GS9DSbzC+MY"); + return call(self, memory, size, workers, sequences, computeSequences, mask, config); +} + +int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(void* self, void* memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, const ConfigParams* config) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE)>("d205iUFMyyE"); + return call(self, memory, size, workers, sequences, config); +} + +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager8shutdownEv)>("chxnvsxxfBY"); + return call(self); +} + +SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE)>("W-yXgQmaozU"); + return call(self, priority); +} + +SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE)>("L5jTAtk3HKA"); + return call(self, priority); +} + +int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void* self, std::uint32_t count) { + static const auto call = Resolve<decltype(&_ZN3sce3Job10JobManager24setPersistentThreadCountEj)>("VTKezfYu-v4"); + return call(self, count); +} + +int APS5_VABI Run_nid_no_patch() { + const char* name = std::getenv("ANYPS5_JOB_MANAGER_CASE"); + return name != nullptr ? RunJobManagerCase(name) : 2; +} + +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index ac824ed4c4..4b03f49404 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -6,6 +6,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| +| `ANYPS5_JOB_MANAGER_CASE` | string | `core/libs/tests/JobManagerReference.cpp` | | selects the CPU fixture run by `tools/test_job_manager_reference.py` | +| `ANYPS5_JOB_MANAGER_HOST` | presence | `core/libs/tests/JobManagerReference.cpp` | | runs the reference adapter against the host fallback instead of the loaded guest module | | `ANYPS5_LOADEXEC_PARENT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | variable the Windows entry stub reads; its wide name is built in `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 123977e935..b3286ee6dc 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,4 +2,8 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. `shutdown` throws while a compute sequence has work in flight, a sequence's label total is written as 32 bits, and since the label is host memory no GPU import covers, the driver drains recorded work before each label write. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run +- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits every compute dispatch with its own label write and interrupt, while the original batches label writes and rewinds commands encoded ahead. Command buffers and labels live on the host heap; labels outside a GPU import force the driver to drain recorded work before each write. Completion labels are written as 32 bits; wrap behavior is unverified. The per-queue command-size formula uses fixed builder sizes of 104, 148 and 32 bytes per dispatch, sync point and wait. +- The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps its state on the host heap rather than in the caller's block, apart from the block header. Blocks smaller than 0x2d8 bytes throw. Worker parking uses a host condition variable and mutex rather than the original semaphore and atomic queues; console scheduling and spin timing have not been measured. +- [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. +- [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. +- The [fallback](../../../core/libs/prx/libSceJobManager/Export.cpp) and Windows [reference runner](../../../tools/test_job_manager_reference.py) disagree: the runner times out in the node-pool exhaustion fixture against the PPSA06888 module with the host kernel; the fallback returns `SequenceInvalid` and recycles its nodes. The cause remains unverified. Original-module execution on Linux and console timing equivalence are not covered by this runner. diff --git a/tools/test_job_manager_reference.py b/tools/test_job_manager_reference.py new file mode 100644 index 0000000000..d766bd51bc --- /dev/null +++ b/tools/test_job_manager_reference.py @@ -0,0 +1,135 @@ +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +from import_audit import read_host_imports +from unwrap_self import unwrap + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) +from guest_import_fixtures import executable + +CASES = ("memory", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_count") + + +def stage_libraries(directory, build, toolchain, dependencies): + providers = {path.name.lower(): path for path in (build / "core/libs/libs").glob("*.prx")} + providers.update({path.name.lower(): path for path in toolchain.glob("*.dll")}) + pending = list(dependencies) + staged = set() + while pending: + name = pending.pop().lower() + if name in staged: + continue + staged.add(name) + source = providers.get(name) + if source is None: + if name.endswith(".prx"): + raise RuntimeError(f"Missing patched runtime {name}; build core/libs/libs/{name}") + continue + shutil.copy2(source, directory / source.name) + pending.extend(read_host_imports(source)) + + +def run(args, directory): + libraries = directory / "libs" + libraries.mkdir() + helper = args.build / "tests/job_manager_reference.prx" + if not helper.is_file(): + raise RuntimeError("Build the job_manager_reference target first") + shutil.copy2(helper, libraries / "jobref.prx") + subprocess.run([str(args.build / "core/libs/nid_patcher.exe"), "jobref", "--preserve-export-list", + str(args.build / "core/libs/libc.exports"), str(libraries / "jobref.prx")], check=True, capture_output=True) + module_hash = None + if args.module: + module_data = args.module.read_bytes() + module_hash = hashlib.sha256(module_data).hexdigest() + modules = directory / "sce_module" + modules.mkdir() + target = modules / "libSceJobManager.prx" + if module_data[:4] == b"\x7fELF": + shutil.copy2(args.module, target) + else: + unwrap(args.module, target) + source = directory / "input.elf" + source.write_bytes(executable("jobref.prx", symbol="Run_nid_no_patch")) + output = directory / "reference.exe" + options = ["--skip-sce-module"] if args.host else [] + result = subprocess.run([str(args.relinker), "--windows", "--registry", *options, str(source), str(output)], + cwd=directory, capture_output=True, timeout=60) + (directory / "relink.log").write_bytes(result.stdout + result.stderr) + if result.returncode: + raise RuntimeError(f"Relinker failed ({result.returncode}); see {directory / 'relink.log'}") + dependencies = set(read_host_imports(helper)) | {"libc.prx"} + for registry in directory.glob("*.registry.json"): + dependencies.update(entry["library"] for entry in json.loads(registry.read_text()) + if entry.get("library") not in ("jobref.prx", "libSceJobManager.prx")) + if args.host: + dependencies.add("libSceJobManager.prx") + stage_libraries(libraries, args.build, args.toolchain, dependencies) + environment = os.environ.copy() + environment["PATH"] = str(libraries) + os.pathsep + environment.get("PATH", "") + environment.pop("ANYPS5_JOB_MANAGER_HOST", None) + if args.host: + environment["ANYPS5_JOB_MANAGER_HOST"] = "1" + results = [] + for case in args.cases: + environment["ANYPS5_JOB_MANAGER_CASE"] = case + try: + result = subprocess.run([str(output)], cwd=directory, env=environment, + capture_output=True, timeout=args.timeout) + log = result.stdout + result.stderr + status = "passed" if result.returncode == 0 else "failed" + code = result.returncode + except subprocess.TimeoutExpired as error: + log = (error.stdout or b"") + (error.stderr or b"") + status, code = "timeout", None + (directory / f"{case}.log").write_bytes(log) + results.append({"case": case, "status": status, "returncode": code}) + print(json.dumps(results[-1]), flush=True) + summary = {"provider": "host" if args.host else "guest", "module_sha256": module_hash, "cases": results} + (directory / "results.json").write_text(json.dumps(summary, indent=2) + "\n") + return 0 if all(result["status"] == "passed" for result in results) else 1 + + +def main(): + parser = argparse.ArgumentParser(description="Run the CPU JobManager fixtures through Windows relinker startup using an owned module or the host fallback.") + provider = parser.add_mutually_exclusive_group(required=True) + provider.add_argument("--module", type=Path, help="Owned decrypted SELF or ELF libSceJobManager.prx") + provider.add_argument("--host", action="store_true", help="Validate the runner against the built host fallback") + parser.add_argument("--build", type=Path, required=True) + parser.add_argument("--relinker", type=Path, required=True) + parser.add_argument("--toolchain", type=Path, required=True, help="Directory containing the matching compiler runtime DLLs") + parser.add_argument("--output", type=Path, help="New directory for staged modules, per-case logs and results.json; defaults to a temporary directory") + parser.add_argument("--cases", nargs="+", choices=CASES, default=list(CASES)) + parser.add_argument("--timeout", type=float, default=20) + args = parser.parse_args() + if sys.platform != "win32": + parser.error("The reference adapter currently supports Windows only") + if args.timeout <= 0 or args.timeout > 30: + parser.error("--timeout must be greater than zero and at most 30 seconds") + try: + for name in ("build", "relinker", "toolchain", "module"): + value = getattr(args, name) + if value is not None: + setattr(args, name, value.resolve(strict=True)) + directory = args.output.resolve() if args.output else Path(tempfile.mkdtemp(prefix="anyps5-jobmanager-")) + if args.output: + directory.mkdir(parents=True, exist_ok=False) + print(f"Artifacts: {directory}", flush=True) + return run(args, directory) + except (OSError, RuntimeError, subprocess.SubprocessError) as error: + print(str(error), file=sys.stderr) + if isinstance(error, subprocess.CalledProcessError) and error.stderr: + print(error.stderr.decode(errors="replace"), file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 14c582fb7555547b08f5a1bdb1a661cffcb162ed Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Fri, 23 Oct 2026 09:18:17 -0300 Subject: [PATCH 1255/1360] fix(tests): distinguish the manual JobManager reference runner --- docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libSceJobManager.md | 2 +- ...st_job_manager_reference.py => run_job_manager_reference.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tools/{test_job_manager_reference.py => run_job_manager_reference.py} (100%) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 4b03f49404..585edc1a90 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -6,7 +6,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `ANYPS5_JOB_MANAGER_CASE` | string | `core/libs/tests/JobManagerReference.cpp` | | selects the CPU fixture run by `tools/test_job_manager_reference.py` | +| `ANYPS5_JOB_MANAGER_CASE` | string | `core/libs/tests/JobManagerReference.cpp` | | selects the CPU fixture run by `tools/run_job_manager_reference.py` | | `ANYPS5_JOB_MANAGER_HOST` | presence | `core/libs/tests/JobManagerReference.cpp` | | runs the reference adapter against the host fallback instead of the loaded guest module | | `ANYPS5_LOADEXEC_PARENT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | variable the Windows entry stub reads; its wide name is built in `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index b3286ee6dc..ab9dfe3e1b 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -6,4 +6,4 @@ - The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps its state on the host heap rather than in the caller's block, apart from the block header. Blocks smaller than 0x2d8 bytes throw. Worker parking uses a host condition variable and mutex rather than the original semaphore and atomic queues; console scheduling and spin timing have not been measured. - [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. - [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. -- The [fallback](../../../core/libs/prx/libSceJobManager/Export.cpp) and Windows [reference runner](../../../tools/test_job_manager_reference.py) disagree: the runner times out in the node-pool exhaustion fixture against the PPSA06888 module with the host kernel; the fallback returns `SequenceInvalid` and recycles its nodes. The cause remains unverified. Original-module execution on Linux and console timing equivalence are not covered by this runner. +- The [fallback](../../../core/libs/prx/libSceJobManager/Export.cpp) and Windows [reference runner](../../../tools/run_job_manager_reference.py) disagree: the runner times out in the node-pool exhaustion fixture against the PPSA06888 module with the host kernel; the fallback returns `SequenceInvalid` and recycles its nodes. The cause remains unverified. Original-module execution on Linux and console timing equivalence are not covered by this runner. diff --git a/tools/test_job_manager_reference.py b/tools/run_job_manager_reference.py similarity index 100% rename from tools/test_job_manager_reference.py rename to tools/run_job_manager_reference.py From fc8f03594d088d50b68c2386a39b9756c9691f11 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Fri, 23 Oct 2026 10:27:59 -0300 Subject: [PATCH 1256/1360] fix(job-manager): spin only for pending work in host clock time --- core/libs/prx/libSceJobManager/CMakeLists.txt | 11 +- core/libs/prx/libSceJobManager/Export.cpp | 13 +- core/libs/tests/GuestJobManager.cpp | 111 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libSceJobManager.md | 6 +- tools/run_job_manager_reference.py | 2 +- 6 files changed, 132 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index fd14b28c4c..002ba69513 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -19,7 +19,7 @@ if(BUILD_TESTING) RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) configure_windows_unwind(job_manager_reference) endif() - foreach(testCase IN ITEMS memory validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_count) + foreach(testCase IN ITEMS memory validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_count worker_spin) add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase}) set_tests_properties(guest_job_manager_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) @@ -27,4 +27,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceJobManager>") endif() endforeach() + set_tests_properties(guest_job_manager_worker_spin PROPERTIES ENVIRONMENT "APS5_TIME_SCALE=1") + foreach(scale IN ITEMS 0.1 10) + add_test(NAME guest_job_manager_worker_spin_scale_${scale} COMMAND guest_job_manager_tests worker_spin) + set_tests_properties(guest_job_manager_worker_spin_scale_${scale} PROPERTIES TIMEOUT 20 ENVIRONMENT "APS5_TIME_SCALE=${scale}") + if(WIN32) + set_tests_properties(guest_job_manager_worker_spin_scale_${scale} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceJobManager>") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 18a578a6c7..4e3f1395b3 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -24,7 +24,6 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSiz int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** result); -std::uint64_t APS5_VABI sceKernelReadTsc(); std::uint64_t APS5_VABI sceKernelGetTscFrequency(); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); @@ -232,7 +231,7 @@ struct Manager { std::uint32_t workerCount = 0; std::uint32_t sleepingWorkers = 0; std::uint32_t wakeTokens = 0; - std::uint64_t outstandingInvocations = 0; + std::uint64_t pendingInvocations = 0; std::uint64_t spinTicks = 0; std::array<std::deque<Item*>, 2> runQueues; std::vector<Sequence> sequences; @@ -301,6 +300,7 @@ void Sequence::Advance() { ++outstanding; ++cursor; manager->runQueues[priority].push_back(&item); + manager->pendingInvocations -= item.count; WakeWorkers(*manager, item.count); continue; } @@ -336,7 +336,6 @@ void Sequence::Advance() { } void Sequence::DispatchCompleted(std::uint32_t invocations) { - manager->outstandingInvocations -= invocations; completed.invocations += invocations; ++completed.dispatches; --outstanding; @@ -355,7 +354,7 @@ int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, cons if (before.invocations + count > limits.invocations || before.dispatches >= limits.dispatches) return Latch(SequenceLimitExceeded); if (!TakeNode(*manager)) return Latch(SequenceNoNodes); items.push_back({ItemType::Dispatch, this, entry, argument, count, 0, 0, nullptr}); - manager->outstandingInvocations += count; + manager->pendingInvocations += count; Advance(); return 0; } @@ -570,14 +569,14 @@ bool RunOne(Manager& manager, std::unique_lock<std::mutex>& guard) { void* APS5_VABI WorkerEntry(void* argument) { Manager& manager = *static_cast<Manager*>(argument); currentWorker = &manager; - std::uint64_t lastWork = sceKernelReadTsc(); + std::uint64_t lastWork = __rdtsc(); std::unique_lock guard(manager.lock); while (manager.running) { if (RunOne(manager, guard)) { - lastWork = sceKernelReadTsc(); + lastWork = __rdtsc(); continue; } - const bool spinning = manager.outstandingInvocations != 0 && sceKernelReadTsc() - lastWork < manager.spinTicks; + const bool spinning = manager.pendingInvocations != 0 && __rdtsc() - lastWork < manager.spinTicks; if (spinning || manager.sleepingWorkers >= manager.workerCount - manager.persistentThreads) { guard.unlock(); _mm_pause(); diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index 9616edabf4..3d3644a538 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -10,6 +10,9 @@ #include <string> #include <thread> #include <vector> +#ifdef _WIN32 +#include <windows.h> +#endif struct MemorySizeQueryParams { std::uint32_t version; @@ -58,6 +61,7 @@ extern "C" { Pthread APS5_VABI scePthreadSelf(); int APS5_VABI pthread_getcpuclockid_nid_postfix(Pthread thread, int* clockId); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* time); +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*); void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*); std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*); @@ -539,6 +543,112 @@ static void WorkerWakeCase() { } } +struct HeldWorker { + std::atomic<bool> started{false}; + std::atomic<bool> release{false}; +}; + +struct SpinClock { + int guest = 0; + std::int64_t reference = 0; +#ifdef _WIN32 + HANDLE thread = nullptr; + ~SpinClock() { if (thread != nullptr) CloseHandle(thread); } +#endif +}; + +static std::int64_t SpinCpuUnits(const SpinClock& clock); + +static void APS5_VABI CaptureSpinClock(void* argument, std::uint32_t) { + auto& clock = *static_cast<SpinClock*>(argument); + CaptureWorkerClock(&clock.guest, 0); +#ifdef _WIN32 + clock.thread = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, GetCurrentThreadId()); + Require(clock.thread != nullptr); +#endif + const auto before = SpinCpuUnits(clock); + const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(5); + while (std::chrono::steady_clock::now() < until) {} + clock.reference = SpinCpuUnits(clock) - before; + Require(clock.reference > 0); +} + +static std::int64_t SpinCpuUnits(const SpinClock& clock) { +#ifdef _WIN32 + ULONG64 cycles = 0; + Require(QueryThreadCycleTime(clock.thread, &cycles)); + return static_cast<std::int64_t>(cycles); +#else + return CpuNanos(clock.guest); +#endif +} + +static void APS5_VABI HoldWorker(void* argument, std::uint32_t) { + auto& worker = *static_cast<HeldWorker*>(argument); + worker.started = true; + worker.started.notify_one(); + worker.release.wait(false); +} + +struct SpinSample { + std::int64_t consumed; + std::int64_t reference; +}; + +static SpinSample MeasureWorkerSpin(std::uint32_t spin, std::uint32_t rounds, bool pending) { + Block block(SizeFor(2, 2, 4, 2, 2)); + ManagerObject manager; + const ConfigParams config{0, 0x10003, 0x10000, 0x2bc, "WorkerSpin", spin, 0, 0, 0}; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, &config) == 0); + auto* factory = Factories(manager.storage, 0); + Sequence* held = nullptr; + Sequence* pulse = nullptr; + Require(factory->Create(&held, 4, 4, 2, 2, "held worker") == 0); + Require(factory->Create(&pulse, 4, 4, 2, 2, "idle worker") == 0); + HeldWorker worker; + Require(held->Dispatch(HoldWorker, &worker, 1, nullptr) == 0); + worker.started.wait(false); + if (pending) { + Require(held->SyncPoint() == 0); + Require(held->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + SpinClock clock; + Require(pulse->Dispatch(CaptureSpinClock, &clock, 1, nullptr) == 0 && pulse->Wait() == 0); + RequireParked(clock.guest); + const auto before = SpinCpuUnits(clock); + for (std::uint32_t round = 0; round < rounds; ++round) { + Require(pulse->Dispatch(Nothing, nullptr, 1, nullptr) == 0 && pulse->Wait() == 0); + Require(sceKernelUsleep_nid_postfix(1000) == 0); + } + const auto consumed = SpinCpuUnits(clock) - before; + RequireParked(clock.guest); + Require(!worker.release); + worker.release = true; + worker.release.notify_one(); + Require(held->Wait() == 0); + Require(factory->Destroy(held) == 0 && factory->Destroy(pulse) == 0); + Require(Shutdown(manager.storage) == 0); + return {consumed, clock.reference}; +} + +static void WorkerSpinCase() { + constexpr std::uint32_t rounds = 512; + const auto parked = MeasureWorkerSpin(0, rounds, true); + const auto spinning = MeasureWorkerSpin(50, rounds, true); + const auto noPending = MeasureWorkerSpin(50, rounds, false); +#ifdef _WIN32 + constexpr const char* units = "cycles"; +#else + constexpr const char* units = "ns"; +#endif + std::fprintf(stderr, "idle worker CPU over %u pulses (%s): spin=0 %lld, spin=50 %lld, no pending work %lld, 5ms active reference %lld\n", rounds, units, + static_cast<long long>(parked.consumed), static_cast<long long>(spinning.consumed), static_cast<long long>(noPending.consumed), static_cast<long long>(spinning.reference)); + const auto interval = spinning.reference * rounds / 100; + Require(spinning.consumed > parked.consumed + interval / 3); + Require(spinning.consumed < parked.consumed + interval * 5); + Require(noPending.consumed < parked.consumed + interval / 3); +} + struct WorkerClocks { std::array<int, 4> clocks{}; std::atomic<unsigned int> arrived{0}; @@ -602,6 +712,7 @@ int RunJobManagerCase(const char* caseName) { else if (name == "shutdown") ShutdownCase(); else if (name == "worker_policy") WorkerPolicyCase(); else if (name == "worker_wake") WorkerWakeCase(); + else if (name == "worker_spin") WorkerSpinCase(); else if (name == "worker_count") WorkerCountCase(); else return 2; return 0; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 585edc1a90..f9b8466115 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -569,7 +569,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | | `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | | `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | -| `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps still take real time | +| `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps and host JobManager worker spin intervals still take real time | | `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | | `APS5_TRACE_MEMORY` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | logs direct-memory map, remap, protect, unmap and physical allocation operations | | `APS5_TRACE_SYNC` | presence | `core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp` | | logs event-flag create, set and wait operations | diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index ab9dfe3e1b..0ce05f3280 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -3,7 +3,7 @@ ### Functional - [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits every compute dispatch with its own label write and interrupt, while the original batches label writes and rewinds commands encoded ahead. Command buffers and labels live on the host heap; labels outside a GPU import force the driver to drain recorded work before each write. Completion labels are written as 32 bits; wrap behavior is unverified. The per-queue command-size formula uses fixed builder sizes of 104, 148 and 32 bytes per dispatch, sync point and wait. -- The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps its state on the host heap rather than in the caller's block, apart from the block header. Blocks smaller than 0x2d8 bytes throw. Worker parking uses a host condition variable and mutex rather than the original semaphore and atomic queues; console scheduling and spin timing have not been measured. +- The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps its state on the host heap rather than in the caller's block, apart from the block header. Blocks smaller than 0x2d8 bytes throw. Worker parking uses a host condition variable and mutex rather than the original semaphore and atomic queues; console scheduling and spin timing have not been measured. The fallback uses raw host TSC for its spin interval, so APS5_TIME_SCALE does not change host CPU use; the original module reads the scaled guest TSC when run through the host kernel. - [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. -- [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. -- The [fallback](../../../core/libs/prx/libSceJobManager/Export.cpp) and Windows [reference runner](../../../tools/run_job_manager_reference.py) disagree: the runner times out in the node-pool exhaustion fixture against the PPSA06888 module with the host kernel; the fallback returns `SequenceInvalid` and recycles its nodes. The cause remains unverified. Original-module execution on Linux and console timing equivalence are not covered by this runner. +- [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of `_sceUlobjmgrRegisterObject`. The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. +- The [fallback](../../../core/libs/prx/libSceJobManager/Export.cpp) and Windows [reference runner](../../../tools/run_job_manager_reference.py) disagree: the runner times out in the node-pool exhaustion fixture against the PPSA06888 module with the host kernel; the fallback returns `SequenceInvalid` and recycles its nodes. The original Wait spins while allocating a node from the exhausted global pool and resumes when another sequence returns one; the fallback deliberately needs no Wait node. Original-module execution on Linux and console timing equivalence are not covered by this runner. diff --git a/tools/run_job_manager_reference.py b/tools/run_job_manager_reference.py index d766bd51bc..488916cba2 100644 --- a/tools/run_job_manager_reference.py +++ b/tools/run_job_manager_reference.py @@ -15,7 +15,7 @@ ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) from guest_import_fixtures import executable -CASES = ("memory", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_count") +CASES = ("memory", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_count", "worker_spin") def stage_libraries(directory, build, toolchain, dependencies): From 83caf5f9973a47aa94190b5a809c57c522776fec Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 10:46:40 -0300 Subject: [PATCH 1257/1360] docs(libkernel): say which sceKernelOpenEventFlag null-output answers were measured Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index dce2237b6e..d959dc8c8f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,7 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, where the console faulted; with a name it cannot open, both return `SCE_KERNEL_ERROR_ESRCH`. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name it also returns `SCE_KERNEL_ERROR_ESRCH`, which was not measured. - [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked From ebf606ed01057514a950b887f13233ff5107da5d Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Fri, 23 Oct 2026 09:07:47 -0300 Subject: [PATCH 1258/1360] fix(libSceJobManager): batch compute completion in caller storage --- .../tests/execution/JobManagerCompute.cpp | 121 +++++++++++--- core/libs/prx/libSceJobManager/CMakeLists.txt | 10 ++ .../prx/libSceJobManager/ComputeProgress.hpp | 31 ++++ core/libs/prx/libSceJobManager/Export.cpp | 149 +++++++++++++----- core/libs/tests/JobManagerComputeProgress.cpp | 27 ++++ docs/dev/debt/libSceJobManager.md | 5 +- 6 files changed, 287 insertions(+), 56 deletions(-) create mode 100644 core/libs/prx/libSceJobManager/ComputeProgress.hpp create mode 100644 core/libs/tests/JobManagerComputeProgress.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp index c149aa41a9..d557f4e4c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -1,5 +1,6 @@ #include "VulkanTestDevice.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/GuestArena.hpp" #include <array> #include <atomic> #include <chrono> @@ -11,6 +12,7 @@ #include <stdexcept> #include <string> #include <thread> +#include <vector> extern "C" void AgcDriverShutdown_nid_postfix(); @@ -60,6 +62,9 @@ struct SequenceFactory { }; extern "C" { +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); void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*); std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*); int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, const ConfigParams*); @@ -94,10 +99,10 @@ alignas(256) volatile std::uint32_t Output[4]{}; alignas(256) std::array<std::uint32_t, 4> OutputDescriptor{}; alignas(32) std::array<std::uint32_t, 8> IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0}; alignas(16) std::array<std::uint32_t, 8> Specials{}; -alignas(16) std::array<std::uint32_t, 8> SpecialsWithoutDispatch{}; +alignas(16) std::array<std::uint32_t, 8> SpecialsDefaultModifier{}; std::array<ShaderRegister, 9> Registers{}; alignas(16) std::array<unsigned char, 0x80> Shader{}; -alignas(16) std::array<unsigned char, 0x80> ShaderWithoutDispatch{}; +alignas(16) std::array<unsigned char, 0x80> ShaderDefaultModifier{}; std::uint32_t Low(const volatile void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer)); @@ -115,8 +120,8 @@ void BuildShader() { {0x242, Low(OutputDescriptor.data())}, {0x243, High(OutputDescriptor.data())}}}; Specials[4] = 1; const ShaderRegister* registers = Registers.data(); - for (auto* shader : {&Shader, &ShaderWithoutDispatch}) { - const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsWithoutDispatch.data(); + for (auto* shader : {&Shader, &ShaderDefaultModifier}) { + const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsDefaultModifier.data(); std::memcpy(shader->data() + 0x20, ®isters, sizeof(registers)); std::memcpy(shader->data() + 0x28, &specials, sizeof(specials)); (*shader)[0x5c] = static_cast<unsigned char>(Registers.size()); @@ -144,10 +149,10 @@ void APS5_VABI Nothing(void*, std::uint32_t) { void SizeCase() { Require(SizeFor(0, 0, false) == 0x10d0 && SizeFor(0, 0, true) == 0x14c0, "size without compute sequences"); - Require(SizeFor(1, 1, false) == 0x24fd8 && SizeFor(1, 1, true) == 0x253c8, "size with one compute sequence"); - Require(SizeFor(2, 3, false) == 0x48e20 && SizeFor(2, 3, true) == 0x49210, "size with two compute sequences"); - Require(SizeFor(3, 7, false) == 0x6cb68, "size with three compute sequences"); - Require(SizeFor(1, 3, true) == 0x491c8, "size with two queues for one compute sequence"); + Require(SizeFor(1, 1, false) == 0x29fd8 && SizeFor(1, 1, true) == 0x2a3c8, "size with one compute sequence"); + Require(SizeFor(2, 3, false) == 0x52de0 && SizeFor(2, 3, true) == 0x531d0, "size with two compute sequences"); + Require(SizeFor(3, 7, false) == 0x7bb28, "size with three compute sequences"); + Require(SizeFor(1, 3, true) == 0x53188, "size with two queues for one compute sequence"); Require(SizeFor(2, 1, true) == 0, "size with fewer queues than compute sequences"); } @@ -192,24 +197,30 @@ void RunCase() { Require(Output[0] == 0x22220002u, "the indirect compute dispatch had not run when the wait returned: " + std::to_string(Output[0])); Value[0] = 0x33330003u; - Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch of a shader without the dispatch bit failed"); - Require(sequence->Wait() == 0, "the wait after a shader without the dispatch bit failed"); - Require(Output[0] == 0x22220002u, "a shader without the dispatch bit ran"); + Require(sequence->DispatchCompute(ShaderDefaultModifier.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch with a default modifier failed"); + Require(sequence->Wait() == 0, "the wait after a default modifier dispatch failed"); + Require(Output[0] == 0x33330003u, "a default modifier dispatch did not run"); + Value[0] = 0x33330004u; + Require(sequence->DispatchComputeIndirect(ShaderDefaultModifier.data(), Table.data(), IndirectArguments.data(), nullptr) == 0, "the indirect dispatch with a default modifier failed"); + Require(sequence->Wait() == 0, "the wait after an indirect default modifier dispatch failed"); + Require(Output[0] == 0x33330004u, "an indirect default modifier dispatch did not run"); + + Value[0] = 0x33330005u; Require(sequence->Dispatch(BlockingJob, nullptr, 1, "blocking") == 0, "the blocking job was not dispatched"); Require(sequence->SyncPoint() == 0, "the sync point failed"); Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the compute dispatch behind a sync point failed"); std::this_thread::sleep_for(std::chrono::milliseconds(200)); - Require(Output[0] == 0x22220002u, "a compute dispatch ran before the CPU job ahead of its sync point finished"); + Require(Output[0] == 0x33330004u, "a compute dispatch ran before the CPU job ahead of its sync point finished"); Released = true; Require(sequence->SyncPoint() == 0, "the second sync point failed"); Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, "reader") == 0, "the reading job was not dispatched"); Require(sequence->Wait() == 0, "the wait after the ordered dispatches failed"); - Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run"); - Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); + Require(Output[0] == 0x33330005u, "the compute dispatch behind a sync point did not run"); + Require(SeenByJob == 0x33330005u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); Value[0] = 0x44440004u; - Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed"); + Require(sequence->DispatchCompute(ShaderDefaultModifier.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed"); Require(sequence->SyncPoint() == 0, "the sync point between compute dispatches failed"); Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "behind") == 0, "the compute dispatch behind a compute dispatch failed"); Require(sequence->Wait() == 0, "the wait after chained compute dispatches failed"); @@ -229,16 +240,83 @@ void RunCase() { ::operator delete(memory, std::align_val_t{256}); } +void StorageCase(bool directMemory) { + constexpr std::uint64_t mask = (1u << 1) | (1u << 5) | (1u << 8); + const MemorySizeQueryParams query{1, 2, 0, 0, 32, 8, 4, 2, mask}; + const auto size = RequiredSize(&query); + const auto allocationSize = (size + 0xffff) & ~std::uint64_t{0xffff}; + std::int64_t physical = 0; + void* memory = nullptr; + if (directMemory) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, allocationSize, 0x10000, 0, &physical) == 0, "direct work area allocation failed"); + Require(sceKernelMapDirectMemory(&memory, allocationSize, 0x33, 0, physical, 0x10000) == 0, "direct work area mapping failed"); + } else { + memory = ::operator new(allocationSize, std::align_val_t{256}); + } + { + const GuestArena::HostWrite destination(memory, allocationSize); + std::memset(memory, 0, allocationSize); + std::memset(static_cast<std::uint8_t*>(memory) + size, 0xa5, allocationSize - size); + } + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 2, 0, 2, mask, nullptr) == 0, "storage manager initialization failed"); + auto* factory = Factories(manager, 0); + std::array<Sequence*, 2> sequences{}; + for (auto& sequence : sequences) Require(factory->CreateCompute(&sequence, 0, 16, 4, 2, nullptr) == 0, "storage sequence creation failed"); + for (std::uint32_t round = 0; round < 3; ++round) { + for (auto* sequence : sequences) { + Value[0] = 0x56780000u + round; + for (int dispatch = 0; dispatch < 8; ++dispatch) + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "batch") == 0, "batched dispatch failed"); + Require(sequence->Wait() == 0, "batched wait failed"); + Require(Output[0] == Value[0], "batched work was not visible after wait"); + Require(sequence->Wait() == 0, "empty wait after a batch failed"); + } + const auto* words = static_cast<const std::uint32_t*>(memory); + const auto base = reinterpret_cast<std::uintptr_t>(memory); + std::vector<std::uintptr_t> labels; + for (std::size_t index = 0; index + 8 <= size / 4; ++index) { + if (words[index] != 0xc0064900u || (words[index + 2] & 0xe7000000u) != 0x22000000u) continue; + const auto address = words[index + 3] | (std::uint64_t{words[index + 4]} << 32); + Require(address >= base && address + 8 <= base + size && (address & 7) == 0, "a completion label is outside the caller's work area"); + Require(words[index + 5] == 8 * (round + 1) && words[index + 6] == 0, "a completion did not publish the batched 32-bit count"); + Require(*reinterpret_cast<const volatile std::uint64_t*>(address) == words[index + 5], "a published batch was not retired"); + labels.push_back(address); + } + Require(labels.size() == 2 && labels[0] != labels[1], "each queue must store one batch completion in the caller's work area"); + } + for (auto* sequence : sequences) Require(factory->DestroyCompute(sequence) == 0, "storage sequence destruction failed"); + for (auto& sequence : sequences) { + Require(factory->CreateCompute(&sequence, 0, 16, 4, 2, nullptr) == 0, "storage sequence reuse failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "dispatch after sequence reuse failed"); + Require(factory->DestroyCompute(sequence) == 0, "destroy did not wait for reused sequence work"); + } + Require(Shutdown(manager) == 0, "storage manager shutdown failed"); + const auto* bytes = static_cast<const std::uint8_t*>(memory); + for (auto offset = size; offset < allocationSize; ++offset) + Require(bytes[offset] == 0xa5, "the manager wrote beyond its supplied work area"); + if (directMemory) Require(sceKernelReleaseDirectMemory(physical, allocationSize) == 0, "direct work area release failed"); + else ::operator delete(memory, std::align_val_t{256}); } -int main() { +} + +int RunJobManagerCase(const char* name) { try { + Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0, "unknown compute fixture"); +#ifndef ANYPS5_JOB_MANAGER_REFERENCE auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; device.reset(); +#endif BuildShader(); - SizeCase(); - RunCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0) SizeCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "execution") == 0) RunCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "storage") == 0) { + StorageCase(false); + StorageCase(true); + } AgcDriverShutdown_nid_postfix(); std::cout << "job manager compute test passed\n"; return 0; @@ -252,3 +330,10 @@ int main() { return 1; } } + +#ifndef ANYPS5_JOB_MANAGER_REFERENCE +int main(int argc, char** argv) { + if (argc > 2) return 2; + return RunJobManagerCase(argc == 2 ? argv[1] : "compute"); +} +#endif diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index 8539bbd0c2..17c2c3d178 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -4,6 +4,16 @@ target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver) configure_windows_unwind(libSceJobManager) if(BUILD_TESTING) + add_test_executable(job_manager_compute_progress_tests ${LIBS_INCLUDE_DIR}/tests/JobManagerComputeProgress.cpp) + target_include_directories(job_manager_compute_progress_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(job_manager_compute_progress_tests PRIVATE libc) + configure_windows_unwind(job_manager_compute_progress_tests) + add_test(NAME guest_job_manager_compute_progress COMMAND job_manager_compute_progress_tests) + set_tests_properties(guest_job_manager_compute_progress PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_job_manager_compute_progress PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test_executable(guest_job_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp) target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc) diff --git a/core/libs/prx/libSceJobManager/ComputeProgress.hpp b/core/libs/prx/libSceJobManager/ComputeProgress.hpp new file mode 100644 index 0000000000..aadd732245 --- /dev/null +++ b/core/libs/prx/libSceJobManager/ComputeProgress.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBSCEJOBMANAGER_COMPUTEPROGRESS_HPP +#define CORE_LIBS_PRX_LIBSCEJOBMANAGER_COMPUTEPROGRESS_HPP + +#include <cstdint> +#include <stdexcept> + +namespace JobManager { + +class ComputeProgress { +public: + std::uint32_t Publish(std::uint32_t count) { + total += count; + return total; + } + + std::uint32_t Collect(std::uint64_t value) { + const auto current = static_cast<std::uint32_t>(value); + const std::uint32_t finished = current - seen; + if (finished > std::uint32_t{total - seen}) throw std::runtime_error("libSceJobManager: a compute label advanced beyond its published work"); + seen = current; + return finished; + } + +private: + std::uint32_t total = 0; + std::uint32_t seen = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 155a95220d..b591aed225 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -12,7 +12,9 @@ #include <vector> #include "SceTypes.hpp" +#include "ComputeProgress.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" extern "C" { int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); @@ -41,6 +43,9 @@ std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); std::uint32_t* APS5_VABI sceAgcCbDispatch(CommandBuffer* buf, std::uint32_t threadGroupX, std::uint32_t threadGroupY, std::uint32_t threadGroupZ, std::uint32_t modifier); std::uint32_t* APS5_VABI sceAgcAcbDispatchIndirect(CommandBuffer* buf, const volatile void* indirectArguments, std::uint32_t modifier); std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t action, std::uint16_t gcrControl, std::uint8_t dst, std::uint8_t cachePolicy, const volatile Label* address, std::uint8_t dataSelect, std::uint64_t data, std::uint16_t gdsOffset, std::uint16_t gdsSize, std::uint8_t interrupt, std::uint32_t interruptContextId); +std::uint32_t* APS5_VABI sceAgcAcbWaitRegMem(CommandBuffer* buf, std::uint8_t size, std::uint8_t compareFunction, std::uint8_t cachePolicy, const volatile void* address, std::uint64_t reference, std::uint64_t mask, std::uint32_t pollCycles); +std::uint32_t* APS5_VABI sceAgcAcbRewind(CommandBuffer* buf, std::uint32_t initialState); +std::uint32_t* APS5_VABI sceAgcAcbWriteData(CommandBuffer* buf, std::uint8_t dst, std::uint8_t cachePolicy, std::uint64_t address, const void* data, std::uint32_t numDwords, std::uint8_t increment, std::uint8_t writeConfirm); } namespace { @@ -70,7 +75,6 @@ constexpr std::uint64_t ThreadRecordBytes = 0x48; constexpr std::uint64_t SequenceBytes = 0x240; constexpr std::uint64_t NodeBytes = 72; constexpr std::uint64_t ComputeContextBytes = 0x168; -constexpr std::uint64_t ComputeCommandBytes = 0x3ff * 104 + 0xff * 148 + 0x3f * 32; constexpr std::uint32_t FirstComputeQueue = 0x20; constexpr std::uint32_t UserDataRegister = 0x240; constexpr int UserEventFilter = -11; @@ -115,6 +119,32 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { return (value + alignment - 1) & ~(alignment - 1); } +std::uint64_t ComputeCommandBytes() { + std::array<std::uint32_t, 256> words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const ShaderRegister shaderRegister{}; + const std::array<std::uint32_t, 2> userData{}; + bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380, nullptr, 0, 0x190) != nullptr; + encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, &shaderRegister, 1) != nullptr; + encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userData.data(), 2) != nullptr; + encoded = encoded && sceAgcAcbPushMarker(&buffer, "1234567890123456789012345678901", 0) != nullptr; + encoded = encoded && sceAgcCbDispatch(&buffer, 1, 1, 1, 0) != nullptr; + encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + const auto dispatchBytes = static_cast<std::uint64_t>(buffer.cursor_up - words.data()) * 4; + buffer.cursor_up = words.data(); + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0, 1, 3, nullptr, 1, 0, 0, 0, 0, 0) != nullptr; + encoded = encoded && sceAgcAcbWaitRegMem(&buffer, 0, 3, 3, nullptr, 0, ~std::uint64_t{0}, 0x190) != nullptr; + encoded = encoded && sceAgcAcbRewind(&buffer, 1) != nullptr; + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x200, 1, 3, nullptr, 1, 0, 0, 0, 0, 0) != nullptr; + encoded = encoded && sceAgcAcbWriteData(&buffer, 2, 3, 0, userData.data(), 1, 1, 0) != nullptr; + const auto syncBytes = static_cast<std::uint64_t>(buffer.cursor_up - words.data()) * 4; + buffer.cursor_up = words.data(); + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x200, 1, 3, nullptr, 1, 0, 0, 0, 2, 0) != nullptr; + const auto waitBytes = static_cast<std::uint64_t>(buffer.cursor_up - words.data()) * 4; + if (!encoded || dispatchBytes > 255 || syncBytes > 255 || waitBytes > 255) throw std::runtime_error("libSceJobManager: compute command size measurement failed"); + return MaxDispatches * dispatchBytes + MaxSyncPoints * syncBytes + MaxWaits * waitBytes; +} + struct Manager; class Sequence; @@ -177,6 +207,7 @@ public: void Advance(); void DispatchCompleted(std::uint32_t invocations); void CollectCompute(); + void SetComputeStorage(std::uint32_t* commands, std::size_t words, Label* completion); bool Busy() const { return outstanding != 0; } Manager* manager = nullptr; @@ -190,6 +221,7 @@ private: int WaitLocked(std::unique_lock<std::mutex>& guard); int AppendCompute(Item item); void SubmitCompute(const Item& item); + void FlushCompute(); Counters limits; Counters submitted; @@ -200,11 +232,13 @@ private: std::size_t cursor = 0; std::deque<Item> items; std::condition_variable passed; - Label label{}; - std::uint64_t labelSeen = 0; - std::uint64_t labelTotal = 0; + Label* label = nullptr; + JobManager::ComputeProgress progress; + std::uint32_t pendingCompute = 0; bool batchStarted = false; - std::deque<std::vector<std::uint32_t>> commands; + std::uint32_t* commandBegin = nullptr; + std::size_t commandWords = 0; + std::size_t commandUsed = 0; }; class Factory final : public ISequenceFactory { @@ -267,7 +301,7 @@ bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueu outstanding = 0; cursor = 0; items.clear(); - commands.clear(); + commandUsed = 0; batchStarted = false; if (!TakeNode(*owner)) return false; open = true; @@ -294,6 +328,7 @@ void Sequence::Advance() { SubmitCompute(item); continue; } + FlushCompute(); if (outstanding != 0) return; if (item.type == ItemType::SyncPoint) { ++completed.syncPoints; @@ -304,7 +339,7 @@ void Sequence::Advance() { const auto retired = static_cast<std::uint32_t>(cursor); items.erase(items.begin(), items.begin() + static_cast<std::ptrdiff_t>(retired + 1)); cursor = 0; - commands.clear(); + commandUsed = 0; batchStarted = false; if (error == 0) { submitted.invocations -= completed.invocations; @@ -353,31 +388,57 @@ void Sequence::SubmitCompute(const Item& item) { std::memcpy(&modifier, specials + 0x10, sizeof(modifier)); const auto userData = reinterpret_cast<std::uint64_t>(item.argument); const std::array<std::uint32_t, 2> userDataWords{static_cast<std::uint32_t>(userData), static_cast<std::uint32_t>(userData >> 32)}; - auto& words = commands.emplace_back(64 + 3 * registerCount + item.name.size() / 4); - CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; - bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; - encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; - encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; - if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; - if ((modifier & 1) != 0) { + const auto capacity = std::min(commandWords - commandUsed, 64 + 3 * registerCount + item.name.size() / 4); + auto* const words = commandBegin + commandUsed; + CommandBuffer buffer{words, words + capacity, words, words + capacity, nullptr, nullptr, 0}; + { + const GuestArena::HostWrite destination(words, capacity * 4); + bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; + encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; + encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; + if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; if (item.type == ItemType::Compute) { encoded = encoded && sceAgcCbDispatch(&buffer, item.groups[0], item.groups[1], item.groups[2], modifier) != nullptr; } else { encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.indirectArguments, modifier) != nullptr; } + if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); } - if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; - encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x620, 0, 0, &label, 1, ++labelTotal, 0, 0, 2, 0) != nullptr; - if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); batchStarted = true; - const Packet packet{words.data(), static_cast<std::uint32_t>(buffer.cursor_up - words.data()), 0, {}}; + ++pendingCompute; + const Packet packet{words, static_cast<std::uint32_t>(buffer.cursor_up - words), 0, {}}; + commandUsed += packet.dw_num; if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed"); } +void Sequence::SetComputeStorage(std::uint32_t* commands, std::size_t words, Label* completion) { + commandBegin = commands; + commandWords = words; + label = completion; + const GuestArena::HostWrite destination(label, sizeof(*label)); + label->value = 0; +} + +void Sequence::FlushCompute() { + if (pendingCompute == 0) return; + if (commandWords - commandUsed < 8) throw std::runtime_error("libSceJobManager: a compute completion did not fit its command buffer"); + auto* const words = commandBegin + commandUsed; + CommandBuffer buffer{words, words + 8, words, words + 8, nullptr, nullptr, 0}; + { + const GuestArena::HostWrite destination(words, 32); + if (sceAgcCbReleaseMem(&buffer, 0x2f, 0x200, 1, 3, label, 1, progress.Publish(pendingCompute), 0, 0, 2, 0) == nullptr) { + throw std::runtime_error("libSceJobManager: a compute completion could not be encoded"); + } + } + pendingCompute = 0; + const Packet packet{words, static_cast<std::uint32_t>(buffer.cursor_up - words), 0, {}}; + commandUsed += packet.dw_num; + if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed for a completion"); +} + void Sequence::CollectCompute() { - const std::uint64_t value = label.value; - const auto finished = static_cast<std::uint32_t>(value - labelSeen); - labelSeen = value; + const auto finished = progress.Collect(label->value); if (finished == 0) return; completed.dispatches += finished; outstanding -= finished; @@ -440,7 +501,7 @@ int Sequence::WaitAndClose(std::uint32_t index) { const auto remaining = static_cast<std::uint32_t>(std::count_if(items.begin(), items.end(), [](const Item& item) { return item.type != ItemType::Wait; })); ReturnNodes(*manager, remaining + 1); items.clear(); - commands.clear(); + commandUsed = 0; cursor = 0; outstanding = 0; open = false; @@ -559,18 +620,28 @@ void* APS5_VABI WorkerEntry(void* argument) { return nullptr; } -std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues, bool* fits) { +struct WorkLayout { + std::uint64_t commands = 0; + std::uint64_t commandBytes = 0; + std::uint64_t labels = 0; + std::uint32_t nodes = 0; +}; + +WorkLayout LayoutFor(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues) { + WorkLayout layout; const std::uint64_t end = memory + size; std::uint64_t address = AlignUp(memory + StateEnd, 8) + workers * ThreadRecordBytes; for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes; address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; if (computeQueues != 0) { - address = AlignUp(address + ComputeContextBytes, 0x100) + AlignUp(ComputeCommandBytes * computeQueues, 8); - address += AlignUp(4 * std::uint64_t{computeQueues}, 8) + 8 * std::uint64_t{computeQueues}; + layout.commandBytes = ComputeCommandBytes(); + layout.commands = AlignUp(address + ComputeContextBytes, 0x100); + layout.labels = AlignUp(layout.commands + layout.commandBytes * computeQueues + 4 * std::uint64_t{computeQueues}, 8); + address = layout.labels + 8 * std::uint64_t{computeQueues}; } address = AlignUp(address, 64); - *fits = address <= end; - return *fits ? static_cast<std::uint32_t>(((end - address) / NodeBytes) & 0xffff) : 0; + if (address <= end) layout.nodes = static_cast<std::uint32_t>(((end - address) / NodeBytes) & 0xffff); + return layout; } int ValidateWorkers(const ConfigParams& config, std::uint32_t workers) { @@ -605,7 +676,7 @@ void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t wo } } -void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) { +void StartCompute(Manager& manager, const WorkLayout& layout, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) { if (sceKernelCreateEqueue(&manager.computeEvents, "SceJobManagerComputeEventQueue") != 0) throw std::runtime_error("libSceJobManager: sceKernelCreateEqueue failed"); for (std::uint64_t mask = computeQueueMask; mask != 0; mask &= mask - 1) { const std::uint32_t queue = FirstComputeQueue + static_cast<std::uint32_t>(std::countr_zero(mask)); @@ -616,6 +687,8 @@ void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_ for (std::uint32_t index = 0; index < computeSequences; ++index) { manager.computeSequences[index].compute = true; manager.computeSequences[index].queue = manager.computeQueues[index]; + manager.computeSequences[index].SetComputeStorage(reinterpret_cast<std::uint32_t*>(layout.commands + index * layout.commandBytes), + static_cast<std::size_t>(layout.commandBytes / 4), reinterpret_cast<Label*>(layout.labels + index * sizeof(Label))); } manager.freeComputeSequences = computeSequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << computeSequences) - 1; const KernelSchedParam schedule{static_cast<int>(priority)}; @@ -642,15 +715,17 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke } if (size < StateOffset) return ManagerMemoryTooSmall; if (size < StateEnd) throw std::runtime_error("sce::Job::JobManager::initialize: a memory block smaller than 0x2d8 bytes is not supported"); - std::memset(static_cast<std::uint8_t*>(memory) + StateOffset, 0, StateEnd - StateOffset); - const BlockHeader header{0x4d4a, static_cast<std::uint16_t>(StateOffset), 0x05000001, static_cast<std::uint32_t>(size), 0}; - std::memcpy(memory, &header, sizeof(header)); + { + const GuestArena::HostWrite destination(memory, StateEnd); + std::memset(static_cast<std::uint8_t*>(memory) + StateOffset, 0, StateEnd - StateOffset); + const BlockHeader header{0x4d4a, static_cast<std::uint16_t>(StateOffset), 0x05000001, static_cast<std::uint32_t>(size), 0}; + std::memcpy(memory, &header, sizeof(header)); + } if (config != nullptr && config->version > 1) return ManagerInvalidArgument; const ConfigParams& settings = config != nullptr ? *config : DefaultConfig; const auto computeQueues = computeSequences != 0 ? static_cast<std::uint32_t>(std::popcount(computeQueueMask)) : 0; - bool fits = false; - const std::uint32_t nodes = NodeCount(base, size, workers, sequences, computeQueues, &fits); - if (!fits || nodes == 0) return ManagerMemoryTooSmall; + const auto layout = LayoutFor(base, size, workers, sequences, computeQueues); + if (layout.nodes == 0) return ManagerMemoryTooSmall; if (const int invalid = ValidateWorkers(settings, workers); invalid != 0) return invalid; const bool ownHandlerSettings = (settings.version & 1) != 0; const std::uint32_t handlerPriority = ownHandlerSettings ? settings.computePriority : 0x100; @@ -658,7 +733,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument; auto* manager = new Manager; manager->running = true; - manager->freeNodes = nodes - 1; + manager->freeNodes = layout.nodes - 1; manager->freeSequences = sequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << sequences) - 1; manager->sequences = std::vector<Sequence>(sequences); for (std::uint32_t priority = 0; priority < 2; ++priority) { @@ -667,7 +742,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke manager->unifiedFactories[priority].manager = manager; manager->unifiedFactories[priority].priority = priority; } - if (computeSequences != 0) StartCompute(*manager, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); + if (computeSequences != 0) StartCompute(*manager, layout, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); Implementation(self) = manager; StartWorkers(*manager, settings, workers); return 0; @@ -724,7 +799,7 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN if (computeSequences != 0) { const auto queues = static_cast<std::uint64_t>(std::popcount(params->computeQueueMask)); bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff}; - bytes = (bytes + ComputeCommandBytes * queues + 3) & ~std::uint64_t{3}; + bytes = (bytes + ComputeCommandBytes() * queues + 3) & ~std::uint64_t{3}; bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7}; bytes += 8 * queues; } else { diff --git a/core/libs/tests/JobManagerComputeProgress.cpp b/core/libs/tests/JobManagerComputeProgress.cpp new file mode 100644 index 0000000000..fb0d90da1c --- /dev/null +++ b/core/libs/tests/JobManagerComputeProgress.cpp @@ -0,0 +1,27 @@ +#include "prx/libSceJobManager/ComputeProgress.hpp" +#include <cstdint> +#include <iostream> +#include <stdexcept> + +int main() { + try { + JobManager::ComputeProgress progress; + if (progress.Publish(0xfffffffd) != 0xfffffffd || progress.Collect(0xfffffffc) != 0xfffffffc) + throw std::runtime_error("partial retirement before wrap failed"); + if (progress.Publish(5) != 2 || progress.Collect(2) != 6 || progress.Collect(2) != 0) + throw std::runtime_error("batched completion across wrap failed"); + if (progress.Publish(9) != 11 || progress.Collect(5) != 3 || progress.Collect(11) != 6) + throw std::runtime_error("partial retirement after wrap failed"); + bool rejected = false; + try { + progress.Collect(12); + } catch (const std::runtime_error&) { + rejected = true; + } + if (!rejected || progress.Collect(11) != 0) throw std::runtime_error("an invalid completion changed retirement state"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 123977e935..184e91e23b 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,4 +2,7 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. `shutdown` throws while a compute sequence has work in flight, a sequence's label total is written as 32 bits, and since the label is host memory no GPU import covers, the driver drains recorded work before each label write. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run +- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait; exhausting the bounded command area throws. Native recovery from command exhaustion and console timing equivalence remain unverified. +- The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps scheduling state and nodes on the host heap; only the header, compute commands and completion labels occupy the caller's block. Blocks smaller than 0x2d8 bytes throw. The worker spin duration and persistent thread count are validated but do not govern scheduling. +- [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. +- [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. From 0891ca275e37a23f4de9971eb6e6d7617f25d623 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Fri, 23 Oct 2026 10:51:24 -0300 Subject: [PATCH 1259/1360] test(job-manager): verify native compute command capacity --- .../tests/execution/JobManagerCompute.cpp | 47 ++++++++++++++++++- core/libs/prx/libSceJobManager/Export.cpp | 6 +-- docs/dev/debt/libSceJobManager.md | 2 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp index d557f4e4c5..8d5f9f14c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -1,6 +1,7 @@ #include "VulkanTestDevice.hpp" #include "SceTypes.hpp" #include "prx/libc/include/GuestArena.hpp" +#include <algorithm> #include <array> #include <atomic> #include <chrono> @@ -240,6 +241,49 @@ void RunCase() { ::operator delete(memory, std::align_val_t{256}); } +void CapacityCase() { + std::array<ShaderRegister, 255> registers; + registers.fill(Registers[2]); + std::copy(Registers.begin(), Registers.end(), registers.begin()); + auto shader = Shader; + const ShaderRegister* data = registers.data(); + std::memcpy(shader.data() + 0x20, &data, sizeof(data)); + shader[0x5c] = static_cast<unsigned char>(registers.size()); + const std::string marker(65535, 'm'); + const MemorySizeQueryParams query{1, 1, 0, 0, 2, 0, 1, 1, 1}; + const auto size = RequiredSize(&query); + const auto allocationSize = (size + 255) & ~std::uint64_t{255}; + auto* memory = static_cast<std::uint8_t*>(::operator new(allocationSize, std::align_val_t{256})); + std::memset(memory, 0, allocationSize); + std::memset(memory + size, 0xa5, allocationSize - size); + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 1, 0, 1, 1, nullptr) == 0, "capacity manager initialization failed"); + auto* factory = Factories(manager, 0); + Sequence* sequence = nullptr; + Require(factory->CreateCompute(&sequence, 0, 2, 0, 1, nullptr) == 0, "capacity sequence creation failed"); + for (std::uint32_t round = 0; round < 2; ++round) { + Value[0] = 0xabcd0000u + round; + for (int dispatch = 0; dispatch < 2; ++dispatch) + Require(sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, marker.c_str()) == 0, "maximum register and marker dispatch failed"); + Require(sequence->Wait() == 0, "maximum packet wait failed"); + Require(Output[0] == Value[0], "maximum packet dispatch did not finish"); + const auto* words = reinterpret_cast<const std::uint32_t*>(memory); + std::uint32_t markers = 0; + for (std::size_t offset = 0; offset + 0x4001 <= size / 4; ++offset) { + if (words[offset] != 0xffff102cu) continue; + Require(std::memcmp(words + offset + 1, marker.c_str(), marker.size() + 1) == 0, "maximum marker was truncated"); + ++markers; + } + Require(markers == 2, "the maximum markers were not stored in the command area"); + } + Require(factory->DestroyCompute(sequence) == 0, "capacity sequence destruction failed"); + Require(Shutdown(manager) == 0, "capacity manager shutdown failed"); + for (auto offset = size; offset < allocationSize; ++offset) + Require(memory[offset] == 0xa5, "maximum packets escaped the supplied work area"); + ::operator delete(memory, std::align_val_t{256}); +} + void StorageCase(bool directMemory) { constexpr std::uint64_t mask = (1u << 1) | (1u << 5) | (1u << 8); const MemorySizeQueryParams query{1, 2, 0, 0, 32, 8, 4, 2, mask}; @@ -304,7 +348,7 @@ void StorageCase(bool directMemory) { int RunJobManagerCase(const char* name) { try { - Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0, "unknown compute fixture"); + Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0 || std::strcmp(name, "capacity") == 0, "unknown compute fixture"); #ifndef ANYPS5_JOB_MANAGER_REFERENCE auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -313,6 +357,7 @@ int RunJobManagerCase(const char* name) { BuildShader(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0) SizeCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "execution") == 0) RunCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "capacity") == 0) CapacityCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "storage") == 0) { StorageCase(false); StorageCase(true); diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index b591aed225..a215aeed0f 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -119,7 +119,7 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { return (value + alignment - 1) & ~(alignment - 1); } -std::uint64_t ComputeCommandBytes() { +std::uint64_t ComputeCommandBudget() { std::array<std::uint32_t, 256> words{}; CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; const ShaderRegister shaderRegister{}; @@ -634,7 +634,7 @@ WorkLayout LayoutFor(std::uint64_t memory, std::uint64_t size, std::uint32_t wor for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes; address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; if (computeQueues != 0) { - layout.commandBytes = ComputeCommandBytes(); + layout.commandBytes = ComputeCommandBudget(); layout.commands = AlignUp(address + ComputeContextBytes, 0x100); layout.labels = AlignUp(layout.commands + layout.commandBytes * computeQueues + 4 * std::uint64_t{computeQueues}, 8); address = layout.labels + 8 * std::uint64_t{computeQueues}; @@ -799,7 +799,7 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN if (computeSequences != 0) { const auto queues = static_cast<std::uint64_t>(std::popcount(params->computeQueueMask)); bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff}; - bytes = (bytes + ComputeCommandBytes() * queues + 3) & ~std::uint64_t{3}; + bytes = (bytes + ComputeCommandBudget() * queues + 3) & ~std::uint64_t{3}; bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7}; bytes += 8 * queues; } else { diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 184e91e23b..5390f41870 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,7 +2,7 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait; exhausting the bounded command area throws. Native recovery from command exhaustion and console timing equivalence remain unverified. +- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait. Command capacity is a separate bound from the dispatch count: the native size query measures one register and a 31-character marker, then its exhaustion callback at 0x29d0 traps instead of growing storage. The fallback throws. Original-module execution with the host AGC builders accepts two dispatches with 255 registers and 65,535-character markers, then traps on a third. Console timing equivalence remains unverified. - The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps scheduling state and nodes on the host heap; only the header, compute commands and completion labels occupy the caller's block. Blocks smaller than 0x2d8 bytes throw. The worker spin duration and persistent thread count are validated but do not govern scheduling. - [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. - [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. From 1aab236614c9b579d627b99f4f5fca62c0de2fd2 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Fri, 23 Oct 2026 13:52:35 +0000 Subject: [PATCH 1260/1360] feat(libSceJson2): implement sce::Json::String at, find and rfind Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDe3GT5MyAARmdWTiGsiNU --- core/libs/prx/libSceJson2/Export.cpp | 29 +++++++++++++ .../libs/prx/libSceJson2/InventoryExports.cpp | 42 ------------------ core/libs/tests/GuestJson.cpp | 43 +++++++++++++++++++ docs/dev/debt/libSceJson2.md | 8 +--- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index a84b073cd9..9a7943f651 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -14,6 +14,7 @@ #include <sstream> #include <stdexcept> #include <string> +#include <string_view> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -725,6 +726,34 @@ String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String* result, const String* s } std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String* self, char character, std::size_t position) { return self->text->find(character, position); } std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String* self, char character, std::size_t position) { return self->text->rfind(character, position); } +char APS5_VABI _ZNK3sce4Json6String2atEm(const String* self, std::size_t position) { + if (position >= self->text->size()) throw std::out_of_range("sce::Json::String::at: position past the end"); + return (*self->text)[position]; +} +std::size_t APS5_VABI _ZNK3sce4Json6String4findEPKcm(const String* self, const char* text, std::size_t position) { + if (text == nullptr) throw std::invalid_argument("sce::Json::String::find: null text"); + return self->text->find(text, position); +} +std::size_t APS5_VABI _ZNK3sce4Json6String4findEPKcmm(const String* self, const char* text, std::size_t position, std::size_t length) { + if (text == nullptr && length > 0) throw std::invalid_argument("sce::Json::String::find: null text"); + return self->text->find(std::string_view(text == nullptr ? "" : text, length), position); +} +std::size_t APS5_VABI _ZNK3sce4Json6String4findERKS1_m(const String* self, const String* other, std::size_t position) { + if (other == nullptr || other->text == nullptr) throw std::invalid_argument("sce::Json::String::find: null other"); + return self->text->find(*other->text, position); +} +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEPKcm(const String* self, const char* text, std::size_t position) { + if (text == nullptr) throw std::invalid_argument("sce::Json::String::rfind: null text"); + return self->text->rfind(text, position); +} +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEPKcmm(const String* self, const char* text, std::size_t position, std::size_t length) { + if (text == nullptr && length > 0) throw std::invalid_argument("sce::Json::String::rfind: null text"); + return self->text->rfind(std::string_view(text == nullptr ? "" : text, length), position); +} +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindERKS1_m(const String* self, const String* other, std::size_t position) { + if (other == nullptr || other->text == nullptr) throw std::invalid_argument("sce::Json::String::rfind: null other"); + return self->text->rfind(*other->text, position); +} int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null text"); return CompareText(self->text->c_str(), text); diff --git a/core/libs/prx/libSceJson2/InventoryExports.cpp b/core/libs/prx/libSceJson2/InventoryExports.cpp index 5c89743172..89026a8217 100644 --- a/core/libs/prx/libSceJson2/InventoryExports.cpp +++ b/core/libs/prx/libSceJson2/InventoryExports.cpp @@ -407,46 +407,4 @@ int APS5_VABI sceJson2Unknown_067_nid_no_patch() { } APS5_EXPORT("_ZNK3sce4Json5ValueixERKNS0_6StringE", sceJson2Unknown_067_nid_no_patch); -int APS5_VABI sceJson2Unknown_068_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String2atEm", sceJson2Unknown_068_nid_no_patch); - -int APS5_VABI sceJson2Unknown_069_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String4findEPKcm", sceJson2Unknown_069_nid_no_patch); - -int APS5_VABI sceJson2Unknown_070_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String4findEPKcmm", sceJson2Unknown_070_nid_no_patch); - -int APS5_VABI sceJson2Unknown_071_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String4findERKS1_m", sceJson2Unknown_071_nid_no_patch); - -int APS5_VABI sceJson2Unknown_072_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String5rfindEPKcm", sceJson2Unknown_072_nid_no_patch); - -int APS5_VABI sceJson2Unknown_073_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String5rfindEPKcmm", sceJson2Unknown_073_nid_no_patch); - -int APS5_VABI sceJson2Unknown_074_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZNK3sce4Json6String5rfindERKS1_m", sceJson2Unknown_074_nid_no_patch); - } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index b2951558b6..989e53d686 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -94,6 +94,13 @@ String* APS5_VABI _ZN3sce4Json6StringpLEh(String*, unsigned char); String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String*, const String*, std::size_t, std::size_t); std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String*, char, std::size_t); std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String*, char, std::size_t); +char APS5_VABI _ZNK3sce4Json6String2atEm(const String*, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String4findEPKcm(const String*, const char*, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String4findEPKcmm(const String*, const char*, std::size_t, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String4findERKS1_m(const String*, const String*, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEPKcm(const String*, const char*, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEPKcmm(const String*, const char*, std::size_t, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindERKS1_m(const String*, const String*, std::size_t); bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const Iterator*, const Iterator*); bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const Iterator*, const Iterator*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); @@ -692,6 +699,42 @@ static void StringMethods() { Require(_ZNK3sce4Json6String4findEcm(&base, 'z', 0) == _ZN3sce4Json6String4nposE); Require(_ZNK3sce4Json6String5rfindEcm(&base, '/', _ZN3sce4Json6String4nposE) == 3); Require(_ZNK3sce4Json6String5rfindEcm(&base, '/', 2) == 1); + const std::size_t npos = _ZN3sce4Json6String4nposE; + Require(_ZNK3sce4Json6String2atEm(&base, 0) == 'a'); + Require(_ZNK3sce4Json6String2atEm(&base, 4) == 'c'); + bool atRefused = false; + try { + _ZNK3sce4Json6String2atEm(&base, 5); + } catch (const std::out_of_range&) { + atRefused = true; + } + Require(atRefused); + Require(_ZNK3sce4Json6String4findEPKcm(&base, "/b", 0) == 1); + Require(_ZNK3sce4Json6String4findEPKcm(&base, "/", 2) == 3); + Require(_ZNK3sce4Json6String4findEPKcm(&base, "", 5) == 5); + Require(_ZNK3sce4Json6String4findEPKcm(&base, "", 6) == npos); + Require(_ZNK3sce4Json6String4findEPKcm(&base, "b/d", 0) == npos); + Require(_ZNK3sce4Json6String4findEPKcmm(&base, "b/d", 0, 2) == 2); + Require(_ZNK3sce4Json6String4findEPKcmm(&base, nullptr, 3, 0) == 3); + Require(_ZNK3sce4Json6String5rfindEPKcm(&base, "/", npos) == 3); + Require(_ZNK3sce4Json6String5rfindEPKcm(&base, "/", 2) == 1); + Require(_ZNK3sce4Json6String5rfindEPKcm(&base, "", npos) == 5); + Require(_ZNK3sce4Json6String5rfindEPKcmm(&base, "a/x", npos, 2) == 0); + Require(_ZNK3sce4Json6String5rfindEPKcmm(&base, "zz", npos, 2) == npos); + String needle{}; + _ZN3sce4Json6StringC1EPKc(&needle, "/c"); + Require(_ZNK3sce4Json6String4findERKS1_m(&base, &needle, 0) == 3); + Require(_ZNK3sce4Json6String4findERKS1_m(&base, &needle, 4) == npos); + Require(_ZNK3sce4Json6String5rfindERKS1_m(&base, &needle, npos) == 3); + Require(_ZNK3sce4Json6String5rfindERKS1_m(&base, &needle, 2) == npos); + _ZN3sce4Json6StringD1Ev(&needle); + bool findRefused = false; + try { + _ZNK3sce4Json6String4findEPKcm(&base, nullptr, 0); + } catch (const std::invalid_argument&) { + findRefused = true; + } + Require(findRefused); String part{}; Require(_ZNK3sce4Json6String6substrEmm(&part, &base, 2, 3) == &part); Require(text(part) == "b/c"); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index b9fac63328..2f2507b74c 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -10,6 +10,7 @@ - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown - [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - storage uses `std::string`; capacity and allocation growth do not match the system 32-bit capacity and allocator settings. Appending null text with a nonzero length throws instead of dereferencing it. +- [sce::Json::String at, find and rfind](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return types follow the PS Vita library's `String` ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L338)) and the search follows `std::string`. `at` past the end and a null text or `String` throw; what the console returns or does there is unverified. - [sce::Json::Array::iterator::advance and Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return type of `advance` remains unverified. Object iterator advancement past the end throws instead of asserting. Dereferencing an empty array iterator throws instead of returning the system null-value pointer or an invalid address. - [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. @@ -82,10 +83,3 @@ - [_ZNK3sce4Json14InitParameter217getFileBufferSizeEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `Pcg6xQUhq9E` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZNK3sce4Json14InitParameter225getSpecialFloatFormatTypeEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `PDkib0zyABg` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZNK3sce4Json5ValueixERKNS0_6StringE](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `clF7J7N9xXE` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String2atEm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `KrKM-5SdQrA` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String4findEPKcm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `yjsf6SvXA08` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String4findEPKcmm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `wwDnDDsQVc0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String4findERKS1_m](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `awkeNR6Ukp8` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String5rfindEPKcm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `VpQBXbaHXOo` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String5rfindEPKcmm](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `A29RHrlfTM0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZNK3sce4Json6String5rfindERKS1_m](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `-R9mdXBspeQ` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. From 87bc1aa04dd2fc2428c75413ee2e4e3e10694fe8 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Fri, 23 Oct 2026 19:48:23 +0600 Subject: [PATCH 1261/1360] fix(tools): recognize spaced environment reader calls --- tools/env_reference.py | 6 ++- .../test_check_conventions_env_reference.py | 21 +++++++++ tools/tests/test_env_reference.py | 45 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index b7277df3bb..f6bb761417 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -12,11 +12,12 @@ SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} -NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") +NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\s*\(") INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' r'|configured|PipelineSwitch|environmentCount|addWideString)' r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') +GREP = GREP.replace(r"\(", r"[[:space:]]*\(") def _name(prefixes): @@ -26,7 +27,7 @@ def _name(prefixes): @functools.lru_cache(maxsize=None) def forms(prefixes=PREFIXES): name = _name(prefixes) - return ( + readers = ( ("getenv", re.compile(r'(?:std::)?getenv\(\s*"(?P<name>' + name + r')"\s*\)')), ("_wgetenv", re.compile(r'(?:std::)?_?wgetenv\(\s*L"(?P<name>' + name + r')"\s*\)')), ("IsEnvironmentSet", re.compile(r'IsEnvironmentSet\(\s*"(?P<name>' + name + r')"\s*\)')), @@ -40,6 +41,7 @@ def forms(prefixes=PREFIXES): ("GetEnvironmentVariableW", re.compile(r'GetEnvironmentVariableW\(\s*L"(?P<name>' + name + r')"')), ("addWideString", re.compile(r'\baddWideString\(\s*u"(?P<name>' + name + r')"\s*\)')), ) + return tuple((reader, re.compile(form.pattern.replace(r"\(", r"\s*\(", 1), form.flags)) for reader, form in readers) def line_reads(line, prefixes=PREFIXES): diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py index 270218d686..d8fe70da63 100644 --- a/tools/tests/test_check_conventions_env_reference.py +++ b/tools/tests/test_check_conventions_env_reference.py @@ -85,6 +85,27 @@ class EnvReferenceDiffTests(unittest.TestCase): with self.subTest(directory=directory): self.assertEqual(self.run_case(base, head, target, directory), expected) + def test_a_new_spaced_read_is_reported_at_its_line(self): + base = {DOC: DOC_TEXT, SAMPLE: "int unrelated;\n"} + head = {SAMPLE: base[SAMPLE] + 'auto value = !std::getenv \t("APS5_NEW_READ");\n'} + self.assertEqual(self.run_case(base, head), [("env-reference", SAMPLE, 2, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_removing_a_compact_read_keeps_a_row_still_used_by_a_spaced_read(self): + other = "core/libs/prx/libSceExample/Other.cpp" + base = { + DOC: DOC_TEXT, + SAMPLE: 'auto a = std::getenv("APS5_DOCUMENTED");\n', + other: 'auto b = _wgetenv \t(L"APS5_DOCUMENTED");\n', + } + self.assertEqual(self.run_case(base, {SAMPLE: "int unrelated;\n"}), []) + + def test_a_removed_row_is_reported_when_a_spaced_read_remains(self): + base = {DOC: DOC_TEXT, SAMPLE: 'auto value = std::getenv \t("APS5_DOCUMENTED");\n'} + head = {DOC: "| `APS5_GONE` | presence |\n"} + self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, + "APS5_DOCUMENTED is still read by the code but this pull request removes its row")]) + def test_only_a_name_the_pull_request_adds_is_reported_at_its_line(self): base = { DOC: DOC_TEXT, diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index f56a933b14..36020ce584 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -78,6 +78,51 @@ class EnvReferenceTests(unittest.TestCase): found = env_reference.reads([root / "core"]) self.assertIn("APS5_MULTILINE", found) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + expected = env_reference.reads([root / "core"]) + for separator in (" ", "\t", " \t "): + with self.subTest(separator=separator): + source.write_text(CODE.replace("(", separator + "("), encoding="utf-8") + self.assertEqual(env_reference.reads([root / "core"]), expected) + + def test_reader_and_parenthesis_on_separate_lines_keep_line_numbers_and_kinds(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text('auto a = !std::getenv\n ("APS5_PRESENT");\n' + 'auto b = _wtoi(_wgetenv\n (L"ANYPS5_INTEGER"));\n', encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(set(found), {"APS5_PRESENT", "ANYPS5_INTEGER"}) + self.assertEqual(found["APS5_PRESENT"], [(source.as_posix(), 1, "presence", "")]) + self.assertEqual(found["ANYPS5_INTEGER"], [(source.as_posix(), 3, "integer", "")]) + + def test_main_check_rejects_an_undocumented_spaced_read(self): + root = self.fixture() + (root / "core" / "Sample.cpp").write_text('auto value = !std::getenv \t("APS5_UNDOCUMENTED");\n', encoding="utf-8") + doc = root / "ENVIRONMENT.md" + doc.write_text("", encoding="utf-8") + with contextlib.redirect_stdout(io.StringIO()) as out: + code = env_reference.main(["--check", "--root", str(root / "core"), "--doc", str(doc)]) + self.assertEqual(code, 1) + self.assertIn("APS5_UNDOCUMENTED is read", out.getvalue()) + self.assertNotIn("all documented", out.getvalue()) + + def test_negated_spaced_getenv_is_a_presence_read(self): + root = self.fixture() + (root / "core" / "Sample.cpp").write_text('auto a = !std::getenv ("APS5_PRESENT");\n' + 'auto b = !_wgetenv\t(L"ANYPS5_PRESENT");\n', encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(set(found), {"APS5_PRESENT", "ANYPS5_PRESENT"}) + self.assertEqual([sites[0][2] for sites in found.values()], ["presence", "presence"]) + + def test_spacing_does_not_change_literal_parentheses_in_a_custom_prefix(self): + for separator in ("", " ", "\t"): + with self.subTest(separator=separator): + source = f'getenv{separator}("CUSTOM(NAME"); getenv{separator}("CUSTOM (WRONG");' + found = env_reference.line_reads(source, ("CUSTOM(",)) + self.assertEqual([match.group("name") for _, match in found], ["CUSTOM(NAME"]) + def test_multiline_reads_keep_line_numbers_and_do_not_duplicate(self): root = self.fixture() source = root / "core" / "Sample.cpp" From 98737e21751ac31365e6c2f72463b8b923d1f36b Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Fri, 23 Oct 2026 20:29:54 +0600 Subject: [PATCH 1262/1360] fix(tools): measure Windows test batches after quoting --- tools/select_tests.py | 7 +++-- tools/tests/test_select_tests.py | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 80005e556d..78f6fc375d 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -146,11 +146,14 @@ def relative(path, root): def chunks(names): batch, size = [], 0 for name in names: - if batch and size + len(name) + 1 > BATCH: + length = len(name) + 1 + if sys.platform == "win32": + length = len(subprocess.list2cmdline([name]).encode("utf-16-le", "surrogatepass")) // 2 + 1 + if batch and size + length > BATCH: yield batch batch, size = [], 0 batch.append(name) - size += len(name) + 1 + size += length if batch: yield batch diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index b89ba0904a..38fa425aa6 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -339,6 +339,51 @@ class CaseInsensitivePathTests(unittest.TestCase): self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) +class ChunkTests(unittest.TestCase): + def test_windows_batches_fit_the_serialized_utf16_budget(self): + cases = ( + (20000, ["\U0001f600" * 50 + f"/{index}.obj" for index in range(400)]), + (64, ['space name\\', 'embedded"quote', "\t", "", 'both \\" slash\\'] * 20), + (64, ["\ud800" * 10, "\U0001f600" * 10] * 20), + ) + for budget, names in cases: + with self.subTest(budget=budget, first=names[0]), mock.patch.object(select_tests.sys, "platform", "win32"), \ + mock.patch.object(select_tests, "BATCH", budget): + batches = list(select_tests.chunks(iter(names))) + self.assertEqual([name for batch in batches for name in batch], names) + self.assertGreater(len(batches), 1) + for batch in batches: + self.assertTrue(batch) + command = subprocess.list2cmdline(batch) + self.assertLessEqual(len(command.encode("utf-16-le", "surrogatepass")) // 2 + 1, budget) + + def test_windows_batch_keeps_an_exact_boundary(self): + with mock.patch.object(select_tests.sys, "platform", "win32"), mock.patch.object(select_tests, "BATCH", 8): + self.assertEqual(list(select_tests.chunks(["a b", "c", "d"])), [["a b", "c"], ["d"]]) + + def test_other_platforms_keep_the_existing_character_budget(self): + names = ["\U0001f600" * 3, "abc", "\udcff"] + for platform in ("linux", "darwin"): + with self.subTest(platform=platform), mock.patch.object(select_tests.sys, "platform", platform), \ + mock.patch.object(select_tests, "BATCH", 8): + self.assertEqual(list(select_tests.chunks(iter(names))), [names[:2], names[2:]]) + + def test_empty_input_and_an_oversized_single_argument_are_preserved(self): + for platform in ("win32", "linux"): + with self.subTest(platform=platform), mock.patch.object(select_tests.sys, "platform", platform), \ + mock.patch.object(select_tests, "BATCH", 4): + self.assertEqual(list(select_tests.chunks(iter([]))), []) + self.assertEqual(list(select_tests.chunks(["long target", "x"])), [["long target"], ["x"]]) + + @unittest.skipUnless(sys.platform == "win32", "needs the Windows command line limit") + def test_windows_batches_launch_without_losing_arguments(self): + names = ["\U0001f600" * 50 + f"/{index}.obj" for index in range(400)] + for batch in select_tests.chunks(names): + result = subprocess.run([sys.executable, "-I", "-c", "import json,sys; print(json.dumps(sys.argv[1:]))", *batch], + capture_output=True, text=True, check=True, timeout=5) + self.assertEqual(json.loads(result.stdout), batch) + + class PlanTests(unittest.TestCase): def test_unresolved_selected_command_keeps_unselected_build_targets(self): build = mock.Mock() From 3bcd89e10eb5e00c4251c6e5a60be6b00c979fff Mon Sep 17 00:00:00 2001 From: Santhosh Kumar <santhosh-pc@outlook.com> Date: Sun, 18 Oct 2026 00:11:09 +0530 Subject: [PATCH 1263/1360] fix(agc): reclaim idle images safely during cache creation Forward new-image recovery permission through AllocateImageMemory while preserving host-memory fallback and allocated-type reporting. Reuse held cache locks only at safe construction points, retain caller-held sampled textures and pending storage results, and try resource-cache removal without waiting. Count released Vulkan allocations and saturate the reclaim reserve. Cover sampled/storage allocation failures, host fallback after image recovery, bounded attempts and overflow. Remove stale conflict markers and unrelated debt, and refresh the recorded validation. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++ .../Graphics/include/BufferPool.hpp | 8 +- .../Graphics/include/Context.hpp | 2 +- .../Graphics/include/ShaderResources.hpp | 4 +- .../Graphics/include/Texture.hpp | 2 +- .../Graphics/src/BufferPool.cpp | 16 +-- .../Graphics/src/ShaderResources.cpp | 113 ++++++++++-------- .../libSceAgcDriver/Graphics/src/Texture.cpp | 12 +- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 18 ++- .../tests/execution/SampledCacheBudget.cpp | 83 ++++++++++++- .../tests/execution/StorageCacheBudget.cpp | 11 +- docs/dev/debt/libSceAgcDriver.md | 1 + docs/user/USAGE.md | 2 + 13 files changed, 203 insertions(+), 77 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 03b1a6a617..08f96fd7ad 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2786,6 +2786,14 @@ if(BUILD_TESTING) set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + foreach(imageRecoveryMode IN ITEMS sampled storage) + add_test(NAME agc_driver_recover_${imageRecoveryMode}_allocation COMMAND agc_driver_sampled_cache_budget_tests recover-${imageRecoveryMode}) + set_tests_properties(agc_driver_recover_${imageRecoveryMode}_allocation PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_recover_${imageRecoveryMode}_allocation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 5dcf54c727..871368f956 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -66,9 +66,9 @@ public: // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); - VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr, bool newImage = false); static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); - VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr, bool newImage = false); void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; std::size_t RetainedImages(); std::optional<BufferAllocation> Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); @@ -143,7 +143,7 @@ private: // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; - VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes); + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes, bool newImage); VkDevice device; PFN_vkUnmapMemory unmap; PFN_vkDestroyBuffer destroyBuffer; @@ -182,7 +182,7 @@ private: }; std::shared_ptr<BufferPool> GetBufferPool(const Context& context); -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr, bool newImage = false); void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 4019bd9ff5..286666da03 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -194,7 +194,7 @@ struct Context { std::uint32_t settleFamily = 0; std::mutex* settleQueueMutex = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; - VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes) = nullptr; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes, bool newImage) = nullptr; RenderCache* renderCache = nullptr; DrawQueue* drawQueue = nullptr; GraphicsPipelineCache* graphicsPipelines = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index d090ebd057..0ed65026c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -30,7 +30,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); -std::uint64_t ReleaseTextureCaches(std::uint64_t bytes); +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage = false); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); @@ -705,6 +705,7 @@ public: // meanwhile by another worker stays). void Remove(const Key& key, const ShaderResources* object = nullptr); void RemoveImage(const StorageTexture* image, std::vector<std::shared_ptr<ShaderResources>>* evicted = nullptr); + bool TryRemoveImage(const StorageTexture* image, std::vector<std::shared_ptr<ShaderResources>>* evicted = nullptr); // Moves the entry to the front of the LRU (a recipe hit uses its template without Find, and a // hot template must not age out under the recipes that depend on it); false when no entry. bool Touch(const Key& key); @@ -727,6 +728,7 @@ private: void noteEvicted(const Key& key); void noteReturn(const Key& key); void noteUse(const Key& key); + void removeImage(const StorageTexture* image, std::vector<std::shared_ptr<ShaderResources>>* evicted); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { constexpr std::uint64_t Prime = 0x9e3779b97f4a7c15ull; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 4dde83053f..e218054371 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -47,7 +47,7 @@ struct OwnedImage { OwnedImage(const OwnedImage&) = delete; OwnedImage& operator=(const OwnedImage&) = delete; ~OwnedImage(); - void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); + void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, bool newImage = false); Context context; VkImage image; VkDeviceMemory memory; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 128970a898..715b75a6b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -57,7 +57,7 @@ BufferPool::~BufferPool() { for (auto& [memory, block] : slabBlocks) freeBlock(*block); } -VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType, std::uint32_t* allocatedType) { +VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType, std::uint32_t* allocatedType, bool newImage) { if (allocatedType != nullptr) *allocatedType = allocation.memoryTypeIndex; VkDeviceMemory memory = VK_NULL_HANDLE; auto result = allocateMemory(device, &allocation, nullptr, &memory); @@ -75,7 +75,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation } } if (exhausted(result)) { - if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); + if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize), newImage); if (released != 0) { reclaimed += trim(); memory = VK_NULL_HANDLE; @@ -146,7 +146,7 @@ bool BufferPool::heapOverBudget() { return heapOver; } -VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType, std::uint32_t* allocatedType) { +VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType, std::uint32_t* allocatedType, bool newImage) { const auto type = allocation.memoryTypeIndex; if (imagePooled(allocation.allocationSize, type)) { std::lock_guard lock(mutex); @@ -168,7 +168,7 @@ VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocatio } ++imageMisses; } - return AllocateMemory(allocation, operation, hostType, allocatedType); + return AllocateMemory(allocation, operation, hostType, allocatedType, newImage); } void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { @@ -217,7 +217,9 @@ std::size_t BufferPool::RetainedImages() { } VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { - return bytes + std::max<VkDeviceSize>(bytes, VkDeviceSize{256} << 20u); + const auto reserve = std::max<VkDeviceSize>(bytes, VkDeviceSize{256} << 20u); + const auto limit = std::numeric_limits<VkDeviceSize>::max(); + return bytes > limit - reserve ? limit : bytes + reserve; } bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { @@ -533,8 +535,8 @@ std::shared_ptr<BufferPool> GetBufferPool(const Context& context) { return context.bufferPool; } -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) { - return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType, bool newImage) { + return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType, newImage); } void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 4d56d0db54..09f1984c79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -16,7 +16,6 @@ #include <atomic> #include <memory> #include <mutex> -#include <thread> #include <array> #include <chrono> #include <cstdio> @@ -163,6 +162,8 @@ TextureCache& Textures() { thread_local unsigned sampledCacheHeld = 0; thread_local unsigned storageCacheHeld = 0; +thread_local unsigned sampledCacheReclaimable = 0; +thread_local unsigned storageCacheReclaimable = 0; struct CacheHold { unsigned& depth; @@ -172,15 +173,6 @@ struct CacheHold { CacheHold& operator=(const CacheHold&) = delete; }; -bool AcquireCache(AgcDriver::Mutex& mutex) { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20); - do { - if (mutex.try_lock()) return true; - std::this_thread::yield(); - } while (std::chrono::steady_clock::now() < deadline); - return false; -} - struct UncachedImages { AgcDriver::Mutex mutex; std::vector<std::weak_ptr<StorageTexture>> images; @@ -919,14 +911,18 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; entry.committed = std::move(committed); entry.surfaceBytes = bytes; + const auto createTexture = [&](auto&&... args) { + const CacheHold reclaimable(sampledCacheReclaimable); + return std::make_shared<Texture>(context, std::forward<decltype(args)>(args)...); + }; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); if (depthCompare) { - entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::DepthCopyTag{}); + entry.texture = createTexture(source, resource, components, Texture::DepthCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (blockCopy) { - entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{}); + entry.texture = createTexture(source, resource, components, Texture::BlockCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (IsRepackedTextureFormat(resource.format)) { entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::RepackCopyTag{}); @@ -935,7 +931,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::SrgbExpandTag{}); entry.texture->RefreshDepthCopy(); } else { - entry.texture = std::make_shared<Texture>(context, source, resource, components); + entry.texture = createTexture(source, resource, components); } entry.accounted = source->Cached() ? 0u : heldBytes(*source); counters.fromStorage.fetch_add(1, std::memory_order_relaxed); @@ -951,7 +947,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s for (std::size_t i = 0; i < entry.bytes.size(); i += 64) nonzero += entry.bytes[i] != std::byte{0}; std::fprintf(stderr, "[texture] 0x%llx %ux%u format %u tile %d: %zu of %zu sampled bytes nonzero\n", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), nonzero, entry.bytes.size() / 64); } - entry.texture = std::make_shared<Texture>(context, *context.detiler, resource, components, entry.bytes, depthCompare); + entry.texture = createTexture(*context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); constexpr std::size_t SnapshotCompareBytes = 256u << 10u; if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector<std::byte>().swap(entry.bytes); @@ -1179,7 +1175,12 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std } else { if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; if (StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); - CachedStorageTexture entry{key, mip, StorageTexture::Create(context, *context.detiler, resource, mip)}; + std::shared_ptr<StorageTexture> created; + { + const CacheHold reclaimable(storageCacheReclaimable); + created = StorageTexture::Create(context, *context.detiler, resource, mip); + } + CachedStorageTexture entry{key, mip, std::move(created)}; static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); entry.accounted = heldBytes(*entry.texture); @@ -1294,6 +1295,7 @@ void FlushCachedTextures(VkDevice device) { GuestMemory::AssertGpuLockHeld("FlushCachedTextures"); auto& cache = StorageTextures(); std::lock_guard lock(cache.mutex); + const CacheHold held(storageCacheHeld); for (const auto& entry : cache.entries) { if (entry.key.device == device) entry.texture->Flush(); } @@ -1304,6 +1306,7 @@ void ClearCachedTextures(VkDevice device) { auto& sampled = Textures(); { std::lock_guard lock(sampled.mutex); + const CacheHold held(sampledCacheHeld); for (auto it = sampled.entries.begin(); it != sampled.entries.end();) { if (it->key.device == device) eraseTexture(sampled, it++); else ++it; @@ -1313,6 +1316,7 @@ void ClearCachedTextures(VkDevice device) { std::vector<std::shared_ptr<StorageTexture>> cleared; { std::lock_guard lock(storage.mutex); + const CacheHold held(storageCacheHeld); for (auto it = storage.entries.begin(); it != storage.entries.end();) { if (it->key.device != device) { ++it; @@ -1339,7 +1343,7 @@ void ClearCachedTextures(VkDevice device) { } } -std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage) { thread_local bool releasing = false; if (releasing) return 0; releasing = true; @@ -1355,63 +1359,67 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { bool storageSkipped = false; { auto& cache = Textures(); - if (sampledCacheHeld != 0 || !AcquireCache(cache.mutex)) { + std::unique_lock lock(cache.mutex, std::defer_lock); + const bool inherited = sampledCacheHeld != 0; + if ((inherited && (!newImage || sampledCacheReclaimable == 0)) || (!inherited && !lock.try_lock())) { sampledSkipped = true; } else { - std::lock_guard lock(cache.mutex, std::adopt_lock); - dropUncachedViews(cache); - while (freed < bytes && !cache.entries.empty()) { - const auto back = std::prev(cache.entries.end()); - const auto accounted = back->accounted; - const bool last = back->texture.use_count() == 1; - eraseTexture(cache, back); + for (auto it = cache.entries.end(); it != cache.entries.begin() && freed < bytes;) { + --it; + if (it->texture.use_count() != 1 || (it->source != nullptr && it->source->Dirty())) continue; + const auto allocated = it->texture->AllocationBytes(); + const auto sourceBytes = it->source != nullptr ? it->source->AllocationBytes() : 0u; + const std::weak_ptr<StorageTexture> source = it->source; + const auto next = std::next(it); + eraseTexture(cache, it); ++cache.counts.evicted; ++sampledEntries; - if (last) freed += accounted; + freed += allocated; + if (source.expired()) freed += sourceBytes; + it = next; } } } if (freed < bytes) { auto& cache = StorageTextures(); std::unique_lock<GuestMemory::GpuMutexType> gpu(GuestMemory::GpuMutex(), std::defer_lock); - if (storageCacheHeld != 0 || !gpu.try_lock()) { + const bool inherited = storageCacheHeld != 0; + if ((inherited && (!newImage || storageCacheReclaimable == 0)) || !gpu.try_lock()) { storageSkipped = true; } else { try { while (freed < bytes) { - std::vector<std::pair<std::shared_ptr<StorageTexture>, std::uint64_t>> evicted; + std::shared_ptr<StorageTexture> image; { - if (!AcquireCache(cache.mutex)) { + std::unique_lock lock(cache.mutex, std::defer_lock); + if (!inherited && !lock.try_lock()) { storageSkipped = true; break; } - std::lock_guard lock(cache.mutex, std::adopt_lock); - std::uint64_t batch = 0; storageKept = 0; - for (auto it = cache.entries.end(); it != cache.entries.begin() && batch + freed < bytes;) { + for (auto it = cache.entries.end(); it != cache.entries.begin();) { --it; if (it->texture->Dirty()) { ++storageKept; continue; } - const auto accounted = it->accounted; - const auto next = std::next(it); - evicted.emplace_back(evictStorage(cache, it), accounted); - batch += accounted; - it = next; + image = evictStorage(cache, it); + break; } - if (evicted.empty()) break; + if (image == nullptr) break; } - for (auto& [image, accounted] : evicted) { - std::vector<std::shared_ptr<ShaderResources>> resources; - SharedResourceCache().RemoveImage(image.get(), &resources); - if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); - } - resources.clear(); - ++storageEntries; - if (image.use_count() == 1) freed += accounted; - image.reset(); + std::vector<std::shared_ptr<ShaderResources>> resources; + const bool removed = SharedResourceCache().TryRemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); + } + resources.clear(); + ++storageEntries; + if (image.use_count() == 1) freed += image->AllocationBytes(); + image.reset(); + if (!removed) { + storageSkipped = true; + break; } } } catch (const std::exception& error) { @@ -3943,6 +3951,17 @@ void ResourceCache::Remove(const Key& key, const ShaderResources* object) { void ResourceCache::RemoveImage(const StorageTexture* image, std::vector<std::shared_ptr<ShaderResources>>* evicted) { std::lock_guard lock(mutex); + removeImage(image, evicted); +} + +bool ResourceCache::TryRemoveImage(const StorageTexture* image, std::vector<std::shared_ptr<ShaderResources>>* evicted) { + std::unique_lock lock(mutex, std::try_to_lock); + if (!lock.owns_lock()) return false; + removeImage(image, evicted); + return true; +} + +void ResourceCache::removeImage(const StorageTexture* image, std::vector<std::shared_ptr<ShaderResources>>* evicted) { for (auto it = entries.begin(); it != entries.end();) { if (!it->second->ReadsImage(image)) { ++it; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 632ed7bb71..5060743d4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -48,8 +48,8 @@ OwnedImage::~OwnedImage() { ReleaseImageMemory(context, memory, memoryBytes, memoryType); } -void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { - memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType); +void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, bool newImage) { + memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType, newImage); memoryBytes = allocation.allocationSize; context.bufferPool = device.bufferPool; } @@ -366,7 +366,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture", true); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); const VkImage uploadImage = image; @@ -701,7 +701,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy", true); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -760,7 +760,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy", true); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -1452,7 +1452,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType, true); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index abdf8f0166..bf7d146519 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -11,6 +11,7 @@ #include <deque> #include <initializer_list> #include <map> +#include <limits> #include <memory> #include <new> #include <set> @@ -675,6 +676,7 @@ struct ReclaimMock { VkDeviceSize externalBytes = 0; std::uint64_t requests = 0; VkDeviceSize asked = 0; + bool newImage = false; }; ReclaimMock reclaimMock; @@ -689,7 +691,8 @@ VkDeviceMemory ExternalAllocation(VkDeviceSize bytes) { return memory; } -VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) { +VkDeviceSize mockCacheReclaim(VkDeviceSize bytes, bool newImage) { + Expect(newImage == reclaimMock.newImage, "an allocation used the wrong image-creation cache access"); ++reclaimMock.requests; reclaimMock.asked = bytes; if (reclaimMock.external == VK_NULL_HANDLE) return 0; @@ -701,6 +704,10 @@ VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) { void CacheReclaimRecovery() { Expect(BufferPool::ReclaimTarget(4 * MiB) == 260 * MiB, "a 4 MiB request does not ask the caches for 256 MiB on top of itself"); Expect(BufferPool::ReclaimTarget(512 * MiB) == 1024 * MiB, "a 512 MiB request does not ask the caches for twice its size"); + const auto limit = std::numeric_limits<VkDeviceSize>::max(); + Expect(BufferPool::ReclaimTarget(limit / 2u) == limit - 1u, "the largest doubled request without overflow lost its reserve"); + Expect(BufferPool::ReclaimTarget(limit / 2u + 1u) == limit, "a reclaim target wrapped when twice the request overflowed"); + Expect(BufferPool::ReclaimTarget(limit) == limit, "the largest reclaim request wrapped to a smaller target"); constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; mock = MockDevice{}; reclaimMock = ReclaimMock{}; @@ -775,18 +782,23 @@ void ImageCacheReclaim() { VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = 8 * MiB; allocation.memoryTypeIndex = 0; - const auto refuse = [&](std::initializer_list<VkResult> results) { + const auto refuse = [&](std::initializer_list<VkResult> results, bool newImage = false) { ExternalAllocation(4 * MiB); + reclaimMock.newImage = newImage; mock.results = results; const auto requests = reclaimMock.requests; - const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture"); + const auto before = mock.attempts; + const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture", newImage); Expect(reclaimMock.requests == requests + 1 && reclaimMock.external == VK_NULL_HANDLE, "a texture refused device memory did not release the texture caches before retrying"); + Expect(mock.attempts == before + results.size() + 1, "image recovery or host fallback exceeded its bounded attempts"); const auto type = mock.memoryTypes.at(memory); mockFreeMemory(VK_NULL_HANDLE, memory, nullptr); return type; }; Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 0, "a texture was placed in host memory although releasing the texture caches made room in device memory"); Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 1, "a texture still refused device memory after releasing the texture caches was not placed in host memory"); + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY}, true) == 0, "a new image was placed in host memory before retrying after cache release"); + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}, true) == 1, "a new image refused device memory after cache release lost its host fallback"); Expect(mock.liveBytes == 0, "a texture allocation leaked memory"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 46e3184ea6..93ef5d147f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -1006,6 +1007,78 @@ void storageMemoryTests(Context context) { std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast<unsigned long long>(ReportedBudget >> 20u), static_cast<unsigned long long>(PressedUsage >> 20u), static_cast<unsigned long long>(pressed >> 20u)); } +struct RecoveryProbe { + PFN_vkGetDeviceProcAddr deviceProc = nullptr; + std::uint64_t released = 0; + unsigned requests = 0; + unsigned failures = 0; + bool pressure = false; +}; + +RecoveryProbe recovery; + +VkDeviceSize ReclaimForTest(VkDeviceSize bytes, bool newImage) { + Require(newImage, "an image allocation did not identify itself to the recovery callback"); + ++recovery.requests; + Require(ReleaseTextureCaches(bytes, false) == 0, "a buffer allocation was allowed to reclaim the cache held by image creation"); + const auto released = ReleaseTextureCaches(bytes, newImage); + recovery.released += released; + return released; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateForTest(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + if (recovery.pressure && recovery.released == 0) { + ++recovery.failures; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + return reinterpret_cast<PFN_vkAllocateMemory>(recovery.deviceProc(device, "vkAllocateMemory"))(device, info, allocator, memory); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL RecoveryProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(AllocateForTest); + return recovery.deviceProc(device, name); +} + +void recoveryTests(Context context, bool storage) { + recovery = RecoveryProbe{}; + recovery.deviceProc = context.deviceProc; + context.deviceProc = RecoveryProc; + context.cacheReclaim = ReclaimForTest; + context.bufferPool.reset(); + Block block(context); + if (storage) { + auto oldest = CachedStorageSurface(context, Resource(block.Surface(0))); + const auto allocated = oldest->AllocationBytes(); + std::weak_ptr<StorageTexture> released = oldest; + oldest.reset(); + auto pending = CachedStorageSurface(context, Resource(block.Surface(1))); + pending->MarkDirty(); + recovery.pressure = true; + auto made = CachedStorageSurface(context, Resource(block.Surface(2))); + recovery.pressure = false; + Require(made != nullptr && released.expired(), "storage allocation recovery did not release its own oldest clean image"); + Require(pending->Dirty() && StorageImageCached(context, pending.get()), "storage allocation recovery changed an image with pending results"); + Require(recovery.released == allocated, "storage allocation recovery counted memory still held by its caller"); + pending->Flush(); + Require(!pending->Dirty(), "pending storage results could not be stored after memory recovery"); + } else { + auto oldest = Sampled(context, block.Surface(0)); + Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); + const auto allocated = oldest->AllocationBytes(); + std::weak_ptr<Texture> released = oldest; + oldest.reset(); + auto held = Sampled(context, block.Surface(1)); + recovery.pressure = true; + auto made = Sampled(context, block.Surface(2)); + recovery.pressure = false; + Require(made != nullptr && released.expired(), "sampled allocation recovery did not release its own oldest unused texture"); + Require(Sampled(context, block.Surface(1)) == held, "sampled allocation recovery removed a texture still held by its caller"); + Require(recovery.released == allocated, "sampled allocation recovery counted memory still held by its caller"); + } + Require(recovery.requests == 1 && recovery.failures >= 1 && recovery.failures <= 2, "image allocation recovery was skipped or retried without a bound"); + std::printf("%s image allocation recovered after %u injected failures: %llu bytes released\n", storage ? "storage" : "sampled", recovery.failures, static_cast<unsigned long long>(recovery.released)); +} + } int main(int argc, char** argv) { @@ -1018,11 +1091,12 @@ int main(int argc, char** argv) { } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; + const bool recover = mode == "recover-sampled" || mode == "recover-storage"; #ifdef _WIN32 - if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1"); if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else - if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1", 1); setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr<Device> device; @@ -1082,6 +1156,11 @@ int main(int argc, char** argv) { std::puts("sampled texture memory budget tests passed"); return 0; } + if (recover) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recoveryTests(context, mode == "recover-storage"); + return 0; + } { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); reportedBudgetTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index a3a273ff50..87090fa580 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -162,6 +162,7 @@ int main(int argc, char** argv) { Require(first != nullptr, "surface 0 has no cached storage image"); Require(AgcDriver::Graphics::SharedResourceCache().Size() != 0, "the storage cache workload did not populate the resource cache"); const auto held = std::max<std::uint64_t>(first->AllocationBytes(), first->GuestBytes()); + const auto allocated = first->AllocationBytes(); const auto reused = static_cast<std::uint32_t>(FixedBudget / held) + 1u; std::printf("texture cache budget %llu MiB; each 512x512 32_UINT surface holds %llu MiB; cyclic set of %u surfaces (%llu MiB)\n", static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(held >> 20u), reused, static_cast<unsigned long long>((reused * held) >> 20u)); Require(budget >= reused * held, "the cyclic set exceeds the test cache budget"); @@ -183,8 +184,8 @@ int main(int argc, char** argv) { Require(cached == (surface >= 2u), "surface " + std::to_string(surface) + (cached ? " is still cached" : " was evicted") + " after " + std::to_string(capacity + 2u) + " surfaces under a budget of " + std::to_string(capacity) + " of them"); Require(cached == !images[surface].expired(), "surface " + std::to_string(surface) + (cached ? " is cached but its weak image expired" : " was evicted but its resource cache image is still alive")); } - const auto released = AgcDriver::Graphics::ReleaseTextureCaches(held); - Require(released >= held, "a release of one image's bytes freed " + std::to_string(released >> 20u) + " MiB, less than the " + std::to_string(held >> 20u) + " MiB of the oldest image"); + const auto released = AgcDriver::Graphics::ReleaseTextureCaches(allocated); + Require(released == allocated, "a release of one image's bytes counted " + std::to_string(released) + " bytes, not its " + std::to_string(allocated) + "-byte allocation"); Require(StorageTexture::FindLive(base + 2u * Stride, SurfaceBytes) == nullptr && images[2].expired(), "the oldest cached storage image survived a release of its bytes"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr && !images[3].expired(), "a release of one image's bytes evicted more than the oldest image"); Require(StorageTexture::FindLive(base + (capacity + 1u) * Stride, SurfaceBytes) != nullptr, "a release of one image's bytes evicted the newest image"); @@ -194,11 +195,11 @@ int main(int argc, char** argv) { Require(Store(*device, base, 4) != nullptr, "surface 4 is not cached"); for (std::uint32_t surface = 5; surface < capacity + 2u; ++surface) Require(Store(*device, base, surface) != nullptr, "surface " + std::to_string(surface) + " is not cached"); } - Require(AgcDriver::Graphics::ReleaseTextureCaches(held) >= held, "a release with a dirty oldest image freed nothing"); + Require(AgcDriver::Graphics::ReleaseTextureCaches(allocated) == allocated, "a release with a dirty oldest image did not free one clean allocation"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr, "a release took the dirty oldest image before a clean one"); Require(StorageTexture::FindLive(base + 4u * Stride, SurfaceBytes) == nullptr && images[4].expired(), "a release did not take the oldest clean image"); const auto rest = AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); - Require(rest >= held * (capacity - 3u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 3u)) >> 20u) + " MiB cached"); + Require(rest == allocated * (capacity - 3u), "a release of everything counted " + std::to_string(rest) + " bytes, not the " + std::to_string(allocated * (capacity - 3u)) + " bytes allocated"); device->WaitIdle(); for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) { if (surface == 3u) continue; @@ -210,7 +211,7 @@ int main(int argc, char** argv) { } StorageTexture::FlushPending(base + 3u * Stride, static_cast<std::size_t>(SurfaceBytes), nullptr, "test"); device->WaitIdle(); - Require(AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}) >= held, "a release did not take the image once its results were stored"); + Require(AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}) == allocated, "a release did not take the image once its results were stored"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) == nullptr, "surface 3 survived a release after its results were stored"); Require(AgcDriver::Graphics::ReleaseTextureCaches(held) == 0, "a release from empty caches reported freed bytes"); } catch (...) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index a9e5829f8c..b573617dbf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -69,6 +69,7 @@ - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. +- [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. Fault-injection fixtures passed on Windows 11 (AMD RX 6700 XT, AMD Vulkan 26.8.1); the integrated image-allocation wrapper passed on Linux (AMD RX 9070 XT, RADV). Other hardware driver stacks are unverified. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index bf137f6dd6..3a08d85d7c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -299,6 +299,8 @@ Besides the recompiled shaders, the shader cache directory holds `pipelines.jour ### Texture cache budget +Image and buffer memory allocations retry after releasing unused pooled buffers and reclaimable texture cache entries. Recovery preserves storage images with pending GPU results and resources retained by recorded work. `[bufferpool]` recovery and failure lines report the requested allocation, memory released and retry count. An allocation still fails if recovery cannot free enough memory. + Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. From 737f6073ec01b1b71558b47844d47878a8018b16 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:09:17 -0300 Subject: [PATCH 1264/1360] test(agc): adapt recovery callback signatures --- core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index bf7d146519..d1efb26289 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -788,7 +788,7 @@ void ImageCacheReclaim() { mock.results = results; const auto requests = reclaimMock.requests; const auto before = mock.attempts; - const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture", newImage); + const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture", nullptr, newImage); Expect(reclaimMock.requests == requests + 1 && reclaimMock.external == VK_NULL_HANDLE, "a texture refused device memory did not release the texture caches before retrying"); Expect(mock.attempts == before + results.size() + 1, "image recovery or host fallback exceeded its bounded attempts"); const auto type = mock.memoryTypes.at(memory); @@ -912,7 +912,7 @@ void DeviceBindingFailures() { VkDeviceMemory reclaimImage = VK_NULL_HANDLE; const Context* reclaimImageContext = nullptr; -VkDeviceSize mockImageReclaim(VkDeviceSize) { +VkDeviceSize mockImageReclaim(VkDeviceSize, bool) { if (reclaimImage == VK_NULL_HANDLE) return 0; ReleaseImageMemory(*reclaimImageContext, reclaimImage, 10 * MiB, 0); reclaimImage = VK_NULL_HANDLE; From 8e47a3b5d6c24fa48ecec3782f1e61fc7033ccff Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:21:12 -0300 Subject: [PATCH 1265/1360] test(agc): cover copied-image allocation recovery --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../tests/execution/SampledCacheBudget.cpp | 57 +++++++++++++++---- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 08f96fd7ad..52b265a546 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2786,7 +2786,7 @@ if(BUILD_TESTING) set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() - foreach(imageRecoveryMode IN ITEMS sampled storage) + foreach(imageRecoveryMode IN ITEMS sampled storage srgb repack) add_test(NAME agc_driver_recover_${imageRecoveryMode}_allocation COMMAND agc_driver_sampled_cache_budget_tests recover-${imageRecoveryMode}) set_tests_properties(agc_driver_recover_${imageRecoveryMode}_allocation PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 93ef5d147f..2fe3e34065 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -327,10 +327,10 @@ private: std::uint8_t* memory = nullptr; }; -std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side) { +std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side, std::uint32_t format = Format32UInt) { return { static_cast<std::uint32_t>(address >> 8u), - static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((side - 1u) & 3u) << 30u), ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, @@ -340,13 +340,13 @@ std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t sid }; } -std::shared_ptr<Texture> Sampled(const Context& context, std::uint64_t address, std::uint32_t side = Side) { - const auto words = Descriptor(address, side); +std::shared_ptr<Texture> Sampled(const Context& context, std::uint64_t address, std::uint32_t side = Side, std::uint32_t format = Format32UInt) { + const auto words = Descriptor(address, side, format); return CachedSampledTexture(context, words); } -GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) { - const auto words = Descriptor(address, side); +GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side, std::uint32_t format = Format32UInt) { + const auto words = Descriptor(address, side, format); return DecodeTextureResource(words); } @@ -1039,13 +1039,14 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL RecoveryProc(VkDevice device, const cha return recovery.deviceProc(device, name); } -void recoveryTests(Context context, bool storage) { +void recoveryTests(Context context, const std::string& mode) { recovery = RecoveryProbe{}; recovery.deviceProc = context.deviceProc; context.deviceProc = RecoveryProc; context.cacheReclaim = ReclaimForTest; context.bufferPool.reset(); Block block(context); + const bool storage = mode == "recover-storage"; if (storage) { auto oldest = CachedStorageSurface(context, Resource(block.Surface(0))); const auto allocated = oldest->AllocationBytes(); @@ -1061,7 +1062,7 @@ void recoveryTests(Context context, bool storage) { Require(recovery.released == allocated, "storage allocation recovery counted memory still held by its caller"); pending->Flush(); Require(!pending->Dirty(), "pending storage results could not be stored after memory recovery"); - } else { + } else if (mode == "recover-sampled") { auto oldest = Sampled(context, block.Surface(0)); Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); const auto allocated = oldest->AllocationBytes(); @@ -1074,9 +1075,43 @@ void recoveryTests(Context context, bool storage) { Require(made != nullptr && released.expired(), "sampled allocation recovery did not release its own oldest unused texture"); Require(Sampled(context, block.Surface(1)) == held, "sampled allocation recovery removed a texture still held by its caller"); Require(recovery.released == allocated, "sampled allocation recovery counted memory still held by its caller"); + } else { + const bool srgb = mode == "recover-srgb"; + if (srgb) { + context.srgbDecodeFormats = 0; + context.srgbCopyFormats |= 1u; + } + auto oldest = Sampled(context, block.Surface(0)); + Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); + const auto allocated = oldest->AllocationBytes(); + std::weak_ptr<Texture> released = oldest; + oldest.reset(); + auto held = Sampled(context, block.Surface(1)); + const auto side = srgb ? 1024u : 512u; + const auto format = srgb ? 128u : 43u; + const auto sourceWords = Descriptor(block.Surface(4), side, format); + const auto sourceResource = DecodeTextureResource(sourceWords); + auto source = CachedStorageSurface(context, sourceResource); + source->MarkDirty(); + if (srgb) { + Require(Texture::CanSrgbExpandFrom(context, *source, sourceResource), "the storage surface cannot serve an sRGB expansion copy"); + } else { + Require(Texture::CanRepackCopyFrom(*source, sourceResource), "the storage surface cannot serve a repacked copy"); + } + recovery.pressure = true; + auto made = CachedSampledTexture(context, sourceWords); + recovery.pressure = false; + Require(made != nullptr && made->SharedStorageSource() == source, "sampled copy allocation recovery did not serve the pending storage image"); + Require(released.expired(), "sampled copy allocation recovery did not release its own oldest unused texture"); + Require(source->Dirty() && StorageImageCached(context, source.get()), "sampled copy allocation recovery changed or evicted an image with pending results"); + Require(Sampled(context, block.Surface(1)) == held, "sampled copy allocation recovery removed a texture still held by its caller"); + Require(recovery.released == allocated, "sampled copy allocation recovery counted memory still held by its caller"); + source->Flush(); + Require(!source->Dirty(), "pending storage results could not be stored after memory recovery"); } Require(recovery.requests == 1 && recovery.failures >= 1 && recovery.failures <= 2, "image allocation recovery was skipped or retried without a bound"); - std::printf("%s image allocation recovered after %u injected failures: %llu bytes released\n", storage ? "storage" : "sampled", recovery.failures, static_cast<unsigned long long>(recovery.released)); + const char* label = storage ? "storage" : mode == "recover-srgb" ? "sRGB" : mode == "recover-repack" ? "repacked" : "sampled"; + std::printf("%s image allocation recovered after %u injected failures: %llu bytes released\n", label, recovery.failures, static_cast<unsigned long long>(recovery.released)); } } @@ -1091,7 +1126,7 @@ int main(int argc, char** argv) { } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; - const bool recover = mode == "recover-sampled" || mode == "recover-storage"; + const bool recover = mode == "recover-sampled" || mode == "recover-storage" || mode == "recover-srgb" || mode == "recover-repack"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1"); if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); @@ -1158,7 +1193,7 @@ int main(int argc, char** argv) { } if (recover) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - recoveryTests(context, mode == "recover-storage"); + recoveryTests(context, mode); return 0; } { From 042a53079da2cc3106103d424731d667b2728c00 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:50:13 -0300 Subject: [PATCH 1266/1360] test(agc): select supported sRGB copy profile --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 52b265a546..30830985c7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2794,6 +2794,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() endforeach() + set_property(TEST agc_driver_recover_srgb_allocation APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_SRGB_SHADER_DECODE=set:copy") add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 2fe3e34065..bc73221190 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" @@ -1078,8 +1079,9 @@ void recoveryTests(Context context, const std::string& mode) { } else { const bool srgb = mode == "recover-srgb"; if (srgb) { - context.srgbDecodeFormats = 0; - context.srgbCopyFormats |= 1u; + context.srgbDecodeFormats = SrgbDecodeFormats(context.formatProperties, context.physical); + context.srgbCopyFormats = SrgbCopyFormats(context.formatProperties, context.physical, context.srgbDecodeFormats); + Require(SrgbExpandsToRgba8(context, 128u), "the device cannot serve the sRGB expansion copy fixture"); } auto oldest = Sampled(context, block.Surface(0)); Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); From 8c521d7d1e66eedac4e36c6121c700dc4a6bf2ed Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:22:15 -0300 Subject: [PATCH 1267/1360] fix(agc): recover initial sampled image copies --- .../libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 4 ++-- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 09f1984c79..b7bcf3311a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -925,10 +925,10 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s entry.texture = createTexture(source, resource, components, Texture::BlockCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (IsRepackedTextureFormat(resource.format)) { - entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::RepackCopyTag{}); + entry.texture = createTexture(source, resource, components, Texture::RepackCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (srgbExpand) { - entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::SrgbExpandTag{}); + entry.texture = createTexture(source, resource, components, Texture::SrgbExpandTag{}); entry.texture->RefreshDepthCopy(); } else { entry.texture = createTexture(source, resource, components); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 5060743d4a..85a8466970 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -282,7 +282,7 @@ ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); + result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy", true); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); result.bytes = requirements.size; return result; @@ -642,7 +642,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy", true); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b573617dbf..20f6a75f9b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -69,7 +69,7 @@ - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. -- [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. Fault-injection fixtures passed on Windows 11 (AMD RX 6700 XT, AMD Vulkan 26.8.1); the integrated image-allocation wrapper passed on Linux (AMD RX 9070 XT, RADV). Other hardware driver stacks are unverified. +- [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. From 91b1fd1c6b3d78b491eb0f9dfbeb84a33c493f33 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:25:04 -0300 Subject: [PATCH 1268/1360] test(agc): check retention before cache refresh --- .../libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index bc73221190..6c414d6cda 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1106,7 +1106,9 @@ void recoveryTests(Context context, const std::string& mode) { Require(made != nullptr && made->SharedStorageSource() == source, "sampled copy allocation recovery did not serve the pending storage image"); Require(released.expired(), "sampled copy allocation recovery did not release its own oldest unused texture"); Require(source->Dirty() && StorageImageCached(context, source.get()), "sampled copy allocation recovery changed or evicted an image with pending results"); - Require(Sampled(context, block.Surface(1)) == held, "sampled copy allocation recovery removed a texture still held by its caller"); + Require(!held->Released() && held.use_count() == 2, "sampled copy allocation recovery removed a texture still held by its caller"); + const auto refreshed = Sampled(context, block.Surface(1)); + Require(refreshed != nullptr && (refreshed == held || refreshed->ViewsStorageImage()), "sampled copy cache refresh did not serve the surface"); Require(recovery.released == allocated, "sampled copy allocation recovery counted memory still held by its caller"); source->Flush(); Require(!source->Dirty(), "pending storage results could not be stored after memory recovery"); From 5836873170754a1cce584944b6dd1fb17d3ad3ad Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:15:29 +0200 Subject: [PATCH 1269/1360] fix(memory): honor low guest mapping hints --- core/libs/prx/libc/src/GuestArena.cpp | 15 ++++- core/libs/prx/libkernel/CMakeLists.txt | 12 ++++ .../libkernel/DirectMemory/PosixMemory.cpp | 2 +- core/libs/tests/GuestLowAddressMemory.cpp | 66 +++++++++++++++++++ core/libs/tests/GuestMemory.cpp | 8 ++- docs/dev/debt/libc.md | 2 +- docs/dev/debt/libkernel.md | 2 +- 7 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 core/libs/tests/GuestLowAddressMemory.cpp diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 20cdbc1820..6a3f41cfc3 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -46,7 +46,12 @@ void noteAccess(const void* pointer, std::size_t bytes) { } constexpr std::uintptr_t GuestArenaOffset = GuestArena::GuestAddressOffset; -constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; +constexpr std::uintptr_t DefaultAllocationStart = 0x0000000200000000ull + GuestArenaOffset; +#ifdef __APPLE__ +constexpr std::uintptr_t ArenaStart = DefaultAllocationStart; +#else +constexpr std::uintptr_t ArenaStart = GuestArenaOffset == 0 ? 0x10000ull : DefaultAllocationStart; +#endif constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull + GuestArenaOffset; @@ -86,7 +91,7 @@ public: RequireAllocatable(alignment); std::lock_guard lock(_lock); auto candidate = hint < _end ? FindAtOrAbove(hint, bytes, alignment) : 0; - if (candidate == 0 && hint > _base) candidate = FindAtOrAbove(_base, bytes, alignment); + if (candidate == 0 && hint > _base) candidate = FindAtOrAbove(hint < DefaultAllocationStart ? _base : DefaultAllocationStart, bytes, alignment); return Claim(candidate, bytes); } @@ -273,6 +278,10 @@ private: const int failed = ReserveNoReplace(cursor, end - cursor); if (failed == 0) continue; if (failed != EEXIST) { + if (cursor < DefaultAllocationStart && (failed == EACCES || failed == EPERM)) { + occupied.emplace_back(cursor, end); + continue; + } char message[128]; std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx", static_cast<unsigned long long>(cursor), static_cast<unsigned long long>(end - cursor)); @@ -309,7 +318,7 @@ private: } std::uintptr_t FindAtOrAbove(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) const { - std::uintptr_t candidate = alignUp(std::max(_base, hint), alignment); + std::uintptr_t candidate = alignUp(hint == 0 ? DefaultAllocationStart : std::max(_base, hint), alignment); auto it = _used.upper_bound(candidate); if (it != _used.begin() && std::prev(it)->second > candidate) --it; for (; it != _used.end(); ++it) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2f39251297..aa785ec361 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1621,6 +1621,18 @@ if(BUILD_TESTING) set_tests_properties(guest_memory guest_mmap_fixed_anonymous PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") endif() + if(NOT ANYPS5_ENABLE_ASAN AND (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")) + add_test_executable(guest_low_address_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestLowAddressMemory.cpp) + target_include_directories(guest_low_address_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_low_address_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_low_address_memory_tests) + add_test(NAME guest_low_address_memory COMMAND guest_low_address_memory_tests) + set_tests_properties(guest_low_address_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_low_address_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") + endif() + endif() add_test_executable(guest_mprotect_unmapped_tests ${LIBS_INCLUDE_DIR}/tests/GuestMprotectUnmapped.cpp) target_include_directories(guest_mprotect_unmapped_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mprotect_unmapped_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index c540f34c99..8f449c1b16 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -171,7 +171,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti #endif const bool exclusive = fixedAnonymous && (mappingFlags & GuestMapExclusive) != 0; const int placement = fixedAnonymous ? GuestMapFixed | (exclusive ? GuestMapNoOverwrite : 0) : 0; - void* mapped = fixedAnonymous ? address : nullptr; + void* mapped = address; const auto alignment = !fixedAnonymous && (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; try { const auto result = DoMapAnon(&mapped, rounded, protection, placement, alignment, exclusive, fixedAnonymous); diff --git a/core/libs/tests/GuestLowAddressMemory.cpp b/core/libs/tests/GuestLowAddressMemory.cpp new file mode 100644 index 0000000000..8627e2bf2d --- /dev/null +++ b/core/libs/tests/GuestLowAddressMemory.cpp @@ -0,0 +1,66 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <source_location> + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Low guest memory check failed at line %u\n", static_cast<unsigned>(location.line())); + std::abort(); + } +} + +int main() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t bufferBytes = 64u << 20; + constexpr std::size_t stackBytes = 128u << 20; + constexpr std::uintptr_t lowLimit = 0x80000000; + const auto failed = reinterpret_cast<void*>(~std::uintptr_t{0}); + auto* buffer = static_cast<unsigned char*>(mmap_nid_postfix(reinterpret_cast<void*>(0x20000000), bufferBytes, 3, 0x1002, -1, 0)); + auto* stack = static_cast<unsigned char*>(mmap_nid_postfix(reinterpret_cast<void*>(0x40000000), stackBytes, 3, 0x1002, -1, 0)); + std::fprintf(stderr, "Low guest memory: buffer=%p stack=%p\n", static_cast<void*>(buffer), static_cast<void*>(stack)); + Require(buffer != failed && stack != failed); + const auto bufferAddress = reinterpret_cast<std::uintptr_t>(buffer); + const auto stackAddress = reinterpret_cast<std::uintptr_t>(stack); + Require(bufferAddress >= 0x20000000 && bufferAddress <= lowLimit - bufferBytes); + Require(stackAddress >= 0x40000000 && stackAddress <= lowLimit - stackBytes); + Require(GuestArena::GuestArenaContains_nid_postfix(buffer, bufferBytes)); + Require(GuestArena::GuestArenaContains_nid_postfix(stack, stackBytes)); + buffer[0] = 0x31; + buffer[bufferBytes - 1] = 0x32; + stack[0] = 0x41; + stack[stackBytes - 1] = 0x42; + + void* collision = mmap_nid_postfix(buffer, page, 3, 0x1002, -1, 0); + Require(collision != failed && collision != buffer); + Require(buffer[0] == 0x31 && buffer[bufferBytes - 1] == 0x32); + Require(munmap_nid_postfix(collision, page) == 0); + Require(mmap_nid_postfix(buffer, page, 3, 0x5012, -1, 0) == failed && buffer[0] == 0x31); + Require(mmap_nid_postfix(buffer, page, 3, 0x1012, -1, 0) == buffer); + Require(buffer[0] == 0 && buffer[bufferBytes - 1] == 0x32); + + Require(mprotect_nid_postfix(buffer, page, 1) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(buffer, 0, &info, sizeof(info)) == 0 && info.protection == 1); + Require(mprotect_nid_postfix(buffer, page, 3) == 0); + buffer[0] = 0x51; + + void* ordinary = mmap_nid_postfix(nullptr, page, 3, 0x1002, -1, 0); + Require(ordinary != failed && reinterpret_cast<std::uintptr_t>(ordinary) >= 0x200000000ull); + Require(munmap_nid_postfix(ordinary, page) == 0); + Require(munmap_nid_postfix(buffer, bufferBytes) == 0); + Require(munmap_nid_postfix(stack, stackBytes) == 0); + Require(mmap_nid_postfix(buffer, page, 3, 0x1002, -1, 0) == buffer); + Require(buffer[0] == 0); + Require(munmap_nid_postfix(buffer, page) == 0); +} diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d03a35de70..b7238e7a70 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1441,7 +1441,13 @@ static void CheckFixedMappingsReachTheApplicationAreaEnd() { 0; #endif constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull + expectedOffset; - Require(base == 0x200000000ull + expectedOffset && base + size == applicationAreaEnd); + constexpr std::uintptr_t expectedBase = +#ifdef __APPLE__ + 0x200000000ull + expectedOffset; +#else + expectedOffset == 0 ? 0x10000ull : 0x200000000ull + expectedOffset; +#endif + Require(base == expectedBase && base + size == applicationAreaEnd); std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); void* const requested = reinterpret_cast<void*>(applicationAreaEnd - page * 2); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2ec10f5024..730ef9b09b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -18,7 +18,7 @@ - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting -- [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 +- [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. The console's low-address layout is not measured. Linux and Windows reserve free addresses from 64 KiB, except host mappings and low chunks the Linux address policy refuses; null hints still allocate from 8 GiB. macOS and ASan builds still start the arena at 8 GiB and cannot supply low 32-bit addresses. Guest thread creation ignores a stack address set in its attributes. A host-loaded executable is not moved, so a PIE above the arena gets no memory within 2 GiB of its code ([#5217](https://github.com/boykopovar/AnyPS5/issues/5217)) - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..f909081b56 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -75,7 +75,7 @@ - [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in PPSA21564, through `sceKernelMapFlexibleMemory` in PPSA22520 01.000.011 - [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 - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints -- [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. +- [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. Linux and Windows accept addresses from 64 KiB that no host mapping occupies, such as the 0x400000 imported by PICO-8 PPSA99808; macOS keeps the 0x200000000 start. Non-fixed private anonymous mappings keep their hint; null hints still use the 8 GiB area. On Windows the host's `KUSER_SHARED_DATA` always occupies 0x7ffe0000, so Wine's shared user data page (Forever Launcher PPSA99030) cannot be placed there. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown From 16d2cbb4b38534805b7e4926f210b86ff00b599a Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 17:05:30 +0200 Subject: [PATCH 1270/1360] fix(libSceRtc): range-check by the compiled SDK version as the PS4 does Measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001. From 0x03000000 on, FormatRFC3339 returns INVALID_VALUE when the zone shift overflows or underflows the tick, GetDayOfWeek checks the month before the year and rejects years past 9999, and SetTime_t rejects negative seconds and converts all 64 bits. 0x02ffffff and earlier keep the wrap, the year-first order and the low 32 bits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceRtc/CMakeLists.txt | 1 + core/libs/prx/libSceRtc/Export.cpp | 27 ++++++++++++++-- core/libs/tests/GuestRtc.cpp | 44 ++++++++++++++++++++++++++ docs/dev/debt/libSceRtc.md | 1 + 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceRtc/CMakeLists.txt b/core/libs/prx/libSceRtc/CMakeLists.txt index e0b93b163e..689ae6f279 100644 --- a/core/libs/prx/libSceRtc/CMakeLists.txt +++ b/core/libs/prx/libSceRtc/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceRtc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_link_libraries(libSceRtc PRIVATE libc) target_include_directories(libSceRtc PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceRtc) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index a1dffea9ac..4b85e9449b 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -7,6 +7,7 @@ #include <limits> #include <mutex> #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -31,6 +32,7 @@ constexpr std::int64_t WIN32_EPOCH_DAYS = 584388; constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; +constexpr std::uint32_t RANGE_CHECKED_SDK_VERSION = 0x03000000; struct ClockState { std::uint64_t tick = 0; @@ -100,6 +102,13 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast<std::uint16_t>(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast<std::uint32_t>(timeOfDay % TICKS_PER_SECOND)}; } +bool rangeChecked() { + const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version >= RANGE_CHECKED_SDK_VERSION; +} + std::uint64_t currentTick(std::size_t clock = 0) { ClockState state; { @@ -370,8 +379,13 @@ int APS5_VABI sceRtcGetDaysInMonth(int year, int month) { } int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { - if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; - if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (rangeChecked()) { + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + } else { + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + } if (day < 1 || day > daysInMonth(year, month)) return SCE_RTC_ERROR_INVALID_DAY; return static_cast<int>((daysFromCivil(year, static_cast<unsigned>(month), static_cast<unsigned>(day)) + 1) % 7); } @@ -451,6 +465,11 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (rangeChecked()) { + if (seconds < 0) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(UNIX_EPOCH_TICK + static_cast<std::uint64_t>(seconds) * TICKS_PER_SECOND); + return 0; + } *time = fromTick(UNIX_EPOCH_TICK + static_cast<std::uint64_t>(static_cast<std::uint32_t>(seconds)) * TICKS_PER_SECOND); return 0; } @@ -590,7 +609,9 @@ int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZo utc = &now; } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; - const RtcTick local{utc->tick + static_cast<std::uint64_t>(static_cast<std::int64_t>(timeZoneMinutes) * TICKS_PER_MINUTE)}; + const auto shift = static_cast<std::int64_t>(timeZoneMinutes) * TICKS_PER_MINUTE; + if (rangeChecked() && (shift < 0 ? utc->tick < static_cast<std::uint64_t>(-shift) : utc->tick > std::numeric_limits<std::uint64_t>::max() - static_cast<std::uint64_t>(shift))) return SCE_RTC_ERROR_INVALID_VALUE; + const RtcTick local{utc->tick + static_cast<std::uint64_t>(shift)}; if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 1746ac5f31..1d72b948e4 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include <array> +#include <cstddef> #include <cstdint> #include <cstdlib> #include <cstring> @@ -113,7 +116,14 @@ static int CallAdd(Add function, RtcTick* destination, const RtcTick* source, st std::abort(); } +static void SetCompiledSdkVersion(std::byte* process, std::uint32_t version) { + std::memcpy(process + 16, &version, sizeof(version)); +} + int main() { + static std::array<std::byte, 64> process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + SetCompiledSdkVersion(process.data(), 0x01000051); constexpr int invalidPointer = static_cast<int>(0x80B50002); constexpr int invalidValue = static_cast<int>(0x80B50003); constexpr int badParse = static_cast<int>(0x80B50007); @@ -952,6 +962,40 @@ int main() { Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } + SetCompiledSdkVersion(process.data(), 0x02ffffff); + Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidYear && sceRtcGetDayOfWeek(10000, 1, 1) == 6); + SetCompiledSdkVersion(process.data(), 0x03000000); + Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidMonth && sceRtcGetDayOfWeek(10000, 1, 1) == invalidYear); + SetCompiledSdkVersion(process.data(), 0x05050001); + Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidMonth && sceRtcGetDayOfWeek(10000, 13, 1) == invalidMonth && sceRtcGetDayOfWeek(2024, 0, 1) == invalidMonth); + Require(sceRtcGetDayOfWeek(0, 1, 0) == invalidYear && sceRtcGetDayOfWeek(10000, 2, 30) == invalidYear && sceRtcGetDayOfWeek(65536, 1, 1) == invalidYear); + Require(sceRtcGetDayOfWeek(2024, 2, 30) == invalidDay && sceRtcGetDayOfWeek(9999, 12, 31) == 5 && sceRtcGetDayOfWeek(1, 1, 1) == 1); + Require(sceRtcGetDaysInMonth(0, 13) == invalidYear && sceRtcGetDaysInMonth(10000, 1) == 31 && sceRtcIsLeapYear(10000) == 1); + RtcDateTime sentinel{0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaaaaaau}; + RtcDateTime ranged = sentinel; + Require(sceRtcSetTime_t(&ranged, -1) == invalidValue && Equal(ranged, sentinel)); + Require(sceRtcSetTime_t(&ranged, std::numeric_limits<std::int64_t>::min()) == invalidValue && Equal(ranged, sentinel)); + Require(sceRtcSetTime_t(&ranged, 4294967296ll) == 0 && Equal(ranged, RtcDateTime{2106, 2, 7, 6, 28, 16, 0})); + Require(sceRtcSetTime_t(&ranged, 253402300800ll) == 0 && Equal(ranged, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); + Require(sceRtcSetTime_t(&ranged, 18446744073709ll) == 0 && Equal(ranged, RtcDateTime{1969, 12, 31, 23, 59, 59, 448384})); + Require(sceRtcSetTime_t(&ranged, std::numeric_limits<std::int64_t>::max()) == 0 && Equal(ranged, RtcDateTime{1969, 12, 31, 23, 59, 59, 0})); + char ranges[64]; + RtcTick shifted{0}; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == invalidValue); + shifted.tick = 59999999; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == invalidValue); + shifted.tick = 60000000; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == 0 && std::strcmp(ranges, "0001-01-01T00:00:00.00-00:01") == 0); + shifted.tick = std::numeric_limits<std::uint64_t>::max(); + Require(sceRtcFormatRFC3339(ranges, &shifted, 1) == invalidValue && sceRtcFormatRFC3339(ranges, &shifted, 1439) == invalidValue); + shifted.tick = std::numeric_limits<std::uint64_t>::max() - 60000000 + 1; + Require(sceRtcFormatRFC3339(ranges, &shifted, 1) == invalidValue); + shifted.tick = std::numeric_limits<std::uint64_t>::max() - 60000000; + Require(sceRtcFormatRFC3339(ranges, &shifted, 1) == invalidYear); + shifted.tick = 1ull << 63; + Require(sceRtcFormatRFC3339(ranges, &shifted, 0) == invalidYear && sceRtcFormatRFC3339(ranges, &shifted, -1) == invalidYear); + shifted.tick = maxTick + 1; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == 0 && std::strcmp(ranges, "9999-12-31T23:59:00.00-00:01") == 0); Require(sceRtcInit() == 0); Require(sceRtcEnd() == 0); } diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index affc0d5d23..dbdbc7f26d 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -8,6 +8,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 +- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 2a05015c2a15917516d6c13422e47b4c08b05b18 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 12:21:25 -0300 Subject: [PATCH 1271/1360] fix(libkernel): refuse long thread names and answer a detach after a join as measured on a PS5 scePthreadCreate returns ENAMETOOLONG for a name of 32 bytes or more without creating the thread, and scePthreadDetach returns ESRCH for a joined thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 5 ++++- core/libs/tests/GuestPthreadSecondJoin.cpp | 19 ++++++++++++++++++- docs/dev/debt/libkernel.md | 3 ++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 1d2fecb349..fdc3881ebc 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -35,6 +35,7 @@ #endif static constexpr int SCE_OK = 0; +static constexpr std::size_t GUEST_THREAD_NAME_CAPACITY = 32; static constexpr std::size_t HOST_STACK_MARGIN = 1u << 20; static constexpr int DETACH_DETACHED = 1; @@ -820,6 +821,7 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg"); + if (name && std::strlen(name) >= GUEST_THREAD_NAME_CAPACITY) return SCE_KERNEL_ERROR_ENAMETOOLONG; if (attr && !*attr) attr = nullptr; auto p = std::make_unique<PthreadPrivate>(attr ? *attr : nullptr); bool detached = false; @@ -988,7 +990,8 @@ int APS5_VABI scePthreadDetach(Pthread thread) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; { std::lock_guard lock(thread->_join_mtx); - if (thread->_detached || thread->_joining || thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; + if (thread->_detached || thread->_joining) return SCE_KERNEL_ERROR_EINVAL; thread->_detached = true; } #ifdef _WIN32 diff --git a/core/libs/tests/GuestPthreadSecondJoin.cpp b/core/libs/tests/GuestPthreadSecondJoin.cpp index 4db4d57034..0173a7bd2c 100644 --- a/core/libs/tests/GuestPthreadSecondJoin.cpp +++ b/core/libs/tests/GuestPthreadSecondJoin.cpp @@ -3,18 +3,21 @@ #include <cstddef> #include <cstdint> #include <cstdlib> +#include <cstring> #include <thread> extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadDetach(Pthread thread); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast<int>(0x80020003u); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = static_cast<int>(0x8002003fu); static constexpr int GUEST_ESRCH = 3; static constexpr std::size_t REUSED_SLOT_ROUNDS = 64; @@ -40,7 +43,7 @@ int main() { result = reinterpret_cast<void*>(std::uintptr_t{0x55}); Require(scePthreadJoin(thread, &result) == SCE_KERNEL_ERROR_ESRCH); Require(result == reinterpret_cast<void*>(std::uintptr_t{0x55})); - Require(scePthreadDetach(thread) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadDetach(thread) == SCE_KERNEL_ERROR_ESRCH); Require(pthread_join_nid_postfix(thread, &result) == GUEST_ESRCH); Require(result == reinterpret_cast<void*>(std::uintptr_t{0x55})); @@ -62,4 +65,18 @@ int main() { Require(result == reinterpret_cast<void*>(std::uintptr_t{0x55})); Require(scePthreadDetach(detached) == SCE_KERNEL_ERROR_EINVAL); release = true; + + Pthread named = nullptr; + char name[64] = {}; + Require(scePthreadCreate(&named, nullptr, Finish, nullptr, "0123456789abcdefghijklmnopqrstu") == SCE_OK); + Require(scePthreadGetname(named, name) == SCE_OK && std::strcmp(name, "0123456789abcdefghijklmnopqrstu") == 0); + Require(scePthreadJoin(named, nullptr) == SCE_OK); + Pthread untouched = reinterpret_cast<Pthread>(std::uintptr_t{0x77}); + Require(scePthreadCreate(&untouched, nullptr, Finish, nullptr, "0123456789abcdefghijklmnopqrstuv") == SCE_KERNEL_ERROR_ENAMETOOLONG); + Require(untouched == reinterpret_cast<Pthread>(std::uintptr_t{0x77})); + Pthread unnamed = nullptr; + std::memset(name, 'x', sizeof(name)); + Require(scePthreadCreate(&unnamed, nullptr, Finish, nullptr, nullptr) == SCE_OK); + Require(scePthreadGetname(unnamed, name) == SCE_OK && name[0] == 0); + Require(scePthreadJoin(unnamed, nullptr) == SCE_OK); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..aeb843b727 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -170,7 +170,8 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it -- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. Detaching a thread while another thread waits to join it returns `EINVAL`, and a second concurrent join returns `EOPNOTSUPP`, as FreeBSD 11.4's [`thr_detach.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_detach.c) and [`thr_join.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_join.c) check `joiner`; before this PPSA34015 stopped once with `Joining guest thread: Access is denied` when a thread it was joining was released under the join, and which of the two calls released it was not captured. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory +- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console +- [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread, 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here. Whether the handle is written before the name check was not measured; here it is left alone - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From 3d4fadbcae49ef23bc9a7fb0345e18b4f8518c0f Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 12:23:42 -0300 Subject: [PATCH 1272/1360] fix(libSceUlt): keep ulthread names within the pthread name limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceUlt/Export.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index f876cfba21..cc3c25c60a 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -10,6 +10,7 @@ #include <memory> #include <mutex> #include <stdexcept> +#include <string> #include <unordered_map> #include <vector> @@ -818,7 +819,8 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread std::memset(ulthread, 0, 512); gUlthreads[ulthread] = state; } - const int result = scePthreadCreate(&state->_thread, nullptr, ulthreadRunner, state.get(), name != nullptr ? name : ""); + const std::string threadName = name != nullptr ? std::string(name).substr(0, 31) : std::string(); + const int result = scePthreadCreate(&state->_thread, nullptr, ulthreadRunner, state.get(), threadName.c_str()); if (result != 0) { std::lock_guard<std::mutex> lock(gMutex); gUlthreads.erase(ulthread); From 54158953772526ea3dbdd21d53d93d461d153f38 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 12:28:46 -0300 Subject: [PATCH 1273/1360] docs(libc): record reallocf as measured on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 561aced31f..efc1981c97 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -24,7 +24,7 @@ - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, as a payload process on a PS5 (firmware 13.60) does with the default heap (null and 0, size 0, 2^40 with `errno` 22 and the block freed, `SIZE_MAX` with `errno` untouched): size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception From 0ae3cbcafb2131e07e6e592cc64ef7083eb322b7 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:39:10 +0800 Subject: [PATCH 1274/1360] fix(libScePad): retain explicit throw for unsupported Lotus IR and virtual-device exports --- core/libs/prx/libScePad/Export.cpp | 25 +++++++++++---------- core/libs/tests/GuestPad.cpp | 35 ++++++++++++++++++++---------- docs/dev/debt/libScePad.md | 3 --- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index dd6c2394f3..27321c0efd 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -22,10 +22,7 @@ struct PadUsbDetectionState; struct PadVrUsbConnectionStatus; struct PadDeviceInfo; struct PadLicenseControllerInformation; -struct PadRemoteSetting { - std::uint8_t data[64]; -}; -static_assert(sizeof(PadRemoteSetting) == 64); +struct PadRemoteSetting; struct PadVirtualTriggerSetting; struct PadProcessFocusParameter; struct PadDataExtended; @@ -666,13 +663,15 @@ int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* r } int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t handle) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - return PAD_OK; + (void)handle; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - return PAD_OK; + (void)handle; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadGetUsbDetectionState(std::int32_t deviceId, PadUsbDetectionState* state) { @@ -1021,8 +1020,10 @@ int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(std::int32_t deviceI } int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t deviceId, PadRemoteSetting* setting) { - if (setting == nullptr || deviceId <= 0) return PAD_ERROR_INVALID_ARG; - return PAD_ERROR_DEVICE_NOT_CONNECTED; + (void)deviceId; + (void)setting; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, const PadVirtualTriggerSetting* setting) { @@ -1031,7 +1032,9 @@ int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, co } int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool disabled) { - return PAD_OK; + (void)disabled; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 2783ffce72..cd4bf6a6d7 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -9,6 +9,7 @@ #include <cstring> #include <filesystem> #include <fstream> +#include <stdexcept> #include <string> #include <thread> #include <utility> @@ -46,6 +47,16 @@ int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t, void*); static void Require(bool value) { if (!value) std::abort(); } +template<typename TCall> +static bool Throws(TCall call, const char* function) { + try { + static_cast<void>(call()); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), function) != nullptr; + } + return false; +} + static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -497,6 +508,8 @@ int main() { Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); + Require(Throws([&] { return scePadSetLotusEnableIrKey(handle); }, "scePadSetLotusEnableIrKey")); + Require(Throws([&] { return scePadSetLotusDisableIrKey(handle); }, "scePadSetLotusDisableIrKey")); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); @@ -506,15 +519,15 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); - Require(scePadSetLotusEnableIrKey(handle) == PAD_OK); - Require(scePadSetLotusEnableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); - Require(scePadSetLotusDisableIrKey(handle) == PAD_OK); - Require(scePadSetLotusDisableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); - Require(scePadVirtualDeviceDisableButtonRemapping(true) == PAD_OK); - Require(scePadVirtualDeviceDisableButtonRemapping(false) == PAD_OK); - std::array<std::uint8_t, 64> remoteSetting{}; - Require(scePadVirtualDeviceGetRemoteSetting(1, remoteSetting.data()) == static_cast<int>(0x80920007)); - Require(scePadVirtualDeviceGetRemoteSetting(1, nullptr) == PAD_ERROR_INVALID_ARG); - Require(scePadVirtualDeviceGetRemoteSetting(0, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); - Require(scePadVirtualDeviceGetRemoteSetting(-1, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); + Require(Throws([&] { return scePadSetLotusEnableIrKey(handle + 1); }, "scePadSetLotusEnableIrKey")); + Require(Throws([&] { return scePadSetLotusDisableIrKey(handle + 1); }, "scePadSetLotusDisableIrKey")); + Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(true); }, "scePadVirtualDeviceDisableButtonRemapping")); + Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(false); }, "scePadVirtualDeviceDisableButtonRemapping")); + std::array<std::uint8_t, 16> scratch{}; + scratch.fill(0xAB); + Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); + Require(Throws([] { return scePadVirtualDeviceGetRemoteSetting(1, nullptr); }, "scePadVirtualDeviceGetRemoteSetting")); + Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(0, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); + Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(-1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); + for (std::uint8_t byte : scratch) Require(byte == 0xAB); } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index e740790bfe..c8fa44cf9d 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -3,8 +3,6 @@ ### Silent stubs - [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing extension-port state. The boolean argument is recovered; connected-device behavior is not modeled. -- [scePadSetLotusEnableIrKey and scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) return success on valid open handle without hardware Lotus IR key dispatching. -- [scePadVirtualDeviceDisableButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing virtual-device remapping tables. ### Unknown function info @@ -22,7 +20,6 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - checks the output pointer and non-zero device id, returning `PAD_ERROR_DEVICE_NOT_CONNECTED` (0x80920007) when no virtual device is connected. Remote setting payload uses a 64-byte record whose internal field meanings remain unverified on hardware. - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. From 81820edf308cf54985a9bb5636ee327436cb19d2 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 18:48:42 +0300 Subject: [PATCH 1275/1360] fix(tools): reconfigure streams with backslashreplace in unwrap_self Reconfigure sys.stdout and sys.stderr with backslashreplace error handling at the entrypoint of main() so unwrapping reports and error messages do not fail on Windows consoles configured with non-UTF-8 code pages. --- tools/tests/test_unwrap_self.py | 28 ++++++++++++++++++++++++++++ tools/unwrap_self.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index f30eb5a74f..964d6bc4af 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -779,5 +779,33 @@ class UnwrapTest(unittest.TestCase): self.assertTrue(output.exists()) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(unwrap_self.sys, "stdout", out), mock.patch.object(unwrap_self.sys, "stderr", err): + try: + unwrap_self.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 659f4eb07a..67261bc79d 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -336,7 +336,7 @@ def unwrap(path, output, verbose=False): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="unwrap a decrypted SELF or fSELF container into a clean ELF") parser.add_argument("input", help="SELF container to read") parser.add_argument("output", help="ELF executable to write") From dd45b07ce119ddc7d0862ceb9011e6ef33845048 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 18:57:48 +0300 Subject: [PATCH 1276/1360] fix(tools): reconfigure streams with backslashreplace in select_tests Reconfigure sys.stdout and sys.stderr with backslashreplace error handling at the entrypoint of main() so test graph dumping and change diagnostics do not fail on Windows consoles configured with non-UTF-8 code pages. --- tools/select_tests.py | 2 +- tools/tests/test_select_tests.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 80005e556d..6d8733a62c 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -263,7 +263,7 @@ def escape(name): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") sub = parser.add_subparsers(dest="command", required=True) d = sub.add_parser("dump", help="write the test graph of a built tree") diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index b89ba0904a..07283fe840 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -392,5 +392,33 @@ class MissingCommandTests(unittest.TestCase): "ambiguous": None, "existing": [helper], "stale": None}) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(select_tests.sys, "stdout", out), mock.patch.object(select_tests.sys, "stderr", err): + try: + select_tests.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From 21200356110d43762f4e3cb563a9cedc4e7d6f9f Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 18:01:16 +0200 Subject: [PATCH 1277/1360] fix(libSceRtc): keep the pre-3.00 range rules to PS4 titles The compiled SDK version of a PS5 title counts from its own SDK, so an early PS5 title would have taken the PS4 pre-3.00 rules. Apply them only to a CUSA title, as libSceConvertKeycode tells PS4 titles apart; anything else, including a title without metadata, gets the later rules main had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceRtc/CMakeLists.txt | 5 ++-- core/libs/prx/libSceRtc/Export.cpp | 16 +++++++++++++ core/libs/tests/GuestRtc.cpp | 33 +++++++++++++++++++++++++- docs/dev/debt/libSceRtc.md | 2 +- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceRtc/CMakeLists.txt b/core/libs/prx/libSceRtc/CMakeLists.txt index 689ae6f279..1411d4f936 100644 --- a/core/libs/prx/libSceRtc/CMakeLists.txt +++ b/core/libs/prx/libSceRtc/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(libSceRtc SHARED EXCLUDE_FROM_ALL Export.cpp) -target_link_libraries(libSceRtc PRIVATE libc) +target_link_libraries(libSceRtc PRIVATE libkernel libc) target_include_directories(libSceRtc PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceRtc) @@ -9,8 +9,9 @@ if(BUILD_TESTING) target_link_libraries(guest_rtc_tests PRIVATE libSceRtc libc) configure_windows_unwind(guest_rtc_tests) add_test(NAME guest_rtc COMMAND guest_rtc_tests) + add_test(NAME guest_rtc_ps5_title COMMAND guest_rtc_tests ps5-title) if(WIN32) - set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_rtc guest_rtc_ps5_title PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRtc>") endif() endif() diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 4b85e9449b..17ec53064c 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -6,9 +6,12 @@ #include <ctime> #include <limits> #include <mutex> +#include <stdexcept> +#include <string_view> #include "SceTypes.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" namespace { @@ -102,7 +105,20 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast<std::uint16_t>(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast<std::uint32_t>(timeOfDay % TICKS_PER_SECOND)}; } +bool ps4Title() { + try { + const auto titleId = GetAppTitleId_nid_postfix(); + return std::string_view(titleId.value, strnlen(titleId.value, sizeof(titleId.value))).substr(0, 4) == "CUSA"; + } catch (const std::runtime_error&) { + return false; + } catch (const std::length_error&) { + return false; + } +} + bool rangeChecked() { + static const bool ps4 = ps4Title(); + if (!ps4) return true; const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); std::uint32_t version; std::memcpy(&version, process + 16, sizeof(version)); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 1d72b948e4..09d0659f5f 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -7,7 +7,12 @@ #include <cstdlib> #include <cstring> #include <ctime> +#include <chrono> #include <exception> +#include <filesystem> +#include <fstream> +#include <string> +#include <string_view> #include <initializer_list> #include <limits> @@ -120,10 +125,36 @@ static void SetCompiledSdkVersion(std::byte* process, std::uint32_t version) { std::memcpy(process + 16, &version, sizeof(version)); } -int main() { +static void EnterTitleDirectory(const char* titleId) { + const auto directory = std::filesystem::temp_directory_path() / ("rtc-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory / "app0" / "sce_sys"); + std::filesystem::current_path(directory); + if (titleId == nullptr) return; + struct Entry { std::uint16_t key, format; std::uint32_t length, maximum, offset; }; + const std::uint32_t header[5] = {0x46535000, 0x0101, 52, 72, 2}; + const Entry entries[2] = {{0, 0x0204, 8, 8, 0}, {6, 0x0204, 10, 12, 8}}; + std::ofstream file("app0/sce_sys/param.sfo", std::ios::binary); + file.write(reinterpret_cast<const char*>(header), sizeof(header)); + file.write(reinterpret_cast<const char*>(entries), sizeof(entries)); + file << std::string("TITLE\0TITLE_ID\0\0\0\0\0\0", 20) << std::string("Example\0", 8) << titleId << std::string("\0\0\0", 3); +} + +int main(int argc, char** argv) { static std::array<std::byte, 64> process{}; Require(ProcessParametersFixture::Install(process.data(), process.size())); SetCompiledSdkVersion(process.data(), 0x01000051); + if (argc == 2 && std::string_view(argv[1]) == "ps5-title") { + EnterTitleDirectory(nullptr); + RtcDateTime untouched{0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaaaaaau}; + RtcDateTime converted = untouched; + Require(sceRtcGetDayOfWeek(10000, 1, 1) == static_cast<int>(0x80B50008) && sceRtcGetDayOfWeek(0, 13, 1) == static_cast<int>(0x80B50009)); + Require(sceRtcSetTime_t(&converted, -1) == static_cast<int>(0x80B50003) && Equal(converted, untouched)); + char text[64]; + RtcTick tick{std::numeric_limits<std::uint64_t>::max()}; + Require(sceRtcFormatRFC3339(text, &tick, 1) == static_cast<int>(0x80B50003)); + return 0; + } + EnterTitleDirectory("CUSA00000"); constexpr int invalidPointer = static_cast<int>(0x80B50002); constexpr int invalidValue = static_cast<int>(0x80B50003); constexpr int badParse = static_cast<int>(0x80B50007); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index dbdbc7f26d..12645457b4 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -8,7 +8,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 -- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. +- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, the behaviour main had before; those are not measured on a PS5. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 0f6fc31c98c279aa27569d6c22d8533297bd9a91 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 19:01:50 +0300 Subject: [PATCH 1278/1360] fix(tools): reconfigure streams with backslashreplace in check_skips --- tools/check_skips.py | 2 +- tools/tests/test_check_skips.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index e44304872d..d29c91b065 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -47,7 +47,7 @@ def skips(path): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="fail when a test is skipped that the job's device is not expected to skip") parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--job", choices=("linux", "windows"), default="linux") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index d0b03c0742..6e3871733b 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -5,6 +5,7 @@ import subprocess import sys import tempfile import unittest +from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_skips @@ -218,6 +219,33 @@ class CheckSkipsTests(unittest.TestCase): code = check_skips.main([str(junit), "--job", "linux"]) self.assertEqual(code, 0) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(check_skips.sys, "stdout", out), mock.patch.object(check_skips.sys, "stderr", err): + try: + check_skips.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + From 69708e0e804948f3a44e66170683b3d1454a4b25 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:02:43 +0000 Subject: [PATCH 1279/1360] fix(tools): implement write on web release test streams --- tools/tests/test_web_release.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index e48e3fd3c7..fe6d45076f 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -420,6 +420,12 @@ class TestWebRunnerTests(unittest.TestCase): self.encoding = encoding self.errors = errors + def write(self, text): + return len(text) + + def flush(self): + pass + out, err = Stream(), Stream() with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): with self.assertRaises(SystemExit): From 685e15d034e67421525cf65765eecce1e0ffb7c6 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 19:03:09 +0300 Subject: [PATCH 1280/1360] fix(tools): reconfigure streams with backslashreplace in check_test_registration --- tools/check_test_registration.py | 2 +- tools/tests/test_test_registration.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7903e7262c..e74e2f72e6 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -160,7 +160,7 @@ def unrun_scripts(root): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index c6bc83ae2b..34cf8c65d5 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -394,6 +394,33 @@ class CheckRegistrationCliTests(unittest.TestCase): code = check_test_registration.main(["--scripts"]) self.assertEqual(code, 0) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(check_test_registration.sys, "stdout", out), patch.object(check_test_registration.sys, "stderr", err): + try: + check_test_registration.main(["--invalid-argument"]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From b0e3c611054b63062c2d884ee8881ef02230d03b Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 19:04:18 +0300 Subject: [PATCH 1281/1360] fix(tools): reconfigure streams with backslashreplace in configure_seed --- tools/configure_seed.py | 2 +- tools/tests/test_configure_seed.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 8e042f9722..4e0a82372c 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -28,7 +28,7 @@ def quote(value): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") parser.add_argument("cache", help="CMakeCache.txt of a configured tree") parser.add_argument("seed", help="file to pass to cmake -C") diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 785b724136..ec7253a1fe 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -4,6 +4,7 @@ import subprocess import sys import tempfile import unittest +from unittest import mock from pathlib import Path TOOL = Path(__file__).resolve().parents[1] / "configure_seed.py" @@ -229,6 +230,33 @@ class ChecksParsingTests(unittest.TestCase): found = configure_seed.checks(cache) self.assertEqual(found, {"FIRST_CHECK": ("1", "Test FIRST_CHECK")}) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(configure_seed.sys, "stdout", out), mock.patch.object(configure_seed.sys, "stderr", err): + try: + configure_seed.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From 082a799e7e9396fc10a094b891beb3220876e971 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 19:06:52 +0300 Subject: [PATCH 1282/1360] fix(tools): reconfigure streams with backslashreplace in build_web --- tools/build_web.py | 2 +- tools/tests/test_build_web.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/build_web.py b/tools/build_web.py index ed871bb1d4..8a5e555bf0 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -167,7 +167,7 @@ def publish_release(output): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) parser.add_argument("--release", "--libraries", dest="release", action="store_true", diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 91e093b70d..11c38bfdd4 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -149,6 +149,33 @@ class BuildWebCliTests(unittest.TestCase): code = build_web.main(["--build", str(build)]) self.assertEqual(code, 0) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(build_web.sys, "stdout", out), patch.object(build_web.sys, "stderr", err): + try: + build_web.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From dfacf431f6564727edce910ca04d0732064c50f4 Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Fri, 23 Oct 2026 19:08:36 +0300 Subject: [PATCH 1283/1360] fix(tools): wrap entrypoint in main with stream reconfigure in trace_digest --- tools/tests/test_trace_digest.py | 28 ++++++++++++++++++++++++++++ tools/trace_digest.py | 6 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 7df5e64fae..8099ca9eee 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -7,6 +7,7 @@ import subprocess import sys import tempfile import unittest +from unittest import mock from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -744,6 +745,33 @@ class IntervalDigestTests(unittest.TestCase): loaded = trace_digest.load_summary(str(summary_file), SUMMARY["trace"]) self.assertEqual(loaded["commit"], SUMMARY["commit"]) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(trace_digest.sys, "stdout", out), mock.patch.object(trace_digest.sys, "stderr", err): + try: + trace_digest.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index d259fbb62d..59c9fb3bf9 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -344,6 +344,9 @@ def nonnegative_integer(value): def main(arguments=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") @@ -378,9 +381,6 @@ def main(arguments=None): if __name__ == "__main__": - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", errors="replace") try: sys.exit(main()) except (EOFError, OSError, ValueError, zlib.error) as error: From dd9716e1c3329d22c5e77dbdb42fb79507f113ad Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Thu, 22 Oct 2026 23:09:37 -0300 Subject: [PATCH 1284/1360] fix(libSceNet): convert sceNetInetPtonEx without an output as measured on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceNet/Export.cpp | 5 ++++- core/libs/tests/GuestSceNet.cpp | 5 ++++- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index f56153435c..27a3eb1bca 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2474,13 +2474,16 @@ int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { if (af != NET_AF_INET) { return sceNetInetPton(af, src, dst); } - if (src == nullptr || dst == nullptr) { + if (src == nullptr) { return fail(NET_EINVAL); } std::uint32_t address = 0; if (!parse_ipv4_parts(src, address)) { return 0; } + if (dst == nullptr) { + return 1; + } const std::array<unsigned char, 4> bytes{static_cast<unsigned char>(address >> 24), static_cast<unsigned char>(address >> 16), static_cast<unsigned char>(address >> 8), static_cast<unsigned char>(address)}; std::memcpy(dst, bytes.data(), bytes.size()); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 276da969b7..058be81bc4 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -984,7 +984,10 @@ static void CheckInetPtonEx() { Require(sceNetInetPtonEx(2, "not an address", address.data(), 0) == 0); Require(Failed(sceNetInetPtonEx(99, "127.0.0.1", address.data(), 0), 47)); Require(Failed(sceNetInetPtonEx(2, nullptr, address.data(), 0), 22)); - Require(Failed(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0), 22)); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0) == 1 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPtonEx(2, "abc", nullptr, 0) == 0 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPtonEx(2, "0x", address.data(), 0) == 1 && address[0] == 0 && address[1] == 0 && address[2] == 0 && address[3] == 0); for (const int flags : {1, 0x100, -1}) { address.fill(0); Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 625feb8f4e..781b0b6276 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -15,7 +15,7 @@ - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. -- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. +- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60); flag values other than 0, single bits up to 0x100, 0x10000, 0x80000000 and -1 were not measured. The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5 (26 inputs, six flag values), IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab, LF, VT, FF or CR), except that a part starting with `09` is decimal and an octal part with `8` or `9` fails; flags are ignored; a null `src` returns `NET_EINVAL`; a null `dst` returns 1 or 0 without writing. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. From d4321c65693574ae8582318a45f99082b7a13372 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Thu, 22 Oct 2026 23:16:59 -0300 Subject: [PATCH 1285/1360] docs(libSceNet): separate the PS4 and PS5 measurements of sceNetInetPtonEx Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestSceNet.cpp | 1 - docs/dev/debt/libSceNet.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 058be81bc4..adbbabbe7a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -987,7 +987,6 @@ static void CheckInetPtonEx() { *sceNetErrnoLoc() = 123; Require(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0) == 1 && *sceNetErrnoLoc() == 123); Require(sceNetInetPtonEx(2, "abc", nullptr, 0) == 0 && *sceNetErrnoLoc() == 123); - Require(sceNetInetPtonEx(2, "0x", address.data(), 0) == 1 && address[0] == 0 && address[1] == 0 && address[2] == 0 && address[3] == 0); for (const int flags : {1, 0x100, -1}) { address.fill(0); Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 781b0b6276..78cb581563 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -15,7 +15,7 @@ - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. -- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60); flag values other than 0, single bits up to 0x100, 0x10000, 0x80000000 and -1 were not measured. The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5 (26 inputs, six flag values), IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab, LF, VT, FF or CR), except that a part starting with `09` is decimal and an octal part with `8` or `9` fails; flags are ignored; a null `src` returns `NET_EINVAL`; a null `dst` returns 1 or 0 without writing. IPv6 converts as `sceNetInetPton` does +- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5, IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails; flags were ignored (PS4: 0, single bits up to 0x100, 0x10000, 0x80000000, -1; PS5: 1, 2, 4, 0x100, 0x10000, -1). On the PS5 only: LF, FF and CR also end the text, a null `src` returns `NET_EINVAL`, and a null `dst` returns 1 or 0 without writing or setting errno. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. From b2dc84b68daa2c1e1b3718858cb890da94d41f92 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 13:17:24 -0300 Subject: [PATCH 1286/1360] fix(libSceNet): read sceNetInetPton text and null outputs as measured on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceNet/Export.cpp | 24 ++++++++++++++++++------ core/libs/tests/GuestSceNet.cpp | 17 ++++++++++++++++- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 27a3eb1bca..29efb3e552 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1962,20 +1962,32 @@ uint32_t APS5_VABI sceNetNtohl_nid_postfix(uint32_t net32) { return swap32(net32 uint16_t APS5_VABI sceNetNtohs_nid_postfix(uint16_t net16) { return swap16(net16); } int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { - if (src == nullptr || dst == nullptr) { + if (src == nullptr) { return fail(NET_EINVAL); } if (af != NET_AF_INET && af != NET_AF_INET6) { return fail(NET_EAFNOSUPPORT); } - const std::string text = af == NET_AF_INET ? std::string(src, std::strcspn(src, " \t\n\v\f\r")) : std::string(src); - std::array<unsigned char, 16> address{}; + if (dst == nullptr && af == NET_AF_INET6) { + return fail(NET_EINVAL); + } + const auto convert = [](int family, const std::string& text, unsigned char* address) { #ifdef _WIN32 - const int converted = InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); + return InetPtonA(family, text.c_str(), address); #else - const int converted = ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); + return ::inet_pton(family, text.c_str(), address); #endif - if (converted == 1) { + }; + const std::string head(src, std::strcspn(src, " \t\n\v\f\r")); + const auto colon = head.rfind(':'); + const bool dottedTail = af == NET_AF_INET6 && colon != std::string::npos && head.find('.', colon) != std::string::npos; + std::array<unsigned char, 16> address{}; + if (dottedTail && convert(AF_INET, head.substr(colon + 1), address.data()) != 1) { + return 0; + } + const std::string text = af == NET_AF_INET || dottedTail ? head : std::string(src); + const int converted = convert(af == NET_AF_INET ? AF_INET : AF_INET6, text, address.data()); + if (converted == 1 && dst != nullptr) { std::memcpy(dst, address.data(), af == NET_AF_INET ? 4 : 16); } return converted; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index adbbabbe7a..ad4e23494a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -926,9 +926,24 @@ static void CheckInetPtonWhitespace() { for (const char* text : {" 1.2.3.4", "1.2.3.4x", "1.2.3.4.", "1.2.3.4\x01", "1.2.3.4\xa0", "1.2. 3.4", "1.2.3", "1.2.3 ", "01.2.3.4", "256.0.0.1"}) { CheckInetPtonRejects(2, text); } - for (const char* text : {"::1 ", "::1\t", " ::1", "::1x"}) { + for (const char* text : {"::1 ", "::1\t", " ::1", "::1x", "::ffff:1.2.3.4x", "::ffff:1.2.3.4\x01", "::ffff:01.2.3.4", "::ffff:1.2.3", + "::ffff:0x1.2.3.4", "::ffff ", "::ffff:1.2 .3.4"}) { CheckInetPtonRejects(28, text); } + for (const char* text : {"::ffff:1.2.3.4 x", "::ffff:1.2.3.4\t", "::ffff:1.2.3.4\nzz", "::ffff:1.2.3.4 "}) { + std::array<std::uint8_t, 16> address{}; + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(28, text, address.data()) == 1 && *sceNetErrnoLoc() == 123); + Require(address[10] == 0xff && address[11] == 0xff && address[12] == 1 && address[15] == 4); + } + std::array<std::uint8_t, 16> full{}; + Require(sceNetInetPton(28, "1:2:3:4:5:6:1.2.3.4 z", full.data()) == 1 && full[1] == 1 && full[11] == 6 && full[12] == 1 && full[15] == 4); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(2, "1.2.3.4", nullptr) == 1 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPton(2, "abc", nullptr) == 0 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPton(28, "::1", nullptr) < 0 && *sceNetErrnoLoc() == 22); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(28, nullptr, full.data()) < 0 && *sceNetErrnoLoc() == 22); } struct AtonCase { diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 78cb581563..b9ad83d736 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -45,6 +45,6 @@ - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. -- [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ends an IPv4 address at the first space, tab, LF, VT, FF or CR and ignores the rest (`"1.2.3.4 x"` converts), leaves the destination and `sceNetErrnoLoc` unchanged when a conversion fails, as PS4 firmware 10.71 does; not yet confirmed on PS5. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and IPv6 text with whitespace fail. +- [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - an IPv6 conversion with a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). On that PS5 and on PS4 firmware 10.71 an IPv4 address ends at the first space, tab, LF, VT, FF or CR and the rest is ignored (`"1.2.3.4 x"` converts), and a failed conversion leaves the destination and `sceNetErrnoLoc` unchanged. On the PS5 the dotted IPv4 part of IPv6 text is read the same way (`"::ffff:1.2.3.4 x"` converts, `"::ffff:01.2.3.4"` fails), and an IPv4 conversion with a null `dst` returns 1 or 0 without writing. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and whitespace after hexadecimal IPv6 text fail - [sceNetDhcpStart, sceNetAddrConfig6Start, sceNetDuplicateIpStart and sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) - argument widths and callback ABI are recovered. Address callbacks receive user data, a 32-bit event and a 32-bit error. DHCP configuration is mutable; the 512-byte IPv6 configuration is read-only. Their complete field layouts remain unknown. The two 32-bit thread parameters are stored but not consumed by this firmware thread wrapper, so their original names and units are unconfirmed. Information-dump options contain flags, an interval in seconds and a 64-byte path. All four exports remain unimplemented. From 1bb17028061e7f4d149138c6672145c89db15530 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:18:15 +0000 Subject: [PATCH 1287/1360] test(tools): assert web release parse error status --- tools/tests/test_web_release.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index fe6d45076f..d67632e117 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -428,8 +428,9 @@ class TestWebRunnerTests(unittest.TestCase): out, err = Stream(), Stream() with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): - with self.assertRaises(SystemExit): + with self.assertRaises(SystemExit) as error: test_web.main([]) + self.assertEqual(error.exception.code, 2) self.assertEqual(out.encoding, "utf-8") self.assertEqual(out.errors, "backslashreplace") self.assertEqual(err.encoding, "utf-8") From 1852888af9234ddce6b8aec9a7a38811e3623727 Mon Sep 17 00:00:00 2001 From: Project-MOC <83237812+Project-MOC@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:27:37 +0200 Subject: [PATCH 1288/1360] fix(tools): preserve SELF outputs on failed writes --- tools/tests/test_unwrap_self.py | 149 ++++++++++++++++++++++++++++++++ tools/unwrap_self.py | 21 +++-- 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index f30eb5a74f..648d5471b2 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -1,4 +1,5 @@ import contextlib +import errno import io import os import struct @@ -116,6 +117,154 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(code, 0) self.assertTrue(output.exists()) + def test_write_failure_preserves_output_and_removes_temporary_files(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + real_open = io.open + + def failing_open(path, mode="r", *args, **kwargs): + stream = real_open(path, mode, *args, **kwargs) + if "w" not in mode: + return stream + writer = mock.MagicMock(wraps=stream) + writer.__enter__.return_value = writer + writer.__exit__.side_effect = lambda *args: stream.close() + + def fail_after_partial_write(data): + stream.write(data[:16]) + stream.flush() + raise OSError(errno.ENOSPC, "simulated write failure") + + writer.write.side_effect = fail_after_partial_write + return writer + + for previous in (None, b"previous output"): + with self.subTest(previous=previous): + output.unlink(missing_ok=True) + if previous is not None: + output.write_bytes(previous) + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch("io.open", side_effect=failing_open), contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated write failure", errors.getvalue()) + self.assertEqual(set(directory.iterdir()), before) + if previous is not None: + self.assertEqual(output.read_bytes(), previous) + self.assertEqual(source.read_bytes(), container) + + def test_replace_failure_preserves_output_and_removes_temporary_files(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + output.write_bytes(b"previous output") + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch.object(Path, "replace", side_effect=OSError("simulated replacement failure")), \ + contextlib.redirect_stderr(errors), contextlib.redirect_stdout(io.StringIO()): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated replacement failure", errors.getvalue()) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(source.read_bytes(), container) + self.assertEqual(set(directory.iterdir()), before) + + def test_successful_conversion_replaces_existing_output_without_temporary_files(self): + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + expected = output.read_bytes() + before = set(Path(self.directory).iterdir()) + output.write_bytes(b"previous output") + with contextlib.redirect_stdout(io.StringIO()): + status = unwrap_self.main([str(Path(self.directory) / "input.self"), str(output)]) + self.assertEqual(status, 0) + self.assertEqual(output.read_bytes(), expected) + self.assertEqual(set(Path(self.directory).iterdir()), before) + + def test_close_failure_preserves_existing_output(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + output.write_bytes(b"previous output") + before = set(directory.iterdir()) + real_open = io.open + + def failing_open(path, mode="r", *args, **kwargs): + stream = real_open(path, mode, *args, **kwargs) + if "w" not in mode: + return stream + writer = mock.MagicMock(wraps=stream) + writer.__enter__.return_value = writer + + def fail_on_close(*args): + stream.close() + raise OSError("simulated close failure") + + writer.__exit__.side_effect = fail_on_close + return writer + + errors = io.StringIO() + with mock.patch("io.open", side_effect=failing_open), contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated close failure", errors.getvalue()) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(set(directory.iterdir()), before) + + def test_temporary_file_creation_failure_preserves_existing_output(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + output.write_bytes(b"previous output") + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch.object(tempfile, "NamedTemporaryFile", side_effect=OSError("simulated creation failure")), \ + contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated creation failure", errors.getvalue()) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(set(directory.iterdir()), before) + + @unittest.skipUnless(os.name == "posix", "POSIX permission bits") + def test_replacing_output_preserves_existing_permissions(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + output.write_bytes(b"previous output") + output.chmod(0o640) + unwrap_self.unwrap(source, output) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertEqual(output.stat().st_mode & 0o777, 0o640) + + def test_output_symlink_is_kept_and_its_target_is_replaced(self): + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + expected = output.read_bytes() + output.unlink() + target = Path(self.directory) / "target.elf" + target.write_bytes(b"previous output") + try: + output.symlink_to(target.name) + except OSError as error: + self.skipTest(str(error)) + before = set(Path(self.directory).iterdir()) + unwrap_self.unwrap(Path(self.directory) / "input.self", output) + self.assertTrue(output.is_symlink()) + self.assertEqual(target.read_bytes(), expected) + self.assertEqual(set(Path(self.directory).iterdir()), before) + def convert(self, container, name="input.self"): directory = Path(self.directory) source = directory / name diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 659f4eb07a..26f7bfd320 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -33,6 +33,7 @@ import argparse import math import struct import sys +import tempfile from collections import Counter from pathlib import Path @@ -235,8 +236,8 @@ def check_decrypted(data, headers): def unwrap(path, output, verbose=False): - source, destination = Path(path), Path(output) - if source.resolve() == destination.resolve() or (destination.exists() and source.samefile(destination)): + source, destination = Path(path), Path(output).resolve() + if source.resolve() == destination or (destination.exists() and source.samefile(destination)): raise Refused("output aliases the input container") data = bytes(source.read_bytes()) if len(data) < FIRST_REGION: @@ -326,10 +327,18 @@ def unwrap(path, output, verbose=False): print(" [%2d] %-18s vaddr=%#011x filesz=%#010x offset=%#010x%s" % (item["index"], name, item["vaddr"], item["size"], item["new"], moved)) - destination = Path(output) - if destination.parent != Path(""): - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(result) + destination.parent.mkdir(parents=True, exist_ok=True) + temporary_path = None + try: + with tempfile.NamedTemporaryFile(mode="wb", dir=destination.parent, prefix=".unwrap-self-", delete=False) as temporary: + temporary_path = Path(temporary.name) + temporary.write(result) + if sys.platform != "win32" and destination.exists(): + temporary_path.chmod(destination.stat().st_mode) + temporary_path.replace(destination) + finally: + if temporary_path is not None: + temporary_path.unlink(missing_ok=True) return len(result) From 950a21aec8809977c39a2f47ee54c010b9694ba7 Mon Sep 17 00:00:00 2001 From: Hermes Agent <agent@herme.ai> Date: Wed, 21 Oct 2026 01:37:26 -0400 Subject: [PATCH 1289/1360] fix(libkernel): return ENOENT for unknown sysctl names --- .../System/src/SystemConfiguration.cpp | 6 ++-- core/libs/tests/GuestSystemConfiguration.cpp | 33 +++++++++++++++---- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index c0f33a4450..ff20f5f917 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -467,7 +467,8 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {6, 12}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); } - throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); + *__error_nid_postfix() = 2; + return -1; } int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { @@ -529,6 +530,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ for (const auto& entry : SystemIntegers) { if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, 2); } - throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); + *__error_nid_postfix() = 2; + return -1; } } diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 707fcd5ea7..02140768bd 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -249,6 +249,29 @@ static void CheckUnansweredSysctl() { Require(size == 4 && untouched[0] == 0x5a5a5a5a && untouched[1] == 0x5a5a5a5a && untouched[2] == 0x5a5a5a5a); } } +static void CheckUnknownSysctlName() { + static constexpr const char* name = "kern.anyps5_no_such_name"; + unsigned char buffer[16]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + + length = 77; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t mibLength = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, mib, &mibLength) == -1 && *__error_nid_postfix() == 2); + Require(mibLength == 4 && mib[0] == 0x5a5a5a5a && mib[1] == 0x5a5a5a5a && mib[2] == 0x5a5a5a5a && mib[3] == 0x5a5a5a5a); + + mibLength = 0; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, nullptr, &mibLength) == -1 && *__error_nid_postfix() == 2 && mibLength == 0); +} static void CheckPathnameSysctl() { *__error_nid_postfix() = 13; char buffer[32] = {}; @@ -793,13 +816,8 @@ static void CheckNameToMib() { threw = true; } Require(threw); - threw = false; - try { - sysctlnametomib_nid_postfix("kern.cp_times", mib, &size); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix("kern.anyps5_no_such_name", mib, &size) == -1 && *__error_nid_postfix() == 2); } int main() { CheckProcessorCountSysctl(); @@ -811,6 +829,7 @@ int main() { #endif CheckTscFrequencySysctl(); CheckUnansweredSysctl(); + CheckUnknownSysctlName(); CheckPathnameSysctl(); CheckSystemStringSysctl(); CheckSystemStringNames(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..23249fc88f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -183,7 +183,7 @@ - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; #8553 implements the mappings. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`; here unknown names return `ENOENT` from `sysctlbyname` and `sysctlnametomib`, following the FreeBSD name lookup error. Whether a title queries an unknown name or sees the same error is unmeasured. - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) From a05ea75447693b84f2844873fcdfeb9a90a05db4 Mon Sep 17 00:00:00 2001 From: Hermes Agent <agent@herme.ai> Date: Wed, 21 Oct 2026 01:41:12 -0400 Subject: [PATCH 1290/1360] docs(libkernel): clarify unknown sysctl lookup behavior --- docs/dev/debt/libkernel.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 23249fc88f..c0804618fa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -183,7 +183,8 @@ - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; #8553 implements the mappings. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`; here unknown names return `ENOENT` from `sysctlbyname` and `sysctlnametomib`, following the FreeBSD name lookup error. Whether a title queries an unknown name or sees the same error is unmeasured. +- [sysctl names measured absent on a PS5 payload](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - On firmware 12.70, a payload gets `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`. `sysctl` returns `ENOENT` for MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}; reads and size queries leave output buffers and lengths untouched. `sysctlnametomib` resolves the seven corresponding MIB names and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size`. Writes and title behavior are unmeasured. +- [Unknown sysctl names](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - A payload process on firmware 12.70 gets `ENOENT` for `kern.anyps5_no_such_name` through `sysctlbyname` and `sysctlnametomib`; here both name-based calls return `ENOENT` for unrecognized names, following FreeBSD name lookup. This is one payload measurement; title behavior for other unknown names is unverified. Numeric MIBs with no implementation still throw. - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) From 2026238d3912150cd0a345e19b8c86467e6a0cb1 Mon Sep 17 00:00:00 2001 From: Hermes Agent <agent@herme.ai> Date: Wed, 21 Oct 2026 06:53:45 -0400 Subject: [PATCH 1291/1360] test(libkernel): expect ENOENT for unresolved sysctl names --- core/libs/tests/GuestSystemConfiguration.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 02140768bd..a4d441e2e0 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -793,16 +793,15 @@ static void CheckNameToMib() { char tooLong[1025]; std::memset(tooLong, 'a', sizeof(tooLong) - 1); tooLong[sizeof(tooLong) - 1] = 0; - Require(sysctlnametomib_nid_postfix(tooLong, mib, &size) == -1 && *__error_nid_postfix() == 63); + int tooLongMib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t tooLongMibSize = 4; + Require(sysctlnametomib_nid_postfix(tooLong, tooLongMib, &tooLongMibSize) == -1 && *__error_nid_postfix() == 63); + Require(tooLongMibSize == 4 && tooLongMib[0] == 0x5a5a5a5a && tooLongMib[1] == 0x5a5a5a5a && tooLongMib[2] == 0x5a5a5a5a && tooLongMib[3] == 0x5a5a5a5a); tooLong[1023] = 0; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(tooLong, tooLongMib, &tooLongMibSize) == -1 && *__error_nid_postfix() == 2 && tooLongMibSize == 4); + Require(tooLongMib[0] == 0x5a5a5a5a && tooLongMib[1] == 0x5a5a5a5a && tooLongMib[2] == 0x5a5a5a5a && tooLongMib[3] == 0x5a5a5a5a); bool threw = false; - try { - sysctlnametomib_nid_postfix(tooLong, mib, &size); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); - threw = false; try { sysctlnametomib_nid_postfix(nullptr, mib, &size); } catch (const std::invalid_argument&) { From a7e441edc2c86444379fd1b2b2f63d9e6c1157a5 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:30:37 -0500 Subject: [PATCH 1292/1360] test(libSceUserService): cover initial user queries and game presets validation --- core/libs/tests/GuestUserService.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index db457f47f3..83e78239ca 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -7,6 +7,8 @@ #include <utility> extern "C" { +int APS5_VABI sceUserServiceGetInitialUser(int* user_id); +int APS5_VABI sceUserServiceGetGamePresets(int user_id, UserServiceGamePresets* presets); int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number); int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* value); int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); @@ -154,6 +156,14 @@ int main() { Require(setting.get(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK); Require(value == setting.value); } + int initialUser = -1; + Require(sceUserServiceGetInitialUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetInitialUser(&initialUser) == USER_SERVICE_OK && initialUser == USER_SERVICE_INITIAL_USER_ID); + Require(sceUserServiceGetGamePresets(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + UserServiceGamePresets presets{}; + presets.difficulty = 0x5a; + Require(sceUserServiceGetGamePresets(0, &presets) == USER_SERVICE_ERROR_INVALID_ARGUMENT && presets.difficulty == 0x5a); + Require(sceUserServiceGetGamePresets(USER_SERVICE_INITIAL_USER_ID, &presets) == USER_SERVICE_OK && presets.difficulty == 0); bool threwKeyremap = false; try { sceUserServiceGetAccessibilityKeyremapEnable(); From bdcf9c6e5dce59e4e58c97996ab97669626f21e6 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:31:05 -0500 Subject: [PATCH 1293/1360] test(libSceAvPlayer): cover null handle validation in sceAvPlayerAddSourceEx and sceAvPlayerDisableStream --- core/libs/tests/GuestAvPlayer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 54220d0b27..b79a260a1c 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -39,6 +39,8 @@ int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal*, std::uint32_t, std::uin int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal*, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAvPlayerStart(AvPlayerInternal*); int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal*, const AvPlayer::StartInfo*); +int APS5_VABI sceAvPlayerAddSourceEx(AvPlayerInternal*, std::uint32_t, const void*); +int APS5_VABI sceAvPlayerDisableStream(AvPlayerInternal*, std::uint32_t); int APS5_VABI sceAvPlayerStop(AvPlayerInternal*); int APS5_VABI sceAvPlayerPause(AvPlayerInternal*); int APS5_VABI sceAvPlayerResume(AvPlayerInternal*); @@ -1090,6 +1092,8 @@ void TestExtendedExports() { Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame after extended restart"); Check(frame.timestamp <= 67, "zero-position start did not restart playback"); Check(sceAvPlayerClose(player) == 0, "close failed"); + Check(sceAvPlayerAddSourceEx(nullptr, 0, nullptr) == InvalidParams, "null player add source rejected"); + Check(sceAvPlayerDisableStream(nullptr, 0) == InvalidParams, "null player disable stream rejected"); } void TestWithoutAllocators() { From c242694d099fa8cb1cd44eb40dceed9ce1699ae3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:31:10 +0000 Subject: [PATCH 1294/1360] test(tools): cover Unicode environment reports --- tools/tests/test_env_reference.py | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index f56a933b14..51fac32289 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -3,6 +3,7 @@ import io import sys import tempfile import unittest +from unittest import mock from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -164,6 +165,64 @@ class EnvReferenceTests(unittest.TestCase): (core / "3rdparty" / "Ignored.cpp").write_text('static bool b = IsEnvironmentSet("APS5_IGNORED");\n', encoding="utf-8") self.assertEqual(set(env_reference.reads([core], BOTH)), {"APS5_TRACE_GPU"}) + def test_main_reconfigures_streams_for_unicode_report_paths(self): + class Stream: + def __init__(self): + self.encoding = "ascii" + self.errors = "strict" + self.text = "" + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + text.encode(self.encoding, self.errors) + self.text += text + return len(text) + + def flush(self): + pass + + root = self.fixture() / "source-Ж" + root.mkdir() + core = root / "core" + core.mkdir() + (core / "Sample.cpp").write_text('static bool a = std::getenv("APS5_TRACE_GPU") != nullptr;\n', encoding="utf-8") + out, err = Stream(), Stream() + with mock.patch.object(env_reference.sys, "stdout", out), mock.patch.object(env_reference.sys, "stderr", err): + self.assertEqual(env_reference.main(["--root", str(core)]), 0) + self.assertEqual((out.encoding, out.errors), ("utf-8", "backslashreplace")) + self.assertEqual((err.encoding, err.errors), ("utf-8", "backslashreplace")) + self.assertIn(str(core / "Sample.cpp"), out.text) + + def test_main_reports_unicode_scan_root_with_non_utf8_stderr(self): + class Stream: + def __init__(self): + self.encoding = "ascii" + self.errors = "strict" + self.text = "" + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + text.encode(self.encoding, self.errors) + self.text += text + return len(text) + + def flush(self): + pass + + root = Path(tempfile.gettempdir()) / "missing-source-Ж" + out, err = Stream(), Stream() + with mock.patch.object(env_reference.sys, "stdout", out), mock.patch.object(env_reference.sys, "stderr", err): + self.assertEqual(env_reference.main(["--root", str(root)]), 1) + self.assertEqual((out.encoding, out.errors), ("utf-8", "backslashreplace")) + self.assertEqual((err.encoding, err.errors), ("utf-8", "backslashreplace")) + self.assertIn(str(root), err.text) + def test_main_rejects_a_scan_root_that_is_not_a_directory(self): root = self.fixture() for name in ("missing", "core/Sample.cpp"): From 8ce4ed4e77ebb19918db3291456fdd096935c9ba Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Fri, 23 Oct 2026 12:31:17 -0400 Subject: [PATCH 1295/1360] fix(pad): timestamp scripted overlays in queued samples --- core/libs/prx/libScePad/src/PadState.cpp | 4 ++- core/libs/tests/GuestPad.cpp | 39 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 5a6795f32e..e4bcf6eb90 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -25,6 +25,7 @@ namespace { std::array<std::uint8_t, 2> audioHapticRumble{}; PadAudioHapticQueue legacyHaptics; std::uint64_t timestamp = 0; + std::uint64_t injectedTimestamp = 0; struct QueuedInput { PadInputState input; @@ -224,6 +225,7 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { if (injected != lastInjected) { lastInjected = injected; timestamp = now; + injectedTimestamp = now; } return injected; } @@ -260,7 +262,7 @@ int Pad::Read(PadData* data, int num) { int count = 0; while (count < num && !queue.empty()) { const QueuedInput& entry = queue.front(); - data[count++] = BuildData(entry.input, entry.timestamp, now, injected); + data[count++] = BuildData(entry.input, std::max(entry.timestamp, injectedTimestamp), now, injected); queue.pop_front(); } return count; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 35abd4888c..c68b413632 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -6,6 +6,7 @@ #include <array> #include <chrono> #include <cstdlib> +#include <cstdio> #include <cstring> #include <filesystem> #include <fstream> @@ -487,6 +488,43 @@ static void CheckInjectedState(const std::filesystem::path& path) { PadPublishInput_nid_postfix(PadInputState{}); } +static void CheckInjectedQueueTimes(const std::filesystem::path& path) { + PadData drained[64]{}; + Pad::Read(drained, 64); + PadInputState physical; + physical.buttons = 0x2000; + PadPublishInput_nid_postfix(physical); + const auto before = Pad::ReadState(); + std::ofstream(path) << "5 16384 300\n"; + PadData pressed{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + pressed = Pad::ReadState(); + if ((pressed.buttons & 0x4000) != 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < deadline); + Require(pressed.buttons == 0x6000 && pressed.timestamp > before.timestamp); + PadData queued{}; + Require(Pad::Read(&queued, 1) == 1); + Require(queued.buttons == pressed.buttons); + if (queued.timestamp < pressed.timestamp) { + std::fputs("queued input predates its injected button press\n", stderr); + std::abort(); + } + physical.sticks[0] = 130; + PadPublishInput_nid_postfix(physical); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto released = Pad::ReadState(); + Require(released.buttons == 0x2000 && released.timestamp > pressed.timestamp); + Require(Pad::Read(&queued, 1) == 1); + Require(queued.buttons == released.buttons && queued.left_stick_x == 130); + if (queued.timestamp < released.timestamp) { + std::fputs("queued input predates its injected button release\n", stderr); + std::abort(); + } + PadPublishInput_nid_postfix(PadInputState{}); +} + int main() { CheckInputCommands(); std::uint32_t accessibilityState = 7; @@ -569,6 +607,7 @@ int main() { CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); CheckInjectedState(commandPath); + CheckInjectedQueueTimes(commandPath); PadLightBarParam lightBar{255, 128, 64}; Require(scePadSetLightBar(handle, &lightBar) == PAD_OK); Require(scePadSetLightBar(handle, nullptr) == PAD_ERROR_INVALID_ARG); From 918bbde6faf58fe89b8506fb6cd70869b18e6de9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:31:23 +0000 Subject: [PATCH 1296/1360] fix(tools): reconfigure environment report streams --- tools/env_reference.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/env_reference.py b/tools/env_reference.py index b7277df3bb..b4640417ac 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -160,6 +160,9 @@ def check(found, doc, prefixes, out): def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser( description="list the environment names the code reads, or check them against a reference document") parser.add_argument("--check", action="store_true", From 9d15da71a6727f6b850e445ae33373bfab0ee7ab Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:31:43 -0500 Subject: [PATCH 1297/1360] test(libSceNpEntitlementAccess): cover flag queries and transaction parameter validation --- core/libs/tests/GuestEntitlements.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp index 868c1f6e7f..46da7dd6b4 100644 --- a/core/libs/tests/GuestEntitlements.cpp +++ b/core/libs/tests/GuestEntitlements.cpp @@ -14,6 +14,10 @@ extern "C" { int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessAddcontEntitlementInfo*); int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(std::uint32_t, NpEntitlementAccessAddcontEntitlementInfo*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetSkuFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetPftFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -31,7 +35,17 @@ int main(int argc, char** argv) { const auto directory = root / std::filesystem::path(u8"dlc-a\u00e7\u00e3o-\u65e5\u672c"); const auto file = directory / "anyps5-entitlements.ini"; if (argc == 2 && std::string_view(argv[1]) == "missing") { - SetEntitlements(file); + std::uint32_t skuFlag = 0; + Require(sceNpEntitlementAccessGetSkuFlag(nullptr) == static_cast<int>(0x80558003)); + Require(sceNpEntitlementAccessGetSkuFlag(&skuFlag) == 0 && skuFlag == 3); + std::uint32_t pftFlag = 1; + Require(sceNpEntitlementAccessGetPftFlag(nullptr) == static_cast<int>(0x817D0002)); + Require(sceNpEntitlementAccessGetPftFlag(&pftFlag) == 0 && pftFlag == 0); + std::uint32_t trialFlag = 1; + Require(sceNpEntitlementAccessGetGameTrialsFlag(nullptr) == static_cast<int>(0x817D0002)); + Require(sceNpEntitlementAccessGetGameTrialsFlag(&trialFlag) == 0 && trialFlag == 0); + Require(sceNpEntitlementAccessGenerateTransactionId(nullptr) == static_cast<int>(0x80558003)); + SetEntitlements(file); std::uint32_t hits = 0; bool rejected = false; try { From 94515fd50604ba3b30fc7640c96007d5ed2a9b77 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:32:09 -0500 Subject: [PATCH 1298/1360] test(libSceWebBrowserDialog): cover uninitialized rejection for cookie operations --- core/libs/tests/GuestWebBrowserDialog.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index bb391077a8..6289900e72 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -6,6 +6,8 @@ extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void); int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogSetCookie(const void* param); +int APS5_VABI sceWebBrowserDialogResetCookie(const void* param); int APS5_VABI sceWebBrowserDialogOpen(const void* param); int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, const WebBrowserDialogAllowList* allowList); int APS5_VABI sceWebBrowserDialogClose(void); @@ -37,6 +39,8 @@ int main() { result.result = 0x5a5a5a5a; Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogSetCookie(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogResetCookie(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); From 492583bfe7454569e3ac37bf74587de026163b48 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:32:21 -0500 Subject: [PATCH 1299/1360] test(libSceNpCommon): cover recursive lock in sceNpLwMutexLock --- core/libs/tests/GuestNpCommon.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestNpCommon.cpp b/core/libs/tests/GuestNpCommon.cpp index d605767148..c3d964f7ea 100644 --- a/core/libs/tests/GuestNpCommon.cpp +++ b/core/libs/tests/GuestNpCommon.cpp @@ -24,6 +24,7 @@ int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint6 int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex); int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex); int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexLock(PthreadMutex* mutex); int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t flags); int APS5_VABI sceNpCondDestroy(PthreadCond* cond); int APS5_VABI sceNpCondSignal(PthreadCond* cond); @@ -127,6 +128,8 @@ static int testSync() { ok &= expect(sceNpMutexUnlock(&plain) == 0, "the plain mutex did not unlock"); ok &= expect(sceNpLwMutexTryLock(&recursive) == 0 && sceNpLwMutexTryLock(&recursive) == 0, "flag 1 did not make the mutex recursive"); ok &= expect(sceNpLwMutexUnlock(&recursive) == 0 && sceNpLwMutexUnlock(&recursive) == 0, "the recursive mutex did not unlock twice"); + ok &= expect(sceNpLwMutexLock(&recursive) == 0, "the recursive mutex did not lock via sceNpLwMutexLock"); + ok &= expect(sceNpLwMutexUnlock(&recursive) == 0, "the recursive mutex did not unlock"); ok &= expect(sceNpLwMutexDestroy(&recursive) == 0, "the recursive mutex was not destroyed"); PthreadCond cond = nullptr; From 8713da0cbb352474529b29a483b338992e8edaa2 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:33:11 -0500 Subject: [PATCH 1300/1360] test(libSceSystemGesture): cover debug version query --- core/libs/tests/GuestSystemGesture.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 656f943aca..c1e62e8934 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -31,6 +31,7 @@ int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(); int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(std::int32_t, std::uint16_t, void*); +int APS5_VABI sceSystemGestureDebugGetVersion(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -280,6 +281,7 @@ int main() { Require(sceSystemGestureUpdateAllTouchRecognizer(handle + 1) == invalidHandle); Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidArgument); + Require(sceSystemGestureDebugGetVersion() == 0x6a00); TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); std::array<std::int32_t, 7> otherHandles{}; From db8785f6ced6b240e4005f3d4a0978e5bdd54ac8 Mon Sep 17 00:00:00 2001 From: Project-MOC <83237812+Project-MOC@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:52:02 +0200 Subject: [PATCH 1301/1360] fix(tools): preserve new SELF output permissions --- tools/tests/test_unwrap_self.py | 93 +++++++++++++++++++++++++++++++-- tools/unwrap_self.py | 11 ++-- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 648d5471b2..c45b80fe40 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -127,7 +127,7 @@ class UnwrapTest(unittest.TestCase): def failing_open(path, mode="r", *args, **kwargs): stream = real_open(path, mode, *args, **kwargs) - if "w" not in mode: + if "w" not in mode and "x" not in mode: return stream writer = mock.MagicMock(wraps=stream) writer.__enter__.return_value = writer @@ -199,7 +199,7 @@ class UnwrapTest(unittest.TestCase): def failing_open(path, mode="r", *args, **kwargs): stream = real_open(path, mode, *args, **kwargs) - if "w" not in mode: + if "w" not in mode and "x" not in mode: return stream writer = mock.MagicMock(wraps=stream) writer.__enter__.return_value = writer @@ -227,14 +227,99 @@ class UnwrapTest(unittest.TestCase): output.write_bytes(b"previous output") before = set(directory.iterdir()) errors = io.StringIO() - with mock.patch.object(tempfile, "NamedTemporaryFile", side_effect=OSError("simulated creation failure")), \ - contextlib.redirect_stderr(errors): + real_open = io.open + + def failing_open(path, mode="r", *args, **kwargs): + if "x" in mode: + raise OSError("simulated creation failure") + return real_open(path, mode, *args, **kwargs) + + with mock.patch("io.open", side_effect=failing_open), contextlib.redirect_stderr(errors): status = unwrap_self.main([str(source), str(output)]) self.assertEqual(status, 2) self.assertIn("simulated creation failure", errors.getvalue()) self.assertEqual(output.read_bytes(), b"previous output") self.assertEqual(set(directory.iterdir()), before) + def test_temporary_name_collision_preserves_all_existing_files(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + collision = directory / ".unwrap-self-collision" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + output.write_bytes(b"previous output") + collision.write_bytes(b"unrelated file") + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch.object(unwrap_self.secrets, "token_hex", return_value="collision"), \ + contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(collision.read_bytes(), b"unrelated file") + self.assertEqual(source.read_bytes(), container) + self.assertEqual(set(directory.iterdir()), before) + + @unittest.skipUnless(sys.platform == "win32", "Windows ACL inheritance") + def test_new_output_inherits_the_destination_directory_acl(self): + directory = Path(self.directory) + grant = subprocess.run(["icacls.exe", str(directory), "/grant", "*S-1-1-0:(OI)(CI)(RX)"], + capture_output=True, text=True, timeout=30) + self.assertEqual(grant.returncode, 0, grant.stdout + grant.stderr) + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + permissions = directory / "output-acl.txt" + saved = subprocess.run(["icacls.exe", str(output), "/save", str(permissions)], + capture_output=True, text=True, timeout=30) + self.assertEqual(saved.returncode, 0, saved.stdout + saved.stderr) + self.assertIn(";;;WD)", permissions.read_text(encoding="utf-16-le")) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertEqual((directory / "input.self").read_bytes(), container) + self.assertFalse(any(path.name.startswith(".unwrap-self-") for path in directory.iterdir())) + + @unittest.skipUnless(os.name == "posix", "POSIX permission bits") + def test_new_output_permissions_follow_the_process_umask(self): + directory = Path(self.directory) + source = directory / "input.self" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + for mask in (0o000, 0o022, 0o027, 0o077, 0o200, 0o777): + with self.subTest(umask=oct(mask)): + output = directory / f"output-{mask:o}.elf" + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), str(source), str(output)], + capture_output=True, text=True, timeout=30, umask=mask) + self.assertEqual(result.returncode, 0, result.stderr) + mode = output.stat().st_mode & 0o777 + self.assertEqual(mode, 0o666 & ~mask) + output.chmod(mode | 0o400) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertFalse(any(path.name.startswith(".unwrap-self-") for path in directory.iterdir())) + self.assertEqual(source.read_bytes(), container) + + @unittest.skipUnless(os.name == "posix", "POSIX group inheritance") + def test_new_output_inherits_the_destination_directory_group(self): + groups = [group for group in os.getgroups() if group != os.getegid()] + if not groups: + self.skipTest("requires a supplementary group") + directory = Path(self.directory) + group = groups[0] + try: + os.chown(directory, -1, group) + directory.chmod(0o2700) + except PermissionError as error: + self.skipTest(str(error)) + if not directory.stat().st_mode & 0o2000: + self.skipTest("filesystem does not retain the setgid bit") + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.stat().st_gid, group) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertEqual((directory / "input.self").read_bytes(), container) + self.assertEqual(set(directory.iterdir()), {directory / "input.self", output}) + @unittest.skipUnless(os.name == "posix", "POSIX permission bits") def test_replacing_output_preserves_existing_permissions(self): directory = Path(self.directory) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 26f7bfd320..f77a000caf 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -31,9 +31,9 @@ usage: unwrap_self.py <input.self> <output.elf> [--verbose] import argparse import math +import secrets import struct import sys -import tempfile from collections import Counter from pathlib import Path @@ -328,17 +328,16 @@ def unwrap(path, output, verbose=False): % (item["index"], name, item["vaddr"], item["size"], item["new"], moved)) destination.parent.mkdir(parents=True, exist_ok=True) - temporary_path = None + temporary_path = destination.parent / (".unwrap-self-" + secrets.token_hex(16)) + temporary = temporary_path.open("xb") try: - with tempfile.NamedTemporaryFile(mode="wb", dir=destination.parent, prefix=".unwrap-self-", delete=False) as temporary: - temporary_path = Path(temporary.name) + with temporary: temporary.write(result) if sys.platform != "win32" and destination.exists(): temporary_path.chmod(destination.stat().st_mode) temporary_path.replace(destination) finally: - if temporary_path is not None: - temporary_path.unlink(missing_ok=True) + temporary_path.unlink(missing_ok=True) return len(result) From 5053c1e8eb97d345c857110d384ddda02747a8e6 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:33:24 -0500 Subject: [PATCH 1302/1360] test(libSceAudio3d): cover speaker array parameter validation and memory query limits --- core/libs/tests/GuestAudio3dSpeakerArray.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestAudio3dSpeakerArray.cpp b/core/libs/tests/GuestAudio3dSpeakerArray.cpp index 7ef0c9942e..4f188b37c4 100644 --- a/core/libs/tests/GuestAudio3dSpeakerArray.cpp +++ b/core/libs/tests/GuestAudio3dSpeakerArray.cpp @@ -47,6 +47,7 @@ void RequireThrows(F f, const char* message) { int main() { Require(sceAudio3dGetSpeakerArrayMemorySize(4, 1) == sceAudioOut2GetSpeakerArrayMemorySize(4, 1, 0), "memory size forwards to AudioOut2 without ambisonics"); + RequireThrows([] { sceAudio3dGetSpeakerArrayMemorySize(0, 0); }, "zero speakers throws invalid_argument"); const AudioOut2Position positions[4] = {{1, 1, 0}, {1, -1, 0}, {-1, 1, 0}, {-1, -1, 0}}; alignas(16) unsigned char memory[1024] = {}; @@ -68,6 +69,7 @@ int main() { Require(sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients2, 4, 1, 0.5f) == 0, "mix coefficients 2 succeed"); Require(coefficients2[3] == 0.0f, "mix coefficients 2 are written"); RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3); }, "a coefficient count that differs from the array is rejected"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, nullptr, 4); }, "null coefficients pointer throws invalid_argument"); Require(sceAudio3dDeleteSpeakerArray(handle) == 0, "delete succeeds"); RequireThrows([&] { sceAudio3dDeleteSpeakerArray(handle); }, "a deleted array is no longer valid"); From 79e884902e289d6dd68045e5ae233d9108d7a90b Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Fri, 23 Oct 2026 11:33:49 -0500 Subject: [PATCH 1303/1360] test(libSceAudio3d): cover invalid reserved argument rejection in sceAudio3dInitialize --- core/libs/tests/GuestAudio3dParameters.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestAudio3dParameters.cpp b/core/libs/tests/GuestAudio3dParameters.cpp index 4335f1ff0f..dc4e19688a 100644 --- a/core/libs/tests/GuestAudio3dParameters.cpp +++ b/core/libs/tests/GuestAudio3dParameters.cpp @@ -6,6 +6,7 @@ #include <stdexcept> extern "C" void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); +extern "C" int APS5_VABI sceAudio3dInitialize(std::int64_t reserved); static_assert(sizeof(Audio3dOpenParameters) == 0x28); static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); @@ -74,4 +75,5 @@ int main() { CheckInvalidDestination(); CheckShortBuffer(); CheckExtendedBuffer(); + Require(sceAudio3dInitialize(-1) == static_cast<int>(0x80EA0004), "non-zero reserved parameter rejected in sceAudio3dInitialize"); } From ad6a55121ebb5285f65601fd37eb4fd87a0dc8a0 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 20 Oct 2026 11:57:56 +0000 Subject: [PATCH 1304/1360] fix(agc): read FMASK of up to 8 samples in every storage width Since the synthetic FMASK mode was removed, a direct FMASK load reads the stored word and only Fmask32_S8_F8 is accepted. A load from any other FMASK format of up to 8 samples now reads the 8-, 16- or 32-bit surface and expands it in the shader to 4 bits per sample, an index past the fragment count reading 8 (unknown). One runtime image mode handles every such format, reading the format from the descriptor at run time or from a specialization constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/GuestTextureResource.cpp | 9 +- .../tests/GuestTextureResource.cpp | 25 +++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 15 +++- .../tests/execution/FmaskImageReads.cpp | 88 ++++++++++++++++--- core/shader/recompiler/CMakeLists.txt | 1 + .../src/DescriptorBindingBuilder.cpp | 3 + .../Optimization/src/ResourceMaterializer.cpp | 11 ++- .../recompiler/PipelineSpecialization.hpp | 1 + .../RdnaDecoder/RdnaDescriptorFormat.hpp | 12 +++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 15 ++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 36 ++++++++ docs/dev/debt/shader.md | 2 +- 12 files changed, 198 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 1249e08037..866371295c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -179,12 +179,15 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) Require(baseLevel <= lastLevel, "guest texture descriptor has a base mip level past its last mip level"); const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast<ShaderRecompiler::IrBufferFormat>(format)); + const auto fmaskLayout = ShaderRecompiler::FmaskLayoutOf(static_cast<ShaderRecompiler::IrBufferFormat>(format)); if (fmask) { - Require(format == static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented"); + Require(fmaskLayout.has_value(), "guest FMASK format is not implemented"); Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata"); Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned"); + Require(!metaCompress || format == static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface"); - for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); + const bool identity = format != static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8) && dstSelX == 4u && dstSelY == 5u && dstSelZ == 6u && dstSelW == 7u; + for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(identity || selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); } const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; }; @@ -265,7 +268,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) result.lastLevel = lastLevel; result.tileMode = tileMode; result.dimension = dimension; - result.format = fmask ? static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format32UInt) : format; + result.format = !fmask ? format : static_cast<std::uint32_t>(fmaskLayout->storageBits == 8u ? ShaderRecompiler::IrBufferFormat::Format8UInt : fmaskLayout->storageBits == 16u ? ShaderRecompiler::IrBufferFormat::Format16UInt : ShaderRecompiler::IrBufferFormat::Format32UInt); result.dstSelX = static_cast<std::uint8_t>(dstSelX); result.dstSelY = static_cast<std::uint8_t>(dstSelY); result.dstSelZ = static_cast<std::uint8_t>(dstSelZ); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 0cc0c41ed7..ab7db93b7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -184,8 +184,29 @@ void RunGuestTextureResourceTests() { fmaskArray.baseArray = 1u; const auto fmaskArrayResource = DecodeTextureResource(pack(fmaskArray)); Require(fmaskArrayResource.format == 20u && fmaskArrayResource.baseArray == 1u && fmaskArrayResource.depthOrLastArray == 2u, "FMASK normalization changed the array view"); - for (std::uint32_t format = 156u; format <= 168u; ++format) { - if (format == 166u) continue; + constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 8> narrowFormats{{{156u, 5u}, {157u, 5u}, {158u, 5u}, {159u, 5u}, {160u, 5u}, {161u, 5u}, {163u, 11u}, {165u, 20u}}}; + for (const auto& [format, surface] : narrowFormats) { + auto narrow = fmask; + narrow.format = format; + const auto narrowResource = DecodeTextureResource(pack(narrow)); + Require(narrowResource.format == surface && narrowResource.tileMode == TextureTileMode::kZ64KBX, "FMASK " + std::to_string(format) + " did not use an integer Z surface of its storage width"); + narrow.dstSelX = 4u; + narrow.dstSelY = 5u; + narrow.dstSelZ = 6u; + narrow.dstSelW = 7u; + Require(DecodeTextureResource(pack(narrow)).format == surface, "an expanded FMASK " + std::to_string(format) + " view with identity selectors was refused"); + auto compressedNarrow = narrow; + compressedNarrow.metaCompress = compressedNarrow.metaPipeAligned = true; + compressedNarrow.metaAddr = 0x123456789aull; + rejectFields(compressedNarrow, "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); + } + auto identityWord = fmask; + identityWord.dstSelX = 4u; + identityWord.dstSelY = 5u; + identityWord.dstSelZ = 6u; + identityWord.dstSelW = 7u; + rejectFields(identityWord, "FMASK destination channel selector is not implemented"); + for (const auto format : {162u, 164u, 167u, 168u}) { auto unsupported = fmask; unsupported.format = format; rejectFields(unsupported, "FMASK format is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index effc0dc179..63fcc75d84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7577,8 +7577,19 @@ void verifyFmaskImageMode() { const auto modes = ResourceMaterializer::RuntimeImageModes(image); const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); require(modes[selected].numericClass == IrTextureNumericClass::Uint && modes[selected].packedFormat == IrBufferFormat::Invalid && !modes[selected].constantSwizzle, "FMASK did not select a bound integer image mode"); - for (std::uint32_t format = 156u; format <= 168u; ++format) { - if (format == 166u) continue; + for (const auto format : {156u, 157u, 158u, 159u, 160u, 161u, 163u, 165u}) { + descriptor.dwords[1] = format << 20u; + const auto expanded = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(expanded != selected && modes[expanded].numericClass == IrTextureNumericClass::Uint && modes[expanded].conversionFormat == FmaskExpansion && modes[expanded].dimension == RdnaImageDimension::Dim2D, "an FMASK format of up to 8 samples did not select the FMASK expansion mode"); + } + require(std::ranges::count_if(modes, [](const ImageResource& mode) { return mode.conversionFormat == FmaskExpansion; }) == 1, "FMASK expansion needs exactly one runtime image mode"); + for (const auto format : {161u, 166u}) { + descriptor.dwords[1] = format << 20u; + descriptor.dwords[3] = 0x91800005u; + expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK destination channel selector is not implemented", "an FMASK load selecting Y was accepted"); + } + descriptor.dwords[3] = 0x91800004u; + for (const auto format : {162u, 164u, 167u, 168u}) { descriptor.dwords[1] = format << 20u; expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK format is not implemented", "an unsupported FMASK format was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp index b76ffe2a6d..e4b54fc027 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp @@ -32,9 +32,9 @@ std::array<std::uint32_t, 4> Buffer(const void* data, std::uint32_t bytes) { return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, bytes, 0x31016facu}; } -std::array<std::uint32_t, 8> Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u) { +std::array<std::uint32_t, 8> Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u, std::uint32_t format = 166u) { const auto address = reinterpret_cast<std::uintptr_t>(Texels.data()) + pipeXor; - return {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u) | (166u << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; + return {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u) | (format << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; } std::uint32_t Mapping(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { @@ -56,15 +56,16 @@ std::uint64_t EquationOffset(const TextureSwizzleEquation& equation, std::uint32 return offset; } -void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { +template<typename TValue> +void FillWith(bool layered, std::uint32_t pipeXor, std::uint32_t surfaceFormat, std::uint32_t bytes, TValue valueAt) { auto words = Image(layered, pipeXor); - words[1] = (words[1] & ~(0x1ffu << 20u)) | (20u << 20u); + words[1] = (words[1] & ~(0x1ffu << 20u)) | (surfaceFormat << 20u); const auto resource = DecodeTextureResource(words); const auto geometry = DescribeSurface(resource); Require(geometry.guestBytes <= Texels.size(), "FMASK fixture exceeds its allocation"); const auto& mip = geometry.mips.at(0); - const auto block = ThinBlockLayout(resource.tileMode, 4u); - const auto* equation = FindTextureSwizzleEquation(24u, 4u); + const auto block = ThinBlockLayout(resource.tileMode, bytes); + const auto* equation = FindTextureSwizzleEquation(24u, bytes); Require(equation != nullptr, "FMASK fixture has no Z swizzle equation"); std::ranges::fill(Texels, std::byte{0}); for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { @@ -73,14 +74,18 @@ void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { auto offset = geometry.GuestLayerOffset(layer) + mip.tiledOffset; if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, layer) ^ pipeXor; else offset += (static_cast<std::uint64_t>(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + (EquationOffset(*equation, x, y, layer) ^ pipeXor); - const auto value = Mapping(x, y, layer, round); - Require(offset + sizeof(value) <= Texels.size(), "FMASK fixture addresses outside its allocation"); - std::memcpy(Texels.data() + offset, &value, sizeof(value)); + const std::uint32_t value = valueAt(x, y, layer); + Require(offset + bytes <= Texels.size(), "FMASK fixture addresses outside its allocation"); + std::memcpy(Texels.data() + offset, &value, bytes); } } } } +void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { + FillWith(layered, pipeXor, 20u, 4u, [round](std::uint32_t x, std::uint32_t y, std::uint32_t layer) { return Mapping(x, y, layer, round); }); +} + ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::span<const std::uint32_t> image, bool registration) { std::vector<std::uint32_t> userData(16u); const auto input = Buffer(Input.data(), sizeof(Input)); @@ -140,6 +145,68 @@ void Run(AgcDriver::VulkanDevice& device, bool layered, bool registration, std:: } } +struct NarrowFmask { + std::uint32_t format; + std::uint32_t surfaceFormat; + std::uint32_t bytes; + std::uint32_t samples; + std::uint32_t fragments; + std::uint32_t sampleBits; + std::uint32_t identity; + std::uint32_t expandedIdentity; +}; + +constexpr std::array<NarrowFmask, 8> NarrowFormats{{ + {156u, 5u, 1u, 2u, 1u, 1u, 0x02u, 0x80u}, + {157u, 5u, 1u, 4u, 1u, 1u, 0x0eu, 0x8880u}, + {158u, 5u, 1u, 8u, 1u, 1u, 0xfeu, 0x88888880u}, + {159u, 5u, 1u, 2u, 2u, 1u, 0x02u, 0x10u}, + {160u, 5u, 1u, 4u, 2u, 2u, 0xa4u, 0x8810u}, + {161u, 5u, 1u, 4u, 4u, 2u, 0xe4u, 0x3210u}, + {163u, 11u, 2u, 8u, 2u, 2u, 0xaaa4u, 0x88888810u}, + {165u, 20u, 4u, 8u, 4u, 4u, 0x44443210u, 0x88883210u}, +}}; + +std::uint32_t Stored(const NarrowFmask& fmask, std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { + if (round == 0u) return fmask.identity; + const auto value = (x * 0x9e3779b1u) ^ (y * 0x85ebca77u) ^ (layer * 0xc2b2ae3du) ^ (fmask.format * 0x27d4eb2fu); + return fmask.bytes == 4u ? value : value & ((1u << (fmask.bytes * 8u)) - 1u); +} + +std::uint32_t Expanded(const NarrowFmask& fmask, std::uint32_t stored) { + std::uint32_t expanded = 0; + for (std::uint32_t sample = 0; sample < fmask.samples; ++sample) { + const auto fragment = (stored >> (sample * fmask.sampleBits)) & ((1u << fmask.sampleBits) - 1u); + expanded |= (fragment >= fmask.fragments ? 8u : fragment) << (sample * 4u); + } + return expanded; +} + +void RunNarrow(AgcDriver::VulkanDevice& device, const NarrowFmask& fmask, bool layered, bool registration) { + auto code = Code; + if (layered) code[4] = 0xf0001f28u; + Require(Expanded(fmask, fmask.identity) == fmask.expandedIdentity, "FMASK " + std::to_string(fmask.format) + " reference does not expand PAL's identity value"); + for (std::uint32_t round = 0; round < 2u; ++round) { + FillWith(layered, 0u, fmask.surfaceFormat, fmask.bytes, [&](std::uint32_t x, std::uint32_t y, std::uint32_t layer) { return Stored(fmask, x, y, layer, round); }); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Input[lane * 4u] = (lane * 37u + 11u) % Width; + Input[lane * 4u + 1u] = (lane * 23u + 5u) % Height; + Input[lane * 4u + 2u] = layered ? lane & 1u : 0u; + Input[lane * 4u + 3u] = 0u; + } + Output.fill(Sentinel); + const auto shader = Compile(device, code, Image(layered, 0u, 4u, fmask.format), registration); + device.Dispatch(shader, 1u, 1u, 1u); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto stored = Stored(fmask, Input[lane * 4u], Input[lane * 4u + 1u], layered ? Input[lane * 4u + 2u] + 1u : 0u, round); + const std::array expected{Expanded(fmask, stored), 0u, 0u, 0u}; + for (std::uint32_t component = 0; component < 4u; ++component) Require(Output[lane * 4u + component] == expected[component], "FMASK " + std::to_string(fmask.format) + " load did not expand its mapping: round " + std::to_string(round) + ", lane " + std::to_string(lane) + ", component " + std::to_string(component) + ", read " + std::to_string(Output[lane * 4u + component]) + ", expected " + std::to_string(expected[component])); + } + Require(std::all_of(Output.begin() + Threads * 4u, Output.end(), [](auto value) { return value == Sentinel; }), "FMASK load wrote outside its output"); + } +} + } int main() { @@ -149,9 +216,10 @@ int main() { for (const bool registration : {false, true}) { for (const bool layered : {false, true}) { for (const auto pipeXor : {0u, 0xf00u}) Run(*device, layered, registration, pipeXor); + for (const auto& fmask : NarrowFormats) RunNarrow(*device, fmask, layered, registration); } } - std::cout << "FMASK image reads passed: 40 dispatches, 5120 component checks, 8 compression refusals\n"; + std::cout << "FMASK image reads passed: 104 dispatches, 13312 component checks, 8 compression refusals\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 0f83ea58a8..07dee3a0d9 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -249,6 +249,7 @@ function(add_shader_recompiler target runtimeTarget) ImageTableAbi::PoisonCode ImageTableAbi::PoisonReasonName ImageTableAbi::TableHeader + FmaskLayoutOf IsFmaskTextureFormat PipelineSpecialization::IsWorkgroupOffset PipelineSpecialization::WorkgroupOffsetBase diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 170b97d6b8..51b8c966e9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -1,5 +1,6 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "Optimization/ResourceMaterializer.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include <spirv/unified1/spirv.hpp> @@ -355,6 +356,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { if (info.images[index].table != NoTable) continue; + const auto& modes = info.runtimeImageModes.at(index); + if (std::ranges::any_of(modes, [](const ImageResource& mode) { return mode.conversionFormat == FmaskExpansion; })) plan.specialization.push_back({PipelineSpecialization::FmaskFormatBase + index, modes.at(imageModes[index]).conversionFormat == FmaskExpansion ? (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu : 0u}); const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5478136a1a..6514f474aa 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -232,9 +232,14 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (decoded.fmask && (storage || base.depthCompare || table)) { return rejectImage(PoisonReason::Fmask, "FMASK requires a direct sampled image load"); } - if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); + if (decoded.fmask && !FmaskLayoutOf(format).has_value()) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); if (decoded.fmask && ((descriptor.dwords[6] & (1u << 20u)) != 0u || ((descriptor.dwords[6] & (1u << 21u)) != 0u && (descriptor.dwords[6] >> 24u) == 0u && descriptor.dwords[7] == 0u))) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented without readable metadata"); + if (decoded.fmask && (descriptor.dwords[6] & (1u << 21u)) != 0u && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); if (decoded.fmask && (base.packed || base.byElements != 0u)) return rejectImage(PoisonReason::Fmask, "packed FMASK image reads are not implemented"); + for (std::uint32_t component = 0; decoded.fmask && component < 4u; ++component) { + const auto selector = (descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u; + if (selector != 0u && selector != 1u && selector != 4u) return rejectImage(PoisonReason::Fmask, "FMASK destination channel selector is not implemented"); + } if (base.packed) { if (table || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { return rejectImage(PoisonReason::InvalidFormat, "packed image access requires a direct image, with identity swizzle when sampled"); @@ -271,6 +276,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) return rejectImage(PoisonReason::Conversion, "selects a channel the converted image format does not have"); } } + if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) decoded.conversionFormat = FmaskExpansion; decoded.srgbDecode = !storage && !base.depthCompare && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (decoded.srgbDecode && !base.srgbDecodeCompatible) { return rejectImage(PoisonReason::SrgbUnsupported, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); @@ -1882,7 +1888,7 @@ std::vector<ImageResource> ResourceMaterializer::RuntimeImageModes(const ImageRe mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; + const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible && conversion != FmaskExpansion; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; const bool volumePlaneBias = image.dimension == RdnaImageDimension::Dim2D && image.singleMipPlaneBiasCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; const bool volumeArraySample = image.dimension == RdnaImageDimension::Dim2DArray && image.volumeArraySampleCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; @@ -1982,6 +1988,7 @@ std::vector<ImageResource> ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, false); append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, true); } + if (image.fmaskCompatible && !table) append(IrTextureNumericClass::Uint, FmaskExpansion, IrBufferFormat::Invalid, false, false); } } } diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index ae9ec178cf..2adcd7e04a 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -42,6 +42,7 @@ inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; +inline constexpr std::uint32_t FmaskFormatBase = 57472u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index e36217d958..83c1f217aa 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -4,6 +4,7 @@ #include "IntermediateRepresentation/IrMetadata.hpp" #include <array> #include <cstdint> +#include <optional> #include <span> namespace ShaderRecompiler { @@ -21,6 +22,17 @@ enum class ImageType : std::uint32_t { [[nodiscard]] IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format); [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); + +struct FmaskLayout { + std::uint32_t samples; + std::uint32_t fragments; + std::uint32_t sampleBits; + std::uint32_t storageBits; +}; + +inline constexpr IrBufferFormat FmaskExpansion = IrBufferFormat::Fmask8_S2_F1; + +[[nodiscard]] std::optional<FmaskLayout> FmaskLayoutOf(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapStorageTextureFormat(IrBufferFormat format); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 763deb31fb..acdbad34d0 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -112,6 +112,21 @@ bool IsFmaskTextureFormat(IrBufferFormat format) { return format >= IrBufferFormat::Fmask8_S2_F1 && format <= IrBufferFormat::Fmask64_S16_F8; } +std::optional<FmaskLayout> FmaskLayoutOf(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Fmask8_S2_F1: return FmaskLayout{2u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S4_F1: return FmaskLayout{4u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S8_F1: return FmaskLayout{8u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S2_F2: return FmaskLayout{2u, 2u, 1u, 8u}; + case IrBufferFormat::Fmask8_S4_F2: return FmaskLayout{4u, 2u, 2u, 8u}; + case IrBufferFormat::Fmask8_S4_F4: return FmaskLayout{4u, 4u, 2u, 8u}; + case IrBufferFormat::Fmask16_S8_F2: return FmaskLayout{8u, 2u, 2u, 16u}; + case IrBufferFormat::Fmask32_S8_F4: return FmaskLayout{8u, 4u, 4u, 32u}; + case IrBufferFormat::Fmask32_S8_F8: return FmaskLayout{8u, 8u, 4u, 32u}; + default: return std::nullopt; + } +} + IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { const FormatInfo* info = FindFormatInfo(format); if (info == nullptr || !info->sampled) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 804aeda284..6b4c1b2d0d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -558,11 +558,47 @@ void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAcce } } +std::uint32_t ExpandFmaskTexel(SpirvEmitterState& state, const ImageEmitAccess& access, std::uint32_t texel) { + const auto resource = access.mem.resource; + const auto u32 = TypeU32(state); + const auto format = state.registrationModules + ? Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 1u), ConstantU32(state, 20u)), ConstantU32(state, 0x1ffu)) + : state.module.SpecializationConstant(u32, PipelineSpecialization::FmaskFormatBase + resource, 0u); + auto layout = ConstantU32(state, 0u); + for (auto value = static_cast<std::uint32_t>(IrBufferFormat::Fmask8_S2_F1); value <= static_cast<std::uint32_t>(IrBufferFormat::Fmask64_S16_F8); ++value) { + const auto fmask = FmaskLayoutOf(static_cast<IrBufferFormat>(value)); + if (!fmask.has_value()) continue; + layout = Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), format, ConstantU32(state, value)), ConstantU32(state, fmask->sampleBits | (fmask->samples << 8u) | (fmask->fragments << 16u)), layout); + } + const auto sampleBits = Binary(state, spv::OpBitwiseAnd, u32, layout, ConstantU32(state, 0xffu)); + const auto samples = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, layout, ConstantU32(state, 8u)), ConstantU32(state, 0xffu)); + const auto fragments = Binary(state, spv::OpShiftRightLogical, u32, layout, ConstantU32(state, 16u)); + const auto packed = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, packed, texel, 0u); + auto expanded = ConstantU32(state, 0u); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto field = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldUExtract, u32, field, packed, Binary(state, spv::OpIMul, u32, sampleBits, ConstantU32(state, sample)), sampleBits); + const auto fragment = Select(state, u32, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), field, fragments), ConstantU32(state, 8u), field); + const auto nibble = Select(state, u32, Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, sample), samples), fragment, ConstantU32(state, 0u)); + expanded = Binary(state, spv::OpBitwiseOr, u32, expanded, Binary(state, spv::OpShiftLeftLogical, u32, nibble, ConstantU32(state, sample * 4u))); + } + const std::array<std::uint32_t, 4> components{expanded, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)}; + std::uint32_t selected[4] = {}; + for (std::uint32_t component = 0; component < 4u; component++) selected[component] = RuntimeSwizzledComponent(state, RuntimeImageSwizzle(state, resource, component), components); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, selected[0], selected[1], selected[2], selected[3]); + return result; +} + 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); } + if (access.image.conversionFormat == FmaskExpansion) { + return ExpandFmaskTexel(state, access, texel); + } const auto info = ImageConversionFormat(access.image); if (info.format == IrBufferFormat::Invalid) { return texel; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index df60e02b6b..448f187bd3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -46,7 +46,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. -- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. +- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. From da5c03c20f8d33a78c27608a2f5840cb77e3a145 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:42:45 +0000 Subject: [PATCH 1305/1360] test(tools): normalize Windows report paths --- tools/tests/test_env_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 51fac32289..388160602c 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -194,7 +194,7 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(env_reference.main(["--root", str(core)]), 0) self.assertEqual((out.encoding, out.errors), ("utf-8", "backslashreplace")) self.assertEqual((err.encoding, err.errors), ("utf-8", "backslashreplace")) - self.assertIn(str(core / "Sample.cpp"), out.text) + self.assertIn((core / "Sample.cpp").as_posix(), out.text) def test_main_reports_unicode_scan_root_with_non_utf8_stderr(self): class Stream: From f334c156e11969b7ce1484f0ab6277e0fc195557 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 13:49:03 -0300 Subject: [PATCH 1306/1360] fix(libkernel): keep socket buffer sizes and SO_NBIO as measured on a PS5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Socket/include/SocketOptions.hpp | 8 ++- .../libkernel/Socket/src/SocketOptions.cpp | 62 ++++++++++++++----- core/libs/prx/libkernel/Socket/src/Udp.cpp | 10 ++- core/libs/tests/GuestSocketOptions.cpp | 50 +++++++++++++++ core/libs/tests/GuestSocketStreamOptions.cpp | 4 +- docs/dev/debt/libkernel.md | 6 +- 6 files changed, 116 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 56bbe00efe..523842f51c 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -9,11 +9,15 @@ struct SocketOptionState { bool noSignal = false; std::int64_t sendTimeout = 0; std::int64_t receiveTimeout = 0; + int sendBuffer = 0; + int receiveBuffer = 0; + int sendLowWater = 0; + int receiveLowWater = 0; }; bool NativeListening(std::uintptr_t native, bool& listening); -int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); -int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length); +int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, const void* value, std::uint32_t length); +int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, void* value, std::uint32_t* length); int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length); int GetTcpLevelOption(std::uintptr_t native, bool tcp, int option, void* value, std::uint32_t* length); int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 37fce2af2e..77abe31e34 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -30,11 +30,15 @@ constexpr int GuestOobInline = 0x100; constexpr int GuestNoSigpipe = 0x800; constexpr int GuestSendBuffer = 0x1001; constexpr int GuestReceiveBuffer = 0x1002; +constexpr int GuestSendLowWater = 0x1003; +constexpr int GuestReceiveLowWater = 0x1004; constexpr int GuestSendTimeout = 0x1005; constexpr int GuestReceiveTimeout = 0x1006; constexpr int GuestError = 0x1007; constexpr int GuestType = 0x1008; constexpr int GuestNonBlocking = 0x1200; +constexpr int GuestNonBlockingState = 0x100; +constexpr int GuestUnix = 1; constexpr int GuestTcpNoDelay = 1; constexpr int GuestTcpNoDelayFlag = 4; constexpr int GuestIpv6Only = 27; @@ -42,6 +46,9 @@ constexpr int GuestIpv6Only = 27; constexpr int ErrorInvalid = 22; constexpr int ErrorDomain = 33; constexpr int ErrorNoProtocolOption = 42; +constexpr int ErrorNoBuffers = 55; +constexpr int MaximumReservation = 5592405; +constexpr int MaximumBuffer = 524288; constexpr int MaximumLinger = 65535; constexpr std::int64_t LongestTimeout = 0x7fffffffffffffffLL; @@ -87,9 +94,21 @@ int NativeBoolean(int option) { } } +void EnsureBuffers(SocketOptionState& state, int family, int type) { + if (state.sendBuffer != 0) return; + if (family == GuestUnix) { + state.sendBuffer = type == 2 ? 2048 : 8192; + state.receiveBuffer = type == 2 ? 4096 : 8192; + } else { + state.sendBuffer = type == 1 ? 32768 : 9216; + state.receiveBuffer = type == 1 ? 65536 : 42080; + } + state.sendLowWater = std::min(2048, state.sendBuffer); + state.receiveLowWater = 1; +} + int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { const std::uint32_t count = std::min(*length, size); - if (count != 0 && !output) return ErrorInvalid; if (count != 0) std::memcpy(output, value, count); *length = count; return 0; @@ -97,11 +116,10 @@ int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t s } -int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length) { +int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, const void* value, std::uint32_t length) { const auto handle = static_cast<NativeHandle>(native); if (option == GuestLinger) { if (length < sizeof(GuestLingerValue)) return ErrorInvalid; - if (!value) return ErrorInvalid; GuestLingerValue guest; std::memcpy(&guest, value, sizeof(guest)); if (guest.seconds < 0 || guest.seconds > MaximumLinger) return ErrorDomain; @@ -119,7 +137,6 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op } if (option == GuestSendTimeout || option == GuestReceiveTimeout) { if (length < sizeof(GuestTimeval)) return ErrorInvalid; - if (!value) return ErrorInvalid; GuestTimeval time; std::memcpy(&time, value, sizeof(time)); if (time.seconds < 0 || time.microseconds < 0 || time.microseconds >= 1000000) return ErrorDomain; @@ -128,7 +145,6 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op } if (option == GuestNoSigpipe || option == GuestNonBlocking) { if (length < sizeof(int)) return ErrorInvalid; - if (!value) return ErrorInvalid; int number; std::memcpy(&number, value, sizeof(number)); if (option == GuestNoSigpipe) { @@ -146,15 +162,28 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op return 0; } const bool native_boolean = NativeBoolean(option) != -1; - if (native_boolean || option == GuestSendBuffer || option == GuestReceiveBuffer) { + const bool buffer = option == GuestSendBuffer || option == GuestReceiveBuffer; + const bool lowWater = option == GuestSendLowWater || option == GuestReceiveLowWater; + if (native_boolean || buffer || lowWater) { if (length < sizeof(int)) return ErrorInvalid; - if (!value) return ErrorInvalid; int number; std::memcpy(&number, value, sizeof(number)); - if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + if (buffer || lowWater) { if (number < 1) return ErrorInvalid; - const int nativeOption = option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF; - if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast<NativeOptionValue>(&number), sizeof(number))) return HostError(); + EnsureBuffers(state, family, type); + const bool send = option == GuestSendBuffer || option == GuestSendLowWater; + int& size = send ? state.sendBuffer : state.receiveBuffer; + int& mark = send ? state.sendLowWater : state.receiveLowWater; + if (lowWater) { + mark = std::min(number, size); + return 0; + } + if (number > MaximumReservation) return ErrorNoBuffers; + const int reserved = std::min(number, MaximumBuffer); + const int nativeOption = send ? SO_SNDBUF : SO_RCVBUF; + if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast<NativeOptionValue>(&reserved), sizeof(reserved))) return HostError(); + size = reserved; + mark = std::min(mark, size); return 0; } const int enabled = number != 0 ? 1 : 0; @@ -178,7 +207,7 @@ bool NativeListening(std::uintptr_t native, bool& listening) { return true; } -int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length) { +int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, void* value, std::uint32_t* length) { const auto handle = static_cast<NativeHandle>(native); if (option == GuestLinger) { linger current{}; @@ -206,7 +235,7 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty } else if (option == GuestNoSigpipe) { number = state.noSignal ? GuestNoSigpipe : 0; } else if (option == GuestNonBlocking) { - number = state.nonblocking ? 1 : 0; + number = state.nonblocking ? GuestNonBlockingState : 0; } else if (option == GuestError) { int pending = 0; socklen_t size = sizeof(pending); @@ -216,9 +245,10 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty else if (pending != 0) state.reportedError = pending; #endif number = pending == 0 ? 0 : GuestErrorFromHost(pending); - } else if (option == GuestSendBuffer || option == GuestReceiveBuffer) { - socklen_t size = sizeof(number); - if (::getsockopt(handle, SOL_SOCKET, option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast<NativeOptionOutput>(&number), &size)) return HostError(); + } else if (option == GuestSendBuffer || option == GuestReceiveBuffer || option == GuestSendLowWater || option == GuestReceiveLowWater) { + EnsureBuffers(state, family, type); + number = option == GuestSendBuffer ? state.sendBuffer : option == GuestReceiveBuffer ? state.receiveBuffer + : option == GuestSendLowWater ? state.sendLowWater : state.receiveLowWater; } else { return ErrorNoProtocolOption; } @@ -229,7 +259,6 @@ int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* v if (!tcp) return ErrorInvalid; if (option != GuestTcpNoDelay) return ErrorNoProtocolOption; if (length < sizeof(int)) return ErrorInvalid; - if (!value) return ErrorInvalid; int number; std::memcpy(&number, value, sizeof(number)); const int enabled = number != 0 ? 1 : 0; @@ -251,7 +280,6 @@ int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* if (!ipv6) return ErrorInvalid; if (option != GuestIpv6Only) return ErrorNoProtocolOption; if (length != sizeof(int)) return ErrorInvalid; - if (!value) return ErrorInvalid; int number; std::memcpy(&number, value, sizeof(number)); const int enabled = number != 0 ? 1 : 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index f17394f4e4..a63772505c 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1113,6 +1113,7 @@ int APS5_VABI _fcntl_nid_postfix(int descriptor, int command, ...) { } int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { + if (!value && length != 0) return Fail(14); int netError = 0; if (!GuestSockets::IsOpen(descriptor) && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { return netError == 0 ? 0 : Fail(netError); @@ -1124,7 +1125,7 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const auto native = static_cast<std::uintptr_t>(socket->value); const int error = level == 6 ? GuestSockets::SetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) : level == 41 ? GuestSockets::SetIpv6LevelOption(native, socket->family == 28, option, value, length) - : GuestSockets::SetSocketLevelOption(native, socket->options, option, value, length); + : GuestSockets::SetSocketLevelOption(native, socket->options, socket->family, socket->type, option, value, length); return error == 0 ? 0 : Fail(error); } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, @@ -1133,11 +1134,12 @@ int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, if (!socket) return -1; if (level != 0xffff && level != 6 && level != 41) return Fail(42); if (!length) return Fail(22); + if (!value) *length = 0; std::lock_guard lock(socket->modeMutex); const auto native = static_cast<std::uintptr_t>(socket->value); const int error = level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) : level == 41 ? GuestSockets::GetIpv6LevelOption(native, socket->family == 28, option, value, length) - : GuestSockets::GetSocketLevelOption(native, socket->options, socket->type, option, value, length); + : GuestSockets::GetSocketLevelOption(native, socket->options, socket->family, socket->type, option, value, length); return error == 0 ? 0 : Fail(error); } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { @@ -1271,6 +1273,10 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l accepted->options.noSignal = listener->options.noSignal; accepted->options.sendTimeout = listener->options.sendTimeout; accepted->options.receiveTimeout = listener->options.receiveTimeout; + accepted->options.sendBuffer = listener->options.sendBuffer; + accepted->options.receiveBuffer = listener->options.receiveBuffer; + accepted->options.sendLowWater = listener->options.sendLowWater; + accepted->options.receiveLowWater = listener->options.receiveLowWater; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; const int acceptedDescriptor = Install(std::move(accepted)); diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index 7ab20ba2f4..730c174523 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -14,6 +14,7 @@ int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI poll_nid_postfix(void*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); @@ -35,6 +36,9 @@ constexpr int SoReusePort = 0x200; constexpr int SoTimestamp = 0x400; constexpr int SoNoSigpipe = 0x800; constexpr int SoSendBuffer = 0x1001; +constexpr int SoReceiveBuffer = 0x1002; +constexpr int SoSendLowWater = 0x1003; +constexpr int SoReceiveLowWater = 0x1004; constexpr int SoError = 0x1007; constexpr int SoType = 0x1008; constexpr int ErrorInvalid = 22; @@ -204,6 +208,50 @@ void LengthsFollowFreeBsd() { Require(close_nid_postfix(stream) == 0, "close failed"); } +void BuffersFollowThePs5() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(28, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + Require(GetInt(stream, SoSendBuffer) == 32768 && GetInt(stream, SoReceiveBuffer) == 65536, "a stream socket starts with 32768 and 65536 bytes"); + Require(GetInt(stream, SoSendLowWater) == 2048 && GetInt(stream, SoReceiveLowWater) == 1, "the low-water marks start at 2048 and 1"); + Require(GetInt(datagram, SoSendBuffer) == 9216 && GetInt(datagram, SoReceiveBuffer) == 42080, "a datagram socket starts with 9216 and 42080 bytes"); + SetInt(stream, SoSendBuffer, 1000); + Require(GetInt(stream, SoSendBuffer) == 1000 && GetInt(stream, SoSendLowWater) == 1000, "a smaller buffer is kept and lowers its low-water mark"); + SetInt(stream, SoReceiveBuffer, 524289); + Require(GetInt(stream, SoReceiveBuffer) == 524288, "a buffer above 512 KiB is limited to 524288 bytes"); + SetInt(stream, SoSendBuffer, 5592405); + Require(GetInt(stream, SoSendBuffer) == 524288, "the largest reservation is accepted and limited"); + const int tooLarge = 5592406; + Require(SetFails(stream, SolSocket, SoSendBuffer, &tooLarge, sizeof(tooLarge), 55), "a reservation above sb_max must fail with ENOBUFS (55)"); + Require(GetInt(stream, SoSendBuffer) == 524288, "a failed reservation keeps the buffer"); + SetInt(stream, SoReceiveLowWater, 600000); + Require(GetInt(stream, SoReceiveLowWater) == 524288, "a low-water mark above the buffer is limited to it"); + SetInt(stream, SoReceiveBuffer, 50); + Require(GetInt(stream, SoReceiveLowWater) == 50, "a smaller buffer lowers the low-water mark"); + const int zero = 0; + Require(SetFails(stream, SolSocket, SoReceiveLowWater, &zero, sizeof(zero), ErrorInvalid), "a low-water mark below 1 must fail with EINVAL (22)"); + int pair[2] = {-1, -1}; + Require(socketpair_nid_postfix(1, Stream, 0, pair) == 0, "socketpair failed"); + Require(GetInt(pair[0], SoSendBuffer) == 8192 && GetInt(pair[0], SoReceiveBuffer) == 8192, "an AF_UNIX stream socket starts with 8192 bytes each way"); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0, "close failed"); + Require(socketpair_nid_postfix(1, Datagram, 0, pair) == 0, "socketpair failed"); + Require(GetInt(pair[0], SoSendBuffer) == 2048 && GetInt(pair[0], SoReceiveBuffer) == 4096, "an AF_UNIX datagram socket starts with 2048 and 4096 bytes"); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0, "close failed"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void NullValues() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + Require(SetFails(stream, SolSocket, SoKeepAlive, nullptr, sizeof(int), 14), "a null value with a length must fail with EFAULT (14)"); + Require(SetFails(-1, SolSocket, SoKeepAlive, nullptr, sizeof(int), 14), "the null value is checked before the descriptor"); + Require(SetFails(stream, SolSocket, 0x7fff, nullptr, sizeof(int), 14), "the null value is checked before the option"); + Require(SetFails(stream, SolSocket, SoKeepAlive, nullptr, 0, ErrorInvalid), "a null value without a length must fail with EINVAL (22)"); + std::uint32_t size = sizeof(int); + Require(getsockopt_nid_postfix(stream, SolSocket, SoSendBuffer, nullptr, &size) == 0 && size == 0, "getsockopt without a value succeeds with length 0"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + void UnknownRequestsFail() { const int stream = socket_nid_postfix(2, Stream, 0); Require(stream >= 0, "socket failed"); @@ -224,6 +272,8 @@ int main() { BooleanOptionsReportTheirBit(); LingerFollowsFreeBsd(); LengthsFollowFreeBsd(); + BuffersFollowThePs5(); + NullValues(); UnknownRequestsFail(); return 0; } diff --git a/core/libs/tests/GuestSocketStreamOptions.cpp b/core/libs/tests/GuestSocketStreamOptions.cpp index f85a693f17..2d03377a36 100644 --- a/core/libs/tests/GuestSocketStreamOptions.cpp +++ b/core/libs/tests/GuestSocketStreamOptions.cpp @@ -124,13 +124,13 @@ int main() { char received = 0; Require(Get(server, SocketLevel, NonBlockingOption) == 0); Require((fcntl_nid_postfix(server, GetStatusFlags) & NonBlockingFlag) == 0); - Require(Set(server, SocketLevel, NonBlockingOption, 5) && Get(server, SocketLevel, NonBlockingOption) == 1); + Require(Set(server, SocketLevel, NonBlockingOption, 5) && Get(server, SocketLevel, NonBlockingOption) == 0x100); Require((fcntl_nid_postfix(server, GetStatusFlags) & NonBlockingFlag) != 0); Require(Fails(recv_nid_postfix(server, &received, sizeof(received), 0), Again)); Require(Set(server, SocketLevel, NonBlockingOption, 0) && Get(server, SocketLevel, NonBlockingOption) == 0); Require((fcntl_nid_postfix(server, GetStatusFlags) & NonBlockingFlag) == 0); Require(Fails(setsockopt_nid_postfix(server, SocketLevel, NonBlockingOption, &one, sizeof(one) - 1), Invalid)); - Require(Set(datagram, SocketLevel, NonBlockingOption, 1) && Get(datagram, SocketLevel, NonBlockingOption) == 1); + Require(Set(datagram, SocketLevel, NonBlockingOption, 1) && Get(datagram, SocketLevel, NonBlockingOption) == 0x100); Require(Fails(recv_nid_postfix(datagram, &received, sizeof(received), 0), Again)); #ifndef _WIN32 diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..0a121b798f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -210,7 +210,11 @@ - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`. `SO_DEBUG`, `SO_REUSEPORT` and `SO_TIMESTAMP`, `IP_TTL` and `IP_TOS`, `TCP_MAXSEG` and `TCP_KEEPIDLE` fail with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) accepted them and read back their bit, 64, 0, 536 and 7200; the rest of the `IPPROTO_IP` level, the other `IPPROTO_TCP` and `IPPROTO_IPV6` options and `SO_USELOOPBACK` fail the same way and were not measured. `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean option reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden +- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60), and the host socket gets the same size. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured +- [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console +- [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener +- [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked From 287b149950b69b1fee370a147906c081fb0974bd Mon Sep 17 00:00:00 2001 From: mazen <mazenshaar@hotmail.com> Date: Mon, 19 Oct 2026 16:24:29 +0400 Subject: [PATCH 1307/1360] fix(shader): keep an unbounded pixel LDS array in a storage buffer A Function array of 8192 dwords faults NVIDIA driver 617.42. The guest array stays that size, and each invocation gets its own region in a buffer. --- .../Execution/src/VulkanDevice.cpp | 2 + .../libSceAgcDriver/Graphics/include/Draw.hpp | 11 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 160 +++++++++++++++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 12 +- .../Graphics/src/ShaderValidation.cpp | 5 + .../tests/execution/PixelLdsLimit.cpp | 165 ++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 10 +- core/shader/recompiler/Recompiler.hpp | 3 +- core/shader/recompiler/ShaderDiskCache.cpp | 2 + .../include/SpirvBackend/SpirvAnalysis.hpp | 2 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 10 +- .../src/SpirvMemory/SpirvBufferAccess.cpp | 50 +++++- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 8 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 4 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 18 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index fe3802c146..c6e39b0346 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -29,6 +29,7 @@ #include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" @@ -2284,6 +2285,7 @@ void VulkanDevice::WaitIdle() { state->recorder->Sync(); } check(state->DeviceFunction<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(state->device), "vkDeviceWaitIdle"); + Graphics::CheckPixelFunctionLds(graphicsContext()); APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle complete"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 535e799c9a..e2ffa0f98d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -18,6 +18,17 @@ struct HostImport; // built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under // APS5_NO_DRAW_RECIPE=1). void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr); + +struct PixelFunctionLdsLayouts { + VkDescriptorSetLayout empty = VK_NULL_HANDLE; + VkDescriptorSetLayout buffer = VK_NULL_HANDLE; +}; + +PixelFunctionLdsLayouts PixelFunctionLdsSetLayouts(const Context& context); +void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, std::uint32_t subgroupSize); +void BindPixelFunctionLds(const Context& context, VkCommandBuffer commands, VkPipelineLayout layout); +void CheckPixelFunctionLds(const Context& context); +void ReleasePixelFunctionLds(VkDevice device); std::optional<std::string> KnownValidationFailure(const Context& context, std::span<const CompiledShader> shaders, const State& state); std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std::span<const VkImageView> targetViews); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index fc04136642..befeb99d09 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -20,6 +20,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvAnalysis.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include <algorithm> @@ -42,6 +43,159 @@ namespace AgcDriver::Graphics { +namespace { + +struct PixelFunctionLdsPool { + Context context{}; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorSetLayout empty = VK_NULL_HANDLE; + VkDescriptorSetLayout bufferLayout = VK_NULL_HANDLE; + std::uint32_t* header = nullptr; + std::uint32_t subgroupSize = 0; + std::uint32_t slots = 0; + bool announced = false; +}; + +std::mutex& PixelLdsMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map<VkDevice, PixelFunctionLdsPool>& PixelLdsPools() { + static std::unordered_map<VkDevice, PixelFunctionLdsPool> pools; + return pools; +} + +void DestroyPixelFunctionLds(PixelFunctionLdsPool& pool) { + if (pool.header != nullptr) pool.context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")(pool.context.device, pool.memory); + if (pool.descriptorPool != VK_NULL_HANDLE) pool.context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(pool.context.device, pool.descriptorPool, nullptr); + if (pool.buffer != VK_NULL_HANDLE) pool.context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(pool.context.device, pool.buffer, nullptr); + if (pool.memory != VK_NULL_HANDLE) pool.context.Function<PFN_vkFreeMemory>("vkFreeMemory")(pool.context.device, pool.memory, nullptr); + if (pool.bufferLayout != VK_NULL_HANDLE) pool.context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(pool.context.device, pool.bufferLayout, nullptr); + if (pool.empty != VK_NULL_HANDLE) pool.context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(pool.context.device, pool.empty, nullptr); + pool.header = nullptr; + pool.descriptorPool = VK_NULL_HANDLE; + pool.set = VK_NULL_HANDLE; + pool.buffer = VK_NULL_HANDLE; + pool.memory = VK_NULL_HANDLE; + pool.bufferLayout = VK_NULL_HANDLE; + pool.empty = VK_NULL_HANDLE; +} + +} + +PixelFunctionLdsLayouts PixelFunctionLdsSetLayouts(const Context& context) { + std::lock_guard lock(PixelLdsMutex()); + auto& pool = PixelLdsPools()[context.device]; + if (pool.bufferLayout == VK_NULL_HANDLE) { + pool.context = context; + VkDescriptorSetLayoutCreateInfo emptyInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + Check(context.Function<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(context.device, &emptyInfo, nullptr, &pool.empty), "vkCreateDescriptorSetLayout pixel function LDS empty"); + const VkDescriptorSetLayoutBinding binding{0u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = 1u; + info.pBindings = &binding; + Check(context.Function<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &pool.bufferLayout), "vkCreateDescriptorSetLayout pixel function LDS"); + } + return {pool.empty, pool.bufferLayout}; +} + +void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, std::uint32_t subgroupSize) { + const auto operations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_BALLOT_BIT; + Require((context.subgroup.supportedStages & VK_SHADER_STAGE_FRAGMENT_BIT) != 0 && (context.subgroup.supportedOperations & operations) == operations, "pixel function LDS needs fragment subgroup elect, ballot and broadcast"); + Require(subgroupSize != 0u && subgroupSize == context.subgroup.subgroupSize, "pixel function LDS subgroup size does not match the device"); + std::lock_guard lock(PixelLdsMutex()); + auto& pool = PixelLdsPools()[context.device]; + Require(pool.bufferLayout != VK_NULL_HANDLE, "pixel function LDS descriptor set layout was not created"); + if (pool.buffer == VK_NULL_HANDLE) { + const auto stride = static_cast<std::uint64_t>(subgroupSize) * ShaderRecompiler::FunctionLdsDwordLimit * 4ull; + const auto budget = std::min<std::uint64_t>(context.limits.maxStorageBufferRange, 2ull << 30); + Require(budget > 16ull + stride, "pixel function LDS does not fit in the device storage buffer range"); + pool.slots = static_cast<std::uint32_t>((budget - 16ull) / stride); + pool.subgroupSize = subgroupSize; + const auto bytes = 16ull + stride * pool.slots; + VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.size = bytes; + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + Check(context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &bufferInfo, nullptr, &pool.buffer), "vkCreateBuffer pixel function LDS"); + VkMemoryRequirements requirements{}; + context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, pool.buffer, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + const auto allocated = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + if (allocated != VK_SUCCESS) { + context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, pool.buffer, nullptr); + pool.buffer = VK_NULL_HANDLE; + Check(allocated, "vkAllocateMemory pixel function LDS"); + } + Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, pool.buffer, pool.memory, 0), "vkBindBufferMemory pixel function LDS"); + Check(context.Function<PFN_vkMapMemory>("vkMapMemory")(context.device, pool.memory, 0, 16, 0, reinterpret_cast<void**>(&pool.header)), "vkMapMemory pixel function LDS"); + pool.header[0] = 0u; + pool.header[1] = 0u; + pool.header[2] = pool.slots; + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1u; + poolInfo.poolSizeCount = 1u; + poolInfo.pPoolSizes = &size; + Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool.descriptorPool), "vkCreateDescriptorPool pixel function LDS"); + VkDescriptorSetAllocateInfo sets{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + sets.descriptorPool = pool.descriptorPool; + sets.descriptorSetCount = 1u; + sets.pSetLayouts = &pool.bufferLayout; + Check(context.Function<PFN_vkAllocateDescriptorSets>("vkAllocateDescriptorSets")(context.device, &sets, &pool.set), "vkAllocateDescriptorSets pixel function LDS"); + const VkDescriptorBufferInfo buffer{pool.buffer, 0, bytes}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = pool.set; + write.dstBinding = 0u; + write.descriptorCount = 1u; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &buffer; + context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + std::fprintf(stderr, "[gpu] pixel function LDS pool: %u subgroups, %llu MiB, stride %u lanes\n", pool.slots, static_cast<unsigned long long>(bytes >> 20u), subgroupSize); + pool.announced = true; + } + Require(pool.subgroupSize == subgroupSize, "pixel function LDS subgroup size does not match the pool"); + context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, pool.buffer, 0, 8, 0u); + const VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); +} + +std::uint32_t PixelFunctionLdsSubgroup(std::span<const CompiledShader> shaders) { + std::uint32_t subgroup = 0u; + for (const auto& shader : shaders) { + if (shader.program != nullptr && shader.program->functionLdsBuffer) subgroup = shader.program->hostSubgroupSize; + } + return subgroup; +} + +void BindPixelFunctionLds(const Context& context, VkCommandBuffer commands, VkPipelineLayout layout) { + std::lock_guard lock(PixelLdsMutex()); + const auto& pool = PixelLdsPools().at(context.device); + Require(pool.set != VK_NULL_HANDLE, "pixel function LDS buffer was not prepared"); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, ShaderRecompiler::PixelFunctionLdsDescriptorSet, 1, &pool.set, 0, nullptr); +} + +void CheckPixelFunctionLds(const Context& context) { + std::lock_guard lock(PixelLdsMutex()); + const auto found = PixelLdsPools().find(context.device); + if (found == PixelLdsPools().end() || found->second.header == nullptr) return; + if (found->second.header[1] != 0u) throw std::runtime_error("AGC graphics: pixel function LDS subgroup slots exhausted"); +} + +void ReleasePixelFunctionLds(VkDevice device) { + std::lock_guard lock(PixelLdsMutex()); + const auto found = PixelLdsPools().find(device); + if (found == PixelLdsPools().end()) return; + DestroyPixelFunctionLds(found->second); + PixelLdsPools().erase(found); +} + std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std::span<const VkImageView> targetViews) { std::uint64_t key = 14695981039346656037ull; const auto mix = [&](std::uint64_t value) { @@ -2068,6 +2222,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); countBarrier(1); } + const auto pixelLdsSubgroup = PixelFunctionLdsSubgroup(shaders); + if (pixelLdsSubgroup != 0u) PreparePixelFunctionLds(context, commands, pixelLdsSubgroup); if (meshIndirect) { meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); @@ -2093,6 +2249,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } else { resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } + if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; })) BindPixelFunctionLds(context, commands, record.pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); @@ -2766,6 +2923,8 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); countBarrier(); } + const auto pixelLdsSubgroup = PixelFunctionLdsSubgroup(shaders); + if (pixelLdsSubgroup != 0u) PreparePixelFunctionLds(context, commands, pixelLdsSubgroup); std::shared_ptr<Buffer> meshArguments; if (state.stages.mesh && args != nullptr) { meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable); @@ -2807,6 +2966,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D pipeline->Begin(commands, *framebuffer, state.renderExtent, state); APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); resources->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->Layout()); + if (pixelLdsSubgroup != 0u) BindPixelFunctionLds(context, commands, pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 9a2bf59a56..470b77f6f3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -279,7 +280,7 @@ void appendKey(std::vector<std::byte>& key, const TValue& value) { bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders) { using Stage = ShaderRecompiler::ShaderStage; - if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state)) return false; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; })) return false; keys = {}; for (const auto& shader : shaders) { const auto identity = shader.program->Identity(); @@ -463,12 +464,14 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const std::array<VkDescriptorSetLayout, 2> setLayouts{resources.main, resources.fragment}; + const bool pixelLds = std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }); + const auto pixelLdsLayouts = pixelLds ? PixelFunctionLdsSetLayouts(context) : PixelFunctionLdsLayouts{}; + const std::array<VkDescriptorSetLayout, 3> setLayouts{resources.main, pixelLds && !resources.split ? pixelLdsLayouts.empty : resources.fragment, pixelLdsLayouts.buffer}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array<VkPushConstantRange, 2> slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; - layoutInfo.setLayoutCount = resources.split ? 2u : 1u; + layoutInfo.flags = pixelLds || !resources.split ? 0 : VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT; + layoutInfo.setLayoutCount = pixelLds ? 3u : resources.split ? 2u : 1u; layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast<std::uint32_t>(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; @@ -1232,6 +1235,7 @@ void ClearCachedPipelines(VkDevice device) { it = store.entries.erase(it); } ClearPipelineLibraries(device); + ReleasePixelFunctionLds(device); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index abcf13c57c..7dd97e4370 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -1,4 +1,5 @@ #include "BdaAbi.hpp" +#include "SpirvBackend/SpirvAnalysis.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -594,6 +595,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } else { Require(variable.storage == spv::StorageClassStorageBuffer && decoration.set && decoration.binding, "unsupported or unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.binding); + if (*decoration.set == ShaderRecompiler::PixelFunctionLdsDescriptorSet) { + Require(fragment && *decoration.binding == 0u, "pixel function LDS buffer binding is wrong"); + continue; + } const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; }); const bool array = binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers; const auto [view, first] = bufferViews.emplace(key, decoration.aliased); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp new file mode 100644 index 0000000000..8ef17d4d0d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp @@ -0,0 +1,165 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdio> +#include <iostream> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t High = 0x11110000u; +constexpr std::uint32_t Last = 0x22220000u; +constexpr std::uint32_t Past = 0x33330000u; +alignas(256) std::array<std::byte, Width * Height * 4> Pixels{}; +alignas(256) std::array<std::uint32_t, Width * Height * 4> Results{}; + +alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +std::vector<std::uint32_t> PixelCode() { + std::vector<std::uint32_t> code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; + const auto mov = [&](std::uint32_t reg, std::uint32_t literal) { + code.push_back(0x7e0002ffu | (reg << 17u)); + code.push_back(literal); + }; + const auto shl = [&](std::uint32_t dest, std::uint32_t source) { + code.push_back(0x34000000u | (dest << 17u) | (source << 9u) | 0x82u); + }; + const auto write = [&](std::uint32_t index, std::uint32_t value) { + mov(16u, index); + shl(17u, 16u); + mov(18u, value); + code.push_back(0xd8340000u); + code.push_back((18u << 8u) | 17u); + }; + write(8000u, High); + write(8191u, Last); + write(8192u, Past); + code.push_back(0xbf8cc07fu); + const auto read = [&](std::uint32_t index, std::uint32_t dest) { + mov(16u, index); + shl(17u, 16u); + code.push_back(0xd8d80000u); + code.push_back((dest << 24u) | 17u); + }; + read(8000u, 10u); + read(8191u, 11u); + read(8192u, 12u); + shl(15u, 3u); + code.push_back(0xd8d80000u); + code.push_back((13u << 24u) | 15u); + code.push_back(0xbf8cc07fu); + for (std::uint32_t slot = 0; slot < 4u; ++slot) { + code.push_back(0xe0702000u | (slot * 4u)); + code.push_back(0x80010006u | ((10u + slot) << 8u)); + } + code.insert(code.end(), {0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}); + return code; +} + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +constexpr std::array<std::array<float, 4>, 3> Triangles{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +void Draw(AgcDriver::VulkanDevice& device) { + const auto pixelCode = PixelCode(); + Pixels.fill(std::byte{0}); + Results.fill(0u); + const auto target = device.Target(); + std::vector<std::uint32_t> vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangles.data(), 16u, static_cast<std::uint32_t>(Triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = false; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector<std::uint32_t> pixelUserData(8, 0u); + const auto results = BufferDescriptor(Results.data(), 16u, Width * Height); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + Require(pixelResult.functionLdsBuffer, "pixel LDS limit: the full array was not backed by a buffer"); + const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Draw(*device); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(Pixels[pixel * 4u + channel] == std::byte{255}, "pixel LDS limit: missing color"); + const auto base = pixel * 4u; + Require(Results[base] == High, "pixel LDS limit: dword 8000 expected " + std::to_string(High) + " got " + std::to_string(Results[base])); + Require(Results[base + 1u] == Last, "pixel LDS limit: dword 8191 expected " + std::to_string(Last) + " got " + std::to_string(Results[base + 1u])); + Require(Results[base + 2u] == 0u, "pixel LDS limit: dword 8192 wrote " + std::to_string(Results[base + 2u])); + } + std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 24bc292f3f..0413a773da 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -650,14 +650,14 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) } catch (const SpannedWaveUnsupported& error) { std::fprintf(stderr, "[spanned-wave] 0x%llx falls back to two lanes per invocation: %s\n", static_cast<unsigned long long>(request.shader.codeAddress), error.what()); targetOptions.spannedWaves = false; - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted); } if (registration && result.spirv.size() > request.target.registrationWordLimit) { registration = false; program.Metadata().bindingLayoutComplete = false; bindings = bindingAllocator.Allocate(program, request.layout, false, stencilReplay); targetOptions.registrationModules = false; - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted); } result.registrationModule = registration; if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; @@ -675,6 +675,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.runtimeImageCount = bindings.layout.runtimeImageCount; result.hostSubgroupSize = HostSubgroupSize(request); result.fullSubgroups = emitted.fullSubgroups; + result.functionLdsBuffer = emitted.functionLdsBuffer; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -835,11 +836,13 @@ std::shared_ptr<const SpecializedModule> buildSpecializedModule(const CompiledSh #endif auto module = std::make_shared<SpecializedModule>(); std::map<std::uint32_t, std::uint32_t> bindingNumbers; + std::unordered_set<std::uint32_t> functionLdsBuffer; for (std::size_t cursor = 5; cursor < materialized.size();) { const auto count = materialized[cursor] >> 16u; const auto op = static_cast<spv::Op>(materialized[cursor] & 0xffffu); if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationBinding) bindingNumbers.emplace(materialized[cursor + 1u], materialized[cursor + 3u]); - if (op == spv::OpVariable && count >= 4u) { + if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationDescriptorSet && materialized[cursor + 3u] == PixelFunctionLdsDescriptorSet) functionLdsBuffer.insert(materialized[cursor + 1u]); + if (op == spv::OpVariable && count >= 4u && !functionLdsBuffer.contains(materialized[cursor + 2u])) { if (const auto found = bindingNumbers.find(materialized[cursor + 2u]); found != bindingNumbers.end()) module->bindings.push_back(found->second); module->pushData |= materialized[cursor + 3u] == spv::StorageClassPushConstant; } @@ -1003,6 +1006,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .instanceOffsetConflict = artifact.instanceOffsetConflict, .hostSubgroupSize = artifact.hostSubgroupSize, .fullSubgroups = artifact.fullSubgroups, + .functionLdsBuffer = artifact.functionLdsBuffer, .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e8ffba43e1..b523040560 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -672,6 +672,7 @@ struct CompiledShaderArtifact { bool instanceOffsetConflict = false; std::uint32_t hostSubgroupSize = 0; bool fullSubgroups = false; + bool functionLdsBuffer = false; std::vector<std::uint32_t> parameterExports; std::vector<FragmentParameter> fragmentParameters; BarycentricEmulation barycentricEmulation; @@ -741,7 +742,7 @@ struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { }; mix(specialization.size()); for (const auto& constant : specialization) mix(constant.id | (static_cast<std::uint64_t>(constant.value) << 32u)); - mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (static_cast<std::uint64_t>(workgroupMemoryDwords) << 32u)); + mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (functionLdsBuffer ? 0x20000u : 0u) | (static_cast<std::uint64_t>(workgroupMemoryDwords) << 32u)); mix(bindings.size()); for (const auto& binding : bindings) mix(binding.descriptorSet | (static_cast<std::uint64_t>(binding.binding) << 16u) | (static_cast<std::uint64_t>(binding.kind) << 40u) | (static_cast<std::uint64_t>(binding.count) << 48u)); if (identity.Empty()) identity.low = 1; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ee1ba3838f..481c5d232b 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -495,6 +495,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.instanceOffsetConflict); writer.Value(result.hostSubgroupSize); writer.Value(result.fullSubgroups); + writer.Value(result.functionLdsBuffer); writer.Values(std::span<const std::uint32_t>(result.parameterExports)); writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) { out.Value(parameter.location); @@ -556,6 +557,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.instanceOffsetConflict); reader.Value(result.hostSubgroupSize); reader.Value(result.fullSubgroups); + reader.Value(result.functionLdsBuffer); reader.Values(result.parameterExports); reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) { in.Value(parameter.location); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index ee435ffc67..2dbe8f7d03 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -39,6 +39,8 @@ struct SpirvRequirements { [[nodiscard]] SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program); inline constexpr std::uint32_t FunctionLdsDwordLimit = 8192u; +inline constexpr std::uint32_t PixelFunctionStorageDwords = 4096u; +inline constexpr std::uint32_t PixelFunctionLdsDescriptorSet = 2u; [[nodiscard]] std::uint32_t FunctionLdsDwords(const IrProgram& program); [[nodiscard]] std::unordered_map<const IrValue*, std::uint32_t> FunctionLdsLaneAddresses(const IrProgram& program); [[nodiscard]] std::unordered_set<const IrValue*> WaveUniformValues(const IrProgram& program); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 74d6b2e281..f43c064182 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -43,6 +43,7 @@ struct SpirvTargetOptions { struct SpirvEmitInfo { bool fullSubgroups = false; + bool functionLdsBuffer = false; }; struct SpannedWaveUnsupported : std::runtime_error { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 31a5981434..38b321e595 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -206,6 +206,7 @@ struct SpirvEmitterState { std::uint32_t ldsBufferVariable = 0; std::uint32_t ldsBufferU64Variable = 0; std::uint32_t ldsBufferBase = 0; + bool pixelFunctionLdsBuffer = false; std::uint32_t numWorkgroupsVariable = 0; std::array<std::uint32_t, 2> scratchVariable {}; std::array<std::uint32_t, 2> vectorWindowVariable {}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 55aa8b12a7..4939cb02e8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,6 +281,11 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; + if (program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + state.pixelFunctionLdsBuffer = true; + state.requirements.subgroupLocalInvocationId = true; + state.requirements.subgroupBallot = true; + } if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { state.requirements.ldsLock = false; state.requirements.ldsBufferInt64Atomics = true; @@ -319,7 +324,10 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh EmitModuleHeader(state, bindings); EmitProgram(state); state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables); - if (info != nullptr) info->fullSubgroups = state.spannedWave; + if (info != nullptr) { + info->fullSubgroups = state.spannedWave; + info->functionLdsBuffer = state.pixelFunctionLdsBuffer; + } return state.module.Finalize(); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 192b60cc27..c405971431 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -70,10 +70,49 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state) { } std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state) { - return spv::MemorySemanticsWorkgroupMemoryMask | (LdsInDeviceMemory(state) ? spv::MemorySemanticsUniformMemoryMask : 0u); + return spv::MemorySemanticsWorkgroupMemoryMask | (LdsInDeviceMemory(state) || state.pixelFunctionLdsBuffer ? spv::MemorySemanticsUniformMemoryMask : 0u); +} + +void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { + if (!state.pixelFunctionLdsBuffer) return; + if (state.ldsBufferVariable == 0u || state.hostSubgroupSize == 0u) FailEmit("pixel function LDS buffer was not declared"); + const auto element = [&](std::uint32_t dword) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.ldsBufferVariable, ConstantU32(state, 0u), ConstantU32(state, dword)); + return pointer; + }; + const auto slotVar = state.module.AllocateId(); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), slotVar, spv::StorageClassFunction, ConstantU32(state, 0u)); + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + EmitIfCondition(state, elected, [&] { + const auto acquired = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicIAdd, TypeU32(state), acquired, element(0u), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsUniformMemoryMask), ConstantU32(state, 1u)); + const auto capacity = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), capacity, element(2u)); + const auto exhausted = state.module.AllocateId(); + state.module.AddFunction(spv::OpUGreaterThanEqual, TypeBool(state), exhausted, acquired, capacity); + EmitIfCondition(state, exhausted, [&] { + state.module.AddFunction(spv::OpAtomicStore, element(1u), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask), ConstantU32(state, 1u)); + }); + const auto claimed = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeU32(state), claimed, exhausted, ConstantU32(state, 0u), acquired); + state.module.AddFunction(spv::OpStore, slotVar, claimed); + }); + const auto ballot = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), elected); + const auto source = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), source, ConstantU32(state, spv::ScopeSubgroup), ballot); + const auto local = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), local, slotVar); + const auto slot = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBroadcast, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local, source); + const auto within = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, slot, ConstantU32(state, state.hostSubgroupSize)), EmitSubgroupLocalInvocationId(state)); + state.ldsBufferBase = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, within, ConstantU32(state, FunctionLdsDwordLimit)), ConstantU32(state, 3u)); } void EmitLdsBufferBase(SpirvEmitterState& state) { + EmitPixelFunctionLdsBase(state); if (!LdsInDeviceMemory(state)) { return; } @@ -125,6 +164,12 @@ MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const access.kind = mem.kind; switch (mem.kind) { case ResourceKind::Lds: + if (state.pixelFunctionLdsBuffer) { + if (state.ldsBufferVariable == 0u || state.ldsBufferBase == 0u) FailEmit("pixel function LDS buffer was not prepared at function entry"); + access.objectPointer = state.ldsBufferVariable; + access.length = ConstantU32(state, LdsDwordCount(state)); + return access; + } if (LdsInDeviceMemory(state)) { if (state.ldsBufferVariable == 0u || state.ldsBufferBase == 0u) { FailEmit("device memory LDS was not prepared at function entry"); @@ -203,6 +248,9 @@ std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResource } std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { + if (access.kind == ResourceKind::Lds && state.pixelFunctionLdsBuffer) { + return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); + } if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index accb81d256..a090a68541 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -574,6 +574,14 @@ void DefineDescriptors(SpirvEmitterState& state) { for (const auto variable : {state.ldsBufferVariable, state.ldsBufferU64Variable}) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); } } + if (state.pixelFunctionLdsBuffer) { + state.ldsBufferVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferBlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferVariable, "pixel_function_lds"); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationDescriptorSet, PixelFunctionLdsDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); + if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(state.ldsBufferVariable); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index ddde52c8e9..ea47f0b3af 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -216,7 +216,7 @@ void DefineModule(SpirvEmitterState& state) { DefineOutputs(state); DefineTessellationInterfaces(state); DefineDescriptors(state); - if (state.requirements.functionLds) { + if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.ldsVariable = state.module.AllocateId(); } if (state.requirements.functionScratch) { @@ -402,7 +402,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupLinearKHR); } state.module.AddName(state.mainFunc, "main"); - if (state.requirements.functionLds) { + if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.module.AddName(state.ldsVariable, "lds_dwords"); } if (state.requirements.functionScratch) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 4f558e43a1..b9f6c7b5d5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1552,7 +1552,7 @@ void EmitProgram(SpirvEmitterState& state) { } state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.pixelGuestFunc != 0u ? state.pixelGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.entryLabel); - if (state.requirements.functionLds) { + if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsStorageDwords(state)), state.ldsVariable, spv::StorageClassFunction); } if (state.requirements.functionScratch) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e66c9cec39..6d88203131 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but is backed by a storage buffer, one 8192-dword region per invocation, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); the buffer holds as many in-flight subgroups as fit in the smaller of 2 GiB and the device's storage-buffer range, and a draw that needs another slot sets a flag that `WaitIdle` throws on instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. From d37e9f4ea77e1cb45f8e2b9e1662c60fa514a119 Mon Sep 17 00:00:00 2001 From: mazen <mazenshaar@hotmail.com> Date: Mon, 19 Oct 2026 21:47:06 +0400 Subject: [PATCH 1308/1360] fix(shader): index pixel LDS by the host lane A wave32 pixel program on a wider subgroup was using the guest lane, so both halves of that subgroup wrote one region. --- .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index c405971431..30ce436187 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -107,7 +107,13 @@ void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { state.module.AddFunction(spv::OpLoad, TypeU32(state), local, slotVar); const auto slot = state.module.AllocateId(); state.module.AddFunction(spv::OpGroupNonUniformBroadcast, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local, source); - const auto within = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, slot, ConstantU32(state, state.hostSubgroupSize)), EmitSubgroupLocalInvocationId(state)); + std::uint32_t lane; + if (state.splitSubgroup) { + if (state.subgroupLocalInvocationIdVariable == 0) FailEmit("pixel function LDS lane was not declared"); + lane = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), lane, state.subgroupLocalInvocationIdVariable); + } else lane = EmitSubgroupLocalInvocationId(state); + const auto within = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, slot, ConstantU32(state, state.hostSubgroupSize)), lane); state.ldsBufferBase = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, within, ConstantU32(state, FunctionLdsDwordLimit)), ConstantU32(state, 3u)); } From dba7f5466c64defe2f878f48d1579dab0c7f7e56 Mon Sep 17 00:00:00 2001 From: mazen <mazenshaar@hotmail.com> Date: Mon, 19 Oct 2026 22:17:01 +0400 Subject: [PATCH 1309/1360] fix(shader): keep a pixel LDS overflow until the GPU is idle The slot counter is reset only after earlier fragment writes, and the overflow flag stays until WaitIdle. The storage buffer is limited to NVIDIA's proprietary driver, which is where the large function array does not finish. --- .../Execution/src/VulkanDevice.cpp | 3 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 12 +++-- .../tests/execution/PixelLdsLimit.cpp | 44 ++++++++++++------ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 ++- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 2 +- .../tests/RequestSerializerTests.cpp | 46 +++++++++++++------ docs/dev/debt/shader.md | 3 +- 11 files changed, 83 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c6e39b0346..a642fba48a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -342,6 +342,7 @@ struct VulkanDevice::State { VkDeviceSize uploadSize = 0; VkPhysicalDeviceProperties properties{}; std::string driverInfo; + VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; @@ -1262,6 +1263,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; + state->driverId = driver.driverID; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); @@ -3781,6 +3783,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; + target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index befeb99d09..104520cf80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -161,9 +161,11 @@ void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, s pool.announced = true; } Require(pool.subgroupSize == subgroupSize, "pixel function LDS subgroup size does not match the pool"); - context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, pool.buffer, 0, 8, 0u); - const VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); + const VkMemoryBarrier beforeFill{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &beforeFill, 0, nullptr, 0, nullptr); + context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, pool.buffer, 0, 4, 0u); + const VkMemoryBarrier afterFill{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 1, &afterFill, 0, nullptr, 0, nullptr); } std::uint32_t PixelFunctionLdsSubgroup(std::span<const CompiledShader> shaders) { @@ -185,7 +187,9 @@ void CheckPixelFunctionLds(const Context& context) { std::lock_guard lock(PixelLdsMutex()); const auto found = PixelLdsPools().find(context.device); if (found == PixelLdsPools().end() || found->second.header == nullptr) return; - if (found->second.header[1] != 0u) throw std::runtime_error("AGC graphics: pixel function LDS subgroup slots exhausted"); + const auto exhausted = found->second.header[1] != 0u; + found->second.header[1] = 0u; + if (exhausted) throw std::runtime_error("AGC graphics: pixel function LDS subgroup slots exhausted"); } void ReleasePixelFunctionLds(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp index 8ef17d4d0d..9299392bb5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp @@ -6,7 +6,9 @@ #include <array> #include <cstdio> #include <iostream> +#include <span> #include <string> +#include <string_view> #include <vector> namespace { @@ -21,13 +23,16 @@ constexpr std::uint32_t Last = 0x22220000u; constexpr std::uint32_t Past = 0x33330000u; alignas(256) std::array<std::byte, Width * Height * 4> Pixels{}; alignas(256) std::array<std::uint32_t, Width * Height * 4> Results{}; +constexpr std::uint32_t Overflow = 256u; +alignas(256) std::array<std::byte, Overflow * Overflow * 4> OverflowPixels{}; +alignas(256) std::array<std::uint32_t, Overflow * Overflow * 4> OverflowResults{}; alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; -std::vector<std::uint32_t> PixelCode() { - std::vector<std::uint32_t> code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; +std::vector<std::uint32_t> PixelCode(std::uint32_t width) { + std::vector<std::uint32_t> code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, width}; const auto mov = [&](std::uint32_t reg, std::uint32_t literal) { code.push_back(0x7e0002ffu | (reg << 17u)); code.push_back(literal); @@ -76,10 +81,10 @@ constexpr std::array<std::array<float, 4>, 3> Triangles{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device) { - const auto pixelCode = PixelCode(); - Pixels.fill(std::byte{0}); - Results.fill(0u); +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t height, std::span<std::byte> pixels, std::span<std::uint32_t> results) { + const auto pixelCode = PixelCode(width); + std::fill(pixels.begin(), pixels.end(), std::byte{0}); + std::fill(results.begin(), results.end(), 0u); const auto target = device.Target(); std::vector<std::uint32_t> vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(Triangles.data(), 16u, static_cast<std::uint32_t>(Triangles.size())); @@ -102,8 +107,8 @@ void Draw(AgcDriver::VulkanDevice& device) { pixel.targetOutputMode[0] = 9; pixel.targetExportMapping.fill(0xe4u); std::vector<std::uint32_t> pixelUserData(8, 0u); - const auto results = BufferDescriptor(Results.data(), 16u, Width * Height); - std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const auto stored = BufferDescriptor(results.data(), 16u, width * height); + std::copy(stored.begin(), stored.end(), pixelUserData.begin() + 4); const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, @@ -113,21 +118,21 @@ void Draw(AgcDriver::VulkanDevice& device) { }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); - Require(pixelResult.functionLdsBuffer, "pixel LDS limit: the full array was not backed by a buffer"); + Require(pixelResult.functionLdsBuffer == target.largePixelFunctionLdsBuffer, target.largePixelFunctionLdsBuffer ? "pixel LDS limit: the full array was not backed by a buffer" : "pixel LDS limit: a driver that keeps the function array used the buffer"); const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {width, height}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; - state.renderExtent = {Width, Height}; + state.renderExtent = {width, height}; state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1}; + state.viewport = {0, static_cast<float>(height), static_cast<float>(width), -static_cast<float>(height), 0, 1}; state.negativeOneToOne = false; - state.scissor = {{0, 0}, {Width, Height}}; + state.scissor = {{0, 0}, {width, height}}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; @@ -148,7 +153,7 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - Draw(*device); + Draw(*device, Width, Height, Pixels, Results); for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(Pixels[pixel * 4u + channel] == std::byte{255}, "pixel LDS limit: missing color"); const auto base = pixel * 4u; @@ -157,6 +162,17 @@ int main() { Require(Results[base + 2u] == 0u, "pixel LDS limit: dword 8192 wrote " + std::to_string(Results[base + 2u])); } std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels"); + if (device->Target().largePixelFunctionLdsBuffer) { + bool threw = false; + try { + Draw(*device, Overflow, Overflow, OverflowPixels, OverflowResults); + } catch (const std::exception& error) { + threw = std::string_view(error.what()) == "AGC graphics: pixel function LDS subgroup slots exhausted"; + if (!threw) throw; + } + Require(threw, "pixel LDS limit: a draw past the subgroup slots did not throw"); + std::puts("pixel LDS limit overflow passed"); + } return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index fdcff33207..cf4bbc0138 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -298,6 +298,7 @@ private: append(key, value.waveLdsMemoryBarriers); append(key, value.float32DenormPreserve); append(key, value.fusedExactArithmetic); + append(key, value.largePixelFunctionLdsBuffer); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 96bb58e1a5..9457081b29 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -832,7 +832,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(44u); + writer.WriteU32(45u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -871,6 +871,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(std::bit_cast<std::uint32_t>(request.context.vertex->pointSizeMax)); } writer.WriteBool(request.target.fusedExactArithmetic); + writer.WriteBool(request.target.largePixelFunctionLdsBuffer); return base64Encode(buffer); } @@ -879,7 +880,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 > 44u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 45u) 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); @@ -934,6 +935,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.vertex = vertex; } if (version >= 44u) result.request.target.fusedExactArithmetic = reader.ReadBool(); + if (version >= 45u) result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 0413a773da..2204a0db94 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -606,6 +606,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float32FlushToZero = request.target.float32FlushToZero; targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.fusedExactArithmetic = request.target.fusedExactArithmetic; + targetOptions.largePixelFunctionLdsBuffer = request.target.largePixelFunctionLdsBuffer; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index b523040560..978d400225 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -299,6 +299,7 @@ struct SpirvTarget { bool waveLdsMemoryBarriers = false; bool float32DenormPreserve = false; bool fusedExactArithmetic = false; + bool largePixelFunctionLdsBuffer = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index f43c064182..4cb86e2472 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -39,6 +39,7 @@ struct SpirvTargetOptions { std::vector<std::uint32_t> meshExportedLocations; std::vector<std::pair<std::uint32_t, std::uint32_t>> meshPerVertexLocations; bool waveLdsMemoryBarriers = false; + bool largePixelFunctionLdsBuffer = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 4939cb02e8..0cfb1a7be2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,7 +281,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; - if (program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; state.requirements.subgroupLocalInvocationId = true; state.requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index eb8d34ad6d..4ebf8833b1 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -219,13 +219,14 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 44u; + upgraded[4] = 45u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); AppendVertexTail(upgraded); upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -260,13 +261,14 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 44u; + layered[4] = 45u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); AppendVertexTail(layered); layered.push_back(0u); + layered.push_back(0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -287,12 +289,13 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 44u; + bytes[4] = 45u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); AppendVertexTail(bytes); bytes.push_back(0u); + bytes.push_back(0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -418,12 +421,13 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); auto upgraded = main38; - upgraded[4] = 44u; + upgraded[4] = 45u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); AppendVertexTail(upgraded); upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -439,8 +443,11 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version44 = DecodeBase64(serializer.Serialize(pointed)); - Require(version44.size() > 15u && version44[4] == 44u); + auto version45 = DecodeBase64(serializer.Serialize(pointed)); + Require(version45.size() > 15u && version45[4] == 45u); + auto version44 = version45; + version44.pop_back(); + version44[4] = 44u; auto version43 = version44; version43.pop_back(); version43[4] = 43u; @@ -471,7 +478,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 44u; + main25[4] = 45u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -480,6 +487,7 @@ int main() { if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); AppendVertexTail(main25); main25.push_back(0u); + main25.push_back(0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -491,19 +499,21 @@ int main() { request.target.float16DenormPreserve = preserve; request.target.float32FlushToZero = !preserve; request.target.float32DenormPreserve = preserve; + request.target.largePixelFunctionLdsBuffer = !preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 44u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 45u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.pop_back(); version40.pop_back(); + version40.pop_back(); version40[4] = 40u; const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 45u; + future[4] = 46u; bool futureRejected = false; try { static_cast<void>(serializer.Deserialize(EncodeBase64(future))); @@ -550,6 +560,7 @@ int main() { Require(version41.end() - order > static_cast<std::ptrdiff_t>(sizeof(pixel.sampleOrder)) && *order == 0x10u); version41.erase(order, order + sizeof(pixel.sampleOrder)); version41.pop_back(); + version41.pop_back(); version41[4] = 41u; const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); Require(replay41.request.context.pixel.has_value() && replay41.request.context.pixel->sampleLocations == pixel.sampleLocations && replay41.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{}); @@ -561,6 +572,7 @@ int main() { request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); version35.pop_back(); + version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto pixelTail = static_cast<std::ptrdiff_t>(1u + sizeof(pixel.sampleOrder)); Require(version35.end() - stencilOperationValue > pixelTail && std::all_of(stencilOperationValue, stencilOperationValue + pixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -605,7 +617,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); + legacy.resize(legacy.size() - (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -617,7 +629,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 44u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); + Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); } } @@ -626,14 +638,14 @@ int main() { const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy42.resize(legacy42.size() - 14u); + legacy42.resize(legacy42.size() - 15u); legacy42[4] = 42u; const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 21u); + legacy34.resize(legacy34.size() - 22u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast<std::ptrdiff_t>(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -645,8 +657,12 @@ int main() { for (const bool fused : {false, true}) { request.target.fusedExactArithmetic = fused; + request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); - Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes.back() == (fused ? 1u : 0u)); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 2u] == (fused ? 1u : 0u) && bytes.back() == 0u); + bytes.pop_back(); + bytes[4] = 44u; + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused); bytes[4] = 43u; bytes.pop_back(); Require(!serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 6d88203131..fe570ca6e3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,8 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but is backed by a storage buffer, one 8192-dword region per invocation, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); the buffer holds as many in-flight subgroups as fit in the smaller of 2 GiB and the device's storage-buffer range, and a draw that needs another slot sets a flag that `WaitIdle` throws on instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. + - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. From d06caa7a8de8e2ad68629f33ea857a1ba0aa320b Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Tue, 20 Oct 2026 13:56:45 +0400 Subject: [PATCH 1310/1360] fix(shader): update request fixtures for pixel LDS version 39 Unsupported-version probes use 40, and legacy downgrade fixtures drop the new trailer bool so Deserialize no longer sees trailing data. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index effc0dc179..9aa30d1018 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5289,6 +5289,7 @@ void verifyVertexInfoVersion19() { auto version32 = decodeBase64(serializer.Serialize(spanned)); version32.pop_back(); version32.pop_back(); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5389,11 +5390,12 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 8u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 8 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 1 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 1 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail and the fused arithmetic flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.pop_back(); legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5403,6 +5405,7 @@ void verifyDepthRemapRequests() { auto version35 = remappedBytes; version35.pop_back(); version35.pop_back(); + version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); From 89fcff6dd8ad76eab49ffc4e3637bb5df13e30fc Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Tue, 20 Oct 2026 15:38:04 +0400 Subject: [PATCH 1311/1360] fix(shader): broadcast the first pixel LDS slot without a dynamic lane --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++-- .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9aa30d1018..167de17f59 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5453,8 +5453,8 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 54u + pointTailBytes && legacy[4] == 44u, "a merged hull request did not use serialization version 44"); - legacy.resize(legacy.size() - 1u - pointTailBytes); + require(legacy.size() > 54u + pointTailBytes && legacy[4] == 45u, "a merged hull request did not use serialization version 45"); + legacy.resize(legacy.size() - 2u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 30ce436187..1a0ebafe6d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -99,14 +99,10 @@ void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { state.module.AddFunction(spv::OpSelect, TypeU32(state), claimed, exhausted, ConstantU32(state, 0u), acquired); state.module.AddFunction(spv::OpStore, slotVar, claimed); }); - const auto ballot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), elected); - const auto source = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), source, ConstantU32(state, spv::ScopeSubgroup), ballot); const auto local = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), local, slotVar); const auto slot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBroadcast, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local, source); + state.module.AddFunction(spv::OpGroupNonUniformBroadcastFirst, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local); std::uint32_t lane; if (state.splitSubgroup) { if (state.subgroupLocalInvocationIdVariable == 0) FailEmit("pixel function LDS lane was not declared"); From fcf4a098780bf589e359de951ea7138306831bf8 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Wed, 21 Oct 2026 10:39:16 +0400 Subject: [PATCH 1312/1360] fix(shader): begin a pass before preparing pixel LDS Continued draws skipped the slot fill while still binding the set. Main already uses serialization version 41, so the LDS flag is 42. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 104520cf80..b291f76dac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2179,7 +2179,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } const PassAccess passAccess{passReads, passWrites, passImages, passAttachments, unknownMemory, unknownWrites, passWriteOnly}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); - const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); + const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect || PixelFunctionLdsSubgroup(shaders) != 0u, fenced, passAccess); for (const auto& target : record.multisampleTargets) KeepMultisampleTarget_nid_no_patch(target); const bool continued = start.continued; outcome.passContinued = continued; From 641a0a4173d8da5c81b97ae6749a1be722493cac Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Wed, 21 Oct 2026 16:01:21 +0400 Subject: [PATCH 1313/1360] fix(shader): check pixel LDS overflow when presents retire Exhausted subgroups share slot 0 until the sticky flag is read; check it on present retire as well as WaitIdle, and cover two draws into one target. --- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 3 +++ .../prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp | 8 ++++---- docs/dev/debt/shader.md | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a642fba48a..0febf9d6f9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3656,6 +3656,7 @@ double VulkanDevice::RetirePresents(std::size_t keepInFlight) { const auto status = state->DeviceFunction<PFN_vkGetFenceStatus>("vkGetFenceStatus"); auto inFlight = static_cast<std::size_t>(std::count_if(slots.begin(), slots.end(), [](const auto& slot) { return slot.inFlight; })); double waitedMs = 0; + bool retired = false; // Oldest first from the cursor; the fences of one queue signal in submission order, so the // first unsignaled one ends the scan, and the ones beyond the bound are the oldest. for (std::size_t i = 0; i < slots.size() && inFlight != 0; ++i) { @@ -3674,9 +3675,11 @@ double VulkanDevice::RetirePresents(std::size_t keepInFlight) { } const bool dump = slot.dumpRecorded; state->RetireSlot(slot); + retired = true; if (dump) timing.Mark("dump"); --inFlight; } + if (retired) Graphics::CheckPixelFunctionLds(graphicsContext()); return waitedMs; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp index 9299392bb5..594281d148 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp @@ -81,7 +81,7 @@ constexpr std::array<std::array<float, 4>, 3> Triangles{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t height, std::span<std::byte> pixels, std::span<std::uint32_t> results) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t height, std::span<std::byte> pixels, std::span<std::uint32_t> results, std::uint32_t draws = 1u) { const auto pixelCode = PixelCode(width); std::fill(pixels.begin(), pixels.end(), std::byte{0}); std::fill(results.begin(), results.end(), 0u); @@ -139,7 +139,7 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t he state.blends = {state.blend}; state.blendConstants = {}; const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangles.size()), 0, 1, 0, false}; - device.Draw(state, draw, shaders); + for (std::uint32_t n = 0; n < draws; ++n) device.Draw(state, draw, shaders); device.WaitIdle(); } @@ -153,7 +153,7 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - Draw(*device, Width, Height, Pixels, Results); + Draw(*device, Width, Height, Pixels, Results, 2u); for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(Pixels[pixel * 4u + channel] == std::byte{255}, "pixel LDS limit: missing color"); const auto base = pixel * 4u; @@ -161,7 +161,7 @@ int main() { Require(Results[base + 1u] == Last, "pixel LDS limit: dword 8191 expected " + std::to_string(Last) + " got " + std::to_string(Results[base + 1u])); Require(Results[base + 2u] == 0u, "pixel LDS limit: dword 8192 wrote " + std::to_string(Results[base + 2u])); } - std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels"); + std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels, two draws"); if (device->Target().largePixelFunctionLdsBuffer) { bool threw = false; try { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index fe570ca6e3..76b39dd32c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,10 +151,13 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a sticky overflow flag and reuses slot 0 for the excess subgroups (sharing that region with the subgroup that owns it); `CheckPixelFunctionLds` throws when the flag is set, from `WaitIdle` and when a present fence retires, then clears it; arrays of 4097 to 6143 dwords take the buffer path though only 4096 (completes), 6144 (timeout), 7680 and 8192 (fault) were measured on that 5090; the pool is the first `HOST_VISIBLE|HOST_COHERENT` type so the host can map the 16-byte header without a separate device-local body (on discrete NVIDIA that is usually system memory for the full allocation, and a device-local body plus host-visible header was not measured); on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From 40c7ff888ca1def7c27d8f9eb27edbcd0e271e58 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Wed, 21 Oct 2026 19:02:25 +0400 Subject: [PATCH 1314/1360] fix(shader): scale pixel LDS Function threshold by SM budget Max measured a 4 GiB private-memory ceiling on a 4090; use VK_NV_shader_sm_builtins so denser cards take the buffer path below 4096. --- .../Execution/src/VulkanDevice.cpp | 13 +++++++++++++ .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 15 ++++++++++++++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../recompiler/SpirvBackend/src/SpirvEmitter.cpp | 2 +- .../recompiler/tests/RequestSerializerTests.cpp | 5 ++++- docs/dev/debt/shader.md | 5 +---- 10 files changed, 38 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 0febf9d6f9..8c3637d70b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -343,6 +343,7 @@ struct VulkanDevice::State { VkPhysicalDeviceProperties properties{}; std::string driverInfo; VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; + std::uint32_t pixelFunctionStorageDwords = 4096u; bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; @@ -1268,6 +1269,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); } + if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) { + VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV}; + VkPhysicalDeviceProperties2 smProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &smBuiltins}; + state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &smProperties); + const auto subgroup = state->subgroup.subgroupSize; + if (smBuiltins.shaderSMCount != 0u && smBuiltins.shaderWarpsPerSM != 0u && subgroup != 0u) { + const auto invocations = static_cast<std::uint64_t>(smBuiltins.shaderSMCount) * smBuiltins.shaderWarpsPerSM * subgroup; + const auto budgetDwords = (4ull << 30) / (invocations * 4ull); + if (budgetDwords < state->pixelFunctionStorageDwords) state->pixelFunctionStorageDwords = static_cast<std::uint32_t>(budgetDwords); + } + } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions); require(hasExtension(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME), "VK_KHR_shader_float_controls is unavailable"); @@ -3787,6 +3799,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.robustBufferAccess2 = state->robustBufferAccess2; target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; + target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 167de17f59..877cf34683 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5176,6 +5176,7 @@ void verifyVertexInfoVersion19() { require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); auto version20Vertex = flagged; + version20Vertex.resize(version20Vertex.size() - sizeof(std::uint32_t)); version20Vertex.pop_back(); version20Vertex.pop_back(); version20Vertex.pop_back(); @@ -5188,6 +5189,7 @@ void verifyVertexInfoVersion19() { const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); auto legacy = plain; + legacy.resize(legacy.size() - sizeof(std::uint32_t)); legacy.pop_back(); legacy.pop_back(); legacy.pop_back(); @@ -5236,6 +5238,7 @@ void verifyVertexInfoVersion19() { const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast<std::ptrdiff_t>(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag"); auto orderedVersion34 = orderedBytes; + orderedVersion34.resize(orderedVersion34.size() - sizeof(std::uint32_t)); orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); @@ -5245,7 +5248,7 @@ void verifyVertexInfoVersion19() { const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 5u); + orderedVersion30.resize(orderedVersion30.size() - 5u - sizeof(std::uint32_t)); orderedVersion30.erase(orderedVersion30.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion30.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -5253,6 +5256,7 @@ void verifyVertexInfoVersion19() { auto orderedSpanned = ordered; orderedSpanned.target.spannedWaves = true; auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); + orderedVersion32.resize(orderedVersion32.size() - sizeof(std::uint32_t)); orderedVersion32.pop_back(); orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); @@ -5275,6 +5279,7 @@ void verifyVertexInfoVersion19() { const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version30 = sampledBytes; + version30.resize(version30.size() - sizeof(std::uint32_t)); version30.pop_back(); version30.pop_back(); version30.pop_back(); @@ -5287,6 +5292,7 @@ void verifyVertexInfoVersion19() { auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.resize(version32.size() - sizeof(std::uint32_t)); version32.pop_back(); version32.pop_back(); version32.pop_back(); @@ -5298,6 +5304,7 @@ void verifyVertexInfoVersion19() { const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array<std::uint8_t, 32>{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); auto version29 = sampledBytes; + version29.resize(version29.size() - sizeof(std::uint32_t)); version29.pop_back(); version29.pop_back(); version29.pop_back(); @@ -5305,6 +5312,7 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = sampledBytes; + version36.resize(version36.size() - sizeof(std::uint32_t)); version36.pop_back(); version36.pop_back(); version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); @@ -5315,6 +5323,7 @@ void verifyVertexInfoVersion19() { const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; + version28.resize(version28.size() - sizeof(std::uint32_t)); version28.pop_back(); version28.pop_back(); version28.pop_back(); @@ -5325,6 +5334,7 @@ void verifyVertexInfoVersion19() { const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; + version27.resize(version27.size() - sizeof(std::uint32_t)); version27.pop_back(); version27.pop_back(); version27.pop_back(); @@ -5335,6 +5345,7 @@ void verifyVertexInfoVersion19() { const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; + legacyPixel.resize(legacyPixel.size() - sizeof(std::uint32_t)); legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); @@ -5393,6 +5404,7 @@ void verifyDepthRemapRequests() { require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); @@ -5403,6 +5415,7 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); version35.pop_back(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index cf4bbc0138..10aaf73ff4 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -299,6 +299,7 @@ private: append(key, value.float32DenormPreserve); append(key, value.fusedExactArithmetic); append(key, value.largePixelFunctionLdsBuffer); + append(key, value.pixelFunctionStorageDwords); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 9457081b29..c754c198e6 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -872,6 +872,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const } writer.WriteBool(request.target.fusedExactArithmetic); writer.WriteBool(request.target.largePixelFunctionLdsBuffer); + writer.WriteU32(request.target.pixelFunctionStorageDwords); return base64Encode(buffer); } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2204a0db94..a310aa792d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -607,6 +607,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.fusedExactArithmetic = request.target.fusedExactArithmetic; targetOptions.largePixelFunctionLdsBuffer = request.target.largePixelFunctionLdsBuffer; + targetOptions.pixelFunctionStorageDwords = request.target.pixelFunctionStorageDwords; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 978d400225..962ddf0d89 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -300,6 +300,7 @@ struct SpirvTarget { bool float32DenormPreserve = false; bool fusedExactArithmetic = false; bool largePixelFunctionLdsBuffer = false; + std::uint32_t pixelFunctionStorageDwords = 4096u; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 4cb86e2472..9a418524a3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -40,6 +40,7 @@ struct SpirvTargetOptions { std::vector<std::pair<std::uint32_t, std::uint32_t>> meshPerVertexLocations; bool waveLdsMemoryBarriers = false; bool largePixelFunctionLdsBuffer = false; + std::uint32_t pixelFunctionStorageDwords = 4096u; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 0cfb1a7be2..8fd7e67442 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,7 +281,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; - if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > target.pixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; state.requirements.subgroupLocalInvocationId = true; state.requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 4ebf8833b1..7e55864b42 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -500,12 +500,14 @@ int main() { request.target.float32FlushToZero = !preserve; request.target.float32DenormPreserve = preserve; request.target.largePixelFunctionLdsBuffer = !preserve; + request.target.pixelFunctionStorageDwords = preserve ? 2048u : 4096u; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve && replay.request.target.pixelFunctionStorageDwords == request.target.pixelFunctionStorageDwords); auto bytes = DecodeBase64(encoded); Require(bytes.size() >= 8u && bytes[4] == 45u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; + version40.resize(version40.size() - sizeof(std::uint32_t)); version40.pop_back(); version40.pop_back(); version40.pop_back(); @@ -571,6 +573,7 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 76b39dd32c..40b1730a16 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,13 +151,10 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a sticky overflow flag and reuses slot 0 for the excess subgroups (sharing that region with the subgroup that owns it); `CheckPixelFunctionLds` throws when the flag is set, from `WaitIdle` and when a present fence retires, then clears it; arrays of 4097 to 6143 dwords take the buffer path though only 4096 (completes), 6144 (timeout), 7680 and 8192 (fault) were measured on that 5090; the pool is the first `HOST_VISIBLE|HOST_COHERENT` type so the host can map the 16-byte header without a separate device-local body (on discrete NVIDIA that is usually system memory for the full allocation, and a device-local body plus host-visible header was not measured); on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The pool fits subgroups in min(2 GiB, storage range); overflow sets a sticky flag checked from `WaitIdle` and present retirement. Header mapped via the first `HOST_VISIBLE|HOST_COHERENT` type. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From aa77a5ba98e4e510a3ef3b164a85ed4d53acb101 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 11:25:44 +0400 Subject: [PATCH 1315/1360] fix(shader): prefer device-local host-visible pixel LDS memory Keep the NVIDIA-only pool mappable for the sticky overflow header while preferring a device-local host-visible type when the driver exposes one. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 11 ++++++++++- docs/dev/debt/shader.md | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b291f76dac..b06370fd79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -126,7 +126,16 @@ void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, s context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, pool.buffer, &requirements); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + constexpr auto hostCoherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + constexpr auto deviceLocalHost = hostCoherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + std::optional<std::uint32_t> deviceLocalType; + for (std::uint32_t i = 0; i < context.memory.memoryTypeCount; ++i) { + if ((requirements.memoryTypeBits & (1u << i)) != 0 && (context.memory.memoryTypes[i].propertyFlags & deviceLocalHost) == deviceLocalHost) { + deviceLocalType = i; + break; + } + } + allocation.memoryTypeIndex = deviceLocalType.value_or(context.MemoryType(requirements.memoryTypeBits, hostCoherent)); const auto allocated = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); if (allocated != VK_SUCCESS) { context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, pool.buffer, nullptr); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 40b1730a16..b77564bc69 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The pool fits subgroups in min(2 GiB, storage range); overflow sets a sticky flag checked from `WaitIdle` and present retirement. Header mapped via the first `HOST_VISIBLE|HOST_COHERENT` type. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) and stays until device teardown; a draw that needs more slots sets a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring a device-local type when the driver exposes one. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 9ef24b1660a8d3fbcf1d118ea916e1810a24e124 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 11:26:33 +0400 Subject: [PATCH 1316/1360] fix(shader): keep the pixel LDS debt note under the length limit --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b77564bc69..64944d40b1 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) and stays until device teardown; a draw that needs more slots sets a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring a device-local type when the driver exposes one. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until device teardown; excess slots set a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring device-local when available. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 761af3c8391835468bed365f3bda31aa046e1254 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 11:27:23 +0400 Subject: [PATCH 1317/1360] fix(shader): keep the pixel LDS debt note under the length limit --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 64944d40b1..049ecaee9b 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until device teardown; excess slots set a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring device-local when available. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until teardown; excess slots sticky-flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent (prefer device-local). Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 913e458369456b7f3708db250b36fd2f7b405baf Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 16:50:43 +0400 Subject: [PATCH 1318/1360] fix(shader): deserialize pixel function storage dwords at version 45 The SM-budget threshold writes the storage size after the LDS flag; read it back and keep the fixtures on the fused+pointList trailer. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 8 ++++---- .../ControlFlow/src/RequestSerializer.cpp | 5 ++++- .../tests/RequestSerializerTests.cpp | 20 +++++++++++++------ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 877cf34683..1228701b44 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4967,7 +4967,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSwAAAA=", "new requests did not use serialization version 44"); + require(requestPrefix(encoded, 8u) == "NVNQQS0AAAA=", "new requests did not use serialization version 45"); const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -5085,7 +5085,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS0AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS4AAAA="}) { expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5401,7 +5401,7 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 13 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 6 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 6 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag and the pixel function storage size of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); @@ -5467,7 +5467,7 @@ void verifyMergedHullConfiguration() { constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); require(legacy.size() > 54u + pointTailBytes && legacy[4] == 45u, "a merged hull request did not use serialization version 45"); - legacy.resize(legacy.size() - 2u - pointTailBytes); + legacy.resize(legacy.size() - sizeof(std::uint32_t) - 2u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index c754c198e6..819b5ebfe9 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -936,7 +936,10 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.vertex = vertex; } if (version >= 44u) result.request.target.fusedExactArithmetic = reader.ReadBool(); - if (version >= 45u) result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); + if (version >= 45u) { + result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); + result.request.target.pixelFunctionStorageDwords = reader.ReadU32(); + } if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 7e55864b42..fdf1caea63 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -227,6 +227,7 @@ int main() { AppendVertexTail(upgraded); upgraded.push_back(0u); upgraded.push_back(0u); + upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -269,6 +270,7 @@ int main() { AppendVertexTail(layered); layered.push_back(0u); layered.push_back(0u); + layered.insert(layered.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -296,6 +298,7 @@ int main() { AppendVertexTail(bytes); bytes.push_back(0u); bytes.push_back(0u); + bytes.insert(bytes.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -428,6 +431,7 @@ int main() { AppendVertexTail(upgraded); upgraded.push_back(0u); upgraded.push_back(0u); + upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -446,6 +450,7 @@ int main() { auto version45 = DecodeBase64(serializer.Serialize(pointed)); Require(version45.size() > 15u && version45[4] == 45u); auto version44 = version45; + version44.resize(version44.size() - 4u); version44.pop_back(); version44[4] = 44u; auto version43 = version44; @@ -488,6 +493,7 @@ int main() { AppendVertexTail(main25); main25.push_back(0u); main25.push_back(0u); + main25.insert(main25.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -515,7 +521,7 @@ int main() { const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 46u; + future[4] = 47u; bool futureRejected = false; try { static_cast<void>(serializer.Deserialize(EncodeBase64(future))); @@ -561,6 +567,7 @@ int main() { const auto order = std::search(version41.begin(), version41.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) + pixel.sampleLocations.size() + 1; Require(version41.end() - order > static_cast<std::ptrdiff_t>(sizeof(pixel.sampleOrder)) && *order == 0x10u); version41.erase(order, order + sizeof(pixel.sampleOrder)); + version41.resize(version41.size() - 4u); version41.pop_back(); version41.pop_back(); version41[4] = 41u; @@ -620,7 +627,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); + legacy.resize(legacy.size() - (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -632,7 +639,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); + Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); } } @@ -641,14 +648,14 @@ int main() { const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy42.resize(legacy42.size() - 15u); + legacy42.resize(legacy42.size() - 19u); legacy42[4] = 42u; const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 22u); + legacy34.resize(legacy34.size() - 26u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast<std::ptrdiff_t>(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -662,7 +669,8 @@ int main() { request.target.fusedExactArithmetic = fused; request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); - Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 2u] == (fused ? 1u : 0u) && bytes.back() == 0u); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 6u] == (fused ? 1u : 0u) && bytes[bytes.size() - 5u] == 0u); + bytes.resize(bytes.size() - 4u); bytes.pop_back(); bytes[4] = 44u; Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused); From 066b9196369c405b1e61160f08f8eb10ca0e2575 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 19:58:33 +0400 Subject: [PATCH 1319/1360] fix(shader): fall back from BAR pixel LDS memory Prefer DEVICE_LOCAL|HOST_VISIBLE only when its heap fits the pool; if that allocate fails, retry plain host-visible/coherent so non-ReBAR NVIDIA does not throw OUT_OF_DEVICE_MEMORY. --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b06370fd79..146b038eba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -129,17 +129,29 @@ void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, s constexpr auto hostCoherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; constexpr auto deviceLocalHost = hostCoherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; std::optional<std::uint32_t> deviceLocalType; + std::optional<std::uint32_t> hostOnlyType; for (std::uint32_t i = 0; i < context.memory.memoryTypeCount; ++i) { - if ((requirements.memoryTypeBits & (1u << i)) != 0 && (context.memory.memoryTypes[i].propertyFlags & deviceLocalHost) == deviceLocalHost) { + if ((requirements.memoryTypeBits & (1u << i)) == 0) continue; + const auto flags = context.memory.memoryTypes[i].propertyFlags; + const auto heap = context.memory.memoryTypes[i].heapIndex; + if (!deviceLocalType && (flags & deviceLocalHost) == deviceLocalHost && context.memory.memoryHeaps[heap].size >= requirements.size) { deviceLocalType = i; - break; + } + if (!hostOnlyType && (flags & hostCoherent) == hostCoherent && (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == 0) { + hostOnlyType = i; } } - allocation.memoryTypeIndex = deviceLocalType.value_or(context.MemoryType(requirements.memoryTypeBits, hostCoherent)); - const auto allocated = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + const auto hostType = hostOnlyType.value_or(context.MemoryType(requirements.memoryTypeBits, hostCoherent)); + allocation.memoryTypeIndex = deviceLocalType.value_or(hostType); + auto allocated = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + if (allocated != VK_SUCCESS && deviceLocalType && allocation.memoryTypeIndex == *deviceLocalType && hostType != *deviceLocalType) { + allocation.memoryTypeIndex = hostType; + allocated = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + } if (allocated != VK_SUCCESS) { context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, pool.buffer, nullptr); pool.buffer = VK_NULL_HANDLE; + pool.memory = VK_NULL_HANDLE; Check(allocated, "vkAllocateMemory pixel function LDS"); } Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, pool.buffer, pool.memory, 0), "vkBindBufferMemory pixel function LDS"); From 0a3a3bb2687b066d90311b5fba917d00e73a49a4 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 20:47:54 +0400 Subject: [PATCH 1320/1360] fix(shader): strip the pixel LDS bool from legacy fixtures Version 45 adds a one-byte large-pixel LDS flag ahead of the storage dword; older fixture truncations left that byte as unexpected trailing data on Windows. --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1228701b44..eb2a9c2e28 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5243,12 +5243,13 @@ void verifyVertexInfoVersion19() { orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); + orderedVersion34.pop_back(); orderedVersion34.erase(orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset - 1u), orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion34[4] = 34u; const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 5u - sizeof(std::uint32_t)); + orderedVersion30.resize(orderedVersion30.size() - 6u - sizeof(std::uint32_t)); orderedVersion30.erase(orderedVersion30.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion30.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -5259,6 +5260,7 @@ void verifyVertexInfoVersion19() { orderedVersion32.resize(orderedVersion32.size() - sizeof(std::uint32_t)); orderedVersion32.pop_back(); orderedVersion32.pop_back(); + orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); orderedVersion32.pop_back(); orderedVersion32.pop_back(); @@ -5285,6 +5287,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -5310,11 +5313,13 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = sampledBytes; version36.resize(version36.size() - sizeof(std::uint32_t)); version36.pop_back(); version36.pop_back(); + version36.pop_back(); version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5329,6 +5334,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -5340,6 +5346,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -5351,6 +5358,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -5379,9 +5387,11 @@ void verifyVertexInfoVersion19() { require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage && replayPixel.request.context.pixel->reversedBlend == 0u, (what + " lost its pixel primitive class or read a layer export, dither flag or reversed-order blend code").c_str()); require(serializer.Serialize(replayPixel.request) == encodedPixel, (what + " misread the fields after its pixel primitive class").c_str()); } + std::cerr << "exit verifyVertexInfoVersion19\n"; } void verifyDepthRemapRequests() { + std::cerr << "enter verifyDepthRemapRequests\n"; using namespace ShaderRecompiler; const std::array<std::uint32_t, 1> code{0xbf810000u}; const RequestSerializer serializer; @@ -5394,6 +5404,7 @@ void verifyDepthRemapRequests() { vertex.target.depthRangeRemap = true; vertex.context.vertex->depthRemapMode = 2u; const auto remapped = serializer.Serialize(vertex); + std::cerr << "DR roundtrip\n"; const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); @@ -5408,8 +5419,10 @@ void verifyDepthRemapRequests() { legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); + std::cerr << "DR legacy\n"; const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str()); require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); @@ -5421,6 +5434,7 @@ void verifyDepthRemapRequests() { version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); + std::cerr << "DR replay35\n"; const auto replay35 = serializer.Deserialize(encodeBase64(version35)); require(replay35.request.target.depthRangeRemap && replay35.request.context.vertex->depthRemapMode == 2u && !replay35.request.target.float32FlushToZero && serializer.Serialize(replay35.request) == remapped, "a version 35 request misread the depth remap fields or read an f32 flush flag"); auto truncated = version35; From 500d77aef31971df7de892e4bd2ee8d911ae954f Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 20:51:20 +0400 Subject: [PATCH 1321/1360] fix(shader): drop debug traces from pixel LDS fixture strip Keep the version-45 bool pops for the pixel legacy fixtures; revert the extra depth-remap legacyBytes pop that truncated those heads, and remove temporary cerr probes. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index eb2a9c2e28..b335495fc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5387,11 +5387,9 @@ void verifyVertexInfoVersion19() { require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage && replayPixel.request.context.pixel->reversedBlend == 0u, (what + " lost its pixel primitive class or read a layer export, dither flag or reversed-order blend code").c_str()); require(serializer.Serialize(replayPixel.request) == encodedPixel, (what + " misread the fields after its pixel primitive class").c_str()); } - std::cerr << "exit verifyVertexInfoVersion19\n"; } void verifyDepthRemapRequests() { - std::cerr << "enter verifyDepthRemapRequests\n"; using namespace ShaderRecompiler; const std::array<std::uint32_t, 1> code{0xbf810000u}; const RequestSerializer serializer; @@ -5404,7 +5402,6 @@ void verifyDepthRemapRequests() { vertex.target.depthRangeRemap = true; vertex.context.vertex->depthRemapMode = 2u; const auto remapped = serializer.Serialize(vertex); - std::cerr << "DR roundtrip\n"; const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); @@ -5419,10 +5416,8 @@ void verifyDepthRemapRequests() { legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); - legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); - std::cerr << "DR legacy\n"; const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str()); require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); @@ -5434,7 +5429,6 @@ void verifyDepthRemapRequests() { version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); - std::cerr << "DR replay35\n"; const auto replay35 = serializer.Deserialize(encodeBase64(version35)); require(replay35.request.target.depthRangeRemap && replay35.request.context.vertex->depthRemapMode == 2u && !replay35.request.target.float32FlushToZero && serializer.Serialize(replay35.request) == remapped, "a version 35 request misread the depth remap fields or read an f32 flush flag"); auto truncated = version35; From a0ce5788b30b3c221a7ed8be3c60beaddbd70532 Mon Sep 17 00:00:00 2001 From: Luka Petrovic <lukamatej17@gmail.com> Date: Fri, 23 Oct 2026 19:37:01 +0200 Subject: [PATCH 1322/1360] fix(tools): validate compilation database entries --- tools/check_compile.py | 41 +++++++++++++++++++++--- tools/tests/test_check_compile.py | 53 ++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 9b66a1cd16..c2c0befc3a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -40,19 +40,53 @@ def repository(): return Path(result.stdout.strip()) +def validate_arguments(arguments): + if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: + raise ValueError("arguments must be a nonempty list of strings with an executable") + + +def compilation_database(text, source, skip_empty_commands=False): + database = json.loads(text) + if not isinstance(database, list): + raise ValueError(f"{source}: expected an array of command objects") + commands = [] + for index, entry in enumerate(database): + context = f"{source}: entry {index}" + if not isinstance(entry, dict): + raise ValueError(f"{context}: expected a command object") + for field in ("directory", "file"): + if field not in entry: + raise ValueError(f"{context}: missing {field}") + if not isinstance(entry[field], str) or not entry[field]: + raise ValueError(f"{context}: {field} must be a nonempty string") + if skip_empty_commands and "arguments" not in entry and entry.get("command") == "": + continue + if "arguments" in entry: + try: + validate_arguments(entry["arguments"]) + except ValueError as error: + raise ValueError(f"{context}: {error}") from error + elif "command" not in entry: + raise ValueError(f"{context}: expected arguments or command") + elif not isinstance(entry["command"], str) or not entry["command"].strip(): + raise ValueError(f"{context}: command must be a nonempty string") + commands.append(entry) + return commands + + def entries(build): if not build.is_dir(): raise ValueError(f"{build} is not a directory; configure the build first") manifest = build / "compile_commands.json" if manifest.is_file(): - return json.loads(manifest.read_text(encoding="utf-8-sig")) + return compilation_database(manifest.read_text(encoding="utf-8-sig"), manifest) ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"], encoding=ninja_encoding(ninja)) if result.returncode != 0: raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") - return json.loads(result.stdout) + return compilation_database(result.stdout, "ninja -t compdb", skip_empty_commands=True) def entry_key(entry, root): @@ -116,8 +150,7 @@ def syntax_only(entry, source): def syntax_arguments(arguments, directory, source): - if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: - raise ValueError("arguments must be a nonempty list of strings with an executable") + validate_arguments(arguments) for argument in arguments[1:]: if argument.startswith("@"): raise ValueError(f"unexpanded response file {argument[1:]}; provide expanded compile arguments before checking syntax") diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index ffd20390ab..c2f6f77643 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -88,7 +88,7 @@ class CheckCompileTests(unittest.TestCase): (self.build / "build.ninja").write_text( "rule cc\n command = " + command + " @$out.rsp\n" " rspfile = $out.rsp\n rspfile_content = -DREQUIRED -c $in -o $out -MD -MF $out.d\n" - "build unit.o: cc ../unit.c\n", encoding="utf-8") + "build order: phony\nbuild unit.o: cc ../unit.c || order\n", encoding="utf-8") self.assertFalse((self.build / "unit.o.rsp").exists()) result = self.tool("unit.c") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) @@ -515,6 +515,57 @@ class CheckCompileTests(unittest.TestCase): self.assertIn("arguments must be a nonempty list of strings with an executable", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_invalid_compilation_databases_are_rejected_without_a_traceback(self): + for database in ({}, {"file": "unit.cpp"}, None, 42, "commands"): + with self.subTest(database=database): + self.manifest(database) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("compile_commands.json: expected an array of command objects", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_entries_are_rejected_before_any_compiler_runs(self): + source = self.source("unit.cpp") + invoked = self.root / "invoked" + script = self.compiler("import sys\nfrom pathlib import Path\nPath(sys.argv[1]).touch()\n") + valid = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{invoked}" "{source}"') + cases = [(value, "expected a command object") for value in (None, [], 42, "command")] + for field in ("directory", "file"): + missing = dict(valid) + del missing[field] + cases.append((missing, f"missing {field}")) + cases.extend(({**valid, field: value}, f"{field} must be a nonempty string") + for value in (None, [], 42, "")) + cases.append(({key: value for key, value in valid.items() if key != "command"}, + "expected arguments or command")) + cases.extend(({**valid, "command": value}, "command must be a nonempty string") + for value in (None, [], 42, "", " \t")) + cases.append(({**valid, "arguments": []}, + "arguments must be a nonempty list of strings with an executable")) + for invalid, message in cases: + with self.subTest(invalid=invalid): + self.manifest([valid, invalid]) + result = self.tool("--jobs", "2", "unit.cpp") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(f"compile_commands.json: entry 1: {message}", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(invoked.exists(), result.stdout + result.stderr) + + def test_invalid_ninja_databases_are_validated_before_compilation(self): + source = self.source("unit.cpp") + cases = (({}, "expected an array of command objects"), + ([self.entry("unit.cpp", "compiler"), None], "entry 1: expected a command object")) + for database, message in cases: + with self.subTest(database=database), \ + patch.object(check_compile.shutil, "which", return_value="ninja"), \ + patch.object(check_compile, "ninja_encoding", return_value="utf-8"), \ + patch.object(check_compile, "capture", return_value=subprocess.CompletedProcess( + [], 0, json.dumps(database), "")), \ + patch.object(check_compile.subprocess, "run") as compiler: + with self.assertRaisesRegex(ValueError, "ninja -t compdb: " + message): + check_compile.check(self.root, self.build, [source]) + compiler.assert_not_called() + def test_parallel_checks_overlap_but_diagnostics_keep_source_order(self): barrier = self.root / "barrier" barrier.mkdir() From 7333de6aee8049133cb8bc47c3ddd7d44809a306 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Wed, 21 Oct 2026 10:57:34 +0000 Subject: [PATCH 1323/1360] fix(agc): give a depth stencil copy's layout barrier both aspects The depth sample reorder copies a D32S8 surface into an image of the same format. When only depth is reordered, the copy's transition to GENERAL named the depth aspect alone, which Vulkan does not allow for a depth/stencil format without separateDepthStencilLayouts (VUID-VkImageMemoryBarrier-image-03320). The barrier now names both aspects of a format with stencil, as transitionLocated does; the copy itself still moves depth only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp index dfcef91efc..647b38274c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp @@ -185,6 +185,7 @@ public: auto copy = copyFor(target.format, target.samples, target.extent, stencil); kept.push_back(copy); const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + const VkImageAspectFlags layoutAspects = VK_IMAGE_ASPECT_DEPTH_BIT | (HasStencil(target.format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); if (!copy->general) { VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -193,7 +194,7 @@ public: toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.image = copy->image; - toGeneral.subresourceRange = {aspects, 0, 1, 0, 1}; + toGeneral.subresourceRange = {layoutAspects, 0, 1, 0, 1}; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); copy->general = true; } From d8d03c2839f16779e651b1fd4ab253dfa393de00 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 15:16:09 -0300 Subject: [PATCH 1324/1360] fix(libkernel): give the host the requested socket buffer and test the inherited sizes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libkernel/Socket/src/SocketOptions.cpp | 7 +++---- core/libs/tests/GuestSocketOptions.cpp | 14 ++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 77abe31e34..88a166ab74 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -108,7 +108,7 @@ void EnsureBuffers(SocketOptionState& state, int family, int type) { } int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { - const std::uint32_t count = std::min(*length, size); + const std::uint32_t count = output ? std::min(*length, size) : 0; if (count != 0) std::memcpy(output, value, count); *length = count; return 0; @@ -179,10 +179,9 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa return 0; } if (number > MaximumReservation) return ErrorNoBuffers; - const int reserved = std::min(number, MaximumBuffer); const int nativeOption = send ? SO_SNDBUF : SO_RCVBUF; - if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast<NativeOptionValue>(&reserved), sizeof(reserved))) return HostError(); - size = reserved; + if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast<NativeOptionValue>(&number), sizeof(number))) return HostError(); + size = std::min(number, MaximumBuffer); mark = std::min(mark, size); return 0; } diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index 730c174523..3f9d26467f 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -238,6 +238,20 @@ void BuffersFollowThePs5() { Require(GetInt(pair[0], SoSendBuffer) == 2048 && GetInt(pair[0], SoReceiveBuffer) == 4096, "an AF_UNIX datagram socket starts with 2048 and 4096 bytes"); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0, "close failed"); Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); + const int listener = socket_nid_postfix(2, Stream, 0); + Require(listener >= 0, "socket failed"); + auto address = Loopback(0); + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 1) == 0, "listen failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0, "getsockname failed"); + SetInt(listener, SoReceiveBuffer, 3000); + SetInt(listener, SoSendLowWater, 700); + const int client = socket_nid_postfix(2, Stream, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0, "connect failed"); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0, "accept failed"); + Require(GetInt(accepted, SoReceiveBuffer) == 3000 && GetInt(accepted, SoSendLowWater) == 700 && GetInt(accepted, SoSendBuffer) == 32768, "an accepted socket takes the listener's buffers"); + Require(close_nid_postfix(accepted) == 0 && close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close failed"); } void NullValues() { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0a121b798f..6bcf1a0608 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -211,7 +211,7 @@ - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`. `SO_DEBUG`, `SO_REUSEPORT` and `SO_TIMESTAMP`, `IP_TTL` and `IP_TOS`, `TCP_MAXSEG` and `TCP_KEEPIDLE` fail with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) accepted them and read back their bit, 64, 0, 536 and 7200; the rest of the `IPPROTO_IP` level, the other `IPPROTO_TCP` and `IPPROTO_IPV6` options and `SO_USELOOPBACK` fail the same way and were not measured. `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean option reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden -- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60), and the host socket gets the same size. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured +- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured - [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console - [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured From b0b7198b7a0ca2673deab11c4a87f64fae871e8b Mon Sep 17 00:00:00 2001 From: Benjamin Cortez <benjacortezf@gmail.com> Date: Wed, 7 Oct 2026 21:35:14 -0300 Subject: [PATCH 1325/1360] feat(libSceAudioOut): implement sceAudioOutGetSystemState --- core/libs/SceTypes.hpp | 7 ++++ core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 +++++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 8 ++++ .../tests/AudioOutSystemState.cpp | 39 +++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 1 + 5 files changed, 65 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 4a61c38ed7..f51297c09b 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -655,6 +655,13 @@ struct AudioOutPortState { std::uint64_t activeState; }; +struct AudioOutSystemState { + float loudness; + std::uint8_t reserved8[4]; + std::uint64_t reserved[3]; +}; +static_assert(sizeof(AudioOutSystemState) == 32); + struct Audio3dOpenParameters { std::uint64_t size_this; std::uint32_t granularity; diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index ba5e91090b..7c97d07a55 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -245,4 +245,14 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAudioOut>") endif() + + add_test_executable(audio_out_system_state_tests tests/AudioOutSystemState.cpp) + target_include_directories(audio_out_system_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_system_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_system_state_tests) + add_test(NAME audio_out_system_state COMMAND audio_out_system_state_tests) + if(WIN32) + set_tests_properties(audio_out_system_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAudioOut>") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 9faeaef957..8f1573f958 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -714,4 +714,12 @@ int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } +int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState* state) { + if (state == nullptr) { + return -2144993276; + } + state->loudness = 0.0f; + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp new file mode 100644 index 0000000000..4cf928d770 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp @@ -0,0 +1,39 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include <cstdint> +#include <cstdio> +#include <cstdlib> + +extern "C" { +int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState*); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int invalidPointer = static_cast<int>(0x80260004); + +void TestGetSystemState() { + AudioOutSystemState state{}; + state.loudness = -1.0f; + Require(sceAudioOutGetSystemState(&state) == 0, "sceAudioOutGetSystemState must succeed with valid pointer"); + Require(state.loudness == 0.0f, "loudness must be reported as 0.0f"); +} + +void TestInvalidPointer() { + Require(sceAudioOutGetSystemState(nullptr) == invalidPointer, "null pointer must return SCE_AUDIO_OUT_ERROR_INVALID_POINTER"); +} + +} + +int main() { + TestGetSystemState(); + TestInvalidPointer(); + return 0; +} diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 37e4a53c5a..83e83145fa 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -18,6 +18,7 @@ - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [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 - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device +- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the 32-byte layout (`float loudness`, 4 reserved bytes, 3 reserved 64-bit words) follows shadPS4's `OrbisAudioOutSystemState` and SharpProspero's `SceAudioOutSystemState` (`Size = 32`); it is not confirmed on a console. Loudness is reported as a constant 0.0f, although SharpProspero documents it in LKFS, so the value is unconfirmed. shadPS4 returns NOT_INIT before `sceAudioOutInit`; that check is not modelled. Imported by PPSA30528, which fails to load without it - [sceAudioOutSysOpen, sceAudioOutSysConfigureOutput, sceAudioOutSysClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; the signatures follow [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L24-L29), whose audio code both titles share, and are not confirmed on a console. `sceAudioOutSysOpen(user, mode)` opens a port only for mode 5, the TrueHD MAT route: [the same project's carrier table](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/iec61937.cpp#L125-L140), read from sceAudioOutExOpen's own tables in firmware 12.70, describes it as a 768 kHz port of 1024 S16 8-channel frames whose 16-byte frames are consumed at 192 kHz; every other mode throws. Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so like an Ex port a Sys port has no host device: `sceAudioOutOutput` paces it at its own rate and drops the MAT bursts. `sceAudioOutSysConfigureOutput(type, flags, mode, target, option)` accepts only the observed `(1, 0, 5, 1, 0)` HDMI MAT switch and changes nothing; other arguments throw. `sceAudioOutSysClose` closes a Sys port and throws for any other handle. The restore path EVO-PLAYER-PS5 takes after a Sys port calls `sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0)`, so no Sys reset combination is known - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order From de28243d263e0d00a36d231b41c18177b6e240e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 20:18:56 +0200 Subject: [PATCH 1326/1360] fix(libSceAudioOut): clear sceAudioOutGetSystemState reserved fields Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 1 + core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 8f1573f958..e973018f35 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -718,6 +718,7 @@ int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState* state) { if (state == nullptr) { return -2144993276; } + *state = {}; state->loudness = 0.0f; return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp index 4cf928d770..614a2ba2eb 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp @@ -22,8 +22,11 @@ constexpr int invalidPointer = static_cast<int>(0x80260004); void TestGetSystemState() { AudioOutSystemState state{}; state.loudness = -1.0f; + state.reserved8[0] = 0xAA; + state.reserved[2] = ~0ull; Require(sceAudioOutGetSystemState(&state) == 0, "sceAudioOutGetSystemState must succeed with valid pointer"); Require(state.loudness == 0.0f, "loudness must be reported as 0.0f"); + Require(state.reserved8[0] == 0 && state.reserved[2] == 0, "reserved fields must be cleared"); } void TestInvalidPointer() { From c345ed42d20242092625b3192d048e2c63eb1acd Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:09:36 +0100 Subject: [PATCH 1327/1360] feat(libSceCesCs): add the strict JIS X 0208 EUC-JP and Shift-JIS profiles Implement sceCesUcsProfileInitEucJpX0208, sceCesUcsProfileInitEucJpX0208Ss2, sceCesUcsProfileInitEucJpX0208Ss2Ss3 and sceCesUcsProfileInitSJis1997X0208. The JIS X 0208 table is JIS0208.TXT, derived from the CP932 table without rows 13 and 89-92 and with six cells changed. The EUC-JP variants add SS2 half-width katakana and SS3 JIS X 0212 as their names say. Cell 0x2140 and the Shift-JIS single bytes 0x5C and 0x7E, where the sources disagree, throw. --- core/libs/prx/libSceCesCs/CMakeLists.txt | 1 + core/libs/prx/libSceCesCs/Export.cpp | 148 +++++++++-- core/libs/prx/libSceCesCs/PendingExports.cpp | 20 -- core/libs/tests/GuestCesJapanese.cpp | 250 ++++++++++++++++++- docs/dev/debt/libSceCesCs.md | 3 +- 5 files changed, 372 insertions(+), 50 deletions(-) diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index c59a762ef5..991387c540 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -38,6 +38,7 @@ if(BUILD_TESTING) add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) endif() target_include_directories(guest_ces_${variant}_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_ces_${variant}_tests) if(variant MATCHES "_module$") target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs-module) else() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 8ebedd1665..46e3aad1fe 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -4,6 +4,7 @@ #include <bit> #include <cstring> #include <iterator> +#include <stdexcept> #include <utility> #include <vector> #include "SceTypes.hpp" @@ -147,14 +148,74 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { } constexpr uint8_t EUC_JP_TAG[8] = {'E', 'U', 'C', 'J', 'P', '9', '3', '2'}; +constexpr uint8_t EUC_JP_X0208_TAG[8] = {'E', 'U', 'C', 'J', 'X', '2', '0', '8'}; +constexpr uint8_t EUC_JP_X0208_SS2_TAG[8] = {'E', 'U', 'C', 'J', 'X', 'S', 'S', '2'}; +constexpr uint8_t EUC_JP_X0208_SS3_TAG[8] = {'E', 'U', 'C', 'J', 'X', 'S', 'S', '3'}; constexpr uint8_t SJIS_TAG[8] = {'S', 'J', 'I', 'S', '1', '9', '9', '7'}; +constexpr uint8_t SJIS_X0208_TAG[8] = {'S', 'J', 'I', 'S', 'X', '2', '0', '8'}; + +bool hasTag(const void* profile, const uint8_t (&tag)[8]) { + return profile != nullptr && std::memcmp(profile, tag, sizeof(tag)) == 0; +} + +bool isEucJpX0208(const void* profile) { + return hasTag(profile, EUC_JP_X0208_TAG) || hasTag(profile, EUC_JP_X0208_SS2_TAG) || hasTag(profile, EUC_JP_X0208_SS3_TAG); +} bool isEucJp(const void* profile) { - return profile != nullptr && std::memcmp(profile, EUC_JP_TAG, sizeof(EUC_JP_TAG)) == 0; + return hasTag(profile, EUC_JP_TAG) || isEucJpX0208(profile); +} + +bool hasSs2(const void* profile) { + return !hasTag(profile, EUC_JP_X0208_TAG); +} + +bool hasSs3(const void* profile) { + return hasTag(profile, EUC_JP_TAG) || hasTag(profile, EUC_JP_X0208_SS3_TAG); +} + +bool isSjisX0208(const void* profile) { + return hasTag(profile, SJIS_X0208_TAG); } bool isSjis(const void* profile) { - return profile != nullptr && std::memcmp(profile, SJIS_TAG, sizeof(SJIS_TAG)) == 0; + return hasTag(profile, SJIS_TAG) || isSjisX0208(profile); +} + +struct JisChange { + uint16_t jis; + uint16_t code; +}; + +constexpr JisChange JIS_X0208_CHANGES[6] = {{0x2141, 0x301C}, {0x2142, 0x2016}, {0x215D, 0x2212}, {0x2171, 0x00A2}, {0x2172, 0x00A3}, {0x224C, 0x00AC}}; +constexpr uint16_t JIS_X0208_UNSETTLED_CELL = 0x2140; +constexpr uint16_t JIS_X0208_UNSETTLED_CODE = 0xFF3C; +constexpr const char* JIS_X0208_UNSETTLED = "libSceCesCs: the console's mapping of JIS X 0208 cell 0x2140 (U+005C or U+FF3C) is unknown"; + +constexpr uint32_t jisIndex(uint16_t jis) { + return ((jis >> 8) - 0x21) * 94 + (jis & 0xFF) - 0x21; +} + +bool isCp932ExtensionRow(uint32_t row) { + return row == 13 || (row >= 89 && row <= 92); +} + +uint16_t strictJisX0208(uint32_t index) { + if (isCp932ExtensionRow(index / 94 + 1) || index == jisIndex(JIS_X0208_UNSETTLED_CELL)) return 0; + for (const JisChange& change : JIS_X0208_CHANGES) { + if (jisIndex(change.jis) == index) return change.code; + } + return JisX0208::TO_UCS2[index]; +} + +uint32_t jisX0208ToUnicode(bool strict, uint32_t index) { + if (!strict) return JisX0208::TO_UCS2[index]; + if (index == jisIndex(JIS_X0208_UNSETTLED_CELL)) throw std::runtime_error(JIS_X0208_UNSETTLED); + return strictJisX0208(index); +} + +void requireSettledSjisX0208(uint32_t code) { + if (code == 0x5C || code == 0x7E || code == 0xA5 || code == 0x203E) throw std::runtime_error("libSceCesCs: the console's mapping of Shift-JIS bytes 0x5C and 0x7E (ASCII or JIS X 0201 Roman) is unknown"); } constexpr uint8_t EUC_CN_TAG[8] = {'E', 'U', 'C', 'C', 'N', '9', '3', '6'}; @@ -391,7 +452,9 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 return 0; } if (isSjis(profile)) { + const bool strict = isSjisX0208(profile); if (lead < 0x80) { + if (strict) requireSettledSjisX0208(lead); length = 1; code = lead; return 0; @@ -405,7 +468,7 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; const uint8_t trail = mbcs[1]; if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return CES_ERROR_INVALID_ENCODE; - if (lead >= 0xFA) { + if (lead >= 0xFA && !strict) { const uint32_t trailIdx = trail < 0x7F ? trail - 0x40 : trail - 0x41; const uint32_t offset = (lead - 0xFA) * 188 + trailIdx; if (offset <= 387) { @@ -428,7 +491,7 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 int cell = 0; sjisToJis(lead, trail, row, cell); if (row < 1 || row > 94 || cell < 1 || cell > 94) return CES_ERROR_UNASSIGNED_CODE; - code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; + code = jisX0208ToUnicode(strict, (row - 1) * 94 + (cell - 1)); if (code == 0) return CES_ERROR_UNASSIGNED_CODE; length = 2; return 0; @@ -440,7 +503,7 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 code = lead; return 0; } - if (lead == 0x8E) { + if (lead == 0x8E && hasSs2(profile)) { if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; if (mbcs[1] < 0xA1 || mbcs[1] > 0xDF) return CES_ERROR_INVALID_ENCODE; length = 2; @@ -451,10 +514,10 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE) return CES_ERROR_INVALID_ENCODE; length = 2; - code = JisX0208::TO_UCS2[(lead - 0xA1) * 94 + (mbcs[1] - 0xA1)]; + code = jisX0208ToUnicode(isEucJpX0208(profile), (lead - 0xA1) * 94 + (mbcs[1] - 0xA1)); return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; } - if (lead == 0x8F) { + if (lead == 0x8F && hasSs3(profile)) { if (mbcsMax < 3) return CES_ERROR_SRC_BUFFER_END; if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE || mbcs[2] < 0xA1 || mbcs[2] > 0xFE) return CES_ERROR_INVALID_ENCODE; length = 3; @@ -470,20 +533,27 @@ struct JisCell { bool supplementary; }; -const std::vector<JisCell>& jisReverse() { - static const auto reverse = [] { - std::vector<JisCell> entries; - entries.reserve(2 * 94 * 94); - for (uint32_t cell = 0; cell < 94 * 94; ++cell) { - if (JisX0208::TO_UCS2[cell] != 0) entries.push_back({JisX0208::TO_UCS2[cell], static_cast<uint16_t>(cell), false}); - } - for (uint32_t cell = 0; cell < 94 * 94; ++cell) { - if (JisX0212::TO_UCS2[cell] != 0) entries.push_back({JisX0212::TO_UCS2[cell], static_cast<uint16_t>(cell), true}); - } - std::stable_sort(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code < right.code; }); - entries.erase(std::unique(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code == right.code; }), entries.end()); - return entries; - }(); +std::vector<JisCell> buildJisReverse(bool strict) { + std::vector<JisCell> entries; + entries.reserve(2 * 94 * 94); + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + const uint16_t code = strict ? strictJisX0208(cell) : JisX0208::TO_UCS2[cell]; + if (code != 0) entries.push_back({code, static_cast<uint16_t>(cell), false}); + } + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + if (JisX0212::TO_UCS2[cell] != 0) entries.push_back({JisX0212::TO_UCS2[cell], static_cast<uint16_t>(cell), true}); + } + std::stable_sort(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code < right.code; }); + entries.erase(std::unique(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code == right.code; }), entries.end()); + return entries; +} + +const std::vector<JisCell>& jisReverse(bool strict) { + if (strict) { + static const auto strictReverse = buildJisReverse(true); + return strictReverse; + } + static const auto reverse = buildJisReverse(false); return reverse; } @@ -496,6 +566,8 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return unicodeToSbc(static_cast<const uint8_t*>(profile), code, mbcs[0]) ? 0 : CES_ERROR_UNASSIGNED_CODE; } if (isSjis(profile)) { + const bool strict = isSjisX0208(profile); + if (strict) requireSettledSjisX0208(code); if (code < 0x80) { length = 1; mbcs[0] = static_cast<uint8_t>(code); @@ -507,7 +579,8 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return 0; } if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; - const auto& reverse = jisReverse(); + if (strict && code == JIS_X0208_UNSETTLED_CODE) throw std::runtime_error(JIS_X0208_UNSETTLED); + const auto& reverse = jisReverse(strict); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast<uint16_t>(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); if (found == reverse.end() || found->code != code || found->supplementary) return CES_ERROR_UNASSIGNED_CODE; jisToSjis(found->cell / 94 + 1, found->cell % 94 + 1, mbcs[0], mbcs[1]); @@ -520,15 +593,18 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return 0; } if (code >= 0xFF61 && code <= 0xFF9F) { + if (!hasSs2(profile)) return CES_ERROR_UNASSIGNED_CODE; length = 2; mbcs[0] = 0x8E; mbcs[1] = static_cast<uint8_t>(0xA1 + (code - 0xFF61)); return 0; } if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; - const auto& reverse = jisReverse(); + const bool strict = isEucJpX0208(profile); + if (strict && code == JIS_X0208_UNSETTLED_CODE) throw std::runtime_error(JIS_X0208_UNSETTLED); + const auto& reverse = jisReverse(strict); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast<uint16_t>(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); - if (found == reverse.end() || found->code != code) return CES_ERROR_UNASSIGNED_CODE; + if (found == reverse.end() || found->code != code || (found->supplementary && !hasSs3(profile))) return CES_ERROR_UNASSIGNED_CODE; if (found->supplementary) { length = 3; mbcs[0] = 0x8F; @@ -942,6 +1018,12 @@ void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { return sheet; } +void* APS5_VABI sceCesUcsProfileInitSJis1997X0208(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, SJIS_X0208_TAG, sizeof(SJIS_X0208_TAG)); + return sheet; +} + int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { if (!context) return CES_ERROR_INVALID_PARAMETER; std::memcpy(context, &profile, sizeof(profile)); @@ -961,6 +1043,24 @@ void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void* sheet) { return sheet; } +void* APS5_VABI sceCesUcsProfileInitEucJpX0208(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_X0208_TAG, sizeof(EUC_JP_X0208_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_X0208_SS2_TAG, sizeof(EUC_JP_X0208_SS2_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_X0208_SS3_TAG, sizeof(EUC_JP_X0208_SS3_TAG)); + return sheet; +} + void* APS5_VABI sceCesUcsProfileInitEucKr(void* sheet) { if (!sheet) return nullptr; std::memcpy(sheet, EUC_KR_TAG, sizeof(EUC_KR_TAG)); diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index c125d6b76b..3680b257df 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -257,21 +257,6 @@ int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitEucJpX0208(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitGb18030_2000(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -297,11 +282,6 @@ int APS5_VABI sceCesUcsProfileInitIso2022JpRfc1468(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitSJis1997X0208(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitSJis2004X0213(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp index 2103b87187..754471576e 100644 --- a/core/libs/tests/GuestCesJapanese.cpp +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -2,10 +2,21 @@ #include <cstdint> #include <cstdlib> #include <cstring> +#include <stdexcept> +#include <string> extern "C" { void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpX0208(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void*); +void* APS5_VABI sceCesUcsProfileInitSJis1997X0208(void*); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucJpToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesEucJpToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); int APS5_VABI sceCesSJisToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); int APS5_VABI sceCesSJisToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); @@ -36,14 +47,242 @@ static void Require(bool value) { if (!value) std::abort(); } static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast<const std::uint8_t*>(text); } +constexpr int invalidParameter = static_cast<int>(0x805C0001); +constexpr int srcBufferEnd = static_cast<int>(0x805C0011); +constexpr int invalidEncode = static_cast<int>(0x805C0014); +constexpr int unassignedCode = static_cast<int>(0x805C0020); +constexpr int dstBufferEnd = static_cast<int>(0x805C0031); +constexpr std::uint32_t jisX0208Cells = 6878; +constexpr std::uint32_t jisX0208Digest = 0xA614D6F6; +constexpr std::uint32_t jisX0212Cells = 6067; +constexpr std::uint32_t jisX0212Digest = 0x7C193A73; +const std::string unsettledCell = "libSceCesCs: the console's mapping of JIS X 0208 cell 0x2140 (U+005C or U+FF3C) is unknown"; +const std::string unsettledByte = "libSceCesCs: the console's mapping of Shift-JIS bytes 0x5C and 0x7E (ASCII or JIS X 0201 Roman) is unknown"; + +template <typename TCall> +static bool Reports(const std::string& message, TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what() == message; + } + return false; +} + +static std::uint32_t Digest(std::uint32_t hash, std::uint16_t value) { + hash = (hash ^ (value & 0xFF)) * 0x01000193u; + return (hash ^ (value >> 8)) * 0x01000193u; +} + +static int EucToUcs2(const void* profile, const char* bytes, std::uint32_t size, std::uint16_t& ucs2) { + std::uint32_t used = 0; + return sceCesEucJpToUcs2(profile, Bytes(bytes), size, &used, &ucs2); +} + +static int SJisToUcs2(const void* profile, const char* bytes, std::uint32_t size, std::uint16_t& ucs2) { + std::uint32_t used = 0; + return sceCesSJisToUcs2(profile, Bytes(bytes), size, &used, &ucs2); +} + +static bool EucEncodes(const void* profile, std::uint16_t ucs2, const char* expected) { + std::uint8_t mbcs[4] = {}; + std::uint32_t size = 0; + return sceCesUcs2ToEucJp(ucs2, profile, mbcs, sizeof(mbcs), &size) == 0 && size == std::strlen(expected) && std::memcmp(mbcs, expected, size) == 0; +} + +static bool SJisEncodes(const void* profile, std::uint16_t ucs2, const char* expected) { + std::uint8_t mbcs[4] = {}; + std::uint32_t size = 0; + return sceCesUcs2ToSJis(ucs2, profile, mbcs, sizeof(mbcs), &size) == 0 && size == std::strlen(expected) && std::memcmp(mbcs, expected, size) == 0; +} + +static bool EncodeLeavesOutputs(int expected, const void* profile, std::uint16_t ucs2, bool sjis) { + std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t size = 77; + const int result = sjis ? sceCesUcs2ToSJis(ucs2, profile, mbcs, sizeof(mbcs), &size) : sceCesUcs2ToEucJp(ucs2, profile, mbcs, sizeof(mbcs), &size); + return result == expected && size == 77 && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; +} + +static bool DecodeLeavesOutputs(int expected, const void* profile, const char* bytes, std::uint32_t size, bool sjis) { + std::uint32_t used = 77; + std::uint16_t ucs2 = 0xEEEE; + const int result = sjis ? sceCesSJisToUcs2(profile, Bytes(bytes), size, &used, &ucs2) : sceCesEucJpToUcs2(profile, Bytes(bytes), size, &used, &ucs2); + return result == expected && used == 77 && ucs2 == 0xEEEE; +} + +static void SweepEucJisX0208(const void* profile) { + std::uint32_t assigned = 0; + std::uint32_t hash = 0x811C9DC5u; + for (std::uint32_t lead = 0xA1; lead <= 0xFE; ++lead) { + for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { + const std::uint8_t cell[2] = {static_cast<std::uint8_t>(lead), static_cast<std::uint8_t>(trail)}; + std::uint32_t used = 77; + std::uint16_t ucs2 = 0xEEEE; + std::uint16_t value = 0; + if (lead == 0xA1 && trail == 0xC0) { + Require(Reports(unsettledCell, [&] { sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); })); + } else if (const int result = sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { + Require(used == 2); + value = ucs2; + ++assigned; + std::uint8_t again[4] = {}; + std::uint32_t size = 0; + Require(sceCesUcs2ToEucJp(ucs2, profile, again, sizeof(again), &size) == 0 && size == 2 && again[0] == lead && again[1] == trail); + } else { + Require(result == unassignedCode && used == 77 && ucs2 == 0xEEEE); + } + hash = Digest(hash, value); + } + } + Require(assigned == jisX0208Cells && hash == jisX0208Digest); +} + +static void SweepSJisJisX0208(const void* profile) { + std::uint32_t assigned = 0; + std::uint32_t hash = 0x811C9DC5u; + for (int row = 1; row <= 94; ++row) { + for (int column = 1; column <= 94; ++column) { + std::uint16_t code = 0; + Require(sceCesJisX0208ToSJisCode(static_cast<std::uint8_t>(row), static_cast<std::uint8_t>(column), &code) == 0); + const std::uint8_t cell[2] = {static_cast<std::uint8_t>(code >> 8), static_cast<std::uint8_t>(code)}; + std::uint32_t used = 77; + std::uint16_t ucs2 = 0xEEEE; + std::uint16_t value = 0; + if (row == 1 && column == 32) { + Require(Reports(unsettledCell, [&] { sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); })); + } else if (const int result = sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { + Require(used == 2); + value = ucs2; + ++assigned; + std::uint8_t again[4] = {}; + std::uint32_t size = 0; + Require(sceCesUcs2ToSJis(ucs2, profile, again, sizeof(again), &size) == 0 && size == 2 && again[0] == cell[0] && again[1] == cell[1]); + } else { + Require(result == unassignedCode && used == 77 && ucs2 == 0xEEEE); + } + hash = Digest(hash, value); + } + } + Require(assigned == jisX0208Cells && hash == jisX0208Digest); +} + +static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { + alignas(8) std::uint8_t sheets[4][256] = {}; + Require(sceCesUcsProfileInitEucJpX0208(nullptr) == nullptr && sceCesUcsProfileInitEucJpX0208Ss2(nullptr) == nullptr); + Require(sceCesUcsProfileInitEucJpX0208Ss2Ss3(nullptr) == nullptr && sceCesUcsProfileInitSJis1997X0208(nullptr) == nullptr); + const void* euc = sceCesUcsProfileInitEucJpX0208(sheets[0]); + const void* ss2 = sceCesUcsProfileInitEucJpX0208Ss2(sheets[1]); + const void* ss3 = sceCesUcsProfileInitEucJpX0208Ss2Ss3(sheets[2]); + const void* sjis = sceCesUcsProfileInitSJis1997X0208(sheets[3]); + Require(euc == sheets[0] && ss2 == sheets[1] && ss3 == sheets[2] && sjis == sheets[3]); + const void* strictEuc[3] = {euc, ss2, ss3}; + + std::uint16_t ucs2 = 0; + Require(EucToUcs2(sjis, "\xA4\xA2", 2, ucs2) == invalidParameter); + Require(SJisToUcs2(euc, "\x82\xA0", 2, ucs2) == invalidParameter); + Require(EncodeLeavesOutputs(invalidParameter, sjis, 0x3042, false) && EncodeLeavesOutputs(invalidParameter, ss3, 0x3042, true)); + + for (const void* profile : strictEuc) { + SweepEucJisX0208(profile); + Require(EucToUcs2(profile, "\\~", 2, ucs2) == 0 && ucs2 == 0x5C && EucEncodes(profile, 0x5C, "\\") && EucEncodes(profile, 0x7E, "~")); + Require(EucToUcs2(profile, "\xA1\xC1", 2, ucs2) == 0 && ucs2 == 0x301C && EucEncodes(profile, 0x301C, "\xA1\xC1")); + Require(EucToUcs2(profile, "\xA1\xC2", 2, ucs2) == 0 && ucs2 == 0x2016); + Require(EucToUcs2(profile, "\xA1\xDD", 2, ucs2) == 0 && ucs2 == 0x2212); + Require(EucToUcs2(profile, "\xA1\xF1", 2, ucs2) == 0 && ucs2 == 0x00A2); + Require(EucToUcs2(profile, "\xA1\xF2", 2, ucs2) == 0 && ucs2 == 0x00A3); + Require(EucToUcs2(profile, "\xA2\xCC", 2, ucs2) == 0 && ucs2 == 0x00AC); + for (const std::uint16_t cp932Form : {0xFF5E, 0x2225, 0xFF0D, 0xFFE0, 0xFFE1, 0xFFE2, 0x2460, 0x2170}) Require(EncodeLeavesOutputs(unassignedCode, profile, cp932Form, false)); + Require(Reports(unsettledCell, [&] { EucEncodes(profile, 0xFF3C, ""); })); + Require(DecodeLeavesOutputs(unassignedCode, profile, "\xAD\xA1", 2, false) && DecodeLeavesOutputs(unassignedCode, profile, "\xF9\xA1", 2, false)); + Require(DecodeLeavesOutputs(unassignedCode, profile, "\xA9\xA1", 2, false) && DecodeLeavesOutputs(unassignedCode, profile, "\xFE\xFE", 2, false)); + Require(DecodeLeavesOutputs(invalidEncode, profile, "\xA4\x41", 2, false) && DecodeLeavesOutputs(invalidEncode, profile, "\xA4\xFF", 2, false)); + Require(DecodeLeavesOutputs(invalidEncode, profile, "\x80", 1, false) && DecodeLeavesOutputs(invalidEncode, profile, "\xFF", 1, false)); + Require(DecodeLeavesOutputs(srcBufferEnd, profile, "\xA4\xA2", 1, false)); + std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t size = 77; + Require(sceCesUcs2ToEucJp(0x3042, profile, mbcs, 1, &size) == dstBufferEnd && size == 77 && mbcs[0] == 0xEE); + } + Require(EucToUcs2(cp51932, "\xA1\xC0", 2, ucs2) == 0 && ucs2 == 0xFF3C && EucEncodes(cp51932, 0xFF3C, "\xA1\xC0")); + Require(EucToUcs2(cp51932, "\xA1\xC1", 2, ucs2) == 0 && ucs2 == 0xFF5E && EucToUcs2(cp51932, "\xAD\xA1", 2, ucs2) == 0 && ucs2 == 0x2460); + Require(EucEncodes(cp51932, 0x7E8A, "\xF9\xA1") && EucEncodes(ss3, 0x7E8A, "\x8F\xD4\xE3")); + Require(EncodeLeavesOutputs(unassignedCode, euc, 0x7E8A, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x7E8A, false)); + + Require(DecodeLeavesOutputs(invalidEncode, euc, "\x8E\xB1", 2, false) && DecodeLeavesOutputs(invalidEncode, euc, "\x8E", 1, false)); + Require(DecodeLeavesOutputs(invalidEncode, euc, "\x8F\xB0\xA1", 3, false) && DecodeLeavesOutputs(invalidEncode, ss2, "\x8F\xB0\xA1", 3, false)); + Require(EncodeLeavesOutputs(unassignedCode, euc, 0xFF71, false)); + Require(EncodeLeavesOutputs(unassignedCode, euc, 0x4E02, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x4E02, false)); + for (const void* profile : {ss2, ss3}) { + for (std::uint32_t kana = 0xA1; kana <= 0xDF; ++kana) { + const char bytes[3] = {'\x8E', static_cast<char>(kana), 0}; + Require(EucToUcs2(profile, bytes, 2, ucs2) == 0 && ucs2 == 0xFF61 + kana - 0xA1 && EucEncodes(profile, ucs2, bytes)); + } + Require(DecodeLeavesOutputs(invalidEncode, profile, "\x8E\xE0", 2, false) && DecodeLeavesOutputs(invalidEncode, profile, "\x8E\xA0", 2, false)); + Require(DecodeLeavesOutputs(srcBufferEnd, profile, "\x8E\xB1", 1, false)); + } + std::uint32_t supplementary = 0; + std::uint32_t hash = 0x811C9DC5u; + for (std::uint32_t lead = 0xA1; lead <= 0xFE; ++lead) { + for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { + const char bytes[4] = {'\x8F', static_cast<char>(lead), static_cast<char>(trail), 0}; + std::uint16_t value = 0; + if (const int result = EucToUcs2(ss3, bytes, 3, ucs2); result == 0) { + value = ucs2; + ++supplementary; + Require(EucEncodes(ss3, ucs2, ucs2 == 0x7E ? "~" : bytes)); + } else { + Require(result == unassignedCode); + } + hash = Digest(hash, value); + } + } + Require(supplementary == jisX0212Cells && hash == jisX0212Digest); + Require(DecodeLeavesOutputs(srcBufferEnd, ss3, "\x8F\xB0", 2, false) && DecodeLeavesOutputs(invalidEncode, ss3, "\x8F\xB0\x41", 3, false)); + Require(EucEncodes(ss3, 0x4E02, "\x8F\xB0\xA1") && EucEncodes(ss2, 0xFF71, "\x8E\xB1")); + + std::uint8_t utf8[4] = {}; + std::uint32_t used = 0; + std::uint32_t produced = 0; + Require(sceCesEucJpToUtf8(euc, Bytes("\xA1\xC1"), 2, &used, utf8, sizeof(utf8), &produced) == 0 && used == 2 && produced == 3 && std::memcmp(utf8, "\xE3\x80\x9C", 3) == 0); + alignas(8) std::uint8_t context[256] = {}; + std::uint16_t wide[8] = {}; + Require(sceCesMbcsUcsContextInit(context, ss2) == 0); + Require(sceCesMbcsStrToUcs2Str(context, Bytes("A\xA4\xA2\x8E\xB1"), 5, &used, wide, 8, &produced) == 0 && used == 5 && produced == 3 && wide[1] == 0x3042 && wide[2] == 0xFF71); + Require(sceCesMbcsUcsContextInit(context, euc) == 0); + Require(sceCesMbcsStrToUcs2Str(context, Bytes("A\xA4\xA2\x8E\xB1"), 5, &used, wide, 8, &produced) == invalidEncode); + + SweepSJisJisX0208(sjis); + Require(SJisToUcs2(sjis, "\x81\x60", 2, ucs2) == 0 && ucs2 == 0x301C && SJisEncodes(sjis, 0x301C, "\x81\x60")); + Require(SJisToUcs2(sjis, "\x81\x91", 2, ucs2) == 0 && ucs2 == 0x00A2 && SJisEncodes(sjis, 0x00AC, "\x81\xCA")); + for (const std::uint16_t cp932Form : {0xFF5E, 0x2225, 0xFF0D, 0xFFE0, 0xFFE1, 0xFFE2, 0x2460, 0x2170, 0x7E8A, 0x4E02}) Require(EncodeLeavesOutputs(unassignedCode, sjis, cp932Form, true)); + for (const char* removed : {"\x87\x40", "\xED\x40", "\xEE\xFC", "\xFA\x40", "\xFC\x4B", "\xF0\x40", "\xEF\xFC"}) Require(DecodeLeavesOutputs(unassignedCode, sjis, removed, 2, true)); + Require(SJisToUcs2(cp932, "\x87\x40", 2, ucs2) == 0 && ucs2 == 0x2460 && SJisToUcs2(cp932, "\xFA\x40", 2, ucs2) == 0 && ucs2 == 0x2170); + Require(SJisToUcs2(cp932, "\x81\x5F", 2, ucs2) == 0 && ucs2 == 0xFF3C && SJisToUcs2(cp932, "\\", 1, ucs2) == 0 && ucs2 == 0x5C); + Require(DecodeLeavesOutputs(invalidEncode, sjis, "\x82\x7F", 2, true) && DecodeLeavesOutputs(invalidEncode, sjis, "\x82\xFD", 2, true)); + Require(DecodeLeavesOutputs(srcBufferEnd, sjis, "\x82\xA0", 1, true)); + for (std::uint32_t byte = 0; byte <= 0xFF; ++byte) { + const char bytes[2] = {static_cast<char>(byte), 0}; + if (byte == 0x5C || byte == 0x7E) { + Require(Reports(unsettledByte, [&] { SJisToUcs2(sjis, bytes, 1, ucs2); })); + } else if (byte < 0x80) { + Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == byte && (byte == 0 || SJisEncodes(sjis, ucs2, bytes))); + } else if (byte >= 0xA1 && byte <= 0xDF) { + Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == 0xFF61 + byte - 0xA1 && SJisEncodes(sjis, ucs2, bytes)); + } else { + Require(DecodeLeavesOutputs(byte == 0x80 || byte == 0xA0 || byte >= 0xFD ? invalidEncode : srcBufferEnd, sjis, bytes, 1, true)); + } + } + for (const std::uint16_t unsettled : {0x5C, 0x7E, 0xA5, 0x203E}) Require(Reports(unsettledByte, [&] { SJisEncodes(sjis, unsettled, ""); })); + Require(Reports(unsettledCell, [&] { SJisEncodes(sjis, 0xFF3C, ""); })); + Require(sceCesMbcsUcsContextInit(context, sjis) == 0); + const std::uint16_t text[] = {0x3042, 0x301C, 0xFF71, 0}; + std::uint8_t mbcs[8] = {}; + Require(sceCesUcs2StrToMbcsStr(context, text, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && used == 3 && produced == 5 && std::memcmp(mbcs, "\x82\xA0\x81\x60\xB1", 5) == 0); + Require(Reports(unsettledByte, [&] { sceCesMbcsStrToUcs2Str(context, Bytes("A\\"), 2, &used, wide, 8, &produced); })); +} + int main() { - constexpr int invalidParameter = static_cast<int>(0x805C0001); constexpr int invalidSrcBuffer = static_cast<int>(0x805C0010); - constexpr int srcBufferEnd = static_cast<int>(0x805C0011); - constexpr int invalidEncode = static_cast<int>(0x805C0014); - constexpr int unassignedCode = static_cast<int>(0x805C0020); constexpr int invalidDstBuffer = static_cast<int>(0x805C0030); - constexpr int dstBufferEnd = static_cast<int>(0x805C0031); alignas(8) std::uint8_t sjisSheet[64] = {}; alignas(8) std::uint8_t eucSheet[64] = {}; @@ -166,5 +405,6 @@ int main() { Require(sceCesSJisCodeToJisX0208(sjisCode, &ku, &ten) == 0 && ku == row && ten == cell); } } + TestJisX0208Profiles(sjis, euc); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 8e967e5b14..6c90ed380d 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,10 +11,11 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements +- [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - cell `0x2140` and U+FF3C throw: JIS0208.TXT gives U+005C, glibc 2.39 and Python's `euc_jp` U+FF3C. In `sceCesUcsProfileInitSJis1997X0208`, bytes `0x5C` and `0x7E` and U+005C, U+007E, U+00A5 and U+203E throw: SHIFTJIS.TXT gives JIS X 0201 Roman, CP932 and Python's `shift_jis` ASCII; the console's is unknown. The table, [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT), is CP932's without rows 13 and 89-92 and with six cells changed, not measured. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), the `X0208Ss2` profile adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212. Signatures follow Orbital; the conversions above take these profiles with the same errors, a missing SS2 or SS3 answering `INVALID_ENCODE` and Shift-JIS leads `0xF0`-`0xFC` `UNASSIGNED_CODE` - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas, GB 18030 four-byte forms and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) - [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 76 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 72 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do From 98df163cc638f6cb5aacbd3feae3be1423e14fa1 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:06:33 +0500 Subject: [PATCH 1328/1360] fix(shader): widen LDS atomic fences to device scope on AMD's proprietary driver The recompiler orders an LDS atomic, and the compare-exchange loop that writes a byte or a halfword of LDS (ds_write_b8/b16), with a workgroup-scope OpMemoryBarrier after the update. AMD's proprietary Windows driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiles that memory barrier to s_barrier, as the code objects in its pipeline cache show. Such updates usually sit in branches that only some waves of a workgroup take, so those waves execute extra s_barriers, which pair with the other waves' barriers, and the other waves read LDS before it is written. An OpMemoryBarrier is not an execution barrier and SPIR-V allows it in non-uniform control flow, so the SPIR-V is valid; a device-scope OpMemoryBarrier compiles to GL0 and GL1 cache invalidations without s_barrier there. PS5LM Plus (PPSA99581, llama.cpp's Vulkan backend) drifted into garbage on that driver with flash attention on. Its flash-attention shader stores two halfwords to LDS in a branch that two of its four waves take, inside the KV loop. The driver's code had 23 s_barrier for the guest's 21; a replay of the dispatch differed from NVIDIA in 159 words, up to 1.7e38, and now has 21 and differs only in the last bit of 9 words. On that driver the fence is now device scope, which orders everything the workgroup-scope fence did. Other drivers keep the workgroup scope. The flag is part of the shader cache key, and agc_shader_replay takes --device-scope-lds-memory-barriers to replay a request the way that driver compiles it. The LDS update helper already fences, so the second, identical fence after it in ds_min/max_f32 and the other read-modify-write LDS operations is dropped. agc_driver_lds_subword_store_branch stores a halfword to LDS in a branch that waves 0-1 of four take, delays those waves and then exchanges LDS slots across one s_barrier. Before, thread 64 read a stale slot on the AMD driver; it passes on AMD and NVIDIA now. agc_subgroup_barrier checks the driver quirk and that ds_write_b16, ds_add_u32 and ds_max_f32 each emit one fence of the expected scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BM2kfDbr6MydxsfTckZJDR --- .../Execution/include/SubgroupBarrier.hpp | 4 + .../Execution/src/VulkanDevice.cpp | 3 + .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tests/SubgroupBarrier.cpp | 62 +++++++++++-- .../tests/execution/LdsSubwordStoreBranch.cpp | 92 +++++++++++++++++++ .../libSceAgcDriver/tools/ShaderReplay.cpp | 8 +- core/shader/recompiler/CacheKey.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 4 +- .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvMemory/SpirvBufferAccess.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvBufferAccess.cpp | 4 + .../SpirvMemory/SpirvMemoryInstructions.cpp | 7 +- docs/dev/debt/shader.md | 1 + 17 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp index 30f42ba2ab..8675650a20 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp @@ -9,6 +9,10 @@ inline bool SubgroupControlBarrierUsable(VkDriverId driver) { return driver != VK_DRIVER_ID_AMD_PROPRIETARY; } +inline bool WorkgroupScopeFenceUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b87e5ba1a8..2b6a4871c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -388,6 +388,7 @@ struct VulkanDevice::State { bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool robustBufferAccess2 = false; bool subgroupControlBarriers = true; + bool workgroupScopeFences = true; // 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; @@ -1274,6 +1275,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); + state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID); } if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) { VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV}; @@ -3833,6 +3835,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords; + target.deviceScopeLdsMemoryBarriers = !state->workgroupScopeFences; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 44299ca46d..2b6dee3b5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -750,6 +750,7 @@ void verifyKeySensitivity() { changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); + changes("device-scope LDS memory barriers", [](SampleRequest& sample) { sample.request.target.deviceScopeLdsMemoryBarriers = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp index 272e5ed914..0578ee283d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp @@ -5,10 +5,12 @@ #include <cstdint> #include <cstdio> #include <exception> +#include <span> #include <stdexcept> #include <string> #include <string_view> #include <unordered_map> +#include <utility> namespace { @@ -34,6 +36,10 @@ constexpr std::array<std::uint32_t, 19> BranchedLdsStep{ 0xbf8cc07fu, 0xbf8a0000u, 0xbf810000u, }; +constexpr std::array<std::uint32_t, 5> LdsHalfStore{0x34040082u, 0xd87c0400u, 0x00000102u, 0xbf8cc07fu, 0xbf810000u}; +constexpr std::array<std::uint32_t, 5> LdsAtomicAdd{0x34040082u, 0xd8000400u, 0x00000102u, 0xbf8cc07fu, 0xbf810000u}; +constexpr std::array<std::uint32_t, 5> LdsAtomicMax{0x34040082u, 0xd84c0400u, 0x00000102u, 0xbf8cc07fu, 0xbf810000u}; + struct Barriers { std::uint32_t subgroupControl = 0; std::uint32_t subgroupMemory = 0; @@ -43,14 +49,19 @@ void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } -Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { +struct MemoryBarriers { + std::uint32_t workgroup = 0; + std::uint32_t device = 0; +}; + +ShaderRecompiler::RecompileResult recompileProgram(std::span<const std::uint32_t> code, std::uint32_t threads, bool waveLdsMemoryBarriers, bool deviceScopeLdsMemoryBarriers) { using namespace ShaderRecompiler; const std::array<std::uint32_t, 2> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform}; const std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, BranchedLdsStep, 0, {}}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 32; - request.context.compute = ShaderComputeStageInfo{{threads, 1u, 1u}, 256u, {false, false, false}, false, 1u}; + request.context.compute = ShaderComputeStageInfo{{threads, 1u, 1u}, 512u, {false, false, false}, false, 1u}; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; @@ -61,15 +72,42 @@ Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { request.target.maxWorkgroupInvocations = 1024; request.target.maxWorkgroupSharedMemoryBytes = 49152; request.target.waveLdsMemoryBarriers = waveLdsMemoryBarriers; + request.target.deviceScopeLdsMemoryBarriers = deviceScopeLdsMemoryBarriers; request.layout = {0, 0, 0, 128}; request.useCache = false; - const auto result = Recompile(request); - const auto& words = result.spirv.Words(); + return Recompile(request); +} + +std::unordered_map<std::uint32_t, std::uint32_t> constantsOf(std::span<const std::uint32_t> words) { std::unordered_map<std::uint32_t, std::uint32_t> constants; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + } + return constants; +} + +MemoryBarriers memoryBarriers(std::span<const std::uint32_t> code, bool deviceScopeLdsMemoryBarriers) { + const auto result = recompileProgram(code, 128, false, deviceScopeLdsMemoryBarriers); + const auto& words = result.spirv.Words(); + const auto constants = constantsOf(words); + MemoryBarriers barriers; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast<spv::Op>(words[at] & 0xffffu) != spv::OpMemoryBarrier) continue; + const auto scope = constants.find(words[at + 1]); + require(scope != constants.end(), "a memory barrier scope is not a 32-bit constant"); + if (scope->second == spv::ScopeWorkgroup) ++barriers.workgroup; + if (scope->second == spv::ScopeDevice) ++barriers.device; + } + return barriers; +} + +Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { + const auto result = recompileProgram(BranchedLdsStep, threads, waveLdsMemoryBarriers, false); + const auto& words = result.spirv.Words(); + const auto constants = constantsOf(words); Barriers barriers; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast<spv::Op>(words[at] & 0xffffu); - if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; if (op != spv::OpControlBarrier && op != spv::OpMemoryBarrier) continue; const auto scope = constants.find(words[at + 1]); require(scope != constants.end(), "a barrier scope is not a 32-bit constant"); @@ -83,6 +121,17 @@ Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { void verifyDriverQuirk() { for (const auto& item : Cases) { require(AgcDriver::SubgroupControlBarrierUsable(item.driver) == item.usable, "driver " + std::to_string(static_cast<int>(item.driver)) + ": expected subgroup control barriers to be " + (item.usable ? "usable" : "unusable")); + require(AgcDriver::WorkgroupScopeFenceUsable(item.driver) == item.usable, "driver " + std::to_string(static_cast<int>(item.driver)) + ": expected workgroup-scope fences to be " + (item.usable ? "usable" : "unusable")); + } +} + +void verifyLdsMemoryBarrierScope() { + const std::array<std::pair<const char*, std::span<const std::uint32_t>>, 3> programs{{{"ds_write_b16", LdsHalfStore}, {"ds_add_u32", LdsAtomicAdd}, {"ds_max_f32", LdsAtomicMax}}}; + for (const auto& [name, code] : programs) { + const auto workgroup = memoryBarriers(code, false); + require(workgroup.workgroup == 1 && workgroup.device == 0, std::string(name) + " did not order its LDS update with one workgroup-scope OpMemoryBarrier"); + const auto device = memoryBarriers(code, true); + require(device.workgroup == 0 && device.device == 1, std::string(name) + " with device-scope LDS memory barriers did not order its LDS update with one device-scope OpMemoryBarrier"); } } @@ -101,6 +150,7 @@ int main() { try { verifyDriverQuirk(); verifyEmittedBarriers(); + verifyLdsMemoryBarrierScope(); std::puts("subgroup barrier tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp new file mode 100644 index 0000000000..ae9d55e2c0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp @@ -0,0 +1,92 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdint> +#include <cstdio> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 128; +constexpr std::uint32_t BranchThreads = 64; +constexpr std::uint32_t Iterations = 0x1000; +constexpr std::uint32_t Rounds = 16; +constexpr std::uint32_t LdsDwords = 512; +alignas(256) std::array<std::uint32_t, 2u * Threads> Output{}; + +alignas(256) constexpr std::array<std::uint32_t, 36> BranchCode{ + 0x34040082, 0xe0301000, 0x80000102, 0xbf8c3f70, 0xbe86037e, 0x7da800c0, 0xbf880002, 0xd87c0400, + 0x00000102, 0xbefe0306, 0x7e0e0500, 0xbf0ac007, 0x850580ff, 0x00001000, 0x7e0c0300, 0xbf068005, + 0xbf850005, 0xd5430006, 0x02050706, 0x80858105, 0xbf078005, 0xbf85fffb, 0xe0701200, 0x80000602, + 0xd8340000, 0x00000102, 0xbf8cc07f, 0xbf8a0000, 0x3a0604ff, 0x00000100, 0xd8d80000, 0x04000003, + 0xbf8cc07f, 0xe0701000, 0x80000402, 0xbf810000, +}; + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::uint32_t BranchResult(std::uint32_t thread) { + std::uint32_t value = thread; + for (std::uint32_t iteration = 0; iteration < Iterations; ++iteration) value = (value & 0xffffffu) * 3u + 1u; + return value; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->ComputeTarget(32); + if (target.subgroupSize != 32u) { + std::printf("skipped, wave32 programs do not run on 32-lane subgroups (%u lanes)\n", target.subgroupSize); + return VulkanTestSkipped; + } + std::vector<std::uint32_t> userData(4, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin()); + const std::span<const std::uint32_t> code(BranchCode); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + std::array<std::uint32_t, Threads> input{}; + for (std::uint32_t round = 0; round < Rounds; ++round) { + for (std::uint32_t thread = 0; thread < Threads; ++thread) input[thread] = (round + 1u) * 0x10000u + thread; + Output.fill(0xdeadbeefu); + std::copy(input.begin(), input.end(), Output.begin()); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device->WaitIdle(); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto expected = input[thread ^ BranchThreads]; + Require(Output[thread] == expected, "LDS exchange after a 16-bit LDS store in a branch that waves 0-1 take, round " + std::to_string(round) + ": thread " + std::to_string(thread) + " (wave " + std::to_string(thread / 32u) + ") read " + std::to_string(Output[thread]) + ", expected " + std::to_string(expected)); + } + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto expected = thread < BranchThreads ? BranchResult(thread) : thread; + Require(Output[Threads + thread] == expected, "loop result of thread " + std::to_string(thread) + " in round " + std::to_string(round)); + } + } + std::puts("LDS subword store branch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index e4eeec9174..7bad5361bb 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -49,6 +49,7 @@ bool g_graph = false; bool g_maintenance8 = false; bool g_robust2 = false; bool g_waveLdsMemoryBarriers = false; +bool g_deviceScopeLdsMemoryBarriers = false; bool g_code = false; bool Replay(const char* path) { @@ -149,6 +150,7 @@ bool Replay(const char* path) { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; if (g_robust2) request.request.target.robustBufferAccess2 = true; if (g_waveLdsMemoryBarriers) request.request.target.waveLdsMemoryBarriers = true; + if (g_deviceScopeLdsMemoryBarriers) request.request.target.deviceScopeLdsMemoryBarriers = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -186,7 +188,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set; shader_<address>.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] [--device-scope-lds-memory-barriers] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set; shader_<address>.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); return 2; } int failures = 0; @@ -215,6 +217,10 @@ int main(int argc, char** argv) { g_waveLdsMemoryBarriers = true; continue; } + if (std::string(argv[i]) == "--device-scope-lds-memory-barriers") { + g_deviceScopeLdsMemoryBarriers = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 10aaf73ff4..530663b309 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -300,6 +300,7 @@ private: append(key, value.fusedExactArithmetic); append(key, value.largePixelFunctionLdsBuffer); append(key, value.pixelFunctionStorageDwords); + append(key, value.deviceScopeLdsMemoryBarriers); } }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ef28af0ff8..6352afa4ee 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -642,6 +642,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) if (budget.bytes > limits.maxOutputMemoryBytes) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(budget.bytes) + " bytes of mesh output memory, the device allows " + std::to_string(limits.maxOutputMemoryBytes)); } targetOptions.waveLdsMemoryBarriers = request.target.waveLdsMemoryBarriers; + targetOptions.deviceScopeLdsMemoryBarriers = request.target.deviceScopeLdsMemoryBarriers; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 7b9cc5ed0f..b4b8e47fb2 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -301,6 +301,7 @@ struct SpirvTarget { bool fusedExactArithmetic = false; bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; + bool deviceScopeLdsMemoryBarriers = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 9a418524a3..146fb2318a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -41,6 +41,7 @@ struct SpirvTargetOptions { bool waveLdsMemoryBarriers = false; bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; + bool deviceScopeLdsMemoryBarriers = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 34213a41b6..1c0d45a569 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -112,6 +112,7 @@ void EmitMemoryOffsets(SpirvEmitterState& state); std::uint32_t LdsDwordCount(const SpirvEmitterState& state); bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); +std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); @@ -276,7 +277,8 @@ std::uint32_t AtomicUpdateTyped(SpirvEmitterState& state, std::uint32_t pointer, EmitLabel(state, cont); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - if (kind != ResourceKind::Lds || !state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); + const auto fenceScope = kind == ResourceKind::Lds ? LdsMemoryBarrierScope(state) : scope; + if (kind != ResourceKind::Lds || !state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, fenceScope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); return observed; } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 38b321e595..1b6d7c5297 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -129,6 +129,7 @@ struct SpirvEmitterState { std::uint32_t pixelFlatPositions = 0; std::map<std::pair<std::uint32_t, std::uint32_t>, std::uint32_t> solvedBarycentrics; bool waveLdsMemoryBarriers = false; + bool deviceScopeLdsMemoryBarriers = false; std::unordered_set<const IrValue*> sharedLaneValues; std::uint32_t laneHalf = 0; std::uint32_t spirvVersion = 0x00010300u; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 53a851625b..18e703456f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -13,6 +13,7 @@ void EmitMemoryOffsets(SpirvEmitterState& state); std::uint32_t LdsDwordCount(const SpirvEmitterState& state); bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); +std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8fd7e67442..96ddd6f796 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -280,6 +280,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.waveLdsMemoryBarriers = target.waveLdsMemoryBarriers; state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; + state.deviceScopeLdsMemoryBarriers = target.deviceScopeLdsMemoryBarriers; state.hostSubgroupSize = target.subgroupSize; if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > target.pixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 1a0ebafe6d..e2f95214ee 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -113,6 +113,10 @@ void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { state.ldsBufferBase = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, within, ConstantU32(state, FunctionLdsDwordLimit)), ConstantU32(state, 3u)); } +std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state) { + return state.deviceScopeLdsMemoryBarriers ? spv::ScopeDevice : spv::ScopeWorkgroup; +} + void EmitLdsBufferBase(SpirvEmitterState& state) { EmitPixelFunctionLdsBase(state); if (!LdsInDeviceMemory(state)) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index cd82fa2998..a6e4c11678 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -916,7 +916,7 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, LdsMemoryBarrierScope(state)), ConstantU32(state, semantics)); } else { EmitDeviceAtomicMemoryBarrier(state); } @@ -1037,10 +1037,7 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { const auto next = [&](std::uint32_t current) { return replacement(state, current); }; if (InFunctionStorage(state, mem.kind)) return UpdateFunctionElement(state, pointer, next); - const auto old = AtomicUpdate(state, pointer, mem.kind, next); - const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); - return old; + return AtomicUpdate(state, pointer, mem.kind, next); }); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index da248bd79d..9546fa4ac0 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -183,3 +183,4 @@ - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. - [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. - [Wave LDS ordering on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) one guest wave's LDS accesses are ordered by a subgroup-scope `OpMemoryBarrier` with no control barrier, relying on a 32-lane subgroup being one hardware wave whose lanes run in lockstep and issue LDS instructions in order, which Vulkan does not guarantee; for LDS kept in device memory the same is assumed of global loads and stores, unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the subgroup-scope `OpControlBarrier` to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus). Measured on that driver only ([LdsTreeReduction.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp)); a Radeon 860M on 32.0.13058.8001 reportedly did not show the fault. +- [LDS atomic fences on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) the fence after an LDS atomic or a sub-dword or misaligned LDS store is a device-scope `OpMemoryBarrier`, which that driver compiles to GL0 and GL1 cache invalidations; the cost is unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the workgroup-scope fence to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus with flash attention). Measured on that driver only ([LdsSubwordStoreBranch.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp)); a Radeon 860M (RDNA 3.5, 32.0.13058.8001) reportedly gave correct flash-attention output, but the gate covers every version. From d44c823834aa7f93eef3abf6f3ae0c22d2db1679 Mon Sep 17 00:00:00 2001 From: ntamas94 <ntamas@live.com> Date: Fri, 23 Oct 2026 20:46:58 +0200 Subject: [PATCH 1329/1360] feat(libSceJson2): implement sce::Json::Array::operator= Copy the source array into this and return this, as the console does (measured on a PS4 Pro, firmware 10.71). Knowledge is Power: Decades imports it; the inventory stub threw NotImplemented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceJson2/Export.cpp | 8 ++++++ .../libs/prx/libSceJson2/InventoryExports.cpp | 6 ---- core/libs/tests/GuestJson.cpp | 28 +++++++++++++++++++ docs/dev/debt/libSceJson2.md | 1 - 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index a84b073cd9..529fee64d0 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -737,6 +737,14 @@ int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String* self, const Strin void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } +Array* APS5_VABI _ZN3sce4Json5ArrayaSERKS1_(Array* self, const Array* other) { + if (self == other) return self; + Array copy{}; + CopyArray(copy, *other); + Destroy(*self); + *self = copy; + return self; +} void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Value* value) { Value copy{}; CopyInto(copy, *value); diff --git a/core/libs/prx/libSceJson2/InventoryExports.cpp b/core/libs/prx/libSceJson2/InventoryExports.cpp index 5c89743172..51220c9a00 100644 --- a/core/libs/prx/libSceJson2/InventoryExports.cpp +++ b/core/libs/prx/libSceJson2/InventoryExports.cpp @@ -167,12 +167,6 @@ int APS5_VABI sceJson2Unknown_027_nid_no_patch() { } APS5_EXPORT("_ZN3sce4Json5ArrayD2Ev", sceJson2Unknown_027_nid_no_patch); -int APS5_VABI sceJson2Unknown_028_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZN3sce4Json5ArrayaSERKS1_", sceJson2Unknown_028_nid_no_patch); - int APS5_VABI sceJson2Unknown_029_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index b2951558b6..9a577f0dd3 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -55,6 +55,7 @@ void APS5_VABI _ZN3sce4Json5Array5clearEv(Array*); const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const Iterator*); void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); +Array* APS5_VABI _ZN3sce4Json5ArrayaSERKS1_(Array*, const Array*); void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); Iterator* APS5_VABI _ZNK3sce4Json5Array3endEv(Iterator*, const Array*); @@ -294,6 +295,32 @@ static void NestingDepth() { _ZN3sce4Json5ValueD1Ev(&root); } +static void ArrayAssignment() { + Array source{}, target{}; + _ZN3sce4Json5ArrayC1Ev(&source); + _ZN3sce4Json5ArrayC1Ev(&target); + Value flag{}, real{}; + _ZN3sce4Json5ValueC1Eb(&flag, true); + _ZN3sce4Json5ValueC1Ed(&real, 1.5); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&source, &flag); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&source, &real); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&source, &flag); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&target, &real); + Require(_ZN3sce4Json5ArrayaSERKS1_(&target, &source) == &target); + Require(_ZNK3sce4Json5Array4sizeEv(&target) == 3); + Require(_ZNK3sce4Json5Array4sizeEv(&source) == 3); + _ZN3sce4Json5Array5clearEv(&source); + Require(_ZNK3sce4Json5Array4sizeEv(&target) == 3); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(&target))); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array4backEv(&target))); + Require(_ZN3sce4Json5ArrayaSERKS1_(&target, &target) == &target); + Require(_ZNK3sce4Json5Array4sizeEv(&target) == 3); + _ZN3sce4Json5ValueD1Ev(&flag); + _ZN3sce4Json5ValueD1Ev(&real); + _ZN3sce4Json5ArrayD1Ev(&source); + _ZN3sce4Json5ArrayD1Ev(&target); +} + static void ObjectsAndArrays() { Object object{}; _ZN3sce4Json6ObjectC1Ev(&object); @@ -1466,6 +1493,7 @@ int main(int argc, char** argv) { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); + ArrayAssignment(); ObjectFind(); ObjectPair(); ObjectInsert(); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index b9fac63328..527684f6a3 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -42,7 +42,6 @@ - [_ZN3sce4Json5ArrayC2ERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `6NlbpyIfUuY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5ArrayC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `t6AHhkWu4mY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5ArrayD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `MIyMPw7YTGM` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZN3sce4Json5ArrayaSERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `w5UbvPLGye0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5Value11referStringEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `m5J28iw-QPs` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5Value12referBooleanEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `QxjfcfBhEdc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5Value12referIntegerEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `R0ac5QOdlpo` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. From 19dd43f0d847c22e441d7a41dec265e7e0fb9126 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Sat, 24 Oct 2026 08:03:40 +1300 Subject: [PATCH 1330/1360] test(libScePad): drop the unimplemented-throw asserts added by #9995 The tests for scePadSetLotusEnableIrKey, scePadSetLotusDisableIrKey, scePadVirtualDeviceDisableButtonRemapping and scePadVirtualDeviceGetRemoteSetting only checked that an unimplemented call throws. A test of that kind fails as soon as someone implements the function, so it pins the absence of a feature rather than behaviour. Tests should cover real behaviour and argument validation. This removes those asserts, the Throws helper and the declarations they were the only users of. The rest of the pad test is unchanged. --- core/libs/tests/GuestPad.cpp | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e5573141da..35abd4888c 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -9,7 +9,6 @@ #include <cstring> #include <filesystem> #include <fstream> -#include <stdexcept> #include <string> #include <thread> #include <utility> @@ -42,24 +41,10 @@ int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); int APS5_VABI scePadSetMotionSensorState(int, bool); -int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t); -int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t); -int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool); -int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } -template<typename TCall> -static bool Throws(TCall call, const char* function) { - try { - static_cast<void>(call()); - } catch (const std::runtime_error& error) { - return std::strstr(error.what(), function) != nullptr; - } - return false; -} - static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -603,8 +588,6 @@ int main() { Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); - Require(Throws([&] { return scePadSetLotusEnableIrKey(handle); }, "scePadSetLotusEnableIrKey")); - Require(Throws([&] { return scePadSetLotusDisableIrKey(handle); }, "scePadSetLotusDisableIrKey")); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); @@ -614,15 +597,4 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); - Require(Throws([&] { return scePadSetLotusEnableIrKey(handle + 1); }, "scePadSetLotusEnableIrKey")); - Require(Throws([&] { return scePadSetLotusDisableIrKey(handle + 1); }, "scePadSetLotusDisableIrKey")); - Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(true); }, "scePadVirtualDeviceDisableButtonRemapping")); - Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(false); }, "scePadVirtualDeviceDisableButtonRemapping")); - std::array<std::uint8_t, 16> scratch{}; - scratch.fill(0xAB); - Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); - Require(Throws([] { return scePadVirtualDeviceGetRemoteSetting(1, nullptr); }, "scePadVirtualDeviceGetRemoteSetting")); - Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(0, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); - Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(-1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); - for (std::uint8_t byte : scratch) Require(byte == 0xAB); } From 0aef357591a6d656d813997ec3598900b14187d4 Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:17:33 +0200 Subject: [PATCH 1331/1360] fix(libkernel_sys): report ELOOP from mkdirat through a Windows symlink loop on a descriptor On Windows, mkdirat with a directory descriptor and a relative path that goes through a symbolic link pointing to itself reported EIO. CreateDirectoryW fails with ERROR_CANT_RESOLVE_FILENAME for such a path, and the error mapping had no case for it, so it fell through to GuestEio. FreeBSD fails the lookup with ELOOP. The path form of mkdir and openat with the same descriptor already return ELOOP. ERROR_CANT_RESOLVE_FILENAME now gives ELOOP. guest_kernel_sys gains a Windows check with a file-type and a directory-type self-loop: mkdirat through the loop returns ELOOP and mkdirat of the link itself still returns EEXIST. The check is skipped when the host denies symbolic link creation. --- core/libs/prx/libkernel_sys/Export.cpp | 1 + core/libs/tests/GuestKernelSys.cpp | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index e9546a3d34..6266663bf3 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -547,6 +547,7 @@ int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) if (code == ERROR_ALREADY_EXISTS || code == ERROR_FILE_EXISTS) return static_cast<int>(Fail(GuestEexist)); if (code == ERROR_ACCESS_DENIED) return static_cast<int>(Fail(GuestEperm)); if (code == ERROR_FILENAME_EXCED_RANGE) return static_cast<int>(Fail(GuestEnametoolong)); + if (code == ERROR_CANT_RESOLVE_FILENAME) return static_cast<int>(Fail(GuestEloop)); if (code == ERROR_PATH_NOT_FOUND) { if (HasNonDirectoryPrefix(native)) return static_cast<int>(Fail(GuestEnotdir)); return static_cast<int>(Fail(GuestEnoent)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a540b9b666..19da282cfc 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -677,6 +677,19 @@ int main() { const std::string longName(256, 'a'); Require(mkdirat_nid_postfix(base, longName.c_str(), 0700) == -1 && Error() == 63); Require(mkdirat_nid_postfix(base, "dir/../dotdot_dir", 0700) == 0 && std::filesystem::is_directory(root / "dotdot_dir")); +#ifdef _WIN32 + for (const DWORD linkFlags : {DWORD(0), DWORD(SYMBOLIC_LINK_FLAG_DIRECTORY)}) { + const auto loopLink = root / "mkdirat_loop"; + if (CreateSymbolicLinkW(loopLink.c_str(), L"mkdirat_loop", linkFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "symbolic links unavailable: the mkdirat loop case not tested\n"); + break; + } + Require(mkdirat_nid_postfix(base, "mkdirat_loop/child", 0700) == -1 && Error() == 62); + Require(mkdirat_nid_postfix(base, "mkdirat_loop", 0700) == -1 && Error() == 17); + Require((linkFlags != 0 ? RemoveDirectoryW(loopLink.c_str()) : DeleteFileW(loopLink.c_str())) != 0); + } +#endif Require(unlinkat_nid_postfix(plain, "file.txt", 0) == -1 && Error() == 20); Require(unlinkat_nid_postfix(-1, "file.txt", 0) == -1 && Error() == 9); Require(sceKernelClose(plain) == 0); From 80677584129eb0bd91383b5012be5e2b7247bafb Mon Sep 17 00:00:00 2001 From: Sam <jb006d6855@gmail.com> Date: Tue, 20 Oct 2026 16:51:20 +0100 Subject: [PATCH 1332/1360] fix(agc): recognise the copy kernel as Demon's Souls 01.007.000 compiles it The buffer copy HLE recognises its copy kernel (dst[i] = src[i % period] for i < count, count and period read from a parameter buffer) by a hash of its first 38 instruction words. Demon's Souls PPSA01341 01.007.000 ships the same kernel compiled to a different instruction sequence, with the same descriptors and parameter layout, so every one of its copies ran as a GPU dispatch and later CPU reads of the copied bytes waited for it. That hash is now accepted too. The new agc_driver_period_copy test runs that kernel through the driver: with the HLE the linear cases must take a copy path, a period below the count and a launch larger than the count needs must not, and every case must produce dst[i] = src[i % period]. agc_driver_period_copy_no_hle runs the same cases as shader dispatches (APS5_NO_COPY_HLE=1), so both paths are checked against the same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../src/Driver/Memory/BufferCopy.cpp | 4 +- .../tests/execution/PeriodCopy.cpp | 116 ++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00..c9ada84c07 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2454,6 +2454,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_driver_period_copy_no_hle COMMAND agc_driver_period_copy_tests) + set_tests_properties(agc_driver_period_copy_no_hle PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_COPY_HLE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_period_copy_no_hle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable) set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index 10d28032c9..9e8ca4fe94 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -18,14 +18,14 @@ bool Driver::matchesCopyKernel(std::span<const std::uint32_t> code, const std::v static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; if (!enabled || userData.size() != 12 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; static constexpr std::size_t copyKernelWords = 38; - static constexpr std::uint64_t copyKernelHash = 0x6ec00fe8aa95f99aull; + static constexpr std::array<std::uint64_t, 2> copyKernelHashes{0x6ec00fe8aa95f99aull, 0xc771c42e4d069d62ull}; if (code.size() < copyKernelWords || code[0] != 0xbfa00002u || code[copyKernelWords - 1] != 0xbf810000u) return false; std::uint64_t hash = 0xcbf29ce484222325ull; for (const auto word : code.first(copyKernelWords)) { hash ^= word; hash *= 0x100000001b3ull; } - if (hash != copyKernelHash) return false; + if (std::find(copyKernelHashes.begin(), copyKernelHashes.end(), hash) == copyKernelHashes.end()) return false; const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; return userData[3] == 0x00014004u && userData[7] == 0x00014004u && userData[11] == 0x0004dfacu && stride(1) == 4 && stride(5) == 4 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[10] >= 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp new file mode 100644 index 0000000000..0c4fecdb35 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp @@ -0,0 +1,116 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include <array> +#include <cstdio> +#include <cstdlib> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array<std::uint32_t, 38> Code{ + 0xbfa00002u, 0xd7460000u, 0x04010c0cu, 0xf4201a84u, 0xfa000000u, 0xbf8cc07fu, 0x7da8006au, 0xbf88001du, + 0xf4200204u, 0xfa000004u, 0xbf8cc07fu, 0x7e020c08u, 0x81ea0880u, 0x7e025701u, 0x100202ffu, 0x4f7ffffeu, + 0x7e020f01u, 0x7ed60501u, 0x936a6b6au, 0x9aea6b6au, 0x4a02026au, 0xd56a0001u, 0x00020101u, 0xd5690001u, + 0x00020208u, 0x4c020300u, 0x7d860208u, 0x4e040208u, 0x02020501u, 0x7d860208u, 0x4e040208u, 0x02020501u, + 0xe0002000u, 0x80000101u, 0xbf8c3f70u, 0xe0102000u, 0x80010100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t ElementFormat = 0x00014004u; +constexpr std::uint32_t RecordFormat = 0x0004dfacu; +constexpr std::uint32_t Elements = 8192; +alignas(256) std::array<std::uint32_t, Elements> Source{}; +alignas(256) std::array<std::uint32_t, Elements> Destination{}; +alignas(256) std::array<std::uint32_t, 4> Record{}; + +void AppendDescriptor(std::vector<std::uint32_t>& data, const void* base, std::uint32_t stride, std::uint32_t records, std::uint32_t format) { + const auto address = reinterpret_cast<std::uintptr_t>(base); + data.insert(data.end(), {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), records, format}); +} + +std::vector<std::uint32_t> Commands(std::uint32_t groups) { + std::vector<std::uint32_t> data; + AppendDescriptor(data, Source.data(), 4, Elements, ElementFormat); + AppendDescriptor(data, Destination.data(), 4, Elements, ElementFormat); + AppendDescriptor(data, Record.data(), 16, 1, RecordFormat); + std::vector<std::uint32_t> words; + const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) { + words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array<std::uint32_t, 3> shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast<std::uintptr_t>(Code.data()); + const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)}; + registers(0x20c, program); + const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +void ExecuteTests() { + struct Case { + const char* name; + std::uint32_t count, period, groups; + bool hle; + }; + constexpr std::array<Case, 5> cases{{ + {"one full group", 64, 64, 1, true}, + {"a period above the count", 1000, 4096, 16, true}, + {"a partial last group", 100, 100, 2, true}, + {"a period below the count", 300, 7, 5, false}, + {"more groups than the count needs", 64, 64, 2, false}, + }}; + const bool hleEnabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + for (const auto& item : cases) { + const bool hle = item.hle && hleEnabled; + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast<std::uint32_t>(i) * 2654435761u + 1u; + Destination.fill(Untouched); + Record = {item.count, item.period, 0, 0}; + const auto taken = Taken(); + const auto words = Commands(item.groups); + Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Destination.data()), sizeof(Destination)); + const std::string where = std::string(" (") + item.name + ")"; + Require(Taken() - taken == (hle ? 1u : 0u), (hle ? "the copy did not take the HLE path" : "the copy took the HLE path") + where); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const auto expected = i < item.count ? Source[i % item.period] : Untouched; + Require(Destination[i] == expected, "copy mismatch at element " + std::to_string(i) + where + ": got " + std::to_string(Destination[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("period copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 548267bfa3d6b7b866d969b2eab1416d3bf3517b Mon Sep 17 00:00:00 2001 From: SuarezPM <SuarezPM@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:37:25 -0300 Subject: [PATCH 1333/1360] feat(shader): support 32-bit image atomics through keyed image tables Table T#s keyed at runtime no longer fault on image_atomic_*32: the table operation stays supported, elements carry the atomic flag through dedup into the descriptor binding, and the table path binds AtomicView like the direct path. 64-bit atomics stay unsupported, packed and BY formats stay unsupported, and the debt entry records the relaxed Device-scoped emission and the remaining gaps. --- .../Graphics/src/ShaderResources.cpp | 4 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 21 ++++++-- .../tests/execution/ImageTable.cpp | 52 +++++++++++++++++++ .../Optimization/ResourceMaterializer.hpp | 1 + .../src/DescriptorBindingBuilder.cpp | 2 +- .../Optimization/src/ResourceMaterializer.cpp | 6 +-- .../Optimization/src/ResourceTracker.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 8 files changed, 77 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 231d471add..a43a347d4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2435,7 +2435,7 @@ void ShaderResources::buildComplete() { for (auto element = first; element < last; ++element) { const auto index = binding.imageAllocations[element]; if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); - else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); @@ -5211,7 +5211,7 @@ void ShaderResources::resolveImageTables() { storageKeys.push_back(resource.dccAddress); storageFirstLayer.push_back(firstLayer); storageWritten.push_back(true); - storageAtomic.push_back(false); + storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); storageAtomic64.push_back(false); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress, true}); bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 18e07d3fcd..df69092486 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1339,6 +1339,7 @@ constexpr std::uint32_t TableFormat8888UNorm = 56; constexpr std::uint32_t TableFormat32UInt = 20; constexpr std::uint32_t TableFormat16SInt = 12; constexpr std::uint32_t TableFormat11_11_10UInt = 34; +constexpr std::uint32_t TableFormat32_32UInt = 62; constexpr std::uint32_t TableType2D = 9; constexpr std::uint32_t TableType3D = 10; @@ -2964,7 +2965,17 @@ void verifyImageTableRegistration() { const auto* storeTable = TableBinding(*store.result, DescriptorRole::ImageStorageTable); require((storeFault & Abi::PoisonFlag) == 0u && storeTable != nullptr && storeTable->count != 0u && std::all_of(storeTable->imageWritten.begin(), storeTable->imageWritten.end(), [](bool written) { return written; }), "image table registration: the store's table was not bound as written storage elements"); require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: a store-only table bound sampled elements"); - const std::array<std::pair<std::uint32_t, const char*>, 3> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}, {0xf0440108u, "image_atomic_add"}}}; + auto atomicCode = storeCode; + atomicCode[8] = 0xf0440108u; + const auto atomic = CompileTable(TableRequest(atomicCode, userData)); + const auto& atomicPlan = *atomic.capture->plan; + require(atomicPlan.info.images.size() == 1u && atomicPlan.info.images[0].table == 0u && atomicPlan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_atomic_add was not a supported table operation"); + const auto& atomicMap = TableMap(*atomic.result); + const auto atomicFault = atomicMap[Abi::TableHeader(0) + Abi::TableFault]; + const auto* atomicTable = TableBinding(*atomic.result, DescriptorRole::ImageStorageTable); + require((atomicFault & Abi::PoisonFlag) == 0u && atomicTable != nullptr && atomicTable->count != 0u && atomicTable->imageAtomic.size() == atomicTable->count && std::all_of(atomicTable->imageWritten.begin(), atomicTable->imageWritten.end(), [](bool written) { return written; }) && std::all_of(atomicTable->imageAtomic.begin(), atomicTable->imageAtomic.end(), [](bool atomic) { return atomic; }), "image table registration: the image_atomic_add table was not bound as atomic storage elements"); + require(TableBinding(*atomic.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*atomic.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an atomic-only table bound sampled elements"); + const std::array<std::pair<std::uint32_t, const char*>, 2> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}}}; for (const auto& [instruction, name] : unsupportedStores) { auto code = storeCode; code[8] = instruction; @@ -3370,7 +3381,7 @@ void verifySelectedRecordDirectOperations() { FillTableSrt(guest, heap.data(), 32u, 4u); guest.srt[16] = 2u; const auto userData = TableUserData(guest); - const std::array<std::tuple<const char*, std::uint32_t, std::uint32_t>, 4> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}}; + const std::array<std::tuple<const char*, std::uint32_t, std::uint32_t>, 5> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_atomic64_add", 0xf0440308u, TableFormat32_32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}}; for (const auto& [name, word0, format] : operations) { const auto what = std::string("selected record direct operations: ") + name; heap[2] = TableT(textures[1].bytes.data(), format, TableType2D); @@ -3378,11 +3389,11 @@ void verifySelectedRecordDirectOperations() { const auto request = TableRequest(code, userData); const auto plan = GetResourcePlan(request); require(plan->info.images.size() == 1u && plan->info.images[0].table == NoTable && !plan->descriptorSources.at(plan->info.images[0].source).tableColumn.has_value(), (what + " through a T# whose record the CPU evaluates was planned through an image table").c_str()); - const auto compiled = CompileTable(request); - require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); + const auto compiled = word0 == 0xf0440308u ? TableCompiled{} : CompileTable(request); + require(word0 == 0xf0440308u || (TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2]))), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); - require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u || word0 == 0xf0440108u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); } const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d353e82b93..d5a89a1586 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -203,6 +203,22 @@ alignas(256) constexpr std::array<std::uint32_t, 16> StoreLaneTableCode{ 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0208f08u, 0x00090401u, 0xbf810000u, }; +alignas(256) constexpr std::array<std::uint32_t, 16> AtomicAddLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0448108u, 0x00090401u, 0xbf810000u, +}; + +alignas(256) constexpr std::array<std::uint32_t, 16> AtomicSwapLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf03c8108u, 0x00090401u, 0xbf810000u, +}; + +alignas(256) constexpr std::array<std::uint32_t, 18> AtomicCmpSwapLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0x7e0a0304u, 0x3a080881u, 0xf0408308u, + 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array<std::uint32_t, 17> StoreTableCutCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7da400ffu, 0x00001000u, 0xf0208f08u, 0x00090401u, @@ -1117,6 +1133,41 @@ void RunStoreTests(AgcDriver::VulkanDevice& device) { Require(untouched(), "image table store: a store through a null entry reached an image:\n" + null.log); } +void RunAtomicTests(AgcDriver::VulkanDevice& device) { + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registeredUint(UintTexels.data(), sizeof(UintTexels)), registeredFloat(FloatTexels.data(), sizeof(FloatTexels)), registeredSint(SintTexels.data(), sizeof(SintTexels)); + SetClassRecords(); + for (std::uint32_t x = 0; x < Width; ++x) UintTexels[x] = 0x5000u + x; + for (std::uint32_t x = 0; x < Width; ++x) FloatTexels[x] = 1.0f; + Keys = {}; + Keys[0] = 1; + const auto added = Run(device, AtomicAddLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(added), "image table atomic: an add through a table faulted:\n" + added.log); + Require(added.imageTableCount != 0u, "image table atomic: the add bound no storage table elements"); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == 0x5000u + x + x, "image table atomic: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(0x5000u + x + x)); + const auto swapped = Run(device, AtomicSwapLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(swapped), "image table atomic: an exchange through a table faulted:\n" + swapped.log); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == x, "image table atomic: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x)); + const auto compared = Run(device, AtomicCmpSwapLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(compared), "image table atomic: a compare exchange through a table faulted:\n" + compared.log); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == (x ^ 1u), "image table atomic: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x ^ 1u)); + Keys[0] = 8; + const auto addedFloat = Run(device, AtomicAddLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(addedFloat), "image table atomic: an add on a float record through a table faulted:\n" + addedFloat.log); + for (std::uint32_t x = 0; x < Width; ++x) Require(std::bit_cast<std::uint32_t>(FloatTexels[x]) == 0x3f800000u + x, "image table atomic: float texel " + std::to_string(x) + " is " + Hex(std::bit_cast<std::uint32_t>(FloatTexels[x])) + ", expected " + Hex(0x3f800000u + x)); + Keys[0] = 6; + const auto null = Run(device, AtomicAddLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == (x ^ 1u) && std::bit_cast<std::uint32_t>(FloatTexels[x]) == 0x3f800000u + x, "image table atomic: an add through a null entry reached an image:\n" + null.log); +} + void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array<std::uint32_t, Width * Height> storeTexels{}; struct Registration { @@ -1509,6 +1560,7 @@ int main() { RunSelectedRecordTests(*device); RunModeTests(*device); RunStoreTests(*device); + RunAtomicTests(*device); RunSelectedStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 1425a5ec58..ae3696883c 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -20,6 +20,7 @@ struct ImageTableElement { bool depthCompare = false; DescriptorBindingKind kind = DescriptorBindingKind::Buffers; std::uint32_t samplers = 0; + bool atomic = false; }; struct ImageTableSamplerElement { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 51b8c966e9..b3c679dc2e 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -613,7 +613,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageShapes.push_back(ImageShapeForResource(shape)); physical.imageDepthCompare.push_back(element.depthCompare); physical.imageWritten.push_back(ImageBindingResourceClass(element.kind) == ImageResourceClass::Storage); - physical.imageAtomic.push_back(false); + physical.imageAtomic.push_back(element.atomic); std::uint32_t samplers = 0; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { if ((element.samplers & (1u << sampler)) != 0u) samplers |= entry.tableSamplerMasks[sampler]; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 1b15e694b4..8e03103c14 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1713,17 +1713,17 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i auto element = words.dwords; if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; const auto kind = DescriptorBindingForImage(mode); - const auto hash = hashWords(element, (static_cast<std::uint64_t>(mode.dimension) << 16u) ^ (static_cast<std::uint64_t>(mode.depthCompare) << 8u) ^ static_cast<std::uint64_t>(kind)); + const auto hash = hashWords(element, (static_cast<std::uint64_t>(image.atomic) << 24u) ^ (static_cast<std::uint64_t>(mode.dimension) << 16u) ^ (static_cast<std::uint64_t>(mode.depthCompare) << 8u) ^ static_cast<std::uint64_t>(kind)); auto index = elementIndex.Find(hash, [&](std::uint32_t known) { const auto& entry = out.elements[known]; - return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind; + return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind && entry.atomic == image.atomic; }); if (!index.has_value() && out.elements.size() >= Abi::ImageElementBudget) { record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { if (!index.has_value()) { index = static_cast<std::uint32_t>(out.elements.size()); - out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u}); + out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u, image.atomic}); elementIndex.Insert(hash, *index); } out.elements[*index].samplers |= directSamplers[resource]; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 0ffeb3be74..48db089c1c 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1707,7 +1707,7 @@ private: continue; } image.table = next++; - if (image.atomic || image.packed || image.byElements != 0u) { + if (image.atomic64 || image.packed || image.byElements != 0u) { image.tableOperation = TableOperation::Unsupported; } } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index cd42d5cc9f..6d217ba708 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -50,7 +50,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). -- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. +- `image_store_mip`, `image_store_pck`, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. 32-bit image atomics through a table lower to `OpAtomic*` on the table `AtomicView` (`Device` scope, relaxed, `AcquireRelease`/`UniformMemory` barrier, as the direct path) instead of faulting; 64-bit atomics still fault (no `Atomic64View` on the table path). - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. - Comparison sampling of a color texture through an [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# throws at the first dispatch or draw that binds it. From 0ef31bad752dff3e48dc61da827b5e4e03e29ce1 Mon Sep 17 00:00:00 2001 From: SuarezPM <SuarezPM@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:34:21 -0300 Subject: [PATCH 1334/1360] fix(shader): emit type-correct zero into table merge phis Table accessors returning scalar uint fed the merge Phi a 4-wide zero, failing SPIR-V validation with SPIRV_TOOLS on. Use ZeroResult like the unsupported path. --- core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index e5c011f423..07ad9585b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -2041,7 +2041,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.tableAccess = {}; if (tables) { EmitLabel(state, invalid); - if (returnsValue) incoming.insert(incoming.end(), {ConstantU32CompositeZero(state, 4u), invalid}); + if (returnsValue) incoming.insert(incoming.end(), {ZeroResult(ctx, inst), invalid}); state.module.AddFunction(spv::OpBranch, merge); } else if (exits) { EmitLabel(state, invalid); @@ -2058,7 +2058,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitLabel(state, guardSkip); state.module.AddFunction(spv::OpBranch, guardMerge); EmitLabel(state, guardMerge); - if (returnsValue) state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), guardedId, resultId, merge, ConstantU32CompositeZero(state, 4u), guardSkip); + if (returnsValue) state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), guardedId, resultId, merge, ZeroResult(ctx, inst), guardSkip); } if (returnsValue) ctx.definitions.emplace(&inst, guardedId); } From 31aaf42381d3fe6a53713e380f9c765d8cf1547d Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:35:16 +0200 Subject: [PATCH 1335/1360] docs(debt): restore the Packed 16-bit textures entry from main The merge resolution had dropped its storage image sentences, which main keeps and this branch does not change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 34bbce8155..05d99dec9e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -202,7 +202,7 @@ - [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices, and so does a `CB_COLOR_VIEW` slice range on a 2D array target through the array's resident image, or on a surface of one slice as an array of the view's last slice + 1 slices, one slice pitch apart: a layered draw into an array without its resident image, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. -- [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. +- [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. Storage images use compatible R16_UINT views with the [buffer UNORM store conversion](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp); quantization uses nearest even and maps NaN to zero. Rounding ties and NaN handling for packed stores have not been checked on AMD or PS5 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 a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. - [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. From 9796a23046bd3306290da6d3f0cf9d5b9e38d178 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:18:22 +0000 Subject: [PATCH 1336/1360] fix(shader): copy only what the SRT evaluator reads of plan branch conditions ExtractPlan copied the whole argument closure of every control-flow condition into the plan. The draw-time walk (SrtDescriptorEvaluation) uses a condition only when the evaluator computes it, and an operation outside the evaluator's set ends the evaluation without reading its arguments. For conditions on vector loads, image results or values carried around loops the copy reached most of the program, and none of it was ever read. Detail::Evaluator::Reads names what the evaluator reads of a value: nothing (an operation it does not evaluate, an immediate), its arguments (the operations it evaluates, identities, phis, and the pairs CompositeExtract reads through), or its whole closure (a raw read, whose handle LoadedFromMemory walks). EvaluateInst returns early for a value it reads nothing of, so the two cannot drift apart. ownPlanValues still copies descriptor sources, SRT reads and fill values whole, and copies a condition only as far as Reads goes: an operation it does not read is copied without its arguments. The pure and guarded flat slots are computed before the copy, from the same values as before. Evidence: a title that registers about 5,700 shaders at load held 24.1 M copied IR values in its plans (about 5.4 GiB by value size), of which 2.1 M were reachable from descriptor sources, SRT reads and fills (counted by a local report over the recompiler's source map). Tested: agc_shader_memory gains verifyPlanConditionPruning: a condition on a load with a 64-addition address keeps the load as a leaf and none of the additions, and conditions read through an extracted pair and through a raw read still evaluate. It fails without the change. Full ctest passes. Resolved onto main: Reads is a static member of BasicEvaluator next to MayEvaluate, and its operation list is main's evaluator's (evaluatedArguments adds BitCount32, FindILsb32 and FindUMsb32; the undefined values, which main never evaluates, read nothing). The EvaluateInst guard reads the node's value, so it covers the compact plans too. ownPlanValues now runs after the flat slots and before the compact plan, and gives a value it keeps without arguments an empty operand list, so the leaf drops the operand capacity main's IrValue constructor reserves. Main's MayEvaluate already drops the condition on the vector load, so the test checks that it is gone. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 76 +++++++++++++++++++ .../Optimization/SrtWalker/SrtEvaluator.hpp | 7 ++ .../Optimization/src/ResourceMaterializer.cpp | 35 ++++++--- .../src/SrtWalker/SrtEvaluator.cpp | 29 +++++++ 4 files changed, 135 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 18e07d3fcd..10701d0bb6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -567,6 +567,81 @@ void verifyRegisterSources() { require(!EquivalentValue(plan, &samplerRegister, &firstVector), "different register types were merged"); } +void verifyPlanConditionPruning() { + using namespace ShaderRecompiler; + std::vector<std::unique_ptr<IrValue>> values; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique<IrValue>(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto equal = [&](IrValue& left, IrValue& right) -> IrValue& { + auto& compare = make(IrOpcode::IEqual32, IrType::Bool); + compare.AddArgument(&left); + compare.AddArgument(&right); + return compare; + }; + constexpr std::uint32_t Additions = 64; + IrValue* address = &userData(0); + for (std::uint32_t index = 0; index < Additions; ++index) { + auto& sum = make(IrOpcode::IAdd32, IrType::U32); + sum.AddArgument(address); + sum.AddArgument(&constant(4)); + address = ∑ + } + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(address); + auto& pair = make(IrOpcode::CompositeConstructU32x2, IrType::U32x2); + pair.AddArgument(&userData(1)); + pair.AddArgument(&load); + auto& low = make(IrOpcode::CompositeExtractU32x2, IrType::U32); + low.AddArgument(&pair); + low.AddArgument(&constant(0)); + static std::uint32_t word = 5; + const auto wordAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&word)); + auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64); + handle.AddArgument(&userData(2)); + handle.AddArgument(&userData(3)); + auto& read = make(IrOpcode::LoadAddressU32, IrType::U32); + read.SetFlags(MemoryFlags{0, 0}); + read.AddArgument(&handle); + read.AddArgument(&constant(0)); + IrProgram program; + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + resources.controlFlow.push_back({&equal(load, constant(0)), {1, 2}, {}, {}}); + resources.controlFlow.push_back({&equal(low, constant(7)), {3, 4}, {}, {}}); + resources.controlFlow.push_back({&equal(read, constant(5)), {5, 6}, {}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + const auto copied = std::ranges::count_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::IAdd32; }); + require(copied == 0, "plan pruning: the address computation under a load the evaluator cannot read was copied"); + const auto copiedLoad = std::ranges::find_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::LoadBufferU32; }); + require(copiedLoad != plan.valueStorage.end() && copiedLoad->ArgumentCount() == 0u, "plan pruning: the unevaluable load was not kept as a leaf"); + const std::array<std::uint32_t, 4> data{0x100u, 7u, static_cast<std::uint32_t>(wordAddress), static_cast<std::uint32_t>(wordAddress >> 32u)}; + SrtRuntime runtime{data}; + Detail::Evaluator evaluator(plan, runtime); + std::uint32_t result = 0; + require(plan.controlFlow[0].condition == nullptr, "plan pruning: a condition on a vector load was kept"); + require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[1].condition), result) && result == 1u, "plan pruning: a condition read through an extracted pair was not evaluated"); + require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[2].condition), result) && result == 1u, "plan pruning: a condition on a raw read lost its handle"); +} + void verifySignedMinimum() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -8054,6 +8129,7 @@ int main(int argc, char** argv) { verifySignedMinimum(); verifyMayEvaluate(); verifyPureFlatSlots(); + verifyPlanConditionPruning(); verifyGuardedDescriptorSources(); verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 2fa129cb49..776a3c6dd9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -74,6 +74,12 @@ struct InaccessibleRead { std::uint64_t address = 0; }; +enum class EvaluatorReads : std::uint8_t { + Nothing, + Arguments, + Closure, +}; + class DenseValues { public: struct Slot { @@ -174,6 +180,7 @@ public: static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); static bool MayDecide(const IrResourcePlan& program, IrValue* value); + static EvaluatorReads Reads(const IrResourcePlan& program, const IrValue& value); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 1b15e694b4..e2cc0cca5b 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -25,6 +25,7 @@ #include <optional> #include <span> #include <unordered_map> +#include <unordered_set> #include <stdexcept> #include <string> #include <utility> @@ -2204,22 +2205,31 @@ void ownPlanValues(IrResourcePlan& plan) { for (auto& dword : source.dwords) roots.push_back(&dword); } for (auto& read : plan.srtReads) roots.push_back(&read.value); - for (auto& block : plan.controlFlow) roots.push_back(&block.condition); for (auto& value : plan.uniformFill.values) roots.push_back(&value); + const auto closures = roots.size(); + for (auto& block : plan.controlFlow) roots.push_back(&block.condition); std::unordered_map<const IrValue*, IrValue*> clones; + std::unordered_set<const IrValue*> closed; + std::unordered_set<const IrValue*> opened; std::vector<const IrValue*> order; - std::vector<const IrValue*> pending; - for (const auto* root : roots) { - if (*root != nullptr) pending.push_back(*root); + std::vector<std::pair<const IrValue*, bool>> pending; + for (std::size_t index = 0; index < roots.size(); ++index) { + if (*roots[index] != nullptr) pending.emplace_back(*roots[index], index < closures); } while (!pending.empty()) { - const auto* value = pending.back(); + const auto [value, closure] = pending.back(); pending.pop_back(); - if (!clones.emplace(value, nullptr).second) continue; - order.push_back(value); + if (clones.emplace(value, nullptr).second) order.push_back(value); + const auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); + if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; + if (reads == Detail::EvaluatorReads::Closure) { + closed.insert(value); + } else if (!opened.insert(value).second) { + continue; + } for (const auto* argument : value->Arguments()) { - if (argument != nullptr) pending.push_back(argument); + if (argument != nullptr) pending.emplace_back(argument, reads == Detail::EvaluatorReads::Closure); } } plan.valueStorage.Reserve(order.size()); @@ -2240,11 +2250,12 @@ void ownPlanValues(IrResourcePlan& plan) { return clone; }; for (const auto* value : order) { + const auto count = closed.contains(value) || opened.contains(value) ? value->ArgumentCount() : 0u; std::vector<IrValue*> arguments; std::vector<IrBlock*> phiBlocks; - arguments.reserve(value->ArgumentCount()); - if (value->IsPhi()) phiBlocks.reserve(value->ArgumentCount()); - for (std::size_t index = 0; index < value->ArgumentCount(); index++) { + arguments.reserve(count); + if (value->IsPhi()) phiBlocks.reserve(count); + for (std::size_t index = 0; index < count; index++) { const auto* argument = value->Argument(index); arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); @@ -2336,7 +2347,6 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const for (auto& block : plan.controlFlow) { if (block.condition != nullptr && !Detail::Evaluator::MayDecide(source, block.condition)) block.condition = nullptr; } - ownPlanValues(plan); const auto addSource = [&plan](std::uint32_t index) { if (index >= plan.descriptorSources.size()) { throw std::runtime_error("ResourceMaterializer::ExtractPlan resource references an unknown descriptor source"); @@ -2363,6 +2373,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const } plan.pureFlatSlots = Detail::ComputePureFlatSlots(plan); plan.guardedSrtSlots = Detail::ComputeGuardedFlatSlots(plan); + ownPlanValues(plan); buildCompactPlan(plan); return plan; } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 1412970d44..7b8536caac 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -463,8 +463,37 @@ bool BasicEvaluator<TNode>::MayDecide(const IrResourcePlan& program, IrValue* va } } +template<typename TNode> +EvaluatorReads BasicEvaluator<TNode>::Reads(const IrResourcePlan& program, const IrValue& value) { + if (value.IsIdentity()) { + return EvaluatorReads::Arguments; + } + if (value.HasImmediate()) { + return EvaluatorReads::Nothing; + } + switch (value.Opcode()) { + case IrOpcode::LoadAddressU32: + case IrOpcode::ReadConstBuffer: return IsRawRead(program, value) ? EvaluatorReads::Closure : EvaluatorReads::Nothing; + case IrOpcode::CompositeConstructU32x2: + case IrOpcode::IAddCarry32: + case IrOpcode::GetUserData: + case IrOpcode::GetShaderBase: + case IrOpcode::Phi: + case IrOpcode::ReadFirstLane: + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: + case IrOpcode::ReadConst: return EvaluatorReads::Arguments; + default: return evaluatedArguments(value.Opcode()) ? EvaluatorReads::Arguments : EvaluatorReads::Nothing; + } +} + template<typename TNode> bool BasicEvaluator<TNode>::EvaluateInst(const TNode& inst, std::uint64_t& result) { + if (Reads(_program, *inst.Value()) == EvaluatorReads::Nothing) { + return false; + } std::uint64_t a = 0; std::uint64_t b = 0; std::uint64_t c = 0; From 402f891787d79783b26ade2a43b053450419a495 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:13:24 +0000 Subject: [PATCH 1337/1360] fix(shader): keep only what a resource plan's conditions can evaluate A resource plan owns copies of the IR values its run-time evaluation reads: the descriptor sources, the SRT reads, the uniform fills, and the branch conditions that tell which images a dispatch can reach. Below a scalar load from memory, the copy took every value the load depends on, whether the evaluator can compute it or not. A condition that reads memory after vector work (a load predicated by a vector compare, an address computed from vector data) brought the program's whole vector graph into the plan: in a title's programs 2.4 million of 2.6 million retained values, about 500 MiB, belonged to twelve or so conditions per program. Below a condition, the copy now follows only what the evaluator reads: the arguments of values it can compute, and for a load from memory the words of its descriptor and its offset; a value the evaluator cannot compute is kept without its arguments, which is what evaluating it needs (it does not evaluate). Descriptor sources keep their whole closure, which the device image pre-pass walks. Tested with a new test (agc_driver_resource_plan_retention): the plan of a program whose branch condition reads a scalar load at an address taken from vector data now keeps the same number of values for vector chains of 1, 16 and 64 steps before the load (84, 114 and 210 before); the device table, written image, buffer descriptor, memory and SRT range and shader disk cache tests pass; in a title's corpus the plans of 314 programs hold 40 thousand values instead of 837 thousand; the corpus recompile with SPIR-V validation has no invalid module. Resolved onto main: the retention test runs on DeviceTableFixture.hpp, which main does not have, so the test and its CMake line are left out here; the next commits test this on main. --- .../Optimization/src/ResourceMaterializer.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index e2cc0cca5b..3e50e96a8d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -2223,13 +2223,21 @@ void ownPlanValues(IrResourcePlan& plan) { if (clones.emplace(value, nullptr).second) order.push_back(value); const auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; - if (reads == Detail::EvaluatorReads::Closure) { + if (closure) { closed.insert(value); } else if (!opened.insert(value).second) { continue; } for (const auto* argument : value->Arguments()) { - if (argument != nullptr) pending.emplace_back(argument, reads == Detail::EvaluatorReads::Closure); + if (argument != nullptr) pending.emplace_back(argument, closure); + } + if (!closure && reads == Detail::EvaluatorReads::Closure && value->ArgumentCount() != 0u && value->Argument(0) != nullptr) { + const auto* handle = value->Argument(0)->Resolve(); + if (opened.insert(handle).second) { + for (const auto* argument : handle->Arguments()) { + if (argument != nullptr) pending.emplace_back(argument, false); + } + } } } plan.valueStorage.Reserve(order.size()); From d257635baee3d123112fce2430520a70fd4d3a8b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:17:01 +0000 Subject: [PATCH 1338/1360] test(shader): check what a resource plan keeps of its branch conditions The plan of a hand-built program keeps exactly the 40 values its two branch conditions and two descriptor sources read: none of the 64 additions under the vector load that the conditions reach only through operations the evaluator does not read, which the whole copy kept (170 values). Its walk decides both conditions and resolves both sources as the program's own walk does. The plan of a compute program whose branch condition reads a scalar load at an address taken from vector data keeps the same number of values for vector chains of 1, 16 and 64 steps before the load. This is fable-void's agc_driver_resource_plan_retention from cca1f5a8d, which runs on his DeviceTableFixture; here the program is built from raw words and read through GetResourcePlan. The pruning case also takes the checks of his cb13fecfe: a condition on a select of a vector load is dropped (main's MayEvaluate drops it), the readable conditions stay, and no plan value keeps use lists or spare operand capacity. Co-authored-by: fablevoid <340827264+fable-void@users.noreply.github.com> --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 10701d0bb6..27bca762d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -628,20 +628,143 @@ void verifyPlanConditionPruning() { resources.controlFlow.push_back({&equal(load, constant(0)), {1, 2}, {}, {}}); resources.controlFlow.push_back({&equal(low, constant(7)), {3, 4}, {}, {}}); resources.controlFlow.push_back({&equal(read, constant(5)), {5, 6}, {}, {}}); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&equal(userData(4), constant(0))); + choice.AddArgument(&load); + choice.AddArgument(&constant(1)); + resources.controlFlow.push_back({&equal(choice, constant(1)), {7, 8}, {}, {}}); const auto plan = ResourceMaterializer{}.ExtractPlan(program); const auto copied = std::ranges::count_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::IAdd32; }); require(copied == 0, "plan pruning: the address computation under a load the evaluator cannot read was copied"); + require(plan.controlFlow[0].condition == nullptr && plan.controlFlow[3].condition == nullptr, "plan pruning: a condition the evaluator can never read was kept"); + require(plan.controlFlow[1].condition != nullptr && plan.controlFlow[2].condition != nullptr, "plan pruning: a condition the evaluator can read was dropped"); const auto copiedLoad = std::ranges::find_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::LoadBufferU32; }); require(copiedLoad != plan.valueStorage.end() && copiedLoad->ArgumentCount() == 0u, "plan pruning: the unevaluable load was not kept as a leaf"); + require(std::ranges::all_of(plan.valueStorage, [](const IrValue& value) { return !value.HasUses() && value.OperandUses().empty() && value.Arguments().capacity() == value.ArgumentCount(); }), "plan compaction: a plan value keeps use lists or spare operand capacity"); const std::array<std::uint32_t, 4> data{0x100u, 7u, static_cast<std::uint32_t>(wordAddress), static_cast<std::uint32_t>(wordAddress >> 32u)}; SrtRuntime runtime{data}; Detail::Evaluator evaluator(plan, runtime); std::uint32_t result = 0; - require(plan.controlFlow[0].condition == nullptr, "plan pruning: a condition on a vector load was kept"); require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[1].condition), result) && result == 1u, "plan pruning: a condition read through an extracted pair was not evaluated"); require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[2].condition), result) && result == 1u, "plan pruning: a condition on a raw read lost its handle"); } +struct PlanWalk { + std::vector<ShaderRecompiler::DescriptorValue> results; + std::vector<std::uint32_t> flat; + std::vector<std::uint8_t> active; +}; + +PlanWalk walkPlan(const ShaderRecompiler::IrResourcePlan& plan, std::span<const std::uint32_t> sources, std::span<const std::uint8_t> cleanFlatSlots, const ShaderRecompiler::SrtRuntime& runtime) { + PlanWalk walk; + ShaderRecompiler::SrtWalker{}.EvaluateRuntimeSources(plan, sources, runtime, walk.results, walk.flat, cleanFlatSlots, walk.active); + return walk; +} + +bool readFive(void*, std::uint64_t, std::uint32_t* value) { + *value = 5u; + return true; +} + +void verifyPlanRetention() { + using namespace ShaderRecompiler; + std::vector<std::unique_ptr<IrValue>> values; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique<IrValue>(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto binary = [&](IrOpcode opcode, IrType type, IrValue& left, IrValue& right) -> IrValue& { + auto& value = make(opcode, type); + value.AddArgument(&left); + value.AddArgument(&right); + return value; + }; + constexpr std::uint32_t Chain = 64; + IrValue* address = &userData(4); + for (std::uint32_t step = 0; step < Chain; ++step) address = &binary(IrOpcode::IAdd32, IrType::U32, *address, constant(1)); + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(address); + auto& low = binary(IrOpcode::CompositeExtractU32x2, IrType::U32, binary(IrOpcode::CompositeConstructU32x2, IrType::U32x2, userData(1), load), constant(0)); + auto& mask = binary(IrOpcode::IEqual32, IrType::Bool, userData(5), constant(0)); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&mask); + choice.AddArgument(&userData(6)); + choice.AddArgument(&load); + auto& offset = binary(IrOpcode::ReadFirstLane, IrType::U32, choice, mask); + auto& read = binary(IrOpcode::LoadAddressU32, IrType::U32, binary(IrOpcode::CompositeConstructU64, IrType::U64, userData(2), userData(3)), offset); + read.SetFlags(MemoryFlags{0, 0}); + IrProgram program; + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + for (std::uint32_t source = 0; source < 2u; ++source) { + DescriptorSource descriptor; + descriptor.dwordCount = 4; + for (std::uint32_t dword = 0; dword < 4u; ++dword) descriptor.dwords[dword] = &userData(10u + source * 4u + dword); + resources.descriptorSources.push_back(descriptor); + BufferResource buffer; + buffer.source = source; + resources.info.buffers.push_back(buffer); + } + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, low, constant(7)), {1, 2}, {0}, {}}); + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, read, constant(5)), {3, 4}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {1}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + constexpr std::size_t Read = 40; + require(plan.valueStorage.Size() == Read, ("plan retention: the plan keeps " + std::to_string(plan.valueStorage.Size()) + " values; its conditions and descriptor sources read " + std::to_string(Read)).c_str()); + require(std::ranges::none_of(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::IAdd32; }), "plan retention: the address under a vector load was copied"); + const auto copiedLoad = std::ranges::find_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::LoadBufferU32; }); + require(copiedLoad != plan.valueStorage.end() && copiedLoad->ArgumentCount() == 0u, "plan retention: the vector load was not kept as a leaf"); + std::array<std::uint32_t, 18> userWords{}; + userWords[1] = 7u; + userWords[2] = 0x1000u; + userWords[6] = 8u; + for (std::uint32_t word = 10; word < userWords.size(); ++word) userWords[word] = 0x100u + word; + SrtRuntime runtime{userWords}; + runtime.readMemory = readFive; + runtime.readSpecializationMemory = readFive; + const auto copiedWalk = walkPlan(plan, plan.materializationSources, plan.cleanFlatSlots, runtime); + const std::array<std::uint32_t, 2> sources{0u, 1u}; + const auto wholeWalk = walkPlan(program.Resources(), sources, {}, runtime); + require(wholeWalk.active == std::vector<std::uint8_t>{1u, 0u}, "plan retention: the program's walk did not decide both conditions"); + require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "plan retention: the plan's walk differs from the program's"); +} + +void verifyPlanRetentionChains() { + using namespace ShaderRecompiler; + std::vector<std::size_t> kept; + std::vector<std::size_t> conditions; + for (const std::uint32_t chain : {1u, 16u, 64u}) { + std::vector<std::uint32_t> code{0xf4080600u, 0xfa000060u, 0xbf8cc07fu, 0x7e140300u}; + code.insert(code.end(), chain, 0x4a141481u); + code.insert(code.end(), {0x34141482u, 0xe0301000u, 0x80060b0au, 0xbf8c0000u, 0x7e28050bu, 0xf4000540u, 0x28000000u, 0xf4080700u, 0xfa000020u, 0xbf8c0000u, 0xbf078015u, 0xbf840002u, 0xe0700000u, 0x80070000u, 0xbf810000u}); + const std::array<std::uint32_t, 2> userData{0x10000u, 0u}; + const auto plan = GetResourcePlan(FrontendRequest(code, userData)); + kept.push_back(plan->valueStorage.Size()); + conditions.push_back(static_cast<std::size_t>(std::ranges::count_if(plan->controlFlow, [](const ResourceBlock& block) { return block.condition != nullptr; }))); + } + require(conditions.front() != 0u, "plan retention: the program's plan keeps no branch condition, so the test does not reach the condition's values"); + require(kept[0] == kept[1] && kept[1] == kept[2], ("plan retention: the plan keeps " + std::to_string(kept[0]) + ", " + std::to_string(kept[1]) + " and " + std::to_string(kept[2]) + " values for vector chains of 1, 16 and 64 steps before the scalar load its branch condition reads; it must keep only what the condition's evaluation can reach").c_str()); +} + void verifySignedMinimum() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -8130,6 +8253,8 @@ int main(int argc, char** argv) { verifyMayEvaluate(); verifyPureFlatSlots(); verifyPlanConditionPruning(); + verifyPlanRetention(); + verifyPlanRetentionChains(); verifyGuardedDescriptorSources(); verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); From 6f36f29ea364b8ed615eef41ba7d924765ea5056 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:18:46 +0000 Subject: [PATCH 1339/1360] fix(shader): answer phis and null bases in a trimmed plan as the program does A plan copies a branch condition only as far as the evaluator reads it, and an operation the evaluator does not read is kept without its arguments. Two questions the walk asks still read below such a leaf. ResolveInvariantPhi compares a phi's incoming values with EquivalentValue, which follows their arguments, so two leaves with the same operation looked equal in the copy where their arguments differ in the program: a phi whose incoming values differ only below a vector load resolved in the plan and not in the program, and the plan decided a condition the program leaves open. LoadedFromMemory looks for a read from memory anywhere below a raw read's handle, so a handle that reaches memory only below a leaf lost it in the copy: a read from a null base counted as a null root, and its condition stayed open where the program decides it. ownPlanValues now checks both after it copies. Each phi it copied in part must resolve in the copy to the copy of what it resolves to in the program, and each raw read's handle must be loaded from memory in the copy when it is in the program. A phi that differs is copied whole; a handle that differs keeps the arguments along its shortest path to a read from memory (its whole closure if that path is already kept), and the copy is made again. The checks only add values, so the loop ends, at worst with the whole copy. IsMemoryRead names the operations that LoadedFromMemory and the path both stop at. Tested: agc_shader_memory's verifyPlanCopyAnswers walks a phi whose incoming values differ only below a vector load, and a read from a null base whose handle reaches memory only through a select arm the active mask never takes, through the plan's tape, the plan's interpreter and the program's own walk, and all three agree. Without the checks the plan decides the first condition and leaves the second open. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 94 ++++++++++ .../SrtWalker/SrtInstructionPredicates.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 177 ++++++++++++------ .../SrtWalker/SrtInstructionPredicates.cpp | 7 +- 4 files changed, 219 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 27bca762d5..e1248cd723 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -765,6 +765,99 @@ void verifyPlanRetentionChains() { require(kept[0] == kept[1] && kept[1] == kept[2], ("plan retention: the plan keeps " + std::to_string(kept[0]) + ", " + std::to_string(kept[1]) + " and " + std::to_string(kept[2]) + " values for vector chains of 1, 16 and 64 steps before the scalar load its branch condition reads; it must keep only what the condition's evaluation can reach").c_str()); } +void verifyPlanCopyAnswers() { + using namespace ShaderRecompiler; + std::vector<std::unique_ptr<IrValue>> values; + std::vector<std::unique_ptr<IrBlock>> blocks; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique<IrValue>(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto binary = [&](IrOpcode opcode, IrType type, IrValue& left, IrValue& right) -> IrValue& { + auto& value = make(opcode, type); + value.AddArgument(&left); + value.AddArgument(&right); + return value; + }; + const auto vectorLoad = [&](IrValue& address) -> IrValue& { + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(&address); + return load; + }; + const auto rawRead = [&](IrValue& low, IrValue& high, IrValue& offset) -> IrValue& { + auto& read = binary(IrOpcode::LoadAddressU32, IrType::U32, binary(IrOpcode::CompositeConstructU64, IrType::U64, low, high), offset); + read.SetFlags(MemoryFlags{0, 0}); + return read; + }; + const auto walks = [&](IrProgram& program, IrValue& read, const std::array<std::uint32_t, 14>& userWords, std::uint8_t expected, const char* what) { + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + DescriptorSource descriptor; + descriptor.dwordCount = 4; + for (std::uint32_t dword = 0; dword < 4u; ++dword) descriptor.dwords[dword] = &userData(10u + dword); + resources.descriptorSources.push_back(descriptor); + BufferResource buffer; + resources.info.buffers.push_back(buffer); + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, read, constant(5)), {1, 2}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {0}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + SrtRuntime runtime{userWords}; + runtime.readMemory = readFive; + runtime.readSpecializationMemory = readFive; + const std::array<std::uint32_t, 1> sources{0u}; + const auto whole = walkPlan(resources, sources, {}, runtime); + const auto taped = walkPlan(plan, plan.materializationSources, plan.cleanFlatSlots, runtime); + const auto interpreted = walkPlan(plan, sources, {}, runtime); + require(whole.active == std::vector<std::uint8_t>{expected}, what); + require(taped.active == whole.active && taped.results == whole.results, what); + require(interpreted.active == whole.active && interpreted.results == whole.results, what); + }; + std::array<std::uint32_t, 14> userWords{}; + for (std::uint32_t word = 10; word < userWords.size(); ++word) userWords[word] = 0x100u + word; + + IrProgram phiProgram; + auto& shared = userData(1); + const auto low = [&](IrValue& hidden) -> IrValue& { + return binary(IrOpcode::CompositeExtractU32x2, IrType::U32, binary(IrOpcode::CompositeConstructU32x2, IrType::U32x2, shared, hidden), constant(0)); + }; + auto& left = *blocks.emplace_back(std::make_unique<IrBlock>(1u)); + auto& right = *blocks.emplace_back(std::make_unique<IrBlock>(2u)); + auto& phi = make(IrOpcode::Phi, IrType::U32); + phi.AddPhiOperand(&left, &low(vectorLoad(userData(4)))); + phi.AddPhiOperand(&right, &low(vectorLoad(userData(5)))); + userWords[1] = 8u; + userWords[2] = 0x1000u; + walks(phiProgram, rawRead(userData(2), userData(3), phi), userWords, 1u, "plan copy: a phi whose incoming values differ below a vector load resolved in the plan, not in the program"); + + IrProgram rootProgram; + auto& mask = binary(IrOpcode::IEqual32, IrType::Bool, userData(5), constant(0)); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&mask); + choice.AddArgument(&userData(2)); + choice.AddArgument(&vectorLoad(rawRead(userData(8), userData(9), constant(0)))); + userWords[1] = 0u; + userWords[2] = 0u; + walks(rootProgram, rawRead(binary(IrOpcode::ReadFirstLane, IrType::U32, choice, mask), userData(3), constant(0)), userWords, 0u, "plan copy: a read from a null base loaded from memory only below a vector load counted as a null root in the plan, not in the program"); +} + void verifySignedMinimum() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -8255,6 +8348,7 @@ int main(int argc, char** argv) { verifyPlanConditionPruning(); verifyPlanRetention(); verifyPlanRetentionChains(); + verifyPlanCopyAnswers(); verifyGuardedDescriptorSources(); verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp index fa1310cff9..34a381669e 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler::Detail { bool IsRawRead(const IrResourcePlan& program, const IrValue& inst); bool IsRawRead(const IrResourcePlan& program, IrOpcode op, std::uint32_t memoryIndex); +bool IsMemoryRead(IrOpcode opcode); bool LoadedFromMemory(const IrValue& handle); bool IsDescriptorHandle(IrOpcode opcode); bool IsRuntimeSelect(IrOpcode opcode); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3e50e96a8d..5387447c2d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1,6 +1,7 @@ #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" +#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" @@ -2199,6 +2200,27 @@ void ResourceMaterializer::PrepareImageModes(ShaderInfo& info) { namespace { +std::vector<const IrValue*> pathToMemoryRead(const IrValue& handle) { + std::unordered_map<const IrValue*, const IrValue*> parents; + std::vector<const IrValue*> queue; + const auto visit = [&](const IrValue* argument, const IrValue* parent) { + if (argument == nullptr) return; + argument = argument->Resolve(); + if (!argument->HasImmediate() && parents.emplace(argument, parent).second) queue.push_back(argument); + }; + for (const auto* argument : handle.Arguments()) visit(argument, nullptr); + for (std::size_t index = 0; index < queue.size(); ++index) { + const auto* value = queue[index]; + if (Detail::IsMemoryRead(value->Opcode())) { + std::vector<const IrValue*> path; + for (const auto* step = parents.at(value); step != nullptr; step = parents.at(step)) path.push_back(step); + return path; + } + for (const auto* argument : value->Arguments()) visit(argument, value); + } + return {}; +} + void ownPlanValues(IrResourcePlan& plan) { std::vector<IrValue**> roots; for (auto& source : plan.descriptorSources) { @@ -2209,71 +2231,110 @@ void ownPlanValues(IrResourcePlan& plan) { const auto closures = roots.size(); for (auto& block : plan.controlFlow) roots.push_back(&block.condition); - std::unordered_map<const IrValue*, IrValue*> clones; - std::unordered_set<const IrValue*> closed; - std::unordered_set<const IrValue*> opened; - std::vector<const IrValue*> order; - std::vector<std::pair<const IrValue*, bool>> pending; - for (std::size_t index = 0; index < roots.size(); ++index) { - if (*roots[index] != nullptr) pending.emplace_back(*roots[index], index < closures); - } - while (!pending.empty()) { - const auto [value, closure] = pending.back(); - pending.pop_back(); - if (clones.emplace(value, nullptr).second) order.push_back(value); - const auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); - if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; - if (closure) { - closed.insert(value); - } else if (!opened.insert(value).second) { - continue; + std::unordered_set<const IrValue*> whole; + std::unordered_set<const IrValue*> linked; + for (;;) { + std::unordered_map<const IrValue*, IrValue*> clones; + std::unordered_set<const IrValue*> closed; + std::unordered_set<const IrValue*> opened; + std::unordered_set<const IrValue*> handles; + std::vector<const IrValue*> phis; + std::vector<const IrValue*> order; + std::vector<std::pair<const IrValue*, bool>> pending; + for (std::size_t index = 0; index < roots.size(); ++index) { + if (*roots[index] != nullptr) pending.emplace_back(*roots[index], index < closures); } - for (const auto* argument : value->Arguments()) { - if (argument != nullptr) pending.emplace_back(argument, closure); - } - if (!closure && reads == Detail::EvaluatorReads::Closure && value->ArgumentCount() != 0u && value->Argument(0) != nullptr) { - const auto* handle = value->Argument(0)->Resolve(); - if (opened.insert(handle).second) { - for (const auto* argument : handle->Arguments()) { - if (argument != nullptr) pending.emplace_back(argument, false); + while (!pending.empty()) { + const auto value = pending.back().first; + const auto closure = pending.back().second || whole.contains(value); + pending.pop_back(); + if (clones.emplace(value, nullptr).second) order.push_back(value); + auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); + if (reads == Detail::EvaluatorReads::Nothing && linked.contains(value)) reads = Detail::EvaluatorReads::Arguments; + if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; + if (closure) { + closed.insert(value); + } else if (!opened.insert(value).second) { + continue; + } else if (value->IsPhi()) { + phis.push_back(value); + } + for (const auto* argument : value->Arguments()) { + if (argument != nullptr) pending.emplace_back(argument, closure); + } + if (!closure && reads == Detail::EvaluatorReads::Closure && value->ArgumentCount() != 0u && value->Argument(0) != nullptr) { + const auto* handle = value->Argument(0)->Resolve(); + handles.insert(handle); + if (opened.insert(handle).second) { + for (const auto* argument : handle->Arguments()) { + if (argument != nullptr) pending.emplace_back(argument, false); + } } } } - } - plan.valueStorage.Reserve(order.size()); - for (const auto* value : order) { - auto* clone = plan.valueStorage.Create(value->Opcode(), value->Type(), static_cast<std::uint32_t>(plan.valueStorage.Size())); - clone->SetFlags(value->Flags<std::uint64_t>()); - if (value->HasImmediate()) clone->SetImmediateU64(value->ImmediateU64()); - clone->SetRegister(value->Register()); - clones[value] = clone; - } - std::unordered_map<const IrBlock*, IrBlock*> blocks; - const auto blockFor = [&](const IrBlock* block) { - auto& clone = blocks[block]; - if (clone == nullptr) { - plan.blockStorage.push_back(std::make_unique<IrBlock>(block->Id())); - clone = plan.blockStorage.back().get(); + IrValueArena storage; + storage.Reserve(order.size()); + for (const auto* value : order) { + auto* clone = storage.Create(value->Opcode(), value->Type(), static_cast<std::uint32_t>(storage.Size())); + clone->SetFlags(value->Flags<std::uint64_t>()); + if (value->HasImmediate()) clone->SetImmediateU64(value->ImmediateU64()); + clone->SetRegister(value->Register()); + clones[value] = clone; } - return clone; - }; - for (const auto* value : order) { - const auto count = closed.contains(value) || opened.contains(value) ? value->ArgumentCount() : 0u; - std::vector<IrValue*> arguments; - std::vector<IrBlock*> phiBlocks; - arguments.reserve(count); - if (value->IsPhi()) phiBlocks.reserve(count); - for (std::size_t index = 0; index < count; index++) { - const auto* argument = value->Argument(index); - arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); - if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); + std::vector<std::unique_ptr<IrBlock>> blockStorage; + std::unordered_map<const IrBlock*, IrBlock*> blocks; + const auto blockFor = [&](const IrBlock* block) { + auto& clone = blocks[block]; + if (clone == nullptr) { + blockStorage.push_back(std::make_unique<IrBlock>(block->Id())); + clone = blockStorage.back().get(); + } + return clone; + }; + for (const auto* value : order) { + const auto count = closed.contains(value) || opened.contains(value) ? value->ArgumentCount() : 0u; + std::vector<IrValue*> arguments; + std::vector<IrBlock*> phiBlocks; + arguments.reserve(count); + if (value->IsPhi()) phiBlocks.reserve(count); + for (std::size_t index = 0; index < count; index++) { + const auto* argument = value->Argument(index); + arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); + if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); + } + clones.at(value)->SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } - clones.at(value)->SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); + const auto resolvesAlike = [&](const IrValue* phi) { + try { + const auto* expected = ResolveInvariantPhi(plan, const_cast<IrValue*>(phi)); + const auto* copied = ResolveInvariantPhi(plan, clones.at(phi)); + if (expected == nullptr) return copied == nullptr; + const auto found = clones.find(expected); + return found != clones.end() && found->second == copied; + } catch (const std::exception&) { + return false; + } + }; + bool grown = false; + for (const auto* phi : phis) { + if (!closed.contains(phi) && !resolvesAlike(phi)) grown = whole.insert(phi).second || grown; + } + for (const auto* handle : handles) { + if (closed.contains(handle) || Detail::LoadedFromMemory(*clones.at(handle))) continue; + const auto path = pathToMemoryRead(*handle); + bool linkedStep = false; + for (const auto* step : path) linkedStep = linked.insert(step).second || linkedStep; + if (!path.empty()) grown = (linkedStep || whole.insert(handle).second) || grown; + } + if (grown) continue; + plan.valueStorage = std::move(storage); + plan.blockStorage = std::move(blockStorage); + for (auto* root : roots) { + if (*root != nullptr) *root = clones.at(*root); + } + plan.denseValueIds = true; + return; } - for (auto* root : roots) { - if (*root != nullptr) *root = clones.at(*root); - } - plan.denseValueIds = true; } void buildCompactPlan(IrResourcePlan& plan) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp index 855fd960af..140e1e7a2b 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp @@ -20,6 +20,10 @@ bool IsRawRead(const IrResourcePlan& program, IrOpcode op, std::uint32_t index) return (op == IrOpcode::LoadAddressU32 && kind == ResourceKind::ScalarAddress) || (op == IrOpcode::ReadConstBuffer && kind == ResourceKind::ScalarBuffer); } +bool IsMemoryRead(IrOpcode opcode) { + return opcode == IrOpcode::ReadConst || opcode == IrOpcode::LoadAddressU32 || opcode == IrOpcode::ReadConstBuffer; +} + bool LoadedFromMemory(const IrValue& handle) { std::vector<const IrValue*> pending; std::vector<const IrValue*> visited; @@ -31,8 +35,7 @@ bool LoadedFromMemory(const IrValue& handle) { value = value->Resolve(); if (value->HasImmediate() || std::find(visited.begin(), visited.end(), value) != visited.end()) continue; visited.push_back(value); - const auto opcode = value->Opcode(); - if (opcode == IrOpcode::ReadConst || opcode == IrOpcode::LoadAddressU32 || opcode == IrOpcode::ReadConstBuffer) return true; + if (IsMemoryRead(value->Opcode())) return true; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) pending.push_back(value->Argument(index)); } return false; From 193b11c32fdcfcad45a88924e83610a3a34de020 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 15:39:43 +0300 Subject: [PATCH 1340/1360] fix(agc): reuse pipeline cache headers on long Windows paths --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 74 +++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + 3 files changed, 81 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00..948ed262c6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1515,6 +1515,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) + add_test(NAME agc_pipeline_cache_header COMMAND agc_driver_recorder_tests --pipeline-cache-header-only) + set_tests_properties(agc_pipeline_cache_header PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_pipeline_cache_header PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) add_test(NAME agc_pipeline_library_off COMMAND agc_driver_recorder_tests --pipeline-library-only) set_tests_properties(agc_pipeline_library agc_pipeline_library_off PROPERTIES SKIP_RETURN_CODE 77) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index cf83ce9e23..870ed541d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -31,6 +31,7 @@ #include "ResidentPresent.hpp" #include "ImageTableAbi.hpp" #include "HostImportLifetime.hpp" +#include "ShaderCacheDirectory.hpp" #include "SampleLod_spv.h" #include "BufferUpdate_spv.h" #include "SampleArray_spv.h" @@ -9741,6 +9742,74 @@ void pipelineCacheTests(const Device& device, bool benchmark) { std::cout << "Pipeline cache key and retained lifetime tests passed\n"; } +struct PipelineCacheFileProbe { + Context& context; + PFN_vkGetDeviceProcAddr resolver; + std::atomic<std::uint32_t> creations{0}; + static inline PipelineCacheFileProbe* active = nullptr; + explicit PipelineCacheFileProbe(Context& context) : context(context), resolver(context.deviceProc) { + Require(active == nullptr, "pipeline cache file probe is already active"); + active = this; + context.deviceProc = resolve; + } + ~PipelineCacheFileProbe() { + context.deviceProc = resolver; + active = nullptr; + } + static VKAPI_ATTR VkResult VKAPI_CALL createCache(VkDevice device, const VkPipelineCacheCreateInfo* info, const VkAllocationCallbacks* callbacks, VkPipelineCache* created) { + ++active->creations; + return reinterpret_cast<PFN_vkCreatePipelineCache>(active->resolver(device, "vkCreatePipelineCache"))(device, info, callbacks, created); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreatePipelineCache") == 0) return reinterpret_cast<PFN_vkVoidFunction>(createCache); + return active->resolver(device, name); + } +}; + +void pipelineCacheHeaderTests(const Device& device) { + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-header-test-" + std::to_string(std::random_device{}())); + Require(root.native().size() < 225, "the pipeline cache header test requires a temporary root shorter than 225 characters"); +#ifdef _WIN32 + std::error_code cleanupError; + const auto cleanupRoot = ShaderRecompiler::NativeCachePath_nid_no_patch(root, cleanupError); + Require(!cleanupError, "the pipeline cache header test could not resolve its cleanup directory"); +#else + const auto& cleanupRoot = root; +#endif + struct RemoveDirectory { + std::filesystem::path directory; + ~RemoveDirectory() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } removeDirectory{cleanupRoot}; + auto directory = root; + while (directory.native().size() < 220) directory /= "long-pipeline-cache-directory"; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); +#endif + auto context = device.GetContext(); + PipelineCacheFileProbe probe(context); + PipelineCache cache(context, device.Properties()); + cache.SaveNow(); + std::vector<std::filesystem::path> files; + for (const auto& entry : std::filesystem::directory_iterator(directory)) files.push_back(entry.path()); + Require(files.size() == 1 && files.front().filename().string().starts_with("vk-pipelines-"), "the long pipeline cache directory does not hold exactly one cache file"); + Require(files.front().native().size() > 260, "the pipeline cache header test did not exceed MAX_PATH"); + std::vector<std::byte> stored; + Require(ShaderRecompiler::ReadWholeFile(files.front(), stored) && !stored.empty(), "the long pipeline cache file was not persisted"); + const auto creations = probe.creations.load(); + for (int repeat = 0; repeat < 3; ++repeat) cache.SaveNow(); + Require(probe.creations.load() == creations, "an unchanged long-path pipeline cache was reloaded for an idle save"); + std::vector<std::byte> after; + Require(ShaderRecompiler::ReadWholeFile(files.front(), after) && after == stored, "an idle save changed the long pipeline cache file"); + std::cout << "long-path pipeline cache header reused without reloading\n"; +} + int pipelineCacheFileTests(const Device& device) { const auto& context = device.GetContext(); const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-cache-file-test-" + std::to_string(std::random_device{}())); @@ -10790,6 +10859,11 @@ int main(int argc, char** argv) { return pipelineCacheFileTests(device); } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-cache-header-only") { + pipelineCacheHeaderTests(device); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-only") { descriptorPoolTests(device); return 0; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 07dee3a0d9..a9dd8b251f 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -259,6 +259,7 @@ function(add_shader_recompiler target runtimeTarget) RayTracingStrict RayTracingMiss ReadWholeFile + NativeCachePath_nid_no_patch Recompile RecompileCacheKey::ContextHash RecompileResult From 19d4bee8231e464de8432b882a0c43a07eea2606 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:22:20 -0300 Subject: [PATCH 1341/1360] test(agc): bound cache directory and check unread --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 870ed541d2..3a7bcffbc0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9784,7 +9784,12 @@ void pipelineCacheHeaderTests(const Device& device) { } } removeDirectory{cleanupRoot}; auto directory = root; - while (directory.native().size() < 220) directory /= "long-pipeline-cache-directory"; + while (directory.native().size() < 220) { + const auto remaining = 245 > directory.native().size() ? 245 - directory.native().size() : 1; + const auto chunk = std::min<std::size_t>(remaining, 16); + directory /= std::string(chunk, 'd'); + } + Require(directory.native().size() >= 220 && directory.native().size() <= 249, "the pipeline cache directory is not between 220 and 249 characters"); #ifdef _WIN32 _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); @@ -9807,6 +9812,19 @@ void pipelineCacheHeaderTests(const Device& device) { Require(probe.creations.load() == creations, "an unchanged long-path pipeline cache was reloaded for an idle save"); std::vector<std::byte> after; Require(ShaderRecompiler::ReadWholeFile(files.front(), after) && after == stored, "an idle save changed the long pipeline cache file"); +#ifdef _WIN32 + std::error_code lockError; + const auto lockNative = ShaderRecompiler::NativeCachePath_nid_no_patch(files.front(), lockError); + Require(!lockError, "could not resolve long cache path"); + const HANDLE lockHandle = CreateFileW(lockNative.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (lockHandle != INVALID_HANDLE_VALUE) { + PipelineCache lockedCache(context, device.Properties()); + lockedCache.SaveNow(); + CloseHandle(lockHandle); + std::vector<std::byte> retained; + Require(ShaderRecompiler::ReadWholeFile(files.front(), retained) && retained == stored, "an unreadable long pipeline cache file was overwritten"); + } +#endif std::cout << "long-path pipeline cache header reused without reloading\n"; } From 48d526af6af517c7c23d323ad7730174f3749fc7 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:41:02 -0300 Subject: [PATCH 1342/1360] test(agc): require lock and verify unreadable cache diagnostic --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3a7bcffbc0..5c589b3b87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9816,14 +9816,27 @@ void pipelineCacheHeaderTests(const Device& device) { std::error_code lockError; const auto lockNative = ShaderRecompiler::NativeCachePath_nid_no_patch(files.front(), lockError); Require(!lockError, "could not resolve long cache path"); - const HANDLE lockHandle = CreateFileW(lockNative.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (lockHandle != INVALID_HANDLE_VALUE) { + struct FileLockGuard { + HANDLE handle = INVALID_HANDLE_VALUE; + ~FileLockGuard() { + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); + } + } lockGuard{CreateFileW(lockNative.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; + Require(lockGuard.handle != INVALID_HANDLE_VALUE, "could not lock pipeline cache file"); + std::string capturedStderr; + { + StderrCapture capture; PipelineCache lockedCache(context, device.Properties()); lockedCache.SaveNow(); - CloseHandle(lockHandle); - std::vector<std::byte> retained; - Require(ShaderRecompiler::ReadWholeFile(files.front(), retained) && retained == stored, "an unreadable long pipeline cache file was overwritten"); + capturedStderr = capture.Finish(); } + Require(capturedStderr.find("cannot read") != std::string::npos, "an unreadable long pipeline cache file did not report the cannot read diagnostic"); + if (lockGuard.handle != INVALID_HANDLE_VALUE) { + CloseHandle(lockGuard.handle); + lockGuard.handle = INVALID_HANDLE_VALUE; + } + std::vector<std::byte> retained; + Require(ShaderRecompiler::ReadWholeFile(files.front(), retained) && retained == stored, "an unreadable long pipeline cache file was overwritten"); #endif std::cout << "long-path pipeline cache header reused without reloading\n"; } From a06df95447629c31241245e0d4deb3b4e7389625 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:36:34 -0300 Subject: [PATCH 1343/1360] test(agc): follow versioned cache directories --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5c589b3b87..9e6af9dfd5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9784,12 +9784,16 @@ void pipelineCacheHeaderTests(const Device& device) { } } removeDirectory{cleanupRoot}; auto directory = root; - while (directory.native().size() < 220) { - const auto remaining = 245 > directory.native().size() ? 245 - directory.native().size() : 1; + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast<unsigned long long>(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); + while ((directory / version).native().size() < 220) { + const auto length = (directory / version).native().size(); + const auto remaining = 245 > length ? 245 - length : 1; const auto chunk = std::min<std::size_t>(remaining, 16); directory /= std::string(chunk, 'd'); } - Require(directory.native().size() >= 220 && directory.native().size() <= 249, "the pipeline cache directory is not between 220 and 249 characters"); + const auto versionDirectory = directory / version; + Require(versionDirectory.native().size() >= 220 && versionDirectory.native().size() <= 249, "the pipeline cache directory is not between 220 and 249 characters"); #ifdef _WIN32 _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); @@ -9802,7 +9806,7 @@ void pipelineCacheHeaderTests(const Device& device) { PipelineCache cache(context, device.Properties()); cache.SaveNow(); std::vector<std::filesystem::path> files; - for (const auto& entry : std::filesystem::directory_iterator(directory)) files.push_back(entry.path()); + for (const auto& entry : std::filesystem::directory_iterator(versionDirectory)) files.push_back(entry.path()); Require(files.size() == 1 && files.front().filename().string().starts_with("vk-pipelines-"), "the long pipeline cache directory does not hold exactly one cache file"); Require(files.front().native().size() > 260, "the pipeline cache header test did not exceed MAX_PATH"); std::vector<std::byte> stored; From 56ce490ced18f65dd59c154c928bf4e8bab74b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 21:52:18 +0200 Subject: [PATCH 1344/1360] fix(shader): drop duplicate NativeCachePath_nid_no_patch allowlist entry --- core/shader/recompiler/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index a9dd8b251f..07dee3a0d9 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -259,7 +259,6 @@ function(add_shader_recompiler target runtimeTarget) RayTracingStrict RayTracingMiss ReadWholeFile - NativeCachePath_nid_no_patch Recompile RecompileCacheKey::ContextHash RecompileResult From 30c324a23be4442cd1668af516a0e23ee7d09173 Mon Sep 17 00:00:00 2001 From: battleblob1 <battleblob1@gmail.com> Date: Wed, 21 Oct 2026 12:42:26 +0300 Subject: [PATCH 1345/1360] fix(recompiler): accept EXEC copies in stencil replay masks Since #6291 a pixel shader's EXEC copy is read as the saved word ANDed with WQM(Ballot(true)). MaskReplaySafety only accepts a whole-quad expansion of a ballot it can show excludes helper lanes, and a ballot of true includes them, so a stencil-exporting shader whose own-lane masks come from an EXEC copy was refused (own-lane mask not stable). Every lane of a quad is active (real or helper), so a ballot of true holds the same quad bits as the all-ones mask the proof already treats as quad-stable; it is now classified the same way. Helper-dependent ballots stay uncertain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 24 +++++++++++++++++++ core/shader/recompiler/StencilReplay.cpp | 1 + docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index f4b855ef84..a77f88d58f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1074,6 +1074,30 @@ int main() { return !StencilReplaySafe(b.program); }); + const auto execCopy = [](Builder& b, IrValue& low, IrValue& high) -> IrValue& { + auto& active = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& lanes = b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&active, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&active, &b.constant(1u)})}); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&lanes}); + return b.threadBit(b.wordAnd(low, b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)})), b.wordAnd(high, b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}))); + }; + + passed &= run("an exec copy masked by the whole-quad active lanes is a stable export guard", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + const auto [low, high] = b.ballotWords(pixelPredicate(b)); + exportUnder(b, execCopy(b, *low, *high), true, true); + return StencilReplaySafe(b.program); + }); + + passed &= run("an exec copy does not hide helper participation in a whole-quad expansion", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + const auto [low, high] = b.ballotWords(helperPredicate(b, true)); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {low, high})}); + stencilByMask(b, execCopy(b, b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}))); + return !StencilReplaySafe(b.program); + }); + passed &= run("only guarded read-only SRT diagnostics can be replayed", [] { Builder b; b.program.Info().usesFaultBuffer = true; diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index 6ed182542f..30bc43b765 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -270,6 +270,7 @@ private: if (lowBallot && highBallot) { const auto low = onHelper(lowBallot->Argument(0), depth + 1u), high = onHelper(highBallot->Argument(0), depth + 1u); if (low && high && *low == 0u && *high == 0u) result = HelperMask::RealOnly; + else if (Boolean(lowBallot->Argument(0), true) && Boolean(highBallot->Argument(0), true)) result = HelperMask::QuadStable; } else if (mask.low->Opcode() == IrOpcode::BitwiseAnd32 && (waveSize == 32u || mask.high->Opcode() == IrOpcode::BitwiseAnd32)) { const auto pair = [&](unsigned low, unsigned high) { return ReplayMask{mask.low->Argument(low)->Resolve(), waveSize == 32u ? mask.low->Argument(low)->Resolve() : mask.high->Argument(high)->Resolve()}; }; const auto combine = [&](unsigned firstHigh, unsigned secondHigh) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3d7bbfd474..c887053906 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -80,7 +80,7 @@ - [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions (a ballot of the active lanes, as in an EXEC copy, covers whole quads and is accepted), since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From 0a4027df9a0822a3ab1bf195769e160e2c35d100 Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:56:10 -0400 Subject: [PATCH 1346/1360] fix(shader): link each table callee next to its own call so large callee sets stay in branch range --- core/libs/tests/ScalarCall.cpp | 40 ++++++++++------ .../ControlFlow/src/GraphBuilder.cpp | 46 ++++++++----------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 2da9bd35ef..abd7cc3d07 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -271,27 +271,39 @@ static void CheckTableCalls() { const std::vector<std::vector<TableCallee>> callees{{{0x1122334455ull, first}, {0x66778899aaull, second}}}; const auto linkedCalls = LinkCalls(program, {}, {}, calls, callees); const auto& linked = linkedCalls.code; - Require(linkedCalls.fallbacks == std::vector<std::uint32_t>{50u * 4u}); + Require(linkedCalls.fallbacks == std::vector<std::uint32_t>{53u * 4u}); ExpectThrow("is shared with another call", [&] { static_cast<void>(BuildCode(linked, swappc)); }); SwappcInfo linkedSwappc = swappc; linkedSwappc.linkedCallFallbacks = linkedCalls.fallbacks; - Require(linked.size() == 59u && linked[9] == 0xbf820001u && linked[11] == 0xbf850010u && linked[58] == 0xbf9f0000u); - Require(linked[12] == 0xbf07ff0eu && linked[13] == 0x22334455u && linked[14] == 0xbf850004u && linked[15] == 0xbf07ff0fu && linked[16] == 0x11u && linked[17] == 0xbf850001u && linked[18] == 0xbf82001bu); - Require(linked[20] == 0x778899aau && linked[26] == 0xbf078080u && linked[27] == 0xbf820016u && linked[34] == 0xbf068080u && linked[35] == 0xbf82000au && linked[44] == 0xbf068080u && linked[45] == 0xbf820004u); - Require(linked[46] == 0xbb0e0005u && linked[47] == 0xbf82ffdau && linked[48] == 0xbb0e0006u && linked[49] == 0xbf82ffd8u && linked[50] == 0xbe8e210eu && linked[51] == 0xbf82ffd6u); - Require(std::equal(first.begin(), first.end(), linked.begin() + 52) && std::equal(second.begin(), second.end(), linked.begin() + 55)); + Require(linked.size() == 56u && linked[9] == 0xbf820001u && linked[11] == 0xbf850007u && linked[55] == 0xbf9f0000u); + Require(linked[12] == 0xbf07ff0eu && linked[13] == 0x22334455u && linked[14] == 0xbf850010u && linked[15] == 0xbf07ff0fu && linked[16] == 0x11u && linked[17] == 0xbf85000du && linked[18] == 0xbf820007u); + Require(linked[19] == 0xbf07ff0eu && linked[21] == 0xbf850010u && linked[24] == 0xbf85000du && linked[25] == 0xbf068080u && linked[26] == 0xbb0e0001u && linked[27] == 0xbf82ffeeu); + Require(linked[32] == 0x778899aau && linked[33] == 0xbf850010u && linked[37] == 0xbf820007u && linked[40] == 0xbf85000bu && linked[43] == 0xbf850008u && linked[45] == 0xbb0e0001u && linked[46] == 0xbf82ffecu); + Require(linked[50] == 0xbf078080u && linked[51] == 0xbf820001u && linked[52] == 0xbf068080u && linked[53] == 0xbe8e210eu && linked[54] == 0xbf82fff7u); + Require(std::equal(first.begin(), first.end(), linked.begin() + 28) && std::equal(second.begin(), second.end(), linked.begin() + 47)); const auto cfg = BuildCode(linked, linkedSwappc); const auto block = [&](std::uint32_t word) { return cfg.FindBlockByProgramCounter(word * 4u).id; }; - Require(cfg.FindBlockByProgramCounter(46u * 4u).terminator.trueBlock == block(52u)); - Require(cfg.FindBlockByProgramCounter(52u * 4u).terminator.trueBlock == block(47u)); - Require(cfg.FindBlockByProgramCounter(48u * 4u).terminator.trueBlock == block(55u)); - Require(cfg.FindBlockByProgramCounter(55u * 4u).terminator.trueBlock == block(49u)); - Require(cfg.unlinkedCallProgramCounters == std::vector<std::uint32_t>{50u * 4u}); + Require(cfg.FindBlockByProgramCounter(26u * 4u).terminator.trueBlock == block(28u)); + Require(cfg.FindBlockByProgramCounter(28u * 4u).terminator.trueBlock == block(27u)); + Require(cfg.FindBlockByProgramCounter(45u * 4u).terminator.trueBlock == block(47u)); + Require(cfg.FindBlockByProgramCounter(47u * 4u).terminator.trueBlock == block(46u)); + Require(cfg.unlinkedCallProgramCounters == std::vector<std::uint32_t>{53u * 4u}); static_cast<void>(TranslateCode(linked, linkedSwappc)); - std::vector<std::uint32_t> large(0x8000u, 0xbf800000u); - const std::vector<std::vector<TableCallee>> far{{{0x10ull, large}, {0x20ull, first}}}; - ExpectThrow("out of s_call_b64 range", [&] { static_cast<void>(LinkCalls(program, {}, {}, calls, far)); }); + std::vector<std::uint32_t> large(0x3000u, 0xbf800000u); + large.push_back(0xbe80200eu); + const std::vector<std::vector<TableCallee>> far{{{0x10ull, large}, {0x20ull, large}, {0x30ull, large}, {0x40ull, first}}}; + const auto farLinked = LinkCalls(program, {}, {}, calls, far); + const auto farCall = 12u + 3u * (16u + 0x3001u) + 14u; + Require(farLinked.code[farCall] == 0xbb0e0001u && std::equal(first.begin(), first.end(), farLinked.code.begin() + farCall + 2u)); + SwappcInfo farSwappc = swappc; + farSwappc.linkedCallFallbacks = farLinked.fallbacks; + const auto farCfg = BuildCode(farLinked.code, farSwappc); + Require(farCfg.FindBlockByProgramCounter(farCall * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).id); + Require(farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 1u) * 4u).id); + std::vector<std::uint32_t> oversized(0x8000u, 0xbf800000u); + const std::vector<std::vector<TableCallee>> overflows{{{0x10ull, oversized}, {0x20ull, first}}}; + ExpectThrow("out of the 16-bit branch range", [&] { static_cast<void>(LinkCalls(program, {}, {}, calls, overflows)); }); const std::vector<std::uint32_t> jumpTable{ 0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u, diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index eb9e209745..322fae20d5 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1591,41 +1591,35 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span<const UserDataCall> const auto at = "the table callees of the call at program counter " + toHexString(table.programCounter); const auto original = linked[word]; const auto count = callees.size(); - const auto dispatch = linked.size(); - const auto zero = dispatch + 1u; - const auto one = zero + count * 7u + 2u; - const auto calls = one + count * 8u + 2u; - const auto fault = calls + count * 2u; + std::vector<std::size_t> segments(count + 1u); + segments[0] = linked.size() + 1u; + for (std::size_t callee = 0; callee < count; ++callee) segments[callee + 1u] = segments[callee] + 16u + callees[callee].code.size(); + const auto one = [&](std::size_t callee) { return segments[callee] + (callee < count ? 7u : 2u); }; + const auto back = [&](std::size_t callee) { return callee == 0u ? word + 1u : segments[callee - 1u] + 15u; }; + const auto fault = segments[count] + 3u; const auto branch = [&](std::uint32_t opcode, std::size_t target) { linked.push_back(branchWord(opcode, linked.size(), target, at.c_str())); }; - linked[word] = branchWord(0xbf820000u, word, dispatch, at.c_str()); - branch(0xbf850000u, one); - for (const bool sccOne : {false, true}) { - for (std::size_t callee = 0; callee < count; ++callee) { - const auto next = linked.size() + (sccOne ? 8u : 7u); + linked[word] = branchWord(0xbf820000u, word, linked.size(), at.c_str()); + branch(0xbf850000u, one(0u)); + for (std::size_t callee = 0; callee < count; ++callee) { + for (const bool sccOne : {false, true}) { + const auto next = sccOne ? one(callee + 1u) : segments[callee + 1u]; linked.insert(linked.end(), {0xbf07ff00u | table.targetRegister, static_cast<std::uint32_t>(callees[callee].target)}); branch(0xbf850000u, next); linked.insert(linked.end(), {0xbf07ff00u | (table.targetRegister + 1u), static_cast<std::uint32_t>(callees[callee].target >> 32u)}); branch(0xbf850000u, next); if (sccOne) linked.push_back(0xbf068080u); - branch(0xbf820000u, calls + callee * 2u); + else branch(0xbf820000u, segments[callee] + 14u); } - linked.push_back(sccOne ? 0xbf068080u : 0xbf078080u); - branch(0xbf820000u, fault); - } - for (std::size_t callee = 0; callee < count; ++callee) { - linked.push_back(0u); - branch(0xbf820000u, word + 1u); - } - fallbacks.push_back(static_cast<std::uint32_t>(linked.size() * 4u)); - linked.push_back(original); - branch(0xbf820000u, word + 1u); - for (std::size_t callee = 0; callee < count; ++callee) { - const auto site = calls + callee * 2u; - const auto offset = static_cast<std::int64_t>(linked.size()) - static_cast<std::int64_t>(site) - 1; - if (offset > 0x7fff) throw std::invalid_argument(at + " land out of s_call_b64 range"); - linked[site] = 0xbb000000u | (table.linkRegister << 16u) | static_cast<std::uint32_t>(offset); + linked.push_back(0xbb000001u | (table.linkRegister << 16u)); + branch(0xbf820000u, back(callee)); linked.insert(linked.end(), callees[callee].code.begin(), callees[callee].code.end()); } + linked.push_back(0xbf078080u); + branch(0xbf820000u, fault); + linked.push_back(0xbf068080u); + fallbacks.push_back(static_cast<std::uint32_t>(linked.size() * 4u)); + linked.push_back(original); + branch(0xbf820000u, back(count)); } linked.push_back(0xbf9f0000u); return {std::move(linked), std::move(fallbacks)}; From a1ae72a2e85f7358af3bf8681620540dc696ed86 Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:16:08 -0400 Subject: [PATCH 1347/1360] fix(shader): a 64-bit SALU result overwrites both dwords of a call link pair --- core/libs/tests/ScalarCall.cpp | 9 +++++++++ core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index abd7cc3d07..9cae3fedc9 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -44,6 +44,11 @@ static RdnaInstruction Sop1(std::uint32_t pc, std::uint32_t destination, std::ui return DecodeRdnaSop1(pc, code, 0u); } +static RdnaInstruction Sop2(std::uint32_t pc, std::uint32_t destination, std::uint32_t op, std::uint32_t source0, std::uint32_t source1) { + const std::array<std::uint32_t, 1> code{0x80000000u | (op << 23u) | (destination << 16u) | (source1 << 8u) | source0}; + return DecodeRdnaSop2(pc, code, 0u); +} + static RdnaInstruction Sopp(std::uint32_t pc, std::uint32_t op, std::uint32_t immediate = 0u) { const std::array<std::uint32_t, 1> code{0xbf800000u | (op << 16u) | immediate}; return DecodeRdnaSopp(pc, code, 0u); @@ -318,6 +323,10 @@ static void CheckTableCalls() { ExpectThrow("escapes the constant-offset call/return model", [] { Build({Sop1(0u, 0u, 3u, 8u), Call(4u, 8u, 2u), Sopp(8u, 2u, 0xfffdu), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)}); }); + static_cast<void>(Build({Call(0u, 8u, 3u), Sop2(4u, 8u, 0x15u, 0u, 2u), Sop2(8u, 10u, 0x1bu, 8u, 2u), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)})); + ExpectThrow("escapes the constant-offset call/return model", [] { + Build({Call(0u, 8u, 3u), Sop2(4u, 8u, 0x0eu, 0u, 2u), Sop2(8u, 10u, 0x1bu, 8u, 2u), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)}); + }); } enum class HoistedShape { InLoop, Hoisted, SecondWriterOnArm, LoadOnArmOnly, WriterAfterCall, WriterAfterLoop, CallOnArm, ConstantPointer, SkipsNullTarget, LongBranchWriter }; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 322fae20d5..71c11f0646 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -503,11 +503,9 @@ bool overwritesScalar(const RdnaInstruction& instruction, std::uint32_t reg, std case RdnaOpcode::SBitset1B64: case RdnaOpcode::SSwappcB64: case RdnaOpcode::SSetpcB64: return false; - case RdnaOpcode::SMovB64: - case RdnaOpcode::SGetpcB64: return first != NoScalarRegister && reg >= first && reg - first < 2u; - default: return first != NoScalarRegister && reg == first; + default: return first != NoScalarRegister && reg >= first && reg - first < std::max(instruction.dataDwordCount, 1u); } - case RdnaInstructionFamily::SOP2: return first != NoScalarRegister && reg == first; + case RdnaInstructionFamily::SOP2: return first != NoScalarRegister && reg >= first && reg - first < std::max(instruction.dataDwordCount, 1u); case RdnaInstructionFamily::SOPK: return instruction.op == RdnaOpcode::SMovkI32 && reg == first; case RdnaInstructionFamily::VOP1: case RdnaInstructionFamily::VOP2: From ef2b164b15be62b13e7d277ecd2bcc545c1409fc Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:41:37 -0400 Subject: [PATCH 1348/1360] fix(shader): relay later table calls past earlier calls' callee copies --- core/libs/tests/ScalarCall.cpp | 31 +++++++++++++++++++ .../ControlFlow/src/GraphBuilder.cpp | 24 +++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 9cae3fedc9..0faf175176 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -5,6 +5,7 @@ #include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" #include "ControlFlow/GraphBuilder.hpp" #include <array> +#include <cstdint> #include <cstdio> #include <algorithm> #include <initializer_list> @@ -306,6 +307,36 @@ static void CheckTableCalls() { const auto farCfg = BuildCode(farLinked.code, farSwappc); Require(farCfg.FindBlockByProgramCounter(farCall * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).id); Require(farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 1u) * 4u).id); + auto twoCallers = caller; + twoCallers.pop_back(); + twoCallers.insert(twoCallers.end(), caller.begin(), caller.end()); + const auto twoProgram = RdnaInstructionDecoder{}.Decode(twoCallers); + auto secondCall = calls[0]; + secondCall.programCounter += 40u; + const std::array<TableCall, 2> twoCalls{calls[0], secondCall}; + const std::vector<std::vector<TableCallee>> twoCallees{far[0], {{0x50ull, first}, {0x60ull, second}}}; + const auto twoLinked = LinkCalls(twoProgram, {}, {}, twoCalls, twoCallees); + const auto& two = twoLinked.code; + const auto follow = [&](std::size_t word, std::size_t& hops) { + hops = 0; + while (two[word] >> 16u == 0xbf82u) { + word = static_cast<std::size_t>(static_cast<std::int64_t>(word) + 1 + static_cast<std::int16_t>(two[word] & 0xffffu)); + ++hops; + } + return word; + }; + std::size_t hops = 0; + Require(follow(9u, hops) == 23u && hops == 1u && two[21] >> 16u == 0xbf82u && two[22] >> 16u == 0xbf82u); + Require(follow(39u, hops) == 10u && hops == 1u); + const auto secondChain = follow(19u, hops); + Require(hops == 6u && two[secondChain] == 0xbf850007u && two[secondChain + 1u] == 0xbf07ff0eu && two[secondChain + 2u] == 0x50u); + Require(two[secondChain + 15u] == 0xbb0e0001u && std::equal(first.begin(), first.end(), two.begin() + static_cast<std::ptrdiff_t>(secondChain + 17u))); + Require(follow(secondChain + 16u, hops) == 20u && hops == 6u); + SwappcInfo twoSwappc = swappc; + twoSwappc.linkedCallFallbacks = twoLinked.fallbacks; + const auto twoCfg = BuildCode(two, twoSwappc); + Require(twoCfg.FindBlockByProgramCounter(static_cast<std::uint32_t>(secondChain + 15u) * 4u).terminator.trueBlock == twoCfg.FindBlockByProgramCounter(static_cast<std::uint32_t>(secondChain + 17u) * 4u).id); + Require(twoCfg.unlinkedCallProgramCounters.size() == 2u); std::vector<std::uint32_t> oversized(0x8000u, 0xbf800000u); const std::vector<std::vector<TableCallee>> overflows{{{0x10ull, oversized}, {0x20ull, first}}}; ExpectThrow("out of the 16-bit branch range", [&] { static_cast<void>(LinkCalls(program, {}, {}, calls, overflows)); }); diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 71c11f0646..6eb1bae337 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1582,6 +1582,7 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span<const UserDataCall> linked[word] = 0xbb000000u | (userDataCalls[call].linkRegister << 16u) | static_cast<std::uint32_t>(offset); linked.insert(linked.end(), userDataCallees[call].begin(), userDataCallees[call].end()); } + std::vector<std::pair<std::size_t, std::size_t>> stations; for (std::size_t call = 0; call < tableCalls.size(); ++call) { const auto& table = tableCalls[call]; const auto& callees = tableCallees[call]; @@ -1589,14 +1590,28 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span<const UserDataCall> const auto at = "the table callees of the call at program counter " + toHexString(table.programCounter); const auto original = linked[word]; const auto count = callees.size(); + const auto relays = 2u * (tableCalls.size() - call - 1u); + std::vector<std::size_t> path; + for (const auto& [position, block] : stations) path.push_back(position + 2u * (call - block - 1u)); + const auto station = [&] { + if (relays == 0u) return; + stations.push_back({linked.size(), call}); + linked.insert(linked.end(), relays, 0u); + }; + station(); + const auto start = linked.size(); std::vector<std::size_t> segments(count + 1u); - segments[0] = linked.size() + 1u; - for (std::size_t callee = 0; callee < count; ++callee) segments[callee + 1u] = segments[callee] + 16u + callees[callee].code.size(); + segments[0] = start + 1u; + for (std::size_t callee = 0; callee < count; ++callee) segments[callee + 1u] = segments[callee] + 16u + callees[callee].code.size() + relays; const auto one = [&](std::size_t callee) { return segments[callee] + (callee < count ? 7u : 2u); }; - const auto back = [&](std::size_t callee) { return callee == 0u ? word + 1u : segments[callee - 1u] + 15u; }; + const auto back = [&](std::size_t callee) { return callee != 0u ? segments[callee - 1u] + 15u : path.empty() ? word + 1u : path.back() + 1u; }; const auto fault = segments[count] + 3u; const auto branch = [&](std::uint32_t opcode, std::size_t target) { linked.push_back(branchWord(opcode, linked.size(), target, at.c_str())); }; - linked[word] = branchWord(0xbf820000u, word, linked.size(), at.c_str()); + for (std::size_t hop = 0; hop < path.size(); ++hop) { + linked[path[hop]] = branchWord(0xbf820000u, path[hop], hop + 1u < path.size() ? path[hop + 1u] : start, at.c_str()); + linked[path[hop] + 1u] = branchWord(0xbf820000u, path[hop] + 1u, hop == 0u ? word + 1u : path[hop - 1u] + 1u, at.c_str()); + } + linked[word] = branchWord(0xbf820000u, word, path.empty() ? start : path.front(), at.c_str()); branch(0xbf850000u, one(0u)); for (std::size_t callee = 0; callee < count; ++callee) { for (const bool sccOne : {false, true}) { @@ -1611,6 +1626,7 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span<const UserDataCall> linked.push_back(0xbb000001u | (table.linkRegister << 16u)); branch(0xbf820000u, back(callee)); linked.insert(linked.end(), callees[callee].code.begin(), callees[callee].code.end()); + station(); } linked.push_back(0xbf078080u); branch(0xbf820000u, fault); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3d7bbfd474..cdd9bf48dc 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -216,7 +216,7 @@ - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. Each copy follows its own compares and call, so no branch spans more than one copy. A later call's branch to its chain and its return cross earlier calls' copies through a pair of `s_branch` relays before the first of those copies and after each one, so they don't span more than one copy either. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB, or a call more than 0x7fff dwords before the end of the program and its user-data callees throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. - [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. From b62b28831da14d585d8d2eccc62c5e6cae71582d Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:14:21 +0100 Subject: [PATCH 1349/1360] feat(libSceCesCs): add the GB18030-2000 and GB18030-2005 profiles Implement sceCesUcsProfileInitGb18030_2000 and sceCesUcsProfileInitGb18030_2005. Two-byte cells are the CP936 table plus the cells that differ, and four-byte sequences are runs over the BMP and the supplementary planes, generated from ICU's gb-18030-2000.ucm (before ICU-8427) and gb-18030-2005.ucm with ICU's GB 18030 ranges. The GB, MBC and MBCS string conversions accept both profiles and produce four-byte UTF-8 and surrogate pairs for supplementary characters. --- core/libs/prx/libSceCesCs/Export.cpp | 225 +++++++++++++--- core/libs/prx/libSceCesCs/Gb18030.hpp | 135 ++++++++++ core/libs/prx/libSceCesCs/PendingExports.cpp | 10 - core/libs/tests/GuestCesChinese.cpp | 260 ++++++++++++++++++- docs/dev/debt/libSceCesCs.md | 5 +- 5 files changed, 583 insertions(+), 52 deletions(-) create mode 100644 core/libs/prx/libSceCesCs/Gb18030.hpp diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 46e3aad1fe..0ba8d0ae27 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -4,6 +4,7 @@ #include <bit> #include <cstring> #include <iterator> +#include <span> #include <stdexcept> #include <utility> #include <vector> @@ -12,6 +13,7 @@ #include "JisX0208.hpp" #include "JisX0212.hpp" #include "Cp936.hpp" +#include "Gb18030.hpp" #include "Cp949.hpp" #include "Cp950.hpp" #include "SingleByte.hpp" @@ -141,10 +143,17 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { utf8[1] = static_cast<uint8_t>(0x80 | (code & 0x3F)); return 2; } - utf8[0] = static_cast<uint8_t>(0xE0 | code >> 12); - utf8[1] = static_cast<uint8_t>(0x80 | (code >> 6 & 0x3F)); - utf8[2] = static_cast<uint8_t>(0x80 | (code & 0x3F)); - return 3; + if (code < 0x10000) { + utf8[0] = static_cast<uint8_t>(0xE0 | code >> 12); + utf8[1] = static_cast<uint8_t>(0x80 | (code >> 6 & 0x3F)); + utf8[2] = static_cast<uint8_t>(0x80 | (code & 0x3F)); + return 3; + } + utf8[0] = static_cast<uint8_t>(0xF0 | code >> 18); + utf8[1] = static_cast<uint8_t>(0x80 | (code >> 12 & 0x3F)); + utf8[2] = static_cast<uint8_t>(0x80 | (code >> 6 & 0x3F)); + utf8[3] = static_cast<uint8_t>(0x80 | (code & 0x3F)); + return 4; } constexpr uint8_t EUC_JP_TAG[8] = {'E', 'U', 'C', 'J', 'P', '9', '3', '2'}; @@ -296,6 +305,125 @@ int encodeGb(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length return CES_ERROR_UNASSIGNED_CODE; } +constexpr uint8_t GB18030_2000_TAG[8] = {'G', 'B', '1', '8', '2', '0', '0', '0'}; +constexpr uint8_t GB18030_2005_TAG[8] = {'G', 'B', '1', '8', '2', '0', '0', '5'}; +constexpr uint32_t GB_CELLS = 126 * 190; + +struct Gb18030Tables { + std::span<const uint32_t[3]> twoByte; + std::span<const uint32_t[3]> fourByte; + std::span<const uint32_t[2]> fallback; +}; + +constexpr Gb18030Tables GB18030_2000{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, {}}; +constexpr Gb18030Tables GB18030_2005{Gb18030::TWO_BYTE_2005, Gb18030::FOUR_BYTE_2005, Gb18030::FALLBACK_2005}; + +const Gb18030Tables* gb18030Tables(const void* profile) { + if (profile == nullptr) return nullptr; + if (std::memcmp(profile, GB18030_2000_TAG, sizeof(GB18030_2000_TAG)) == 0) return &GB18030_2000; + if (std::memcmp(profile, GB18030_2005_TAG, sizeof(GB18030_2005_TAG)) == 0) return &GB18030_2005; + return nullptr; +} + +bool isGb18030(const void* profile) { + return gb18030Tables(profile) != nullptr; +} + +bool isGb(const void* profile) { + return isGbk(profile) || isGb18030(profile); +} + +const uint32_t* findRun(std::span<const uint32_t[3]> runs, uint32_t key) { + const auto next = std::upper_bound(runs.begin(), runs.end(), key, [](uint32_t value, const uint32_t (&run)[3]) { return value < run[0]; }); + if (next == runs.begin()) return nullptr; + const uint32_t* run = *std::prev(next); + return key - run[0] < run[2] ? run : nullptr; +} + +uint32_t gb18030Cell(const Gb18030Tables& tables, uint32_t cell) { + const uint32_t* run = findRun(tables.twoByte, cell); + return run ? run[1] + cell - run[0] : Cp936::TO_UCS2[cell]; +} + +void putGbCell(uint32_t cell, uint8_t* mbcs) { + const uint32_t column = cell % 190; + mbcs[0] = static_cast<uint8_t>(0x81 + cell / 190); + mbcs[1] = static_cast<uint8_t>(column < 0x3F ? 0x40 + column : 0x41 + column); +} + +int decodeGb18030(const Gb18030Tables& tables, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + const uint8_t lead = mbcs[0]; + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead == 0x80 || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t second = mbcs[1]; + if (second >= 0x30 && second <= 0x39) { + if (mbcsMax < 4) return CES_ERROR_SRC_BUFFER_END; + if (mbcs[2] < 0x81 || mbcs[2] == 0xFF || mbcs[3] < 0x30 || mbcs[3] > 0x39) return CES_ERROR_INVALID_ENCODE; + const uint32_t linear = (((lead - 0x81) * 10 + (second - 0x30)) * 126 + (mbcs[2] - 0x81)) * 10 + (mbcs[3] - 0x30); + const uint32_t* run = findRun(tables.fourByte, linear); + if (!run) return CES_ERROR_UNASSIGNED_CODE; + length = 4; + code = run[1] + linear - run[0]; + return 0; + } + if (second < 0x40 || second == 0x7F || second == 0xFF) return CES_ERROR_INVALID_ENCODE; + length = 2; + code = gb18030Cell(tables, (lead - 0x81) * 190 + (second < 0x7F ? second - 0x40 : second - 0x41)); + return 0; +} + +const std::vector<std::pair<uint32_t, uint16_t>>& gb18030Reverse(const Gb18030Tables& tables) { + const auto build = [](const Gb18030Tables& source) { + std::vector<std::pair<uint32_t, uint16_t>> entries; + entries.reserve(GB_CELLS); + for (uint32_t cell = 0; cell < GB_CELLS; ++cell) entries.emplace_back(gb18030Cell(source, cell), static_cast<uint16_t>(cell)); + std::sort(entries.begin(), entries.end()); + return entries; + }; + static const auto reverse2000 = build(GB18030_2000); + static const auto reverse2005 = build(GB18030_2005); + return &tables == &GB18030_2005 ? reverse2005 : reverse2000; +} + +int encodeGb18030(const Gb18030Tables& tables, uint32_t code, uint8_t* mbcs, uint32_t& length) { + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast<uint8_t>(code); + return 0; + } + const auto& reverse = gb18030Reverse(tables); + const auto found = std::lower_bound(reverse.begin(), reverse.end(), std::pair<uint32_t, uint16_t>(code, 0)); + if (found != reverse.end() && found->first == code) { + putGbCell(found->second, mbcs); + length = 2; + return 0; + } + for (const auto& run : tables.fourByte) { + if (code - run[1] >= run[2]) continue; + uint32_t linear = run[0] + code - run[1]; + mbcs[3] = static_cast<uint8_t>(0x30 + linear % 10); + linear /= 10; + mbcs[2] = static_cast<uint8_t>(0x81 + linear % 126); + linear /= 126; + mbcs[1] = static_cast<uint8_t>(0x30 + linear % 10); + mbcs[0] = static_cast<uint8_t>(0x81 + linear / 10); + length = 4; + return 0; + } + for (const auto& entry : tables.fallback) { + if (entry[0] != code) continue; + putGbCell(entry[1], mbcs); + length = 2; + return 0; + } + return CES_ERROR_UNASSIGNED_CODE; +} + bool sjisToJis(uint8_t lead, uint8_t trail, int& row, int& cell) { if (!((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC))) return false; if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return false; @@ -349,7 +477,7 @@ bool isBig5(const void* profile) { } bool isMbcs(const void* profile) { - return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucCn(profile) || isGbk(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); + return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucCn(profile) || isGb(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); } int decodeCp949(bool unified, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { @@ -444,6 +572,7 @@ int encodeDbcs(const std::vector<DbcsCell>& reverse, bool euc, uint32_t code, ui int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { if (isEucKr(profile) || isUhc(profile)) return decodeCp949(isUhc(profile), mbcs, mbcsMax, length, code); if (isBig5(profile)) return decodeCp950(mbcs, mbcsMax, length, code); + if (const Gb18030Tables* tables = gb18030Tables(profile)) return decodeGb18030(*tables, mbcs, mbcsMax, length, code); if (isEucCn(profile) || isGbk(profile)) return decodeGb(profile, mbcs, mbcsMax, length, code); const uint8_t lead = mbcs[0]; if (isSbc(profile)) { @@ -560,6 +689,7 @@ const std::vector<JisCell>& jisReverse(bool strict) { int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length) { if (isEucKr(profile) || isUhc(profile)) return encodeDbcs(cp949Reverse(), isEucKr(profile), code, mbcs, length); if (isBig5(profile)) return encodeDbcs(cp950Reverse(), false, code, mbcs, length); + if (const Gb18030Tables* tables = gb18030Tables(profile)) return encodeGb18030(*tables, code, mbcs, length); if (isEucCn(profile) || isGbk(profile)) return encodeGb(profile, code, mbcs, length); if (isSbc(profile)) { length = 1; @@ -625,10 +755,21 @@ int decodeUcs2(const uint16_t* ucs2, uint32_t, uint32_t& length, uint32_t& code) } uint32_t encodeUcs2(uint32_t code, uint16_t* ucs2) { + if (code > 0xFFFF) return 0; ucs2[0] = static_cast<uint16_t>(code); return 1; } +uint32_t encodeUtf16(uint32_t code, uint16_t* utf16) { + if (code < 0x10000) { + utf16[0] = static_cast<uint16_t>(code); + return 1; + } + utf16[0] = static_cast<uint16_t>(0xD800 | (code - 0x10000) >> 10); + utf16[1] = static_cast<uint16_t>(0xDC00 | (code & 0x3FF)); + return 2; +} + template<typename Source, typename Decode> int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { if (!context) return CES_ERROR_INVALID_PARAMETER; @@ -641,7 +782,7 @@ int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32 uint32_t length = 0; uint32_t code = 0; if (const int result = decode(source + read, sourceMax - read, length, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; uint32_t size = 0; if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; if (mbcs) { @@ -670,6 +811,7 @@ int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_ if (const int result = decodeMbcs(profile, mbcs + read, mbcsMax - read, length, code); result != 0) return result; Target encoded[4]; const uint32_t size = encode(code, encoded); + if (size == 0) return CES_ERROR_UNASSIGNED_CODE; if (target) { if (written + size > targetMax) return CES_ERROR_DST_BUFFER_END; std::memcpy(target + written, encoded, size * sizeof(Target)); @@ -690,7 +832,7 @@ int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t uint32_t consumed = 0; uint32_t code = 0; if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8, encoded, produced); @@ -746,6 +888,7 @@ int profiledToUcs2(bool (*matches)(const void*), const void* profile, const uint uint32_t length = 0; uint32_t code = 0; if (const int result = decodeProfiled(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return result; + if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; *ucs2 = static_cast<uint16_t>(code); if (sourceSize) *sourceSize = length; return 0; @@ -755,7 +898,7 @@ int profiledToUtf8(bool (*matches)(const void*), const void* profile, const uint uint32_t length = 0; uint32_t code = 0; if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf8, length, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8, encoded, produced); @@ -769,10 +912,16 @@ int profiledToUtf16(bool (*matches)(const void*), const void* profile, const uin uint32_t length = 0; uint32_t code = 0; if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf16, length, code); result != 0) return result; - if (utf16max < 1) return CES_ERROR_DST_BUFFER_END; - storeUnit<uint16_t, BigEndian>(utf16, code); + const uint32_t units = code < 0x10000 ? 1 : 2; + if (utf16max < units) return CES_ERROR_DST_BUFFER_END; + if (units == 1) { + storeUnit<uint16_t, BigEndian>(utf16, code); + } else { + storeUnit<uint16_t, BigEndian>(utf16, 0xD800 | (code - 0x10000) >> 10); + storeUnit<uint16_t, BigEndian>(utf16 + 1, 0xDC00 | (code & 0x3FF)); + } if (sourceSize) *sourceSize = length; - if (utf16Len) *utf16Len = 1; + if (utf16Len) *utf16Len = units; return 0; } @@ -787,7 +936,7 @@ int profiledToUtf32(bool (*matches)(const void*), const void* profile, const uin } int encodeProfiled(const void* profile, uint32_t code, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { - uint8_t encoded[3]; + uint8_t encoded[4]; uint32_t size = 0; if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; if (targetMax < size) return CES_ERROR_DST_BUFFER_END; @@ -1132,7 +1281,7 @@ int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8 if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t length = encodeUtf8(sbcToUnicode(profile, sbc), encoded); if (utf8max < length) return CES_ERROR_DST_BUFFER_END; for (uint32_t i = 0; i < length; ++i) utf8[i] = encoded[i]; @@ -1162,7 +1311,7 @@ int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, u uint32_t consumed = 0; uint32_t code = 0; if (const int result = decodeMbcs(profile, eucBuffer, eucMax, consumed, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8Buffer, encoded, produced); @@ -1487,11 +1636,11 @@ int APS5_VABI sceCesUtf16StrGetMbcsLen(void* context, const uint16_t* utf16, uin int APS5_VABI sceCesMbcsStrToUtf16Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { if (!utf16) return CES_ERROR_INVALID_DST_BUFFER; - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, encodeUcs2); + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, encodeUtf16); } int APS5_VABI sceCesMbcsStrGetUtf16Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf16Len) { - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast<uint16_t*>(nullptr), 0, utf16Len, encodeUcs2); + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast<uint16_t*>(nullptr), 0, utf16Len, encodeUtf16); } int APS5_VABI sceCesUtf32StrToMbcsStr(void* context, const uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { @@ -1524,6 +1673,18 @@ void* APS5_VABI sceCesUcsProfileInitGbkCp936(void* sheet) { return sheet; } +void* APS5_VABI sceCesUcsProfileInitGb18030_2000(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, GB18030_2000_TAG, sizeof(GB18030_2000_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitGb18030_2005(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, GB18030_2005_TAG, sizeof(GB18030_2005_TAG)); + return sheet; +} + int APS5_VABI sceCesEucCnToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { return profiledToUcs2(isEucCn, profile, eucBuffer, eucMax, eucSize, ucs2); } @@ -1589,67 +1750,67 @@ int APS5_VABI sceCesUtf32leToEucCn(const uint32_t* utf32, const void* profile, u } int APS5_VABI sceCesGbToUcs2(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* ucs2) { - return profiledToUcs2(isGbk, profile, gbBuffer, gbMax, gbSize, ucs2); + return profiledToUcs2(isGb, profile, gbBuffer, gbMax, gbSize, ucs2); } int APS5_VABI sceCesGbToUtf8(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { - return profiledToUtf8(isGbk, profile, gbBuffer, gbMax, gbSize, utf8Buffer, utf8Max, utf8Length); + return profiledToUtf8(isGb, profile, gbBuffer, gbMax, gbSize, utf8Buffer, utf8Max, utf8Length); } int APS5_VABI sceCesGbToUtf16(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return profiledToUtf16<NATIVE_BIG_ENDIAN>(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16<NATIVE_BIG_ENDIAN>(isGb, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesGbToUtf16be(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return profiledToUtf16<true>(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16<true>(isGb, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesGbToUtf16le(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return profiledToUtf16<false>(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16<false>(isGb, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesGbToUtf32(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { - return profiledToUtf32<NATIVE_BIG_ENDIAN>(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); + return profiledToUtf32<NATIVE_BIG_ENDIAN>(isGb, profile, gbBuffer, gbMax, gbSize, utf32); } int APS5_VABI sceCesGbToUtf32be(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { - return profiledToUtf32<true>(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); + return profiledToUtf32<true>(isGb, profile, gbBuffer, gbMax, gbSize, utf32); } int APS5_VABI sceCesGbToUtf32le(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { - return profiledToUtf32<false>(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); + return profiledToUtf32<false>(isGb, profile, gbBuffer, gbMax, gbSize, utf32); } int APS5_VABI sceCesUcs2ToGb(uint16_t ucs2, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return scalarToProfiled(isGbk, ucs2, profile, gbBuffer, gbMax, gbSize); + return scalarToProfiled(isGb, ucs2, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesUtf8ToGb(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeUtf8); + return bufferToProfiled(isGb, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeUtf8); } int APS5_VABI sceCesUtf16ToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16<NATIVE_BIG_ENDIAN>); + return bufferToProfiled(isGb, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16<NATIVE_BIG_ENDIAN>); } int APS5_VABI sceCesUtf16beToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16<true>); + return bufferToProfiled(isGb, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16<true>); } int APS5_VABI sceCesUtf16leToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16<false>); + return bufferToProfiled(isGb, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16<false>); } int APS5_VABI sceCesUtf32ToGb(uint32_t utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return scalarToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); + return scalarToProfiled(isGb, utf32, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesUtf32beToGb(const uint32_t* utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return utf32ToProfiled<true>(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); + return utf32ToProfiled<true>(isGb, utf32, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesUtf32leToGb(const uint32_t* utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return utf32ToProfiled<false>(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); + return utf32ToProfiled<false>(isGb, utf32, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesEucKrToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { diff --git a/core/libs/prx/libSceCesCs/Gb18030.hpp b/core/libs/prx/libSceCesCs/Gb18030.hpp new file mode 100644 index 0000000000..5f13d60813 --- /dev/null +++ b/core/libs/prx/libSceCesCs/Gb18030.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include <cstdint> + +namespace Gb18030 { + +inline constexpr uint32_t TWO_BYTE_2000[111][3] = { + {0x17C0, 0xE4C6, 0x60}, {0x187E, 0xE526, 0x60}, {0x18E8, 0xE766, 0x6}, {0x1920, 0x20AC, 0x1}, {0x1921, 0xE76D, 0x1}, {0x192C, 0xE76E, 0x2}, + {0x193A, 0xE770, 0x2}, {0x193C, 0xE586, 0x60}, {0x19FA, 0xE5E6, 0x60}, {0x1AAD, 0xE772, 0xB}, {0x1AB8, 0xE646, 0x60}, {0x1B6E, 0xE77D, 0x8}, + {0x1B76, 0xE6A6, 0x60}, {0x1BEE, 0xE785, 0x8}, {0x1C0E, 0xE78D, 0x7}, {0x1C21, 0xE794, 0x2}, {0x1C28, 0xE796, 0x1}, {0x1C2B, 0xE797, 0x9}, + {0x1C34, 0xE706, 0x60}, {0x1CB5, 0xE7A0, 0xF}, {0x1CE5, 0xE7AF, 0xD}, {0x1D47, 0xE7BC, 0xB}, {0x1D6D, 0xE7C7, 0x1}, {0x1D70, 0x1F9, 0x1}, + {0x1D72, 0xE7C9, 0x4}, {0x1D9B, 0xE7CD, 0x15}, {0x1DC8, 0xE7E2, 0x1}, {0x1DCB, 0xE7E3, 0x1}, {0x1DCD, 0xE7E4, 0x3}, {0x1DF8, 0x303E, 0x1}, + {0x1DF9, 0x2FF0, 0xC}, {0x1E06, 0xE7F4, 0xD}, {0x1E5F, 0xE801, 0xF}, {0x1ECE, 0xE000, 0x5E}, {0x1F8C, 0xE05E, 0x5E}, {0x204A, 0xE0BC, 0x5E}, + {0x2108, 0xE11A, 0x5E}, {0x21C6, 0xE178, 0x5E}, {0x2284, 0xE1D6, 0x5E}, {0x408D, 0xE810, 0x5}, {0x58B2, 0xE234, 0x5E}, {0x5970, 0xE292, 0x5E}, + {0x5A2E, 0xE2F0, 0x5E}, {0x5AEC, 0xE34E, 0x5E}, {0x5BAA, 0xE3AC, 0x5E}, {0x5C68, 0xE40A, 0x5E}, {0x5CD6, 0x2E81, 0x1}, {0x5CD7, 0xE816, 0x3}, + {0x5CDA, 0x2E84, 0x1}, {0x5CDB, 0x3473, 0x1}, {0x5CDC, 0x3447, 0x1}, {0x5CDD, 0x2E88, 0x1}, {0x5CDE, 0x2E8B, 0x1}, {0x5CDF, 0xE81E, 0x1}, + {0x5CE0, 0x359E, 0x1}, {0x5CE1, 0x361A, 0x1}, {0x5CE2, 0x360E, 0x1}, {0x5CE3, 0x2E8C, 0x1}, {0x5CE4, 0x2E97, 0x1}, {0x5CE5, 0x396E, 0x1}, + {0x5CE6, 0x3918, 0x1}, {0x5CE7, 0xE826, 0x1}, {0x5CE8, 0x39CF, 0x1}, {0x5CE9, 0x39DF, 0x1}, {0x5CEA, 0x3A73, 0x1}, {0x5CEB, 0x39D0, 0x1}, + {0x5CEC, 0xE82B, 0x2}, {0x5CEE, 0x3B4E, 0x1}, {0x5CEF, 0x3C6E, 0x1}, {0x5CF0, 0x3CE0, 0x1}, {0x5CF1, 0x2EA7, 0x1}, {0x5CF2, 0xE831, 0x2}, + {0x5CF4, 0x2EAA, 0x1}, {0x5CF5, 0x4056, 0x1}, {0x5CF6, 0x415F, 0x1}, {0x5CF7, 0x2EAE, 0x1}, {0x5CF8, 0x4337, 0x1}, {0x5CF9, 0x2EB3, 0x1}, + {0x5CFA, 0x2EB6, 0x2}, {0x5CFC, 0xE83B, 0x1}, {0x5CFD, 0x43B1, 0x1}, {0x5CFE, 0x43AC, 0x1}, {0x5CFF, 0x2EBB, 0x1}, {0x5D00, 0x43DD, 0x1}, + {0x5D01, 0x44D6, 0x1}, {0x5D02, 0x4661, 0x1}, {0x5D03, 0x464C, 0x1}, {0x5D04, 0xE843, 0x1}, {0x5D05, 0x4723, 0x1}, {0x5D06, 0x4729, 0x1}, + {0x5D07, 0x477C, 0x1}, {0x5D08, 0x478D, 0x1}, {0x5D09, 0x2ECA, 0x1}, {0x5D0A, 0x4947, 0x1}, {0x5D0B, 0x497A, 0x1}, {0x5D0C, 0x497D, 0x1}, + {0x5D0D, 0x4982, 0x2}, {0x5D0F, 0x4985, 0x2}, {0x5D11, 0x499F, 0x1}, {0x5D12, 0x499B, 0x1}, {0x5D13, 0x49B7, 0x1}, {0x5D14, 0x49B6, 0x1}, + {0x5D15, 0xE854, 0x2}, {0x5D17, 0x4CA3, 0x1}, {0x5D18, 0x4C9F, 0x3}, {0x5D1B, 0x4C77, 0x1}, {0x5D1C, 0x4CA2, 0x1}, {0x5D1D, 0x4D13, 0x7}, + {0x5D24, 0x4DAE, 0x1}, {0x5D25, 0xE864, 0x1}, {0x5D26, 0xE468, 0x5E}, +}; + +inline constexpr uint32_t FOUR_BYTE_2000[207][3] = { + {0x0, 0x80, 0x24}, {0x24, 0xA5, 0x2}, {0x26, 0xA9, 0x7}, {0x2D, 0xB2, 0x5}, {0x32, 0xB8, 0x1F}, {0x51, 0xD8, 0x8}, + {0x59, 0xE2, 0x6}, {0x5F, 0xEB, 0x1}, {0x60, 0xEE, 0x4}, {0x64, 0xF4, 0x3}, {0x67, 0xF8, 0x1}, {0x68, 0xFB, 0x1}, + {0x69, 0xFD, 0x4}, {0x6D, 0x102, 0x11}, {0x7E, 0x114, 0x7}, {0x85, 0x11C, 0xF}, {0x94, 0x12C, 0x18}, {0xAC, 0x145, 0x3}, + {0xAF, 0x149, 0x4}, {0xB3, 0x14E, 0x1D}, {0xD0, 0x16C, 0x62}, {0x132, 0x1CF, 0x1}, {0x133, 0x1D1, 0x1}, {0x134, 0x1D3, 0x1}, + {0x135, 0x1D5, 0x1}, {0x136, 0x1D7, 0x1}, {0x137, 0x1D9, 0x1}, {0x138, 0x1DB, 0x1}, {0x139, 0x1DD, 0x1C}, {0x155, 0x1FA, 0x57}, + {0x1AC, 0x252, 0xF}, {0x1BB, 0x262, 0x65}, {0x220, 0x2C8, 0x1}, {0x221, 0x2CC, 0xD}, {0x22E, 0x2DA, 0xB7}, {0x2E5, 0x3A2, 0x1}, + {0x2E6, 0x3AA, 0x7}, {0x2ED, 0x3C2, 0x1}, {0x2EE, 0x3CA, 0x37}, {0x325, 0x402, 0xE}, {0x333, 0x450, 0x1}, {0x334, 0x452, 0x1BBE}, + {0x1EF2, 0x2011, 0x2}, {0x1EF4, 0x2017, 0x1}, {0x1EF5, 0x201A, 0x2}, {0x1EF7, 0x201E, 0x7}, {0x1EFE, 0x2027, 0x9}, {0x1F07, 0x2031, 0x1}, + {0x1F08, 0x2034, 0x1}, {0x1F09, 0x2036, 0x5}, {0x1F0E, 0x203C, 0x70}, {0x1F7E, 0x20AD, 0x56}, {0x1FD4, 0x2104, 0x1}, {0x1FD5, 0x2106, 0x3}, + {0x1FD8, 0x210A, 0xC}, {0x1FE4, 0x2117, 0xA}, {0x1FEE, 0x2122, 0x3E}, {0x202C, 0x216C, 0x4}, {0x2030, 0x217A, 0x16}, {0x2046, 0x2194, 0x2}, + {0x2048, 0x219A, 0x6E}, {0x20B6, 0x2209, 0x6}, {0x20BC, 0x2210, 0x1}, {0x20BD, 0x2212, 0x3}, {0x20C0, 0x2216, 0x4}, {0x20C4, 0x221B, 0x2}, + {0x20C6, 0x2221, 0x2}, {0x20C8, 0x2224, 0x1}, {0x20C9, 0x2226, 0x1}, {0x20CA, 0x222C, 0x2}, {0x20CC, 0x222F, 0x5}, {0x20D1, 0x2238, 0x5}, + {0x20D6, 0x223E, 0xA}, {0x20E0, 0x2249, 0x3}, {0x20E3, 0x224D, 0x5}, {0x20E8, 0x2253, 0xD}, {0x20F5, 0x2262, 0x2}, {0x20F7, 0x2268, 0x6}, + {0x20FD, 0x2270, 0x25}, {0x2122, 0x2296, 0x3}, {0x2125, 0x229A, 0xB}, {0x2130, 0x22A6, 0x19}, {0x2149, 0x22C0, 0x52}, {0x219B, 0x2313, 0x14D}, + {0x22E8, 0x246A, 0xA}, {0x22F2, 0x249C, 0x64}, {0x2356, 0x254C, 0x4}, {0x235A, 0x2574, 0xD}, {0x2367, 0x2590, 0x3}, {0x236A, 0x2596, 0xA}, + {0x2374, 0x25A2, 0x10}, {0x2384, 0x25B4, 0x8}, {0x238C, 0x25BE, 0x8}, {0x2394, 0x25C8, 0x3}, {0x2397, 0x25CC, 0x2}, {0x2399, 0x25D0, 0x12}, + {0x23AB, 0x25E6, 0x1F}, {0x23CA, 0x2607, 0x2}, {0x23CC, 0x260A, 0x36}, {0x2402, 0x2641, 0x1}, {0x2403, 0x2643, 0x83E}, {0x2C41, 0x2E82, 0x2}, + {0x2C43, 0x2E85, 0x3}, {0x2C46, 0x2E89, 0x2}, {0x2C48, 0x2E8D, 0xA}, {0x2C52, 0x2E98, 0xF}, {0x2C61, 0x2EA8, 0x2}, {0x2C63, 0x2EAB, 0x3}, + {0x2C66, 0x2EAF, 0x4}, {0x2C6A, 0x2EB4, 0x2}, {0x2C6C, 0x2EB8, 0x3}, {0x2C6F, 0x2EBC, 0xE}, {0x2C7D, 0x2ECB, 0x125}, {0x2DA2, 0x2FFC, 0x4}, + {0x2DA6, 0x3004, 0x1}, {0x2DA7, 0x3018, 0x5}, {0x2DAC, 0x301F, 0x2}, {0x2DAE, 0x302A, 0x14}, {0x2DC2, 0x303F, 0x2}, {0x2DC4, 0x3094, 0x7}, + {0x2DCB, 0x309F, 0x2}, {0x2DCD, 0x30F7, 0x5}, {0x2DD2, 0x30FF, 0x6}, {0x2DD8, 0x312A, 0xF6}, {0x2ECE, 0x322A, 0x7}, {0x2ED5, 0x3232, 0x71}, + {0x2F46, 0x32A4, 0xEA}, {0x3030, 0x3390, 0xC}, {0x303C, 0x339F, 0x2}, {0x303E, 0x33A2, 0x22}, {0x3060, 0x33C5, 0x9}, {0x3069, 0x33CF, 0x2}, + {0x306B, 0x33D3, 0x2}, {0x306D, 0x33D6, 0x71}, {0x30DE, 0x3448, 0x2B}, {0x3109, 0x3474, 0x12A}, {0x3233, 0x359F, 0x6F}, {0x32A2, 0x360F, 0xB}, + {0x32AD, 0x361B, 0x2FD}, {0x35AA, 0x3919, 0x55}, {0x35FF, 0x396F, 0x60}, {0x365F, 0x39D1, 0xE}, {0x366D, 0x39E0, 0x93}, {0x3700, 0x3A74, 0xDA}, + {0x37DA, 0x3B4F, 0x11F}, {0x38F9, 0x3C6F, 0x71}, {0x396A, 0x3CE1, 0x375}, {0x3CDF, 0x4057, 0x108}, {0x3DE7, 0x4160, 0x1D7}, {0x3FBE, 0x4338, 0x74}, + {0x4032, 0x43AD, 0x4}, {0x4036, 0x43B2, 0x2B}, {0x4061, 0x43DE, 0xF8}, {0x4159, 0x44D7, 0x175}, {0x42CE, 0x464D, 0x14}, {0x42E2, 0x4662, 0xC1}, + {0x43A3, 0x4724, 0x5}, {0x43A8, 0x472A, 0x52}, {0x43FA, 0x477D, 0x10}, {0x440A, 0x478E, 0x1B9}, {0x45C3, 0x4948, 0x32}, {0x45F5, 0x497B, 0x2}, + {0x45F7, 0x497E, 0x4}, {0x45FB, 0x4984, 0x1}, {0x45FC, 0x4987, 0x14}, {0x4610, 0x499C, 0x3}, {0x4613, 0x49A0, 0x16}, {0x4629, 0x49B8, 0x2BF}, + {0x48E8, 0x4C78, 0x27}, {0x490F, 0x4CA4, 0x6F}, {0x497E, 0x4D1A, 0x94}, {0x4A12, 0x4DAF, 0x51}, {0x4A63, 0x9FA6, 0x385A}, {0x82BD, 0xE76C, 0x1}, + {0x82BE, 0xE7C8, 0x1}, {0x82BF, 0xE7E7, 0xD}, {0x82CC, 0xE815, 0x1}, {0x82CD, 0xE819, 0x5}, {0x82D2, 0xE81F, 0x7}, {0x82D9, 0xE827, 0x4}, + {0x82DD, 0xE82D, 0x4}, {0x82E1, 0xE833, 0x8}, {0x82E9, 0xE83C, 0x7}, {0x82F0, 0xE844, 0x10}, {0x8300, 0xE856, 0xE}, {0x830E, 0xE865, 0x10C7}, + {0x93D5, 0xF92D, 0x4C}, {0x9421, 0xF97A, 0x1B}, {0x943C, 0xF996, 0x51}, {0x948D, 0xF9E8, 0x9}, {0x9496, 0xF9F2, 0x1A}, {0x94B0, 0xFA10, 0x1}, + {0x94B1, 0xFA12, 0x1}, {0x94B2, 0xFA15, 0x3}, {0x94B5, 0xFA19, 0x6}, {0x94BB, 0xFA22, 0x1}, {0x94BC, 0xFA25, 0x2}, {0x94BE, 0xFA2A, 0x406}, + {0x98C4, 0xFE32, 0x1}, {0x98C5, 0xFE45, 0x4}, {0x98C9, 0xFE53, 0x1}, {0x98CA, 0xFE58, 0x1}, {0x98CB, 0xFE67, 0x1}, {0x98CC, 0xFE6C, 0x95}, + {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, +}; + +inline constexpr uint32_t TWO_BYTE_2005[118][3] = { + {0x17C0, 0xE4C6, 0x60}, {0x187E, 0xE526, 0x60}, {0x18E8, 0xE766, 0x6}, {0x1920, 0x20AC, 0x1}, {0x1921, 0xE76D, 0x1}, {0x192C, 0xE76E, 0x2}, + {0x193A, 0xE770, 0x2}, {0x193C, 0xE586, 0x60}, {0x19FA, 0xE5E6, 0x60}, {0x1AAD, 0xE772, 0xB}, {0x1AB8, 0xE646, 0x60}, {0x1B6E, 0xE77D, 0x8}, + {0x1B76, 0xE6A6, 0x60}, {0x1BEE, 0xE785, 0x8}, {0x1C0E, 0xFE10, 0x1}, {0x1C0F, 0xFE12, 0x1}, {0x1C10, 0xFE11, 0x1}, {0x1C11, 0xFE13, 0x4}, + {0x1C21, 0xFE17, 0x2}, {0x1C28, 0xFE19, 0x1}, {0x1C2B, 0xE797, 0x9}, {0x1C34, 0xE706, 0x60}, {0x1CB5, 0xE7A0, 0xF}, {0x1CE5, 0xE7AF, 0xD}, + {0x1D47, 0xE7BC, 0xB}, {0x1D6D, 0x1E3F, 0x1}, {0x1D70, 0x1F9, 0x1}, {0x1D72, 0xE7C9, 0x4}, {0x1D9B, 0xE7CD, 0x15}, {0x1DC8, 0xE7E2, 0x1}, + {0x1DCB, 0xE7E3, 0x1}, {0x1DCD, 0xE7E4, 0x3}, {0x1DF8, 0x303E, 0x1}, {0x1DF9, 0x2FF0, 0xC}, {0x1E06, 0xE7F4, 0xD}, {0x1E5F, 0xE801, 0xF}, + {0x1ECE, 0xE000, 0x5E}, {0x1F8C, 0xE05E, 0x5E}, {0x204A, 0xE0BC, 0x5E}, {0x2108, 0xE11A, 0x5E}, {0x21C6, 0xE178, 0x5E}, {0x2284, 0xE1D6, 0x5E}, + {0x408D, 0xE810, 0x5}, {0x58B2, 0xE234, 0x5E}, {0x5970, 0xE292, 0x5E}, {0x5A2E, 0xE2F0, 0x5E}, {0x5AEC, 0xE34E, 0x5E}, {0x5BAA, 0xE3AC, 0x5E}, + {0x5C68, 0xE40A, 0x5E}, {0x5CD6, 0x2E81, 0x1}, {0x5CD7, 0x20087, 0x1}, {0x5CD8, 0x20089, 0x1}, {0x5CD9, 0x200CC, 0x1}, {0x5CDA, 0x2E84, 0x1}, + {0x5CDB, 0x3473, 0x1}, {0x5CDC, 0x3447, 0x1}, {0x5CDD, 0x2E88, 0x1}, {0x5CDE, 0x2E8B, 0x1}, {0x5CDF, 0x9FB4, 0x1}, {0x5CE0, 0x359E, 0x1}, + {0x5CE1, 0x361A, 0x1}, {0x5CE2, 0x360E, 0x1}, {0x5CE3, 0x2E8C, 0x1}, {0x5CE4, 0x2E97, 0x1}, {0x5CE5, 0x396E, 0x1}, {0x5CE6, 0x3918, 0x1}, + {0x5CE7, 0x9FB5, 0x1}, {0x5CE8, 0x39CF, 0x1}, {0x5CE9, 0x39DF, 0x1}, {0x5CEA, 0x3A73, 0x1}, {0x5CEB, 0x39D0, 0x1}, {0x5CEC, 0x9FB6, 0x2}, + {0x5CEE, 0x3B4E, 0x1}, {0x5CEF, 0x3C6E, 0x1}, {0x5CF0, 0x3CE0, 0x1}, {0x5CF1, 0x2EA7, 0x1}, {0x5CF2, 0x215D7, 0x1}, {0x5CF3, 0x9FB8, 0x1}, + {0x5CF4, 0x2EAA, 0x1}, {0x5CF5, 0x4056, 0x1}, {0x5CF6, 0x415F, 0x1}, {0x5CF7, 0x2EAE, 0x1}, {0x5CF8, 0x4337, 0x1}, {0x5CF9, 0x2EB3, 0x1}, + {0x5CFA, 0x2EB6, 0x2}, {0x5CFC, 0x2298F, 0x1}, {0x5CFD, 0x43B1, 0x1}, {0x5CFE, 0x43AC, 0x1}, {0x5CFF, 0x2EBB, 0x1}, {0x5D00, 0x43DD, 0x1}, + {0x5D01, 0x44D6, 0x1}, {0x5D02, 0x4661, 0x1}, {0x5D03, 0x464C, 0x1}, {0x5D04, 0x9FB9, 0x1}, {0x5D05, 0x4723, 0x1}, {0x5D06, 0x4729, 0x1}, + {0x5D07, 0x477C, 0x1}, {0x5D08, 0x478D, 0x1}, {0x5D09, 0x2ECA, 0x1}, {0x5D0A, 0x4947, 0x1}, {0x5D0B, 0x497A, 0x1}, {0x5D0C, 0x497D, 0x1}, + {0x5D0D, 0x4982, 0x2}, {0x5D0F, 0x4985, 0x2}, {0x5D11, 0x499F, 0x1}, {0x5D12, 0x499B, 0x1}, {0x5D13, 0x49B7, 0x1}, {0x5D14, 0x49B6, 0x1}, + {0x5D15, 0x9FBA, 0x1}, {0x5D16, 0x241FE, 0x1}, {0x5D17, 0x4CA3, 0x1}, {0x5D18, 0x4C9F, 0x3}, {0x5D1B, 0x4C77, 0x1}, {0x5D1C, 0x4CA2, 0x1}, + {0x5D1D, 0x4D13, 0x7}, {0x5D24, 0x4DAE, 0x1}, {0x5D25, 0x9FBB, 0x1}, {0x5D26, 0xE468, 0x5E}, +}; + +inline constexpr uint32_t FOUR_BYTE_2005[209][3] = { + {0x0, 0x80, 0x24}, {0x24, 0xA5, 0x2}, {0x26, 0xA9, 0x7}, {0x2D, 0xB2, 0x5}, {0x32, 0xB8, 0x1F}, {0x51, 0xD8, 0x8}, + {0x59, 0xE2, 0x6}, {0x5F, 0xEB, 0x1}, {0x60, 0xEE, 0x4}, {0x64, 0xF4, 0x3}, {0x67, 0xF8, 0x1}, {0x68, 0xFB, 0x1}, + {0x69, 0xFD, 0x4}, {0x6D, 0x102, 0x11}, {0x7E, 0x114, 0x7}, {0x85, 0x11C, 0xF}, {0x94, 0x12C, 0x18}, {0xAC, 0x145, 0x3}, + {0xAF, 0x149, 0x4}, {0xB3, 0x14E, 0x1D}, {0xD0, 0x16C, 0x62}, {0x132, 0x1CF, 0x1}, {0x133, 0x1D1, 0x1}, {0x134, 0x1D3, 0x1}, + {0x135, 0x1D5, 0x1}, {0x136, 0x1D7, 0x1}, {0x137, 0x1D9, 0x1}, {0x138, 0x1DB, 0x1}, {0x139, 0x1DD, 0x1C}, {0x155, 0x1FA, 0x57}, + {0x1AC, 0x252, 0xF}, {0x1BB, 0x262, 0x65}, {0x220, 0x2C8, 0x1}, {0x221, 0x2CC, 0xD}, {0x22E, 0x2DA, 0xB7}, {0x2E5, 0x3A2, 0x1}, + {0x2E6, 0x3AA, 0x7}, {0x2ED, 0x3C2, 0x1}, {0x2EE, 0x3CA, 0x37}, {0x325, 0x402, 0xE}, {0x333, 0x450, 0x1}, {0x334, 0x452, 0x19ED}, + {0x1D21, 0xE7C7, 0x1}, {0x1D22, 0x1E40, 0x1D0}, {0x1EF2, 0x2011, 0x2}, {0x1EF4, 0x2017, 0x1}, {0x1EF5, 0x201A, 0x2}, {0x1EF7, 0x201E, 0x7}, + {0x1EFE, 0x2027, 0x9}, {0x1F07, 0x2031, 0x1}, {0x1F08, 0x2034, 0x1}, {0x1F09, 0x2036, 0x5}, {0x1F0E, 0x203C, 0x70}, {0x1F7E, 0x20AD, 0x56}, + {0x1FD4, 0x2104, 0x1}, {0x1FD5, 0x2106, 0x3}, {0x1FD8, 0x210A, 0xC}, {0x1FE4, 0x2117, 0xA}, {0x1FEE, 0x2122, 0x3E}, {0x202C, 0x216C, 0x4}, + {0x2030, 0x217A, 0x16}, {0x2046, 0x2194, 0x2}, {0x2048, 0x219A, 0x6E}, {0x20B6, 0x2209, 0x6}, {0x20BC, 0x2210, 0x1}, {0x20BD, 0x2212, 0x3}, + {0x20C0, 0x2216, 0x4}, {0x20C4, 0x221B, 0x2}, {0x20C6, 0x2221, 0x2}, {0x20C8, 0x2224, 0x1}, {0x20C9, 0x2226, 0x1}, {0x20CA, 0x222C, 0x2}, + {0x20CC, 0x222F, 0x5}, {0x20D1, 0x2238, 0x5}, {0x20D6, 0x223E, 0xA}, {0x20E0, 0x2249, 0x3}, {0x20E3, 0x224D, 0x5}, {0x20E8, 0x2253, 0xD}, + {0x20F5, 0x2262, 0x2}, {0x20F7, 0x2268, 0x6}, {0x20FD, 0x2270, 0x25}, {0x2122, 0x2296, 0x3}, {0x2125, 0x229A, 0xB}, {0x2130, 0x22A6, 0x19}, + {0x2149, 0x22C0, 0x52}, {0x219B, 0x2313, 0x14D}, {0x22E8, 0x246A, 0xA}, {0x22F2, 0x249C, 0x64}, {0x2356, 0x254C, 0x4}, {0x235A, 0x2574, 0xD}, + {0x2367, 0x2590, 0x3}, {0x236A, 0x2596, 0xA}, {0x2374, 0x25A2, 0x10}, {0x2384, 0x25B4, 0x8}, {0x238C, 0x25BE, 0x8}, {0x2394, 0x25C8, 0x3}, + {0x2397, 0x25CC, 0x2}, {0x2399, 0x25D0, 0x12}, {0x23AB, 0x25E6, 0x1F}, {0x23CA, 0x2607, 0x2}, {0x23CC, 0x260A, 0x36}, {0x2402, 0x2641, 0x1}, + {0x2403, 0x2643, 0x83E}, {0x2C41, 0x2E82, 0x2}, {0x2C43, 0x2E85, 0x3}, {0x2C46, 0x2E89, 0x2}, {0x2C48, 0x2E8D, 0xA}, {0x2C52, 0x2E98, 0xF}, + {0x2C61, 0x2EA8, 0x2}, {0x2C63, 0x2EAB, 0x3}, {0x2C66, 0x2EAF, 0x4}, {0x2C6A, 0x2EB4, 0x2}, {0x2C6C, 0x2EB8, 0x3}, {0x2C6F, 0x2EBC, 0xE}, + {0x2C7D, 0x2ECB, 0x125}, {0x2DA2, 0x2FFC, 0x4}, {0x2DA6, 0x3004, 0x1}, {0x2DA7, 0x3018, 0x5}, {0x2DAC, 0x301F, 0x2}, {0x2DAE, 0x302A, 0x14}, + {0x2DC2, 0x303F, 0x2}, {0x2DC4, 0x3094, 0x7}, {0x2DCB, 0x309F, 0x2}, {0x2DCD, 0x30F7, 0x5}, {0x2DD2, 0x30FF, 0x6}, {0x2DD8, 0x312A, 0xF6}, + {0x2ECE, 0x322A, 0x7}, {0x2ED5, 0x3232, 0x71}, {0x2F46, 0x32A4, 0xEA}, {0x3030, 0x3390, 0xC}, {0x303C, 0x339F, 0x2}, {0x303E, 0x33A2, 0x22}, + {0x3060, 0x33C5, 0x9}, {0x3069, 0x33CF, 0x2}, {0x306B, 0x33D3, 0x2}, {0x306D, 0x33D6, 0x71}, {0x30DE, 0x3448, 0x2B}, {0x3109, 0x3474, 0x12A}, + {0x3233, 0x359F, 0x6F}, {0x32A2, 0x360F, 0xB}, {0x32AD, 0x361B, 0x2FD}, {0x35AA, 0x3919, 0x55}, {0x35FF, 0x396F, 0x60}, {0x365F, 0x39D1, 0xE}, + {0x366D, 0x39E0, 0x93}, {0x3700, 0x3A74, 0xDA}, {0x37DA, 0x3B4F, 0x11F}, {0x38F9, 0x3C6F, 0x71}, {0x396A, 0x3CE1, 0x375}, {0x3CDF, 0x4057, 0x108}, + {0x3DE7, 0x4160, 0x1D7}, {0x3FBE, 0x4338, 0x74}, {0x4032, 0x43AD, 0x4}, {0x4036, 0x43B2, 0x2B}, {0x4061, 0x43DE, 0xF8}, {0x4159, 0x44D7, 0x175}, + {0x42CE, 0x464D, 0x14}, {0x42E2, 0x4662, 0xC1}, {0x43A3, 0x4724, 0x5}, {0x43A8, 0x472A, 0x52}, {0x43FA, 0x477D, 0x10}, {0x440A, 0x478E, 0x1B9}, + {0x45C3, 0x4948, 0x32}, {0x45F5, 0x497B, 0x2}, {0x45F7, 0x497E, 0x4}, {0x45FB, 0x4984, 0x1}, {0x45FC, 0x4987, 0x14}, {0x4610, 0x499C, 0x3}, + {0x4613, 0x49A0, 0x16}, {0x4629, 0x49B8, 0x2BF}, {0x48E8, 0x4C78, 0x27}, {0x490F, 0x4CA4, 0x6F}, {0x497E, 0x4D1A, 0x94}, {0x4A12, 0x4DAF, 0x51}, + {0x4A63, 0x9FA6, 0x385A}, {0x82BD, 0xE76C, 0x1}, {0x82BE, 0xE7C8, 0x1}, {0x82BF, 0xE7E7, 0xD}, {0x82CC, 0xE815, 0x1}, {0x82CD, 0xE819, 0x5}, + {0x82D2, 0xE81F, 0x7}, {0x82D9, 0xE827, 0x4}, {0x82DD, 0xE82D, 0x4}, {0x82E1, 0xE833, 0x8}, {0x82E9, 0xE83C, 0x7}, {0x82F0, 0xE844, 0x10}, + {0x8300, 0xE856, 0xE}, {0x830E, 0xE865, 0x10C7}, {0x93D5, 0xF92D, 0x4C}, {0x9421, 0xF97A, 0x1B}, {0x943C, 0xF996, 0x51}, {0x948D, 0xF9E8, 0x9}, + {0x9496, 0xF9F2, 0x1A}, {0x94B0, 0xFA10, 0x1}, {0x94B1, 0xFA12, 0x1}, {0x94B2, 0xFA15, 0x3}, {0x94B5, 0xFA19, 0x6}, {0x94BB, 0xFA22, 0x1}, + {0x94BC, 0xFA25, 0x2}, {0x94BE, 0xFA2A, 0x406}, {0x98C4, 0xFE32, 0x1}, {0x98C5, 0xFE45, 0x4}, {0x98C9, 0xFE53, 0x1}, {0x98CA, 0xFE58, 0x1}, + {0x98CB, 0xFE67, 0x1}, {0x98CC, 0xFE6C, 0x95}, {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, +}; + +inline constexpr uint32_t FALLBACK_2005[24][2] = { + {0xE78D, 0x1C0E}, {0xE78E, 0x1C0F}, {0xE78F, 0x1C10}, {0xE790, 0x1C11}, {0xE791, 0x1C12}, {0xE792, 0x1C13}, + {0xE793, 0x1C14}, {0xE794, 0x1C21}, {0xE795, 0x1C22}, {0xE796, 0x1C28}, {0xE816, 0x5CD7}, {0xE817, 0x5CD8}, + {0xE818, 0x5CD9}, {0xE81E, 0x5CDF}, {0xE826, 0x5CE7}, {0xE82B, 0x5CEC}, {0xE82C, 0x5CED}, {0xE831, 0x5CF2}, + {0xE832, 0x5CF3}, {0xE83B, 0x5CFC}, {0xE843, 0x5D04}, {0xE854, 0x5D15}, {0xE855, 0x5D16}, {0xE864, 0x5D25}, +}; + +} diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 3680b257df..1c38e00eed 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -257,16 +257,6 @@ int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitGb18030_2000(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUcsProfileInitGb18030_2005(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index 8e4f16cf5a..5c3b00ea29 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -25,21 +25,254 @@ int APS5_VABI sceCesUtf32ToEucCn(std::uint32_t, const void*, std::uint8_t*, std: int APS5_VABI sceCesUtf32beToGb(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +void* APS5_VABI sceCesUcsProfileInitGb18030_2000(void*); +void* APS5_VABI sceCesUcsProfileInitGb18030_2005(void*); +int APS5_VABI sceCesGbToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesGbToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUtf16ToGb(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToGb(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesMbcToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf16Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrGetUtf16Len(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUtf16StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast<const std::uint8_t*>(text); } -int main() { - constexpr int invalidParameter = static_cast<int>(0x805C0001); - constexpr int invalidSrcBuffer = static_cast<int>(0x805C0010); - constexpr int srcBufferEnd = static_cast<int>(0x805C0011); - constexpr int invalidEncode = static_cast<int>(0x805C0014); - constexpr int unassignedCode = static_cast<int>(0x805C0020); - constexpr int invalidDstBuffer = static_cast<int>(0x805C0030); - constexpr int dstBufferEnd = static_cast<int>(0x805C0031); +static constexpr int invalidParameter = static_cast<int>(0x805C0001); +static constexpr int invalidSrcBuffer = static_cast<int>(0x805C0010); +static constexpr int srcBufferEnd = static_cast<int>(0x805C0011); +static constexpr int invalidEncode = static_cast<int>(0x805C0014); +static constexpr int unassignedCode = static_cast<int>(0x805C0020); +static constexpr int invalidDstBuffer = static_cast<int>(0x805C0030); +static constexpr int dstBufferEnd = static_cast<int>(0x805C0031); +struct Gb18030Sequence { + std::uint32_t bytes; + std::uint32_t code2000; + std::uint32_t code2005; +}; + +struct Gb18030Difference { + std::uint32_t bytes; + std::uint32_t code2000; + std::uint32_t code2005; + bool fallback; +}; + +static constexpr Gb18030Sequence GB18030_BOUNDARIES[88] = { + {0x81308130, 0x80, 0x80}, {0x8130D239, 0x450, 0x450}, {0x8130D330, 0x452, 0x452}, {0x8135F436, 0x1E3E, 0x1E3E}, + {0x8135F437, 0x1E3F, 0xE7C7}, {0x8135F438, 0x1E40, 0x1E40}, {0x8136A531, 0x200F, 0x200F}, {0x8136A532, 0x2011, 0x2011}, + {0x8137A838, 0x2641, 0x2641}, {0x8137A839, 0x2643, 0x2643}, {0x8138FD38, 0x2E80, 0x2E80}, {0x8138FD39, 0x2E82, 0x2E82}, + {0x8230A632, 0x3619, 0x3619}, {0x8230A633, 0x361B, 0x361B}, {0x8230F237, 0x3917, 0x3917}, {0x8230F238, 0x3919, 0x3919}, + {0x8231D437, 0x3CDF, 0x3CDF}, {0x8231D438, 0x3CE1, 0x3CE1}, {0x8232AF32, 0x4055, 0x4055}, {0x8232AF33, 0x4057, 0x4057}, + {0x8232C936, 0x415E, 0x415E}, {0x8232C937, 0x4160, 0x4160}, {0x8232F837, 0x4336, 0x4336}, {0x8232F838, 0x4338, 0x4338}, + {0x8233A338, 0x44D5, 0x44D5}, {0x8233A339, 0x44D7, 0x44D7}, {0x8233C931, 0x464B, 0x464B}, {0x8233C932, 0x464D, 0x464D}, + {0x8233E837, 0x478C, 0x478C}, {0x8233E838, 0x478E, 0x478E}, {0x82349638, 0x4946, 0x4946}, {0x82349639, 0x4948, 0x4948}, + {0x8234A130, 0x49B5, 0x49B5}, {0x8234A131, 0x49B8, 0x49B8}, {0x8234E733, 0x4C76, 0x4C76}, {0x8234E734, 0x4C78, 0x4C78}, + {0x82358F32, 0x4DFF, 0x4DFF}, {0x82358F33, 0x9FA6, 0x9FA6}, {0x8336C738, 0xD7FF, 0xD7FF}, {0x8336C739, 0xE76C, 0xE76C}, + {0x8336CF39, 0xE863, 0xE863}, {0x8336D030, 0xE865, 0xE865}, {0x84308534, 0xF92B, 0xF92B}, {0x84308535, 0xF92D, 0xF92D}, + {0x84309C37, 0xFA26, 0xFA26}, {0x84309C38, 0xFA2A, 0xFA2A}, {0x84318537, 0xFE2F, 0xFE2F}, {0x84318538, 0xFE32, 0xFE32}, + {0x8431A233, 0xFFDF, 0xFFDF}, {0x8431A234, 0xFFE6, 0xFFE6}, {0x8431A439, 0xFFFF, 0xFFFF}, {0x8431A530, 0x0, 0x0}, + {0x8F39FE39, 0x0, 0x0}, {0x90308130, 0x10000, 0x10000}, {0x95328235, 0x1FFFF, 0x1FFFF}, {0x95328236, 0x20000, 0x20000}, + {0x9A348431, 0x2FFFF, 0x2FFFF}, {0x9A348432, 0x30000, 0x30000}, {0x9F368537, 0x3FFFF, 0x3FFFF}, {0x9F368538, 0x40000, 0x40000}, + {0xA4388733, 0x4FFFF, 0x4FFFF}, {0xA4388734, 0x50000, 0x50000}, {0xAA308839, 0x5FFFF, 0x5FFFF}, {0xAA308930, 0x60000, 0x60000}, + {0xAF328A35, 0x6FFFF, 0x6FFFF}, {0xAF328A36, 0x70000, 0x70000}, {0xB4348C31, 0x7FFFF, 0x7FFFF}, {0xB4348C32, 0x80000, 0x80000}, + {0xB9368D37, 0x8FFFF, 0x8FFFF}, {0xB9368D38, 0x90000, 0x90000}, {0xBE388F33, 0x9FFFF, 0x9FFFF}, {0xBE388F34, 0xA0000, 0xA0000}, + {0xC4309039, 0xAFFFF, 0xAFFFF}, {0xC4309130, 0xB0000, 0xB0000}, {0xC9329235, 0xBFFFF, 0xBFFFF}, {0xC9329236, 0xC0000, 0xC0000}, + {0xCE349431, 0xCFFFF, 0xCFFFF}, {0xCE349432, 0xD0000, 0xD0000}, {0xD3369537, 0xDFFFF, 0xDFFFF}, {0xD3369538, 0xE0000, 0xE0000}, + {0xD8389733, 0xEFFFF, 0xEFFFF}, {0xD8389734, 0xF0000, 0xF0000}, {0xDE309839, 0xFFFFF, 0xFFFFF}, {0xDE309930, 0x100000, 0x100000}, + {0xE3329A35, 0x10FFFF, 0x10FFFF}, {0xE3329A36, 0x0, 0x0}, {0xFD308130, 0x0, 0x0}, {0xFE39FE39, 0x0, 0x0}, +}; +static constexpr Gb18030Difference GB18030_DIFFERENCES[26] = { + {0xA6D9, 0xE78D, 0xFE10, true}, {0xA6DA, 0xE78E, 0xFE12, true}, {0xA6DB, 0xE78F, 0xFE11, true}, {0xA6DC, 0xE790, 0xFE13, true}, + {0xA6DD, 0xE791, 0xFE14, true}, {0xA6DE, 0xE792, 0xFE15, true}, {0xA6DF, 0xE793, 0xFE16, true}, {0xA6EC, 0xE794, 0xFE17, true}, + {0xA6ED, 0xE795, 0xFE18, true}, {0xA6F3, 0xE796, 0xFE19, true}, {0xA8BC, 0xE7C7, 0x1E3F, false}, {0xFE51, 0xE816, 0x20087, true}, + {0xFE52, 0xE817, 0x20089, true}, {0xFE53, 0xE818, 0x200CC, true}, {0xFE59, 0xE81E, 0x9FB4, true}, {0xFE61, 0xE826, 0x9FB5, true}, + {0xFE66, 0xE82B, 0x9FB6, true}, {0xFE67, 0xE82C, 0x9FB7, true}, {0xFE6C, 0xE831, 0x215D7, true}, {0xFE6D, 0xE832, 0x9FB8, true}, + {0xFE76, 0xE83B, 0x2298F, true}, {0xFE7E, 0xE843, 0x9FB9, true}, {0xFE90, 0xE854, 0x9FBA, true}, {0xFE91, 0xE855, 0x241FE, true}, + {0xFEA0, 0xE864, 0x9FBB, true}, {0x8135F437, 0x1E3F, 0xE7C7, false}, +}; +static std::uint32_t Gb18030Bytes(std::uint32_t linear) { + return (0x81 + linear / 12600) << 24 | (0x30 + linear / 1260 % 10) << 16 | (0x81 + linear / 10 % 126) << 8 | (0x30 + linear % 10); +} + +static void Fnv(std::uint32_t& hash, std::uint32_t value) { + for (int i = 0; i < 4; ++i) { + hash ^= (value >> (8 * i)) & 0xFF; + hash *= 0x01000193; + } +} + +static std::uint32_t Gb18030Decode(const void* gb, std::uint32_t packed) { + std::uint8_t bytes[4] = {}; + const std::uint32_t size = packed > 0xFFFF ? 4 : 2; + for (std::uint32_t i = 0; i < size; ++i) bytes[i] = static_cast<std::uint8_t>(packed >> (8 * (size - 1 - i))); + std::uint32_t consumed = 0x55; + std::uint32_t code = 0xDEADBEEF; + const int result = sceCesGbToUtf32(gb, bytes, size, &consumed, &code); + if (result != 0) { + Require(result == unassignedCode && consumed == 0x55 && code == 0xDEADBEEF); + return 0; + } + Require(consumed == size); + return code; +} + +static std::uint32_t Gb18030Encode(const void* gb, std::uint32_t code) { + std::uint8_t bytes[4] = {}; + std::uint32_t size = 0; + Require(sceCesUtf32ToGb(code, gb, bytes, sizeof(bytes), &size) == 0 && (size == 2 || size == 4)); + std::uint32_t packed = 0; + for (std::uint32_t i = 0; i < size; ++i) packed = packed << 8 | bytes[i]; + return packed; +} + +static void RequireGb18030Error(const void* gb, const char* bytes, std::uint32_t size, int error) { + std::uint32_t consumed = 0x55; + std::uint32_t utf32 = 0xDEADBEEF; + Require(sceCesGbToUtf32(gb, Bytes(bytes), size, &consumed, &utf32) == error && consumed == 0x55 && utf32 == 0xDEADBEEF); + std::uint16_t ucs2 = 0xBEEF; + Require(sceCesMbcToUcs2(gb, Bytes(bytes), size, &consumed, &ucs2) == error && consumed == 0x55 && ucs2 == 0xBEEF); +} + +static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByteHash, std::uint32_t fourByteHash, std::uint32_t encodeHash, int fourByteOneWay, int codeOneWay) { + std::uint32_t hash = 0x811C9DC5; + for (std::uint32_t cell = 0; cell < 126 * 190; ++cell) { + const std::uint32_t column = cell % 190; + const std::uint32_t bytes = (0x81 + cell / 190) << 8 | (column < 0x3F ? 0x40 + column : 0x41 + column); + const std::uint32_t code = Gb18030Decode(gb, bytes); + Require(code != 0 && Gb18030Encode(gb, code) == bytes); + Fnv(hash, code); + } + Require(hash == twoByteHash); + + hash = 0x811C9DC5; + int oneWay = 0; + for (std::uint32_t linear = 0; linear < 39420; ++linear) { + const std::uint32_t bytes = Gb18030Bytes(linear); + const std::uint32_t code = Gb18030Decode(gb, bytes); + Require(code >= 0x80 && code <= 0xFFFF); + const std::uint32_t back = Gb18030Encode(gb, code); + if (back != bytes) { + Require(back <= 0xFFFF && Gb18030Decode(gb, back) == code); + ++oneWay; + } + Fnv(hash, code); + } + Require(hash == fourByteHash && oneWay == fourByteOneWay); + + hash = 0x811C9DC5; + oneWay = 0; + for (std::uint32_t code = 0x80; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + const std::uint32_t bytes = Gb18030Encode(gb, code); + if (Gb18030Decode(gb, bytes) != code) ++oneWay; + Fnv(hash, bytes); + } + Require(hash == encodeHash && oneWay == codeOneWay); + + hash = 0x811C9DC5; + int samples = 0; + for (std::uint32_t linear = 189000; linear <= 189000 + 0xFFFFF; linear += 997) { + Fnv(hash, Gb18030Decode(gb, Gb18030Bytes(linear))); + ++samples; + } + Require(hash == 0x9E77B552 && samples == 1052); + hash = 0x811C9DC5; + for (std::uint32_t code = 0x10000; code < 0x110000; code += 997) Fnv(hash, Gb18030Encode(gb, code)); + Require(hash == 0x3D088D7E); + + for (const Gb18030Sequence& entry : GB18030_BOUNDARIES) { + const std::uint32_t code = revision2005 ? entry.code2005 : entry.code2000; + Require(Gb18030Decode(gb, entry.bytes) == code); + if (code != 0) Require(Gb18030Encode(gb, code) == entry.bytes); + } + for (const Gb18030Difference& entry : GB18030_DIFFERENCES) { + const std::uint32_t code = revision2005 ? entry.code2005 : entry.code2000; + Require(Gb18030Decode(gb, entry.bytes) == code && Gb18030Encode(gb, code) == entry.bytes); + if (revision2005 && entry.fallback) Require(Gb18030Encode(gb, entry.code2000) == entry.bytes); + } + + RequireGb18030Error(gb, "\x80", 1, invalidEncode); + RequireGb18030Error(gb, "\xFF", 1, invalidEncode); + RequireGb18030Error(gb, "\x81", 1, srcBufferEnd); + RequireGb18030Error(gb, "\x81\x30", 2, srcBufferEnd); + RequireGb18030Error(gb, "\x81\x30\x81", 3, srcBufferEnd); + RequireGb18030Error(gb, "\x81\x00", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x2F", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x3A", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x7F", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\xFF", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x80\x30", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\xFF\x30", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x30\x30", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x81\x2F", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x81\x3A", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x81\x81", 4, invalidEncode); + RequireGb18030Error(gb, "\x84\x31\xA5\x30", 4, unassignedCode); + RequireGb18030Error(gb, "\xE3\x32\x9A\x36", 4, unassignedCode); + RequireGb18030Error(gb, "\xFE\x39\xFE\x39", 4, unassignedCode); + + std::uint32_t consumed = 0x55; + std::uint32_t produced = 0x55; + std::uint16_t ucs2 = 0xBEEF; + Require(sceCesGbToUcs2(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, &ucs2) == unassignedCode && consumed == 0x55 && ucs2 == 0xBEEF); + std::uint8_t utf8[4] = {1, 2, 3, 4}; + Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 3, &produced) == dstBufferEnd && consumed == 0x55 && produced == 0x55); + Require(std::memcmp(utf8, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 4, &produced) == 0 && consumed == 4 && produced == 4); + Require(std::memcmp(utf8, "\xF0\x90\x80\x80", 4) == 0); + std::uint16_t utf16[2] = {0xBEEF, 0xBEEF}; + produced = 0x55; + Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 1, &produced) == dstBufferEnd && produced == 0x55 && utf16[0] == 0xBEEF); + Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 2, &produced) == 0 && produced == 2 && utf16[0] == 0xDBFF && utf16[1] == 0xDFFF); + Require(sceCesGbToUtf16be(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf16, 2, &produced) == 0 && std::memcmp(utf16, "\xD8\x00\xDC\x00", 4) == 0); + Require(sceCesMbcToUtf16le(gb, Bytes("\x94\x39\xFC\x36"), 4, &consumed, utf16, 2, &produced) == 0 && consumed == 4 && std::memcmp(utf16, "\x3D\xD8\x00\xDE", 4) == 0); + Require(sceCesGbToUcs2(gb, Bytes("\xA2\xE3"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x20AC); + + std::uint8_t mbcs[4] = {1, 2, 3, 4}; + produced = 0x55; + Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 0x55 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUtf32ToGb(0x110000, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToGb(0x80, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 4 && std::memcmp(mbcs, "\x81\x30\x81\x30", 4) == 0); + Require(sceCesUcs2ToGb(0x20AC, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xA2\xE3", 2) == 0); + Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 4 && produced == 4); + Require(std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); + const std::uint16_t pair[2] = {0xD83D, 0xDE00}; + Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 2 && produced == 4 && std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); + Require(sceCesUtf16ToGb(pair, 1, &consumed, gb, mbcs, sizeof(mbcs), &produced) == srcBufferEnd); + + alignas(8) std::uint8_t context[64] = {}; + Require(sceCesMbcsUcsContextInit(context, gb) == 0); + const char* utf8Text = "A\xE4\xB8\xAD\xC2\x80\xF0\x9F\x98\x80\xEF\xB8\x90"; + const char* gbText = revision2005 ? "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\xA6\xD9" : "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\x84\x31\x82\x36"; + const std::uint32_t gbSize = revision2005 ? 13 : 15; + std::uint8_t text[16] = {}; + Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8Text), 13, &consumed, text, sizeof(text), &produced) == 0 && consumed == 13 && produced == gbSize); + Require(std::memcmp(text, gbText, gbSize) == 0); + std::uint8_t backUtf8[16] = {}; + Require(sceCesMbcsStrToUtf8Str(context, text, gbSize, &consumed, backUtf8, sizeof(backUtf8), &produced) == 0 && consumed == gbSize && produced == 13); + Require(std::memcmp(backUtf8, utf8Text, 13) == 0); + std::uint16_t backUtf16[8] = {}; + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == 6); + Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[3] == 0xD83D && backUtf16[4] == 0xDE00 && backUtf16[5] == 0xFE10); + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 4, &produced) == dstBufferEnd); + Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == 6); + std::uint8_t again[16] = {}; + Require(sceCesUtf16StrToMbcsStr(context, backUtf16, 6, &consumed, again, sizeof(again), &produced) == 0 && consumed == 6 && produced == gbSize); + Require(std::memcmp(again, gbText, gbSize) == 0); + std::uint16_t ucs2Text[8] = {}; + Require(sceCesMbcsStrToUcs2Str(context, text, gbSize, &consumed, ucs2Text, 8, &produced) == unassignedCode); + Require(sceCesUtf8StrToMbcsStr(context, Bytes("\xC2\x80"), 2, &consumed, text, 3, &produced) == dstBufferEnd); +} + +int main() { alignas(8) std::uint8_t eucSheet[64] = {}; alignas(8) std::uint8_t gbkSheet[64] = {}; alignas(8) std::uint8_t sjisSheet[64] = {}; @@ -134,5 +367,16 @@ int main() { std::uint8_t back[16] = {}; Require(sceCesMbcsStrToUtf8Str(context, text, produced, &consumed, back, sizeof(back), &produced) == 0); Require(consumed == 4 && produced == 7 && std::memcmp(back, "A\xE4\xB8\xAD\xE2\x82\xAC", 7) == 0); + + alignas(8) std::uint8_t gb2000Sheet[64] = {}; + alignas(8) std::uint8_t gb2005Sheet[64] = {}; + const void* gb2000 = sceCesUcsProfileInitGb18030_2000(gb2000Sheet); + const void* gb2005 = sceCesUcsProfileInitGb18030_2005(gb2005Sheet); + Require(gb2000 == gb2000Sheet && gb2005 == gb2005Sheet); + Require(sceCesUcsProfileInitGb18030_2000(nullptr) == nullptr && sceCesUcsProfileInitGb18030_2005(nullptr) == nullptr); + Require(sceCesEucCnToUcs2(gb2000, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == invalidParameter); + Require(sceCesGbToUcs2(gb2005, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); + CheckGb18030(gb2000, false, 0xE61D3BD3, 0x83476043, 0x469CFCE5, 0, 0); + CheckGb18030(gb2005, true, 0x83EDDA69, 0x8D188DFC, 0x31CD0B55, 18, 24); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 6c90ed380d..f0af2576a1 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -13,9 +13,10 @@ - [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements - [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - cell `0x2140` and U+FF3C throw: JIS0208.TXT gives U+005C, glibc 2.39 and Python's `euc_jp` U+FF3C. In `sceCesUcsProfileInitSJis1997X0208`, bytes `0x5C` and `0x7E` and U+005C, U+007E, U+00A5 and U+203E throw: SHIFTJIS.TXT gives JIS X 0201 Roman, CP932 and Python's `shift_jis` ASCII; the console's is unknown. The table, [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT), is CP932's without rows 13 and 89-92 and with six cells changed, not measured. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), the `X0208Ss2` profile adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212. Signatures follow Orbital; the conversions above take these profiles with the same errors, a missing SS2 or SS3 answering `INVALID_ENCODE` and Shift-JIS leads `0xF0`-`0xFC` `UNASSIGNED_CODE` - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units -- [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas, GB 18030 four-byte forms and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified +- [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified +- [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the console's tables are not measured, and the sources disagree on 26 sequences. The 2000 profile follows ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (before ICU-8427), which matches Python's codec: `0xA8BC` is U+E7C7 and `0x8135F437` U+1E3F. The 2005 profile follows `gb-18030-2005.ucm` (91850aec), which swaps those two and maps 24 PUA cells to U+FE10-U+FE19, U+9FB4-U+9FBB and six supplementary ideographs, keeping the PUA code points as encode fallbacks. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp); four-byte runs include ICU's `gb18030Ranges`. The inits tag the sheet like GBK's; the `sceCesGb*`, `sceCesMbc*` and MBCS string forms accept them. The full mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE`; other errors follow the GBK forms - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) - [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 72 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 70 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do From dca749aaa0cd89340ed04a908ac6b0a4f7033d33 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Fri, 23 Oct 2026 17:27:22 -0300 Subject: [PATCH 1350/1360] fix(job-manager): wake runnable work before a worker waits --- core/libs/prx/libSceJobManager/CMakeLists.txt | 2 +- core/libs/prx/libSceJobManager/Export.cpp | 11 +++- core/libs/tests/GuestJobManager.cpp | 56 +++++++++++++++++++ tools/run_job_manager_reference.py | 2 +- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index 002ba69513..02088aa791 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -19,7 +19,7 @@ if(BUILD_TESTING) RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) configure_windows_unwind(job_manager_reference) endif() - foreach(testCase IN ITEMS memory validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_count worker_spin) + foreach(testCase IN ITEMS memory validation dispatch ordering nested limits node_pool priority shutdown worker_policy worker_wake worker_wait worker_count worker_spin) add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase}) set_tests_properties(guest_job_manager_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 4e3f1395b3..ad0c9ed5ce 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -247,8 +247,8 @@ struct Manager { thread_local Manager* currentWorker = nullptr; -void WakeWorkers(Manager& manager, std::uint32_t invocations) { - const std::uint32_t available = invocations - (currentWorker == &manager ? 1u : 0u); +void WakeWorkers(Manager& manager, std::uint32_t invocations, bool publisherCanRun = true) { + const std::uint32_t available = invocations - (publisherCanRun && currentWorker == &manager ? 1u : 0u); manager.wakeTokens += std::min(available, manager.sleepingWorkers - manager.wakeTokens); if (manager.wakeTokens != 0) manager.work.notify_all(); } @@ -477,6 +477,13 @@ int Sequence::WaitLocked(std::unique_lock<std::mutex>& guard) { bool done = false; items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done}); Advance(); + if (!done && currentWorker == manager) { + std::uint64_t runnable = 0; + for (const auto& queue : manager->runQueues) { + for (const Item* item : queue) runnable += item->count - item->handedOut; + } + WakeWorkers(*manager, static_cast<std::uint32_t>(std::min(runnable, std::uint64_t{manager->workerCount})), false); + } passed.wait(guard, [&done] { return done; }); --manager->blockedWaiters; return result; diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index 3d3644a538..e8bfeb526f 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -303,6 +303,61 @@ static void APS5_VABI LeaderJob(void* argument, std::uint32_t) { nested.leaderFinished = true; } +static void RequireParked(int clock); + +struct WorkerWait { + int spareClock = 0; + Sequence* child; + std::atomic<bool> publisherEntered{false}; + std::atomic<bool> spareEntered{false}; + std::atomic<bool> publish{false}; + std::atomic<bool> finished{false}; + std::atomic<unsigned> childCalls{0}; +}; + +static void APS5_VABI WorkerWaitChild(void* argument, std::uint32_t) { + ++static_cast<WorkerWait*>(argument)->childCalls; +} + +static void APS5_VABI WorkerWaitPublisher(void* argument, std::uint32_t index) { + auto& state = *static_cast<WorkerWait*>(argument); + if (index != 0) { + Require(pthread_getcpuclockid_nid_postfix(scePthreadSelf(), &state.spareClock) == 0); + state.spareEntered = true; + return; + } + state.publisherEntered = true; + while (!state.publish) std::this_thread::yield(); + Require(state.child->Dispatch(WorkerWaitChild, &state, 1, "child") == 0); + Require(state.child->Wait() == 0); + state.finished = true; +} + +static void WorkerWaitCase() { + Block block(SizeFor(2, 2, 4, 0, 4)); + ManagerObject manager; + const ConfigParams config{0, 0x21fff, 0x10000, 0x2bc, "WorkerWait", 0, 0, 0, 0}; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, &config) == 0); + Require(SetPersistentThreadCount(manager.storage, 0) == 0); + SequenceFactory* factory = Factories(manager.storage, 0); + Sequence* publisher = nullptr; + WorkerWait state{}; + Require(factory->Create(&publisher, 2, 1, 0, 1, "publisher") == 0); + Require(factory->Create(&state.child, 1, 1, 0, 1, "child") == 0); + Require(publisher->Dispatch(WorkerWaitPublisher, &state, 2, "park spare") == 0); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while ((!state.publisherEntered || !state.spareEntered) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(state.publisherEntered && state.spareEntered); + RequireParked(state.spareClock); + state.publish = true; + deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (!state.finished && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(state.finished && state.childCalls == 1); + Require(publisher->Wait() == 0); + Require(factory->Destroy(publisher) == 0 && factory->Destroy(state.child) == 0); + Require(Shutdown(manager.storage) == 0); +} + static void NestedCase() { Block block(SizeFor(2, 1, 4, 2, 2)); ManagerObject manager; @@ -711,6 +766,7 @@ int RunJobManagerCase(const char* caseName) { else if (name == "priority") PriorityCase(); else if (name == "shutdown") ShutdownCase(); else if (name == "worker_policy") WorkerPolicyCase(); + else if (name == "worker_wait") WorkerWaitCase(); else if (name == "worker_wake") WorkerWakeCase(); else if (name == "worker_spin") WorkerSpinCase(); else if (name == "worker_count") WorkerCountCase(); diff --git a/tools/run_job_manager_reference.py b/tools/run_job_manager_reference.py index 488916cba2..d804f3e573 100644 --- a/tools/run_job_manager_reference.py +++ b/tools/run_job_manager_reference.py @@ -15,7 +15,7 @@ ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) from guest_import_fixtures import executable -CASES = ("memory", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_count", "worker_spin") +CASES = ("memory", "validation", "dispatch", "ordering", "nested", "limits", "node_pool", "priority", "shutdown", "worker_policy", "worker_wake", "worker_wait", "worker_count", "worker_spin") def stage_libraries(directory, build, toolchain, dependencies): From 3654f15bfaf40914956bccc430bca3aa7fc593d4 Mon Sep 17 00:00:00 2001 From: thibaut <thibaut-leaux-angeli@live.fr> Date: Wed, 21 Oct 2026 19:55:54 +0200 Subject: [PATCH 1351/1360] feat(shader): bit-exact RDNA v_rcp_f32 behind APS5_EXACT_RCP v_rcp_f32 and v_rcp_iflag_f32 lower to the driver's 1/x, whose low bits differ from the console's on NVIDIA and Intel. With APS5_EXACT_RCP set, normal inputs with normal results take the RDNA reciprocal datapath instead: a cubic over 32 segments of the mantissa in 32-bit integer arithmetic, the linear term rounded to nearest even, and a 2,761-entry table of one-ULP corrections, looked up only when the dropped bits fall next to the rounding cut (6.25 % of inputs). This reproduces all 2^23 mantissas of an RDNA2 (gfx1031) sweep. Other inputs keep 1/x. It is off by default: the tables add about 13k SPIR-V words to each shader that uses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017EjjFU1FjMR11oWDYs8UMo --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../tests/execution/RcpHardware.cpp | 162 ++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 8 - .../SpirvAlu/SpirvAluEmitterReciprocal.cpp | 502 ++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 1 + docs/dev/ENVIRONMENT.md | 1 + docs/dev/debt/shader.md | 1 + 10 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp create mode 100644 core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00..e2f14d2d93 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2128,6 +2128,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests) + set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() endif() foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp new file mode 100644 index 0000000000..66a3c78357 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp @@ -0,0 +1,162 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <bit> +#include <cstdint> +#include <cstdlib> +#include <cstdio> +#include <iostream> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::array<std::array<std::uint32_t, 2>, 256> Measured{{ + {0x000000u, 0x3f800000u}, {0x002922u, 0x3f7fadd6u}, {0x00a1a9u, 0x3f7ebe44u}, {0x010c47u, 0x3f7debceu}, + {0x013df5u, 0x3f7d8a32u}, {0x01a87fu, 0x3f7cb9ddu}, {0x024f43u, 0x3f7b766fu}, {0x02d76du, 0x3f7a70beu}, + {0x031690u, 0x3f79f820u}, {0x033035u, 0x3f79c742u}, {0x03adabu, 0x3f78d946u}, {0x04435bu, 0x3f77bfa4u}, + {0x04f3a1u, 0x3f76772au}, {0x057a96u, 0x3f757df8u}, {0x05c6afu, 0x3f74f253u}, {0x06342au, 0x3f742a80u}, + {0x06bf6fu, 0x3f732e24u}, {0x07709au, 0x3f71f00au}, {0x07eb02u, 0x3f711627u}, {0x086273u, 0x3f704304u}, + {0x09362bu, 0x3f6ed04au}, {0x099950u, 0x3f6e2438u}, {0x09f75cu, 0x3f6d81e2u}, {0x0a097cu, 0x3f6d62b3u}, + {0x0ab7e3u, 0x3f6c3840u}, {0x0b1a69u, 0x3f6b90f0u}, {0x0b93a5u, 0x3f6ac454u}, {0x0becd7u, 0x3f6a2eadu}, + {0x0bf0b4u, 0x3f6a2836u}, {0x0c5c7fu, 0x3f697463u}, {0x0c9164u, 0x3f691c8au}, {0x0cb1e2u, 0x3f68e6b5u}, + {0x0d5f89u, 0x3f67c8a0u}, {0x0dd27au, 0x3f670cc6u}, {0x0e09e4u, 0x3f66b2a2u}, {0x0ead02u, 0x3f65aae2u}, + {0x0ed904u, 0x3f656421u}, {0x0f17a3u, 0x3f64ffbeu}, {0x0f21ddu, 0x3f64ef61u}, {0x0f2bcdu, 0x3f64df7du}, + {0x0f4205u, 0x3f64bbfeu}, {0x0f8808u, 0x3f644c6cu}, {0x0faddcu, 0x3f641050u}, {0x0fd630u, 0x3f63d05fu}, + {0x1012f0u, 0x3f637050u}, {0x108fe7u, 0x3f62abb4u}, {0x10a3d6u, 0x3f628c77u}, {0x1184b9u, 0x3f612e5au}, + {0x119a72u, 0x3f610cc2u}, {0x11e20bu, 0x3f609e4fu}, {0x123f59u, 0x3f600f00u}, {0x128b2fu, 0x3f5f9b0du}, + {0x12bd4au, 0x3f5f4eb3u}, {0x12f18du, 0x3f5eff48u}, {0x135d16u, 0x3f5e5c8du}, {0x13deefu, 0x3f5d994bu}, + {0x140ceeu, 0x3f5d5472u}, {0x14a0f9u, 0x3f5c77fdu}, {0x14f473u, 0x3f5bfc70u}, {0x15051au, 0x3f5be3dau}, + {0x153e7cu, 0x3f5b8f4fu}, {0x15b949u, 0x3f5adb3au}, {0x1600a3u, 0x3f5a7320u}, {0x1676ebu, 0x3f59c766u}, + {0x172abbu, 0x3f58c45au}, {0x1738f7u, 0x3f58aff3u}, {0x17a05au, 0x3f581c33u}, {0x17f5e8u, 0x3f57a288u}, + {0x1818e8u, 0x3f5770e9u}, {0x185f1au, 0x3f570da8u}, {0x18f135u, 0x3f564038u}, {0x194f2fu, 0x3f55bce2u}, + {0x1a61dbu, 0x3f54409cu}, {0x1a63e2u, 0x3f543dd2u}, {0x1af098u, 0x3f537d12u}, {0x1bd958u, 0x3f52413au}, + {0x1cb224u, 0x3f511e54u}, {0x1deb7au, 0x3f4f7f68u}, {0x1df9fdu, 0x3f4f6c59u}, {0x1e27a1u, 0x3f4f307du}, + {0x1e398fu, 0x3f4f1903u}, {0x1ef9d8u, 0x3f4e1e85u}, {0x1fb17cu, 0x3f4d317eu}, {0x1fec12u, 0x3f4ce652u}, + {0x202036u, 0x3f4ca39au}, {0x20b671u, 0x3f4be450u}, {0x211c70u, 0x3f4b633bu}, {0x21b455u, 0x3f4aa42eu}, + {0x2217beu, 0x3f4a27e7u}, {0x22c947u, 0x3f494b6eu}, {0x230d97u, 0x3f48f719u}, {0x23a75fu, 0x3f483a41u}, + {0x244f6fu, 0x3f476d74u}, {0x24ede6u, 0x3f46add8u}, {0x250d33u, 0x3f46882au}, {0x254b0cu, 0x3f463de2u}, + {0x263ba6u, 0x3f451ef3u}, {0x269e0du, 0x3f44aa89u}, {0x26a2c0u, 0x3f44a4fdu}, {0x26bb7du, 0x3f4487d0u}, + {0x26e875u, 0x3f4452ddu}, {0x2744e3u, 0x3f43e660u}, {0x281972u, 0x3f42eeaau}, {0x29b882u, 0x3f4111f2u}, + {0x2a3af4u, 0x3f407e00u}, {0x2a96fbu, 0x3f401628u}, {0x2afa95u, 0x3f3fa642u}, {0x2b0537u, 0x3f3f9a58u}, + {0x2baf6au, 0x3f3edc66u}, {0x2c7557u, 0x3f3e015au}, {0x2d1c9au, 0x3f3d49c5u}, {0x2d711au, 0x3f3ced8cu}, + {0x2e0531u, 0x3f3c4cc6u}, {0x2e4415u, 0x3f3c08d1u}, {0x2e8e7du, 0x3f3bb8aau}, {0x2eae05u, 0x3f3b96c8u}, + {0x2fa54eu, 0x3f3a8eaeu}, {0x301850u, 0x3f3a14d7u}, {0x30d16au, 0x3f39520au}, {0x31f5efu, 0x3f38216cu}, + {0x32c6ecu, 0x3f374a2du}, {0x33d0ceu, 0x3f363b28u}, {0x34b9b5u, 0x3f355051u}, {0x34c9ebu, 0x3f35400eu}, + {0x364b9du, 0x3f33c092u}, {0x36f675u, 0x3f3318bau}, {0x376cbfu, 0x3f32a53au}, {0x37dc76u, 0x3f3238afu}, + {0x387a34u, 0x3f31a04au}, {0x3898d1u, 0x3f3182d5u}, {0x392630u, 0x3f30fb4bu}, {0x398406u, 0x3f30a1c6u}, + {0x3aa8b5u, 0x3f2f8cd0u}, {0x3b1287u, 0x3f2f2983u}, {0x3b6186u, 0x3f2edfabu}, {0x3bbbe9u, 0x3f2e8b79u}, + {0x3bfb1eu, 0x3f2e50c8u}, {0x3cf38cu, 0x3f2d6b98u}, {0x3d9c17u, 0x3f2cd172u}, {0x3e7d1bu, 0x3f2c054du}, + {0x3e875fu, 0x3f2bfc08u}, {0x3f63afu, 0x3f2b360fu}, {0x3f98e2u, 0x3f2b0685u}, {0x3fa7d9u, 0x3f2af92au}, + {0x407b49u, 0x3f2a3d5au}, {0x4174afu, 0x3f2961e2u}, {0x427df9u, 0x3f287ad8u}, {0x43435cu, 0x3f27d089u}, + {0x437bcbu, 0x3f27a016u}, {0x44985eu, 0x3f26ad72u}, {0x451abdu, 0x3f263f34u}, {0x457d42u, 0x3f25ec44u}, + {0x472077u, 0x3f248ef6u}, {0x4724dau, 0x3f248b56u}, {0x47469au, 0x3f246f78u}, {0x482c9cu, 0x3f23b287u}, + {0x486824u, 0x3f2381e6u}, {0x48db40u, 0x3f232432u}, {0x499523u, 0x3f228dc2u}, {0x49b64au, 0x3f22730au}, + {0x49dea9u, 0x3f22528du}, {0x4a23d5u, 0x3f221b02u}, {0x4b14f9u, 0x3f215a86u}, {0x4c25fcu, 0x3f2082beu}, + {0x4cbd87u, 0x3f200befu}, {0x4cdd20u, 0x3f1ff340u}, {0x4dd422u, 0x3f1f334cu}, {0x4ef8aau, 0x3f1e5249u}, + {0x4f20f3u, 0x3f1e337eu}, {0x4f426du, 0x3f1e19f1u}, {0x5051c1u, 0x3f1d4c05u}, {0x506bf2u, 0x3f1d3841u}, + {0x507313u, 0x3f1d32e0u}, {0x519088u, 0x3f1c5cc0u}, {0x520935u, 0x3f1c02eau}, {0x52e6b4u, 0x3f1b5f10u}, + {0x536d35u, 0x3f1afc38u}, {0x54a6d8u, 0x3f1a17a2u}, {0x55c663u, 0x3f19485eu}, {0x56ed5bu, 0x3f187600u}, + {0x571242u, 0x3f185bd8u}, {0x5790f8u, 0x3f180249u}, {0x57ee05u, 0x3f17c0c8u}, {0x588a7eu, 0x3f175320u}, + {0x58d556u, 0x3f171ee4u}, {0x59a54au, 0x3f168e80u}, {0x59f999u, 0x3f165444u}, {0x5affb2u, 0x3f15a05bu}, + {0x5b855cu, 0x3f15453fu}, {0x5bd86du, 0x3f150cd9u}, {0x5c90a9u, 0x3f149059u}, {0x5cd546u, 0x3f146230u}, + {0x5d9dc9u, 0x3f13dbf0u}, {0x5e14d9u, 0x3f138caau}, {0x5e8766u, 0x3f1340b6u}, {0x5f41cau, 0x3f12c5c6u}, + {0x5f5572u, 0x3f12b8dcu}, {0x60fd9bu, 0x3f11a440u}, {0x616499u, 0x3f1161b4u}, {0x625d1cu, 0x3f10c218u}, + {0x62c33au, 0x3f1080e8u}, {0x63c0e3u, 0x3f0fdff7u}, {0x641547u, 0x3f0faabcu}, {0x64b8d7u, 0x3f0f43feu}, + {0x651327u, 0x3f0f0b83u}, {0x658cdau, 0x3f0ebfadu}, {0x65dc9fu, 0x3f0e8e23u}, {0x662b59u, 0x3f0e5d60u}, + {0x66d228u, 0x3f0df67eu}, {0x670f27u, 0x3f0dd104u}, {0x67e4c4u, 0x3f0d4e61u}, {0x6916adu, 0x3f0c94eeu}, + {0x6a50dfu, 0x3f0bd86cu}, {0x6a7a5au, 0x3f0bbfaeu}, {0x6b0a18u, 0x3f0b6a37u}, {0x6b0d54u, 0x3f0b684cu}, + {0x6b4013u, 0x3f0b4a3au}, {0x6b4cb2u, 0x3f0b42c1u}, {0x6bd361u, 0x3f0af338u}, {0x6bf46cu, 0x3f0adfc3u}, + {0x6cad4au, 0x3f0a734au}, {0x6cb93du, 0x3f0a6c4cu}, {0x6d76b0u, 0x3f09fdddu}, {0x6dc676u, 0x3f09cf92u}, + {0x6e36aau, 0x3f098ea8u}, {0x6f0367u, 0x3f0918d3u}, {0x6fa5cau, 0x3f08bbedu}, {0x70c958u, 0x3f08165eu}, + {0x71e6c2u, 0x3f0775ccu}, {0x721583u, 0x3f075ba3u}, {0x72e6ccu, 0x3f06e703u}, {0x72fdf2u, 0x3f06da29u}, + {0x73596du, 0x3f06a778u}, {0x7403e4u, 0x3f064966u}, {0x744916u, 0x3f06235cu}, {0x74c9dfu, 0x3f05dccau}, + {0x74e69au, 0x3f05cd16u}, {0x756dbau, 0x3f05836cu}, {0x7631a9u, 0x3f05192au}, {0x7731afu, 0x3f048f50u}, + {0x7774a2u, 0x3f046b72u}, {0x791a7cu, 0x3f038b32u}, {0x795e82u, 0x3f036751u}, {0x7aa2c1u, 0x3f02bd52u}, + {0x7c2684u, 0x3f01f444u}, {0x7c64ebu, 0x3f01d422u}, {0x7d2cafu, 0x3f016db2u}, {0x7d959fu, 0x3f013822u}, + {0x7e62aau, 0x3f00cffbu}, {0x7ebff2u, 0x3f00a0d0u}, {0x7eecf4u, 0x3f008a1au}, {0x7f1505u, 0x3f0075eau}, + {0x7f1b10u, 0x3f0072dfu}, {0x7f2614u, 0x3f006d53u}, {0x7ff486u, 0x3f0005beu}, {0x7fffffu, 0x3f000001u}, +}}; +constexpr std::array<std::uint32_t, 6> Exponents{1u, 60u, 127u, 128u, 200u, 252u}; +constexpr std::uint32_t Cases = static_cast<std::uint32_t>(Measured.size() * Exponents.size() * 2u); +constexpr std::uint32_t Batches = Cases / Threads; +alignas(256) std::array<std::uint32_t, Threads> Input{}; +alignas(256) std::array<std::uint32_t, Threads> Output{}; + +alignas(256) constexpr std::array<std::uint32_t, 9> Code{ + 0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0x7e065502, 0xe0701000, 0x80010301, 0xbf8c3f70, + 0xbf810000, +}; + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Sample(std::uint32_t index) { + const auto& measured = Measured[index % Measured.size()]; + const auto exponent = Exponents[(index / Measured.size()) % Exponents.size()]; + const auto sign = (index / static_cast<std::uint32_t>(Measured.size() * Exponents.size())) << 31u; + return sign | exponent << 23u | measured[0]; +} + +std::uint32_t Expected(std::uint32_t index) { + const auto& measured = Measured[index % Measured.size()]; + const auto exponent = Exponents[(index / Measured.size()) % Exponents.size()]; + const auto sign = (index / static_cast<std::uint32_t>(Measured.size() * Exponents.size())) << 31u; + return sign | ((measured[1] >> 23u) + 127u - exponent) << 23u | (measured[1] & 0x7fffffu); +} +} + +int main() { + try { + if (std::getenv("APS5_EXACT_RCP") == nullptr) return VulkanTestSkipped; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector<std::uint32_t> userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(Code); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; + const auto result = ShaderRecompiler::Recompile(request); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid] = Sample(batch * Threads + tid); + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device->WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto wanted = Expected(batch * Threads + tid); + Require(Output[tid] == wanted, std::string("v_rcp_f32 of ") + Hex(Input[tid]) + " is " + Hex(Output[tid]) + ", expected the RDNA 2 value " + Hex(wanted)); + } + } + std::puts("rcp hardware 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 b278d2b8a0..2f8d98958f 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -131,6 +131,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAnalysis.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvFlowEmitter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvImageEmitter.cpp diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 34213a41b6..6556b78469 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -154,6 +154,7 @@ std::uint32_t EmitF16BitsToF32Decoded(SpirvEmitterState& state, std::uint32_t bi void EmitProgram(SpirvEmitterState& state); void DefineGetBdaPointer(SpirvEmitterState& state); void DefineDot2F32F16Function(SpirvEmitterState& state); +void DefineReciprocalCorrectionFunction(SpirvEmitterState& state); void EmitLabel(SpirvEmitterState& state, std::uint32_t label); std::uint32_t Unary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t value); std::uint32_t Binary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 38b321e595..16541a653a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -167,6 +167,8 @@ struct SpirvEmitterState { bool nativeF16ModesEmitted = false; bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; + std::uint32_t reciprocalCorrectionFunction = 0; + std::array<std::uint32_t, 3> reciprocalTables {}; std::map<std::pair<bool, std::uint32_t>, std::uint32_t> interpolateFunctions; bool integerInterpolation = false; std::array<std::array<std::uint32_t, 2>, 2> bdaDwordReadFunctions {}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 1ce65a7506..abf7b6fce4 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -983,14 +983,6 @@ std::uint32_t EmitFPMedTri32(SpirvEmitterState& state, std::uint32_t arg0, std:: return value(Select(state, u32, anyNan, min3, median)); } -std::uint32_t EmitFPRecip32(SpirvEmitterState& state, std::uint32_t arg0) { - return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0); -} - -std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { - return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0); -} - std::uint32_t EmitFPRecipSqrt32(SpirvEmitterState& state, std::uint32_t arg0) { return EmitExt(state, TypeF32(state), GLSLstd450InverseSqrt, {arg0}); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp new file mode 100644 index 0000000000..69a21a884c --- /dev/null +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp @@ -0,0 +1,502 @@ +#include "SpirvBackend/SpirvEmitterHelpers.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" +#include "IntermediateRepresentation/IrValue.hpp" +#include <spirv/unified1/spirv.hpp> +#include <array> +#include <cstdint> +#include <cstdlib> +#include <span> + +namespace ShaderRecompiler { +namespace { + +constexpr std::array<std::uint32_t, 32> ReciprocalConstant{ + 0x04000000u, 0x03e0f83eu, 0x03c3c3c3u, 0x03a83a83u, 0x038e38e3u, 0x03759f22u, 0x035e50d7u, 0x03483483u, + 0x03333333u, 0x031f3831u, 0x030c30c3u, 0x02fa0be8u, 0x02e8ba2eu, 0x02d82d82u, 0x02c8590bu, 0x02b93105u, + 0x02aaaaaau, 0x029cbc14u, 0x028f5c28u, 0x02828282u, 0x02762762u, 0x026a439fu, 0x025ed097u, 0x0253c825u, + 0x02492492u, 0x023ee08fu, 0x0234f72cu, 0x022b63cbu, 0x02222222u, 0x02192e29u, 0x02108421u, 0x02082082u, +}; + +constexpr std::array<std::uint32_t, 32> ReciprocalLinear{ + 0x001ffff2u, 0x001e16feu, 0x001c588au, 0x001abfcfu, 0x001948a9u, 0x0017ef84u, 0x0016b143u, 0x00158b2cu, + 0x00147adcu, 0x00137e3bu, 0x0012936eu, 0x0011b8d2u, 0x0010ecf2u, 0x00102e83u, 0x000f7c5cu, 0x000ed575u, + 0x000e38e1u, 0x000da5cau, 0x000d1b6fu, 0x000c9924u, 0x000c1e4au, 0x000baa54u, 0x000b3cbfu, 0x000ad517u, + 0x000a72efu, 0x000a15e6u, 0x0009bda3u, 0x000969d3u, 0x00091a2au, 0x0008ce64u, 0x00088642u, 0x00084187u, +}; + +constexpr std::array<std::uint32_t, 32> ReciprocalHigherOrder{ + 0x0785ffb6u, 0x06a9e92du, 0x05ecd536u, 0x0548c377u, 0x04bab3a2u, 0x043ea577u, 0x03d198c0u, 0x03728d4eu, + 0x031e82f9u, 0x02d479a0u, 0x02927126u, 0x02576970u, 0x0223626au, 0x01f55c00u, 0x01cb5622u, 0x01a550c1u, + 0x01844bd0u, 0x01654744u, 0x014a4313u, 0x01313f35u, 0x011a3ba2u, 0x01063852u, 0x00f33540u, 0x00e23266u, + 0x00d22fbfu, 0x00c42d47u, 0x00b72afau, 0x00ab28d4u, 0x00a026d2u, 0x009624f2u, 0x008c232fu, 0x00832189u, +}; + +constexpr std::array<std::uint32_t, 1025> ReciprocalCorrectionStarts{ + 0x00000000u, 0x00020000u, 0x00040002u, 0x000a0008u, 0x0012000cu, 0x00160014u, 0x001a0018u, 0x0023001cu, + 0x00280026u, 0x00290029u, 0x002f002cu, 0x00320031u, 0x00390038u, 0x0042003eu, 0x00480045u, 0x004a004au, + 0x004f004du, 0x00550052u, 0x00570056u, 0x005d005bu, 0x005f005eu, 0x00640061u, 0x006b0069u, 0x0076006fu, + 0x007d0077u, 0x0082007eu, 0x00850084u, 0x008d0088u, 0x00920090u, 0x00990097u, 0x00a0009cu, 0x00a400a3u, + 0x00ab00a8u, 0x00ab00abu, 0x00b000adu, 0x00b700b5u, 0x00bb00b8u, 0x00be00bdu, 0x00c100c1u, 0x00c300c3u, + 0x00c900c5u, 0x00d000cdu, 0x00d300d2u, 0x00d800d5u, 0x00db00dau, 0x00de00dcu, 0x00e100dfu, 0x00e900e3u, + 0x00ea00e9u, 0x00ef00edu, 0x00f300f1u, 0x00fb00f7u, 0x00fb00fbu, 0x00ff00feu, 0x01060105u, 0x01080106u, + 0x010c010au, 0x0112010fu, 0x01150112u, 0x011a0117u, 0x011c011bu, 0x01240120u, 0x012b0126u, 0x0132012fu, + 0x01360135u, 0x013b0137u, 0x013f013du, 0x01490143u, 0x014a0149u, 0x014e014du, 0x014f014fu, 0x01540151u, + 0x01580158u, 0x015b0158u, 0x0161015du, 0x01650163u, 0x01670166u, 0x016a0169u, 0x016d016bu, 0x0170016eu, + 0x01730173u, 0x01750175u, 0x017b0179u, 0x01830181u, 0x01860184u, 0x01870187u, 0x018c018au, 0x0191018fu, + 0x01970194u, 0x01a1019eu, 0x01a601a4u, 0x01ab01a8u, 0x01b001acu, 0x01b701b5u, 0x01c101bfu, 0x01c701c4u, + 0x01cd01cbu, 0x01d001cfu, 0x01d501d4u, 0x01d701d6u, 0x01de01ddu, 0x01e301e2u, 0x01e601e5u, 0x01eb01eau, + 0x01eb01ebu, 0x01ee01ecu, 0x01f001f0u, 0x01f601f4u, 0x01fa01f7u, 0x01fb01fbu, 0x020101feu, 0x02030201u, + 0x020a0207u, 0x020e020cu, 0x02140212u, 0x02170217u, 0x021d021cu, 0x0220021du, 0x02210221u, 0x02280225u, + 0x0230022bu, 0x02370233u, 0x023b023bu, 0x023f023bu, 0x02470241u, 0x024b0249u, 0x024f024cu, 0x02540250u, + 0x02560255u, 0x025a0257u, 0x025a025au, 0x025f025cu, 0x0261025fu, 0x02660264u, 0x02670267u, 0x026d026bu, + 0x026f026fu, 0x02710271u, 0x02740274u, 0x02760276u, 0x02790276u, 0x027e027cu, 0x0282027fu, 0x02830283u, + 0x02850285u, 0x02880285u, 0x028d028du, 0x028f028eu, 0x0292028fu, 0x02960293u, 0x029b029au, 0x02a0029du, + 0x02aa02a5u, 0x02b302acu, 0x02b602b4u, 0x02b902b8u, 0x02bc02bbu, 0x02be02bdu, 0x02c302c0u, 0x02c902c5u, + 0x02ce02cau, 0x02d202d0u, 0x02d302d2u, 0x02d802d6u, 0x02db02dau, 0x02de02deu, 0x02e102deu, 0x02e102e1u, + 0x02e602e2u, 0x02ec02eau, 0x02f002eeu, 0x02f502f2u, 0x02f602f6u, 0x02f802f7u, 0x02fc02fau, 0x030002fdu, + 0x03040302u, 0x03060304u, 0x030a0309u, 0x030d030cu, 0x0310030fu, 0x03130311u, 0x03160316u, 0x031c0319u, + 0x031f031du, 0x03260322u, 0x0330032au, 0x03350332u, 0x03380337u, 0x033c0339u, 0x0341033du, 0x03470344u, + 0x034b0349u, 0x034d034cu, 0x034d034du, 0x03500350u, 0x03530350u, 0x03570354u, 0x035a0358u, 0x035b035bu, + 0x0361035fu, 0x03620361u, 0x03660363u, 0x03680366u, 0x03690368u, 0x036b036au, 0x036d036cu, 0x0370036eu, + 0x03720371u, 0x03750375u, 0x03790376u, 0x037f037bu, 0x03830382u, 0x03870384u, 0x038a0389u, 0x0390038du, + 0x03950393u, 0x03960395u, 0x03990396u, 0x039e039bu, 0x039f039fu, 0x03a303a2u, 0x03a503a5u, 0x03ab03a8u, + 0x03af03aeu, 0x03b103b0u, 0x03b403b3u, 0x03b603b4u, 0x03b803b8u, 0x03bc03b9u, 0x03c003beu, 0x03c303c0u, + 0x03c503c3u, 0x03c503c5u, 0x03c603c5u, 0x03ca03c8u, 0x03ca03cau, 0x03cc03cbu, 0x03cf03cdu, 0x03d303d0u, + 0x03d503d4u, 0x03d803d7u, 0x03da03d9u, 0x03dc03dbu, 0x03de03dcu, 0x03de03deu, 0x03e203e0u, 0x03e703e5u, + 0x03e903e8u, 0x03ea03eau, 0x03ed03ebu, 0x03ef03eeu, 0x03f103efu, 0x03f503f2u, 0x03fb03f7u, 0x03fe03fbu, + 0x04080401u, 0x040d040bu, 0x040e040eu, 0x040f040eu, 0x04130411u, 0x04130413u, 0x04150414u, 0x04160415u, + 0x04180416u, 0x041b041au, 0x041e041du, 0x0420041fu, 0x04240421u, 0x04280428u, 0x042b042bu, 0x042d042bu, + 0x042f042fu, 0x04330431u, 0x04330433u, 0x04350433u, 0x04380436u, 0x043d043bu, 0x043f043eu, 0x0441043fu, + 0x04450443u, 0x04460445u, 0x04460446u, 0x044a0449u, 0x0450044eu, 0x04560454u, 0x04590457u, 0x045e045bu, + 0x04640462u, 0x04650464u, 0x046b0466u, 0x046c046cu, 0x0470046eu, 0x04750473u, 0x04780477u, 0x047b047au, + 0x0480047eu, 0x04810480u, 0x04830481u, 0x04850483u, 0x04870486u, 0x048b0488u, 0x048f048eu, 0x0490048fu, + 0x04920492u, 0x04940492u, 0x04970497u, 0x049b049au, 0x049f049eu, 0x04a1049fu, 0x04a304a2u, 0x04a504a5u, + 0x04a704a6u, 0x04a904a8u, 0x04ad04abu, 0x04b304afu, 0x04b504b5u, 0x04b604b5u, 0x04b804b7u, 0x04bc04bbu, + 0x04c104beu, 0x04c204c2u, 0x04c404c3u, 0x04c404c4u, 0x04c504c5u, 0x04c704c6u, 0x04c804c7u, 0x04c904c9u, + 0x04c904c9u, 0x04ce04cdu, 0x04d004cfu, 0x04d304d2u, 0x04d304d3u, 0x04d604d6u, 0x04d904d7u, 0x04dc04d9u, + 0x04e004dfu, 0x04e004e0u, 0x04e104e0u, 0x04e204e1u, 0x04e404e4u, 0x04e404e4u, 0x04e704e7u, 0x04eb04e8u, + 0x04ef04edu, 0x04f304f2u, 0x04f704f4u, 0x04fb04f9u, 0x04fd04fdu, 0x05030500u, 0x05090505u, 0x050d050bu, + 0x05130510u, 0x05140514u, 0x05160515u, 0x05160516u, 0x051a0518u, 0x051d051bu, 0x051e051du, 0x0520051fu, + 0x05260523u, 0x05280527u, 0x05290529u, 0x052d052au, 0x0531052eu, 0x05320531u, 0x05350533u, 0x053a0538u, + 0x053d053bu, 0x053f053fu, 0x05420542u, 0x05420542u, 0x05440542u, 0x05480546u, 0x054c054bu, 0x054f054eu, + 0x054f054fu, 0x0551054fu, 0x05530552u, 0x05550553u, 0x05570557u, 0x055c0559u, 0x055e055du, 0x055e055eu, + 0x055f055fu, 0x0563055fu, 0x05660566u, 0x05690568u, 0x056c056bu, 0x056d056cu, 0x056e056du, 0x05710571u, + 0x05720572u, 0x05720572u, 0x05730573u, 0x05750573u, 0x05760575u, 0x05790578u, 0x057f057cu, 0x05830581u, + 0x05850585u, 0x05870587u, 0x058e0589u, 0x058f058eu, 0x05930592u, 0x05980595u, 0x059c059au, 0x05a205a0u, + 0x05a205a2u, 0x05a305a3u, 0x05a805a6u, 0x05aa05a9u, 0x05ac05acu, 0x05ad05adu, 0x05b005adu, 0x05b705b2u, + 0x05ba05b9u, 0x05bb05bbu, 0x05bd05bbu, 0x05bd05bdu, 0x05bf05bfu, 0x05c305c1u, 0x05c605c4u, 0x05c605c6u, + 0x05c705c7u, 0x05c905c8u, 0x05ca05c9u, 0x05cd05ccu, 0x05cf05cdu, 0x05d105d0u, 0x05d305d2u, 0x05d605d4u, + 0x05d805d6u, 0x05d905d8u, 0x05dc05dau, 0x05dc05dcu, 0x05de05dcu, 0x05df05deu, 0x05e005e0u, 0x05e405e3u, + 0x05e705e5u, 0x05ec05eau, 0x05ee05edu, 0x05f205f1u, 0x05f305f2u, 0x05f705f4u, 0x05f905f8u, 0x05fb05fbu, + 0x05fd05fcu, 0x06000600u, 0x06050604u, 0x06050605u, 0x06090609u, 0x060c060bu, 0x0610060fu, 0x06100610u, + 0x06130610u, 0x06140613u, 0x061a0618u, 0x061b061au, 0x061d061cu, 0x06250622u, 0x06260625u, 0x06280628u, + 0x062a0629u, 0x062b062bu, 0x062e062cu, 0x062e062eu, 0x06300630u, 0x06330632u, 0x06350635u, 0x06370636u, + 0x06390638u, 0x063a063au, 0x063b063au, 0x063d063cu, 0x0641063eu, 0x06420641u, 0x06450643u, 0x06480646u, + 0x064e064au, 0x064f064fu, 0x06510651u, 0x06530652u, 0x06570656u, 0x06580658u, 0x065a0659u, 0x065d065cu, + 0x065e065eu, 0x0661065fu, 0x06630662u, 0x06640664u, 0x06660666u, 0x06670666u, 0x06690668u, 0x066b0669u, + 0x066d066du, 0x066e066du, 0x06710670u, 0x06720671u, 0x06740673u, 0x06760676u, 0x06780678u, 0x067a0679u, + 0x067c067cu, 0x067e067eu, 0x067f067eu, 0x06820680u, 0x06860685u, 0x068a0688u, 0x068c068cu, 0x068e068cu, + 0x06990690u, 0x069b069au, 0x069b069bu, 0x069e069cu, 0x06a206a0u, 0x06a506a4u, 0x06a606a5u, 0x06ab06aau, + 0x06ad06adu, 0x06b006afu, 0x06b406b1u, 0x06b406b4u, 0x06b606b5u, 0x06ba06b8u, 0x06ba06bau, 0x06be06bdu, + 0x06c106c1u, 0x06c506c2u, 0x06c806c6u, 0x06c906c9u, 0x06ce06ccu, 0x06ce06ceu, 0x06d306d3u, 0x06d706d4u, + 0x06dd06dcu, 0x06dd06ddu, 0x06de06ddu, 0x06e006deu, 0x06e406e1u, 0x06e506e5u, 0x06e806e8u, 0x06e906e8u, + 0x06ed06ebu, 0x06ee06eeu, 0x06f106eeu, 0x06f206f2u, 0x06f606f3u, 0x06fa06f9u, 0x06fc06fbu, 0x06fd06fdu, + 0x07000700u, 0x07020700u, 0x07070703u, 0x070a0709u, 0x070c070cu, 0x070d070cu, 0x070d070du, 0x070f070eu, + 0x07100710u, 0x07120710u, 0x07120712u, 0x07140714u, 0x07150714u, 0x07160715u, 0x07170717u, 0x071a0719u, + 0x0720071eu, 0x07230720u, 0x07260725u, 0x07290727u, 0x072a0729u, 0x072d072bu, 0x072f072eu, 0x0732072fu, + 0x07340733u, 0x07370735u, 0x07370737u, 0x07380737u, 0x073a0739u, 0x073b073au, 0x073e073eu, 0x07410740u, + 0x07420741u, 0x07440743u, 0x07450744u, 0x07460746u, 0x07480747u, 0x074b074au, 0x074b074bu, 0x074e074du, + 0x0751074eu, 0x07520751u, 0x07550755u, 0x07580757u, 0x07590759u, 0x075c075bu, 0x075f075fu, 0x0760075fu, + 0x07610760u, 0x07660766u, 0x07690769u, 0x076a0769u, 0x076d076bu, 0x076e076eu, 0x0771076fu, 0x07740772u, + 0x07780774u, 0x077a0778u, 0x077d077cu, 0x077e077du, 0x0781077fu, 0x07830782u, 0x07830783u, 0x07840783u, + 0x07870786u, 0x07890788u, 0x078a0789u, 0x078c078bu, 0x078d078du, 0x078d078du, 0x078e078eu, 0x07920792u, + 0x07930792u, 0x07970796u, 0x07990799u, 0x079a0799u, 0x079d079au, 0x079e079eu, 0x07a007a0u, 0x07a007a0u, + 0x07a207a0u, 0x07a407a3u, 0x07a507a5u, 0x07a507a5u, 0x07a807a6u, 0x07ac07abu, 0x07ad07adu, 0x07ad07adu, + 0x07b207aeu, 0x07b407b3u, 0x07b607b6u, 0x07b907b9u, 0x07bd07bcu, 0x07be07bdu, 0x07bf07beu, 0x07c207bfu, + 0x07c407c4u, 0x07c507c5u, 0x07c507c5u, 0x07c607c5u, 0x07c707c6u, 0x07c907c8u, 0x07ca07cau, 0x07cc07cau, + 0x07d007ceu, 0x07d307d1u, 0x07d407d4u, 0x07d607d6u, 0x07d607d6u, 0x07d907d8u, 0x07db07dbu, 0x07dc07dbu, + 0x07de07ddu, 0x07df07dfu, 0x07e207e0u, 0x07e807e5u, 0x07eb07e9u, 0x07eb07ebu, 0x07ee07eeu, 0x07ef07efu, + 0x07f407f1u, 0x07f407f4u, 0x07f807f7u, 0x07f807f8u, 0x07f807f8u, 0x07fc07fau, 0x07fe07fcu, 0x080207ffu, + 0x08040803u, 0x08060806u, 0x08080806u, 0x080a0809u, 0x080a080au, 0x080d080bu, 0x08120810u, 0x08130813u, + 0x08160814u, 0x08180818u, 0x081b0819u, 0x081e081eu, 0x0821081eu, 0x08220821u, 0x08240824u, 0x08270826u, + 0x08270827u, 0x082b0829u, 0x082e082cu, 0x082f082eu, 0x0830082fu, 0x08310830u, 0x08320832u, 0x08340833u, + 0x08360834u, 0x08360836u, 0x08380837u, 0x08390839u, 0x083d083bu, 0x083f083du, 0x08410840u, 0x08410841u, + 0x08420841u, 0x08440843u, 0x08450845u, 0x08470846u, 0x084a0849u, 0x084d084bu, 0x084e084eu, 0x084f084fu, + 0x08530852u, 0x08540854u, 0x08550855u, 0x08570855u, 0x085a0858u, 0x085b085au, 0x085c085cu, 0x085d085cu, + 0x085d085du, 0x08630861u, 0x08650864u, 0x08670865u, 0x086a0867u, 0x086e086cu, 0x08710871u, 0x08730872u, + 0x08760873u, 0x08790878u, 0x08790879u, 0x087b087au, 0x087c087bu, 0x087c087cu, 0x087d087du, 0x0880087eu, + 0x08810881u, 0x08830883u, 0x08870886u, 0x08890888u, 0x088d088cu, 0x088e088du, 0x088f088fu, 0x08900890u, + 0x08920890u, 0x08960894u, 0x08970896u, 0x08970897u, 0x08990898u, 0x089d089cu, 0x089f089du, 0x08a208a2u, + 0x08a408a4u, 0x08a808a7u, 0x08a908a8u, 0x08ab08aau, 0x08ae08acu, 0x08af08aeu, 0x08b108b0u, 0x08b208b2u, + 0x08b408b2u, 0x08b608b4u, 0x08b808b7u, 0x08bb08b9u, 0x08bc08bcu, 0x08bd08bcu, 0x08bd08bdu, 0x08be08beu, + 0x08c008bfu, 0x08c108c0u, 0x08c508c3u, 0x08ca08c8u, 0x08cd08ccu, 0x08ce08cdu, 0x08ce08ceu, 0x08d108d0u, + 0x08d208d2u, 0x08d408d4u, 0x08d408d4u, 0x08d508d5u, 0x08d808d6u, 0x08dc08d9u, 0x08dc08dcu, 0x08dd08ddu, + 0x08e008dfu, 0x08e408e2u, 0x08e408e4u, 0x08e508e4u, 0x08e708e6u, 0x08e808e7u, 0x08eb08e9u, 0x08f008eeu, + 0x08f308f2u, 0x08f608f4u, 0x08f608f6u, 0x08f908f9u, 0x08fc08f9u, 0x08fe08fcu, 0x09010901u, 0x09020902u, + 0x09040902u, 0x09050904u, 0x09060906u, 0x09080907u, 0x090b090au, 0x090d090du, 0x090f090eu, 0x09100910u, + 0x09140913u, 0x09160915u, 0x09190917u, 0x091d091cu, 0x091f091eu, 0x09220920u, 0x09260926u, 0x092a0929u, + 0x092d092bu, 0x092e092du, 0x09320931u, 0x09340933u, 0x09350935u, 0x093b0938u, 0x093e093du, 0x0943093fu, + 0x09450945u, 0x09470946u, 0x09480947u, 0x094d0949u, 0x094f094fu, 0x09500950u, 0x09550951u, 0x09560955u, + 0x09560956u, 0x09570957u, 0x09590957u, 0x095c095au, 0x095c095cu, 0x095e095eu, 0x09600960u, 0x09610960u, + 0x09630962u, 0x09650964u, 0x09670967u, 0x096b096au, 0x096e096du, 0x09720970u, 0x09740974u, 0x09740974u, + 0x09750975u, 0x09790978u, 0x097d097bu, 0x097f097fu, 0x097f097fu, 0x09810981u, 0x09820981u, 0x09850983u, + 0x098b0985u, 0x098b098bu, 0x0990098du, 0x09920990u, 0x09940993u, 0x09950994u, 0x09990998u, 0x099e099cu, + 0x099f099eu, 0x09a209a1u, 0x09a309a3u, 0x09a509a4u, 0x09a909a7u, 0x09ad09aau, 0x09b009adu, 0x09b209b0u, + 0x09b209b2u, 0x09b409b2u, 0x09b509b4u, 0x09b509b5u, 0x09b609b5u, 0x09b609b6u, 0x09b909b8u, 0x09bb09bbu, + 0x09be09bdu, 0x09bf09beu, 0x09c109c0u, 0x09c109c1u, 0x09c209c1u, 0x09c409c3u, 0x09c709c6u, 0x09c909c9u, + 0x09ce09cbu, 0x09ce09ceu, 0x09cf09ceu, 0x09cf09cfu, 0x09d109d0u, 0x09d509d2u, 0x09d909d8u, 0x09d909d9u, + 0x09de09ddu, 0x09e009dfu, 0x09e209e1u, 0x09e609e3u, 0x09e809e8u, 0x09e809e8u, 0x09ec09e9u, 0x09ef09eeu, + 0x09f109f1u, 0x09f209f2u, 0x09f409f3u, 0x09f509f4u, 0x09f609f5u, 0x09f909f6u, 0x09fa09f9u, 0x09fd09fbu, + 0x09fe09feu, 0x09ff09ffu, 0x0a030a02u, 0x0a060a05u, 0x0a090a08u, 0x0a0c0a0bu, 0x0a120a0eu, 0x0a140a13u, + 0x0a170a16u, 0x0a190a18u, 0x0a1b0a1au, 0x0a1c0a1cu, 0x0a1d0a1du, 0x0a200a1fu, 0x0a220a20u, 0x0a250a24u, + 0x0a280a27u, 0x0a2b0a2au, 0x0a2d0a2cu, 0x0a300a2du, 0x0a320a31u, 0x0a360a35u, 0x0a380a36u, 0x0a3c0a3au, + 0x0a3d0a3cu, 0x0a3d0a3du, 0x0a3d0a3du, 0x0a3e0a3du, 0x0a400a3fu, 0x0a400a40u, 0x0a410a41u, 0x0a420a41u, + 0x0a420a42u, 0x0a440a43u, 0x0a440a44u, 0x0a440a44u, 0x0a470a47u, 0x0a490a47u, 0x0a490a49u, 0x0a4b0a4au, + 0x0a4e0a4cu, 0x0a500a4eu, 0x0a530a52u, 0x0a540a54u, 0x0a560a55u, 0x0a570a56u, 0x0a580a58u, 0x0a590a58u, + 0x0a590a59u, 0x0a5d0a5cu, 0x0a5e0a5du, 0x0a5f0a5fu, 0x0a600a5fu, 0x0a640a62u, 0x0a660a64u, 0x0a670a66u, + 0x0a6a0a68u, 0x0a6b0a6bu, 0x0a6e0a6eu, 0x0a700a6fu, 0x0a710a71u, 0x0a720a71u, 0x0a730a73u, 0x0a740a74u, + 0x0a750a75u, 0x0a760a76u, 0x0a780a77u, 0x0a7b0a79u, 0x0a7c0a7bu, 0x0a7e0a7cu, 0x0a7e0a7eu, 0x0a7f0a7eu, + 0x0a820a7fu, 0x0a840a83u, 0x0a860a85u, 0x0a880a86u, 0x0a8a0a89u, 0x0a8c0a8bu, 0x0a8f0a8du, 0x0a910a90u, + 0x0a910a91u, 0x0a950a92u, 0x0a970a96u, 0x0a990a98u, 0x0a9b0a99u, 0x0a9d0a9du, 0x0a9f0a9du, 0x0aa00aa0u, + 0x0aa40aa0u, 0x0aa50aa4u, 0x0aaa0aa6u, 0x0aab0aaau, 0x0aaf0aacu, 0x0ab00aafu, 0x0ab10ab1u, 0x0ab40ab1u, + 0x0ab50ab4u, 0x0aba0ab9u, 0x0abd0abbu, 0x0abf0abeu, 0x0ac20ac0u, 0x0ac50ac3u, 0x0ac50ac5u, 0x0ac80ac8u, + 0x00000ac9u, +}; + +constexpr std::array<std::uint32_t, 1381> ReciprocalCorrections{ + 0x09230922u, 0x0dcd0dccu, 0x01d101d0u, 0x04e704e6u, 0x0e8d0e8cu, 0x05df05deu, 0x07a507a4u, 0x085b085au, + 0x0f7f0f7eu, 0x0e070e06u, 0x01a901a8u, 0x0bdf0bdeu, 0x06e306e2u, 0x04db04dau, 0x02650264u, 0x0a430a42u, + 0x0fda0e3au, 0x039b0fdbu, 0x03df03deu, 0x0ca30ca2u, 0x096e0c74u, 0x0df5096fu, 0x0e7c0b2au, 0x09020e7du, + 0x08c409abu, 0x005b005au, 0x096e072eu, 0x0e7d096fu, 0x04ff07a9u, 0x07530752u, 0x0a830a82u, 0x04670466u, + 0x087f087eu, 0x06ef0524u, 0x02880cb3u, 0x05b70289u, 0x0d2b0d2au, 0x05d5013eu, 0x08fb0ef3u, 0x03020a8du, + 0x0906042eu, 0x089f00d9u, 0x0f340ea5u, 0x03790f43u, 0x045b045au, 0x01b60f1fu, 0x02970938u, 0x062d0626u, + 0x02b408cfu, 0x0f670c20u, 0x02b50068u, 0x0cdd0cdcu, 0x06e10ebeu, 0x00ab0c9eu, 0x08a7076du, 0x01230da0u, + 0x02590258u, 0x056e051eu, 0x0ecb08ffu, 0x02ba06dcu, 0x03ac0396u, 0x03c303adu, 0x03720927u, 0x01250031u, + 0x12f6018fu, 0x058e0035u, 0x001101e0u, 0x0a6f06a0u, 0x03c2017eu, 0x0975055fu, 0x06490f45u, 0x09520742u, + 0x074d051fu, 0x087204f7u, 0x0c5a0a55u, 0x01650c5bu, 0x01f30dabu, 0x0d5b0364u, 0x087f0368u, 0x0e730953u, + 0x036101bcu, 0x064a1e36u, 0x03a602d2u, 0x0d240954u, 0x081e002au, 0x06690e87u, 0x035b0e6bu, 0x0aa9058au, + 0x044a0018u, 0x06f504f5u, 0x01160bbbu, 0x068101bfu, 0x02790121u, 0x033b0e62u, 0x0dc80b27u, 0x046b014du, + 0x00a70cb1u, 0x03a10230u, 0x03320549u, 0x092708ddu, 0x00640c76u, 0x193b01acu, 0x02eb0998u, 0x0fa30e9du, + 0x0d680286u, 0x0ae40808u, 0x03870f6du, 0x09fa0451u, 0x0a960098u, 0x09ef0cbeu, 0x031702c2u, 0x017d0185u, + 0x02ed03c8u, 0x003e0453u, 0x068801c4u, 0x0f4d0d62u, 0x0b360fe3u, 0x0afb0293u, 0x064a0c79u, 0x042a0ed6u, + 0x014e0c5du, 0x03530535u, 0x0499039fu, 0x02460953u, 0x086f0303u, 0x07b50d76u, 0x0dc40928u, 0x024e0a5au, + 0x068304ecu, 0x07b2076au, 0x07750f6fu, 0x0d3c02a5u, 0x0c6c03bfu, 0x086a00c7u, 0x02b50270u, 0x00ad06cdu, + 0x05b00515u, 0x097b063cu, 0x04d00df2u, 0x09890e9cu, 0x0d830992u, 0x0f40009au, 0x0d86022bu, 0x0ee70e34u, + 0x09560897u, 0x0d240cf7u, 0x06650343u, 0x0488008fu, 0x06bf053cu, 0x0ba80c1cu, 0x0caf0c32u, 0x071f0d84u, + 0x0ded0b02u, 0x07c40558u, 0x00220b18u, 0x018b04cdu, 0x098d05e3u, 0x0b1e0fb9u, 0x01710dc4u, 0x030b0accu, + 0x0f4d0a1cu, 0x01b20fcbu, 0x0273021bu, 0x0b27092du, 0x0d640d14u, 0x08cb0258u, 0x06b10bb5u, 0x0ab506a2u, + 0x01281af9u, 0x0eb00d04u, 0x06b102d1u, 0x0bc70b39u, 0x096b0783u, 0x062b0d74u, 0x01500e6fu, 0x1a11033bu, + 0x09ad0f37u, 0x07150d85u, 0x0a480fabu, 0x081b098eu, 0x0c850be0u, 0x03fc04bfu, 0x0e160d48u, 0x0e130c66u, + 0x075c01f8u, 0x03580e3du, 0x00bd0877u, 0x064602b4u, 0x052b0697u, 0x00bd0c4du, 0x02270179u, 0x096e06bbu, + 0x011c0a9cu, 0x04f80a78u, 0x0fcb020bu, 0x17e301a8u, 0x0e9909d5u, 0x0cbf06abu, 0x072d019du, 0x07110d1eu, + 0x03e60785u, 0x0f5807b7u, 0x076b0114u, 0x009f08b3u, 0x048b0153u, 0x09ed0879u, 0x0eeb0a69u, 0x05c500e7u, + 0x06530d54u, 0x18010680u, 0x06b502c8u, 0x0a8209f2u, 0x062204f0u, 0x10440a5au, 0x042e0254u, 0x0ca908a8u, + 0x0073003au, 0x074103a5u, 0x025a1908u, 0x02a10ce1u, 0x03e503c9u, 0x06ab043cu, 0x097008a3u, 0x05c20d91u, + 0x03250b4cu, 0x0cc70a0fu, 0x071a006eu, 0x00710c41u, 0x0b5a00b4u, 0x00300e76u, 0x08fa0994u, 0x0dfa0973u, + 0x054104cfu, 0x0d5c0d47u, 0x0abf0257u, 0x021d0bc4u, 0x043c03c3u, 0x0e16087fu, 0x09b30eafu, 0x17000164u, + 0x07750726u, 0x02f208d3u, 0x03f306e3u, 0x06280379u, 0x0dce0ae4u, 0x02c400c3u, 0x09d6067cu, 0x0bf10552u, + 0x09eb00bau, 0x0f890af0u, 0x03640035u, 0x00520f97u, 0x05590223u, 0x0c16028du, 0x0d840c21u, 0x068c0130u, + 0x00360f87u, 0x0598018eu, 0x080f07c4u, 0x04110cc6u, 0x09e404eeu, 0x089802d2u, 0x0ed60c97u, 0x0cd10722u, + 0x0ecf0dbbu, 0x0f6008f8u, 0x0b650931u, 0x06dc0cc0u, 0x0a9e07ddu, 0x0ed90ca7u, 0x01150058u, 0x0d020745u, + 0x06e80cc6u, 0x086f072au, 0x013f0e21u, 0x0fc61bdeu, 0x06ff00cau, 0x061a0d5fu, 0x073b06cfu, 0x0f650a40u, + 0x0bc60234u, 0x00280bd0u, 0x0bcd00c3u, 0x006e0f98u, 0x057e04c6u, 0x01ae07d0u, 0x09d7043du, 0x08710d0eu, + 0x08220b46u, 0x0a3909c6u, 0x0c320ab3u, 0x02e70dd1u, 0x0d850f0bu, 0x027f0ddcu, 0x0599044cu, 0x07710a83u, + 0x1b610ae2u, 0x0cca0b68u, 0x001c06aau, 0x002f08a7u, 0x0b990a66u, 0x04a4043du, 0x09c402d4u, 0x0ba20ba9u, + 0x02680fe7u, 0x0b8908a8u, 0x09ce0491u, 0x01af0966u, 0x0c3e0797u, 0x02750f5cu, 0x02d70a0du, 0x0057064au, + 0x0126018au, 0x0ed30792u, 0x0a5504c3u, 0x0cef04b9u, 0x07d10de6u, 0x0f5d0a98u, 0x0f8a0d52u, 0x03c60aebu, + 0x0caf088fu, 0x06b406eau, 0x09040a91u, 0x0bbb0a57u, 0x08a6003au, 0x0f590de3u, 0x06020fb7u, 0x008e083du, + 0x01940124u, 0x07a101bbu, 0x0f1e0c68u, 0x092c0310u, 0x09b4096du, 0x0d120297u, 0x02b70e2du, 0x0c2e19ceu, + 0x118d00a9u, 0x0cf7067fu, 0x01790e25u, 0x0dd91421u, 0x0f040dfeu, 0x0b3c0133u, 0x033b013bu, 0x0ac2048eu, + 0x0d050bcdu, 0x0da40eddu, 0x0ae2046fu, 0x0d1602a4u, 0x01d20669u, 0x0f9b0f24u, 0x04cb1b07u, 0x0d2f06feu, + 0x0f4c0ad0u, 0x076e0fd4u, 0x039a0ea2u, 0x05b203a2u, 0x0cfe0fc6u, 0x00040000u, 0x0cee04a8u, 0x0a9313e9u, + 0x08e60433u, 0x03370317u, 0x0f12092cu, 0x0e8e06b0u, 0x07191400u, 0x02480745u, 0x1bc10a4fu, 0x0930051du, + 0x18de0e5fu, 0x069f051au, 0x0e8e0abbu, 0x09a10231u, 0x0fe10afbu, 0x02f70146u, 0x0a38020cu, 0x0b380995u, + 0x0b420727u, 0x091a0494u, 0x07361c40u, 0x0c320376u, 0x09b10949u, 0x0a270370u, 0x059302c5u, 0x0f4a06cbu, + 0x0e1e069eu, 0x0e0503f9u, 0x0df6017cu, 0x07cb0184u, 0x0c1d08bau, 0x0e3500b8u, 0x02730c99u, 0x0a3f06ebu, + 0x063d06e2u, 0x05ff086cu, 0x0a8208b7u, 0x085f0660u, 0x02dd0f51u, 0x06130576u, 0x061a0581u, 0x00380e54u, + 0x06b102dfu, 0x0752029eu, 0x1c810abbu, 0x0a4502dbu, 0x0ecb0d85u, 0x051a01a6u, 0x09ea0774u, 0x0e390cf0u, + 0x04c40019u, 0x055100cfu, 0x037206c8u, 0x049403b8u, 0x005a153du, 0x0243011du, 0x00ac007cu, 0x088c06f7u, + 0x0ce70b5cu, 0x0e7b0d84u, 0x040902a2u, 0x031a084bu, 0x002e064au, 0x0c9d0cd2u, 0x00c8036cu, 0x0f1a07a7u, + 0x07420150u, 0x0b850f5du, 0x0bb20072u, 0x0cae0dbau, 0x06891371u, 0x04f40adfu, 0x08360659u, 0x12810f3bu, + 0x019108fdu, 0x037e0c9du, 0x0f2f0b57u, 0x0cc405a8u, 0x064a07d5u, 0x08340f67u, 0x02410d1fu, 0x08c506cbu, + 0x0d3e0d7fu, 0x088a0198u, 0x0d3b0eb6u, 0x08340031u, 0x01591f7du, 0x03e20cdfu, 0x0cf30c50u, 0x00fe1ef7u, + 0x050f01c4u, 0x07ac0c36u, 0x0658057du, 0x02c80743u, 0x0d3e0d4cu, 0x06570264u, 0x01ee0e6eu, 0x0ab30a4bu, + 0x01160098u, 0x00e307e3u, 0x07da050bu, 0x0349013eu, 0x00d10568u, 0x023014d7u, 0x0efa02a0u, 0x0b9f0f6au, + 0x0ff50d0fu, 0x034e08b8u, 0x03f40a10u, 0x09d60958u, 0x09670336u, 0x02d70e9du, 0x08490548u, 0x073f0012u, + 0x007c0b8fu, 0x19120fc8u, 0x0ed008dcu, 0x0d890533u, 0x030802fbu, 0x0e410c8du, 0x1cb80224u, 0x0e7d08fcu, + 0x18c108beu, 0x06ef0e4bu, 0x0b0b0b85u, 0x02bb019cu, 0x09331414u, 0x01900342u, 0x063b1bf8u, 0x0d2a0d59u, + 0x0b7a090du, 0x087f0e27u, 0x06dc046du, 0x05380a6du, 0x09070314u, 0x0ce40ee0u, 0x0a8d09b7u, 0x0e680367u, + 0x0d920486u, 0x091b0288u, 0x0d010fb4u, 0x09d80eceu, 0x0ea6048au, 0x02da0b5du, 0x01570b88u, 0x016c078bu, + 0x074d037cu, 0x08be078du, 0x025f0b3fu, 0x05360c4cu, 0x09b706f9u, 0x04b00b46u, 0x0c1208ebu, 0x1e8a0a46u, + 0x00080f72u, 0x003a0021u, 0x13f402cfu, 0x0642053du, 0x00e700aau, 0x04b10ad4u, 0x0fa00e90u, 0x00970eb0u, + 0x04740b05u, 0x16710ba9u, 0x0a3007d6u, 0x0b8a0a4fu, 0x0d2d0af9u, 0x02e109bbu, 0x0c650d75u, 0x0f5c01d9u, + 0x01990f6fu, 0x0ea30ac5u, 0x0d1507b3u, 0x0f7a0ed1u, 0x05ec0455u, 0x01b609e8u, 0x04bd0cc1u, 0x07bd0bd0u, + 0x02470b13u, 0x062a19afu, 0x04da0a7cu, 0x0ee308cau, 0x0be5000fu, 0x046d0ef7u, 0x050d0eccu, 0x04780947u, + 0x0b141721u, 0x04a30c26u, 0x00f30e27u, 0x086a0034u, 0x0efb0e58u, 0x09470553u, 0x0f770bf9u, 0x0c100059u, + 0x077b02e8u, 0x0e8507d0u, 0x0e3f075fu, 0x073601c9u, 0x05640dd8u, 0x075f0a4bu, 0x0ee60e3du, 0x057a0212u, + 0x0db10c5au, 0x067f0008u, 0x039301bbu, 0x085e0565u, 0x0efa0bc3u, 0x03380f6fu, 0x0cba0034u, 0x14d7100fu, + 0x096d006eu, 0x03d40d1au, 0x049609eeu, 0x0f7f0b78u, 0x0a0e08feu, 0x01e20b90u, 0x0e6705b0u, 0x0d3310c3u, + 0x055f0302u, 0x096b08c2u, 0x1acc09c1u, 0x08aa0345u, 0x037602c2u, 0x057504d4u, 0x0f3b0ee8u, 0x0aa00f66u, + 0x04fc0215u, 0x0e43005eu, 0x0ba60350u, 0x04c81cf9u, 0x0df707afu, 0x069707cbu, 0x0ed70965u, 0x007b05bcu, + 0x0c960f1au, 0x05f60c04u, 0x0e800dcbu, 0x09c70f82u, 0x04b40612u, 0x04e30c57u, 0x09c508b9u, 0x045b1f4au, + 0x0aa814d7u, 0x0c2b0ecbu, 0x180a1ef5u, 0x0ab0045du, 0x039100b3u, 0x0c7c0587u, 0x0d5b02a9u, 0x002d000eu, + 0x09720c68u, 0x0ecf0231u, 0x03c70afau, 0x0d6c06f5u, 0x01320bcfu, 0x04e811cfu, 0x0cdf0f7bu, 0x06ca09deu, + 0x0b0e06c8u, 0x01230562u, 0x0fb20b65u, 0x02c70882u, 0x08c40749u, 0x0d480b66u, 0x177103cfu, 0x06ca09f2u, + 0x063c0456u, 0x0add00d5u, 0x0b3308afu, 0x0ea30f84u, 0x0b7204d7u, 0x04e70e35u, 0x043b0a95u, 0x18450cbfu, + 0x0fd70a2bu, 0x0c8c03e2u, 0x087a008au, 0x1dd31d76u, 0x00300f35u, 0x0bd6084eu, 0x01050d8bu, 0x06600568u, + 0x0c9e0c4du, 0x13550f6au, 0x15bc073au, 0x082306ddu, 0x02500c6cu, 0x15710e9bu, 0x084f1eedu, 0x0a5f0949u, + 0x046a000eu, 0x092317f2u, 0x0cd11c78u, 0x05570228u, 0x061c008bu, 0x092d081du, 0x04b40b07u, 0x01b706ecu, + 0x099414f2u, 0x120d0a3cu, 0x0e600a1bu, 0x1719049cu, 0x08da0984u, 0x0627033eu, 0x011a09c4u, 0x0727010du, + 0x04f0082cu, 0x057a015du, 0x1a8509d7u, 0x0d2b1cccu, 0x097d0792u, 0x01c80e69u, 0x02bb0366u, 0x009b0c68u, + 0x0a2909f7u, 0x0e7d071au, 0x0e941c04u, 0x03a911d3u, 0x0a240957u, 0x0c940bf4u, 0x0b5f0b28u, 0x028e0fc5u, + 0x08ae0d84u, 0x05d20c71u, 0x05710aedu, 0x052e0a9cu, 0x054e0ac8u, 0x0f880c7eu, 0x0ef6083du, 0x006f0a28u, + 0x19d50787u, 0x02ac0f28u, 0x0a5f033cu, 0x1e5c0c9bu, 0x0d2f0553u, 0x0fe60f84u, 0x08ae0214u, 0x09d1016au, + 0x083d0d58u, 0x11e00e5du, 0x02ea0e50u, 0x0e27081du, 0x028002eau, 0x099205d2u, 0x0c5e0726u, 0x09ba1e66u, + 0x00290ef6u, 0x05b50223u, 0x079305efu, 0x06e31f47u, 0x00b00e11u, 0x02b0028fu, 0x09fe0634u, 0x024901d1u, + 0x07251586u, 0x0a681520u, 0x02571e25u, 0x0e8802c8u, 0x0f130029u, 0x1aa606ecu, 0x1c6015fau, 0x0f990ef3u, + 0x089406f0u, 0x02b30befu, 0x0f1b0d89u, 0x04a60145u, 0x10980c5bu, 0x06a2017fu, 0x167f1e12u, 0x0b0c19a0u, + 0x19a500ceu, 0x04f6018eu, 0x0e710c36u, 0x00120f15u, 0x00001e03u, 0x0156194fu, 0x14b80330u, 0x1e570bc2u, + 0x01050f3du, 0x1af905a8u, 0x09eb0862u, 0x0d5f05b1u, 0x048c03ebu, 0x0963061au, 0x19dc019du, 0x04691bebu, + 0x07f20e07u, 0x0fcf0bbbu, 0x1cc007fdu, 0x1e800b64u, 0x04651b38u, 0x0b9d0a7du, 0x0b720005u, 0x0fd6063du, + 0x06ae11bau, 0x013816b1u, 0x082c021bu, 0x03221beau, 0x16f906f8u, 0x025001a2u, 0x009f1a20u, 0x0acd100fu, + 0x0cbf0ef2u, 0x0569036du, 0x070e102bu, 0x06210b4du, 0x18fa0d3eu, 0x04fd0b06u, 0x084507b6u, 0x0bea1a45u, + 0x08be0573u, 0x0d0a1be9u, 0x192700a4u, 0x0ad60a34u, 0x05410f23u, 0x08c40c2du, 0x08bf026du, 0x007e09bbu, + 0x012c1041u, 0x0a5911b9u, 0x18a202c8u, 0x0b540a76u, 0x07af0530u, 0x040309e1u, 0x11f90406u, 0x0b5f032fu, + 0x0db00d5eu, 0x081b07deu, 0x0e291f5eu, 0x1ab202d6u, 0x0ab60b13u, 0x0b3107b4u, 0x069c11f8u, 0x09a3163eu, + 0x056b026cu, 0x042608b5u, 0x01c30788u, 0x079414e1u, 0x085f054fu, 0x02fd1d41u, 0x169e0794u, 0x0d5d1680u, + 0x074f1ffcu, 0x192e091au, 0x1b411e08u, 0x0b4f0a9au, 0x0e570b1eu, 0x0288001fu, 0x078e05a2u, 0x01b01c0du, + 0x069313e1u, 0x0ad708feu, 0x0ed01dcfu, 0x060b0a56u, 0x03c807e4u, 0x1fbc0d62u, 0x038c1d8du, 0x033609fcu, + 0x09ca086au, 0x0a260bacu, 0x0f7708e4u, 0x0774040fu, 0x0e340e69u, 0x0cfe0e3fu, 0x0e9f0fbfu, 0x0a4f0715u, + 0x01160fcdu, 0x075f0642u, 0x0a3f090du, 0x0dac0382u, 0x03b40ce9u, 0x16410523u, 0x0355003eu, 0x02590d33u, + 0x0c6516bdu, 0x088f05c3u, 0x068a0971u, 0x0f4b0dd7u, 0x07d9004fu, 0x086a1548u, 0x07341000u, 0x068c0274u, + 0x00090000u, 0x001b0012u, 0x002d0024u, 0x003f0036u, 0x03a006e2u, 0x0c7a0985u, 0x13a310f2u, 0x0b491025u, + 0x033800d3u, 0x1c7206e3u, 0x0c691176u, 0x1a11016bu, 0x0cc20b72u, 0x05710df1u, 0x15680f1au, 0x0cd108aau, + 0x04630ce6u, 0x0c35050eu, 0x046004afu, 0x0fed061du, 0x1be00967u, 0x0d1d16c4u, 0x0f640e55u, 0x07e00417u, + 0x14190c5au, 0x1d921554u, 0x1df90ea5u, 0x0eb00a5eu, 0x110309cau, 0x0e5a0df9u, 0x0e690a65u, 0x075801eeu, + 0x0db30a05u, 0x0ee30f7bu, 0x04cf02d1u, 0x008a094du, 0x08a51872u, 0x0e300bd0u, 0x09450f24u, 0x0fb7016cu, + 0x00960bcbu, 0x0ade00f8u, 0x10940bfcu, 0x075e1465u, 0x031a0539u, 0x00111b10u, 0x0b15187bu, 0x035200b4u, + 0x0a2e1bdeu, 0x112e195bu, 0x095a13eau, 0x09ce085eu, 0x18060f1du, 0x07130d9du, 0x182d0b88u, 0x1c990a9du, + 0x1ac702fdu, 0x01cc0bf4u, 0x04ea04d7u, 0x05870cd2u, 0x0afe0a48u, 0x0d421cb2u, 0x02d71310u, 0x0df21bf7u, + 0x0c0e165eu, 0x040a0165u, 0x015401adu, 0x11a0021bu, 0x0daa0d0eu, 0x07780545u, 0x0a3c098eu, 0x1f620f4bu, + 0x04da034du, 0x038f1e83u, 0x01ef0a87u, 0x014e0b47u, 0x19c6028du, 0x1264026bu, 0x075b0a7eu, 0x0480044cu, + 0x0c4a1acbu, 0x0024022fu, 0x11820dc5u, 0x08241f8fu, 0x07c00859u, 0x03f50058u, 0x0d1a0430u, 0x099306a9u, + 0x00d60f4du, 0x0eb308b5u, 0x10360779u, 0x0c6e0606u, 0x05e603b3u, 0x032a0b25u, 0x11c00ea9u, 0x1632033eu, + 0x0d40078bu, 0x022710a8u, 0x03530a0fu, 0x07f206edu, 0x02c209e0u, 0x0f9a0cd0u, 0x0e1f1817u, 0x099f0fc7u, + 0x00331fbeu, 0x05a814f9u, 0x08cb16f3u, 0x0b0004ccu, 0x07430d5du, 0x04330e7eu, 0x17e20c33u, 0x03ec1ddcu, + 0x03ea1604u, 0x0eac0c76u, 0x00210000u, 0x0d0d19dcu, 0x0ae615fcu, 0x1b300887u, 0x0a060fdbu, 0x1d4a08dau, + 0x0ef51ff9u, 0x0514076fu, 0x0258005fu, 0x0d641050u, 0x015b0045u, 0x08090032u, 0x0c250a8du, 0x07030422u, + 0x0d180a87u, 0x00860588u, 0x0e5608c4u, 0x11110c54u, 0x15110b74u, 0x096d0068u, 0x089c0b25u, 0x064102dau, + 0x15cd048au, 0x00f308f1u, 0x0ddb1fb3u, 0x0d8e06aau, 0x07360644u, 0x02910f00u, 0x076e16bfu, 0x00420000u, + 0x0f821945u, 0x1e1202c5u, 0x08170125u, 0x07d50672u, 0x03131806u, 0x1b5904beu, 0x0c5a03dau, 0x104908f8u, + 0x0f6e1de5u, 0x1c2016a6u, 0x01ec02b7u, 0x0bbc11f9u, 0x1df102d1u, 0x0e880345u, 0x06190350u, 0x1935015eu, + 0x0185006bu, 0x03640955u, 0x165711ceu, 0x0a3102fbu, 0x08d802fbu, 0x0c760ac8u, 0x07ed0103u, 0x02040b8eu, + 0x0ff50cdcu, 0x1e6707c3u, 0x0d351edfu, 0x1ea41e21u, 0x00450603u, 0x055c0b12u, 0x0fab1f82u, 0x08430a7eu, + 0x005419ddu, 0x076416beu, 0x08c3014au, 0x0b9d19a3u, 0x09a112b2u, 0x1b6906d8u, 0x0262011cu, 0x014715f9u, + 0x076d15e0u, 0x0aae0380u, 0x086d022eu, 0x1fe3003eu, 0x01071660u, 0x135d0e1du, 0x070a16c5u, 0x0c7c0ad7u, + 0x1f540663u, 0x061612b5u, 0x1ce40423u, 0x06561183u, 0x03b904ccu, 0x04470860u, 0x0c0e0a18u, 0x1a4f0997u, + 0x00670b7du, 0x0e9f113du, 0x0a601481u, 0x05560d5bu, 0x08cb0a58u, 0x1e190c37u, 0x164301fbu, 0x03370418u, + 0x04c20a9cu, 0x023918a4u, 0x079b0010u, 0x069f00a9u, 0x00a40146u, 0x141c0687u, 0x13d91a7eu, 0x10b81811u, + 0x0b62170au, 0x1bc10fbbu, 0x017d00a1u, 0x04a707dcu, 0x019c08d3u, 0x03dd1ea4u, 0x0c190f3au, 0x071b028eu, + 0x09990925u, 0x01570979u, 0x0529006fu, 0x03e118f1u, 0x1b7e01d2u, 0x14d609cbu, 0x11280c7eu, 0x01b90190u, + 0x006b09b0u, 0x0f0c0566u, 0x043a0d40u, 0x055c05e4u, 0x0f510979u, 0x17a50103u, 0x0f3c0688u, 0x126c1269u, + 0x05251cbfu, 0x000704d9u, 0x19910048u, 0x0fe21090u, 0x1ecb17b3u, 0x0f2e0c18u, 0x05460a35u, 0x07560314u, + 0x0d68054du, 0x13291e85u, 0x0d751c10u, 0x1d4815b4u, 0x041d08f3u, 0x0f8107b4u, 0x0b520641u, 0x02f20cefu, + 0x09e70742u, 0x19e004d9u, 0x0e2d0c5du, 0x00201b11u, 0x108e0118u, 0x03cf03ccu, 0x003b0b77u, 0x012a08d5u, + 0x0aca09a4u, 0x0ed80807u, 0x18171265u, 0x057f097fu, 0x043d01cau, 0x005d0c51u, 0x1efd079au, 0x0c03012fu, + 0x184e1258u, 0x00310000u, 0x1f960f23u, 0x11f00ff4u, 0x140f038du, 0x0a421607u, 0x01620388u, 0x0eee0d9bu, + 0x0cd80a39u, 0x0aea0d38u, 0x05b90ba7u, 0x0d250730u, 0x07830276u, 0x0c2915f9u, 0x0ba20be3u, 0x0aa60303u, + 0x0e861e0cu, 0x1fbb07aeu, 0x04bf0d1cu, 0x11b4005au, 0x10b11792u, 0x1d870202u, 0x023b0bfdu, 0x15b81f61u, + 0x0b3008bcu, 0x0cf01148u, 0x10800ec6u, 0x0fd8018bu, 0x00e314c5u, 0x005805d4u, 0x08961309u, 0x15a400cbu, + 0x1b481956u, 0x04770015u, 0x0d021957u, 0x16a713feu, 0x008b0876u, 0x1d5318c1u, 0x09a70279u, 0x08d703cau, + 0x05310a00u, 0x0dba1a6bu, 0x03970e3eu, 0x00301562u, 0x1cc214d6u, 0x168c0949u, 0x1a0206b1u, 0x0def0f0cu, + 0x02460055u, 0x03ca05c4u, 0x0b591ac3u, 0x188f128eu, 0x01980a22u, 0x156502a8u, 0x07cd100cu, 0x1a7004b2u, + 0x0151007eu, 0x1863023du, 0x0ac10a73u, 0x05df1469u, 0x146f0666u, 0x0f27043bu, 0x17161f46u, 0x0a7314a8u, + 0x05d90c56u, 0x0bb215d3u, 0x03ba0edeu, 0x08c716d7u, 0x14450138u, 0x02d01b31u, 0x0eea0cc4u, 0x016d0326u, + 0x058404c4u, 0x03c5066du, 0x066105deu, 0x05ef014cu, 0x056d05a8u, 0x17c0165cu, 0x02b109a3u, 0x0382092cu, + 0x13e707dcu, 0x0c8a143du, 0x05550d36u, 0x13b916adu, 0x0e9f0f47u, 0x0f3f137cu, 0x0c1010d1u, 0x04c80072u, + 0x006403e8u, 0x06a20a4au, 0x0a3e004au, 0x03720f4eu, 0x0eae0d91u, 0x05980076u, 0x1bf10a5au, 0x15191332u, + 0x1b980322u, 0x0bdc1ad4u, 0x121a1ef7u, 0x048f03feu, 0x17000b34u, 0x18fc0c20u, 0x04e61979u, 0x12241579u, + 0x074a14d7u, 0x05860361u, 0x000e0be1u, 0x0030001fu, 0x00520041u, 0x00f61e9bu, 0x00130615u, 0x0c7e0964u, + 0x045f00cdu, 0x14061a21u, 0x023d085bu, 0x093d06fbu, 0x046d0deau, 0x0a6407d6u, 0x194d1640u, 0x00a6085bu, + 0x013009bcu, 0x058116a0u, 0x09541decu, 0x03c01dd0u, 0x0d240864u, 0x05950108u, 0x16761db9u, 0x0ef606a8u, + 0x15fe05ebu, 0x0f7715e9u, 0x174d1f96u, 0x1e3504a6u, 0x0165004bu, 0x02130278u, 0x09a90a61u, 0x177d063cu, + 0x09070105u, 0x05ca1855u, 0x1a051152u, 0x07851330u, 0x1df209a7u, 0x00001f55u, 0x07440007u, 0x1a5d1cbfu, + 0x0cbc0885u, 0x089f00d2u, 0x02e91d3au, 0x0bf9092bu, 0x11551958u, 0x19b503cau, 0x06b40a1au, 0x18110ed0u, + 0x05bf07a6u, 0x03cf07fdu, 0x1d4008f3u, 0x0bf312c8u, 0x000a070au, 0x0e1a0abeu, 0x1e5e1dc8u, 0x128006c2u, + 0x0d861299u, 0x0ba303c8u, 0x07761fceu, 0x0b7409f7u, 0x09bd1efcu, 0x05ad184fu, 0x0aba1b89u, 0x00790713u, + 0x0a631655u, 0x09640f98u, 0x132f196du, 0x043e0351u, 0x13c707c7u, 0x08630605u, 0x0ee30212u, 0x0e6e0397u, + 0x0f0a1ebcu, 0x0fc41ec8u, 0x0c651a13u, 0x0ada1d6cu, 0x09571288u, 0x062a0916u, 0x087e14b4u, 0x0bd70e45u, + 0x172611e8u, 0x0e6815b9u, 0x12850c43u, 0x17db14a3u, 0x0f9b1a71u, 0x07021de6u, 0x08f01334u, 0x1dba1ccbu, + 0x07bd01d0u, 0x16440166u, 0x017a1ad3u, 0x1d210b61u, 0x160c1080u, 0x070c0405u, 0x0537192bu, 0x08d412acu, + 0x0b1d0786u, 0x0a560117u, 0x04a20130u, 0x04ca0b7bu, 0x082709bfu, 0x0da50a93u, 0x19590007u, 0x16ba068bu, + 0x1ed401c3u, 0x00b50f57u, 0x165c0a4fu, 0x0a680e76u, 0x010c1db6u, 0x0a7c07e9u, 0x12e818f3u, 0x1f991014u, + 0x155c02f6u, 0x0e7600f4u, 0x0747008eu, 0x1a6413d0u, 0x0d0303b6u, 0x12d105e2u, 0x04bc03e6u, 0x01741353u, + 0x12c1062du, 0x1a8c0f24u, 0x046606d9u, 0x11c106d6u, 0x18850fa7u, 0x0cce097au, 0x1f9b1509u, 0x000001eeu, + 0x003e001fu, 0x1b2c08b8u, 0x152b0237u, 0x1deb04ebu, 0x14001b2fu, 0x11280499u, 0x006b1be7u, 0x160015e1u, + 0x00f41df0u, 0x08181371u, 0x01530a86u, 0x0da20b03u, 0x0a820433u, 0x049c0ab5u, 0x0816059fu, 0x0d640e1fu, + 0x0cd00303u, 0x1f9b1d50u, 0x0f040c3cu, 0x19e017dbu, 0x0fb20b51u, 0x18fa0384u, 0x0d4f08e4u, 0x0a890da6u, + 0x07e7120fu, 0x1de00cf4u, 0x04e01320u, 0x0826070au, 0x1a740ff8u, 0x012a1619u, 0x1a2d0481u, 0x00741488u, + 0x069d0388u, 0x11f00f3cu, 0x03ff1615u, 0x1720071eu, 0x00001486u, +}; + +template<std::size_t Size> +std::uint32_t DefineTable(SpirvEmitterState& state, const std::array<std::uint32_t, Size>& values) { + std::array<std::uint32_t, Size> words{}; + for (std::size_t index = 0; index < Size; ++index) words[index] = ConstantU32(state, values[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast<std::uint32_t>(Size))); + const auto table = state.module.Constant(spv::OpConstantComposite, arrayType, std::span<const std::uint32_t>(words)); + return state.module.DefineInitializedGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate, table); +} + +std::uint32_t LoadTable(SpirvEmitterState& state, std::uint32_t table, std::uint32_t index) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, TypeU32(state)), pointer, table, index); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::array<std::uint32_t, 2> MultiplyExtended(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs) { + const auto product = state.module.AllocateId(); + state.module.AddFunction(spv::OpUMulExtended, TypeU32Pair(state), product, lhs, rhs); + std::array<std::uint32_t, 2> words{state.module.AllocateId(), state.module.AllocateId()}; + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), words[0], product, 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), words[1], product, 1u); + return words; +} + +std::uint32_t ReciprocalCorrection(SpirvEmitterState& state, std::uint32_t mantissa) { + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; + const auto starts = DefineTable(state, ReciprocalCorrectionStarts); + const auto corrections = DefineTable(state, ReciprocalCorrections); + const auto halfword = [&](std::uint32_t table, std::uint32_t index, std::uint32_t last) { + const auto clamped = Select(state, u32, Binary(state, spv::OpULessThan, boolean, index, ConstantU32(state, last)), index, ConstantU32(state, last)); + const auto word = LoadTable(state, table, op(spv::OpShiftRightLogical, clamped, 1u)); + return op(spv::OpBitwiseAnd, Binary(state, spv::OpShiftRightLogical, u32, word, op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, clamped, 1u), 4u)), 0xffffu); + }; + const auto lastStart = static_cast<std::uint32_t>(ReciprocalCorrectionStarts.size() * 2u - 1u); + const auto lastCorrection = static_cast<std::uint32_t>(ReciprocalCorrections.size() * 2u - 1u); + const auto bucket = op(spv::OpShiftRightLogical, mantissa, 12u); + const auto key = op(spv::OpBitwiseAnd, mantissa, 0xfffu); + const auto end = halfword(starts, op(spv::OpIAdd, bucket, 1u), lastStart); + auto first = halfword(starts, bucket, lastStart); + auto remaining = Binary(state, spv::OpISub, u32, end, first); + for (const std::uint32_t step : {8u, 4u, 2u, 1u}) { + const auto probe = op(spv::OpBitwiseAnd, halfword(corrections, op(spv::OpIAdd, first, step - 1u), lastCorrection), 0xfffu); + const auto inside = Binary(state, spv::OpULessThanEqual, boolean, ConstantU32(state, step), remaining); + const auto skip = Binary(state, spv::OpLogicalAnd, boolean, inside, Binary(state, spv::OpULessThan, boolean, probe, key)); + first = Select(state, u32, skip, op(spv::OpIAdd, first, step), first); + remaining = Select(state, u32, skip, op(spv::OpISub, remaining, step), remaining); + } + const auto entry = halfword(corrections, first, lastCorrection); + const auto hit = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpULessThan, boolean, first, end), Binary(state, spv::OpIEqual, boolean, op(spv::OpBitwiseAnd, entry, 0xfffu), key)); + const auto delta = Select(state, u32, Binary(state, spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, entry, 0x1000u), ConstantU32(state, 0u)), ConstantU32(state, 1u), ConstantU32(state, 0xffffffffu)); + return Select(state, u32, hit, delta, ConstantU32(state, 0u)); +} + +bool ExactReciprocal() { + static const bool exact = std::getenv("APS5_EXACT_RCP") != nullptr; + return exact; +} + +std::uint32_t DeviceReciprocal(SpirvEmitterState& state, std::uint32_t value) { + return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), value); +} + +std::uint32_t HardwareReciprocal(SpirvEmitterState& state, std::uint32_t value) { + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); + const auto boolean = TypeBool(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t constant) { return Binary(state, opcode, u32, lhs, ConstantU32(state, constant)); }; + const auto add = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIAdd, u32, lhs, rhs); }; + const auto sub = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpISub, u32, lhs, rhs); }; + const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, boolean, lhs, rhs); }; + const auto pick = [&](std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, u32, condition, ConstantU32(state, whenTrue), ConstantU32(state, whenFalse)); }; + if (state.reciprocalTables[0] == 0u) { + state.reciprocalTables = {DefineTable(state, ReciprocalConstant), DefineTable(state, ReciprocalLinear), DefineTable(state, ReciprocalHigherOrder)}; + } + const auto bits = Unary(state, spv::OpBitcast, u32, value); + const auto exponent = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, bits, 23u), 0xffu); + const auto mantissa = op(spv::OpBitwiseAnd, bits, 0x7fffffu); + const auto segment = op(spv::OpShiftRightLogical, mantissa, 18u); + const auto fraction = op(spv::OpBitwiseAnd, mantissa, 0x3ffffu); + const auto constant = LoadTable(state, state.reciprocalTables[0], segment); + const auto linearFactor = LoadTable(state, state.reciprocalTables[1], segment); + const auto higherOrder = LoadTable(state, state.reciprocalTables[2], segment); + + const auto linearProduct = MultiplyExtended(state, linearFactor, fraction); + const auto wide = test(spv::OpUGreaterThanEqual, linearProduct[1], ConstantU32(state, 64u)); + const auto shift = pick(wide, 15u, 14u); + const auto quotient = Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpShiftLeftLogical, u32, linearProduct[1], sub(ConstantU32(state, 32u), shift)), Binary(state, spv::OpShiftRightLogical, u32, linearProduct[0], shift)); + const auto midpoint = pick(wide, 0x4000u, 0x2000u); + const auto remainder = Binary(state, spv::OpBitwiseAnd, u32, linearProduct[0], pick(wide, 0x7fffu, 0x3fffu)); + const auto odd = test(spv::OpINotEqual, op(spv::OpBitwiseAnd, quotient, 1u), ConstantU32(state, 0u)); + const auto roundUp = Binary(state, spv::OpLogicalOr, boolean, test(spv::OpUGreaterThan, remainder, midpoint), Binary(state, spv::OpLogicalAnd, boolean, test(spv::OpIEqual, remainder, midpoint), odd)); + const auto rounded = add(quotient, pick(roundUp, 1u, 0u)); + const auto linear = Binary(state, spv::OpShiftLeftLogical, u32, rounded, pick(wide, 1u, 0u)); + + const auto half = op(spv::OpShiftRightLogical, fraction, 1u); + const auto quadraticProduct = MultiplyExtended(state, op(spv::OpBitwiseAnd, higherOrder, 0xffffu), half); + const auto shiftedHigh = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, quadraticProduct[1], 18u), op(spv::OpShiftRightLogical, quadraticProduct[0], 14u)); + const auto shiftedLow = op(spv::OpShiftLeftLogical, quadraticProduct[0], 18u); + const auto cubicProduct = MultiplyExtended(state, Binary(state, spv::OpIMul, u32, op(spv::OpShiftRightLogical, higherOrder, 16u), fraction), half); + const auto innerLow = sub(shiftedLow, cubicProduct[0]); + const auto innerHigh = sub(sub(shiftedHigh, cubicProduct[1]), pick(test(spv::OpULessThan, shiftedLow, cubicProduct[0]), 1u, 0u)); + const auto upper = MultiplyExtended(state, innerHigh, half); + const auto lower = MultiplyExtended(state, innerLow, half); + const auto upperPart = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, upper[1], 15u), op(spv::OpShiftRightLogical, upper[0], 17u)); + const auto carryPart = op(spv::OpShiftRightLogical, add(op(spv::OpBitwiseAnd, upper[0], 0x1ffffu), lower[1]), 17u); + const auto quadratic = add(upperPart, carryPart); + + const auto sum = add(sub(op(spv::OpShiftLeftLogical, constant, 4u), linear), op(spv::OpIAdd, op(spv::OpShiftLeftLogical, quadratic, 1u), 32u)); + const auto dropped = op(spv::OpBitwiseAnd, sum, 63u); + const auto nearCut = Binary(state, spv::OpLogicalOr, boolean, test(spv::OpULessThan, dropped, ConstantU32(state, 2u)), test(spv::OpUGreaterThanEqual, dropped, ConstantU32(state, 62u))); + const auto correction = EmitValueOrZeroIfCondition(state, nearCut, [&] { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u32, result, state.reciprocalCorrectionFunction, mantissa); + return result; + }); + const auto normalized = add(op(spv::OpIAdd, op(spv::OpShiftRightLogical, sum, 6u), 0x3e800000u), correction); + const auto resultExponent = sub(op(spv::OpIAdd, op(spv::OpShiftRightLogical, normalized, 23u), 127u), exponent); + const auto exactBits = Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, bits, 0x80000000u), op(spv::OpShiftLeftLogical, resultExponent, 23u)), op(spv::OpBitwiseAnd, normalized, 0x7fffffu)); + const auto normalInput = Binary(state, spv::OpLogicalAnd, boolean, test(spv::OpINotEqual, exponent, ConstantU32(state, 0u)), test(spv::OpINotEqual, exponent, ConstantU32(state, 0xffu))); + const auto normalResult = Binary(state, spv::OpLogicalAnd, boolean, test(spv::OpSGreaterThan, resultExponent, ConstantU32(state, 0u)), test(spv::OpSLessThan, resultExponent, ConstantU32(state, 0xffu))); + const auto approximate = DeviceReciprocal(state, value); + return Select(state, f32, Binary(state, spv::OpLogicalAnd, boolean, normalInput, normalResult), Unary(state, spv::OpBitcast, f32, exactBits), approximate); +} + +} + +void DefineReciprocalCorrectionFunction(SpirvEmitterState& state) { + bool used = false; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) used = used || inst->Opcode() == IrOpcode::FPRecip32 || inst->Opcode() == IrOpcode::FPRecipIFlag32; + } + if (!used || !ExactReciprocal()) return; + const auto u32 = TypeU32(state); + state.reciprocalCorrectionFunction = state.module.AllocateId(); + state.module.AddName(state.reciprocalCorrectionFunction, "rcp_f32_correction"); + state.module.AddFunction(spv::OpFunction, u32, state.reciprocalCorrectionFunction, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u32, u32)); + const auto mantissa = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, mantissa); + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, ReciprocalCorrection(state, mantissa)); + state.module.AddFunction(spv::OpFunctionEnd); +} + +std::uint32_t EmitFPRecip32(SpirvEmitterState& state, std::uint32_t arg0) { + if (!ExactReciprocal()) return DeviceReciprocal(state, arg0); + if (state.reciprocalCorrectionFunction == 0u) FailEmit("v_rcp_f32 correction function was not defined before function emission"); + return HardwareReciprocal(state, arg0); +} + +std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { + if (!ExactReciprocal()) return DeviceReciprocal(state, arg0); + if (state.reciprocalCorrectionFunction == 0u) FailEmit("v_rcp_iflag_f32 correction function was not defined before function emission"); + return HardwareReciprocal(state, arg0); +} + +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index ed2edb7466..2c9ebf55bd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1534,6 +1534,7 @@ void EmitProgram(SpirvEmitterState& state) { } DefineGetBdaPointer(state); DefineDot2F32F16Function(state); + DefineReciprocalCorrectionFunction(state); for (const IrBlock* block : program.BlockOrder()) { const bool needsScratch = std::any_of(block->Instructions().begin(), block->Instructions().end(), [](const IrValue* inst) { return inst->Opcode() == IrOpcode::SwizzleU32; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d14be69b73..9df29ac79b 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -28,6 +28,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_BDA_BYTE_READS` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp` | | forces one address lookup and byte load per byte for BDA reads instead of a wide dword load | | `APS5_BDA_DWORD_STORES` | presence | `core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp` | | reverts BDA writes to one address store per dword instead of a wide multi-dword store | | `APS5_DUMP_IR` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code address (or all) of the programs whose IR is printed after each front-end pass | +| `APS5_EXACT_RCP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp` | | emits `v_rcp_f32` and `v_rcp_iflag_f32` as the RDNA hardware reciprocal (bit-exact for normal inputs and results) instead of the driver's division; adds about 13k SPIR-V words to each shader that uses them | | `APS5_GATHER_CONST` | integer | `core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp` | | integer every non-depth gather returns in all four texels instead of the sampled value | | `APS5_IMAGE_TABLE_KEY_SCAN` | integer | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | 256 | number of image-table entries the key scan examines | | `APS5_IMAGE_TABLE_STRICT` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | sets image-table materialization to its strict form; 0 leaves it off | diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1f438c1157..0dc4b1b688 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -124,6 +124,7 @@ - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler From 86d9e49f2ee344e3a92b22176b721364413d0430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 22:50:13 +0200 Subject: [PATCH 1352/1360] docs(shader): note APS5_EXACT_RCP reaches v_rcp_f16 and position W Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 0dc4b1b688..89f88ad5f4 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -124,7 +124,7 @@ - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. -- [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. +- [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. The switch also reaches v_rcp_f16 (rounded from the f32 reciprocal) and the pixel shader's position W input. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler From c9147cbeecf926ef1f27f01d08f5dda8e69973b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Fri, 23 Oct 2026 22:57:38 +0200 Subject: [PATCH 1353/1360] test(agc): register only the APS5_EXACT_RCP entry of the rcp hardware test Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e2f14d2d93..ddcfb73d18 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2128,6 +2128,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + set_target_properties(agc_driver_rcp_hardware_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests) set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) @@ -2427,7 +2428,7 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_locks_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests" OR agcTest STREQUAL "agc_driver_rcp_hardware_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") From 378ce8f9b526287f5962e8dbaa30695161a4d7c4 Mon Sep 17 00:00:00 2001 From: Akul Tyagi <69496404+AkulTyagi11@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:27:42 +0530 Subject: [PATCH 1354/1360] test(libSceSaveData.native): cover transferring and v1 mount lifecycle --- core/libs/tests/GuestSaveDataPs4.cpp | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 6ee6868bd4..d795b28195 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -8,7 +8,21 @@ #include <stdexcept> #include <string> +struct SaveDataMountPs4 { + std::int32_t user_id; + std::int32_t pad; + const void* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; +}; + +extern "C" int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); +extern "C" int APS5_VABI sceSaveDataMount(const SaveDataMountPs4*, SaveDataMountResult*); +extern "C" int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataInitialize(const void*); @@ -21,6 +35,9 @@ namespace { constexpr int SaveDataErrorNotFound = -2137063416; constexpr int SaveDataErrorNotInitialized = -2137063423; +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotMounted = -2137063420; +constexpr std::uint32_t MountReadWrite = 2; int failures = 0; @@ -53,6 +70,11 @@ void TestTransferringMountPs4() { std::memset(&result, 0xAA, sizeof(result)); Check(sceSaveDataTransferringMountPs4(&mount, &result) == SaveDataErrorNotFound, "TransferringMountPs4 returns NOT_FOUND"); Check(result.mount_point.data[0] == '\0', "TransferringMountPs4 reports no mount point"); + std::memset(&result, 0xAA, sizeof(result)); + Check(sceSaveDataTransferringMount(&mount, &result) == SaveDataErrorNotFound, "TransferringMount returns NOT_FOUND"); + Check(result.mount_point.data[0] == '\0', "TransferringMount reports no mount point"); + Check(sceSaveDataTransferringMount(nullptr, nullptr) == SaveDataErrorNotFound, "TransferringMount handles null arguments"); + Check(sceSaveDataTransferringMountPs4(nullptr, nullptr) == SaveDataErrorNotFound, "TransferringMountPs4 handles null arguments"); } void TestDirNameSearchPs4() { @@ -96,6 +118,23 @@ void TestSaveDataMemoryV1() { Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); } +void TestMountPs4() { + SaveDataMountResult result{}; + Check(sceSaveDataMount(nullptr, &result) == SaveDataErrorParameter, "null mount is PARAMETER"); + SaveDataMountPs4 mount{}; + Check(sceSaveDataMount(&mount, &result) == SaveDataErrorParameter, "null dir_name is PARAMETER"); + Check(sceSaveDataUmount(nullptr) == SaveDataErrorParameter, "umount null point is PARAMETER"); + SceSaveDataDirName dir{}; + std::strcpy(dir.data, "kept"); + mount.dir_name = &dir; + mount.mount_mode = MountReadWrite; + Check(ThrowsRuntimeError([&] { sceSaveDataMount(&mount, nullptr); }), "null mount_result throws"); + Check(sceSaveDataMount(&mount, &result) == 0, "mount existing save directory succeeds"); + Check(result.mount_point.data[0] != '\0', "mount writes mount point"); + Check(sceSaveDataUmount(&result.mount_point) == 0, "umount mounted point succeeds"); + Check(sceSaveDataUmount(&result.mount_point) == SaveDataErrorNotMounted, "umount unmounted point is NOT_MOUNTED"); +} + } int main() { @@ -107,6 +146,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); TestSaveDataMemoryV1(); + TestMountPs4(); std::filesystem::current_path(previous); std::error_code error; From 3ddaa6470770b89c4fd3b7c65eacd8775c2eaa65 Mon Sep 17 00:00:00 2001 From: Max <mgiess@live.de> Date: Mon, 19 Oct 2026 03:05:09 +0200 Subject: [PATCH 1355/1360] fix(libkernel): return EINVAL for the semaphore arguments the console rejects The prosper console oracle measured SCE_KERNEL_ERROR_EINVAL for creating a semaphore with an initial count above the maximum, a maximum of 0, a negative initial count or a null name, for polling 0 or more than the maximum, and for signalling 0. These threw, or polled past the maximum returned EBUSY, and signalling 0 succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 12 +++++++--- core/libs/tests/GuestKernelErrors.cpp | 4 ++-- core/libs/tests/GuestSemaphore.cpp | 24 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index eab1d9b52d..a1b4e1bd60 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -21,9 +21,12 @@ extern "C" { int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt) { (void)opt; - if (sem == nullptr || name == nullptr || attr > 2 || init < 0 || max <= 0 || init > max) { + if (sem == nullptr || attr > 2 || max < 0) { APS5_INVALID_ARG_EX; } + if (name == nullptr || init < 0 || max == 0 || init > max) { + return SCE_KERNEL_ERROR_EINVAL; + } *sem = new KernelSemaPrivate(init, max, std::string(name), attr == 1); if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast<void*>(*sem), name, attr, init, max); @@ -34,8 +37,8 @@ int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { if (sem == nullptr) { return SCE_KERNEL_ERROR_ESRCH; } - if (need <= 0) { - APS5_INVALID_ARG_EX; + if (need <= 0 || need > sem->maxCount) { + return SCE_KERNEL_ERROR_EINVAL; } std::lock_guard<std::mutex> lock(sem->mutex); @@ -53,6 +56,9 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { if (count < 0) { APS5_INVALID_ARG_EX; } + if (count == 0) { + return SCE_KERNEL_ERROR_EINVAL; + } std::lock_guard<std::mutex> lock(sem->mutex); if (count > sem->maxCount - sem->tokenCount) { diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 8989624672..66b6210e17 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -95,9 +95,9 @@ int main() { Require(sceKernelCreateSema(&semaphore, "limit", 1, 1, 3, nullptr) == SCE_OK); Require(sceKernelSignalSema(semaphore, 3) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelSignalSema(semaphore, 2) == SCE_OK); - Require(sceKernelSignalSema(semaphore, 0) == SCE_OK); + Require(sceKernelSignalSema(semaphore, 0) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollSema(semaphore, 3) == SCE_OK); - Require(sceKernelSignalSema(semaphore, 0) == SCE_OK); + Require(sceKernelSignalSema(semaphore, 0) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollSema(semaphore, 1) == SCE_KERNEL_ERROR_EBUSY); Require(sceKernelDeleteSema(semaphore) == SCE_OK); diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index 0ba58d4520..4ee50ec2ea 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -36,6 +36,28 @@ static void* APS5_VABI Wait(void* arg) { return nullptr; } +static void RequireInvalidArguments() { + KernelSema rejected = nullptr; + Require(sceKernelCreateSema(&rejected, "rejected", 0, 5, 3, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateSema(&rejected, "rejected", 0, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateSema(&rejected, "rejected", 0, -1, 3, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateSema(&rejected, nullptr, 0, 0, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(rejected == nullptr); + + KernelSema sem = nullptr; + Require(sceKernelCreateSema(&sem, "limits", 0, 2, 3, nullptr) == SCE_OK); + Require(sceKernelPollSema(sem, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollSema(sem, -1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollSema(sem, 4) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollSema(sem, 100) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollSema(sem, 3) == SCE_KERNEL_ERROR_EBUSY); + Require(sceKernelSignalSema(sem, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollSema(sem, 2) == SCE_OK); + Require(sceKernelSignalSema(sem, 3) == SCE_OK); + Require(sceKernelPollSema(sem, 3) == SCE_OK); + Require(sceKernelDeleteSema(sem) == SCE_OK); +} + int main() { KernelUseconds nullTimeout = 1000; int nullWaiters = -1; @@ -94,4 +116,6 @@ int main() { Require(sceKernelSignalSema(sem, 1) == SCE_OK); Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_OK); Require(sceKernelDeleteSema(sem) == SCE_OK); + + RequireInvalidArguments(); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 80453ebf50..ec49912b08 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -58,6 +58,7 @@ - [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. +- [sceKernelCreateSema, sceKernelPollSema, sceKernelSignalSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - a poll, a signal or a second delete of a deleted semaphore reads freed memory here, where the console returns `0x80020003` (ESRCH) ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L17-L19)). A null output pointer to `sceKernelCreateSema`, an attribute above 2, a negative maximum and a negative signal count throw, as they were not measured. `SCE_KERNEL_ERROR_EINVAL` for an initial count above the maximum or negative, a maximum of 0, a null name, a poll of 0 or above the maximum and a signal of 0 is [measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L10-L23). - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded From 39d4e73e2636336896dd4e64c4d97856e178c7f6 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Fri, 23 Oct 2026 20:34:40 -0300 Subject: [PATCH 1356/1360] test(libc): stop pinning the unmeasured reallocalign zero-size realignment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestReallocalign.cpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index cd36081ec0..f94576018d 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -53,22 +53,5 @@ int main() { void* empty = reallocalign_nid_postfix(nullptr, 0, 0); Require(empty != nullptr); free_nid_postfix(empty); - for (std::size_t attempt = 0; attempt < 64; ++attempt) { - void* loose = reallocalign_nid_postfix(nullptr, 40, 32); - Require(loose != nullptr); - if (reinterpret_cast<std::uintptr_t>(loose) % 4096 == 0) { - free_nid_postfix(loose); - continue; - } - bool threw = false; - try { - reallocalign_nid_postfix(loose, 0, 4096); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw); - free_nid_postfix(loose); - return 0; - } - throw std::runtime_error("reallocalign check could not get a block without 4096-byte alignment"); + return 0; } From 82b151597a50cd768b21182936d6a9b110b98b8f Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Sat, 24 Oct 2026 13:15:57 +1300 Subject: [PATCH 1357/1360] docs: point game status links to the Game bring-up discussions The README, the Game bring-up discussion form and the converter page now link to the Game bring-up discussions for per-game status and test reports. --- .github/DISCUSSION_TEMPLATE/game-bring-up.yml | 2 +- README.md | 2 +- demo/web/index.html | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/game-bring-up.yml b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml index afb5f3d3d3..ff08c3cca6 100644 --- a/.github/DISCUSSION_TEMPLATE/game-bring-up.yml +++ b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml @@ -6,7 +6,7 @@ body: One thread per title, for every platform: search this category for the title ID first. If a thread exists, reply there, also when your OS or GPU differs from the one it reports. Name the discussion `Game name (title ID)`, for example `DOOM Shareware (PPSA99666)`, without the platform. Bug reports and implementation work belong in issues and pull requests; link them here as the blocker. - Per-game progress is also tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), built from public GitHub data. Only share material you have the right to distribute. + Only share material you have the right to distribute. - type: checkboxes id: searched attributes: diff --git a/README.md b/README.md index 3e7fc3ccf4..1a3a9ac30e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully prod ## Compatibility -Per-game progress (stage reached, open blockers, libraries and GPU coverage) is tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), a community project built from public GitHub data ([source](https://github.com/AlxcBTW/anyps5-roadmap)). Test reports go in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title. +Per-game status and test reports live in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title. ## Input mapping diff --git a/demo/web/index.html b/demo/web/index.html index e0d468febc..5d974cd58f 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -6,7 +6,7 @@ <title>AnyPS5 · Converter -
+

LOCAL CONVERSION

PS5 apps to Windows/Linux converter

@@ -23,7 +23,7 @@

Memory downloads have a 256 MiB budget, checked against expanded files and output overhead. For multiple ZIPs, the limit applies to the combined bundle; larger batches require Save ZIP to disk in desktop Chrome or Edge. Executable and modules are limited to 256 MiB per app; the icon is also limited to 16 MiB and counts toward the Windows conversion limit. Use the native relinker for larger conversion inputs.

-

Conversion does not guarantee that an app runs. Check the community compatibility list for known blockers and reports.

+

Conversion does not guarantee that an app runs. Check the Game bring-up discussions for known blockers and reports.

From dd6220698a3e2fa3b6abb4bc212d89dba599e381 Mon Sep 17 00:00:00 2001 From: Dom3005 Date: Wed, 21 Oct 2026 18:38:51 +0200 Subject: [PATCH 1358/1360] fix(libkernel): change O_APPEND on a Windows file through fcntl fcntl(F_SETFL) threw when it changed O_APPEND on a Windows file. The guest flag already chooses WriteAtEnd, but a descriptor opened with O_APPEND also carries the CRT's append flag, so _write kept appending after the guest cleared it. F_SETFL now replaces the CRT descriptor of every descriptor sharing the open file's status with a duplicate handle opened with or without _O_APPEND, as libc fdopen does for append mode. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/File/src/DescriptorControl.cpp | 29 ++++++++++++++++++- core/libs/tests/GuestKernelFcntl.cpp | 22 +++++++++++--- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index f832da7ae0..8f7dd2c448 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -22,8 +22,10 @@ #include #ifdef _WIN32 +#include #include #include +#include "prx/libc/include/General.hpp" #include "prx/libc/include/WindowsDescriptor.hpp" #else #include @@ -180,8 +182,33 @@ Entry InitialEntry(int fd) { return {false, std::make_shared(status)}; } +void SetHostAppend(int fd, bool append, const char* function) { + const int translation = ::_setmode(fd, _O_BINARY); + if (translation == -1 || ::_setmode(fd, translation) == -1) + throw std::runtime_error(std::string(function) + ": cannot read the translation mode of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + HANDLE copy = nullptr; + if (!::DuplicateHandle(::GetCurrentProcess(), WindowsDescriptorHandle_nid_no_patch(fd), ::GetCurrentProcess(), ©, 0, TRUE, DUPLICATE_SAME_ACCESS)) + throw std::runtime_error(std::string(function) + ": cannot duplicate the handle of descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); + const int replacement = ::_open_osfhandle(reinterpret_cast(copy), (append ? _O_APPEND : 0) | translation); + if (replacement < 0) { + ::CloseHandle(copy); + throw std::runtime_error(std::string(function) + ": cannot open a replacement for descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + } + auto* const standard = HostStandardHandle_nid_no_patch(fd); + const int moved = ::_dup2(replacement, fd); + const int error = errno; + ::_close(replacement); + KeepStandardHandle_nid_no_patch(fd, standard); + if (moved != 0) + throw std::runtime_error(std::string(function) + ": cannot replace descriptor " + std::to_string(fd) + ", errno=" + std::to_string(error)); +} + void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* function) { - if ((changed & Append) && kind == Kind::File) Unsupported(function, "changing O_APPEND", kind, fd); + if ((changed & Append) && kind == Kind::File) { + const auto status = g_entries.at(fd).status; + for (const auto& [descriptor, entry] : g_entries) + if (entry.status == status && DescriptorIsOpen_nid_no_patch(descriptor)) SetHostAppend(descriptor, (next & Append) != 0, function); + } if ((changed & Nonblocking) && kind == Kind::Device) Unsupported(function, "changing O_NONBLOCK", kind, fd); if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 3220eaa25c..63cae9c110 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -275,12 +275,26 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append)); Require(fcntl_nid_postfix(appending, SetFlags, Append | NonBlocking) == 0); Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append | NonBlocking)); -#ifdef _WIN32 - Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, 0); }, "fcntl: changing O_APPEND on a file is not implemented")); -#else + Require(write_nid_postfix(appending, "ab", 2) == 2); Require(fcntl_nid_postfix(appending, SetFlags, 0) == 0); Require(fcntl_nid_postfix(appending, GetFlags) == WriteOnly); -#endif + Require(sceKernelLseek(appending, 0, FromStart) == 0); + Require(write_nid_postfix(appending, "X", 1) == 1); + const int sharing = fcntl_nid_postfix(appending, DuplicateCommand, 0); + Require(sharing >= 0); + Require(fcntl_nid_postfix(sharing, SetFlags, Append) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append)); + Require(sceKernelLseek(appending, 0, FromStart) == 0); + Require(write_nid_postfix(appending, "c", 1) == 1); + Require(fcntl_nid_postfix(sharing, SetFlags, 0) == 0); + Require(sceKernelLseek(appending, 0, FromStart) == 0); + Require(write_nid_postfix(appending, "Y", 1) == 1); + Require(close_nid_postfix(sharing) == 0); + const int reading = sceKernelOpen("kernel_fcntl_probe.bin", ReadOnly, 0); + Require(reading >= 0); + char written[16]{}; + Require(sceKernelRead(reading, written, sizeof(written)) == 8 && std::memcmp(written, "Ybcdeabc", 8) == 0); + Require(sceKernelClose(reading) == 0); Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, Append | 0x80); }, "fcntl: changing O_FSYNC on a file is not implemented")); Require(sceKernelClose(appending) == 0); std::remove("kernel_fcntl_probe.bin"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 67bf4903a9..de2b407332 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -230,7 +230,7 @@ - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [Creating through a trailing slash](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `open` and `sceKernelOpen` with `O_CREAT` on a missing name followed by a slash (`new/`) fail with `EINVAL` on Windows and with the host's `EISDIR` on Linux, where FreeBSD 11.4's `lookup()` (sys/kern/vfs_lookup.c) fails with `ENOENT` because only a directory may be created through a trailing slash -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) do not pass a `sceNetSocket` socket to libSceNet as `fcntl` does. On Linux its number duplicates the host socket, so `FIONBIO` switches the host `O_NONBLOCK` but not the library's `SO_NBIO` state; on Windows it is a `NUL` placeholder, so `FIONREAD` and enabling `FIONBIO` throw as on a character device. `sceNetIoctl` handles both requests on such a socket. From 69cdebdbb55aeaa0312cce8fe2198087691c64f0 Mon Sep 17 00:00:00 2001 From: Dom3005 Date: Wed, 21 Oct 2026 23:55:51 +0200 Subject: [PATCH 1359/1360] test(libkernel): remove a stale fcntl probe file before the flags test Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestKernelFcntl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 63cae9c110..164a289714 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -199,6 +199,7 @@ static void SocketsKeepTheirPath() { } static void FileFlagsRoundTrip() { + std::remove("kernel_fcntl_probe.bin"); const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644); Require(file >= 0); Require(sceKernelFcntl(file, GetFlags) == ReadWrite); From 371652fd0e87047b399cccd12e0a2299ae3186dd Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 22:25:04 +0000 Subject: [PATCH 1360/1360] fix(agc): draw a PS_ITER_SAMPLE program once per pixel into a one-sample color target Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 3 ++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 +++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 56436ff48f..e5fad3bdb1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1492,9 +1492,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { color.uintExport = slotExport == 7; if (color.samples == 1 && result.samples > 1 && result.colors.empty() && !result.depth && (read(cx, 0x292) & 1u) == 0) { const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage)); - Require(!result.perSampleShading && sampleInputs == 0 && (read(cx, 0x203) & 0x100u) == 0, "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with PS_ITER_SAMPLE, a sample index or coverage input, or a sample-mask export"); + Require(sampleInputs == 0 && (read(cx, 0x203) & 0x100u) == 0, "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with a sample index or coverage input, or a sample-mask export"); result.samples = 1; result.customSampleLocations = false; + result.perSampleShading = false; } if (color.samples > 1 && result.samples == 1 && result.colors.empty() && !result.depth && !result.alphaToCoverage && (read(cx, 0x292) & 1u) == 0) { Require(result.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || (read(cx, 0x1b3) & 0x44u) == 0, "conservative rasterization with centroid interpolation of a draw raised to a multisampled target is unsupported"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index abdb669923..00390f67fb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -824,7 +824,7 @@ void msaaEnableClearOneSampleTargetTests() { queue = drawC(); queue.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); requireOneSample(queue, "draw C with an enabled but unaddressed ANCILLARY input"); - constexpr std::string_view refused = "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with PS_ITER_SAMPLE, a sample index or coverage input, or a sample-mask export"; + constexpr std::string_view refused = "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with a sample index or coverage input, or a sample-mask export"; for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { queue = drawC(); queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input); @@ -832,6 +832,10 @@ void msaaEnableClearOneSampleTargetTests() { } queue = drawC(); queue.context[0x293] = 0x10000u; + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample); + requireOneSample(queue, "draw C with PS_ITER_SAMPLE and a sample-interpolated input"); + Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample color target without MSAA_ENABLE asked for sample shading"); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); expectFailure([&] { DecodeState(queue); }, refused); queue = drawC(); queue.context[0x1c4] = 9; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7a8f6ad52d..9e87ef8f93 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -37,7 +37,7 @@ - [Point geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` emits mesh points with `OutputPoints` and scalar `PrimitivePointIndicesEXT` as required by [SPV_EXT_mesh_shader](https://github.khronos.org/SPIRV-Registry/extensions/EXT/SPV_EXT_mesh_shader.html). Each packed primitive export contributes its first 9-bit vertex index and its null bit, using the same NGG primitive packing as line and triangle outputs. The subgroup reserves at least `GE_MAX_OUTPUT_PER_SUBGROUP` point slots, as the line path does; the console budget is not measured. Points use the register size or the misc-X point-size export and clamp interpreted by the vertex-point path; those register, clamp and rasterization inferences remain unmeasured on the console. Point sprites remain unsupported. Point output from a vertex workgroup still throws. Cocoon (PPSA08766) reaches this path with triangle-strip input. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. -- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA.INTERPOLATE_COMP_Z` is accepted: with every sample at the pixel center, Z interpolated to any sample position is the center's Z, which the one-sample draw tests. That rests on the same inference; neither [PAL](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MsaaState.cpp#L117-L118) nor [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/761d5c8a54f93b3d43608f718910ed742983dd0b/src/gallium/drivers/radeonsi/si_state.c#L3314-L3316) sets the bit, and its meaning is not documented. Line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `INTERPOLATE_SRC_Z` and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) still throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. Gran Turismo 7 (PPSA01317) draws its main pass with a one-sample depth target, `DB_EQAA` 0x00050000 and a 4x configuration. +- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. A pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA.INTERPOLATE_COMP_Z` is accepted: with every sample at the pixel center, Z interpolated to any sample position is the center's Z, which the one-sample draw tests. That rests on the same inference; neither [PAL](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MsaaState.cpp#L117-L118) nor [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/761d5c8a54f93b3d43608f718910ed742983dd0b/src/gallium/drivers/radeonsi/si_state.c#L3314-L3316) sets the bit, and its meaning is not documented. Line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `INTERPOLATE_SRC_Z` and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) still throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. Gran Turismo 7 (PPSA01317) draws its main pass with a one-sample depth target, `DB_EQAA` 0x00050000 and a 4x configuration. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw - [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - not measured on hardware: `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear puts every color, depth and stencil sample at the pixel center, as in GL without multisampling ([ARB_multisample](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089/extensions/ARB/ARB_multisample.txt#L185), [RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11)). Lines cover whole pixels by the host's line rule, not the console's aliased one ([D3D11.3](https://github.com/microsoft/DirectX-Specs/blob/f37b0d6033a1/d3d/archive/D3D11_3_FunctionalSpec.htm#L13705)). Depth sample tests and occlusion counts are unknown. Rasterized points, alpha-to-coverage, and Z interpolation, overrasterization or a sample-mask export with depth throw. PPSA06888 draws a 2x target, PPSA18216 a 4x depth and color pair and 4x lines. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw